const std = @import("std");
const mem = std.mem;
const assert = std.debug.assert;
const CallingConvention = std.builtin.CallingConvention;

const aro = @import("aro");
const CToken = aro.Tokenizer.Token;
const Tree = aro.Tree;
const Node = Tree.Node;
const TokenIndex = Tree.TokenIndex;
const QualType = aro.QualType;

const ast = @import("ast.zig");
const ZigNode = ast.Node;
const ZigTag = ZigNode.Tag;
const builtins = @import("builtins.zig");
const helpers = @import("helpers.zig");
const MacroTranslator = @import("MacroTranslator.zig");
const PatternList = @import("PatternList.zig");
const Scope = @import("Scope.zig");

const AnonymousRecordFieldNames = struct {
    pub const Key = struct {
        parent: QualType,
        field: QualType,
    };

    pub const Context = struct {
        pub fn hash(ctx: Context, key: Key) u64 {
            const auto_hash = std.hash_map.getAutoHashFn(Key, Context);
            return auto_hash(ctx, .{
                .parent = key.parent.unqualified(),
                .field = key.field.unqualified(),
            });
        }

        pub fn eql(ctx: Context, a: Key, b: Key) bool {
            const auto_eql = std.hash_map.getAutoEqlFn(Key, Context);
            return auto_eql(ctx, .{
                .parent = a.parent.unqualified(),
                .field = a.field.unqualified(),
            }, .{
                .parent = b.parent.unqualified(),
                .field = b.field.unqualified(),
            });
        }
    };
};

pub const QualTypeHashContext = struct {
    pub fn hash(ctx: QualTypeHashContext, key: QualType) u64 {
        const auto_hash = std.hash_map.getAutoHashFn(QualType, QualTypeHashContext);
        return auto_hash(ctx, key.unqualified());
    }

    pub fn eql(ctx: QualTypeHashContext, a: QualType, b: QualType) bool {
        const auto_eql = std.hash_map.getAutoEqlFn(QualType, QualTypeHashContext);
        return auto_eql(ctx, a.unqualified(), b.unqualified());
    }
};

pub const Error = std.mem.Allocator.Error;
pub const MacroProcessingError = Error || error{UnexpectedMacroToken};
pub const TypeError = Error || error{UnsupportedType};
pub const TransError = TypeError || error{ UnsupportedTranslation, SelfReferential };

/// Control when to treat a trailing array as a flexible array member.
/// Mirrors the -fstrict-flex-arrays=<n> compiler flag.
pub const StrictFlexArraysLevel = enum {
    /// Any trailing array member is a flexible array.
    @"0",
    /// Trailing arrays of size 0, 1, or undefined are flexible.
    @"1",
    /// Trailing arrays of size 0 or undefined are flexible (default).
    @"2",
    /// Only trailing arrays of undefined size are flexible.
    @"3",
};

const Translator = @This();

/// The C AST to be translated.
tree: *const Tree,
/// The compilation corresponding to the AST.
comp: *aro.Compilation,
/// The Preprocessor that produced the source for `tree`.
pp: *const aro.Preprocessor,

/// Should static functions be translated as `pub`.
pub_static: bool,
/// Should function bodies be translated.
func_bodies: bool,
/// Should macro names of literals be preserved.
keep_macro_literals: bool,
/// Should struct fields be default initialized.
default_init: bool,
/// Control when to treat a trailing array as a flexible array member.
strict_flex_arrays: StrictFlexArraysLevel,

gpa: mem.Allocator,
arena: mem.Allocator,

alias_list: Scope.AliasList,
global_scope: *Scope.Root,
/// Running number used for creating new unique identifiers.
mangle_count: u32 = 0,

/// Table of declarations for enum, struct, union and typedef types.
type_decls: std.AutoArrayHashMapUnmanaged(Node.Index, []const u8) = .empty,
/// Table of record decls that have been demoted to opaques.
opaque_demotes: std.HashMapUnmanaged(QualType, void, QualTypeHashContext, std.hash_map.default_max_load_percentage) = .empty,
/// Table of unnamed enums and records that are child types of typedefs.
unnamed_typedefs: std.HashMapUnmanaged(QualType, []const u8, QualTypeHashContext, std.hash_map.default_max_load_percentage) = .empty,
/// Table of anonymous record to generated field names.
anonymous_record_field_names: std.HashMapUnmanaged(
    AnonymousRecordFieldNames.Key,
    []const u8,
    AnonymousRecordFieldNames.Context,
    std.hash_map.default_max_load_percentage,
) = .empty,

/// This one is different than the root scope's name table. This contains
/// a list of names that we found by visiting all the top level decls without
/// translating them. The other maps are updated as we translate; this one is updated
/// up front in a pre-processing step.
global_names: std.StringArrayHashMapUnmanaged(void) = .empty,

/// This is similar to `global_names`, but contains names which we would
/// *like* to use, but do not strictly *have* to if they are unavailable.
/// These are relevant to types, which ideally we would name like
/// 'struct_foo' with an alias 'foo', but if either of those names is taken,
/// may be mangled.
/// This is distinct from `global_names` so we can detect at a type
/// declaration whether or not the name is available.
weak_global_names: std.StringArrayHashMapUnmanaged(void) = .empty,

/// Set of identifiers known to refer to typedef declarations.
/// Used when parsing macros.
typedefs: std.StringArrayHashMapUnmanaged(void) = .empty,

/// The lhs lval of a compound assignment expression.
compound_assign_dummy: ?ZigNode = null,

/// Set of variables whose initializers are currently being translated.
/// Used to detect self-referential initializers.
wip_var_inits: std.AutoHashMapUnmanaged(Node.Index, void) = .empty,

pub fn getMangle(t: *Translator) u32 {
    t.mangle_count += 1;
    return t.mangle_count;
}

/// Convert an `aro.Source.Location` to a 'file:line:column' string.
pub fn locStr(t: *Translator, loc: aro.Source.Location) ![]const u8 {
    const expanded = loc.expand(t.comp);
    const filename = expanded.path;

    const line = expanded.line_no;
    const col = expanded.col;

    return std.fmt.allocPrint(t.arena, "{s}:{d}:{d}", .{ filename, line, col });
}

fn maybeSuppressResult(t: *Translator, used: ResultUsed, result: ZigNode) TransError!ZigNode {
    if (used == .used) return result;
    return ZigTag.discard.create(t.arena, .{ .should_skip = false, .value = result });
}

pub fn addTopLevelDecl(t: *Translator, name: []const u8, decl_node: ZigNode) !void {
    const gop = try t.global_scope.sym_table.getOrPut(t.gpa, name);
    if (gop.found_existing) return; // Any duplicate decls are equivalent
    gop.value_ptr.* = decl_node;
    try t.global_scope.nodes.append(t.gpa, decl_node);
}

fn fail(
    t: *Translator,
    err: anytype,
    source_loc: TokenIndex,
    comptime format: []const u8,
    args: anytype,
) (@TypeOf(err) || error{OutOfMemory}) {
    try t.warn(&t.global_scope.base, source_loc, format, args);
    return err;
}

pub fn failDecl(
    t: *Translator,
    scope: *Scope,
    tok_idx: TokenIndex,
    name: []const u8,
    comptime format: []const u8,
    args: anytype,
) Error!void {
    const loc = t.tree.tokens.items(.loc)[tok_idx];
    return t.failDeclExtra(scope, loc, name, format, args);
}

pub fn failDeclExtra(
    t: *Translator,
    scope: *Scope,
    loc: aro.Source.Location,
    name: []const u8,
    comptime format: []const u8,
    args: anytype,
) Error!void {
    // location
    // pub const name = @compileError(msg);
    const fail_msg = try std.fmt.allocPrint(t.arena, format, args);
    const fail_decl = try ZigTag.fail_decl.create(t.arena, .{
        .actual = name,
        .mangled = fail_msg,
        .local = scope.id != .root,
    });

    const str = try t.locStr(loc);
    const location_comment = try std.fmt.allocPrint(t.arena, "// {s}", .{str});
    const loc_node = try ZigTag.warning.create(t.arena, location_comment);

    if (scope.id == .root) {
        try t.addTopLevelDecl(name, fail_decl);
        try scope.appendNode(loc_node);
    } else {
        try scope.appendNode(fail_decl);
        try scope.appendNode(loc_node);

        const bs = try scope.findBlockScope(t);
        try bs.discardVariable(name);
    }
}

fn warn(t: *Translator, scope: *Scope, tok_idx: TokenIndex, comptime format: []const u8, args: anytype) !void {
    const loc = t.tree.tokens.items(.loc)[tok_idx];
    const str = try t.locStr(loc);
    const value = try std.fmt.allocPrint(t.arena, "// {s}: warning: " ++ format, .{str} ++ args);
    try scope.appendNode(try ZigTag.warning.create(t.arena, value));
}

pub const Options = struct {
    gpa: mem.Allocator,
    comp: *aro.Compilation,
    pp: *const aro.Preprocessor,
    tree: *const aro.Tree,
    module_libs: bool,
    pub_static: bool,
    func_bodies: bool,
    keep_macro_literals: bool,
    default_init: bool,
    strict_flex_arrays: StrictFlexArraysLevel,
};

pub fn translate(options: Options) mem.Allocator.Error![]u8 {
    const gpa = options.gpa;
    var arena_allocator = std.heap.ArenaAllocator.init(gpa);
    defer arena_allocator.deinit();
    const arena = arena_allocator.allocator();

    var translator: Translator = .{
        .gpa = gpa,
        .arena = arena,
        .alias_list = .empty,
        .global_scope = try arena.create(Scope.Root),
        .comp = options.comp,
        .pp = options.pp,
        .tree = options.tree,
        .pub_static = options.pub_static,
        .func_bodies = options.func_bodies,
        .keep_macro_literals = options.keep_macro_literals,
        .default_init = options.default_init,
        .strict_flex_arrays = options.strict_flex_arrays,
    };
    translator.global_scope.* = Scope.Root.init(&translator);
    defer {
        translator.type_decls.deinit(gpa);
        translator.alias_list.deinit(gpa);
        translator.global_names.deinit(gpa);
        translator.weak_global_names.deinit(gpa);
        translator.opaque_demotes.deinit(gpa);
        translator.unnamed_typedefs.deinit(gpa);
        translator.anonymous_record_field_names.deinit(gpa);
        translator.typedefs.deinit(gpa);
        translator.global_scope.deinit();
        translator.wip_var_inits.deinit(gpa);
    }

    try translator.prepopulateGlobalNameTable();
    try translator.transTopLevelDecls();

    // Insert empty line before macros.
    try translator.global_scope.nodes.append(gpa, try ZigTag.warning.create(arena, "\n"));

    try translator.transMacros();

    for (translator.alias_list.items) |alias| {
        if (!translator.global_scope.sym_table.contains(alias.alias)) {
            const node = try ZigTag.alias.create(arena, .{ .actual = alias.alias, .mangled = alias.name });
            try translator.addTopLevelDecl(alias.alias, node);
        }
    }

    try translator.global_scope.processContainerMemberFns();

    var allocating: std.Io.Writer.Allocating = .init(gpa);
    defer allocating.deinit();

    allocating.writer.writeAll(
        \\const __root = @This();
        \\pub const __builtin = @import("std").zig.c_translation.builtins;
        \\pub const __helpers = @import("std").zig.c_translation.helpers;
        \\
    ) catch return error.OutOfMemory;

    var zig_ast = try ast.render(gpa, translator.global_scope.nodes.items);
    defer {
        gpa.free(zig_ast.source);
        zig_ast.deinit(gpa);
    }
    zig_ast.render(gpa, &allocating.writer, .{}) catch return error.OutOfMemory;
    return allocating.toOwnedSlice();
}

fn prepopulateGlobalNameTable(t: *Translator) !void {
    for (t.tree.root_decls.items) |decl| {
        switch (decl.get(t.tree)) {
            .typedef => |typedef_decl| {
                const decl_name = t.tree.tokSlice(typedef_decl.name_tok);
                try t.global_names.put(t.gpa, decl_name, {});

                // Check for typedefs with unnamed enum/record child types.
                const base = typedef_decl.qt.base(t.comp);
                switch (base.type) {
                    .@"enum" => |enum_ty| {
                        if (enum_ty.name.lookup(t.comp)[0] != '(') continue;
                    },
                    .@"struct", .@"union" => |record_ty| {
                        if (record_ty.name.lookup(t.comp)[0] != '(') continue;
                    },
                    else => continue,
                }

                const gop = try t.unnamed_typedefs.getOrPut(t.gpa, base.qt);
                if (gop.found_existing) {
                    // One typedef can declare multiple names.
                    // Don't put this one in `decl_table` so it's processed later.
                    continue;
                }
                gop.value_ptr.* = decl_name;
                try t.type_decls.put(t.gpa, decl, decl_name);
                try t.typedefs.put(t.gpa, decl_name, {});
            },

            .struct_decl,
            .union_decl,
            .struct_forward_decl,
            .union_forward_decl,
            .enum_decl,
            .enum_forward_decl,
            => {
                const decl_qt = decl.qt(t.tree);
                const prefix, const name = switch (decl_qt.base(t.comp).type) {
                    .@"struct" => |struct_ty| .{ "struct", struct_ty.name.lookup(t.comp) },
                    .@"union" => |union_ty| .{ "union", union_ty.name.lookup(t.comp) },
                    .@"enum" => |enum_ty| .{ "enum", enum_ty.name.lookup(t.comp) },
                    else => unreachable,
                };
                const prefixed_name = try std.fmt.allocPrint(t.arena, "{s}_{s}", .{ prefix, name });
                // `name` and `prefixed_name` are the preferred names for this type.
                // However, we can name it anything else if necessary, so these are "weak names".
                try t.weak_global_names.ensureUnusedCapacity(t.gpa, 2);
                t.weak_global_names.putAssumeCapacity(name, {});
                t.weak_global_names.putAssumeCapacity(prefixed_name, {});
            },

            .function, .variable => {
                const decl_name = t.tree.tokSlice(decl.tok(t.tree));
                try t.global_names.put(t.gpa, decl_name, {});
            },
            .static_assert => {},
            .empty_decl => {},
            .global_asm => {},
            else => unreachable,
        }
    }

    for (t.pp.defines.keys(), t.pp.defines.values()) |name, macro| {
        if (macro.isBuiltin()) continue;
        if (!t.isSelfDefinedMacro(name, macro)) {
            try t.global_names.put(t.gpa, name, {});
        }
    }
}

/// Determines whether macro is of the form: `#define FOO FOO` (Possibly with trailing tokens)
/// Macros of this form will not be translated.
fn isSelfDefinedMacro(t: *Translator, name: []const u8, macro: aro.Preprocessor.Macro) bool {
    if (macro.is_func) return false;

    if (macro.tokens.len < 1) return false;
    const first_tok = macro.tokens[0];

    const source = t.comp.getSource(macro.loc.id);
    const slice = source.buf[first_tok.start..first_tok.end];

    return std.mem.eql(u8, name, slice);
}

// =======================
// Declaration translation
// =======================

fn transTopLevelDecls(t: *Translator) !void {
    for (t.tree.root_decls.items) |decl| {
        try t.transDecl(&t.global_scope.base, decl);
    }
}

fn transDecl(t: *Translator, scope: *Scope, decl: Node.Index) !void {
    switch (decl.get(t.tree)) {
        .typedef => |typedef_decl| {
            // Implicit typedefs are translated only if referenced.
            if (typedef_decl.implicit) return;
            try t.transTypeDef(scope, decl);
        },

        .struct_decl, .union_decl => |record_decl| {
            try t.transRecordDecl(scope, record_decl.container_qt);
        },

        .struct_forward_decl, .union_forward_decl => |record_decl| {
            if (record_decl.definition) |some| {
                return t.transDecl(scope, some);
            }
            try t.transRecordDecl(scope, record_decl.container_qt);
        },

        .enum_decl => |enum_decl| {
            try t.transEnumDecl(scope, enum_decl.container_qt);
        },

        .enum_forward_decl => |enum_decl| {
            if (enum_decl.definition) |some| {
                return t.transDecl(scope, some);
            }
            try t.transEnumDecl(scope, enum_decl.container_qt);
        },

        .enum_field,
        .record_field,
        => return,

        .function => |function| {
            if (function.definition) |definition| {
                return t.transFnDecl(scope, definition.get(t.tree).function);
            }
            try t.transFnDecl(scope, function);
        },

        .variable => |variable| {
            if (variable.definition != null) return;
            try t.transVarDecl(scope, variable, decl);
        },
        .static_assert => |static_assert| {
            try t.transStaticAssert(&t.global_scope.base, static_assert);
        },
        .global_asm => |global_asm| {
            try t.transGlobalAsm(&t.global_scope.base, global_asm);
        },
        .empty_decl => {},
        else => unreachable,
    }
}

pub const builtin_typedef_map = std.StaticStringMap([]const u8).initComptime(.{
    .{ "uint8_t", "u8" },
    .{ "int8_t", "i8" },
    .{ "uint16_t", "u16" },
    .{ "int16_t", "i16" },
    .{ "uint24_t", "u24" },
    .{ "int24_t", "i24" },
    .{ "uint32_t", "u32" },
    .{ "int32_t", "i32" },
    .{ "uint48_t", "u48" },
    .{ "int48_t", "i48" },
    .{ "uint64_t", "u64" },
    .{ "int64_t", "i64" },
    .{ "intptr_t", "isize" },
    .{ "uintptr_t", "usize" },
    .{ "ssize_t", "isize" },
    .{ "size_t", "usize" },
});

fn transTypeDef(t: *Translator, scope: *Scope, typedef_node: Node.Index) Error!void {
    const typedef_decl = typedef_node.get(t.tree).typedef;
    if (t.type_decls.get(typedef_node)) |_|
        return; // Avoid processing this decl twice

    const toplevel = scope.id == .root;
    const bs: *Scope.Block = if (!toplevel) try scope.findBlockScope(t) else undefined;

    var name: []const u8 = t.tree.tokSlice(typedef_decl.name_tok);
    try t.typedefs.put(t.gpa, name, {});

    if (builtin_typedef_map.get(name)) |builtin| {
        return t.type_decls.putNoClobber(t.gpa, typedef_node, builtin);
    }
    if (!toplevel) name = try bs.makeMangledName(name);
    try t.type_decls.putNoClobber(t.gpa, typedef_node, name);

    const typedef_loc = typedef_decl.name_tok;
    const init_node = t.transType(scope, typedef_decl.qt, typedef_loc) catch |err| switch (err) {
        error.UnsupportedType => {
            return t.failDecl(scope, typedef_loc, name, "unable to resolve typedef child type", .{});
        },
        error.OutOfMemory => |e| return e,
    };

    const payload = try t.arena.create(ast.Payload.SimpleVarDecl);
    payload.* = .{
        .base = .{ .tag = if (toplevel) .pub_var_simple else .var_simple },
        .data = .{
            .name = name,
            .init = init_node,
        },
    };
    const node = ZigNode.initPayload(&payload.base);

    if (toplevel) {
        try t.addTopLevelDecl(name, node);
    } else {
        try scope.appendNode(node);
        try bs.discardVariable(name);
    }
}

fn mangleWeakGlobalName(t: *Translator, want_name: []const u8) Error![]const u8 {
    var cur_name = want_name;

    if (!t.weak_global_names.contains(want_name)) {
        // This type wasn't noticed by the name detection pass, so nothing has been treating this as
        // a weak global name. We must mangle it to avoid conflicts with locals.
        cur_name = try std.fmt.allocPrint(t.arena, "{s}_{d}", .{ want_name, t.getMangle() });
    }

    while (t.global_names.contains(cur_name)) {
        cur_name = try std.fmt.allocPrint(t.arena, "{s}_{d}", .{ want_name, t.getMangle() });
    }
    return cur_name;
}

