Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

zig.zig

Builds of the Zig compiler are distributed partly in source form. That source lives here. These APIs are provided as-is and have absolutely no API guarantees whatsoever.

File

lib/std/zig.zig

Code

//! Builds of the Zig compiler are distributed partly in source form. That
//! source lives here. These APIs are provided as-is and have absolutely no API
//! guarantees whatsoever.

const builtin = @import("builtin");

const std = @import("std.zig");
const assert = std.debug.assert;
const mem = std.mem;
const log = std.log;
const Allocator = std.mem.Allocator;
const Io = std.Io;
const Writer = std.Io.Writer;
const Cache = std.Build.Cache;
const fatal = std.process.fatal;
const Dir = std.Io.Dir;

const tokenizer = @import("zig/tokenizer.zig");

pub const ErrorBundle = @import("zig/ErrorBundle.zig");
pub const Server = @import("zig/Server.zig");
pub const Client = @import("zig/Client.zig");
pub const Token = tokenizer.Token;
pub const Tokenizer = tokenizer.Tokenizer;
pub const TokenSmith = @import("zig/TokenSmith.zig");
pub const string_literal = @import("zig/string_literal.zig");
pub const number_literal = @import("zig/number_literal.zig");
pub const primitives = @import("zig/primitives.zig");
pub const isPrimitive = primitives.isPrimitive;
pub const Ast = @import("zig/Ast.zig");
pub const AstGen = @import("zig/AstGen.zig");
pub const Zir = @import("zig/Zir.zig");
pub const Zoir = @import("zig/Zoir.zig");
pub const ZonGen = @import("zig/ZonGen.zig");
pub const system = @import("zig/system.zig");
pub const BuiltinFn = @import("zig/BuiltinFn.zig");
pub const AstRlAnnotate = @import("zig/AstRlAnnotate.zig");
pub const LibCInstallation = @import("zig/LibCInstallation.zig");
pub const WindowsSdk = @import("zig/WindowsSdk.zig");
pub const LibCDirs = @import("zig/LibCDirs.zig");
pub const PkgConfig = @import("zig/PkgConfig.zig");
pub const target = @import("zig/target.zig");
pub const llvm = @import("zig/llvm.zig");

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

// Character literal parsing
pub const ParsedCharLiteral = string_literal.ParsedCharLiteral;
pub const parseCharLiteral = string_literal.parseCharLiteral;
pub const parseNumberLiteral = number_literal.parseNumberLiteral;

pub const c_translation = struct {
    pub const builtins = @import("zig/c_translation/builtins.zig");
    pub const helpers = @import("zig/c_translation/helpers.zig");
};

pub const default_local_zig_cache_basename = ".zig-cache";
pub const build_zig_basename = "build.zig";

pub const SrcHasher = std.crypto.hash.Blake3;
pub const SrcHash = [16]u8;

pub const Color = enum {
    /// Auto-detect whether stream supports terminal colors.
    auto,
    /// Force-enable colors.
    off,
    /// Suppress colors.
    on,

    pub fn terminalMode(color: Color) ?Io.Terminal.Mode {
        return switch (color) {
            .auto => null,
            .on => .escape_codes,
            .off => .no_color,
        };
    }

    /// Determine the preference for color or no color based on the NO_COLOR and
    /// CLICOLOR_FORCE environment variables. Color is always disabled on WASI per
    /// https://github.com/WebAssembly/WASI/issues/162
    pub fn settingFromEnvironment(environ_map: *const std.process.Environ.Map) Color {
        return if (builtin.os.tag == .wasi or EnvVar.NO_COLOR.isSet(environ_map))
            .off
        else if (EnvVar.CLICOLOR_FORCE.isSet(environ_map))
            .on
        else
            .auto;
    }
};

/// There are many assumptions in the entire codebase that Zig source files can
/// be byte-indexed with a u32 integer.
pub const max_src_size = std.math.maxInt(u32);

pub fn hashSrc(src: []const u8) SrcHash {
    var out: SrcHash = undefined;
    SrcHasher.hash(src, &out, .{});
    return out;
}

pub fn srcHashEql(a: SrcHash, b: SrcHash) bool {
    return @as(u128, @bitCast(a)) == @as(u128, @bitCast(b));
}

pub fn hashName(parent_hash: SrcHash, sep: []const u8, name: []const u8) SrcHash {
    var out: SrcHash = undefined;
    var hasher = SrcHasher.init(.{});
    hasher.update(&parent_hash);
    hasher.update(sep);
    hasher.update(name);
    hasher.final(&out);
    return out;
}

pub const Loc = struct {
    line: usize,
    column: usize,
    /// Does not include the trailing newline.
    source_line: []const u8,

    pub fn eql(a: Loc, b: Loc) bool {
        return a.line == b.line and a.column == b.column and mem.eql(u8, a.source_line, b.source_line);
    }
};

pub fn findLineColumn(source: []const u8, byte_offset: usize) Loc {
    var line: usize = 0;
    var column: usize = 0;
    var line_start: usize = 0;
    var i: usize = 0;
    while (i < byte_offset) : (i += 1) {
        switch (source[i]) {
            '\n' => {
                line += 1;
                column = 0;
                line_start = i + 1;
            },
            else => {
                column += 1;
            },
        }
    }
    while (i < source.len and source[i] != '\n') {
        i += 1;
    }
    return .{
        .line = line,
        .column = column,
        .source_line = source[line_start..i],
    };
}

pub fn lineDelta(source: []const u8, start: usize, end: usize) isize {
    var line: isize = 0;
    if (end >= start) {
        for (source[start..end]) |byte| switch (byte) {
            '\n' => line += 1,
            else => continue,
        };
    } else {
        for (source[end..start]) |byte| switch (byte) {
            '\n' => line -= 1,
            else => continue,
        };
    }
    return line;
}

pub const BinNameOptions = struct {
    root_name: []const u8,
    cpu_arch: std.Target.Cpu.Arch,
    os_tag: std.Target.Os.Tag,
    ofmt: std.Target.ObjectFormat,
    abi: std.Target.Abi,
    output_mode: std.lang.OutputMode,
    link_mode: ?std.lang.LinkMode = null,
    version: ?std.SemanticVersion = null,
};

/// Returns the standard file system basename of a binary generated by the Zig compiler.
pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMemory}![]u8 {
    const root_name = options.root_name;
    switch (options.ofmt) {
        .coff => switch (options.output_mode) {
            .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{
                root_name,
                options.os_tag.exeFileExt(options.cpu_arch),
            }),
            .Lib => {
                const suffix = switch (options.link_mode orelse .static) {
                    .static => ".lib",
                    .dynamic => ".dll",
                };
                return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, suffix });
            },
            .Obj => return std.fmt.allocPrint(allocator, "{s}.obj", .{root_name}),
        },
        .elf => switch (options.output_mode) {
            .Exe => return allocator.dupe(u8, root_name),
            .Lib => {
                switch (options.link_mode orelse .static) {
                    .static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
                        options.os_tag.libPrefix(options.abi), root_name,
                    }),
                    .dynamic => {
                        if (options.version) |ver| {
                            return std.fmt.allocPrint(allocator, "{s}{s}.so.{d}.{d}.{d}", .{
                                options.os_tag.libPrefix(options.abi), root_name, ver.major, ver.minor, ver.patch,
                            });
                        } else {
                            return std.fmt.allocPrint(allocator, "{s}{s}.so", .{
                                options.os_tag.libPrefix(options.abi), root_name,
                            });
                        }
                    },
                }
            },
            .Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}),
        },
        .macho => switch (options.output_mode) {
            .Exe => return allocator.dupe(u8, root_name),
            .Lib => {
                switch (options.link_mode orelse .static) {
                    .static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
                        options.os_tag.libPrefix(options.abi), root_name,
                    }),
                    .dynamic => {
                        if (options.version) |ver| {
                            return std.fmt.allocPrint(allocator, "{s}{s}.{d}.{d}.{d}.dylib", .{
                                options.os_tag.libPrefix(options.abi), root_name, ver.major, ver.minor, ver.patch,
                            });
                        } else {
                            return std.fmt.allocPrint(allocator, "{s}{s}.dylib", .{
                                options.os_tag.libPrefix(options.abi), root_name,
                            });
                        }
                    },
                }
            },
            .Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}),
        },
        .wasm => switch (options.output_mode) {
            .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{
                root_name,
                options.os_tag.exeFileExt(options.cpu_arch),
            }),
            .Lib => {
                switch (options.link_mode orelse .static) {
                    .static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
                        options.os_tag.libPrefix(options.abi), root_name,
                    }),
                    .dynamic => return std.fmt.allocPrint(allocator, "{s}.wasm", .{root_name}),
                }
            },
            .Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}),
        },
        .c => return std.fmt.allocPrint(allocator, "{s}.c", .{root_name}),
        .spirv => return std.fmt.allocPrint(allocator, "{s}.spv", .{root_name}),
        .hex => return std.fmt.allocPrint(allocator, "{s}.ihex", .{root_name}),
        .raw => return std.fmt.allocPrint(allocator, "{s}.bin", .{root_name}),
        .plan9 => switch (options.output_mode) {
            .Exe => return allocator.dupe(u8, root_name),
            .Obj => return std.fmt.allocPrint(allocator, "{s}{s}", .{
                root_name, options.ofmt.fileExt(options.cpu_arch),
            }),
            .Lib => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{
                options.os_tag.libPrefix(options.abi), root_name,
            }),
        },
    }
}

