zig/lib/std / zig/ZonGen.zig

Ingests an Ast and produces a Zoir.

//! Ingests an `Ast` and produces a `Zoir`.

Options

When false, string literals are not parsed. string_literal nodes will contain empty strings, and errors that normally occur during string parsing will not be raised. parseStrLit and strLitSizeHint may be used to parse string literals after the fact.


const std = @import("std");
const assert = std.debug.assert;
const mem = std.mem;
const Allocator = mem.Allocator;
const StringIndexAdapter = std.hash_map.StringIndexAdapter;
const StringIndexContext = std.hash_map.StringIndexContext;
const ZonGen = @This();
const Zoir = @import("Zoir.zig");
const Ast = @import("Ast.zig");
const Writer = std.io.Writer;

generate()

Estimates the size of a string node without parsing it.


gpa: Allocator,

strLitSizeHint()

Parses the given node as a string literal.

tree: Ast,

parseStrLit()


options: Options,

nodes: std.MultiArrayList(Zoir.Node.Repr),
extra: std.ArrayListUnmanaged(u32),
limbs: std.ArrayListUnmanaged(std.math.big.Limb),
string_bytes: std.ArrayListUnmanaged(u8),
string_table: std.HashMapUnmanaged(u32, void, StringIndexContext, std.hash_map.default_max_load_percentage),

compile_errors: std.ArrayListUnmanaged(Zoir.CompileError),
error_notes: std.ArrayListUnmanaged(Zoir.CompileError.Note),

pub const Options = struct {
    /// When false, string literals are not parsed. `string_literal` nodes will contain empty
    /// strings, and errors that normally occur during string parsing will not be raised.
    ///
    /// `parseStrLit` and `strLitSizeHint` may be used to parse string literals after the fact.
    parse_str_lits: bool = true,
};

pub fn generate(gpa: Allocator, tree: Ast, options: Options) Allocator.Error!Zoir {
    assert(tree.mode == .zon);

    var zg: ZonGen = .{
        .gpa = gpa,
        .tree = tree,
        .options = options,
        .nodes = .empty,
        .extra = .empty,
        .limbs = .empty,
        .string_bytes = .empty,
        .string_table = .empty,
        .compile_errors = .empty,
        .error_notes = .empty,
    };
    defer {
        zg.nodes.deinit(gpa);
        zg.extra.deinit(gpa);
        zg.limbs.deinit(gpa);
        zg.string_bytes.deinit(gpa);
        zg.string_table.deinit(gpa);
        zg.compile_errors.deinit(gpa);
        zg.error_notes.deinit(gpa);
    }

    if (tree.errors.len == 0) {
        const root_ast_node = tree.rootDecls()[0];
        try zg.nodes.append(gpa, undefined); // index 0; root node
        try zg.expr(root_ast_node, .root);
    } else {
        try zg.lowerAstErrors();
    }

    if (zg.compile_errors.items.len > 0) {
        const string_bytes = try zg.string_bytes.toOwnedSlice(gpa);
        errdefer gpa.free(string_bytes);
        const compile_errors = try zg.compile_errors.toOwnedSlice(gpa);
        errdefer gpa.free(compile_errors);
        const error_notes = try zg.error_notes.toOwnedSlice(gpa);
        errdefer gpa.free(error_notes);

        return .{
            .nodes = .empty,
            .extra = &.{},
            .limbs = &.{},
            .string_bytes = string_bytes,
            .compile_errors = compile_errors,
            .error_notes = error_notes,
        };
    } else {
        assert(zg.error_notes.items.len == 0);

        var nodes = zg.nodes.toOwnedSlice();
        errdefer nodes.deinit(gpa);
        const extra = try zg.extra.toOwnedSlice(gpa);
        errdefer gpa.free(extra);
        const limbs = try zg.limbs.toOwnedSlice(gpa);
        errdefer gpa.free(limbs);
        const string_bytes = try zg.string_bytes.toOwnedSlice(gpa);
        errdefer gpa.free(string_bytes);

        return .{
            .nodes = nodes,
            .extra = extra,
            .limbs = limbs,
            .string_bytes = string_bytes,
            .compile_errors = &.{},
            .error_notes = &.{},
        };
    }
}