fn transRecordDecl(t: *Translator, scope: *Scope, record_qt: QualType) Error!void {
    const base = record_qt.base(t.comp);
    const record_ty = switch (base.type) {
        .@"struct", .@"union" => |record_ty| record_ty,
        else => unreachable,
    };

    if (t.type_decls.get(record_ty.decl_node)) |_|
        return; // Avoid processing this decl twice

    const toplevel = scope.id == .root;
    const bs: *Scope.Block = if (!toplevel) try scope.findBlockScope(t) else undefined;

    const container_kind: ZigTag = if (base.type == .@"union") .@"union" else .@"struct";
    const container_kind_name = @tagName(container_kind);

    var bare_name = record_ty.name.lookup(t.comp);
    var is_unnamed = false;
    var name = bare_name;

    if (t.unnamed_typedefs.get(base.qt)) |typedef_name| {
        bare_name = typedef_name;
        name = typedef_name;
    } else {
        if (record_ty.isAnonymous(t.comp)) {
            bare_name = try std.fmt.allocPrint(t.arena, "unnamed_{d}", .{t.getMangle()});
            is_unnamed = true;
        }
        name = try std.fmt.allocPrint(t.arena, "{s}_{s}", .{ container_kind_name, bare_name });
        if (toplevel and !is_unnamed) {
            name = try t.mangleWeakGlobalName(name);
        }
    }
    if (!toplevel) name = try bs.makeMangledName(name);
    try t.type_decls.putNoClobber(t.gpa, record_ty.decl_node, name);

    const is_pub = toplevel and !is_unnamed;
    const init_node = init: {
        if (record_ty.layout == null) {
            try t.opaque_demotes.put(t.gpa, base.qt, {});
            break :init ZigTag.opaque_literal.init();
        }

        var fields: std.ArrayList(ast.Payload.Container.Field) = .empty;
        defer fields.deinit(t.gpa);
        try fields.ensureUnusedCapacity(t.gpa, record_ty.fields.len);

        var functions: std.ArrayList(ZigNode) = .empty;
        defer functions.deinit(t.gpa);

        var unnamed_field_count: u32 = 0;

        // If a record doesn't have any attributes that would affect the alignment and
        // layout, then we can just use a simple `extern` type. If it does have attributes,
        // then we need to inspect the layout and assign an `align` value for each field.
        const has_alignment_attributes = aligned: {
            if (record_qt.hasAttribute(t.comp, .@"packed")) break :aligned true;
            if (record_qt.hasAttribute(t.comp, .aligned)) break :aligned true;
            for (record_ty.fields) |field| {
                const field_attrs = field.attributes(t.comp);
                for (field_attrs) |field_attr| {
                    switch (field_attr.tag) {
                        .@"packed", .aligned => break :aligned true,
                        else => {},
                    }
                }
            }
            break :aligned false;
        };
        const head_field_alignment: ?c_uint = if (has_alignment_attributes) t.headFieldAlignment(record_ty) else null;

        for (record_ty.fields, 0..) |field, field_index| {
            const field_loc = field.name_tok;

            // Demote record to opaque if it contains a bitfield
            if (field.bit_width != .null) {
                try t.opaque_demotes.put(t.gpa, base.qt, {});
                try t.warn(scope, field_loc, "{s} demoted to opaque type - has bitfield", .{container_kind_name});
                break :init ZigTag.opaque_literal.init();
            }

            var field_name = field.name.lookup(t.comp);
            if (field.name_tok == 0) {
                field_name = try std.fmt.allocPrint(t.arena, "unnamed_{d}", .{unnamed_field_count});
                unnamed_field_count += 1;
                try t.anonymous_record_field_names.put(t.gpa, .{
                    .parent = base.qt,
                    .field = field.qt,
                }, field_name);
            }

            const field_type = field_type: {
                // Check if this is a flexible array member.
                flexible: {
                    if (field_index != record_ty.fields.len - 1 and container_kind != .@"union") break :flexible;
                    const array_ty = field.qt.get(t.comp, .array) orelse break :flexible;
                    if (!t.isFlexibleArrayLen(array_ty.len)) break :flexible;

                    const elem_type = t.transType(scope, array_ty.elem, field_loc) catch |err| switch (err) {
                        error.UnsupportedType => break :flexible,
                        else => |e| return e,
                    };
                    const backing_array_len: usize = switch (array_ty.len) {
                        .fixed => |n| @intCast(n),
                        else => 0,
                    };
                    const backing_array = try ZigTag.array_type.create(t.arena, .{ .len = backing_array_len, .elem_type = elem_type });

                    const member_name = field_name;
                    field_name = try std.fmt.allocPrint(t.arena, "_{s}", .{field_name});

                    const member = try t.createFlexibleMemberFn(member_name, field_name);
                    try functions.append(t.gpa, member);

                    break :field_type backing_array;
                }

                break :field_type t.transType(scope, field.qt, field_loc) catch |err| switch (err) {
                    error.UnsupportedType => {
                        try t.opaque_demotes.put(t.gpa, base.qt, {});
                        try t.warn(scope, field.name_tok, "{s} demoted to opaque type - unable to translate type of field {s}", .{
                            container_kind_name,
                            field_name,
                        });
                        break :init ZigTag.opaque_literal.init();
                    },
                    else => |e| return e,
                };
            };

            // Demote record to opaque if it contains an opaque field
            if (t.typeWasDemotedToOpaque(field.qt)) {
                try t.opaque_demotes.put(t.gpa, base.qt, {});
                try t.warn(scope, field_loc, "{s} demoted to opaque type - has opaque field", .{container_kind_name});
                break :init ZigTag.opaque_literal.init();
            }

            const field_alignment = if (has_alignment_attributes)
                t.alignmentForField(record_ty, head_field_alignment, field_index)
            else
                null;

            // C99 introduced designated initializers for structs. Omitted fields are implicitly
            // initialized to zero. Some C APIs are designed with this in mind. Defaulting to zero
            // values for translated struct fields permits Zig code to comfortably use such an API.
            const default_value = if (t.default_init and container_kind == .@"struct")
                try t.createZeroValueNode(field.qt, field_type, .no_as)
            else
                null;

            fields.appendAssumeCapacity(.{
                .name = field_name,
                .type = field_type,
                .alignment = field_alignment,
                .default_value = default_value,
            });
        }

        // A record is empty if it has no fields or only flexible array fields.
        if (record_ty.fields.len == functions.items.len and
            t.comp.target.os.tag == .windows and t.comp.target.abi == .msvc)
        {
            // In MSVC empty records have the same size as their alignment.
            const padding_bits = record_ty.layout.?.size_bits;
            const alignment_bits = record_ty.layout.?.field_alignment_bits;

            try fields.append(t.gpa, .{
                .name = "_padding",
                .type = try ZigTag.type.create(t.arena, try std.fmt.allocPrint(t.arena, "u{d}", .{padding_bits})),
                .alignment = @divExact(alignment_bits, 8),
                .default_value = if (t.default_init and container_kind == .@"struct")
                    ZigTag.zero_literal.init()
                else
                    null,
            });
        }

        const container_payload = try t.arena.create(ast.Payload.Container);
        container_payload.* = .{
            .base = .{ .tag = container_kind },
            .data = .{
                .layout = .@"extern",
                .fields = try t.arena.dupe(ast.Payload.Container.Field, fields.items),
                .decls = try t.arena.dupe(ZigNode, functions.items),
            },
        };
        break :init ZigNode.initPayload(&container_payload.base);
    };

    const payload = try t.arena.create(ast.Payload.SimpleVarDecl);
    payload.* = .{
        .base = .{ .tag = if (is_pub) .pub_var_simple else .var_simple },
        .data = .{
            .name = name,
            .init = init_node,
        },
    };
    const node = ZigNode.initPayload(&payload.base);
    if (toplevel) {
        try t.addTopLevelDecl(name, node);
        // Only add the alias if the name is available *and* it was caught by
        // name detection. Don't bother performing a weak mangle, since a
        // mangled name is of no real use here.
        if (!is_unnamed and !t.global_names.contains(bare_name) and t.weak_global_names.contains(bare_name))
            try t.alias_list.append(t.gpa, .{ .alias = bare_name, .name = name });
        try t.global_scope.container_member_fns_map.put(t.gpa, record_qt, .{
            .container_decl_ptr = &payload.data.init,
        });
    } else {
        try scope.appendNode(node);
        try bs.discardVariable(name);
    }
}

fn transFnDecl(t: *Translator, scope: *Scope, function: Node.Function) Error!void {
    const func_ty = function.qt.get(t.comp, .func).?;

    const fn_name = t.tree.tokSlice(function.name_tok);
    if (scope.getAlias(fn_name) != null or t.global_scope.containsNow(fn_name))
        return; // Avoid processing this decl twice

    const fn_decl_loc = function.name_tok;
    const has_body = function.body != null and func_ty.kind != .variadic and t.func_bodies;
    if (function.body != null and func_ty.kind == .variadic) {
        try t.warn(scope, function.name_tok, "TODO unable to translate variadic function, demoted to extern", .{});
    }

    const is_always_inline = has_body and function.qt.getAttribute(t.comp, .always_inline) != null;
    const proto_ctx: FnProtoContext = .{
        .fn_name = fn_name,
        .is_always_inline = is_always_inline,
        .is_extern = !has_body,
        .is_export = !function.static and has_body and !is_always_inline and !function.@"inline",
        .is_pub = scope.id == .root and (!function.static or t.pub_static),
        .has_body = has_body,
        .cc = if (function.qt.getAttribute(t.comp, .calling_convention)) |some| switch (some.cc) {
            .c => .c,
            .stdcall => .x86_stdcall,
            .thiscall => .x86_thiscall,
            .fastcall => .x86_fastcall,
            .regcall => .x86_regcall,
            .riscv_vector => .riscv_vector,
            .aarch64_sve_pcs => .aarch64_sve_pcs,
            .aarch64_vector_pcs => .aarch64_vfabi,
            .arm_aapcs => .arm_aapcs,
            .arm_aapcs_vfp => .arm_aapcs_vfp,
            .vectorcall => switch (t.comp.target.cpu.arch) {
                .x86 => .x86_vectorcall,
                .aarch64, .aarch64_be => .aarch64_vfabi,
                else => .c,
            },
            .x86_64_sysv => .x86_64_sysv,
            .x86_64_win => .x86_64_win,
        } else .c,
    };

    const proto_node = t.transFnType(&t.global_scope.base, function.qt, func_ty, fn_decl_loc, proto_ctx) catch |err| switch (err) {
        error.UnsupportedType => {
            return t.failDecl(scope, fn_decl_loc, fn_name, "unable to resolve prototype of function", .{});
        },
        error.OutOfMemory => |e| return e,
    };

    const proto_payload = proto_node.castTag(.func).?;
    if (!has_body) {
        if (scope.id != .root) {
            const bs: *Scope.Block = try scope.findBlockScope(t);
            const mangled_name = try bs.createMangledName(fn_name, false, Scope.Block.extern_local_prefix);
            const wrapped = try ZigTag.wrapped_local.create(t.arena, .{ .name = mangled_name, .init = proto_node });
            try scope.appendNode(wrapped);
            try bs.discardVariable(mangled_name);
            return;
        }
        try t.global_scope.addMemberFunction(func_ty, proto_payload);
        return t.addTopLevelDecl(fn_name, proto_node);
    }

    // actual function definition with body
    const body_stmt = function.body.?.get(t.tree).compound_stmt;
    var block_scope = try Scope.Block.init(t, &t.global_scope.base, false);
    block_scope.return_type = func_ty.return_type;
    defer block_scope.deinit();

    var param_id: c_uint = 0;
    for (proto_payload.data.params, func_ty.params) |*param, param_info| {
        const param_name = param.name orelse {
            proto_payload.data.is_extern = true;
            proto_payload.data.is_export = false;
            proto_payload.data.is_inline = false;
            try t.warn(&t.global_scope.base, fn_decl_loc, "function {s} parameter has no name, demoted to extern", .{fn_name});
            return t.addTopLevelDecl(fn_name, proto_node);
        };

        const is_const = param_info.qt.@"const";

        const mangled_param_name = try block_scope.makeMangledName(param_name);
        param.name = mangled_param_name;

        if (!is_const) {
            const bare_arg_name = try std.fmt.allocPrint(t.arena, "arg_{s}", .{mangled_param_name});
            const arg_name = try block_scope.makeMangledName(bare_arg_name);
            param.name = arg_name;

            const redecl_node = try ZigTag.arg_redecl.create(t.arena, .{ .actual = mangled_param_name, .mangled = arg_name });
            try block_scope.statements.append(t.gpa, redecl_node);
        }
        try block_scope.discardVariable(mangled_param_name);

        param_id += 1;
    }

    t.transCompoundStmtInline(body_stmt, &block_scope) catch |err| switch (err) {
        error.OutOfMemory => |e| return e,
        error.SelfReferential => unreachable,
        error.UnsupportedTranslation,
        error.UnsupportedType,
        => {
            proto_payload.data.is_extern = true;
            proto_payload.data.is_export = false;
            proto_payload.data.is_inline = false;
            try t.warn(&t.global_scope.base, fn_decl_loc, "unable to translate function, demoted to extern", .{});
            return t.addTopLevelDecl(fn_name, proto_node);
        },
    };

    try t.global_scope.addMemberFunction(func_ty, proto_payload);
    proto_payload.data.body = try block_scope.complete();
    return t.addTopLevelDecl(fn_name, proto_node);
}

fn transVarDecl(t: *Translator, scope: *Scope, variable: Node.Variable, decl_node: Node.Index) Error!void {
    const base_name = t.tree.tokSlice(variable.name_tok);
    const toplevel = scope.id == .root;
    const bs: *Scope.Block = if (!toplevel) try scope.findBlockScope(t) else undefined;
    const name, const use_base_name = blk: {
        if (toplevel) break :blk .{ base_name, false };

        // Local extern and static variables are wrapped in a struct.
        const prefix: ?[]const u8 = switch (variable.storage_class) {
            .@"extern" => Scope.Block.extern_local_prefix,
            .static => Scope.Block.static_local_prefix,
            else => null,
        };
        break :blk .{ try bs.createMangledName(base_name, false, prefix), prefix != null };
    };

    if (t.typeWasDemotedToOpaque(variable.qt)) {
        if (variable.storage_class != .@"extern" and scope.id == .root) {
            return t.failDecl(scope, variable.name_tok, name, "non-extern variable has opaque type", .{});
        } else {
            return t.failDecl(scope, variable.name_tok, name, "local variable has opaque type", .{});
        }
    }

    const type_node = (if (variable.initializer) |init|
        t.transTypeInit(scope, variable.qt, init, variable.name_tok)
    else
        t.transType(scope, variable.qt, variable.name_tok)) catch |err| switch (err) {
        error.UnsupportedType => {
            return t.failDecl(scope, variable.name_tok, name, "unable to translate variable declaration type", .{});
        },
        else => |e| return e,
    };

    const array_ty = variable.qt.get(t.comp, .array);
    var is_const = variable.qt.@"const" or (array_ty != null and array_ty.?.elem.@"const");
    var is_extern = variable.storage_class == .@"extern";

    var self_referential = false;
    const init_node = init: {
        if (variable.initializer) |init| {
            const maybe_literal = init.get(t.tree);
            if (!toplevel) try t.wip_var_inits.putNoClobber(t.gpa, decl_node, {});
            defer _ = t.wip_var_inits.remove(decl_node);

            const init_node = (if (maybe_literal == .string_literal_expr)
                t.transStringLiteralInitializer(init, maybe_literal.string_literal_expr, type_node)
            else
                t.transExprCoercing(scope, init, .used)) catch |err| switch (err) {
                error.SelfReferential => {
                    self_referential = true;
                    break :init ZigTag.undefined_literal.init();
                },
                error.UnsupportedTranslation, error.UnsupportedType => {
                    return t.failDecl(scope, variable.name_tok, name, "unable to resolve var init expr", .{});
                },
                else => |e| return e,
            };

            break :init try t.toNonBool(init_node, variable.qt);
        }
        if (variable.storage_class == .@"extern") {
            if (array_ty != null and array_ty.?.len == .incomplete) {
                // Oh no, an extern array of unknown size! These are really fun because there's no
                // direct equivalent in Zig. To translate correctly, we'll have to create a C-pointer
                // to the data initialized via @extern.

                // Since this is really a pointer to the underlying data, we tweak a few properties.
                is_extern = false;
                is_const = true;

                const name_str = try std.fmt.allocPrint(t.arena, "\"{s}\"", .{base_name});
                break :init try ZigTag.builtin_extern.create(t.arena, .{
                    .type = type_node,
                    .name = try ZigTag.string_literal.create(t.arena, name_str),
                });
            }
            break :init null;
        }
        if (toplevel or variable.storage_class == .static or variable.thread_local) {
            // The C language specification states that variables with static or threadlocal
            // storage without an initializer are initialized to a zero value.
            break :init try t.createZeroValueNode(variable.qt, type_node, .no_as);
        }
        break :init ZigTag.undefined_literal.init();
    };

    const linksection_string = blk: {
        if (variable.qt.getAttribute(t.comp, .section)) |section| {
            break :blk t.comp.interner.get(section.name.ref()).bytes;
        }
        break :blk null;
    };

    // TODO actually set with @export/@extern
    const linkage = variable.qt.linkage(t.comp);
    if (linkage != .strong) {
        try t.warn(scope, variable.name_tok, "TODO {s} linkage ignored", .{@tagName(linkage)});
    }

    const alignment: ?c_uint = variable.qt.requestedAlignment(t.comp) orelse null;
    var node = try ZigTag.var_decl.create(t.arena, .{
        .is_pub = toplevel,
        .is_const = is_const and !self_referential,
        .is_extern = is_extern,
        .is_export = toplevel and variable.storage_class == .auto and linkage == .strong,
        .is_threadlocal = variable.thread_local,
        .linksection_string = linksection_string,
        .alignment = alignment,
        .name = if (use_base_name) base_name else name,
        .type = type_node,
        .init = init_node,
    });

    if (toplevel) {
        try t.addTopLevelDecl(name, node);
    } else {
        if (use_base_name) {
            node = try ZigTag.wrapped_local.create(t.arena, .{ .name = name, .init = node });
        }
        try scope.appendNode(node);
        if (self_referential) {
            const deferred_init = t.transExprCoercing(scope, variable.initializer.?, .used) catch |err| switch (err) {
                error.SelfReferential => unreachable,
                error.UnsupportedTranslation, error.UnsupportedType => {
                    return t.failDecl(scope, variable.name_tok, name, "unable to resolve var init expr", .{});
                },
                else => |e| return e,
            };

            const assign = try ZigTag.assign.create(t.arena, .{
                .lhs = try ZigTag.identifier.create(t.arena, name),
                .rhs = try t.toNonBool(deferred_init, variable.qt),
            });
            try scope.appendNode(assign);
        }
        try bs.discardVariable(name);

        if (variable.qt.getAttribute(t.comp, .cleanup)) |cleanup_attr| {
            const cleanup_fn_name = t.tree.tokSlice(cleanup_attr.function.tok);
            const mangled_fn_name = scope.getAlias(cleanup_fn_name) orelse cleanup_fn_name;
            const fn_id = try ZigTag.identifier.create(t.arena, mangled_fn_name);

            const varname = try ZigTag.identifier.create(t.arena, name);
            const args = try t.arena.alloc(ZigNode, 1);
            args[0] = try ZigTag.address_of.create(t.arena, varname);

            const cleanup_call = try ZigTag.call.create(t.arena, .{ .lhs = fn_id, .args = args });
            const discard = try ZigTag.discard.create(t.arena, .{ .should_skip = false, .value = cleanup_call });
            const deferred_cleanup = try ZigTag.@"defer".create(t.arena, discard);

            try bs.statements.append(t.gpa, deferred_cleanup);
        }
    }
}

fn transEnumDecl(t: *Translator, scope: *Scope, enum_qt: QualType) Error!void {
    const base = enum_qt.base(t.comp);
    const enum_ty = base.type.@"enum";

    if (t.type_decls.get(enum_ty.decl_node)) |_|
        return; // Avoid processing this decl twice

    const toplevel = scope.id == .root;
    const bs: *Scope.Block = if (!toplevel) try scope.findBlockScope(t) else undefined;

    var bare_name = enum_ty.name.lookup(t.comp);
    var is_unnamed = false;
    var name = bare_name;
    if (t.unnamed_typedefs.get(base.qt)) |typedef_name| {
        bare_name = typedef_name;
        name = typedef_name;
    } else {
        if (enum_ty.isAnonymous(t.comp)) {
            bare_name = try std.fmt.allocPrint(t.arena, "unnamed_{d}", .{t.getMangle()});
            is_unnamed = true;
        }
        name = try std.fmt.allocPrint(t.arena, "enum_{s}", .{bare_name});
    }
    if (!toplevel) name = try bs.makeMangledName(name);
    try t.type_decls.putNoClobber(t.gpa, enum_ty.decl_node, name);

    const enum_type_node = if (!base.qt.hasIncompleteSize(t.comp)) blk: {
        const enum_decl = enum_ty.decl_node.get(t.tree).enum_decl;
        for (enum_ty.fields, enum_decl.fields) |field, field_node| {
            var enum_val_name = field.name.lookup(t.comp);
            if (!toplevel) {
                enum_val_name = try bs.makeMangledName(enum_val_name);
            }

            const enum_const_type_node: ?ZigNode = t.transType(scope, field.qt, field.name_tok) catch |err| switch (err) {
                error.UnsupportedType => null,
                else => |e| return e,
            };

            const val = t.tree.value_map.get(field_node).?;
            const enum_const_def = try ZigTag.enum_constant.create(t.arena, .{
                .name = enum_val_name,
                .is_public = toplevel,
                .type = enum_const_type_node,
                .value = try t.createIntNode(val),
            });
            if (toplevel)
                try t.addTopLevelDecl(enum_val_name, enum_const_def)
            else {
                try scope.appendNode(enum_const_def);
                try bs.discardVariable(enum_val_name);
            }
        }

        break :blk t.transType(scope, enum_ty.tag.?, enum_decl.name_or_kind_tok) catch |err| switch (err) {
            error.UnsupportedType => {
                return t.failDecl(scope, enum_decl.name_or_kind_tok, name, "unable to translate enum integer type", .{});
            },
            else => |e| return e,
        };
    } else blk: {
        try t.opaque_demotes.put(t.gpa, base.qt, {});
        break :blk ZigTag.opaque_literal.init();
    };

    const is_pub = toplevel and !is_unnamed;
    const payload = try t.arena.create(ast.Payload.SimpleVarDecl);
    payload.* = .{
        .base = .{ .tag = if (is_pub) .pub_var_simple else .var_simple },
        .data = .{
            .init = enum_type_node,
            .name = name,
        },
    };
    const node = ZigNode.initPayload(&payload.base);
    if (toplevel) {
        try t.addTopLevelDecl(name, node);
        if (!is_unnamed)
            try t.alias_list.append(t.gpa, .{ .alias = bare_name, .name = name });
    } else {
        try scope.appendNode(node);
        try bs.discardVariable(name);
    }
}

fn transStaticAssert(t: *Translator, scope: *Scope, static_assert: Node.StaticAssert) Error!void {
    const condition = t.transExpr(scope, static_assert.cond, .used) catch |err| switch (err) {
        error.SelfReferential => unreachable,
        error.UnsupportedTranslation, error.UnsupportedType => {
            return try t.warn(&t.global_scope.base, static_assert.cond.tok(t.tree), "unable to translate _Static_assert condition", .{});
        },
        error.OutOfMemory => |e| return e,
    };

    // generate @compileError message that matches C compiler output
    const diagnostic = if (static_assert.message) |message| str: {
        // Aro guarantees this to be a string literal.
        const str_val = t.tree.value_map.get(message).?;
        const str_qt = message.qt(t.tree);

        const bytes = t.comp.interner.get(str_val.ref()).bytes;
        var allocating: std.Io.Writer.Allocating = .init(t.gpa);
        defer allocating.deinit();

        allocating.writer.writeAll("\"static assertion failed \\") catch return error.OutOfMemory;

        aro.Value.printString(bytes, str_qt, t.comp, &allocating.writer) catch return error.OutOfMemory;
        allocating.writer.end -= 1; // printString adds a terminating " so we need to remove it
        allocating.writer.writeAll("\\\"\"") catch return error.OutOfMemory;

        break :str try ZigTag.string_literal.create(t.arena, try t.arena.dupe(u8, allocating.written()));
    } else try ZigTag.string_literal.create(t.arena, "\"static assertion failed\"");

    const assert_node = try ZigTag.static_assert.create(t.arena, .{ .lhs = condition, .rhs = diagnostic });
    try scope.appendNode(assert_node);
}