pub const SanitizeC = enum {
    off,
    trap,
    full,
};

pub const BuildId = union(enum) {
    none,
    fast,
    uuid,
    sha1,
    md5,
    hexstring: HexString,

    pub fn eql(a: BuildId, b: BuildId) bool {
        const Tag = @typeInfo(BuildId).@"union".tag_type.?;
        const a_tag: Tag = a;
        const b_tag: Tag = b;
        if (a_tag != b_tag) return false;
        return switch (a) {
            .none, .fast, .uuid, .sha1, .md5 => true,
            .hexstring => |a_hexstring| mem.eql(u8, a_hexstring.toSlice(), b.hexstring.toSlice()),
        };
    }

    pub const HexString = struct {
        bytes: [32]u8,
        len: u8,

        /// Result is byte values, *not* hex-encoded.
        pub fn toSlice(hs: *const HexString) []const u8 {
            return hs.bytes[0..hs.len];
        }
    };

    /// Input is byte values, *not* hex-encoded.
    /// Asserts `bytes` fits inside `HexString`
    pub fn initHexString(bytes: []const u8) BuildId {
        var result: BuildId = .{ .hexstring = .{
            .bytes = undefined,
            .len = @intCast(bytes.len),
        } };
        @memcpy(result.hexstring.bytes[0..bytes.len], bytes);
        return result;
    }

    /// Converts UTF-8 text to a `BuildId`.
    pub fn parse(text: []const u8) !BuildId {
        if (mem.eql(u8, text, "none")) {
            return .none;
        } else if (mem.eql(u8, text, "fast")) {
            return .fast;
        } else if (mem.eql(u8, text, "uuid")) {
            return .uuid;
        } else if (mem.eql(u8, text, "sha1") or mem.eql(u8, text, "tree")) {
            return .sha1;
        } else if (mem.eql(u8, text, "md5")) {
            return .md5;
        } else if (mem.startsWith(u8, text, "0x")) {
            var result: BuildId = .{ .hexstring = undefined };
            const slice = try std.fmt.hexToBytes(&result.hexstring.bytes, text[2..]);
            result.hexstring.len = @as(u8, @intCast(slice.len));
            return result;
        }
        return error.InvalidBuildIdStyle;
    }

    test parse {
        try std.testing.expectEqual(BuildId.md5, try parse("md5"));
        try std.testing.expectEqual(BuildId.none, try parse("none"));
        try std.testing.expectEqual(BuildId.fast, try parse("fast"));
        try std.testing.expectEqual(BuildId.uuid, try parse("uuid"));
        try std.testing.expectEqual(BuildId.sha1, try parse("sha1"));
        try std.testing.expectEqual(BuildId.sha1, try parse("tree"));

        try std.testing.expect(BuildId.initHexString("").eql(try parse("0x")));
        try std.testing.expect(BuildId.initHexString("\x12\x34\x56").eql(try parse("0x123456")));
        try std.testing.expectError(error.InvalidLength, parse("0x12-34"));
        try std.testing.expectError(error.InvalidCharacter, parse("0xfoobbb"));
        try std.testing.expectError(error.InvalidBuildIdStyle, parse("yaddaxxx"));
    }

    pub fn format(id: BuildId, writer: *Writer) Writer.Error!void {
        switch (id) {
            .none, .fast, .uuid, .sha1, .md5 => {
                try writer.writeAll(@tagName(id));
            },
            .hexstring => |hs| {
                try writer.print("0x{x}", .{hs.toSlice()});
            },
        }
    }

    test format {
        try std.testing.expectFmt("none", "{f}", .{@as(BuildId, .none)});
        try std.testing.expectFmt("fast", "{f}", .{@as(BuildId, .fast)});
        try std.testing.expectFmt("uuid", "{f}", .{@as(BuildId, .uuid)});
        try std.testing.expectFmt("sha1", "{f}", .{@as(BuildId, .sha1)});
        try std.testing.expectFmt("md5", "{f}", .{@as(BuildId, .md5)});
        try std.testing.expectFmt("0x", "{f}", .{BuildId.initHexString("")});
        try std.testing.expectFmt("0x1234cdef", "{f}", .{BuildId.initHexString("\x12\x34\xcd\xef")});
    }
};

pub const LtoMode = enum { none, full, thin };

pub const Subsystem = enum {
    console,
    windows,
    posix,
    native,
    efi_application,
    efi_boot_service_driver,
    efi_rom,
    efi_runtime_driver,
};

pub const CompressDebugSections = enum(u2) { none, zlib, zstd };

pub const RcIncludes = enum(u2) {
    /// Use MSVC if available, fall back to MinGW.
    any,
    /// Use MSVC include paths (MSVC install + Windows SDK, must be present on the system).
    msvc,
    /// Use MinGW include paths (distributed with Zig).
    gnu,
    /// Do not use any autodetected include paths.
    none,
};

/// Renders a `std.Target.Cpu` value into a textual representation that can be parsed
/// via the `-mcpu` flag passed to the Zig compiler.
/// Appends the result to `buffer`.
pub fn serializeCpu(buffer: *std.array_list.Managed(u8), cpu: std.Target.Cpu) Allocator.Error!void {
    const all_features = cpu.arch.allFeaturesList();
    var populated_cpu_features = cpu.model.features;
    populated_cpu_features.populateDependencies(all_features);

    try buffer.appendSlice(cpu.model.name);

    if (populated_cpu_features.eql(cpu.features)) {
        // The CPU name alone is sufficient.
        return;
    }

    for (all_features, 0..) |feature, i_usize| {
        const i: std.Target.Cpu.Feature.Set.Index = @intCast(i_usize);
        const in_cpu_set = populated_cpu_features.isEnabled(i);
        const in_actual_set = cpu.features.isEnabled(i);
        try buffer.ensureUnusedCapacity(feature.name.len + 1);
        if (in_cpu_set and !in_actual_set) {
            buffer.appendAssumeCapacity('-');
            buffer.appendSliceAssumeCapacity(feature.name);
        } else if (!in_cpu_set and in_actual_set) {
            buffer.appendAssumeCapacity('+');
            buffer.appendSliceAssumeCapacity(feature.name);
        }
    }
}

pub fn serializeCpuAlloc(ally: Allocator, cpu: std.Target.Cpu) Allocator.Error![]u8 {
    var buffer = std.array_list.Managed(u8).init(ally);
    try serializeCpu(&buffer, cpu);
    return buffer.toOwnedSlice();
}

/// Return a Formatter for a Zig identifier, escaping it with `@""` syntax if needed.
///
/// See also `fmtIdFlags`.
pub fn fmtId(bytes: []const u8) FormatId {
    return .{ .bytes = bytes, .flags = .{} };
}

/// Return a Formatter for a Zig identifier, escaping it with `@""` syntax if needed.
///
/// See also `fmtId`.
pub fn fmtIdFlags(bytes: []const u8, flags: FormatId.Flags) FormatId {
    return .{ .bytes = bytes, .flags = flags };
}

pub fn fmtIdPU(bytes: []const u8) FormatId {
    return .{ .bytes = bytes, .flags = .{ .allow_primitive = true, .allow_underscore = true } };
}

pub fn fmtIdP(bytes: []const u8) FormatId {
    return .{ .bytes = bytes, .flags = .{ .allow_primitive = true } };
}