fn expr(zg: *ZonGen, node: Ast.Node.Index, dest_node: Zoir.Node.Index) Allocator.Error!void {
    const gpa = zg.gpa;
    const tree = zg.tree;

    switch (tree.nodeTag(node)) {
        .root => unreachable,
        .test_decl => unreachable,
        .container_field_init => unreachable,
        .container_field_align => unreachable,
        .container_field => unreachable,
        .fn_decl => unreachable,
        .global_var_decl => unreachable,
        .local_var_decl => unreachable,
        .simple_var_decl => unreachable,
        .aligned_var_decl => unreachable,
        .@"defer" => unreachable,
        .@"errdefer" => unreachable,
        .switch_case => unreachable,
        .switch_case_inline => unreachable,
        .switch_case_one => unreachable,
        .switch_case_inline_one => unreachable,
        .switch_range => unreachable,
        .asm_output => unreachable,
        .asm_input => unreachable,
        .for_range => unreachable,
        .assign => unreachable,
        .assign_destructure => unreachable,
        .assign_shl => unreachable,
        .assign_shl_sat => unreachable,
        .assign_shr => unreachable,
        .assign_bit_and => unreachable,
        .assign_bit_or => unreachable,
        .assign_bit_xor => unreachable,
        .assign_div => unreachable,
        .assign_sub => unreachable,
        .assign_sub_wrap => unreachable,
        .assign_sub_sat => unreachable,
        .assign_mod => unreachable,
        .assign_add => unreachable,
        .assign_add_wrap => unreachable,
        .assign_add_sat => unreachable,
        .assign_mul => unreachable,
        .assign_mul_wrap => unreachable,
        .assign_mul_sat => unreachable,

        .shl,
        .shr,
        .add,
        .add_wrap,
        .add_sat,
        .sub,
        .sub_wrap,
        .sub_sat,
        .mul,
        .mul_wrap,
        .mul_sat,
        .div,
        .mod,
        .shl_sat,
        .bit_and,
        .bit_or,
        .bit_xor,
        .bang_equal,
        .equal_equal,
        .greater_than,
        .greater_or_equal,
        .less_than,
        .less_or_equal,
        .array_cat,
        .array_mult,
        .bool_and,
        .bool_or,
        .bool_not,
        .bit_not,
        .negation_wrap,
        => try zg.addErrorTok(tree.nodeMainToken(node), "operator '{s}' is not allowed in ZON", .{tree.tokenSlice(tree.nodeMainToken(node))}),

        .error_union,
        .merge_error_sets,
        .optional_type,
        .anyframe_literal,
        .anyframe_type,
        .ptr_type_aligned,
        .ptr_type_sentinel,
        .ptr_type,
        .ptr_type_bit_range,
        .container_decl,
        .container_decl_trailing,
        .container_decl_arg,
        .container_decl_arg_trailing,
        .container_decl_two,
        .container_decl_two_trailing,
        .tagged_union,
        .tagged_union_trailing,
        .tagged_union_enum_tag,
        .tagged_union_enum_tag_trailing,
        .tagged_union_two,
        .tagged_union_two_trailing,
        .array_type,
        .array_type_sentinel,
        .error_set_decl,
        .fn_proto_simple,
        .fn_proto_multi,
        .fn_proto_one,
        .fn_proto,
        => try zg.addErrorNode(node, "types are not available in ZON", .{}),

        .call_one,
        .call_one_comma,
        .call,
        .call_comma,
        .@"return",
        .if_simple,
        .@"if",
        .while_simple,
        .while_cont,
        .@"while",
        .for_simple,
        .@"for",
        .@"catch",
        .@"orelse",
        .@"break",
        .@"continue",
        .@"switch",
        .switch_comma,
        .@"nosuspend",
        .@"suspend",
        .@"resume",
        .@"try",
        .unreachable_literal,
        => try zg.addErrorNode(node, "control flow is not allowed in ZON", .{}),

        .@"comptime" => try zg.addErrorNode(node, "keyword 'comptime' is not allowed in ZON", .{}),
        .asm_simple, .@"asm", .asm_legacy => try zg.addErrorNode(node, "inline asm is not allowed in ZON", .{}),

        .builtin_call_two,
        .builtin_call_two_comma,
        .builtin_call,
        .builtin_call_comma,
        => try zg.addErrorNode(node, "builtin function calls are not allowed in ZON", .{}),

        .field_access => try zg.addErrorNode(node, "field accesses are not allowed in ZON", .{}),

        .slice_open,
        .slice,
        .slice_sentinel,
        => try zg.addErrorNode(node, "slice operator is not allowed in ZON", .{}),

        .deref, .address_of => try zg.addErrorTok(tree.nodeMainToken(node), "pointers are not available in ZON", .{}),
        .unwrap_optional => try zg.addErrorTok(tree.nodeMainToken(node), "optionals are not available in ZON", .{}),
        .error_value => try zg.addErrorNode(node, "errors are not available in ZON", .{}),

        .array_access => try zg.addErrorNode(node, "array indexing is not allowed in ZON", .{}),

        .block_two,
        .block_two_semicolon,
        .block,
        .block_semicolon,
        => {
            var buffer: [2]Ast.Node.Index = undefined;
            const statements = tree.blockStatements(&buffer, node).?;
            if (statements.len == 0) {
                try zg.addErrorNodeNotes(node, "void literals are not available in ZON", .{}, &.{
                    try zg.errNoteNode(node, "void union payloads can be represented by enum literals", .{}),
                });
            } else {
                try zg.addErrorNode(node, "blocks are not allowed in ZON", .{});
            }
        },

        .array_init_one,
        .array_init_one_comma,
        .array_init,
        .array_init_comma,
        .struct_init_one,
        .struct_init_one_comma,
        .struct_init,
        .struct_init_comma,
        => {
            var buf: [2]Ast.Node.Index = undefined;

            const type_node = if (tree.fullArrayInit(&buf, node)) |full|
                full.ast.type_expr.unwrap().?
            else if (tree.fullStructInit(&buf, node)) |full|
                full.ast.type_expr.unwrap().?
            else
                unreachable;

            try zg.addErrorNodeNotes(type_node, "types are not available in ZON", .{}, &.{
                try zg.errNoteNode(type_node, "replace the type with '.'", .{}),
            });
        },

        .grouped_expression => {
            try zg.addErrorTokNotes(tree.nodeMainToken(node), "expression grouping is not allowed in ZON", .{}, &.{
                try zg.errNoteTok(tree.nodeMainToken(node), "these parentheses are always redundant", .{}),
            });
            return zg.expr(tree.nodeData(node).node_and_token[0], dest_node);
        },

        .negation => {
            const child_node = tree.nodeData(node).node;
            switch (tree.nodeTag(child_node)) {
                .number_literal => return zg.numberLiteral(child_node, node, dest_node, .negative),
                .identifier => {
                    const child_ident = tree.tokenSlice(tree.nodeMainToken(child_node));
                    if (mem.eql(u8, child_ident, "inf")) {
                        zg.setNode(dest_node, .{
                            .tag = .neg_inf,
                            .data = 0, // ignored
                            .ast_node = node,
                        });
                        return;
                    }
                },
                else => {},
            }
            try zg.addErrorTok(tree.nodeMainToken(node), "expected number or 'inf' after '-'", .{});
        },
        .number_literal => try zg.numberLiteral(node, node, dest_node, .positive),
        .char_literal => try zg.charLiteral(node, dest_node),

        .identifier => try zg.identifier(node, dest_node),

        .enum_literal => {
            const str_index = zg.identAsString(tree.nodeMainToken(node)) catch |err| switch (err) {
                error.BadString => undefined, // doesn't matter, there's an error
                error.OutOfMemory => |e| return e,
            };
            zg.setNode(dest_node, .{
                .tag = .enum_literal,
                .data = @intFromEnum(str_index),
                .ast_node = node,
            });
        },
        .string_literal, .multiline_string_literal => if (zg.strLitAsString(node)) |result| switch (result) {
            .nts => |nts| zg.setNode(dest_node, .{
                .tag = .string_literal_null,
                .data = @intFromEnum(nts),
                .ast_node = node,
            }),
            .slice => |slice| {
                const extra_index: u32 = @intCast(zg.extra.items.len);
                try zg.extra.appendSlice(zg.gpa, &.{ slice.start, slice.len });
                zg.setNode(dest_node, .{
                    .tag = .string_literal,
                    .data = extra_index,
                    .ast_node = node,
                });
            },
        } else |err| switch (err) {
            error.BadString => {},
            error.OutOfMemory => |e| return e,
        },

        .array_init_dot_two,
        .array_init_dot_two_comma,
        .array_init_dot,
        .array_init_dot_comma,
        => {
            var buf: [2]Ast.Node.Index = undefined;
            const full = tree.fullArrayInit(&buf, node).?;
            assert(full.ast.elements.len != 0); // Otherwise it would be a struct init
            assert(full.ast.type_expr == .none); // The tag was `array_init_dot_*`

            const first_elem: u32 = @intCast(zg.nodes.len);
            try zg.nodes.resize(gpa, zg.nodes.len + full.ast.elements.len);

            const extra_index: u32 = @intCast(zg.extra.items.len);
            try zg.extra.appendSlice(gpa, &.{
                @intCast(full.ast.elements.len),
                first_elem,
            });

            zg.setNode(dest_node, .{
                .tag = .array_literal,
                .data = extra_index,
                .ast_node = node,
            });

            for (full.ast.elements, first_elem..) |elem_node, elem_dest_node| {
                try zg.expr(elem_node, @enumFromInt(elem_dest_node));
            }
        },

        .struct_init_dot_two,
        .struct_init_dot_two_comma,
        .struct_init_dot,
        .struct_init_dot_comma,
        => {
            var buf: [2]Ast.Node.Index = undefined;
            const full = tree.fullStructInit(&buf, node).?;
            assert(full.ast.type_expr == .none); // The tag was `struct_init_dot_*`

            if (full.ast.fields.len == 0) {
                zg.setNode(dest_node, .{
                    .tag = .empty_literal,
                    .data = 0, // ignored
                    .ast_node = node,
                });
                return;
            }

            const first_elem: u32 = @intCast(zg.nodes.len);
            try zg.nodes.resize(gpa, zg.nodes.len + full.ast.fields.len);

            const extra_index: u32 = @intCast(zg.extra.items.len);
            try zg.extra.ensureUnusedCapacity(gpa, 2 + full.ast.fields.len);
            zg.extra.appendSliceAssumeCapacity(&.{
                @intCast(full.ast.fields.len),
                first_elem,
            });
            const names_start = extra_index + 2;
            zg.extra.appendNTimesAssumeCapacity(undefined, full.ast.fields.len);

            zg.setNode(dest_node, .{
                .tag = .struct_literal,
                .data = extra_index,
                .ast_node = node,
            });

            // For short initializers, track the names on the stack rather than going through gpa.
            var sfba_state = std.heap.stackFallback(256, gpa);
            const sfba = sfba_state.get();
            var field_names: std.AutoHashMapUnmanaged(Zoir.NullTerminatedString, Ast.TokenIndex) = .empty;
            defer field_names.deinit(sfba);

            var reported_any_duplicate = false;

            for (full.ast.fields, names_start.., first_elem..) |elem_node, extra_name_idx, elem_dest_node| {
                const name_token = tree.firstToken(elem_node) - 2;
                if (zg.identAsString(name_token)) |name_str| {
                    zg.extra.items[extra_name_idx] = @intFromEnum(name_str);
                    const gop = try field_names.getOrPut(sfba, name_str);
                    if (gop.found_existing and !reported_any_duplicate) {
                        reported_any_duplicate = true;
                        const earlier_token = gop.value_ptr.*;
                        try zg.addErrorTokNotes(earlier_token, "duplicate struct field name", .{}, &.{
                            try zg.errNoteTok(name_token, "duplicate name here", .{}),
                        });
                    }
                    gop.value_ptr.* = name_token;
                } else |err| switch (err) {
                    error.BadString => {}, // there's an error, so it's fine to not populate `zg.extra`
                    error.OutOfMemory => |e| return e,
                }
                try zg.expr(elem_node, @enumFromInt(elem_dest_node));
            }
        },
    }
}