fn transGlobalAsm(t: *Translator, scope: *Scope, global_asm: Node.GlobalAsm) Error!void {
    const asm_string = t.tree.value_map.get(global_asm.asm_str).?;
    const bytes = t.comp.interner.get(asm_string.ref()).bytes;

    var allocating: std.Io.Writer.Allocating = try .initCapacity(t.gpa, bytes.len);
    defer allocating.deinit();
    aro.Value.printString(bytes, global_asm.asm_str.qt(t.tree), t.comp, &allocating.writer) catch return error.OutOfMemory;

    const str_node = try ZigTag.string_literal.create(t.arena, try t.arena.dupe(u8, allocating.written()));

    const asm_node = try ZigTag.asm_simple.create(t.arena, str_node);
    const block = try ZigTag.block_single.create(t.arena, asm_node);
    const comptime_node = try ZigTag.@"comptime".create(t.arena, block);

    try scope.appendNode(comptime_node);
}

// ================
// Type translation
// ================

fn getTypeStr(t: *Translator, qt: QualType) ![]const u8 {
    var allocating: std.Io.Writer.Allocating = .init(t.gpa);
    defer allocating.deinit();
    qt.print(t.comp, &allocating.writer) catch return error.OutOfMemory;
    return t.arena.dupe(u8, allocating.written());
}

fn transType(t: *Translator, scope: *Scope, qt: QualType, source_loc: TokenIndex) TypeError!ZigNode {
    loop: switch (qt.type(t.comp)) {
        .atomic => {
            const type_name = try t.getTypeStr(qt);
            return t.fail(error.UnsupportedType, source_loc, "TODO support atomic type: '{s}'", .{type_name});
        },
        .void => return ZigTag.type.create(t.arena, "anyopaque"),
        .bool => return ZigTag.type.create(t.arena, "bool"),
        .int => |int_ty| switch (int_ty) {
            //.char => return ZigTag.type.create(t.arena, "c_char"), // TODO: this is the preferred translation
            .char => return ZigTag.type.create(t.arena, "u8"),
            .schar => return ZigTag.type.create(t.arena, "i8"),
            .uchar => return ZigTag.type.create(t.arena, "u8"),
            .short => return ZigTag.type.create(t.arena, "c_short"),
            .ushort => return ZigTag.type.create(t.arena, "c_ushort"),
            .int => return ZigTag.type.create(t.arena, "c_int"),
            .uint => return ZigTag.type.create(t.arena, "c_uint"),
            .long => return ZigTag.type.create(t.arena, "c_long"),
            .ulong => return ZigTag.type.create(t.arena, "c_ulong"),
            .long_long => return ZigTag.type.create(t.arena, "c_longlong"),
            .ulong_long => return ZigTag.type.create(t.arena, "c_ulonglong"),
            .int128 => return ZigTag.type.create(t.arena, "i128"),
            .uint128 => return ZigTag.type.create(t.arena, "u128"),
        },
        .float => |float_ty| switch (float_ty) {
            .fp16, .float16 => return ZigTag.type.create(t.arena, "f16"),
            .float, .float32 => return ZigTag.type.create(t.arena, "f32"),
            .double, .float64, .float32x => return ZigTag.type.create(t.arena, "f64"),
            .long_double, .float64x => return ZigTag.type.create(t.arena, "c_longdouble"),
            .float128 => return ZigTag.type.create(t.arena, "f128"),
            .bf16 => return t.fail(error.UnsupportedType, source_loc, "TODO support bfloat16", .{}),
            .dfloat32,
            .dfloat64,
            .dfloat128,
            .dfloat64x,
            => return t.fail(error.UnsupportedType, source_loc, "TODO support decimal float type: '{s}'", .{try t.getTypeStr(qt)}),
            .float128x => unreachable, // Unsupported on all targets
        },
        .pointer => |pointer_ty| {
            const child_qt = pointer_ty.child;

            const is_fn_proto = child_qt.is(t.comp, .func);
            const is_const = is_fn_proto or child_qt.@"const";
            const is_volatile = child_qt.@"volatile";
            const elem_type = try t.transType(scope, child_qt, source_loc);
            const ptr_info: @FieldType(ast.Payload.Pointer, "data") = .{
                .is_const = is_const,
                .is_volatile = is_volatile,
                .elem_type = elem_type,
                .is_allowzero = false,
            };
            if (is_fn_proto or
                t.typeIsOpaque(child_qt) or
                t.typeWasDemotedToOpaque(child_qt))
            {
                const ptr = try ZigTag.single_pointer.create(t.arena, ptr_info);
                return ZigTag.optional_type.create(t.arena, ptr);
            }

            return ZigTag.c_pointer.create(t.arena, ptr_info);
        },
        .array => |array_ty| {
            const elem_qt = array_ty.elem;
            switch (array_ty.len) {
                .incomplete, .unspecified_variable => {
                    const elem_type = try t.transType(scope, elem_qt, source_loc);
                    return ZigTag.c_pointer.create(t.arena, .{
                        .is_const = elem_qt.@"const",
                        .is_volatile = elem_qt.@"volatile",
                        .is_allowzero = false,
                        .elem_type = elem_type,
                    });
                },
                .fixed, .static => |len| {
                    const elem_type = try t.transType(scope, elem_qt, source_loc);
                    return ZigTag.array_type.create(t.arena, .{ .len = len, .elem_type = elem_type });
                },
                .variable => return t.fail(error.UnsupportedType, source_loc, "VLA unsupported '{s}'", .{try t.getTypeStr(qt)}),
            }
        },
        .func => |func_ty| return t.transFnType(scope, qt, func_ty, source_loc, .{}),
        .@"struct", .@"union" => |record_ty| {
            var trans_scope = scope;
            if (!record_ty.isAnonymous(t.comp)) {
                if (t.weak_global_names.contains(record_ty.name.lookup(t.comp))) trans_scope = &t.global_scope.base;
            }
            try t.transRecordDecl(trans_scope, qt);
            const name = t.type_decls.get(record_ty.decl_node).?;
            return ZigTag.identifier.create(t.arena, name);
        },
        .@"enum" => |enum_ty| {
            var trans_scope = scope;
            const is_anonymous = enum_ty.isAnonymous(t.comp);
            if (!is_anonymous) {
                if (t.weak_global_names.contains(enum_ty.name.lookup(t.comp))) trans_scope = &t.global_scope.base;
            }
            try t.transEnumDecl(trans_scope, qt);
            const name = t.type_decls.get(enum_ty.decl_node).?;
            return ZigTag.identifier.create(t.arena, name);
        },
        .typedef => |typedef_ty| {
            var trans_scope = scope;
            const typedef_name = typedef_ty.name.lookup(t.comp);
            if (builtin_typedef_map.get(typedef_name)) |builtin| return ZigTag.type.create(t.arena, builtin);
            if (t.global_names.contains(typedef_name)) trans_scope = &t.global_scope.base;

            try t.transTypeDef(trans_scope, typedef_ty.decl_node);
            const name = t.type_decls.get(typedef_ty.decl_node).?;
            return ZigTag.identifier.create(t.arena, name);
        },
        .attributed => |attributed_ty| continue :loop attributed_ty.base.type(t.comp),
        .typeof => |typeof_ty| {
            if (typeof_ty.expr) |expr| {
                if (t.transExpr(scope, expr, .used)) |node| {
                    return ZigTag.typeof.create(t.arena, node);
                } else |err| switch (err) {
                    error.SelfReferential => {},
                    error.UnsupportedTranslation => {},
                    error.UnsupportedType => {},
                    error.OutOfMemory => |e| return e,
                }
            }
            continue :loop typeof_ty.base.type(t.comp);
        },
        .vector => |vector_ty| {
            const len = try t.createNumberNode(vector_ty.len);
            const elem_type = try t.transType(scope, vector_ty.elem, source_loc);
            return ZigTag.vector.create(t.arena, .{ .lhs = len, .rhs = elem_type });
        },
        else => return t.fail(error.UnsupportedType, source_loc, "unsupported type: '{s}'", .{try t.getTypeStr(qt)}),
    }
}

/// Look ahead through the fields of the record to determine what the alignment of the record
/// would be without any align/packed/etc. attributes. This helps us determine whether or not
/// the fields with 0 offset need an `align` qualifier. Strictly speaking, we could just
/// pedantically assign those fields the same alignment as the parent's pointer alignment,
/// but this helps the generated code to be a little less verbose.
fn headFieldAlignment(t: *Translator, record_decl: aro.Type.Record) ?c_uint {
    const bits_per_byte = 8;
    const parent_ptr_alignment_bits = record_decl.layout.?.pointer_alignment_bits;
    const parent_ptr_alignment = parent_ptr_alignment_bits / bits_per_byte;
    var max_field_alignment_bits: u64 = 0;
    for (record_decl.fields) |field|
        max_field_alignment_bits = @max(max_field_alignment_bits, bits_per_byte * field.qt.alignof(t.comp));
    if (max_field_alignment_bits != parent_ptr_alignment_bits) {
        return parent_ptr_alignment;
    } else {
        return null;
    }
}

/// This function inspects the generated layout of a record to determine the alignment for a
/// particular field. This approach is necessary because unlike Zig, a C compiler is not
/// required to fulfill the requested alignment, which means we'd risk generating different code
/// if we only look at the user-requested alignment.
///
/// Returns a ?c_uint to match Clang's behavior of using c_uint. The return type can be changed
/// after the Clang frontend for translate-c is removed. A null value indicates that a field is
/// 'naturally aligned'.
fn alignmentForField(
    t: *Translator,
    record_decl: aro.Type.Record,
    head_field_alignment: ?c_uint,
    field_index: usize,
) ?c_uint {
    const fields = record_decl.fields;
    assert(fields.len != 0);
    const field = fields[field_index];

    const bits_per_byte = 8;
    const parent_ptr_alignment_bits = record_decl.layout.?.pointer_alignment_bits;
    const parent_ptr_alignment = parent_ptr_alignment_bits / bits_per_byte;

    // bitfields aren't supported yet. Until support is added, records with bitfields
    // should be demoted to opaque, and this function shouldn't be called for them.
    if (field.bit_width != .null) {
        @panic("TODO: add bitfield support for records");
    }

    const field_offset_bits: u64 = field.layout.offset_bits;
    const field_size_bits: u64 = field.layout.size_bits;

    // Fields with zero width always have an alignment of 1
    if (field_size_bits == 0) {
        return 1;
    }

    // Fields with 0 offset inherit the parent's pointer alignment.
    if (field_offset_bits == 0) {
        return head_field_alignment;
    }

    // Records have a natural alignment when used as a field, and their size is
    // a multiple of this alignment value. For all other types, the natural alignment
    // is their size.
    const field_natural_alignment_bits: u64 = bits_per_byte * field.qt.alignof(t.comp);
    const rem_bits = field_offset_bits % field_natural_alignment_bits;

    // If there's a remainder, then the alignment is smaller than the field's
    // natural alignment
    if (rem_bits > 0) {
        const rem_alignment = rem_bits / bits_per_byte;
        if (rem_alignment > 0 and std.math.isPowerOfTwo(rem_alignment)) {
            const actual_alignment = @min(rem_alignment, parent_ptr_alignment);
            return @as(c_uint, @truncate(actual_alignment));
        } else {
            return 1;
        }
    }

    // A field may have an offset which positions it to be naturally aligned, but the
    // parent's pointer alignment determines if this is actually true, so we take the minimum
    // value.
    // For example, a float field (4 bytes wide) with a 4 byte offset is positioned to have natural
    // alignment, but if the parent pointer alignment is 2, then the actual alignment of the
    // float is 2.
    const field_natural_alignment: u64 = field_natural_alignment_bits / bits_per_byte;
    const offset_alignment = field_offset_bits / bits_per_byte;
    const possible_alignment = @min(parent_ptr_alignment, offset_alignment);
    if (possible_alignment == field_natural_alignment) {
        return null;
    } else if (possible_alignment < field_natural_alignment) {
        if (std.math.isPowerOfTwo(possible_alignment)) {
            return possible_alignment;
        } else {
            return 1;
        }
    } else { // possible_alignment > field_natural_alignment
        // Here, the field is positioned be at a higher alignment than it's natural alignment. This means we
        // need to determine whether it's a specified alignment. We can determine that from the padding preceding
        // the field.
        const padding_from_prev_field: u64 = blk: {
            if (field_offset_bits != 0) {
                const previous_field = fields[field_index - 1];
                break :blk (field_offset_bits - previous_field.layout.offset_bits) - previous_field.layout.size_bits;
            } else {
                break :blk 0;
            }
        };
        if (padding_from_prev_field < field_natural_alignment_bits) {
            return null;
        } else {
            return possible_alignment;
        }
    }
}

const FnProtoContext = struct {
    is_pub: bool = false,
    is_export: bool = false,
    is_extern: bool = false,
    is_always_inline: bool = false,
    fn_name: ?[]const u8 = null,
    has_body: bool = false,
    cc: ast.Payload.Func.CallingConvention = .c,
};

fn transFnType(
    t: *Translator,
    scope: *Scope,
    func_qt: QualType,
    func_ty: aro.Type.Func,
    source_loc: TokenIndex,
    ctx: FnProtoContext,
) !ZigNode {
    const param_count: usize = func_ty.params.len;
    const fn_params = try t.arena.alloc(ast.Payload.Param, param_count);

    for (func_ty.params, fn_params) |param_info, *param_node| {
        const param_qt = param_info.qt;
        const is_noalias = param_qt.restrict;

        const param_name: ?[]const u8 = if (param_info.name == .empty)
            null
        else
            param_info.name.lookup(t.comp);

        const type_node = try t.transType(scope, param_qt, param_info.name_tok);
        param_node.* = .{
            .is_noalias = is_noalias,
            .name = param_name,
            .type = type_node,
        };
    }

    const linksection_string = blk: {
        if (func_qt.getAttribute(t.comp, .section)) |section| {
            break :blk t.comp.interner.get(section.name.ref()).bytes;
        }
        break :blk null;
    };

    const alignment: ?c_uint = func_qt.requestedAlignment(t.comp) orelse null;

    const explicit_callconv = if ((ctx.is_always_inline or ctx.is_export or ctx.is_extern) and ctx.cc == .c) null else ctx.cc;

    const return_type_node = blk: {
        if (func_qt.getAttribute(t.comp, .noreturn) != null) {
            break :blk ZigTag.noreturn_type.init();
        } else {
            const return_qt = func_ty.return_type;
            if (return_qt.is(t.comp, .void)) {
                // convert primitive anyopaque to actual void (only for return type)
                break :blk ZigTag.void_type.init();
            } else {
                break :blk t.transType(scope, return_qt, source_loc) catch |err| switch (err) {
                    error.UnsupportedType => {
                        try t.warn(scope, source_loc, "unsupported function proto return type", .{});
                        return err;
                    },
                    error.OutOfMemory => |e| return e,
                };
            }
        }
    };

    // TODO actually set with @export/@extern
    const linkage = func_qt.linkage(t.comp);
    if (linkage != .strong) {
        try t.warn(scope, source_loc, "TODO {s} linkage ignored", .{@tagName(linkage)});
    }

    const payload = try t.arena.create(ast.Payload.Func);
    payload.* = .{
        .base = .{ .tag = .func },
        .data = .{
            .is_pub = ctx.is_pub,
            .is_extern = ctx.is_extern,
            .is_export = ctx.is_export and linkage == .strong,
            .is_inline = ctx.is_always_inline,
            .is_var_args = switch (func_ty.kind) {
                .normal => false,
                .variadic => true,
                .old_style => if (t.comp.target.cpu.arch.isWasm())
                    false
                else
                    !ctx.is_export and !ctx.is_always_inline and !ctx.has_body,
            },
            .name = ctx.fn_name,
            .linksection_string = linksection_string,
            .explicit_callconv = explicit_callconv,
            .params = fn_params,
            .return_type = return_type_node,
            .body = null,
            .alignment = alignment,
        },
    };
    return ZigNode.initPayload(&payload.base);
}

/// Produces a Zig AST node by translating a Type, respecting the width, but modifying the signed-ness.
/// Asserts the type is an integer.
fn transTypeIntWidthOf(t: *Translator, qt: QualType, is_signed: bool) TypeError!ZigNode {
    return ZigTag.type.create(t.arena, loop: switch (qt.base(t.comp).type) {
        .int => |int_ty| switch (int_ty) {
            .char, .schar, .uchar => if (is_signed) "i8" else "u8",
            .short, .ushort => if (is_signed) "c_short" else "c_ushort",
            .int, .uint => if (is_signed) "c_int" else "c_uint",
            .long, .ulong => if (is_signed) "c_long" else "c_ulong",
            .long_long, .ulong_long => if (is_signed) "c_longlong" else "c_ulonglong",
            .int128, .uint128 => if (is_signed) "i128" else "u128",
        },
        .bit_int => |bit_int_ty| try std.fmt.allocPrint(t.arena, "{s}{d}", .{
            if (is_signed) "i" else "u",
            bit_int_ty.bits,
        }),
        .@"enum" => |enum_ty| blk: {
            const tag_ty = enum_ty.tag orelse
                break :blk if (is_signed) "c_int" else "c_uint";

            continue :loop tag_ty.base(t.comp).type;
        },
        else => unreachable, // only call this function when it has already been determined the type is int
    });
}

fn transTypeInit(
    t: *Translator,
    scope: *Scope,
    qt: QualType,
    init: Node.Index,
    source_loc: TokenIndex,
) TypeError!ZigNode {
    switch (init.get(t.tree)) {
        .string_literal_expr => |literal| {
            const elem_ty = try t.transType(scope, qt.childType(t.comp), source_loc);

            const string_lit_size = literal.qt.arrayLen(t.comp).?;
            const array_size = qt.arrayLen(t.comp).?;

            if (array_size == string_lit_size) {
                return ZigTag.null_sentinel_array_type.create(t.arena, .{ .len = array_size - 1, .elem_type = elem_ty });
            } else {
                return ZigTag.array_type.create(t.arena, .{ .len = array_size, .elem_type = elem_ty });
            }
        },
        else => {},
    }
    return t.transType(scope, qt, source_loc);
}

// ============
// Type helpers
// ============

fn typeIsOpaque(t: *Translator, qt: QualType) bool {
    return switch (qt.base(t.comp).type) {
        .void => true,
        .@"struct", .@"union" => |record_ty| {
            if (record_ty.layout == null) return true;
            for (record_ty.fields) |field| {
                if (field.bit_width != .null) return true;
            }
            return false;
        },
        else => false,
    };
}

fn typeWasDemotedToOpaque(t: *Translator, qt: QualType) bool {
    return t.opaque_demotes.contains(qt.base(t.comp).qt);
}

fn typeHasWrappingOverflow(t: *Translator, qt: QualType) bool {
    if (t.signedness(qt) == .unsigned) {
        // unsigned integer overflow wraps around.
        return true;
    } else {
        // float, signed integer, and pointer overflow is undefined behavior.
        return false;
    }
}

/// Signedness of type when translated to Zig.
/// Different from `QualType.signedness()` for `char` and enums.
/// Returns null for non-int types.
fn signedness(t: *Translator, qt: QualType) ?std.builtin.Signedness {
    return loop: switch (qt.base(t.comp).type) {
        .bool => .unsigned,
        .bit_int => |bit_int| bit_int.signedness,
        .int => |int_ty| switch (int_ty) {
            .char => .unsigned, // Always translated as u8
            .schar, .short, .int, .long, .long_long, .int128 => .signed,
            .uchar, .ushort, .uint, .ulong, .ulong_long, .uint128 => .unsigned,
        },
        .@"enum" => |enum_ty| {
            const tag_qt = enum_ty.tag orelse return .signed;
            continue :loop tag_qt.base(t.comp).type;
        },
        else => return null,
    };
}

// =====================
// Statement translation
// =====================

fn transStmt(t: *Translator, scope: *Scope, stmt: Node.Index) TransError!ZigNode {
    switch (stmt.get(t.tree)) {
        .compound_stmt => |compound| {
            return t.transCompoundStmt(scope, compound);
        },
        .static_assert => |static_assert| {
            try t.transStaticAssert(scope, static_assert);
            return ZigTag.declaration.init();
        },
        .return_stmt => |return_stmt| return t.transReturnStmt(scope, return_stmt),
        .null_stmt => return ZigTag.empty_block.init(),
        .if_stmt => |if_stmt| return t.transIfStmt(scope, if_stmt),
        .while_stmt => |while_stmt| return t.transWhileStmt(scope, while_stmt),
        .do_while_stmt => |do_while_stmt| return t.transDoWhileStmt(scope, do_while_stmt),
        .for_stmt => |for_stmt| return t.transForStmt(scope, for_stmt),
        .continue_stmt => return ZigTag.@"continue".init(),
        .break_stmt => return ZigTag.@"break".init(),
        .typedef => |typedef_decl| {
            assert(!typedef_decl.implicit);
            try t.transTypeDef(scope, stmt);
            return ZigTag.declaration.init();
        },
        .struct_decl, .union_decl => |record_decl| {
            try t.transRecordDecl(scope, record_decl.container_qt);
            return ZigTag.declaration.init();
        },
        .struct_forward_decl, .union_forward_decl => |record_decl| {
            if (record_decl.definition) |some| {
                return t.transStmt(scope, some);
            }
            try t.transRecordDecl(scope, record_decl.container_qt);
            return ZigTag.declaration.init();
        },
        .enum_decl => |enum_decl| {
            try t.transEnumDecl(scope, enum_decl.container_qt);
            return ZigTag.declaration.init();
        },
        .enum_forward_decl => |enum_decl| {
            if (enum_decl.definition) |some| {
                return t.transStmt(scope, some);
            }
            try t.transEnumDecl(scope, enum_decl.container_qt);
            return ZigTag.declaration.init();
        },
        .function => |function| {
            try t.transFnDecl(scope, function);
            return ZigTag.declaration.init();
        },
        .variable => |variable| {
            try t.transVarDecl(scope, variable, stmt);
            return ZigTag.declaration.init();
        },
        .switch_stmt => |switch_stmt| return t.transSwitch(scope, switch_stmt),
        .case_stmt, .default_stmt => {
            return t.fail(error.UnsupportedTranslation, stmt.tok(t.tree), "TODO complex switch", .{});
        },
        .goto_stmt, .computed_goto_stmt, .labeled_stmt => {
            return t.fail(error.UnsupportedTranslation, stmt.tok(t.tree), "TODO goto", .{});
        },
        .asm_stmt => {
            return t.fail(error.UnsupportedTranslation, stmt.tok(t.tree), "TODO asm stmt", .{});
        },
        else => return t.transExprCoercing(scope, stmt, .unused),
    }
}