test fmtId {
    const expectFmt = std.testing.expectFmt;
    try expectFmt("@\"while\"", "{f}", .{fmtId("while")});
    try expectFmt("@\"while\"", "{f}", .{fmtIdFlags("while", .{ .allow_primitive = true })});
    try expectFmt("@\"while\"", "{f}", .{fmtIdFlags("while", .{ .allow_underscore = true })});
    try expectFmt("@\"while\"", "{f}", .{fmtIdFlags("while", .{ .allow_primitive = true, .allow_underscore = true })});

    try expectFmt("hello", "{f}", .{fmtId("hello")});
    try expectFmt("hello", "{f}", .{fmtIdFlags("hello", .{ .allow_primitive = true })});
    try expectFmt("hello", "{f}", .{fmtIdFlags("hello", .{ .allow_underscore = true })});
    try expectFmt("hello", "{f}", .{fmtIdFlags("hello", .{ .allow_primitive = true, .allow_underscore = true })});

    try expectFmt("@\"type\"", "{f}", .{fmtId("type")});
    try expectFmt("type", "{f}", .{fmtIdFlags("type", .{ .allow_primitive = true })});
    try expectFmt("@\"type\"", "{f}", .{fmtIdFlags("type", .{ .allow_underscore = true })});
    try expectFmt("type", "{f}", .{fmtIdFlags("type", .{ .allow_primitive = true, .allow_underscore = true })});

    try expectFmt("@\"_\"", "{f}", .{fmtId("_")});
    try expectFmt("@\"_\"", "{f}", .{fmtIdFlags("_", .{ .allow_primitive = true })});
    try expectFmt("_", "{f}", .{fmtIdFlags("_", .{ .allow_underscore = true })});
    try expectFmt("_", "{f}", .{fmtIdFlags("_", .{ .allow_primitive = true, .allow_underscore = true })});

    try expectFmt("@\"i123\"", "{f}", .{fmtId("i123")});
    try expectFmt("i123", "{f}", .{fmtIdFlags("i123", .{ .allow_primitive = true })});
    try expectFmt("@\"4four\"", "{f}", .{fmtId("4four")});
    try expectFmt("_underscore", "{f}", .{fmtId("_underscore")});
    try expectFmt("@\"11\\\"23\"", "{f}", .{fmtId("11\"23")});
    try expectFmt("@\"11\\x0f23\"", "{f}", .{fmtId("11\x0F23")});

    // These are technically not currently legal in Zig.
    try expectFmt("@\"\"", "{f}", .{fmtId("")});
    try expectFmt("@\"\\x00\"", "{f}", .{fmtId("\x00")});
}

pub const FormatId = struct {
    bytes: []const u8,
    flags: Flags,
    pub const Flags = struct {
        allow_primitive: bool = false,
        allow_underscore: bool = false,
    };

    /// Print the string as a Zig identifier, escaping it with `@""` syntax if needed.
    pub fn format(ctx: FormatId, writer: *Writer) Writer.Error!void {
        const bytes = ctx.bytes;
        if (isValidId(bytes) and
            (ctx.flags.allow_primitive or !isPrimitive(bytes)) and
            (ctx.flags.allow_underscore or !isUnderscore(bytes)))
        {
            return writer.writeAll(bytes);
        }
        try writer.writeAll("@\"");
        try stringEscape(bytes, writer);
        try writer.writeByte('"');
    }
};

/// Return a formatter for escaping a double quoted Zig string.
pub fn fmtString(bytes: []const u8) std.fmt.Alt([]const u8, stringEscape) {
    return .{ .data = bytes };
}

/// Return a formatter for escaping a single quoted Zig string.
pub fn fmtChar(c: u21) std.fmt.Alt(u21, charEscape) {
    return .{ .data = c };
}

test fmtString {
    try std.testing.expectFmt("\\x0f", "{f}", .{fmtString("\x0f")});
    try std.testing.expectFmt(
        \\" \\ hi \x07 \x11 \" derp '"
    , "\"{f}\"", .{fmtString(" \\ hi \x07 \x11 \" derp '")});
}

test fmtChar {
    try std.testing.expectFmt("c \\u{26a1}", "{f} {f}", .{ fmtChar('c'), fmtChar('âš¡') });
}

/// Print the string as escaped contents of a double quoted string.
pub fn stringEscape(bytes: []const u8, w: *Writer) Writer.Error!void {
    for (bytes) |byte| switch (byte) {
        '\n' => try w.writeAll("\\n"),
        '\r' => try w.writeAll("\\r"),
        '\t' => try w.writeAll("\\t"),
        '\\' => try w.writeAll("\\\\"),
        '"' => try w.writeAll("\\\""),
        '\'' => try w.writeByte('\''),
        ' ', '!', '#'...'&', '('...'[', ']'...'~' => try w.writeByte(byte),
        else => {
            try w.writeAll("\\x");
            try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' });
        },
    };
}

/// Print as escaped contents of a single-quoted string.
pub fn charEscape(codepoint: u21, w: *Writer) Writer.Error!void {
    switch (codepoint) {
        '\n' => try w.writeAll("\\n"),
        '\r' => try w.writeAll("\\r"),
        '\t' => try w.writeAll("\\t"),
        '\\' => try w.writeAll("\\\\"),
        '\'' => try w.writeAll("\\'"),
        '"', ' ', '!', '#'...'&', '('...'[', ']'...'~' => try w.writeByte(@intCast(codepoint)),
        else => {
            if (std.math.cast(u8, codepoint)) |byte| {
                try w.writeAll("\\x");
                try w.printInt(byte, 16, .lower, .{ .width = 2, .fill = '0' });
            } else {
                try w.writeAll("\\u{");
                try w.printInt(codepoint, 16, .lower, .{});
                try w.writeByte('}');
            }
        },
    }
}

pub fn isValidId(bytes: []const u8) bool {
    if (bytes.len == 0) return false;
    for (bytes, 0..) |c, i| {
        switch (c) {
            '_', 'a'...'z', 'A'...'Z' => {},
            '0'...'9' => if (i == 0) return false,
            else => return false,
        }
    }
    return Token.getKeyword(bytes) == null;
}

test isValidId {
    try std.testing.expect(!isValidId(""));
    try std.testing.expect(isValidId("foobar"));
    try std.testing.expect(!isValidId("a b c"));
    try std.testing.expect(!isValidId("3d"));
    try std.testing.expect(!isValidId("enum"));
    try std.testing.expect(isValidId("i386"));
}

pub fn isUnderscore(bytes: []const u8) bool {
    return bytes.len == 1 and bytes[0] == '_';
}

test isUnderscore {
    try std.testing.expect(isUnderscore("_"));
    try std.testing.expect(!isUnderscore("__"));
    try std.testing.expect(!isUnderscore("_foo"));
    try std.testing.expect(isUnderscore("\x5f"));
    try std.testing.expect(!isUnderscore("\\x5f"));
}

/// If the source can be UTF-16LE encoded, this function asserts that `gpa`
/// will align a byte-sized allocation to at least 2. Allocators that don't do
/// this are rare.
pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *Io.File.Reader) ![:0]u8 {
    var buffer: std.ArrayList(u8) = .empty;
    defer buffer.deinit(gpa);

    if (file_reader.getSize()) |size| {
        const casted_size = std.math.cast(u32, size) orelse return error.StreamTooLong;
        // +1 to avoid resizing for the null byte added in toOwnedSliceSentinel below.
        try buffer.ensureTotalCapacityPrecise(gpa, casted_size + 1);
    } else |_| {}

    try file_reader.interface.appendRemaining(gpa, &buffer, .limited(max_src_size));

    // Detect unsupported file types with their Byte Order Mark
    const unsupported_boms = [_][]const u8{
        "\xff\xfe\x00\x00", // UTF-32 little endian
        "\xfe\xff\x00\x00", // UTF-32 big endian
        "\xfe\xff", // UTF-16 big endian
    };
    for (unsupported_boms) |bom| {
        if (mem.startsWith(u8, buffer.items, bom)) {
            return error.UnsupportedEncoding;
        }
    }

    // If the file starts with a UTF-16 little endian BOM, translate it to UTF-8
    if (mem.startsWith(u8, buffer.items, "\xff\xfe")) {
        if (buffer.items.len % 2 != 0) return error.InvalidEncoding;
        return std.unicode.utf16LeToUtf8AllocZ(gpa, @ptrCast(@alignCast(buffer.items))) catch |err| switch (err) {
            error.DanglingSurrogateHalf => error.UnsupportedEncoding,
            error.ExpectedSecondSurrogateHalf => error.UnsupportedEncoding,
            error.UnexpectedSecondSurrogateHalf => error.UnsupportedEncoding,
            else => |e| return e,
        };
    }

    return buffer.toOwnedSliceSentinel(gpa, 0);
}

pub fn printAstErrorsToStderr(gpa: Allocator, io: Io, tree: Ast, path: []const u8, color: Color) !void {
    var wip_errors: ErrorBundle.Wip = undefined;
    try wip_errors.init(gpa);
    defer wip_errors.deinit();

    try putAstErrorsIntoBundle(gpa, tree, path, &wip_errors);

    var error_bundle = try wip_errors.toOwnedBundle("");
    defer error_bundle.deinit(gpa);
    return error_bundle.renderToStderr(io, .{}, color);
}