fn appendIdentStr(zg: *ZonGen, ident_token: Ast.TokenIndex) error{ OutOfMemory, BadString }!u32 {
    const gpa = zg.gpa;
    const tree = zg.tree;
    assert(tree.tokenTag(ident_token) == .identifier);
    const ident_name = tree.tokenSlice(ident_token);
    if (!mem.startsWith(u8, ident_name, "@")) {
        const start = zg.string_bytes.items.len;
        try zg.string_bytes.appendSlice(gpa, ident_name);
        return @intCast(start);
    }
    const offset = 1;
    const start: u32 = @intCast(zg.string_bytes.items.len);
    const raw_string = zg.tree.tokenSlice(ident_token)[offset..];
    try zg.string_bytes.ensureUnusedCapacity(gpa, raw_string.len);
    const result = r: {
        var aw: std.io.Writer.Allocating = .fromArrayList(gpa, &zg.string_bytes);
        defer zg.string_bytes = aw.toArrayList();
        break :r std.zig.string_literal.parseWrite(&aw.writer, raw_string) catch |err| switch (err) {
            error.WriteFailed => return error.OutOfMemory,
        };
    };
    switch (result) {
        .success => {},
        .failure => |err| {
            try zg.lowerStrLitError(err, ident_token, raw_string, offset);
            return error.BadString;
        },
    }

    const slice = zg.string_bytes.items[start..];
    if (mem.indexOfScalar(u8, slice, 0) != null) {
        try zg.addErrorTok(ident_token, "identifier cannot contain null bytes", .{});
        return error.BadString;
    } else if (slice.len == 0) {
        try zg.addErrorTok(ident_token, "identifier cannot be empty", .{});
        return error.BadString;
    }
    return start;
}