fn transCompoundStmtInline(t: *Translator, compound: Node.CompoundStmt, block: *Scope.Block) TransError!void {
    for (compound.body) |stmt| {
        const result = try t.transStmt(&block.base, stmt);
        switch (result.tag()) {
            .declaration, .empty_block => {},
            else => {
                try block.statements.append(t.gpa, result);
                if (result.isNoreturn()) return;
            },
        }
    }
}

fn transCompoundStmt(t: *Translator, scope: *Scope, compound: Node.CompoundStmt) TransError!ZigNode {
    var block_scope = try Scope.Block.init(t, scope, false);
    defer block_scope.deinit();
    try t.transCompoundStmtInline(compound, &block_scope);
    return try block_scope.complete();
}

fn transReturnStmt(t: *Translator, scope: *Scope, return_stmt: Node.ReturnStmt) TransError!ZigNode {
    switch (return_stmt.operand) {
        .none => return ZigTag.return_void.init(),
        .expr => |operand| {
            const rhs = try t.transExprCoercing(scope, operand, .used);
            const return_qt = scope.findBlockReturnType();
            return ZigTag.@"return".create(t.arena, try t.toNonBool(rhs, return_qt));
        },
        .implicit => |zero| {
            if (zero) return ZigTag.@"return".create(t.arena, ZigTag.zero_literal.init());

            const return_qt = scope.findBlockReturnType();
            if (return_qt.is(t.comp, .void)) return ZigTag.empty_block.init();

            return ZigTag.@"return".create(t.arena, ZigTag.undefined_literal.init());
        },
    }
}

/// If a statement can possibly translate to a Zig assignment (either directly because it's
/// an assignment in C or indirectly via result assignment to `_`) AND it's the sole statement
/// in the body of an if statement or loop, then we need to put the statement into its own block.
/// The `else` case here corresponds to statements that could result in an assignment. If a statement
/// class never needs a block, add its enum to the top prong.
fn maybeBlockify(t: *Translator, scope: *Scope, stmt: Node.Index) TransError!ZigNode {
    switch (stmt.get(t.tree)) {
        .break_stmt,
        .continue_stmt,
        .compound_stmt,
        .decl_ref_expr,
        .enumeration_ref,
        .do_while_stmt,
        .for_stmt,
        .if_stmt,
        .return_stmt,
        .null_stmt,
        .while_stmt,
        => return t.transStmt(scope, stmt),
        else => return t.blockify(scope, stmt),
    }
}

/// Translate statement and place it in its own block.
fn blockify(t: *Translator, scope: *Scope, stmt: Node.Index) TransError!ZigNode {
    var block_scope = try Scope.Block.init(t, scope, false);
    defer block_scope.deinit();
    const result = try t.transStmt(&block_scope.base, stmt);
    try block_scope.statements.append(t.gpa, result);
    return block_scope.complete();
}

fn transIfStmt(t: *Translator, scope: *Scope, if_stmt: Node.IfStmt) TransError!ZigNode {
    var cond_scope: Scope.Condition = .{
        .base = .{
            .parent = scope,
            .id = .condition,
        },
    };
    defer cond_scope.deinit();
    const cond = try t.transBoolExpr(&cond_scope.base, if_stmt.cond);

    // block needed to keep else statement from attaching to inner while
    const must_blockify = (if_stmt.else_body != null) and switch (if_stmt.then_body.get(t.tree)) {
        .while_stmt, .do_while_stmt, .for_stmt => true,
        else => false,
    };

    const then_node = if (must_blockify)
        try t.blockify(scope, if_stmt.then_body)
    else
        try t.maybeBlockify(scope, if_stmt.then_body);

    const else_node = if (if_stmt.else_body) |stmt|
        try t.maybeBlockify(scope, stmt)
    else
        null;
    return ZigTag.@"if".create(t.arena, .{ .cond = cond, .then = then_node, .@"else" = else_node });
}

fn transWhileStmt(t: *Translator, scope: *Scope, while_stmt: Node.WhileStmt) TransError!ZigNode {
    var cond_scope: Scope.Condition = .{
        .base = .{
            .parent = scope,
            .id = .condition,
        },
    };
    defer cond_scope.deinit();
    const cond = try t.transBoolExpr(&cond_scope.base, while_stmt.cond);

    var loop_scope: Scope = .{
        .parent = scope,
        .id = .loop,
    };
    const body = try t.maybeBlockify(&loop_scope, while_stmt.body);
    return ZigTag.@"while".create(t.arena, .{ .cond = cond, .body = body, .cont_expr = null });
}

fn transDoWhileStmt(t: *Translator, scope: *Scope, do_stmt: Node.DoWhileStmt) TransError!ZigNode {
    var loop_scope: Scope = .{
        .parent = scope,
        .id = .do_loop,
    };

    // if (!cond) break;
    var cond_scope: Scope.Condition = .{
        .base = .{
            .parent = scope,
            .id = .condition,
        },
    };
    defer cond_scope.deinit();
    const cond = try t.transBoolExpr(&cond_scope.base, do_stmt.cond);
    const if_not_break = switch (cond.tag()) {
        .true_literal => {
            const body_node = try t.maybeBlockify(scope, do_stmt.body);
            return ZigTag.while_true.create(t.arena, body_node);
        },
        else => try ZigTag.if_not_break.create(t.arena, cond),
    };

    var body_node = try t.transStmt(&loop_scope, do_stmt.body);
    if (body_node.isNoreturn()) {
        // The body node ends in a noreturn statement. Simply put it in a while (true)
        // in case it contains breaks or continues.
    } else if (do_stmt.body.get(t.tree) == .compound_stmt) {
        // there's already a block in C, so we'll append our condition to it.
        // c: do {
        // c:   a;
        // c:   b;
        // c: } while(c);
        // zig: while (true) {
        // zig:   a;
        // zig:   b;
        // zig:   if (!cond) break;
        // zig: }
        const block = body_node.castTag(.block).?;
        block.data.stmts.len += 1; // This is safe since we reserve one extra space in Scope.Block.complete.
        block.data.stmts[block.data.stmts.len - 1] = if_not_break;
    } else {
        // the C statement is without a block, so we need to create a block to contain it.
        // c: do
        // c:   a;
        // c: while(c);
        // zig: while (true) {
        // zig:   a;
        // zig:   if (!cond) break;
        // zig: }
        const statements = try t.arena.alloc(ZigNode, 2);
        statements[0] = body_node;
        statements[1] = if_not_break;
        body_node = try ZigTag.block.create(t.arena, .{ .label = null, .stmts = statements });
    }
    return ZigTag.while_true.create(t.arena, body_node);
}

fn transForStmt(t: *Translator, scope: *Scope, for_stmt: Node.ForStmt) TransError!ZigNode {
    var loop_scope: Scope = .{
        .parent = scope,
        .id = .loop,
    };

    var block_scope: ?Scope.Block = null;
    defer if (block_scope) |*bs| bs.deinit();

    switch (for_stmt.init) {
        .decls => |decls| {
            block_scope = try Scope.Block.init(t, scope, false);
            loop_scope.parent = &block_scope.?.base;
            for (decls) |decl| {
                try t.transDecl(&block_scope.?.base, decl);
            }
        },
        .expr => |maybe_init| if (maybe_init) |init| {
            block_scope = try Scope.Block.init(t, scope, false);
            loop_scope.parent = &block_scope.?.base;
            const init_node = try t.transStmt(&block_scope.?.base, init);
            try loop_scope.appendNode(init_node);
        },
    }
    var cond_scope: Scope.Condition = .{
        .base = .{
            .parent = &loop_scope,
            .id = .condition,
        },
    };
    defer cond_scope.deinit();

    const cond = if (for_stmt.cond) |cond|
        try t.transBoolExpr(&cond_scope.base, cond)
    else
        ZigTag.true_literal.init();

    const cont_expr = if (for_stmt.incr) |incr|
        try t.transExpr(&cond_scope.base, incr, .unused)
    else
        null;

    const body = try t.maybeBlockify(&loop_scope, for_stmt.body);
    const while_node = try ZigTag.@"while".create(t.arena, .{ .cond = cond, .body = body, .cont_expr = cont_expr });
    if (block_scope) |*bs| {
        try bs.statements.append(t.gpa, while_node);
        return try bs.complete();
    } else {
        return while_node;
    }
}

fn transSwitch(t: *Translator, scope: *Scope, switch_stmt: Node.SwitchStmt) TransError!ZigNode {
    var loop_scope: Scope = .{
        .parent = scope,
        .id = .loop,
    };

    var block_scope = try Scope.Block.init(t, &loop_scope, false);
    defer block_scope.deinit();

    const base_scope = &block_scope.base;

    var cond_scope: Scope.Condition = .{
        .base = .{
            .parent = base_scope,
            .id = .condition,
        },
    };
    defer cond_scope.deinit();
    const switch_expr = try t.transExpr(&cond_scope.base, switch_stmt.cond, .used);

    var cases: std.ArrayList(ZigNode) = .empty;
    defer cases.deinit(t.gpa);
    var has_default = false;

    const body_node = switch_stmt.body.get(t.tree);
    if (body_node != .compound_stmt) {
        return t.fail(error.UnsupportedTranslation, switch_stmt.switch_tok, "TODO complex switch", .{});
    }
    const body = body_node.compound_stmt.body;
    // Iterate over switch body and collect all cases.
    // Fallthrough is handled by duplicating statements.
    for (body, 0..) |stmt, i| {
        switch (stmt.get(t.tree)) {
            .case_stmt => {
                var items: std.ArrayList(ZigNode) = .empty;
                defer items.deinit(t.gpa);
                const sub = try t.transCaseStmt(base_scope, stmt, &items);
                const res = try t.transSwitchProngStmt(base_scope, sub, body[i..]);

                if (items.items.len == 0) {
                    has_default = true;
                    const switch_else = try ZigTag.switch_else.create(t.arena, res);
                    try cases.append(t.gpa, switch_else);
                } else {
                    const switch_prong = try ZigTag.switch_prong.create(t.arena, .{
                        .cases = try t.arena.dupe(ZigNode, items.items),
                        .cond = res,
                    });
                    try cases.append(t.gpa, switch_prong);
                }
            },
            .default_stmt => |default_stmt| {
                has_default = true;

                var sub = default_stmt.body;
                while (true) switch (sub.get(t.tree)) {
                    .case_stmt => |sub_case| sub = sub_case.body,
                    .default_stmt => |sub_default| sub = sub_default.body,
                    else => break,
                };

                const res = try t.transSwitchProngStmt(base_scope, sub, body[i..]);

                const switch_else = try ZigTag.switch_else.create(t.arena, res);
                try cases.append(t.gpa, switch_else);
            },
            else => {}, // collected in transSwitchProngStmt
        }
    }

    if (!has_default) {
        const else_prong = try ZigTag.switch_else.create(t.arena, ZigTag.empty_block.init());
        try cases.append(t.gpa, else_prong);
    }

    const switch_node = try ZigTag.@"switch".create(t.arena, .{
        .cond = switch_expr,
        .cases = try t.arena.dupe(ZigNode, cases.items),
    });
    try block_scope.statements.append(t.gpa, switch_node);
    try block_scope.statements.append(t.gpa, ZigTag.@"break".init());
    const while_body = try block_scope.complete();

    return ZigTag.while_true.create(t.arena, while_body);
}

/// Collects all items for this case, returns the first statement after the labels.
/// If items ends up empty, the prong should be translated as an else.
fn transCaseStmt(
    t: *Translator,
    scope: *Scope,
    stmt: Node.Index,
    items: *std.ArrayList(ZigNode),
) TransError!Node.Index {
    var sub = stmt;
    var seen_default = false;
    while (true) {
        switch (sub.get(t.tree)) {
            .default_stmt => |default_stmt| {
                seen_default = true;
                items.items.len = 0;
                sub = default_stmt.body;
            },
            .case_stmt => |case_stmt| {
                if (seen_default) {
                    items.items.len = 0;
                    sub = case_stmt.body;
                    continue;
                }

                const expr = if (case_stmt.end) |end| blk: {
                    const start_node = try t.transExpr(scope, case_stmt.start, .used);
                    const end_node = try t.transExpr(scope, end, .used);

                    break :blk try ZigTag.ellipsis3.create(t.arena, .{ .lhs = start_node, .rhs = end_node });
                } else try t.transExpr(scope, case_stmt.start, .used);

                try items.append(t.gpa, expr);
                sub = case_stmt.body;
            },
            else => return sub,
        }
    }
}

/// Collects all statements seen by this case into a block.
/// Avoids creating a block if the first statement is a break or return.
fn transSwitchProngStmt(
    t: *Translator,
    scope: *Scope,
    stmt: Node.Index,
    body: []const Node.Index,
) TransError!ZigNode {
    switch (stmt.get(t.tree)) {
        .case_stmt, .default_stmt => unreachable,
        else => {
            var block_scope = try Scope.Block.init(t, scope, false);
            defer block_scope.deinit();

            // we do not need to translate `stmt` since it is the first stmt of `body`
            try t.transSwitchProngStmtInline(&block_scope, body);
            return try block_scope.complete();
        },
    }
}

/// Collects all statements seen by this case into a block.
fn transSwitchProngStmtInline(
    t: *Translator,
    block: *Scope.Block,
    body: []const Node.Index,
) TransError!void {
    for (body) |stmt| {
        switch (stmt.get(t.tree)) {
            .case_stmt => |case_stmt| {
                var sub = case_stmt.body;
                while (true) switch (sub.get(t.tree)) {
                    .case_stmt => |sub_case| sub = sub_case.body,
                    .default_stmt => |sub_default| sub = sub_default.body,
                    else => break,
                };
                const result = try t.transStmt(&block.base, sub);
                assert(result.tag() != .declaration);
                try block.statements.append(t.gpa, result);
                if (result.isNoreturn()) return;
            },
            .default_stmt => |default_stmt| {
                var sub = default_stmt.body;
                while (true) switch (sub.get(t.tree)) {
                    .case_stmt => |sub_case| sub = sub_case.body,
                    .default_stmt => |sub_default| sub = sub_default.body,
                    else => break,
                };
                const result = try t.transStmt(&block.base, sub);
                assert(result.tag() != .declaration);
                try block.statements.append(t.gpa, result);
                if (result.isNoreturn()) return;
            },
            else => {
                const result = try t.transStmt(&block.base, stmt);
                switch (result.tag()) {
                    .declaration, .empty_block => {},
                    else => {
                        try block.statements.append(t.gpa, result);
                        if (result.isNoreturn()) return;
                    },
                }
            },
        }
    }
}

// ======================
// Expression translation
// ======================

const ResultUsed = enum { used, unused };

fn transExpr(t: *Translator, scope: *Scope, expr: Node.Index, used: ResultUsed) TransError!ZigNode {
    const qt = expr.qt(t.tree);
    return t.maybeSuppressResult(used, switch (expr.get(t.tree)) {
        .paren_expr => |paren_expr| {
            return t.transExpr(scope, paren_expr.operand, used);
        },
        .cast => |cast| return t.transCastExpr(scope, cast, cast.qt, used, .with_as),
        .decl_ref_expr => |decl_ref| try t.transDeclRefExpr(scope, decl_ref),
        .enumeration_ref => |enum_ref| try t.transDeclRefExpr(scope, enum_ref),
        .addr_of_expr => |addr_of_expr| try ZigTag.address_of.create(t.arena, try t.transExpr(scope, addr_of_expr.operand, .used)),
        .deref_expr => |deref_expr| res: {
            if (t.typeWasDemotedToOpaque(qt))
                return t.fail(error.UnsupportedTranslation, deref_expr.op_tok, "cannot dereference opaque type", .{});

            // Dereferencing a function pointer is a no-op.
            if (qt.is(t.comp, .func)) return t.transExpr(scope, deref_expr.operand, used);

            break :res try ZigTag.deref.create(t.arena, try t.transExpr(scope, deref_expr.operand, .used));
        },
        .bool_not_expr => |bool_not_expr| try ZigTag.not.create(t.arena, try t.transBoolExpr(scope, bool_not_expr.operand)),
        .bit_not_expr => |bit_not_expr| try ZigTag.bit_not.create(t.arena, op: {
            const operand = try t.transExpr(scope, bit_not_expr.operand, .used);
            if (!operand.isBoolRes()) break :op operand;

            const casted = try ZigTag.int_from_bool.create(t.arena, operand);
            const ty = try t.transType(scope, bit_not_expr.qt, bit_not_expr.op_tok);
            break :op try ZigTag.as.create(t.arena, .{ .lhs = ty, .rhs = casted });
        }),
        .plus_expr => |plus_expr| return t.transExpr(scope, plus_expr.operand, used),
        .negate_expr => |negate_expr| res: {
            const operand_qt = negate_expr.operand.qt(t.tree);
            if (!t.typeHasWrappingOverflow(operand_qt)) {
                const sub_expr_node = try t.transExpr(scope, negate_expr.operand, .used);
                const to_negate = if (sub_expr_node.isBoolRes()) blk: {
                    const ty_node = try ZigTag.type.create(t.arena, "c_int");
                    const int_node = try ZigTag.int_from_bool.create(t.arena, sub_expr_node);
                    break :blk try ZigTag.as.create(t.arena, .{ .lhs = ty_node, .rhs = int_node });
                } else sub_expr_node;

                break :res try ZigTag.negate.create(t.arena, to_negate);
            } else if (t.signedness(operand_qt) == .unsigned) {
                // use -% x for unsigned integers
                break :res try ZigTag.negate_wrap.create(t.arena, try t.transExpr(scope, negate_expr.operand, .used));
            } else return t.fail(error.UnsupportedTranslation, negate_expr.op_tok, "C negation with non float non integer", .{});
        },
        .div_expr => |div_expr| res: {
            if (qt.isInt(t.comp) and t.signedness(qt) == .signed) {
                // signed integer division uses @divTrunc
                const lhs = try t.transExpr(scope, div_expr.lhs, .used);
                const rhs = try t.transExpr(scope, div_expr.rhs, .used);
                break :res try ZigTag.div_trunc.create(t.arena, .{ .lhs = lhs, .rhs = rhs });
            }
            // unsigned/float division uses the operator
            break :res try t.transBinExpr(scope, div_expr, .div);
        },
        .mod_expr => |mod_expr| res: {
            if (qt.isInt(t.comp) and t.signedness(qt) == .signed) {
                // signed integer remainder uses __helpers.signedRemainder
                const lhs = try t.transExpr(scope, mod_expr.lhs, .used);
                const rhs = try t.transExpr(scope, mod_expr.rhs, .used);
                break :res try t.createHelperCallNode(.signedRemainder, &.{ lhs, rhs });
            }
            // unsigned/float division uses the operator
            break :res try t.transBinExpr(scope, mod_expr, .mod);
        },
        .add_expr => |add_expr| res: {
            // `ptr + idx` and `idx + ptr` -> ptr + @as(usize, @bitCast(@as(isize, @intCast(idx))))
            const lhs_qt = add_expr.lhs.qt(t.tree);
            const rhs_qt = add_expr.rhs.qt(t.tree);
            if (qt.isPointer(t.comp) and (t.signedness(lhs_qt) == .signed or
                t.signedness(rhs_qt) == .signed))
            {
                break :res try t.transPointerArithmeticSignedOp(scope, add_expr, .add);
            }

            if (t.signedness(qt) == .unsigned) {
                break :res try t.transBinExpr(scope, add_expr, .add_wrap);
            } else {
                break :res try t.transBinExpr(scope, add_expr, .add);
            }
        },
        .sub_expr => |sub_expr| res: {
            // `ptr - idx` -> ptr - @as(usize, @bitCast(@as(isize, @intCast(idx))))
            const lhs_qt = sub_expr.lhs.qt(t.tree);
            const rhs_qt = sub_expr.rhs.qt(t.tree);
            if (qt.isPointer(t.comp) and (t.signedness(lhs_qt) == .signed or
                t.signedness(rhs_qt) == .signed))
            {
                break :res try t.transPointerArithmeticSignedOp(scope, sub_expr, .sub);
            }

            if (sub_expr.lhs.qt(t.tree).isPointer(t.comp) and sub_expr.rhs.qt(t.tree).isPointer(t.comp)) {
                break :res try t.transPtrDiffExpr(scope, sub_expr);
            } else if (t.signedness(qt) == .unsigned) {
                break :res try t.transBinExpr(scope, sub_expr, .sub_wrap);
            } else {
                break :res try t.transBinExpr(scope, sub_expr, .sub);
            }
        },
        .mul_expr => |mul_expr| if (t.signedness(qt) == .unsigned)
            try t.transBinExpr(scope, mul_expr, .mul_wrap)
        else
            try t.transBinExpr(scope, mul_expr, .mul),

        .less_than_expr => |lt| try t.transBinExpr(scope, lt, .less_than),
        .greater_than_expr => |gt| try t.transBinExpr(scope, gt, .greater_than),
        .less_than_equal_expr => |lte| try t.transBinExpr(scope, lte, .less_than_equal),
        .greater_than_equal_expr => |gte| try t.transBinExpr(scope, gte, .greater_than_equal),
        .equal_expr => |equal_expr| try t.transBinExpr(scope, equal_expr, .equal),
        .not_equal_expr => |not_equal_expr| try t.transBinExpr(scope, not_equal_expr, .not_equal),

        .bool_and_expr => |bool_and_expr| try t.transBoolBinExpr(scope, bool_and_expr, .@"and"),
        .bool_or_expr => |bool_or_expr| try t.transBoolBinExpr(scope, bool_or_expr, .@"or"),

        .bit_and_expr => |bit_and_expr| try t.transBinExpr(scope, bit_and_expr, .bit_and),
        .bit_or_expr => |bit_or_expr| try t.transBinExpr(scope, bit_or_expr, .bit_or),
        .bit_xor_expr => |bit_xor_expr| try t.transBinExpr(scope, bit_xor_expr, .bit_xor),

        .shl_expr => |shl_expr| try t.transShiftExpr(scope, shl_expr, .shl),
        .shr_expr => |shr_expr| try t.transShiftExpr(scope, shr_expr, .shr),

        .member_access_expr => |member_access| try t.transMemberAccess(scope, .normal, member_access, null, .accessor),
        .member_access_ptr_expr => |member_access| try t.transMemberAccess(scope, .ptr, member_access, null, .accessor),
        .array_access_expr => |array_access| try t.transArrayAccess(scope, array_access, null),

        .builtin_ref => unreachable,
        .builtin_call_expr => |call| return t.transBuiltinCall(scope, call, used),
        .call_expr => |call| return t.transCall(scope, call, used),

        .builtin_types_compatible_p => |compatible| blk: {
            const lhs = try t.transType(scope, compatible.lhs, compatible.builtin_tok);
            const rhs = try t.transType(scope, compatible.rhs, compatible.builtin_tok);

            break :blk try ZigTag.equal.create(t.arena, .{
                .lhs = lhs,
                .rhs = rhs,
            });
        },
        .builtin_choose_expr => |choose| return t.transCondExpr(scope, choose, used),
        .cond_expr => |cond_expr| return t.transCondExpr(scope, cond_expr, used),
        .binary_cond_expr => |conditional| return t.transBinaryCondExpr(scope, conditional, used),
        .cond_dummy_expr => unreachable,

        .assign_expr => |assign| return t.transAssignExpr(scope, assign, used),
        .add_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .sub_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .mul_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .div_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .mod_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .shl_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .shr_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .bit_and_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .bit_xor_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .bit_or_assign_expr => |assign| return t.transCompoundAssign(scope, assign, used),
        .compound_assign_dummy_expr => {
            assert(used == .used);
            return t.compound_assign_dummy.?;
        },

        .comma_expr => |comma_expr| return t.transCommaExpr(scope, comma_expr, used),
        .pre_inc_expr => |un| return t.transIncDecExpr(scope, un, .pre, .inc, used),
        .pre_dec_expr => |un| return t.transIncDecExpr(scope, un, .pre, .dec, used),
        .post_inc_expr => |un| return t.transIncDecExpr(scope, un, .post, .inc, used),
        .post_dec_expr => |un| return t.transIncDecExpr(scope, un, .post, .dec, used),

        .int_literal => return t.transIntLiteral(scope, expr, used, .with_as),
        .char_literal => return t.transCharLiteral(scope, expr, used, .with_as),
        .float_literal => return t.transFloatLiteral(scope, expr, used, .with_as),
        .string_literal_expr => |literal| try t.transStringLiteral(scope, expr, literal),
        .bool_literal => res: {
            const val = t.tree.value_map.get(expr).?;
            break :res if (val.toBool(t.comp))
                ZigTag.true_literal.init()
            else
                ZigTag.false_literal.init();
        },
        .nullptr_literal => ZigTag.null_literal.init(),
        .imaginary_literal => |literal| {
            return t.fail(error.UnsupportedTranslation, literal.op_tok, "TODO complex numbers", .{});
        },
        .compound_literal_expr => |literal| return t.transCompoundLiteral(scope, literal, used),

        .default_init_expr => |default_init| return t.transDefaultInit(scope, default_init, used, .with_as),
        .array_init_expr => |array_init| return t.transArrayInit(scope, array_init, used),
        .union_init_expr => |union_init| return t.transUnionInit(scope, union_init, used),
        .struct_init_expr => |struct_init| return t.transStructInit(scope, struct_init, used),
        .array_filler_expr => unreachable,

        .sizeof_expr => |sizeof| try t.transTypeInfo(scope, .sizeof, sizeof),
        .alignof_expr => |alignof| try t.transTypeInfo(scope, .alignof, alignof),

        .imag_expr, .real_expr => |un| {
            return t.fail(error.UnsupportedTranslation, un.op_tok, "TODO complex numbers", .{});
        },
        .addr_of_label => |addr_of_label| {
            return t.fail(error.UnsupportedTranslation, addr_of_label.label_tok, "TODO computed goto", .{});
        },

        .generic_expr => |generic| return t.transExpr(scope, generic.chosen, used),
        .generic_association_expr => |generic| return t.transExpr(scope, generic.expr, used),
        .generic_default_expr => |generic| return t.transExpr(scope, generic.expr, used),

        .stmt_expr => |stmt_expr| return t.transStmtExpr(scope, stmt_expr, used),

        .builtin_convertvector => |convertvector| try t.transConvertvectorExpr(scope, convertvector),
        .builtin_shufflevector => |shufflevector| try t.transShufflevectorExpr(scope, shufflevector),

        .builtin_va_arg_pack, .builtin_va_arg_pack_len => |va_arg_pack| {
            return t.fail(error.UnsupportedTranslation, va_arg_pack.builtin_tok, "TODO va arg pack", .{});
        },

        .compound_stmt,
        .static_assert,
        .return_stmt,
        .null_stmt,
        .if_stmt,
        .while_stmt,
        .do_while_stmt,
        .for_stmt,
        .continue_stmt,
        .break_stmt,
        .labeled_stmt,
        .switch_stmt,
        .case_stmt,
        .default_stmt,
        .goto_stmt,
        .computed_goto_stmt,
        .asm_stmt,
        .global_asm,
        .typedef,
        .struct_decl,
        .union_decl,
        .enum_decl,
        .function,
        .param,
        .variable,
        .enum_field,
        .record_field,
        .struct_forward_decl,
        .union_forward_decl,
        .enum_forward_decl,
        .empty_decl,
        => unreachable, // not an expression
    });
}