pub fn putAstErrorsIntoBundle(
    gpa: Allocator,
    tree: Ast,
    path: []const u8,
    wip_errors: *ErrorBundle.Wip,
) Allocator.Error!void {
    switch (tree.mode) {
        .zig => {
            var zir = try AstGen.generate(gpa, tree);
            defer zir.deinit(gpa);

            try wip_errors.addZirErrorMessages(zir, tree, tree.source, path);
        },
        .zon => {
            var zoir = try ZonGen.generate(gpa, tree, .{});
            defer zoir.deinit(gpa);

            try wip_errors.addZoirErrorMessages(zoir, tree, tree.source, path);
        },
    }
}

pub fn resolveTargetQueryOrFatal(io: Io, target_query: std.Target.Query) std.Target {
    return system.resolveTargetQuery(io, target_query) catch |err|
        std.process.fatal("unable to resolve target: {t}", .{err});
}

pub fn parseTargetQueryOrReportFatalError(
    allocator: Allocator,
    opts: std.Target.Query.ParseOptions,
) std.Target.Query {
    var opts_with_diags = opts;
    var diags: std.Target.Query.ParseOptions.Diagnostics = .{};
    if (opts_with_diags.diagnostics == null) {
        opts_with_diags.diagnostics = &diags;
    }
    return std.Target.Query.parse(opts_with_diags) catch |err| switch (err) {
        error.UnknownCpuModel => {
            help: {
                var help_text = std.array_list.Managed(u8).init(allocator);
                defer help_text.deinit();
                for (diags.arch.?.allCpuModels()) |cpu| {
                    help_text.print(" {s}\n", .{cpu.name}) catch break :help;
                }
                log.info("available CPUs for architecture '{s}':\n{s}", .{
                    @tagName(diags.arch.?), help_text.items,
                });
            }
            std.process.fatal("unknown CPU: '{s}'", .{diags.cpu_name.?});
        },
        error.UnknownCpuFeature => {
            help: {
                var help_text = std.array_list.Managed(u8).init(allocator);
                defer help_text.deinit();
                for (diags.arch.?.allFeaturesList()) |feature| {
                    help_text.print(" {s}: {s}\n", .{ feature.name, feature.description }) catch break :help;
                }
                log.info("available CPU features for architecture '{s}':\n{s}", .{
                    @tagName(diags.arch.?), help_text.items,
                });
            }
            std.process.fatal("unknown CPU feature: '{s}'", .{diags.unknown_feature_name.?});
        },
        error.UnknownObjectFormat => {
            help: {
                var help_text = std.array_list.Managed(u8).init(allocator);
                defer help_text.deinit();
                inline for (@typeInfo(std.Target.ObjectFormat).@"enum".field_names) |field_name| {
                    help_text.print(" {s}\n", .{field_name}) catch break :help;
                }
                log.info("available object formats:\n{s}", .{help_text.items});
            }
            std.process.fatal("unknown object format: '{s}'", .{opts.object_format.?});
        },
        error.UnknownArchitecture => {
            help: {
                var help_text = std.array_list.Managed(u8).init(allocator);
                defer help_text.deinit();
                inline for (@typeInfo(std.Target.Cpu.Arch).@"enum".field_names) |field_name| {
                    help_text.print(" {s}\n", .{field_name}) catch break :help;
                }
                log.info("available architectures:\n{s} native\n", .{help_text.items});
            }
            std.process.fatal("unknown architecture: '{s}'", .{diags.unknown_architecture_name.?});
        },
        else => |e| std.process.fatal("unable to parse target query '{s}': {s}", .{
            opts.arch_os_abi, @errorName(e),
        }),
    };
}

/// Collects all the environment variables that Zig could possibly inspect, so
/// that we can do reflection on this and print them with `zig env`.
pub const EnvVar = enum {
    ZIG_GLOBAL_CACHE_DIR,
    ZIG_LOCAL_CACHE_DIR,
    ZIG_LOCAL_PKG_DIR,
    ZIG_LIB_DIR,
    ZIG_LIBC,
    ZIG_BUILD_ERROR_STYLE,
    ZIG_BUILD_MULTILINE_ERRORS,
    ZIG_VERBOSE_LINK,
    ZIG_VERBOSE_CC,
    ZIG_VERBOSE_CMD,
    ZIG_DEBUG_CMD,
    ZIG_DEBUG_MAKER,
    ZIG_IS_DETECTING_LIBC_PATHS,
    ZIG_IS_AVOIDING_CALLING_ITSELF,

    // C toolchain integration
    NIX_CFLAGS_COMPILE,
    NIX_CFLAGS_LINK,
    NIX_LDFLAGS,
    C_INCLUDE_PATH,
    CPLUS_INCLUDE_PATH,
    LIBRARY_PATH,
    CC,
    PKG_CONFIG,

    // Terminal integration
    NO_COLOR,
    CLICOLOR_FORCE,

    // Debug info integration
    XDG_CACHE_HOME,
    LOCALAPPDATA,
    HOME,

    // Windows SDK integration
    PROGRAMDATA,

    // Homebrew integration
    HOMEBREW_PREFIX,

    pub fn isSet(ev: EnvVar, map: *const std.process.Environ.Map) bool {
        return map.contains(@tagName(ev));
    }

    pub fn get(ev: EnvVar, map: *const std.process.Environ.Map) ?[]const u8 {
        return map.get(@tagName(ev));
    }
};