/// Estimates the size of a string node without parsing it.
pub fn strLitSizeHint(tree: Ast, node: Ast.Node.Index) usize {
    switch (tree.nodeTag(node)) {
        // Parsed string literals are typically around the size of the raw strings.
        .string_literal => {
            const token = tree.nodeMainToken(node);
            const raw_string = tree.tokenSlice(token);
            return raw_string.len;
        },
        // Multiline string literal lengths can be computed exactly.
        .multiline_string_literal => {
            const first_tok, const last_tok = tree.nodeData(node).token_and_token;

            var size = tree.tokenSlice(first_tok)[2..].len;
            for (first_tok + 1..last_tok + 1) |tok_idx| {
                size += 1; // Newline
                size += tree.tokenSlice(@intCast(tok_idx))[2..].len;
            }
            return size;
        },
        else => unreachable,
    }
}

/// Parses the given node as a string literal.
pub fn parseStrLit(
    tree: Ast,
    node: Ast.Node.Index,
    writer: *Writer,
) Writer.Error!std.zig.string_literal.Result {
    switch (tree.nodeTag(node)) {
        .string_literal => {
            const token = tree.nodeMainToken(node);
            const raw_string = tree.tokenSlice(token);
            return std.zig.string_literal.parseWrite(writer, raw_string);
        },
        .multiline_string_literal => {
            const first_tok, const last_tok = tree.nodeData(node).token_and_token;

            // First line: do not append a newline.
            {
                const line_bytes = tree.tokenSlice(first_tok)[2..];
                try writer.writeAll(line_bytes);
            }

            // Following lines: each line prepends a newline.
            for (first_tok + 1..last_tok + 1) |tok_idx| {
                const line_bytes = tree.tokenSlice(@intCast(tok_idx))[2..];
                try writer.writeByte('\n');
                try writer.writeAll(line_bytes);
            }

            return .success;
        },
        // Node must represent a string
        else => unreachable,
    }
}