/// Same as `transExpr` but with the knowledge that the operand will be type coerced, and therefore
/// an `@as` would be redundant. This is used to prevent redundant `@as` in integer literals.
fn transExprCoercing(t: *Translator, scope: *Scope, expr: Node.Index, used: ResultUsed) TransError!ZigNode {
    switch (expr.get(t.tree)) {
        .int_literal => return t.transIntLiteral(scope, expr, used, .no_as),
        .char_literal => return t.transCharLiteral(scope, expr, used, .no_as),
        .float_literal => return t.transFloatLiteral(scope, expr, used, .no_as),
        .cast => |cast| switch (cast.kind) {
            .no_op => {
                const operand = cast.operand.get(t.tree);
                if (operand == .cast) {
                    return t.transCastExpr(scope, operand.cast, cast.qt, used, .no_as);
                }
                return t.transExprCoercing(scope, cast.operand, used);
            },
            .lval_to_rval => return t.transExprCoercing(scope, cast.operand, used),
            else => return t.transCastExpr(scope, cast, cast.qt, used, .no_as),
        },
        .default_init_expr => |default_init| return try t.transDefaultInit(scope, default_init, used, .no_as),
        .compound_literal_expr => |literal| {
            if (!literal.thread_local and literal.storage_class != .static) {
                return t.transExprCoercing(scope, literal.initializer, used);
            }
        },
        else => {},
    }

    return t.transExpr(scope, expr, used);
}

fn transBoolExpr(t: *Translator, scope: *Scope, expr: Node.Index) TransError!ZigNode {
    switch (expr.get(t.tree)) {
        .int_literal => {
            const int_val = t.tree.value_map.get(expr).?;
            return if (int_val.isZero(t.comp))
                ZigTag.false_literal.init()
            else
                ZigTag.true_literal.init();
        },
        .cast => |cast| switch (cast.kind) {
            .bool_to_int => return t.transExpr(scope, cast.operand, .used),
            .array_to_pointer => {
                const operand = cast.operand.get(t.tree);
                if (operand == .string_literal_expr) {
                    // @intFromPtr("foo") != 0, always true
                    const str = try t.transStringLiteral(scope, cast.operand, operand.string_literal_expr);
                    const int_from_ptr = try ZigTag.int_from_ptr.create(t.arena, str);
                    return ZigTag.not_equal.create(t.arena, .{ .lhs = int_from_ptr, .rhs = ZigTag.zero_literal.init() });
                }
            },
            else => {},
        },
        else => {},
    }

    const maybe_bool_res = try t.transExpr(scope, expr, .used);
    if (maybe_bool_res.isBoolRes()) {
        return maybe_bool_res;
    }

    return t.finishBoolExpr(expr.qt(t.tree), maybe_bool_res);
}

fn toNonBool(t: *Translator, node: ZigNode, qt: QualType) Error!ZigNode {
    if (!node.isBoolRes()) return node;
    if (qt.is(t.comp, .bool)) return node;
    return ZigTag.int_from_bool.create(t.arena, node);
}

fn finishBoolExpr(t: *Translator, qt: QualType, node: ZigNode) TransError!ZigNode {
    const sk = qt.scalarKind(t.comp);
    if (sk == .bool) return node;
    if (sk == .nullptr_t) {
        // node == null, always true
        return ZigTag.equal.create(t.arena, .{ .lhs = node, .rhs = ZigTag.null_literal.init() });
    }
    if (sk.isPointer()) {
        // node != null
        return ZigTag.not_equal.create(t.arena, .{ .lhs = node, .rhs = ZigTag.null_literal.init() });
    }
    if (sk != .none) {
        // node != 0
        return ZigTag.not_equal.create(t.arena, .{ .lhs = node, .rhs = ZigTag.zero_literal.init() });
    }
    unreachable; // Unexpected bool expression type
}

fn transCastExpr(
    t: *Translator,
    scope: *Scope,
    cast: Node.Cast,
    dest_qt: QualType,
    used: ResultUsed,
    suppress_as: SuppressCast,
) TransError!ZigNode {
    const operand = switch (cast.kind) {
        .no_op => {
            const operand = cast.operand.get(t.tree);
            if (operand == .cast) {
                return t.transCastExpr(scope, operand.cast, cast.qt, used, suppress_as);
            }
            return t.transExpr(scope, cast.operand, used);
        },
        .lval_to_rval, .function_to_pointer => {
            return t.transExpr(scope, cast.operand, used);
        },
        .int_cast => int_cast: {
            const src_qt = cast.operand.qt(t.tree);

            if (cast.implicit) {
                if (t.tree.value_map.get(cast.operand)) |val| {
                    const max_int = try aro.Value.maxInt(dest_qt, t.comp);
                    const min_int = try aro.Value.minInt(dest_qt, t.comp);

                    if (val.compare(.lte, max_int, t.comp) and val.compare(.gte, min_int, t.comp)) {
                        break :int_cast try t.transExprCoercing(scope, cast.operand, .used);
                    }
                }
            }
            const operand = try t.transExpr(scope, cast.operand, .used);
            break :int_cast try t.transIntCast(operand, src_qt, dest_qt);
        },
        .to_void => {
            assert(used == .unused);
            return try t.transExpr(scope, cast.operand, .unused);
        },
        .null_to_pointer => ZigTag.null_literal.init(),
        .array_to_pointer => array_to_pointer: {
            const child_qt = dest_qt.childType(t.comp);

            loop: switch (cast.operand.get(t.tree)) {
                .string_literal_expr => |literal| {
                    const sub_expr_node = try t.transExpr(scope, cast.operand, .used);

                    const ref = if (literal.kind == .utf8 or literal.kind == .ascii)
                        sub_expr_node
                    else
                        try ZigTag.address_of.create(t.arena, sub_expr_node);

                    const casted = if (child_qt.@"const")
                        ref
                    else
                        try ZigTag.const_cast.create(t.arena, sub_expr_node);

                    return t.maybeSuppressResult(used, casted);
                },
                .paren_expr => |paren_expr| {
                    continue :loop paren_expr.operand.get(t.tree);
                },
                .generic_expr => |generic| {
                    continue :loop generic.chosen.get(t.tree);
                },
                .generic_association_expr => |generic| {
                    continue :loop generic.expr.get(t.tree);
                },
                .generic_default_expr => |generic| {
                    continue :loop generic.expr.get(t.tree);
                },
                else => {},
            }

            // Flexible array members are translated as member functions returning
            // [*c]T, so no address-of + @ptrCast wrapping is needed.
            flexible: {
                if (cast.operand.qt(t.tree).arrayLen(t.comp) == null) {
                    return try t.transExpr(scope, cast.operand, used);
                }

                const member_index, const base_qt = switch (cast.operand.get(t.tree)) {
                    .member_access_expr => |ma| .{ ma.member_index, ma.base.qt(t.tree) },
                    .member_access_ptr_expr => |ma| .{ ma.member_index, ma.base.qt(t.tree).childType(t.comp) },
                    else => break :flexible,
                };
                const record = base_qt.getRecord(t.comp) orelse break :flexible;
                if (member_index != record.fields.len - 1 and base_qt.base(t.comp).type != .@"union") break :flexible;
                const array_ty = record.fields[member_index].qt.get(t.comp, .array) orelse break :flexible;
                if (t.isFlexibleArrayLen(array_ty.len)) return try t.transExpr(scope, cast.operand, used);
            }

            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            const ref = try ZigTag.address_of.create(t.arena, sub_expr_node);
            const align_cast = try ZigTag.align_cast.create(t.arena, ref);
            break :array_to_pointer try ZigTag.ptr_cast.create(t.arena, align_cast);
        },
        .int_to_pointer => int_to_pointer: {
            var sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            const operand_qt = cast.operand.qt(t.tree);
            if (t.signedness(operand_qt) == .signed or operand_qt.bitSizeof(t.comp) > t.comp.target.ptrBitWidth()) {
                sub_expr_node = try ZigTag.as.create(t.arena, .{
                    .lhs = try ZigTag.type.create(t.arena, "usize"),
                    .rhs = try ZigTag.int_cast.create(t.arena, sub_expr_node),
                });
            } else if (sub_expr_node.isBoolRes()) {
                sub_expr_node = try ZigTag.int_from_bool.create(t.arena, sub_expr_node);
            }
            break :int_to_pointer try ZigTag.ptr_from_int.create(t.arena, sub_expr_node);
        },
        .int_to_bool => {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            if (sub_expr_node.isBoolRes()) return sub_expr_node;
            if (cast.operand.qt(t.tree).is(t.comp, .bool)) return sub_expr_node;
            const cmp_node = try ZigTag.not_equal.create(t.arena, .{ .lhs = sub_expr_node, .rhs = ZigTag.zero_literal.init() });
            return t.maybeSuppressResult(used, cmp_node);
        },
        .float_to_bool => {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            const cmp_node = try ZigTag.not_equal.create(t.arena, .{ .lhs = sub_expr_node, .rhs = ZigTag.zero_literal.init() });
            return t.maybeSuppressResult(used, cmp_node);
        },
        .pointer_to_bool => {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);

            // Special case function pointers as @intFromPtr(expr) != 0
            if (cast.operand.qt(t.tree).get(t.comp, .pointer)) |ptr_ty| if (ptr_ty.child.is(t.comp, .func)) {
                const ptr_node = if (sub_expr_node.tag() == .identifier)
                    try ZigTag.address_of.create(t.arena, sub_expr_node)
                else
                    sub_expr_node;
                const int_from_ptr = try ZigTag.int_from_ptr.create(t.arena, ptr_node);
                const cmp_node = try ZigTag.not_equal.create(t.arena, .{ .lhs = int_from_ptr, .rhs = ZigTag.zero_literal.init() });
                return t.maybeSuppressResult(used, cmp_node);
            };

            const cmp_node = try ZigTag.not_equal.create(t.arena, .{ .lhs = sub_expr_node, .rhs = ZigTag.null_literal.init() });
            return t.maybeSuppressResult(used, cmp_node);
        },
        .bool_to_int => bool_to_int: {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            break :bool_to_int try ZigTag.int_from_bool.create(t.arena, sub_expr_node);
        },
        .bool_to_float => bool_to_float: {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            const int_from_bool = try ZigTag.int_from_bool.create(t.arena, sub_expr_node);
            break :bool_to_float try ZigTag.float_from_int.create(t.arena, int_from_bool);
        },
        .bool_to_pointer => bool_to_pointer: {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            const int_from_bool = try ZigTag.int_from_bool.create(t.arena, sub_expr_node);
            break :bool_to_pointer try ZigTag.ptr_from_int.create(t.arena, int_from_bool);
        },
        .float_cast => float_cast: {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            break :float_cast try ZigTag.float_cast.create(t.arena, sub_expr_node);
        },
        .int_to_float => int_to_float: {
            const sub_expr_node = try t.transExpr(scope, cast.operand, used);
            const int_node = if (sub_expr_node.isBoolRes())
                try ZigTag.int_from_bool.create(t.arena, sub_expr_node)
            else
                sub_expr_node;
            break :int_to_float try ZigTag.float_from_int.create(t.arena, int_node);
        },
        .float_to_int => float_to_int: {
            const sub_expr_node = try t.transExpr(scope, cast.operand, .used);
            break :float_to_int try ZigTag.int_from_float.create(t.arena, sub_expr_node);
        },
        .pointer_to_int => pointer_to_int: {
            const sub_expr_node = try t.transPointerCastExpr(scope, cast.operand);
            const ptr_node = try ZigTag.int_from_ptr.create(t.arena, sub_expr_node);
            break :pointer_to_int try ZigTag.int_cast.create(t.arena, ptr_node);
        },
        .bitcast => bitcast: {
            const sub_expr_node = try t.transPointerCastExpr(scope, cast.operand);
            const operand_qt = cast.operand.qt(t.tree);
            if (dest_qt.isPointer(t.comp) and operand_qt.isPointer(t.comp)) {
                var casted = try ZigTag.align_cast.create(t.arena, sub_expr_node);
                casted = try ZigTag.ptr_cast.create(t.arena, casted);

                const src_elem = operand_qt.childType(t.comp);
                const dest_elem = dest_qt.childType(t.comp);
                if ((src_elem.@"const" or src_elem.is(t.comp, .func)) and !dest_elem.@"const") {
                    casted = try ZigTag.const_cast.create(t.arena, casted);
                }
                if (src_elem.@"volatile" and !dest_elem.@"volatile") {
                    casted = try ZigTag.volatile_cast.create(t.arena, casted);
                }
                break :bitcast casted;
            }

            break :bitcast try ZigTag.bit_cast.create(t.arena, sub_expr_node);
        },
        .union_cast => union_cast: {
            const union_type = try t.transType(scope, dest_qt, cast.l_paren);

            const operand_qt = cast.operand.qt(t.tree);
            const union_base = dest_qt.base(t.comp);
            const field = for (union_base.type.@"union".fields) |field| {
                if (field.qt.eql(operand_qt, t.comp)) break field;
            } else unreachable;
            const field_name = if (field.name_tok == 0) t.anonymous_record_field_names.get(.{
                .parent = union_base.qt,
                .field = field.qt,
            }).? else field.name.lookup(t.comp);

            const field_init = try t.arena.create(ast.Payload.ContainerInit.Initializer);
            field_init.* = .{
                .name = field_name,
                .value = try t.transExpr(scope, cast.operand, .used),
            };
            break :union_cast try ZigTag.container_init.create(t.arena, .{
                .lhs = union_type,
                .inits = field_init[0..1],
            });
        },
        else => return t.fail(error.UnsupportedTranslation, cast.l_paren, "TODO translate {s} cast", .{@tagName(cast.kind)}),
    };
    if (suppress_as == .no_as) return t.maybeSuppressResult(used, operand);
    if (used == .unused) return t.maybeSuppressResult(used, operand);
    const as = try ZigTag.as.create(t.arena, .{
        .lhs = try t.transType(scope, dest_qt, cast.l_paren),
        .rhs = operand,
    });
    return as;
}

fn transIntCast(t: *Translator, operand: ZigNode, src_qt: QualType, dest_qt: QualType) !ZigNode {
    const src_dest_order = src_qt.intRankOrder(dest_qt, t.comp);
    const different_sign = t.signedness(src_qt) != t.signedness(dest_qt);
    const needs_bitcast = different_sign and !(t.signedness(src_qt) == .unsigned and src_dest_order == .lt);

    var casted = operand;
    if (casted.isBoolRes()) {
        casted = try ZigTag.int_from_bool.create(t.arena, casted);
    } else if (src_dest_order == .gt) {
        // No C type is smaller than the 1 bit from @intFromBool
        casted = try ZigTag.truncate.create(t.arena, casted);
    }
    if (needs_bitcast) {
        if (src_dest_order != .eq) {
            casted = try ZigTag.as.create(t.arena, .{
                .lhs = try t.transTypeIntWidthOf(dest_qt, t.signedness(src_qt) == .signed),
                .rhs = casted,
            });
        }
        return ZigTag.bit_cast.create(t.arena, casted);
    }
    return casted;
}

/// Same as `transExpr` but adds a `&` if the expression is an identifier referencing a function type.
fn transPointerCastExpr(t: *Translator, scope: *Scope, expr: Node.Index) TransError!ZigNode {
    const sub_expr_node = try t.transExpr(scope, expr, .used);
    switch (expr.get(t.tree)) {
        .cast => |cast| if (cast.kind == .function_to_pointer and sub_expr_node.tag() == .identifier) {
            return ZigTag.address_of.create(t.arena, sub_expr_node);
        },
        else => {},
    }
    return sub_expr_node;
}