pub const SimpleComptimeReason = enum(u32) {
    // Evaluating at comptime because a builtin operand must be comptime-known.
    // These messages all mention a specific builtin.
    operand_setEvalBranchQuota,
    operand_setFloatMode,
    operand_branchHint,
    operand_setRuntimeSafety,
    operand_embedFile,
    operand_shuffle_mask,
    operand_atomicRmw_operation,
    operand_reduce_operation,

    // Evaluating at comptime because an operand must be comptime-known.
    // These messages do not mention a specific builtin (and may not be about a builtin at all).
    export_target,
    export_options,
    extern_options,
    prefetch_options,
    call_modifier,
    compile_error_string,
    inline_assembly_code,
    atomic_order,
    slice_cat_operand,
    inline_call_target,
    generic_call_target,
    wasm_memory_index,
    work_group_dim_index,
    clobber,

    // Evaluating at comptime because types must be comptime-known.
    // Reasons other than `.type` are just more specific messages.
    type,
    int_signedness,
    int_bit_width,
    array_sentinel,
    array_length,
    pointer_size,
    pointer_attrs,
    pointer_sentinel,
    slice_sentinel,
    vector_length,
    fn_ret_ty,
    fn_param_types,
    fn_param_attrs,
    fn_attrs,
    struct_layout,
    struct_field_names,
    struct_field_types,
    struct_field_attrs,
    union_layout,
    union_field_names,
    union_field_types,
    union_field_attrs,
    tuple_field_types,
    enum_field_names,
    enum_field_values,
    union_enum_tag_type,
    enum_int_tag_type,
    packed_struct_backing_int_type,
    packed_union_backing_int_type,

    // Evaluating at comptime because decl/field name must be comptime-known.
    decl_name,
    field_name,
    tuple_field_index,

    // Evaluating at comptime because it is an attribute of a global declaration.
    container_var_init,
    @"callconv",
    @"align",
    @"addrspace",
    @"linksection",

    // Miscellaneous reasons.
    comptime_keyword,
    comptime_call_modifier,
    inline_loop_operand,
    switch_item,
    tuple_field_default_value,
    struct_field_default_value,
    enum_field_tag_value,
    slice_single_item_ptr_bounds,
    stored_to_comptime_field,
    stored_to_comptime_var,
    casted_to_comptime_int,
    casted_to_comptime_float,
    std_lang_decl,

    pub fn message(r: SimpleComptimeReason) []const u8 {
        return switch (r) {
            // zig fmt: off
            .operand_setEvalBranchQuota  => "operand to '@setEvalBranchQuota' must be comptime-known",
            .operand_setFloatMode        => "operand to '@setFloatMode' must be comptime-known",
            .operand_branchHint          => "operand to '@branchHint' must be comptime-known",
            .operand_setRuntimeSafety    => "operand to '@setRuntimeSafety' must be comptime-known",
            .operand_embedFile           => "operand to '@embedFile' must be comptime-known",
            .operand_shuffle_mask        => "'@shuffle' mask must be comptime-known",
            .operand_atomicRmw_operation => "'@atomicRmw' operation must be comptime-known",
            .operand_reduce_operation    => "'@reduce' operation must be comptime-known",

            .export_target        => "export target must be comptime-known",
            .export_options       => "export options must be comptime-known",
            .extern_options       => "extern options must be comptime-known",
            .prefetch_options     => "prefetch options must be comptime-known",
            .call_modifier        => "call modifier must be comptime-known",
            .compile_error_string => "compile error string must be comptime-known",
            .inline_assembly_code => "inline assembly code must be comptime-known",
            .atomic_order         => "atomic order must be comptime-known",
            .slice_cat_operand    => "slice being concatenated must be comptime-known",
            .inline_call_target   => "function being called inline must be comptime-known",
            .generic_call_target  => "generic function being called must be comptime-known",
            .wasm_memory_index    => "wasm memory index must be comptime-known",
            .work_group_dim_index => "work group dimension index must be comptime-known",
            .clobber              => "clobber must be comptime-known",

            .type                => "types must be comptime-known",
            .int_signedness      => "integer signedness must be comptime-known",
            .int_bit_width       => "integer bit width must be comptime-known",
            .array_sentinel      => "array sentinel value must be comptime-known",
            .array_length        => "array length must be comptime-known",
            .pointer_size        => "pointer size must be comptime-known",
            .pointer_attrs       => "pointer attributes must be comptime-known",
            .pointer_sentinel    => "pointer sentinel value must be comptime-known",
            .slice_sentinel      => "slice sentinel value must be comptime-known",
            .vector_length       => "vector length must be comptime-known",
            .fn_ret_ty           => "function return type must be comptime-known",
            .fn_param_types      => "function parameter types must be comptime-known",
            .fn_param_attrs      => "function parameter attributes must be comptime-known",
            .fn_attrs            => "function attributes must be comptime-known",
            .struct_layout       => "struct layout must be comptime-known",
            .struct_field_names  => "struct field names must be comptime-known",
            .struct_field_types  => "struct field types must be comptime-known",
            .struct_field_attrs  => "struct field attributes must be comptime-known",
            .union_layout        => "union layout must be comptime-known",
            .union_field_names   => "union field names must be comptime-known",
            .union_field_types   => "union field types must be comptime-known",
            .union_field_attrs   => "union field attributes must be comptime-known",
            .tuple_field_types   => "tuple field types must be comptime-known",
            .enum_field_names    => "enum field names must be comptime-known",
            .enum_field_values   => "enum field values must be comptime-known",

            .union_enum_tag_type            => "enum tag type of union must be comptime-known",
            .enum_int_tag_type              => "integer tag type of enum must be comptime-known",
            .packed_struct_backing_int_type => "packed struct backing integer type must be comptime-known",
            .packed_union_backing_int_type  => "packed struct backing integer type must be comptime-known",

            .decl_name         => "declaration name must be comptime-known",
            .field_name        => "field name must be comptime-known",
            .tuple_field_index => "tuple field index must be comptime-known",

            .container_var_init => "initializer of container-level variable must be comptime-known",
            .@"callconv"        => "calling convention must be comptime-known",
            .@"align"           => "alignment must be comptime-known",
            .@"addrspace"       => "address space must be comptime-known",
            .@"linksection"     => "linksection must be comptime-known",

            .comptime_keyword             => "'comptime' keyword forces comptime evaluation",
            .comptime_call_modifier       => "'.compile_time' call modifier forces comptime evaluation",
            .inline_loop_operand          => "inline loop condition must be comptime-known",
            .switch_item                  => "switch prong values must be comptime-known",
            .tuple_field_default_value    => "tuple field default value must be comptime-known",
            .struct_field_default_value   => "struct field default value must be comptime-known",
            .enum_field_tag_value         => "enum field tag value must be comptime-known",
            .slice_single_item_ptr_bounds => "slice of single-item pointer must have comptime-known bounds",
            .stored_to_comptime_field     => "value stored to a comptime field must be comptime-known",
            .stored_to_comptime_var       => "value stored to a comptime variable must be comptime-known",
            .casted_to_comptime_int       => "value casted to 'comptime_int' must be comptime-known",
            .casted_to_comptime_float     => "value casted to 'comptime_float' must be comptime-known",
            .std_lang_decl                => "'std.lang' declaration values must be comptime-known",
            // zig fmt: on
        };
    }
};

/// Every kind of artifact which the compiler can emit.
pub const EmitArtifact = enum {
    bin,
    @"asm",
    implib,
    llvm_ir,
    llvm_bc,
    docs,
    pdb,
    h,

    /// If using `Server` to communicate with the compiler, it will place requested artifacts in
    /// paths under the output directory, where those paths are named according to this function.
    /// Returned string is allocated with `gpa` and owned by the caller.
    pub fn cacheName(ea: EmitArtifact, gpa: Allocator, opts: BinNameOptions) Allocator.Error![]const u8 {
        const suffix: []const u8 = switch (ea) {
            .bin => return binNameAlloc(gpa, opts),
            .@"asm" => ".s",
            .implib => ".lib",
            .llvm_ir => ".ll",
            .llvm_bc => ".bc",
            .docs => "-docs",
            .pdb => ".pdb",
            .h => ".h",
        };
        return std.fmt.allocPrint(gpa, "{s}{s}", .{ opts.root_name, suffix });
    }
};

/// The defaults are chosen here to reduce the size of src/clang_options.zon
pub const ClangCliParam = struct {
    name: []const u8,
    ze: ZigEquivalent = .other,
    syntax: Syntax = .flag,
    /// Prefixed by "-"
    pd1: bool = true,
    /// Prefixed by "--"
    pd2: bool = false,
    /// Prefixed by "/"
    psl: bool = false,

    pub const Syntax = union(enum) {
        /// A flag with no values.
        flag,
        /// An option which prefixes its (single) value.
        joined,
        /// An option which is followed by its value.
        separate,
        /// An option which is either joined to its (non-empty) value, or followed by its value.
        joined_or_separate,
        /// An option which is both joined to its (first) value, and followed by its (second) value.
        joined_and_separate,
        /// An option followed by its values, which are separated by commas.
        comma_joined,
        /// An option which consumes an optional joined argument and any other remaining arguments.
        remaining_args_joined,
        /// An option which is which takes multiple (separate) arguments.
        multi_arg: u8,
    };

    pub const ZigEquivalent = enum {
        target,
        o,
        c,
        r,
        m,
        x,
        other,
        positional,
        l,
        ignore,
        driver_punt,
        pic,
        no_pic,
        pie,
        no_pie,
        lto,
        no_lto,
        unwind_tables,
        no_unwind_tables,
        asynchronous_unwind_tables,
        no_asynchronous_unwind_tables,
        nostdlib,
        nostdlib_cpp,
        shared,
        rdynamic,
        wl,
        wp,
        preprocess_only,
        asm_only,
        optimize,
        debug,
        gdwarf32,
        gdwarf64,
        sanitize,
        no_sanitize,
        sanitize_trap,
        no_sanitize_trap,
        linker_script,
        dry_run,
        verbose,
        for_linker,
        linker_input_z,
        lib_dir,
        mcpu,
        dep_file,
        dep_file_to_stdout,
        framework_dir,
        framework,
        nostdlibinc,
        red_zone,
        no_red_zone,
        omit_frame_pointer,
        no_omit_frame_pointer,
        function_sections,
        no_function_sections,
        data_sections,
        no_data_sections,
        builtin,
        no_builtin,
        color_diagnostics,
        no_color_diagnostics,
        stack_check,
        no_stack_check,
        stack_protector,
        no_stack_protector,
        strip,
        exec_model,
        emit_llvm,
        sysroot,
        entry,
        force_undefined_symbol,
        weak_library,
        weak_framework,
        headerpad_max_install_names,
        compress_debug_sections,
        install_name,
        undefined,
        force_load_objc,
        mingw_unicode_entry_point,
        san_cov_trace_pc_guard,
        san_cov,
        no_san_cov,
        rtlib,
        static,
        dynamic,
        version,
    };

    pub fn matchEql(self: @This(), arg: []const u8) u2 {
        if (self.pd1 and arg.len >= self.name.len + 1 and
            mem.startsWith(u8, arg, "-") and mem.eql(u8, arg[1..], self.name))
        {
            return 1;
        }
        if (self.pd2 and arg.len >= self.name.len + 2 and
            mem.startsWith(u8, arg, "--") and mem.eql(u8, arg[2..], self.name))
        {
            return 2;
        }
        if (self.psl and arg.len >= self.name.len + 1 and
            mem.startsWith(u8, arg, "/") and mem.eql(u8, arg[1..], self.name))
        {
            return 1;
        }
        return 0;
    }

    pub fn matchStartsWith(self: @This(), arg: []const u8) usize {
        if (self.pd1 and arg.len >= self.name.len + 1 and
            mem.startsWith(u8, arg, "-") and mem.startsWith(u8, arg[1..], self.name))
        {
            return self.name.len + 1;
        }
        if (self.pd2 and arg.len >= self.name.len + 2 and
            mem.startsWith(u8, arg, "--") and mem.startsWith(u8, arg[2..], self.name))
        {
            return self.name.len + 2;
        }
        if (self.psl and arg.len >= self.name.len + 1 and
            mem.startsWith(u8, arg, "/") and mem.startsWith(u8, arg[1..], self.name))
        {
            return self.name.len + 1;
        }
        return 0;
    }
};