const StringLiteralResult = union(enum) {
    nts: Zoir.NullTerminatedString,
    slice: struct { start: u32, len: u32 },
};

fn strLitAsString(zg: *ZonGen, str_node: Ast.Node.Index) error{ OutOfMemory, BadString }!StringLiteralResult {
    if (!zg.options.parse_str_lits) return .{ .slice = .{ .start = 0, .len = 0 } };

    const gpa = zg.gpa;
    const string_bytes = &zg.string_bytes;
    const str_index: u32 = @intCast(zg.string_bytes.items.len);
    const size_hint = strLitSizeHint(zg.tree, str_node);
    try string_bytes.ensureUnusedCapacity(gpa, size_hint);
    const result = r: {
        var aw: std.io.Writer.Allocating = .fromArrayList(gpa, &zg.string_bytes);
        defer zg.string_bytes = aw.toArrayList();
        break :r parseStrLit(zg.tree, str_node, &aw.writer) catch |err| switch (err) {
            error.WriteFailed => return error.OutOfMemory,
        };
    };
    switch (result) {
        .success => {},
        .failure => |err| {
            const token = zg.tree.nodeMainToken(str_node);
            const raw_string = zg.tree.tokenSlice(token);
            try zg.lowerStrLitError(err, token, raw_string, 0);
            return error.BadString;
        },
    }
    const key: []const u8 = string_bytes.items[str_index..];
    if (std.mem.indexOfScalar(u8, key, 0) != null) return .{ .slice = .{
        .start = str_index,
        .len = @intCast(key.len),
    } };
    const gop = try zg.string_table.getOrPutContextAdapted(
        gpa,
        key,
        StringIndexAdapter{ .bytes = string_bytes },
        StringIndexContext{ .bytes = string_bytes },
    );
    if (gop.found_existing) {
        string_bytes.shrinkRetainingCapacity(str_index);
        return .{ .nts = @enumFromInt(gop.key_ptr.*) };
    }
    gop.key_ptr.* = str_index;
    try string_bytes.append(gpa, 0);
    return .{ .nts = @enumFromInt(str_index) };
}

fn identAsString(zg: *ZonGen, ident_token: Ast.TokenIndex) !Zoir.NullTerminatedString {
    const gpa = zg.gpa;
    const string_bytes = &zg.string_bytes;
    const str_index = try zg.appendIdentStr(ident_token);
    const key: []const u8 = string_bytes.items[str_index..];
    const gop = try zg.string_table.getOrPutContextAdapted(
        gpa,
        key,
        StringIndexAdapter{ .bytes = string_bytes },
        StringIndexContext{ .bytes = string_bytes },
    );
    if (gop.found_existing) {
        string_bytes.shrinkRetainingCapacity(str_index);
        return @enumFromInt(gop.key_ptr.*);
    }
    gop.key_ptr.* = str_index;
    try string_bytes.append(gpa, 0);
    return @enumFromInt(str_index);
}