fn transDeclRefExpr(t: *Translator, scope: *Scope, decl_ref: Node.DeclRef) TransError!ZigNode {
    if (t.wip_var_inits.contains(decl_ref.decl)) return error.SelfReferential;

    const name = t.tree.tokSlice(decl_ref.name_tok);
    const maybe_alias = scope.getAlias(name);
    const mangled_name = maybe_alias orelse name;

    switch (decl_ref.decl.get(t.tree)) {
        .function => |function| if (function.definition == null and function.body == null) {
            // Try translating the decl again in case of out of scope declaration.
            try t.transFnDecl(scope, function);
        },
        else => {},
    }

    const decl = decl_ref.decl.get(t.tree);
    const ref_expr = blk: {
        const identifier = try ZigTag.identifier.create(t.arena, mangled_name);
        if (decl_ref.qt.is(t.comp, .func) and maybe_alias != null) {
            break :blk try ZigTag.field_access.create(t.arena, .{
                .lhs = identifier,
                .field_name = name,
            });
        }
        if (decl == .variable and maybe_alias != null) {
            switch (decl.variable.storage_class) {
                .@"extern", .static => {
                    break :blk try ZigTag.field_access.create(t.arena, .{
                        .lhs = identifier,
                        .field_name = name,
                    });
                },
                else => {},
            }
        }
        break :blk identifier;
    };

    scope.skipVariableDiscard(mangled_name);
    return ref_expr;
}

fn transBinExpr(t: *Translator, scope: *Scope, bin: Node.Binary, op_id: ZigTag) TransError!ZigNode {
    const lhs_uncasted = try t.transExpr(scope, bin.lhs, .used);
    const rhs_uncasted = try t.transExpr(scope, bin.rhs, .used);

    const lhs = if (lhs_uncasted.isBoolRes())
        try ZigTag.int_from_bool.create(t.arena, lhs_uncasted)
    else
        lhs_uncasted;

    const rhs = if (rhs_uncasted.isBoolRes())
        try ZigTag.int_from_bool.create(t.arena, rhs_uncasted)
    else
        rhs_uncasted;

    return t.createBinOpNode(op_id, lhs, rhs);
}

fn transBoolBinExpr(t: *Translator, scope: *Scope, bin: Node.Binary, op: ZigTag) !ZigNode {
    std.debug.assert(op == .@"and" or op == .@"or");

    const lhs = try t.transBoolExpr(scope, bin.lhs);
    const rhs = try t.transBoolExpr(scope, bin.rhs);

    return t.createBinOpNode(op, lhs, rhs);
}

fn transShiftExpr(t: *Translator, scope: *Scope, bin: Node.Binary, op_id: ZigTag) !ZigNode {
    std.debug.assert(op_id == .shl or op_id == .shr);

    // lhs >> @intCast(rh)
    const lhs = try t.transExpr(scope, bin.lhs, .used);

    const rhs = try t.transExpr(scope, bin.rhs, .used);
    const rhs_casted = try ZigTag.int_cast.create(t.arena, rhs);

    return t.createBinOpNode(op_id, lhs, rhs_casted);
}

fn transCondExpr(
    t: *Translator,
    scope: *Scope,
    conditional: Node.Conditional,
    used: ResultUsed,
) TransError!ZigNode {
    var cond_scope: Scope.Condition = .{
        .base = .{
            .parent = scope,
            .id = .condition,
        },
    };
    defer cond_scope.deinit();

    const res_is_bool = conditional.qt.is(t.comp, .bool);
    const cond = try t.transBoolExpr(&cond_scope.base, conditional.cond);

    var then_body = try t.transExpr(scope, conditional.then_expr, used);
    if (!res_is_bool and then_body.isBoolRes()) {
        then_body = try ZigTag.int_from_bool.create(t.arena, then_body);
    }

    var else_body = try t.transExpr(scope, conditional.else_expr, used);
    if (!res_is_bool and else_body.isBoolRes()) {
        else_body = try ZigTag.int_from_bool.create(t.arena, else_body);
    }

    // The `ResultUsed` is forwarded to both branches so no need to suppress the result here.
    return ZigTag.@"if".create(t.arena, .{ .cond = cond, .then = then_body, .@"else" = else_body });
}

fn transBinaryCondExpr(
    t: *Translator,
    scope: *Scope,
    conditional: Node.Conditional,
    used: ResultUsed,
) TransError!ZigNode {
    // GNU extension of the ternary operator where the middle expression is
    // omitted, the condition itself is returned if it evaluates to true.

    if (used == .unused) {
        // Result unused so this can be translated as
        // if (condition) else_expr;
        var cond_scope: Scope.Condition = .{
            .base = .{
                .parent = scope,
                .id = .condition,
            },
        };
        defer cond_scope.deinit();

        return ZigTag.@"if".create(t.arena, .{
            .cond = try t.transBoolExpr(&cond_scope.base, conditional.cond),
            .then = try t.transExpr(scope, conditional.else_expr, .unused),
            .@"else" = null,
        });
    }

    const res_is_bool = conditional.qt.is(t.comp, .bool);
    // c:   (condition)?:(else_expr)
    // zig: (blk: {
    //          const _cond_temp = (condition);
    //          break :blk if (_cond_temp) _cond_temp else (else_expr);
    //      })
    var block_scope = try Scope.Block.init(t, scope, true);
    defer block_scope.deinit();

    const cond_temp = try block_scope.reserveMangledName("cond_temp");
    const init_node = try t.transExpr(&block_scope.base, conditional.cond, .used);
    const temp_decl = try ZigTag.var_simple.create(t.arena, .{ .name = cond_temp, .init = init_node });
    try block_scope.statements.append(t.gpa, temp_decl);

    var cond_scope: Scope.Condition = .{
        .base = .{
            .parent = &block_scope.base,
            .id = .condition,
        },
    };
    defer cond_scope.deinit();

    const cond_ident = try ZigTag.identifier.create(t.arena, cond_temp);
    const cond_node = try t.finishBoolExpr(conditional.cond.qt(t.tree), cond_ident);
    var then_body = cond_ident;
    if (!res_is_bool and init_node.isBoolRes()) {
        then_body = try ZigTag.int_from_bool.create(t.arena, then_body);
    }

    var else_body = try t.transExpr(&block_scope.base, conditional.else_expr, .used);
    if (!res_is_bool and else_body.isBoolRes()) {
        else_body = try ZigTag.int_from_bool.create(t.arena, else_body);
    }
    const if_node = try ZigTag.@"if".create(t.arena, .{
        .cond = cond_node,
        .then = then_body,
        .@"else" = else_body,
    });
    const break_node = try ZigTag.break_val.create(t.arena, .{
        .label = block_scope.label,
        .val = if_node,
    });
    try block_scope.statements.append(t.gpa, break_node);
    return block_scope.complete();
}

fn transCommaExpr(t: *Translator, scope: *Scope, bin: Node.Binary, used: ResultUsed) TransError!ZigNode {
    if (used == .unused) {
        const lhs = try t.transExprCoercing(scope, bin.lhs, .unused);
        try scope.appendNode(lhs);
        const rhs = try t.transExprCoercing(scope, bin.rhs, .unused);
        return rhs;
    }

    var block_scope = try Scope.Block.init(t, scope, true);
    defer block_scope.deinit();

    const lhs = try t.transExprCoercing(&block_scope.base, bin.lhs, .unused);
    try block_scope.statements.append(t.gpa, lhs);

    const rhs = try t.transExprCoercing(&block_scope.base, bin.rhs, .used);
    const break_node = try ZigTag.break_val.create(t.arena, .{
        .label = block_scope.label,
        .val = try t.toNonBool(rhs, bin.qt),
    });
    try block_scope.statements.append(t.gpa, break_node);

    return try block_scope.complete();
}

fn transAssignExpr(t: *Translator, scope: *Scope, bin: Node.Binary, used: ResultUsed) !ZigNode {
    if (used == .unused) {
        const lhs = try t.transExpr(scope, bin.lhs, .used);
        const rhs = try t.transExprCoercing(scope, bin.rhs, .used);

        const lhs_qt = bin.lhs.qt(t.tree);
        return t.createBinOpNode(.assign, lhs, try t.toNonBool(rhs, lhs_qt));
    }

    var block_scope = try Scope.Block.init(t, scope, true);
    defer block_scope.deinit();

    const tmp = try block_scope.reserveMangledName("tmp");

    const rhs = try t.transExpr(&block_scope.base, bin.rhs, .used);
    const lhs_qt = bin.lhs.qt(t.tree);
    const tmp_decl = try ZigTag.var_simple.create(t.arena, .{
        .name = tmp,
        .init = try t.toNonBool(rhs, lhs_qt),
    });
    try block_scope.statements.append(t.gpa, tmp_decl);

    const lhs = try t.transExprCoercing(&block_scope.base, bin.lhs, .used);
    const tmp_ident = try ZigTag.identifier.create(t.arena, tmp);

    const assign = try t.createBinOpNode(.assign, lhs, tmp_ident);
    try block_scope.statements.append(t.gpa, assign);

    const break_node = try ZigTag.break_val.create(t.arena, .{
        .label = block_scope.label,
        .val = tmp_ident,
    });
    try block_scope.statements.append(t.gpa, break_node);

    return try block_scope.complete();
}

fn transCompoundAssign(
    t: *Translator,
    scope: *Scope,
    assign: Node.Binary,
    used: ResultUsed,
) !ZigNode {
    // If the result is unused we can try using the equivalent Zig operator
    // without a block
    if (used == .unused) {
        if (try t.transCompoundAssignSimple(scope, null, assign)) |some| {
            return some;
        }
    }

    // Otherwise we need to wrap the the compound assignment in a block.
    var block_scope = try Scope.Block.init(t, scope, used == .used);
    defer block_scope.deinit();
    const ref = try block_scope.reserveMangledName("ref");

    const lhs_expr = try t.transExpr(&block_scope.base, assign.lhs, .used);
    const addr_of = try ZigTag.address_of.create(t.arena, lhs_expr);
    const ref_decl = try ZigTag.var_simple.create(t.arena, .{ .name = ref, .init = addr_of });
    try block_scope.statements.append(t.gpa, ref_decl);

    const lhs_node = try ZigTag.identifier.create(t.arena, ref);
    const ref_node = try ZigTag.deref.create(t.arena, lhs_node);

    // Use the equivalent Zig operator if possible.
    if (try t.transCompoundAssignSimple(scope, ref_node, assign)) |some| {
        try block_scope.statements.append(t.gpa, some);
    } else {
        const old_dummy = t.compound_assign_dummy;
        defer t.compound_assign_dummy = old_dummy;
        t.compound_assign_dummy = ref_node;

        // Otherwise do the operation and assignment separately.
        const rhs_node = try t.transExprCoercing(&block_scope.base, assign.rhs, .used);
        const assign_node = try t.createBinOpNode(.assign, ref_node, rhs_node);
        try block_scope.statements.append(t.gpa, assign_node);
    }

    if (used == .used) {
        const break_node = try ZigTag.break_val.create(t.arena, .{
            .label = block_scope.label,
            .val = ref_node,
        });
        try block_scope.statements.append(t.gpa, break_node);
    }
    return block_scope.complete();
}

/// Translates compound assignment using the equivalent Zig operator if possible.
fn transCompoundAssignSimple(t: *Translator, scope: *Scope, lhs_dummy_opt: ?ZigNode, assign: Node.Binary) TransError!?ZigNode {
    const assign_rhs = assign.rhs.get(t.tree);
    if (assign_rhs == .cast) return null;

    const is_signed = t.signedness(assign.qt) == .signed;
    switch (assign_rhs) {
        .div_expr, .mod_expr => if (is_signed) return null,
        else => {},
    }
    const lhs_ptr = assign.qt.isPointer(t.comp);

    const bin, const op: ZigTag, const cast: enum { none, shift, usize } = switch (assign_rhs) {
        .add_expr => |bin| .{
            bin,
            if (t.typeHasWrappingOverflow(bin.qt)) .add_wrap_assign else .add_assign,
            if (lhs_ptr and t.signedness(bin.rhs.qt(t.tree)) == .signed) .usize else .none,
        },
        .sub_expr => |bin| .{
            bin,
            if (t.typeHasWrappingOverflow(bin.qt)) .sub_wrap_assign else .sub_assign,
            if (lhs_ptr and t.signedness(bin.rhs.qt(t.tree)) == .signed) .usize else .none,
        },
        .mul_expr => |bin| .{
            bin,
            if (t.typeHasWrappingOverflow(bin.qt)) .mul_wrap_assign else .mul_assign,
            .none,
        },
        .mod_expr => |bin| .{ bin, .mod_assign, .none },
        .div_expr => |bin| .{ bin, .div_assign, .none },
        .shl_expr => |bin| .{ bin, .shl_assign, .shift },
        .shr_expr => |bin| .{ bin, .shr_assign, .shift },
        .bit_and_expr => |bin| .{ bin, .bit_and_assign, .none },
        .bit_xor_expr => |bin| .{ bin, .bit_xor_assign, .none },
        .bit_or_expr => |bin| .{ bin, .bit_or_assign, .none },
        else => unreachable,
    };

    const lhs_node = blk: {
        const old_dummy = t.compound_assign_dummy;
        defer t.compound_assign_dummy = old_dummy;
        t.compound_assign_dummy = lhs_dummy_opt orelse try t.transExpr(scope, assign.lhs, .used);

        break :blk try t.transExpr(scope, bin.lhs, .used);
    };

    const rhs_node = try t.transExprCoercing(scope, bin.rhs, .used);
    const casted_rhs = switch (cast) {
        .none => rhs_node,
        .shift => try ZigTag.int_cast.create(t.arena, rhs_node),
        .usize => try t.usizeCastForWrappingPtrArithmetic(rhs_node),
    };
    return try t.createBinOpNode(op, lhs_node, casted_rhs);
}

fn transIncDecExpr(
    t: *Translator,
    scope: *Scope,
    un: Node.Unary,
    position: enum { pre, post },
    kind: enum { inc, dec },
    used: ResultUsed,
) !ZigNode {
    const is_wrapping = t.typeHasWrappingOverflow(un.qt);
    const op: ZigTag = switch (kind) {
        .inc => if (is_wrapping) .add_wrap_assign else .add_assign,
        .dec => if (is_wrapping) .sub_wrap_assign else .sub_assign,
    };

    const one_literal = ZigTag.one_literal.init();
    if (used == .unused) {
        const operand = try t.transExpr(scope, un.operand, .used);
        return try t.createBinOpNode(op, operand, one_literal);
    }

    var block_scope = try Scope.Block.init(t, scope, true);
    defer block_scope.deinit();

    const ref = try block_scope.reserveMangledName("ref");
    const operand = try t.transExprCoercing(&block_scope.base, un.operand, .used);
    const operand_ref = try ZigTag.address_of.create(t.arena, operand);
    const ref_decl = try ZigTag.var_simple.create(t.arena, .{ .name = ref, .init = operand_ref });
    try block_scope.statements.append(t.gpa, ref_decl);

    const ref_ident = try ZigTag.identifier.create(t.arena, ref);
    const ref_deref = try ZigTag.deref.create(t.arena, ref_ident);
    const effect = try t.createBinOpNode(op, ref_deref, one_literal);

    switch (position) {
        .pre => {
            try block_scope.statements.append(t.gpa, effect);

            const break_node = try ZigTag.break_val.create(t.arena, .{
                .label = block_scope.label,
                .val = ref_deref,
            });
            try block_scope.statements.append(t.gpa, break_node);
        },
        .post => {
            const tmp = try block_scope.reserveMangledName("tmp");
            const tmp_decl = try ZigTag.var_simple.create(t.arena, .{ .name = tmp, .init = ref_deref });
            try block_scope.statements.append(t.gpa, tmp_decl);

            try block_scope.statements.append(t.gpa, effect);

            const tmp_ident = try ZigTag.identifier.create(t.arena, tmp);
            const break_node = try ZigTag.break_val.create(t.arena, .{
                .label = block_scope.label,
                .val = tmp_ident,
            });
            try block_scope.statements.append(t.gpa, break_node);
        },
    }

    return try block_scope.complete();
}

fn transPtrDiffExpr(t: *Translator, scope: *Scope, bin: Node.Binary) TransError!ZigNode {
    const lhs_uncasted = try t.transExpr(scope, bin.lhs, .used);
    const rhs_uncasted = try t.transExpr(scope, bin.rhs, .used);

    const lhs = try ZigTag.int_from_ptr.create(t.arena, lhs_uncasted);
    const rhs = try ZigTag.int_from_ptr.create(t.arena, rhs_uncasted);

    const sub_res = try t.createBinOpNode(.sub_wrap, lhs, rhs);

    // @divExact(@as(<platform-ptrdiff_t>, @bitCast(@intFromPtr(lhs)) -% @intFromPtr(rhs)), @sizeOf(<lhs target type>))
    const ptrdiff_type = try t.transTypeIntWidthOf(bin.qt, true);

    const bitcast = try ZigTag.as.create(t.arena, .{
        .lhs = ptrdiff_type,
        .rhs = try ZigTag.bit_cast.create(t.arena, sub_res),
    });

    // C standard requires that pointer subtraction operands are of the same type,
    // otherwise it is undefined behavior. So we can assume the left and right
    // sides are the same Type and arbitrarily choose left.
    const lhs_ty = try t.transType(scope, bin.lhs.qt(t.tree), bin.lhs.tok(t.tree));
    const c_pointer = t.getContainer(lhs_ty).?;

    if (c_pointer.castTag(.c_pointer)) |c_pointer_payload| {
        const sizeof = try ZigTag.sizeof.create(t.arena, c_pointer_payload.data.elem_type);
        return ZigTag.div_exact.create(t.arena, .{
            .lhs = bitcast,
            .rhs = sizeof,
        });
    } else {
        // This is an opaque/incomplete type. This subtraction exhibits Undefined Behavior by the C99 spec.
        // However, allowing subtraction on `void *` and function pointers is a commonly used extension.
        // So, just return the value in byte units, mirroring the behavior of this language extension as implemented by GCC and Clang.
        return bitcast;
    }
}

/// Translate an arithmetic expression with a pointer operand and a signed-integer operand.
/// Zig requires a usize argument for pointer arithmetic, so we intCast to isize and then
/// bitcast to usize; pointer wraparound makes the math work.
/// Zig pointer addition is not commutative (unlike C); the pointer operand needs to be on the left.
/// The + operator in C is not a sequence point so it should be safe to switch the order if necessary.
fn transPointerArithmeticSignedOp(t: *Translator, scope: *Scope, bin: Node.Binary, op_id: ZigTag) TransError!ZigNode {
    std.debug.assert(op_id == .add or op_id == .sub);

    const lhs_qt = bin.lhs.qt(t.tree);
    const swap_operands = op_id == .add and t.signedness(lhs_qt) == .signed;

    const swizzled_lhs = if (swap_operands) bin.rhs else bin.lhs;
    const swizzled_rhs = if (swap_operands) bin.lhs else bin.rhs;

    const lhs_node = try t.transExpr(scope, swizzled_lhs, .used);
    const rhs_node = try t.transExpr(scope, swizzled_rhs, .used);

    const bitcast_node = try t.usizeCastForWrappingPtrArithmetic(rhs_node);

    return t.createBinOpNode(op_id, lhs_node, bitcast_node);
}

fn transMemberAccess(
    t: *Translator,
    scope: *Scope,
    kind: enum { normal, ptr },
    member_access: Node.MemberAccess,
    opt_base: ?ZigNode,
    flex_array_mode: enum { accessor, backing },
) TransError!ZigNode {
    const base_info = switch (kind) {
        .normal => member_access.base.qt(t.tree),
        .ptr => member_access.base.qt(t.tree).childType(t.comp),
    };
    if (t.typeWasDemotedToOpaque(base_info)) {
        return t.fail(error.UnsupportedTranslation, member_access.access_tok, "member access of demoted record", .{});
    }

    const record = base_info.getRecord(t.comp).?;
    const field = record.fields[member_access.member_index];
    const field_name = if (field.name_tok == 0) t.anonymous_record_field_names.get(.{
        .parent = base_info.base(t.comp).qt,
        .field = field.qt,
    }).? else field.name.lookup(t.comp);
    const base_node = opt_base orelse try t.transExpr(scope, member_access.base, .used);
    const lhs = switch (kind) {
        .normal => base_node,
        .ptr => try ZigTag.deref.create(t.arena, base_node),
    };
    const field_access = try ZigTag.field_access.create(t.arena, .{
        .lhs = lhs,
        .field_name = field_name,
    });

    // Flexible array members are translated as member functions.
    if (member_access.member_index == record.fields.len - 1 or base_info.base(t.comp).type == .@"union") {
        if (field.qt.get(t.comp, .array)) |array_ty| {
            if (t.isFlexibleArrayLen(array_ty.len)) {
                switch (flex_array_mode) {
                    .accessor => return ZigTag.call.create(t.arena, .{ .lhs = field_access, .args = &.{} }),
                    .backing => {
                        const backing_name = try std.fmt.allocPrint(t.arena, "_{s}", .{field_name});
                        return ZigTag.field_access.create(t.arena, .{ .lhs = lhs, .field_name = backing_name });
                    },
                }
            }
        }
    }

    return field_access;
}