/// Deprecated
pub const AllocPrintCmdOptions = struct {
    cwd: ?[]const u8 = null,
    parent_env: ?*const std.process.Environ.Map = null,
    child_env: ?*const std.process.Environ.Map = null,
};

/// Deprecated
pub fn allocPrintCmd(gpa: Allocator, argv: []const []const u8, options: AllocPrintCmdOptions) Allocator.Error![]u8 {
    var aw: Io.Writer.Allocating = .init(gpa);
    defer aw.deinit();
    SubprocessCommand.format(.{
        .argv = argv,
        .cwd = options.cwd,
        .parent_env = options.parent_env,
        .child_env = options.child_env,
    }, &aw.writer) catch return error.OutOfMemory;
    return aw.toOwnedSlice();
}

fn shellEscape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
    for (string) |c| {
        if (switch (c) {
            else => true,
            '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
            '=' => is_argv0,
        }) break;
    } else return writer.writeAll(string);

    try writer.writeByte('"');
    for (string) |c| {
        if (switch (c) {
            std.ascii.control_code.nul => break,
            '!', '"', '$', '\\', '`' => true,
            else => !std.ascii.isPrint(c),
        }) try writer.writeByte('\\');
        switch (c) {
            std.ascii.control_code.nul => unreachable,
            std.ascii.control_code.bel => try writer.writeByte('a'),
            std.ascii.control_code.bs => try writer.writeByte('b'),
            std.ascii.control_code.ht => try writer.writeByte('t'),
            std.ascii.control_code.lf => try writer.writeByte('n'),
            std.ascii.control_code.vt => try writer.writeByte('v'),
            std.ascii.control_code.ff => try writer.writeByte('f'),
            std.ascii.control_code.cr => try writer.writeByte('r'),
            std.ascii.control_code.esc => try writer.writeByte('E'),
            ' '...'~' => try writer.writeByte(c),
            else => try writer.print("{o:0>3}", .{c}),
        }
    }
    try writer.writeByte('"');
}

pub const SubprocessCommand = struct {
    argv: []const []const u8,
    cwd: ?[]const u8 = null,
    parent_env: ?*const std.process.Environ.Map = null,
    child_env: ?*const std.process.Environ.Map = null,

    pub fn format(sc: SubprocessCommand, w: *Io.Writer) Io.Writer.Error!void {
        if (sc.cwd) |path| {
            try w.print("cd {s} && ", .{path});
        }
        if (sc.child_env) |child_env| {
            for (child_env.keys(), child_env.values()) |key, value| {
                if (sc.parent_env) |parent_env| {
                    if (parent_env.get(key)) |process_value| {
                        if (mem.eql(u8, value, process_value)) continue;
                    }
                }
                try w.print("{s}=", .{key});
                try shellEscape(w, value, false);
                try w.writeByte(' ');
            }
        }
        try shellEscape(w, sc.argv[0], true);
        for (sc.argv[1..]) |arg| {
            try w.writeByte(' ');
            try shellEscape(w, arg, false);
        }
    }
};

/// Like `std.process.currentPathAlloc`, but also resolves the path with `Dir.path.resolve`. This
/// means the path has no repeated separators, no "." or ".." components, and no trailing separator.
/// On WASI, "" is returned instead of ".".
pub fn getResolvedCwd(io: Io, gpa: Allocator) std.process.CurrentPathAllocError![]u8 {
    if (builtin.os.tag == .wasi) {
        if (std.debug.runtime_safety) {
            const cwd = try std.process.currentPathAlloc(io, gpa);
            defer gpa.free(cwd);
            assert(mem.eql(u8, cwd, "."));
        }
        return "";
    }
    const cwd = try std.process.currentPathAlloc(io, gpa);
    defer gpa.free(cwd);
    const resolved = try Dir.path.resolve(gpa, &.{cwd});
    assert(Dir.path.isAbsolute(resolved));
    return resolved;
}

pub const Directories = struct {
    /// The string returned by `introspect.getResolvedCwd`. This is typically an absolute path,
    /// but on WASI is the empty string "" instead, because WASI does not have absolute paths.
    cwd: []const u8,
    /// The Zig 'lib' directory.
    /// `zig_lib.path` is resolved (`resolvePath`) or `null` for cwd.
    /// Guaranteed to be a different path from `global_cache` and `local_cache`.
    zig_lib: Cache.Directory,
    /// The global Zig cache directory.
    /// `global_cache.path` is resolved (`resolvePath`) or `null` for cwd.
    global_cache: Cache.Directory,
    /// The local Zig cache directory.
    /// `local_cache.path` is resolved (`resolvePath`) or `null` for cwd.
    /// This may be the same as `global_cache`.
    local_cache: Cache.Directory,

    pub fn deinit(dirs: *Directories, io: Io) void {
        // The local and global caches could be the same.
        const close_local = dirs.local_cache.handle.handle != dirs.global_cache.handle.handle;

        dirs.global_cache.handle.close(io);
        if (close_local) dirs.local_cache.handle.close(io);
        dirs.zig_lib.handle.close(io);
    }

    /// Returns a `Directories` where `local_cache` is replaced with `global_cache`, intended for
    /// use by sub-compilations (e.g. compiler_rt). Do not `deinit` the returned `Directories`; it
    /// shares handles with `dirs`.
    pub fn withoutLocalCache(dirs: Directories) Directories {
        return .{
            .cwd = dirs.cwd,
            .zig_lib = dirs.zig_lib,
            .global_cache = dirs.global_cache,
            .local_cache = dirs.global_cache,
        };
    }

    const LocalCacheStrategy = union(enum) {
        override: []const u8,
        search,
        global,
    };

    /// Uses `std.process.fatal` on error conditions.
    pub fn init(
        arena: Allocator,
        io: Io,
        override_zig_lib: ?[]const u8,
        override_global_cache: ?[]const u8,
        local_cache_strat: LocalCacheStrategy,
        preopens: std.process.Preopens,
        self_exe_path: switch (builtin.target.os.tag) {
            .wasi => void,
            else => []const u8,
        },
        environ_map: *const std.process.Environ.Map,
        cwd: []const u8,
    ) Directories {
        const wasi = builtin.target.os.tag == .wasi;

        const zig_lib: Cache.Directory = d: {
            if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib");
            if (wasi) break :d getPreopen(preopens, "/lib");
            break :d findZigLibDirFromSelfExe(arena, io, cwd, self_exe_path) catch |err| {
                fatal("unable to find zig installation directory from executable path {q}: {t}", .{ self_exe_path, err });
            };
        };

        const global_cache: Cache.Directory = d: {
            if (override_global_cache) |path| break :d openUnresolved(arena, io, cwd, path, .@"global cache");
            if (wasi) break :d getPreopen(preopens, "/cache");
            const path = resolveGlobalCacheDir(arena, environ_map) catch |err| {
                fatal("unable to resolve zig cache directory: {t}", .{err});
            };
            break :d openUnresolved(arena, io, cwd, path, .@"global cache");
        };

        const local_cache = getLocalCacheDirectory(arena, io, cwd, global_cache, local_cache_strat);

        if (mem.eql(u8, zig_lib.path orelse "", global_cache.path orelse "")) {
            fatal("zig lib directory '{f}' cannot be equal to global cache directory '{f}'", .{ zig_lib, global_cache });
        }
        if (mem.eql(u8, zig_lib.path orelse "", local_cache.path orelse "")) {
            fatal("zig lib directory '{f}' cannot be equal to local cache directory '{f}'", .{ zig_lib, local_cache });
        }

        return .{
            .cwd = cwd,
            .zig_lib = zig_lib,
            .global_cache = global_cache,
            .local_cache = local_cache,
        };
    }

    fn getLocalCacheDirectory(
        arena: Allocator,
        io: Io,
        cwd: []const u8,
        global_cache: Cache.Directory,
        local_cache_strat: LocalCacheStrategy,
    ) Cache.Directory {
        return switch (local_cache_strat) {
            .override => |path| openUnresolved(arena, io, cwd, path, .@"local cache"),
            .search => d: {
                const maybe_path = resolveSuitableLocalCacheDir(arena, io, cwd) catch |err|
                    fatal("unable to resolve zig cache directory: {t}", .{err});
                const path = maybe_path orelse break :d global_cache;
                break :d openUnresolved(arena, io, cwd, path, .@"local cache");
            },
            .global => global_cache,
        };
    }

    fn getPreopen(preopens: std.process.Preopens, name: []const u8) Cache.Directory {
        return .{
            .path = if (mem.eql(u8, name, ".")) null else name,
            .handle = switch (preopens.get(name) orelse fatal("preopen not found: {q}", .{name})) {
                .file => fatal("preopen {q} is not a directory", .{name}),
                .dir => |d| d,
            },
        };
    }
    pub fn openUnresolved(
        arena: Allocator,
        io: Io,
        cwd: []const u8,
        unresolved_path: []const u8,
        thing: enum { @"zig lib", @"global cache", @"local cache" },
    ) Cache.Directory {
        const path = resolvePath(arena, cwd, &.{unresolved_path}) catch |err| {
            fatal("unable to resolve {t} directory: {t}", .{ thing, err });
        };
        const nonempty_path = if (path.len == 0) "." else path;
        const handle_or_err = switch (thing) {
            .@"zig lib" => Dir.cwd().openDir(io, nonempty_path, .{}),
            .@"global cache", .@"local cache" => Dir.cwd().createDirPathOpen(io, nonempty_path, .{}),
        };
        return .{
            .path = if (path.len == 0) null else path,
            .handle = handle_or_err catch |err| {
                const extra_str: []const u8 = e: {
                    if (thing == .@"global cache") switch (err) {
                        error.AccessDenied, error.ReadOnlyFileSystem => break :e "\n" ++
                            "If this location is not writable then consider specifying an alternative with " ++
                            "the ZIG_GLOBAL_CACHE_DIR environment variable or the --global-cache-dir option.",
                        else => {},
                    };
                    break :e "";
                };
                fatal("unable to open {t} directory {q}: {t}{s}", .{ thing, nonempty_path, err, extra_str });
            },
        };
    }
};