fn numberLiteral(zg: *ZonGen, num_node: Ast.Node.Index, src_node: Ast.Node.Index, dest_node: Zoir.Node.Index, sign: enum { negative, positive }) !void {
    const tree = zg.tree;
    const num_token = tree.nodeMainToken(num_node);
    const num_bytes = tree.tokenSlice(num_token);

    switch (std.zig.parseNumberLiteral(num_bytes)) {
        .int => |unsigned_num| {
            if (unsigned_num == 0 and sign == .negative) {
                try zg.addErrorTokNotes(num_token, "integer literal '-0' is ambiguous", .{}, &.{
                    try zg.errNoteTok(num_token, "use '0' for an integer zero", .{}),
                    try zg.errNoteTok(num_token, "use '-0.0' for a floating-point signed zero", .{}),
                });
                return;
            }
            const num: i65 = switch (sign) {
                .positive => unsigned_num,
                .negative => -@as(i65, unsigned_num),
            };
            if (std.math.cast(i32, num)) |x| {
                zg.setNode(dest_node, .{
                    .tag = .int_literal_small,
                    .data = @bitCast(x),
                    .ast_node = src_node,
                });
                return;
            }
            const max_limbs = comptime std.math.big.int.calcTwosCompLimbCount(@bitSizeOf(@TypeOf(num)));
            var limbs: [max_limbs]std.math.big.Limb = undefined;
            var big_int: std.math.big.int.Mutable = .init(&limbs, num);
            try zg.setBigIntLiteralNode(dest_node, src_node, big_int.toConst());
        },
        .big_int => |base| {
            const gpa = zg.gpa;
            const num_without_prefix = switch (base) {
                .decimal => num_bytes,
                .hex, .binary, .octal => num_bytes[2..],
            };
            var big_int: std.math.big.int.Managed = try .init(gpa);
            defer big_int.deinit();
            big_int.setString(@intFromEnum(base), num_without_prefix) catch |err| switch (err) {
                error.InvalidCharacter => unreachable, // caught in `parseNumberLiteral`
                error.InvalidBase => unreachable, // we only pass 16, 8, 2, see above
                error.OutOfMemory => return error.OutOfMemory,
            };
            switch (sign) {
                .positive => {},
                .negative => big_int.negate(),
            }
            try zg.setBigIntLiteralNode(dest_node, src_node, big_int.toConst());
        },
        .float => {
            const unsigned_num = std.fmt.parseFloat(f128, num_bytes) catch |err| switch (err) {
                error.InvalidCharacter => unreachable, // validated by tokenizer
            };
            const num: f128 = switch (sign) {
                .positive => unsigned_num,
                .negative => -unsigned_num,
            };

            {
                // If the value fits into an f32 without losing any precision, store it that way.
                @setFloatMode(.strict);
                const smaller_float: f32 = @floatCast(num);
                const bigger_again: f128 = smaller_float;
                if (bigger_again == num) {
                    zg.setNode(dest_node, .{
                        .tag = .float_literal_small,
                        .data = @bitCast(smaller_float),
                        .ast_node = src_node,
                    });
                    return;
                }
            }

            const elems: [4]u32 = @bitCast(num);
            const extra_index: u32 = @intCast(zg.extra.items.len);
            try zg.extra.appendSlice(zg.gpa, &elems);
            zg.setNode(dest_node, .{
                .tag = .float_literal,
                .data = extra_index,
                .ast_node = src_node,
            });
        },
        .failure => |err| try zg.lowerNumberError(err, num_token, num_bytes),
    }
}

fn setBigIntLiteralNode(zg: *ZonGen, dest_node: Zoir.Node.Index, src_node: Ast.Node.Index, val: std.math.big.int.Const) !void {
    try zg.extra.ensureUnusedCapacity(zg.gpa, 2);
    try zg.limbs.ensureUnusedCapacity(zg.gpa, val.limbs.len);

    const limbs_idx: u32 = @intCast(zg.limbs.items.len);
    zg.limbs.appendSliceAssumeCapacity(val.limbs);

    const extra_idx: u32 = @intCast(zg.extra.items.len);
    zg.extra.appendSliceAssumeCapacity(&.{ @intCast(val.limbs.len), limbs_idx });

    zg.setNode(dest_node, .{
        .tag = if (val.positive) .int_literal_pos else .int_literal_neg,
        .data = extra_idx,
        .ast_node = src_node,
    });
}

fn charLiteral(zg: *ZonGen, node: Ast.Node.Index, dest_node: Zoir.Node.Index) !void {
    const tree = zg.tree;
    assert(tree.nodeTag(node) == .char_literal);
    const main_token = tree.nodeMainToken(node);
    const slice = tree.tokenSlice(main_token);
    switch (std.zig.parseCharLiteral(slice)) {
        .success => |codepoint| zg.setNode(dest_node, .{
            .tag = .char_literal,
            .data = codepoint,
            .ast_node = node,
        }),
        .failure => |err| try zg.lowerStrLitError(err, main_token, slice, 0),
    }
}