fn transArrayAccess(t: *Translator, scope: *Scope, array_access: Node.ArrayAccess, opt_base: ?ZigNode) TransError!ZigNode {
    // Unwrap the base statement if it's an array decayed to a bare pointer type
    // so that we index the array itself
    const base = base: {
        const base = array_access.base.get(t.tree);
        if (base != .cast) break :base array_access.base;
        if (base.cast.kind != .array_to_pointer) break :base array_access.base;
        break :base base.cast.operand;
    };

    const base_node = opt_base orelse try t.transExpr(scope, base, .used);
    const index = index: {
        const index = try t.transExpr(scope, array_access.index, .used);
        const index_qt = array_access.index.qt(t.tree);
        const maybe_bigger_than_usize = type: switch (index_qt.base(t.comp).type) {
            .bool => {
                break :index try ZigTag.int_from_bool.create(t.arena, index);
            },
            .int => |int| switch (int) {
                .long_long, .ulong_long, .int128, .uint128 => true,
                else => false,
            },
            .bit_int => |bit_int| bit_int.bits > t.comp.target.ptrBitWidth(),
            .@"enum" => |e| if (e.tag) |tag| continue :type tag.base(t.comp).type else false,
            else => unreachable,
        };

        const is_nonnegative_int_literal = if (t.tree.value_map.get(array_access.index)) |val|
            val.compare(.gte, .zero, t.comp)
        else
            false;
        const is_signed = t.signedness(index_qt) == .signed;

        if (is_signed and !is_nonnegative_int_literal) {
            // First cast to `isize` to get proper sign extension and
            // then @bitCast to `usize` to satisfy the compiler.
            const index_isize = try ZigTag.as.create(t.arena, .{
                .lhs = try ZigTag.type.create(t.arena, "isize"),
                .rhs = try ZigTag.int_cast.create(t.arena, index),
            });
            break :index try ZigTag.bit_cast.create(t.arena, index_isize);
        }

        if (maybe_bigger_than_usize) {
            break :index try ZigTag.int_cast.create(t.arena, index);
        }
        break :index index;
    };

    return ZigTag.array_access.create(t.arena, .{
        .lhs = base_node,
        .rhs = index,
    });
}

fn transOffsetof(t: *Translator, scope: *Scope, arg: Node.Index) TransError!ZigNode {
    // Translate __builtin_offsetof(T, designator) as
    // @intFromPtr(&(@as(*allowzero T, @ptrFromInt(0)).designator))
    const member = try t.transMemberDesignator(scope, arg);
    const address = try ZigTag.address_of.create(t.arena, member);
    return ZigTag.int_from_ptr.create(t.arena, address);
}

fn transMemberDesignator(t: *Translator, scope: *Scope, arg: Node.Index) TransError!ZigNode {
    switch (arg.get(t.tree)) {
        .default_init_expr => |default| {
            const elem_node = try t.transType(scope, default.qt, default.last_tok);
            const ptr_ty = try ZigTag.single_pointer.create(t.arena, .{
                .elem_type = elem_node,
                .is_allowzero = true,
                .is_const = false,
                .is_volatile = false,
            });
            const zero = try ZigTag.ptr_from_int.create(t.arena, ZigTag.zero_literal.init());
            return ZigTag.as.create(t.arena, .{ .lhs = ptr_ty, .rhs = zero });
        },
        .array_access_expr => |access| {
            const base = try t.transMemberDesignator(scope, access.base);
            return t.transArrayAccess(scope, access, base);
        },
        .member_access_expr => |access| {
            const base = try t.transMemberDesignator(scope, access.base);
            // In offsetof context, flexible array members must be accessed via
            // the backing field (`_name`) rather than the accessor function,
            // because you can't take the address of a function call result.
            return t.transMemberAccess(scope, .normal, access, base, .backing);
        },
        .cast => |cast| {
            assert(cast.kind == .array_to_pointer);
            return t.transMemberDesignator(scope, cast.operand);
        },
        else => unreachable,
    }
}

fn transBuiltinCall(
    t: *Translator,
    scope: *Scope,
    call: Node.BuiltinCall,
    used: ResultUsed,
) TransError!ZigNode {
    const builtin_name = t.tree.tokSlice(call.builtin_tok);
    if (std.mem.eql(u8, builtin_name, "__builtin_offsetof")) {
        const res = try t.transOffsetof(scope, call.args[0]);
        return t.maybeSuppressResult(used, res);
    }

    const builtin = builtins.map.get(builtin_name) orelse
        return t.fail(error.UnsupportedTranslation, call.builtin_tok, "TODO implement function '{s}' in std.zig.c_builtins", .{builtin_name});

    if (builtin.tag) |tag| switch (tag) {
        .byte_swap, .ceil, .cos, .sin, .exp, .exp2, .exp10, .abs, .log, .log2, .log10, .round, .sqrt, .trunc, .floor => {
            assert(call.args.len == 1);
            const arg = try t.transExprCoercing(scope, call.args[0], .used);
            const arg_ty = try t.transType(scope, call.args[0].qt(t.tree), call.args[0].tok(t.tree));
            const coerced = try ZigTag.as.create(t.arena, .{ .lhs = arg_ty, .rhs = arg });

            const ptr = try t.arena.create(ast.Payload.UnOp);
            ptr.* = .{ .base = .{ .tag = tag }, .data = coerced };
            return t.maybeSuppressResult(used, ZigNode.initPayload(&ptr.base));
        },
        .@"unreachable" => return ZigTag.@"unreachable".init(),
        else => unreachable,
    };

    const arg_nodes = try t.arena.alloc(ZigNode, call.args.len);
    for (call.args, arg_nodes) |c_arg, *zig_arg| {
        zig_arg.* = try t.transExprCoercing(scope, c_arg, .used);
    }

    const builtin_identifier = try ZigTag.identifier.create(t.arena, "__builtin");
    const field_access = try ZigTag.field_access.create(t.arena, .{
        .lhs = builtin_identifier,
        .field_name = builtin.name,
    });

    const res = try ZigTag.call.create(t.arena, .{
        .lhs = field_access,
        .args = arg_nodes,
    });
    if (call.qt.is(t.comp, .void)) return res;
    return t.maybeSuppressResult(used, res);
}

fn transCall(
    t: *Translator,
    scope: *Scope,
    call: Node.Call,
    used: ResultUsed,
) TransError!ZigNode {
    const raw_fn_expr = try t.transExpr(scope, call.callee, .used);
    const fn_expr = blk: {
        loop: switch (call.callee.get(t.tree)) {
            .paren_expr => |paren_expr| {
                continue :loop paren_expr.operand.get(t.tree);
            },
            .decl_ref_expr => |decl_ref| {
                if (decl_ref.qt.is(t.comp, .func)) break :blk raw_fn_expr;
            },
            .cast => |cast| {
                if (cast.kind == .function_to_pointer) {
                    continue :loop cast.operand.get(t.tree);
                }
            },
            .deref_expr, .addr_of_expr => |un| {
                continue :loop un.operand.get(t.tree);
            },
            .generic_expr => |generic| {
                continue :loop generic.chosen.get(t.tree);
            },
            .generic_association_expr => |generic| {
                continue :loop generic.expr.get(t.tree);
            },
            .generic_default_expr => |generic| {
                continue :loop generic.expr.get(t.tree);
            },
            else => {},
        }
        break :blk try ZigTag.unwrap.create(t.arena, raw_fn_expr);
    };

    const callee_qt = call.callee.qt(t.tree);
    const maybe_ptr_ty = callee_qt.get(t.comp, .pointer);
    const func_qt = if (maybe_ptr_ty) |ptr| ptr.child else callee_qt;
    const func_ty = func_qt.get(t.comp, .func).?;

    const arg_nodes = try t.arena.alloc(ZigNode, call.args.len);
    for (call.args, arg_nodes, 0..) |c_arg, *zig_arg, i| {
        if (i < func_ty.params.len) {
            zig_arg.* = try t.transExprCoercing(scope, c_arg, .used);

            if (zig_arg.isBoolRes() and !func_ty.params[i].qt.is(t.comp, .bool)) {
                // In C the result type of a boolean expression is int. If this result is passed as
                // an argument to a function whose parameter is also int, there is no cast. Therefore
                // in Zig we'll need to cast it from bool to u1 (which will safely coerce to c_int).
                zig_arg.* = try ZigTag.int_from_bool.create(t.arena, zig_arg.*);
            }
        } else {
            zig_arg.* = try t.transExpr(scope, c_arg, .used);

            if (zig_arg.isBoolRes()) {
                // Same as above but now we don't have a result type.
                const u1_node = try ZigTag.int_from_bool.create(t.arena, zig_arg.*);
                const c_int_node = try ZigTag.type.create(t.arena, "c_int");
                zig_arg.* = try ZigTag.as.create(t.arena, .{ .lhs = c_int_node, .rhs = u1_node });
            }
        }
    }

    const res = try ZigTag.call.create(t.arena, .{
        .lhs = fn_expr,
        .args = arg_nodes,
    });
    if (call.qt.is(t.comp, .void)) return res;
    return t.maybeSuppressResult(used, res);
}

const SuppressCast = enum { with_as, no_as };

/// Attempt to translate literal as the name of the simple macro
/// it was expanded from.
fn checkLiteralMacro(t: *Translator, tok: TokenIndex, used: ResultUsed) !?ZigNode {
    if (!t.keep_macro_literals) return null;
    const expansion_locs = t.pp.expansionSlice(tok);
    if (expansion_locs.len == 0) return null;

    const last_expand = expansion_locs[0];
    const source = t.comp.getSource(last_expand.id);
    var tokenizer: aro.Tokenizer = .{
        .buf = source.buf,
        .langopts = t.comp.langopts,
        .source = last_expand.id,
        .index = last_expand.byte_offset,
        .splice_locs = &.{},
    };
    const name_tok = tokenizer.next();
    if (!name_tok.id.isMacroIdentifier()) return null;

    const name = t.pp.tokSlice(name_tok);
    if (t.global_scope.containsNow(name)) return null;
    const macro = t.pp.defines.get(name) orelse return null;
    if (macro.is_func) return null;
    if (macro.isBuiltin()) return null;

    var tok_count: u8 = 0;
    for (macro.tokens) |macro_tok| {
        switch (macro_tok.id) {
            .invalid => continue,
            .whitespace => continue,
            .comment => continue,
            .macro_ws => continue,
            else => {
                if (tok_count != 0) return null;
                tok_count += 1;
            },
        }
    }

    if (t.checkTranslatableMacro(macro.tokens, macro.params) != null) return null;

    const ident = try ZigTag.identifier.create(t.arena, name);
    return try t.maybeSuppressResult(used, ident);
}

fn transIntLiteral(
    t: *Translator,
    scope: *Scope,
    literal_index: Node.Index,
    used: ResultUsed,
    suppress_as: SuppressCast,
) TransError!ZigNode {
    if (try t.checkLiteralMacro(literal_index.tok(t.tree), used)) |node| return node;
    const val = t.tree.value_map.get(literal_index).?;
    const int_lit_node = try t.createIntNode(val);
    if (suppress_as == .no_as) {
        return t.maybeSuppressResult(used, int_lit_node);
    }

    // Integer literals in C have types, and this can matter for several reasons.
    // For example, this is valid C:
    //     unsigned char y = 256;
    // How this gets evaluated is the 256 is an integer, which gets truncated to signed char, then bit-casted
    // to unsigned char, resulting in 0. In order for this to work, we have to emit this zig code:
    //     var y = @as(u8, @bitCast(@as(i8, @truncate(@as(c_int, 256)))));

    // @as(T, x)
    const ty_node = try t.transType(scope, literal_index.qt(t.tree), literal_index.tok(t.tree));
    const as = try ZigTag.as.create(t.arena, .{ .lhs = ty_node, .rhs = int_lit_node });
    return t.maybeSuppressResult(used, as);
}

fn transCharLiteral(
    t: *Translator,
    scope: *Scope,
    literal_index: Node.Index,
    used: ResultUsed,
    suppress_as: SuppressCast,
) TransError!ZigNode {
    if (try t.checkLiteralMacro(literal_index.tok(t.tree), used)) |node| return node;
    const val = t.tree.value_map.get(literal_index).?;
    const char_literal = literal_index.get(t.tree).char_literal;
    const narrow = char_literal.kind == .ascii or char_literal.kind == .utf8;

    // C has a somewhat obscure feature called multi-character character constant
    // e.g. 'abcd'
    const int_value = val.toInt(u32, t.comp).?;
    const int_lit_node = if (char_literal.kind == .ascii and int_value > 255)
        try t.createNumberNode(int_value)
    else
        try t.createCharLiteralNode(narrow, int_value);

    if (suppress_as == .no_as) {
        return t.maybeSuppressResult(used, int_lit_node);
    }

    // See comment in `transIntLiteral` for why this code is here.
    // @as(T, x)
    const as_node = try ZigTag.as.create(t.arena, .{
        .lhs = try t.transType(scope, char_literal.qt, char_literal.literal_tok),
        .rhs = int_lit_node,
    });
    return t.maybeSuppressResult(used, as_node);
}

fn transFloatLiteral(
    t: *Translator,
    scope: *Scope,
    literal_index: Node.Index,
    used: ResultUsed,
    suppress_as: SuppressCast,
) TransError!ZigNode {
    if (try t.checkLiteralMacro(literal_index.tok(t.tree), used)) |node| return node;
    const val = t.tree.value_map.get(literal_index).?;
    const float_literal = literal_index.get(t.tree).float_literal;

    var allocating: std.Io.Writer.Allocating = .init(t.gpa);
    defer allocating.deinit();
    _ = val.print(float_literal.qt, t.comp, &allocating.writer) catch return error.OutOfMemory;
    if (mem.findScalar(u8, allocating.written(), '.') == null) {
        allocating.writer.writeAll(".0") catch return error.OutOfMemory;
    }

    const float_lit_node = try ZigTag.float_literal.create(t.arena, try t.arena.dupe(u8, allocating.written()));
    if (suppress_as == .no_as) {
        return t.maybeSuppressResult(used, float_lit_node);
    }

    const as_node = try ZigTag.as.create(t.arena, .{
        .lhs = try t.transType(scope, float_literal.qt, float_literal.literal_tok),
        .rhs = float_lit_node,
    });
    return t.maybeSuppressResult(used, as_node);
}

fn transStringLiteral(
    t: *Translator,
    scope: *Scope,
    expr: Node.Index,
    literal: Node.CharLiteral,
) TransError!ZigNode {
    switch (literal.kind) {
        .ascii, .utf8 => return t.transNarrowStringLiteral(expr, literal),
        .utf16, .utf32, .wide => {
            const name = try std.fmt.allocPrint(t.arena, "{s}_string_{d}", .{ @tagName(literal.kind), t.getMangle() });

            const array_type = try t.transTypeInit(scope, literal.qt, expr, literal.literal_tok);
            const lit_array = try t.transStringLiteralInitializer(expr, literal, array_type);
            const decl = try ZigTag.var_simple.create(t.arena, .{ .name = name, .init = lit_array });
            try scope.appendNode(decl);
            return ZigTag.identifier.create(t.arena, name);
        },
    }
}

fn transNarrowStringLiteral(
    t: *Translator,
    expr: Node.Index,
    literal: Node.CharLiteral,
) TransError!ZigNode {
    const val = t.tree.value_map.get(expr).?;

    const bytes = t.comp.interner.get(val.ref()).bytes;
    var allocating: std.Io.Writer.Allocating = try .initCapacity(t.gpa, bytes.len);
    defer allocating.deinit();

    aro.Value.printString(bytes, literal.qt, t.comp, &allocating.writer) catch return error.OutOfMemory;

    return ZigTag.string_literal.create(t.arena, try t.arena.dupe(u8, allocating.written()));
}

/// Translate a string literal that is initializing an array. In general narrow string
/// literals become `"<string>".*` or `"<string>"[0..<size>].*` if they need truncation.
/// Wide string literals become an array of integers. zero-fillers pad out the array to
/// the appropriate length, if necessary.
fn transStringLiteralInitializer(
    t: *Translator,
    expr: Node.Index,
    literal: Node.CharLiteral,
    array_type: ZigNode,
) TransError!ZigNode {
    assert(array_type.tag() == .array_type or array_type.tag() == .null_sentinel_array_type);

    const is_narrow = literal.kind == .ascii or literal.kind == .utf8;

    // The length of the string literal excluding the sentinel.
    const str_length = literal.qt.arrayLen(t.comp).? - 1;

    const payload = (array_type.castTag(.array_type) orelse array_type.castTag(.null_sentinel_array_type).?).data;
    const array_size = payload.len;
    const elem_type = payload.elem_type;

    if (array_size == 0) return ZigTag.empty_array.create(t.arena, array_type);

    const num_inits = @min(str_length, array_size);
    if (num_inits == 0) {
        return ZigTag.array_filler.create(t.arena, .{
            .type = elem_type,
            .filler = ZigTag.zero_literal.init(),
            .count = array_size,
        });
    }

    const init_node = if (is_narrow) blk: {
        // "string literal".* or string literal"[0..num_inits].*
        var str = try t.transNarrowStringLiteral(expr, literal);
        if (str_length != array_size) str = try ZigTag.string_slice.create(t.arena, .{ .string = str, .end = num_inits });
        break :blk try ZigTag.deref.create(t.arena, str);
    } else blk: {
        const size = literal.qt.childType(t.comp).sizeof(t.comp);

        const val = t.tree.value_map.get(expr).?;
        const bytes = t.comp.interner.get(val.ref()).bytes;

        const init_list = try t.arena.alloc(ZigNode, @intCast(num_inits));
        for (init_list, 0..) |*item, i| {
            const codepoint = switch (size) {
                2 => @as(*const u16, @ptrCast(@alignCast(bytes.ptr + i * 2))).*,
                4 => @as(*const u32, @ptrCast(@alignCast(bytes.ptr + i * 4))).*,
                else => unreachable,
            };
            item.* = try t.createCharLiteralNode(false, codepoint);
        }
        const init_args: ast.Payload.Array.ArrayTypeInfo = .{ .len = num_inits, .elem_type = elem_type };
        const init_array_type = if (array_type.tag() == .array_type)
            try ZigTag.array_type.create(t.arena, init_args)
        else
            try ZigTag.null_sentinel_array_type.create(t.arena, init_args);
        break :blk try ZigTag.array_init.create(t.arena, .{
            .cond = init_array_type,
            .cases = init_list,
        });
    };

    if (num_inits == array_size) return init_node;
    assert(array_size > str_length); // If array_size <= str_length, `num_inits == array_size` and we've already returned.

    const filler_node = try ZigTag.array_filler.create(t.arena, .{
        .type = elem_type,
        .filler = ZigTag.zero_literal.init(),
        .count = array_size - str_length,
    });
    return ZigTag.array_cat.create(t.arena, .{ .lhs = init_node, .rhs = filler_node });
}

fn transCompoundLiteral(
    t: *Translator,
    scope: *Scope,
    literal: Node.CompoundLiteral,
    used: ResultUsed,
) TransError!ZigNode {
    if (used == .unused) {
        return t.transExpr(scope, literal.initializer, .unused);
    }

    // TODO taking a reference to a compound literal should result in a mutable
    // pointer (unless the literal is const).

    const initializer = try t.transExprCoercing(scope, literal.initializer, .used);
    const ty = try t.transType(scope, literal.qt, literal.l_paren_tok);
    if (!literal.thread_local and literal.storage_class != .static) {
        // In the simple case a compound literal can be translated
        // simply as `@as(type, initializer)`.
        return ZigTag.as.create(t.arena, .{ .lhs = ty, .rhs = initializer });
    }

    // Otherwise static or thread local compound literals are translated as
    // a reference to a variable wrapped in a struct.

    var block_scope = try Scope.Block.init(t, scope, true);
    defer block_scope.deinit();

    const tmp = try block_scope.reserveMangledName("tmp");
    const wrapped_name = "compound_literal";

    // const tmp = struct { var compound_literal = initializer };
    const temp_decl = try ZigTag.var_decl.create(t.arena, .{
        .is_pub = false,
        .is_const = literal.qt.@"const",
        .is_extern = false,
        .is_export = false,
        .is_threadlocal = literal.thread_local,
        .linksection_string = null,
        .alignment = null,
        .name = wrapped_name,
        .type = ty,
        .init = initializer,
    });
    const wrapped = try ZigTag.wrapped_local.create(t.arena, .{ .name = tmp, .init = temp_decl });
    try block_scope.statements.append(t.gpa, wrapped);

    // break :blk tmp.compound_literal
    const static_tmp_ident = try ZigTag.identifier.create(t.arena, tmp);
    const field_access = try ZigTag.field_access.create(t.arena, .{
        .lhs = static_tmp_ident,
        .field_name = wrapped_name,
    });
    const break_node = try ZigTag.break_val.create(t.arena, .{
        .label = block_scope.label,
        .val = field_access,
    });
    try block_scope.statements.append(t.gpa, break_node);

    return block_scope.complete();
}

fn transDefaultInit(
    t: *Translator,
    scope: *Scope,
    default_init: Node.DefaultInit,
    used: ResultUsed,
    suppress_as: SuppressCast,
) TransError!ZigNode {
    assert(used == .used);
    const type_node = try t.transType(scope, default_init.qt, default_init.last_tok);
    return try t.createZeroValueNode(default_init.qt, type_node, suppress_as);
}

fn transArrayInit(
    t: *Translator,
    scope: *Scope,
    array_init: Node.ContainerInit,
    used: ResultUsed,
) TransError!ZigNode {
    assert(used == .used);
    const array_item_qt = array_init.container_qt.childType(t.comp);
    const array_item_type = try t.transType(scope, array_item_qt, array_init.l_brace_tok);
    var maybe_lhs: ?ZigNode = null;
    var val_list: std.ArrayList(ZigNode) = .empty;
    defer val_list.deinit(t.gpa);
    var i: usize = 0;
    while (i < array_init.items.len) {
        const rhs = switch (array_init.items[i].get(t.tree)) {
            .array_filler_expr => |array_filler| blk: {
                const node = try ZigTag.array_filler.create(t.arena, .{
                    .type = array_item_type,
                    .filler = try t.createZeroValueNode(array_item_qt, array_item_type, .no_as),
                    .count = @intCast(array_filler.count),
                });
                i += 1;
                break :blk node;
            },
            else => blk: {
                defer val_list.clearRetainingCapacity();
                while (i < array_init.items.len) : (i += 1) {
                    if (array_init.items[i].get(t.tree) == .array_filler_expr) break;
                    const expr = try t.transExprCoercing(scope, array_init.items[i], .used);
                    try val_list.append(t.gpa, try t.toNonBool(expr, array_item_qt));
                }
                const array_type = try ZigTag.array_type.create(t.arena, .{
                    .elem_type = array_item_type,
                    .len = val_list.items.len,
                });
                const array_init_node = try ZigTag.array_init.create(t.arena, .{
                    .cond = array_type,
                    .cases = try t.arena.dupe(ZigNode, val_list.items),
                });
                break :blk array_init_node;
            },
        };
        maybe_lhs = if (maybe_lhs) |lhs| blk: {
            const cat = try ZigTag.array_cat.create(t.arena, .{
                .lhs = lhs,
                .rhs = rhs,
            });
            break :blk cat;
        } else rhs;
    }
    return maybe_lhs orelse try ZigTag.container_init_dot.create(t.arena, &.{});
}