/// Both the directory handle and the path are newly allocated resources which the caller now owns.
pub fn findZigLibDir(gpa: Allocator, io: Io) !Cache.Directory {
    const cwd_path = try getResolvedCwd(io, gpa);
    defer gpa.free(cwd_path);
    const self_exe_path = try std.process.executablePathAlloc(io, gpa);
    defer gpa.free(self_exe_path);

    return findZigLibDirFromSelfExe(gpa, io, cwd_path, self_exe_path);
}

/// Both the directory handle and the path are newly allocated resources which the caller now owns.
pub fn findZigLibDirFromSelfExe(
    allocator: Allocator,
    io: Io,
    /// The return value of `getResolvedCwd`.
    /// Passed as an argument to avoid pointlessly repeating the call.
    cwd_path: []const u8,
    self_exe_path: []const u8,
) error{ OutOfMemory, FileNotFound }!Cache.Directory {
    const cwd = Dir.cwd();
    var cur_path: []const u8 = self_exe_path;
    while (Dir.path.dirname(cur_path)) |dirname| : (cur_path = dirname) {
        var base_dir = cwd.openDir(io, dirname, .{}) catch continue;
        defer base_dir.close(io);

        const sub_directory = testZigInstallPrefix(io, base_dir) orelse continue;
        const p = try Dir.path.join(allocator, &.{ dirname, sub_directory.path.? });
        defer allocator.free(p);

        const resolved = try resolvePath(allocator, cwd_path, &.{p});
        return .{
            .handle = sub_directory.handle,
            .path = if (resolved.len == 0) null else resolved,
        };
    }
    return error.FileNotFound;
}

/// Returns the sub_path that worked, or `null` if none did.
/// The path of the returned Directory is relative to `base`.
/// The handle of the returned Directory is open.
fn testZigInstallPrefix(io: Io, base_dir: Dir) ?Cache.Directory {
    const test_index_file = "std" ++ Dir.path.sep_str ++ "std.zig";

    zig_dir: {
        // Try lib/zig/std/std.zig
        const lib_zig = "lib" ++ Dir.path.sep_str ++ "zig";
        var test_zig_dir = base_dir.openDir(io, lib_zig, .{}) catch break :zig_dir;
        const file = test_zig_dir.openFile(io, test_index_file, .{}) catch {
            test_zig_dir.close(io);
            break :zig_dir;
        };
        file.close(io);
        return .{ .handle = test_zig_dir, .path = lib_zig };
    }

    // Try lib/std/std.zig
    var test_zig_dir = base_dir.openDir(io, "lib", .{}) catch return null;
    const file = test_zig_dir.openFile(io, test_index_file, .{}) catch {
        test_zig_dir.close(io);
        return null;
    };
    file.close(io);
    return .{ .handle = test_zig_dir, .path = "lib" };
}

pub fn resolveGlobalCacheDir(arena: Allocator, environ_map: *const std.process.Environ.Map) ![]const u8 {
    if (EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map)) |value| return value;

    const app_name = "zig";

    switch (builtin.os.tag) {
        .wasi => @compileError("on WASI the global cache dir must be resolved with preopens"),
        .windows => {
            const local_app_data_dir = EnvVar.LOCALAPPDATA.get(environ_map) orelse
                return error.AppDataDirUnavailable;
            return Dir.path.join(arena, &.{ local_app_data_dir, app_name });
        },
        else => {
            if (EnvVar.XDG_CACHE_HOME.get(environ_map)) |cache_root| {
                if (cache_root.len > 0) {
                    return Dir.path.join(arena, &.{ cache_root, app_name });
                }
            }
            if (EnvVar.HOME.get(environ_map)) |home| {
                if (home.len > 0) {
                    return Dir.path.join(arena, &.{ home, ".cache", app_name });
                }
            }
            return error.AppDataDirUnavailable;
        },
    }
}

/// Searches upwards from `cwd` for a directory containing a `build.zig` file.
/// If such a directory is found, returns the path to it joined to the `.zig_cache` name.
/// Otherwise, returns `null`, indicating no suitable local cache location.
pub fn resolveSuitableLocalCacheDir(arena: Allocator, io: Io, cwd: []const u8) Allocator.Error!?[]u8 {
    var cur_dir = cwd;
    while (true) {
        const joined = try Dir.path.join(arena, &.{ cur_dir, build_zig_basename });
        if (Dir.cwd().access(io, joined, .{})) |_| {
            return try Dir.path.join(arena, &.{ cur_dir, default_local_zig_cache_basename });
        } else |err| switch (err) {
            error.FileNotFound => {
                cur_dir = Dir.path.dirname(cur_dir) orelse return null;
                continue;
            },
            else => return null,
        }
    }
}

/// Similar to `Dir.path.resolve`, but converts to a cwd-relative path, or, if that would
/// start with a relative up-dir (".."), an absolute path based on the cwd. Also, the cwd
/// returns the empty string ("") instead of ".".
pub fn resolvePath(
    gpa: Allocator,
    /// The return value of `getResolvedCwd`.
    /// Passed as an argument to avoid pointlessly repeating the call.
    cwd_resolved: []const u8,
    paths: []const []const u8,
) Allocator.Error![]u8 {
    if (builtin.target.os.tag == .wasi) {
        assert(mem.eql(u8, cwd_resolved, ""));
        const res = try Dir.path.resolve(gpa, paths);
        if (mem.eql(u8, res, ".")) {
            gpa.free(res);
            return "";
        }
        return res;
    }

    // Heuristic for a fast path: if no component is absolute and ".." never appears, we just need to resolve `paths`.
    for (paths) |p| {
        if (Dir.path.isAbsolute(p)) break; // absolute path
        if (mem.indexOf(u8, p, "..") != null) break; // may contain up-dir
    } else {
        // no absolute path, no "..".
        const res = try Dir.path.resolve(gpa, paths);
        if (mem.eql(u8, res, ".")) {
            gpa.free(res);
            return "";
        }
        assert(!Dir.path.isAbsolute(res));
        assert(!isUpDir(res));
        return res;
    }

    // The fast path failed; resolve the whole thing.
    // Optimization: `paths` often has just one element.
    const path_resolved = switch (paths.len) {
        0 => unreachable,
        1 => try Dir.path.resolve(gpa, &.{ cwd_resolved, paths[0] }),
        else => r: {
            const all_paths = try gpa.alloc([]const u8, paths.len + 1);
            defer gpa.free(all_paths);
            all_paths[0] = cwd_resolved;
            @memcpy(all_paths[1..], paths);
            break :r try Dir.path.resolve(gpa, all_paths);
        },
    };
    errdefer gpa.free(path_resolved);

    assert(Dir.path.isAbsolute(path_resolved));
    assert(Dir.path.isAbsolute(cwd_resolved));

    if (!mem.startsWith(u8, path_resolved, cwd_resolved)) return path_resolved; // not in cwd
    if (path_resolved.len == cwd_resolved.len) {
        // equal to cwd
        gpa.free(path_resolved);
        return "";
    }
    if (path_resolved[cwd_resolved.len] != Dir.path.sep) return path_resolved; // not in cwd (last component differs)

    // in cwd; extract sub path
    const sub_path = try gpa.dupe(u8, path_resolved[cwd_resolved.len + 1 ..]);
    gpa.free(path_resolved);
    return sub_path;
}