fn identifier(zg: *ZonGen, node: Ast.Node.Index, dest_node: Zoir.Node.Index) !void {
    const tree = zg.tree;
    assert(tree.nodeTag(node) == .identifier);
    const main_token = tree.nodeMainToken(node);
    const ident = tree.tokenSlice(main_token);

    const tag: Zoir.Node.Repr.Tag = t: {
        if (mem.eql(u8, ident, "true")) break :t .true;
        if (mem.eql(u8, ident, "false")) break :t .false;
        if (mem.eql(u8, ident, "null")) break :t .null;
        if (mem.eql(u8, ident, "inf")) break :t .pos_inf;
        if (mem.eql(u8, ident, "nan")) break :t .nan;
        try zg.addErrorNodeNotes(node, "invalid expression", .{}, &.{
            try zg.errNoteNode(node, "ZON allows identifiers 'true', 'false', 'null', 'inf', and 'nan'", .{}),
            try zg.errNoteNode(node, "precede identifier with '.' for an enum literal", .{}),
        });
        return;
    };

    zg.setNode(dest_node, .{
        .tag = tag,
        .data = 0, // ignored
        .ast_node = node,
    });
}

fn setNode(zg: *ZonGen, dest: Zoir.Node.Index, repr: Zoir.Node.Repr) void {
    zg.nodes.set(@intFromEnum(dest), repr);
}

fn lowerStrLitError(
    zg: *ZonGen,
    err: std.zig.string_literal.Error,
    token: Ast.TokenIndex,
    raw_string: []const u8,
    offset: u32,
) Allocator.Error!void {
    return ZonGen.addErrorTokOff(zg, token, @intCast(offset + err.offset()), "{f}", .{err.fmt(raw_string)});
}

fn lowerNumberError(zg: *ZonGen, err: std.zig.number_literal.Error, token: Ast.TokenIndex, bytes: []const u8) Allocator.Error!void {
    const is_float = std.mem.indexOfScalar(u8, bytes, '.') != null;
    switch (err) {
        .leading_zero => if (is_float) {
            try zg.addErrorTok(token, "number '{s}' has leading zero", .{bytes});
        } else {
            try zg.addErrorTokNotes(token, "number '{s}' has leading zero", .{bytes}, &.{
                try zg.errNoteTok(token, "use '0o' prefix for octal literals", .{}),
            });
        },
        .digit_after_base => try zg.addErrorTok(token, "expected a digit after base prefix", .{}),
        .upper_case_base => |i| try zg.addErrorTokOff(token, @intCast(i), "base prefix must be lowercase", .{}),
        .invalid_float_base => |i| try zg.addErrorTokOff(token, @intCast(i), "invalid base for float literal", .{}),
        .repeated_underscore => |i| try zg.addErrorTokOff(token, @intCast(i), "repeated digit separator", .{}),
        .invalid_underscore_after_special => |i| try zg.addErrorTokOff(token, @intCast(i), "expected digit before digit separator", .{}),
        .invalid_digit => |info| try zg.addErrorTokOff(token, @intCast(info.i), "invalid digit '{c}' for {s} base", .{ bytes[info.i], @tagName(info.base) }),
        .invalid_digit_exponent => |i| try zg.addErrorTokOff(token, @intCast(i), "invalid digit '{c}' in exponent", .{bytes[i]}),
        .duplicate_exponent => |i| try zg.addErrorTokOff(token, @intCast(i), "duplicate exponent", .{}),
        .exponent_after_underscore => |i| try zg.addErrorTokOff(token, @intCast(i), "expected digit before exponent", .{}),
        .special_after_underscore => |i| try zg.addErrorTokOff(token, @intCast(i), "expected digit before '{c}'", .{bytes[i]}),
        .trailing_special => |i| try zg.addErrorTokOff(token, @intCast(i), "expected digit after '{c}'", .{bytes[i - 1]}),
        .trailing_underscore => |i| try zg.addErrorTokOff(token, @intCast(i), "trailing digit separator", .{}),
        .duplicate_period => unreachable, // Validated by tokenizer
        .invalid_character => unreachable, // Validated by tokenizer
        .invalid_exponent_sign => |i| {
            assert(bytes.len >= 2 and bytes[0] == '0' and bytes[1] == 'x'); // Validated by tokenizer
            try zg.addErrorTokOff(token, @intCast(i), "sign '{c}' cannot follow digit '{c}' in hex base", .{ bytes[i], bytes[i - 1] });
        },
        .period_after_exponent => |i| try zg.addErrorTokOff(token, @intCast(i), "unexpected period after exponent", .{}),
    }
}

fn errNoteNode(zg: *ZonGen, node: Ast.Node.Index, comptime format: []const u8, args: anytype) Allocator.Error!Zoir.CompileError.Note {
    const message_idx: u32 = @intCast(zg.string_bytes.items.len);
    try zg.string_bytes.print(zg.gpa, format ++ "\x00", args);
    return .{
        .msg = @enumFromInt(message_idx),
        .token = .none,
        .node_or_offset = @intFromEnum(node),
    };
}