fn transUnionInit(
    t: *Translator,
    scope: *Scope,
    union_init: Node.UnionInit,
    used: ResultUsed,
) TransError!ZigNode {
    assert(used == .used);
    const init_expr = union_init.initializer orelse
        return ZigTag.undefined_literal.init();

    if (init_expr.get(t.tree) == .default_init_expr) {
        return try t.transExpr(scope, init_expr, used);
    }

    const union_type = try t.transType(scope, union_init.union_qt, union_init.l_brace_tok);

    const union_base = union_init.union_qt.base(t.comp);
    const field = union_base.type.@"union".fields[union_init.field_index];
    const field_name = if (field.name_tok == 0) t.anonymous_record_field_names.get(.{
        .parent = union_base.qt,
        .field = field.qt,
    }).? else field.name.lookup(t.comp);

    const field_init = try t.arena.create(ast.Payload.ContainerInit.Initializer);
    field_init.* = .{
        .name = field_name,
        .value = try t.toNonBool(try t.transExprCoercing(scope, init_expr, .used), field.qt),
    };
    const container_init = try ZigTag.container_init.create(t.arena, .{
        .lhs = union_type,
        .inits = field_init[0..1],
    });
    return container_init;
}

fn transStructInit(
    t: *Translator,
    scope: *Scope,
    struct_init: Node.ContainerInit,
    used: ResultUsed,
) TransError!ZigNode {
    assert(used == .used);
    const struct_type = try t.transType(scope, struct_init.container_qt, struct_init.l_brace_tok);
    const field_inits = try t.arena.alloc(ast.Payload.ContainerInit.Initializer, struct_init.items.len);

    const struct_base = struct_init.container_qt.base(t.comp);
    for (
        field_inits,
        struct_init.items,
        struct_base.type.@"struct".fields,
    ) |*init, field_expr, field| {
        const field_name = if (field.name_tok == 0) t.anonymous_record_field_names.get(.{
            .parent = struct_base.qt,
            .field = field.qt,
        }).? else field.name.lookup(t.comp);
        init.* = .{
            .name = field_name,
            .value = try t.toNonBool(try t.transExprCoercing(scope, field_expr, .used), field.qt),
        };
    }

    const container_init = try ZigTag.container_init.create(t.arena, .{
        .lhs = struct_type,
        .inits = field_inits,
    });
    return container_init;
}

fn transTypeInfo(
    t: *Translator,
    scope: *Scope,
    op: ZigTag,
    typeinfo: Node.TypeInfo,
) TransError!ZigNode {
    const operand = operand: {
        if (typeinfo.expr) |expr| {
            const operand = try t.transExpr(scope, expr, .used);
            if (operand.tag() == .string_literal) {
                const deref = try ZigTag.deref.create(t.arena, operand);
                break :operand try ZigTag.typeof.create(t.arena, deref);
            }
            break :operand try ZigTag.typeof.create(t.arena, operand);
        }
        break :operand try t.transType(scope, typeinfo.operand_qt, typeinfo.op_tok);
    };

    const payload = try t.arena.create(ast.Payload.UnOp);
    payload.* = .{
        .base = .{ .tag = op },
        .data = operand,
    };
    return ZigNode.initPayload(&payload.base);
}

fn transStmtExpr(
    t: *Translator,
    scope: *Scope,
    stmt_expr: Node.Unary,
    used: ResultUsed,
) TransError!ZigNode {
    const compound_stmt = stmt_expr.operand.get(t.tree).compound_stmt;
    if (used == .unused) {
        return t.transCompoundStmt(scope, compound_stmt);
    }
    var block_scope = try Scope.Block.init(t, scope, true);
    defer block_scope.deinit();

    for (compound_stmt.body[0 .. compound_stmt.body.len - 1]) |stmt| {
        const result = try t.transStmt(&block_scope.base, stmt);
        switch (result.tag()) {
            .declaration, .empty_block => {},
            else => try block_scope.statements.append(t.gpa, result),
        }
    }

    const last_result = try t.transExpr(&block_scope.base, compound_stmt.body[compound_stmt.body.len - 1], .used);
    switch (last_result.tag()) {
        .declaration, .empty_block => {},
        else => {
            const break_node = try ZigTag.break_val.create(t.arena, .{
                .label = block_scope.label,
                .val = last_result,
            });
            try block_scope.statements.append(t.gpa, break_node);
        },
    }
    return block_scope.complete();
}

fn transConvertvectorExpr(
    t: *Translator,
    scope: *Scope,
    convertvector: Node.Convertvector,
) TransError!ZigNode {
    var block_scope = try Scope.Block.init(t, scope, true);
    defer block_scope.deinit();

    const src_expr_node = try t.transExpr(&block_scope.base, convertvector.operand, .used);
    const tmp = try block_scope.reserveMangledName("tmp");
    const tmp_decl = try ZigTag.var_simple.create(t.arena, .{ .name = tmp, .init = src_expr_node });
    try block_scope.statements.append(t.gpa, tmp_decl);
    const tmp_ident = try ZigTag.identifier.create(t.arena, tmp);

    const dest_type_node = try t.transType(&block_scope.base, convertvector.dest_qt, convertvector.builtin_tok);
    const dest_vec_ty = convertvector.dest_qt.get(t.comp, .vector).?;
    const src_vec_ty = convertvector.operand.qt(t.tree).get(t.comp, .vector).?;

    const src_elem_sk = src_vec_ty.elem.scalarKind(t.comp);
    const dest_elem_sk = convertvector.dest_qt.childType(t.comp).scalarKind(t.comp);

    const items = try t.arena.alloc(ZigNode, dest_vec_ty.len);
    for (items, 0..dest_vec_ty.len) |*item, i| {
        const value = try ZigTag.array_access.create(t.arena, .{
            .lhs = tmp_ident,
            .rhs = try t.createNumberNode(i),
        });

        if (src_elem_sk == .float and dest_elem_sk == .float) {
            item.* = try ZigTag.float_cast.create(t.arena, value);
        } else if (src_elem_sk == .float) {
            item.* = try ZigTag.int_from_float.create(t.arena, value);
        } else if (dest_elem_sk == .float) {
            item.* = try ZigTag.float_from_int.create(t.arena, value);
        } else {
            item.* = try t.transIntCast(value, src_vec_ty.elem, dest_vec_ty.elem);
        }
    }

    const vec_init = try ZigTag.array_init.create(t.arena, .{
        .cond = dest_type_node,
        .cases = items,
    });
    const break_node = try ZigTag.break_val.create(t.arena, .{
        .label = block_scope.label,
        .val = vec_init,
    });
    try block_scope.statements.append(t.gpa, break_node);

    return block_scope.complete();
}

fn transShufflevectorExpr(
    t: *Translator,
    scope: *Scope,
    shufflevector: Node.Shufflevector,
) TransError!ZigNode {
    if (shufflevector.indexes.len == 0) {
        return t.fail(error.UnsupportedTranslation, shufflevector.builtin_tok, "@shuffle needs at least 1 index", .{});
    }

    const a = try t.transExpr(scope, shufflevector.lhs, .used);
    const b = try t.transExpr(scope, shufflevector.rhs, .used);

    // First two arguments to __builtin_shufflevector must be the same type
    const vector_child_type = try t.vectorTypeInfo(a, "child");
    const vector_len = try t.vectorTypeInfo(a, "len");
    const shuffle_mask = blk: {
        const mask_len = shufflevector.indexes.len;

        const mask_type = try ZigTag.vector.create(t.arena, .{
            .lhs = try t.createNumberNode(mask_len),
            .rhs = try ZigTag.type.create(t.arena, "i32"),
        });

        const init_list = try t.arena.alloc(ZigNode, mask_len);
        for (init_list, shufflevector.indexes) |*init, index| {
            const index_expr = try t.transExprCoercing(scope, index, .used);
            const converted_index = try t.createHelperCallNode(.shuffleVectorIndex, &.{ index_expr, vector_len });
            init.* = converted_index;
        }

        break :blk try ZigTag.array_init.create(t.arena, .{
            .cond = mask_type,
            .cases = init_list,
        });
    };

    return ZigTag.shuffle.create(t.arena, .{
        .element_type = vector_child_type,
        .a = a,
        .b = b,
        .mask_vector = shuffle_mask,
    });
}

// =====================
// Node creation helpers
// =====================

fn createZeroValueNode(
    t: *Translator,
    qt: QualType,
    type_node: ZigNode,
    suppress_as: SuppressCast,
) !ZigNode {
    switch (qt.base(t.comp).type) {
        .bool => return ZigTag.false_literal.init(),
        .int, .bit_int, .float => {
            const zero_literal = ZigTag.zero_literal.init();
            return switch (suppress_as) {
                .with_as => try t.createBinOpNode(.as, type_node, zero_literal),
                .no_as => zero_literal,
            };
        },
        .pointer => {
            const null_literal = ZigTag.null_literal.init();
            return switch (suppress_as) {
                .with_as => try t.createBinOpNode(.as, type_node, null_literal),
                .no_as => null_literal,
            };
        },
        else => {},
    }
    return try ZigTag.std_mem_zeroes.create(t.arena, type_node);
}

fn createIntNode(t: *Translator, int: aro.Value) !ZigNode {
    var space: aro.Interner.Tag.Int.BigIntSpace = undefined;
    var big = t.comp.interner.get(int.ref()).toBigInt(&space);
    const is_negative = !big.positive;
    big.positive = true;

    const str = big.toStringAlloc(t.arena, 10, .lower) catch |err| switch (err) {
        error.OutOfMemory => |e| return e,
    };
    const res = try ZigTag.integer_literal.create(t.arena, str);
    if (is_negative) return ZigTag.negate.create(t.arena, res);
    return res;
}

fn createNumberNode(t: *Translator, num: anytype) !ZigNode {
    const str = try std.fmt.allocPrint(t.arena, "{d}", .{num});
    return ZigTag.integer_literal.create(t.arena, str);
}

fn createCharLiteralNode(t: *Translator, narrow: bool, val: u32) TransError!ZigNode {
    return ZigTag.char_literal.create(t.arena, if (narrow)
        try std.fmt.allocPrint(t.arena, "'{f}'", .{std.zig.fmtChar(@as(u8, @intCast(val)))})
    else
        try std.fmt.allocPrint(t.arena, "'\\u{{{x}}}'", .{val}));
}

fn createBinOpNode(
    t: *Translator,
    op: ZigTag,
    lhs: ZigNode,
    rhs: ZigNode,
) !ZigNode {
    const payload = try t.arena.create(ast.Payload.BinOp);
    payload.* = .{
        .base = .{ .tag = op },
        .data = .{
            .lhs = lhs,
            .rhs = rhs,
        },
    };
    return ZigNode.initPayload(&payload.base);
}

pub fn createHelperCallNode(t: *Translator, name: std.meta.DeclEnum(std.zig.c_translation.helpers), args_opt: ?[]const ZigNode) !ZigNode {
    if (args_opt) |args| {
        return ZigTag.helper_call.create(t.arena, .{
            .name = @tagName(name),
            .args = try t.arena.dupe(ZigNode, args),
        });
    } else {
        return ZigTag.helper_ref.create(t.arena, @tagName(name));
    }
}

/// Cast a signed integer node to a usize, for use in pointer arithmetic. Negative numbers
/// will become very large positive numbers but that is ok since we only use this in
/// pointer arithmetic expressions, where wraparound will ensure we get the correct value.
/// node -> @as(usize, @bitCast(@as(isize, @intCast(node))))
fn usizeCastForWrappingPtrArithmetic(t: *Translator, node: ZigNode) TransError!ZigNode {
    const intcast_node = try ZigTag.as.create(t.arena, .{
        .lhs = try ZigTag.type.create(t.arena, "isize"),
        .rhs = try ZigTag.int_cast.create(t.arena, node),
    });

    return ZigTag.as.create(t.arena, .{
        .lhs = try ZigTag.type.create(t.arena, "usize"),
        .rhs = try ZigTag.bit_cast.create(t.arena, intcast_node),
    });
}

/// @typeInfo(@TypeOf(vec_node)).vector.<field>
fn vectorTypeInfo(t: *Translator, vec_node: ZigNode, field: []const u8) TransError!ZigNode {
    const typeof_call = try ZigTag.typeof.create(t.arena, vec_node);
    const typeinfo_call = try ZigTag.typeinfo.create(t.arena, typeof_call);
    const vector_type_info = try ZigTag.field_access.create(t.arena, .{ .lhs = typeinfo_call, .field_name = "vector" });
    return ZigTag.field_access.create(t.arena, .{ .lhs = vector_type_info, .field_name = field });
}

/// Returns true if the given array length qualifies as a flexible array member
/// under the current -fstrict-flex-arrays level.
fn isFlexibleArrayLen(t: *const Translator, len: anytype) bool {
    return switch (t.strict_flex_arrays) {
        .@"0" => true,
        .@"1" => switch (len) {
            .incomplete => true,
            .fixed => |n| n <= 1,
            else => false,
        },
        .@"2" => switch (len) {
            .incomplete => true,
            .fixed => |n| n == 0,
            else => false,
        },
        .@"3" => len == .incomplete,
    };
}

/// Build a getter function for a flexible array field in a C record
/// e.g. `T items[]` or `T items[0]`. The generated function returns a [*c] pointer
/// to the flexible array with the correct const and volatile qualifiers
fn createFlexibleMemberFn(
    t: *Translator,
    member_name: []const u8,
    field_name: []const u8,
) Error!ZigNode {
    // Use `_self` instead of the conventional `self` to avoid the Zig error
    // "function parameter shadows declaration of 'self'".
    // `processContainerMemberFns` merges C functions matching a struct's name
    // prefix into the struct as `pub const` aliases (e.g. `foo_self()` becomes
    // `pub const self = __root.foo_self`). A parameter also named `self` would
    // then shadow that declaration, which Zig rejects.
    const self_param_name = "_self";
    const self_param = try ZigTag.identifier.create(t.arena, self_param_name);
    const self_type = try ZigTag.typeof.create(t.arena, self_param);

    const fn_params = try t.arena.alloc(ast.Payload.Param, 1);
    fn_params[0] = .{
        .name = self_param_name,
        .type = ZigTag.@"anytype".init(),
        .is_noalias = false,
    };

    // @typeInfo(@TypeOf(self.*.<field_name>)).pointer.child
    const dereffed = try ZigTag.deref.create(t.arena, self_param);
    const field_access = try ZigTag.field_access.create(t.arena, .{ .lhs = dereffed, .field_name = field_name });
    const type_of = try ZigTag.typeof.create(t.arena, field_access);
    const type_info = try ZigTag.typeinfo.create(t.arena, type_of);
    const array_info = try ZigTag.field_access.create(t.arena, .{ .lhs = type_info, .field_name = "array" });
    const child_info = try ZigTag.field_access.create(t.arena, .{ .lhs = array_info, .field_name = "child" });

    const return_type = try t.createHelperCallNode(.FlexibleArrayType, &.{ self_type, child_info });

    // return @ptrCast(&self.*.<field_name>);
    const address_of = try ZigTag.address_of.create(t.arena, field_access);
    const aligned = try ZigTag.align_cast.create(t.arena, address_of);
    const casted = try ZigTag.ptr_cast.create(t.arena, aligned);
    const return_stmt = try ZigTag.@"return".create(t.arena, casted);
    const body = try ZigTag.block_single.create(t.arena, return_stmt);

    return ZigTag.func.create(t.arena, .{
        .is_pub = true,
        .is_extern = false,
        .is_export = false,
        .is_inline = false,
        .is_var_args = false,
        .name = member_name,
        .linksection_string = null,
        .explicit_callconv = null,
        .params = fn_params,
        .return_type = return_type,
        .body = body,
        .alignment = null,
    });
}

// =================
// Macro translation
// =================

fn transMacros(t: *Translator) !void {
    var tok_list: std.ArrayList(CToken) = .empty;
    defer tok_list.deinit(t.gpa);

    var pattern_list = try PatternList.init(t.gpa);
    defer pattern_list.deinit(t.gpa);

    for (t.pp.defines.keys(), t.pp.defines.values()) |name, macro| {
        if (macro.isBuiltin()) continue;
        if (t.global_scope.containsNow(name)) {
            continue;
        }

        tok_list.items.len = 0;
        try tok_list.ensureUnusedCapacity(t.gpa, macro.tokens.len);
        for (macro.tokens) |tok| {
            switch (tok.id) {
                .invalid => continue,
                .whitespace => continue,
                .comment => continue,
                .macro_ws => continue,
                else => {},
            }
            tok_list.appendAssumeCapacity(tok);
        }

        if (macro.is_func) {
            const ms: PatternList.MacroSlicer = .{
                .tokens = tok_list.items,
                .source = t.comp.getSource(macro.loc.id).buf,
                .params = @intCast(macro.params.len),
            };
            if (try pattern_list.match(ms)) |impl| {
                const decl = try ZigTag.pub_var_simple.create(t.arena, .{
                    .name = name,
                    .init = try t.createHelperCallNode(impl, null),
                });
                try t.addTopLevelDecl(name, decl);
                continue;
            }
        }

        if (t.checkTranslatableMacro(tok_list.items, macro.params)) |err| {
            switch (err) {
                .undefined_identifier => |ident| try t.failDeclExtra(&t.global_scope.base, macro.loc, name, "unable to translate macro: undefined identifier `{s}`", .{ident}),
                .invalid_arg_usage => |ident| try t.failDeclExtra(&t.global_scope.base, macro.loc, name, "unable to translate macro: untranslatable usage of arg `{s}`", .{ident}),
            }
            continue;
        }

        var macro_translator: MacroTranslator = .{
            .t = t,
            .tokens = tok_list.items,
            .source = t.comp.getSource(macro.loc.id).buf,
            .name = name,
            .macro = macro,
        };

        const res = if (macro.is_func)
            macro_translator.transFnMacro()
        else
            macro_translator.transMacro();
        res catch |err| switch (err) {
            error.ParseError => continue,
            error.OutOfMemory => |e| return e,
        };
    }
}

const MacroTranslateError = union(enum) {
    undefined_identifier: []const u8,
    invalid_arg_usage: []const u8,
};

fn checkTranslatableMacro(t: *Translator, tokens: []const CToken, params: []const []const u8) ?MacroTranslateError {
    var last_is_type_kw = false;
    var i: usize = 0;
    while (i < tokens.len) : (i += 1) {
        const token = tokens[i];
        switch (token.id) {
            .period, .arrow => i += 1, // skip next token since field identifiers can be unknown
            .keyword_struct, .keyword_union, .keyword_enum => if (!last_is_type_kw) {
                last_is_type_kw = true;
                continue;
            },
            .macro_param, .macro_param_no_expand => {
                if (last_is_type_kw) {
                    return .{ .invalid_arg_usage = params[token.end] };
                }
            },
            .identifier, .extended_identifier => {
                const identifier = t.pp.tokSlice(token);
                if (!t.global_scope.contains(identifier) and !builtins.map.has(identifier)) {
                    return .{ .undefined_identifier = identifier };
                }
            },
            else => {},
        }
        last_is_type_kw = false;
    }
    return null;
}

fn getContainer(t: *Translator, node: ZigNode) ?ZigNode {
    switch (node.tag()) {
        .@"union",
        .@"struct",
        .address_of,
        .bit_not,
        .not,
        .optional_type,
        .negate,
        .negate_wrap,
        .array_type,
        .c_pointer,
        .single_pointer,
        => return node,

        .identifier => {
            const ident = node.castTag(.identifier).?;
            if (t.global_scope.sym_table.get(ident.data)) |value| {
                if (value.castTag(.var_decl)) |var_decl|
                    return t.getContainer(var_decl.data.init.?);
                if (value.castTag(.var_simple) orelse value.castTag(.pub_var_simple)) |var_decl|
                    return t.getContainer(var_decl.data.init);
            }
        },

        .field_access => {
            const field_access = node.castTag(.field_access).?;

            if (t.getContainerTypeOf(field_access.data.lhs)) |ty_node| {
                if (ty_node.castTag(.@"struct") orelse ty_node.castTag(.@"union")) |container| {
                    for (container.data.fields) |field| {
                        if (mem.eql(u8, field.name, field_access.data.field_name)) {
                            return t.getContainer(field.type);
                        }
                    }
                }
            }
        },

        else => {},
    }
    return null;
}

fn getContainerTypeOf(t: *Translator, ref: ZigNode) ?ZigNode {
    if (ref.castTag(.identifier)) |ident| {
        if (t.global_scope.sym_table.get(ident.data)) |value| {
            if (value.castTag(.var_decl)) |var_decl| {
                return t.getContainer(var_decl.data.type);
            }
        }
    } else if (ref.castTag(.field_access)) |field_access| {
        if (t.getContainerTypeOf(field_access.data.lhs)) |ty_node| {
            if (ty_node.castTag(.@"struct") orelse ty_node.castTag(.@"union")) |container| {
                for (container.data.fields) |field| {
                    if (mem.eql(u8, field.name, field_access.data.field_name)) {
                        return t.getContainer(field.type);
                    }
                }
            } else return ty_node;
        }
    }
    return null;
}

pub fn getFnProto(t: *Translator, ref: ZigNode) ?*ast.Payload.Func {
    const init = if (ref.castTag(.var_decl)) |v|
        v.data.init orelse return null
    else if (ref.castTag(.var_simple) orelse ref.castTag(.pub_var_simple)) |v|
        v.data.init
    else
        return null;
    if (t.getContainerTypeOf(init)) |ty_node| {
        if (ty_node.castTag(.optional_type)) |prefix| {
            if (prefix.data.castTag(.single_pointer)) |sp| {
                if (sp.data.elem_type.castTag(.func)) |fn_proto| {
                    return fn_proto;
                }
            }
        }
    }
    return null;
}