pub fn isUpDir(p: []const u8) bool {
    return mem.startsWith(u8, p, "..") and (p.len == 2 or p[2] == Dir.path.sep);
}

pub const BuildExeSubprocessOptions = struct {
    argv: []const []const u8,
    cache_root: Cache.Directory,
    root_name: []const u8,

    environ_map: ?*std.process.Environ.Map = null,
    cache_manifest: ?*Cache.Manifest = null,
    arch_os_abi: ?[]const u8 = null,
    cpu_features: ?[]const u8 = null,
    progress_node: std.Progress.Node = .none,
    skip_log_cmdline_on_compile_errors: bool = false,
};

pub const BuildExeSubprocessError = error{
    /// Error message has been logged.
    AlreadyReported,
    /// Error message has been logged, and source files added to the `Cache.Manifest`.
    FailedButCacheIntact,
} || Io.Cancelable || Allocator.Error;

pub const BuildExeSubprocessResult = struct {
    received_fs_inputs: bool,
    cache_hit: bool,
    path: Cache.Path,
};

/// Assumes `argv` has `--listen=-` in it and the child process is `zig build-exe`.
///
/// Result path is allocated via gpa.
pub fn buildExeSubprocess(
    gpa: Allocator,
    io: Io,
    options: BuildExeSubprocessOptions,
) BuildExeSubprocessError!BuildExeSubprocessResult {
    const cmd: SubprocessCommand = .{ .argv = options.argv };

    var child = std.process.spawn(io, .{
        .argv = options.argv,
        .environ_map = options.environ_map,
        .stdin = .pipe,
        .stdout = .pipe,
        .stderr = .pipe,
        .progress_node = options.progress_node,
    }) catch |err| {
        log.err("spawning command {t}: {f}", .{ err, cmd });
        return error.AlreadyReported;
    };
    defer child.kill(io);

    var stderr_task = io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited }) catch
        @panic("TODO use multireader instead");
    defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};

    var stdout_buffer: [512]u8 = undefined;
    var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
    const stdout = &stdout_reader.interface;

    {
        var w = child.stdin.?.writer(io, &.{});
        w.interface.writeStruct(Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
            error.WriteFailed => {
                log.err("{t} writing to command: {f}", .{ w.err.?, cmd });
                return error.AlreadyReported;
            },
        };
        w.interface.writeStruct(Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
            error.WriteFailed => {
                log.err("{t} writing to command: {f}", .{ w.err.?, cmd });
                return error.AlreadyReported;
            },
        };
    }

    const Header = Server.Message.Header;

    var result: ?Cache.Path = null;
    defer if (result) |r| gpa.free(r.sub_path);

    var result_error_bundle: ErrorBundle = .empty;
    defer result_error_bundle.deinit(gpa);

    var body_buffer: std.ArrayList(u8) = .empty;
    defer body_buffer.deinit(gpa);

    var received_fs_inputs = false;
    var cache_hit = false;

    while (true) {
        const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
            error.ReadFailed => {
                log.err("{t} reading from command: {f}", .{ stdout_reader.err.?, cmd });
                return error.AlreadyReported;
            },
            error.EndOfStream => break,
        };
        body_buffer.clearRetainingCapacity();
        stdout.appendExact(gpa, &body_buffer, header.bytes_len) catch |err| switch (err) {
            error.ReadFailed => {
                log.err("{t} reading from command: {f}", .{ stdout_reader.err.?, cmd });
                return error.AlreadyReported;
            },
            error.OutOfMemory => |e| return e,
            error.EndOfStream => {
                log.err("unexpected end of stream from command: {f}", .{cmd});
                return error.AlreadyReported;
            },
        };
        const body = body_buffer.items;

        switch (header.tag) {
            .zig_version => {
                if (!mem.eql(u8, builtin.zig_version_string, body)) {
                    log.err("zig protocol version mismatch from command: {f}", .{cmd});
                    return error.AlreadyReported;
                }
            },
            .error_bundle => {
                result_error_bundle.deinit(gpa);
                result_error_bundle = Server.allocErrorBundle(gpa, body) catch |err| switch (err) {
                    error.EndOfStream => break,
                    else => |e| return e,
                };
            },
            .emit_digest => {
                const EmitDigest = Server.Message.EmitDigest;
                const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
                cache_hit = ebp_hdr.flags.cache_hit;
                const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
                if (result) |r| gpa.free(r.sub_path);
                result = .{
                    .root_dir = options.cache_root,
                    .sub_path = try Dir.path.join(gpa, &.{ "o", &Cache.binToHex(digest.*) }),
                };
            },
            .file_system_inputs => if (options.cache_manifest) |man| {
                received_fs_inputs = true;
                var it = mem.splitScalar(u8, body, 0);
                while (it.next()) |prefixed_path| {
                    const prefix: Server.Message.PathPrefix = @fromBackingInt(@intCast(prefixed_path[0] - 1));
                    const sub_path = try gpa.dupe(u8, prefixed_path[1..]);
                    var keep = false;
                    defer if (!keep) gpa.free(sub_path);
                    keep = man.addPrefixedPathPost(.{
                        .prefix = @backingInt(prefix),
                        .sub_path = sub_path,
                    }) catch |err| switch (err) {
                        error.Canceled, error.OutOfMemory => |e| return e,
                        else => |e| {
                            log.err("adding {t} {s} to cache failed: {t}", .{ prefix, sub_path, e });
                            return error.AlreadyReported;
                        },
                    };
                }
            },
            else => {}, // ignore other messages
        }
    }

    const stderr_contents = stderr_task.await(io) catch |err| switch (err) {
        error.Canceled, error.OutOfMemory => |e| return e,
        else => |e| c: {
            log.warn("{t} reading stderr from command: {f}", .{ e, cmd });
            break :c "";
        },
    };
    if (stderr_contents.len > 0)
        log.warn("unexpected stderr from {s} command:\n{s}", .{ options.argv[0], stderr_contents });

    // Send EOF to stdin.
    child.stdin.?.close(io);
    child.stdin = null;

    const term = child.wait(io) catch |err| switch (err) {
        error.Canceled => |e| return e,
        else => |e| {
            log.err("{t} waiting for command: {f}", .{ e, cmd });
            return error.AlreadyReported;
        },
    };

    if (!term.success()) {
        log.err("command {f}: {f}", .{ term, cmd });
        if (received_fs_inputs) return error.FailedButCacheIntact;
        return error.AlreadyReported;
    }

    if (result_error_bundle.errorMessageCount() > 0) {
        result_error_bundle.renderToStderr(io, .{}, .auto) catch |err| switch (err) {
            error.Canceled => |e| return e,
            else => |e| {
                log.err("failed rendering error bundle: {t}", .{e});
                return error.AlreadyReported;
            },
        };
        if (!options.skip_log_cmdline_on_compile_errors) log.err("command reported {d} compilation errors: {f}", .{
            result_error_bundle.errorMessageCount(), cmd,
        });
        if (received_fs_inputs) return error.FailedButCacheIntact;
        return error.AlreadyReported;
    }

    const base_path = result orelse {
        log.err("command failed to report result: {f}", .{cmd});
        return error.AlreadyReported;
    };
    const parsed_target = system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
        .arch_os_abi = options.arch_os_abi orelse "native",
        .cpu_features = options.cpu_features,
    }) catch unreachable) catch unreachable;
    const bin_name = try binNameAlloc(gpa, .{
        .root_name = options.root_name,
        .cpu_arch = parsed_target.cpu.arch,
        .os_tag = parsed_target.os.tag,
        .ofmt = parsed_target.ofmt,
        .abi = parsed_target.abi,
        .output_mode = .Exe,
    });
    defer gpa.free(bin_name);
    return .{
        .received_fs_inputs = received_fs_inputs,
        .cache_hit = cache_hit,
        .path = try base_path.join(gpa, bin_name),
    };
}

fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
    var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
    return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
        error.ReadFailed => return file_reader.err.?,
        else => |e| return e,
    };
}

test {
    _ = Ast;
    _ = AstRlAnnotate;
    _ = BuiltinFn;
    _ = Client;
    _ = ErrorBundle;
    _ = LibCDirs;
    _ = LibCInstallation;
    _ = Server;
    _ = TokenSmith;
    _ = WindowsSdk;
    _ = number_literal;
    _ = primitives;
    _ = string_literal;
    _ = system;
    _ = target;
    _ = c_translation;
    _ = llvm;
    _ = @import("zig/parser_fuzz.zig");
}