fn errNoteTok(zg: *ZonGen, tok: Ast.TokenIndex, comptime format: []const u8, args: anytype) Allocator.Error!Zoir.CompileError.Note {
    const message_idx: u32 = @intCast(zg.string_bytes.items.len);
    try zg.string_bytes.print(zg.gpa, format ++ "\x00", args);
    return .{
        .msg = @enumFromInt(message_idx),
        .token = .fromToken(tok),
        .node_or_offset = 0,
    };
}

fn addErrorNode(zg: *ZonGen, node: Ast.Node.Index, comptime format: []const u8, args: anytype) Allocator.Error!void {
    return zg.addErrorInner(.none, @intFromEnum(node), format, args, &.{});
}
fn addErrorTok(zg: *ZonGen, tok: Ast.TokenIndex, comptime format: []const u8, args: anytype) Allocator.Error!void {
    return zg.addErrorInner(.fromToken(tok), 0, format, args, &.{});
}
fn addErrorNodeNotes(zg: *ZonGen, node: Ast.Node.Index, comptime format: []const u8, args: anytype, notes: []const Zoir.CompileError.Note) Allocator.Error!void {
    return zg.addErrorInner(.none, @intFromEnum(node), format, args, notes);
}
fn addErrorTokNotes(zg: *ZonGen, tok: Ast.TokenIndex, comptime format: []const u8, args: anytype, notes: []const Zoir.CompileError.Note) Allocator.Error!void {
    return zg.addErrorInner(.fromToken(tok), 0, format, args, notes);
}
fn addErrorTokOff(zg: *ZonGen, tok: Ast.TokenIndex, offset: u32, comptime format: []const u8, args: anytype) Allocator.Error!void {
    return zg.addErrorInner(.fromToken(tok), offset, format, args, &.{});
}
fn addErrorTokNotesOff(zg: *ZonGen, tok: Ast.TokenIndex, offset: u32, comptime format: []const u8, args: anytype, notes: []const Zoir.CompileError.Note) Allocator.Error!void {
    return zg.addErrorInner(.fromToken(tok), offset, format, args, notes);
}

fn addErrorInner(
    zg: *ZonGen,
    token: Ast.OptionalTokenIndex,
    node_or_offset: u32,
    comptime format: []const u8,
    args: anytype,
    notes: []const Zoir.CompileError.Note,
) Allocator.Error!void {
    const gpa = zg.gpa;

    const first_note: u32 = @intCast(zg.error_notes.items.len);
    try zg.error_notes.appendSlice(gpa, notes);

    const message_idx: u32 = @intCast(zg.string_bytes.items.len);
    try zg.string_bytes.print(gpa, format ++ "\x00", args);

    try zg.compile_errors.append(gpa, .{
        .msg = @enumFromInt(message_idx),
        .token = token,
        .node_or_offset = node_or_offset,
        .first_note = first_note,
        .note_count = @intCast(notes.len),
    });
}

fn lowerAstErrors(zg: *ZonGen) Allocator.Error!void {
    const gpa = zg.gpa;
    const tree = zg.tree;
    assert(tree.errors.len > 0);

    var msg: std.io.Writer.Allocating = .init(gpa);
    defer msg.deinit();
    const msg_bw = &msg.writer;

    var notes: std.ArrayListUnmanaged(Zoir.CompileError.Note) = .empty;
    defer notes.deinit(gpa);

    var cur_err = tree.errors[0];
    for (tree.errors[1..]) |err| {
        if (err.is_note) {
            tree.renderError(err, msg_bw) catch return error.OutOfMemory;
            try notes.append(gpa, try zg.errNoteTok(err.token, "{s}", .{msg.getWritten()}));
        } else {
            // Flush error
            tree.renderError(cur_err, msg_bw) catch return error.OutOfMemory;
            const extra_offset = tree.errorOffset(cur_err);
            try zg.addErrorTokNotesOff(cur_err.token, extra_offset, "{s}", .{msg.getWritten()}, notes.items);
            notes.clearRetainingCapacity();
            cur_err = err;

            // TODO: `Parse` currently does not have good error recovery
            // mechanisms, so the remaining errors could be bogus. As such,
            // we'll ignore all remaining errors for now. We should improve
            // `Parse` so that we can report all the errors.
            return;
        }
        msg.clearRetainingCapacity();
    }

    // Flush error
    const extra_offset = tree.errorOffset(cur_err);
    tree.renderError(cur_err, msg_bw) catch return error.OutOfMemory;
    try zg.addErrorTokNotesOff(cur_err.token, extra_offset, "{s}", .{msg.getWritten()}, notes.items);
}