const Object = @This();

const Wasm = @import("../Wasm.zig");
const Alignment = Wasm.Alignment;

const std = @import("std");
const Allocator = std.mem.Allocator;
const Path = std.Build.Cache.Path;
const log = std.log.scoped(.object);
const assert = std.debug.assert;

/// Wasm spec version used for this `Object`
version: u32,
/// For error reporting purposes only.
/// Name (read path) of the object or archive file.
path: Path,
/// For error reporting purposes only.
/// If this represents an object in an archive, it's the basename of the
/// object, and path refers to the archive.
archive_member_name: Wasm.OptionalString,
/// Represents the function ID that must be called on startup.
/// This is `null` by default as runtimes may determine the startup
/// function themselves. This is essentially legacy.
start_function: Wasm.OptionalObjectFunctionIndex,
/// A slice of features that tell the linker what features are mandatory, used
/// (or therefore missing) and must generate an error when another object uses
/// features that are not supported by the other.
features: Wasm.Feature.Set,
/// Points into `Wasm.object_functions`
functions: RelativeSlice,
/// Points into `Wasm.object_function_imports`
function_imports: RelativeSlice,
/// Points into `Wasm.object_global_imports`
global_imports: RelativeSlice,
/// Points into `Wasm.object_table_imports`
table_imports: RelativeSlice,
// Points into `Wasm.object_data_imports`
data_imports: RelativeSlice,
/// Points into Wasm object_custom_segments
custom_segments: RelativeSlice,
/// Points into Wasm object_init_funcs
init_funcs: RelativeSlice,
/// Points into Wasm object_comdats
comdats: RelativeSlice,
/// Guaranteed to be non-null when functions has nonzero length.
code_section_index: ?Wasm.ObjectSectionIndex,
/// Guaranteed to be non-null when globals has nonzero length.
global_section_index: ?Wasm.ObjectSectionIndex,
/// Guaranteed to be non-null when data segments has nonzero length.
data_section_index: ?Wasm.ObjectSectionIndex,
is_included: bool,

pub const RelativeSlice = struct {
    off: u32,
    len: u32,
};

pub const SegmentInfo = struct {
    name: Wasm.String,
    flags: Flags,

    /// Matches the ABI.
    pub const Flags = packed struct(u32) {
        /// Signals that the segment contains only null terminated strings allowing
        /// the linker to perform merging.
        strings: bool,
        /// The segment contains thread-local data. This means that a unique copy
        /// of this segment will be created for each thread.
        tls: bool,
        /// If the object file is included in the final link, the segment should be
        /// retained in the final output regardless of whether it is used by the
        /// program.
        retain: bool,
        alignment: Alignment,

        _: u23 = 0,
    };
};

pub const FunctionImport = struct {
    module_name: Wasm.String,
    name: Wasm.String,
    function_index: ScratchSpace.FuncTypeIndex,
};

pub const GlobalImport = struct {
    module_name: Wasm.String,
    name: Wasm.String,
    valtype: std.wasm.Valtype,
    mutable: bool,
};

pub const TableImport = struct {
    module_name: Wasm.String,
    name: Wasm.String,
    limits_min: u32,
    limits_max: u32,
    limits_has_max: bool,
    limits_is_shared: bool,
    ref_type: std.wasm.RefType,
};

pub const DataSegmentFlags = enum(u32) { active, passive, active_memidx };

pub const SubsectionType = enum(u8) {
    segment_info = 5,
    init_funcs = 6,
    comdat_info = 7,
    symbol_table = 8,
};

/// Specified by https://github.com/WebAssembly/tool-conventions/blob/main/Linking.md
pub const RelocationType = enum(u8) {
    function_index_leb = 0,
    table_index_sleb = 1,
    table_index_i32 = 2,
    memory_addr_leb = 3,
    memory_addr_sleb = 4,
    memory_addr_i32 = 5,
    type_index_leb = 6,
    global_index_leb = 7,
    function_offset_i32 = 8,
    section_offset_i32 = 9,
    event_index_leb = 10,
    memory_addr_rel_sleb = 11,
    table_index_rel_sleb = 12,
    global_index_i32 = 13,
    memory_addr_leb64 = 14,
    memory_addr_sleb64 = 15,
    memory_addr_i64 = 16,
    memory_addr_rel_sleb64 = 17,
    table_index_sleb64 = 18,
    table_index_i64 = 19,
    table_number_leb = 20,
    memory_addr_tls_sleb = 21,
    function_offset_i64 = 22,
    memory_addr_locrel_i32 = 23,
    table_index_rel_sleb64 = 24,
    memory_addr_tls_sleb64 = 25,
    function_index_i32 = 26,
};

pub const Symbol = struct {
    flags: Wasm.SymbolFlags,
    name: Wasm.OptionalString,
    pointee: Pointee,

    /// https://github.com/WebAssembly/tool-conventions/blob/df8d737539eb8a8f446ba5eab9dc670c40dfb81e/Linking.md#symbol-table-subsection
    const Tag = enum(u8) {
        function,
        data,
        global,
        section,
        event,
        table,
    };

    const Pointee = union(enum) {
        function: Wasm.ObjectFunctionIndex,
        function_import: ScratchSpace.FuncImportIndex,
        data: Wasm.ObjectData.Index,
        data_import: void,
        global: Wasm.ObjectGlobalIndex,
        global_import: ScratchSpace.GlobalImportIndex,
        section: Wasm.ObjectSectionIndex,
        table: Wasm.ObjectTableIndex,
        table_import: ScratchSpace.TableImportIndex,
    };
};

pub const ScratchSpace = struct {
    func_types: std.ArrayListUnmanaged(Wasm.FunctionType.Index) = .empty,
    func_type_indexes: std.ArrayListUnmanaged(FuncTypeIndex) = .empty,
    func_imports: std.ArrayListUnmanaged(FunctionImport) = .empty,
    global_imports: std.ArrayListUnmanaged(GlobalImport) = .empty,
    table_imports: std.ArrayListUnmanaged(TableImport) = .empty,
    symbol_table: std.ArrayListUnmanaged(Symbol) = .empty,
    segment_info: std.ArrayListUnmanaged(SegmentInfo) = .empty,
    exports: std.ArrayListUnmanaged(Export) = .empty,

    const Export = struct {
        name: Wasm.String,
        pointee: Pointee,

        const Pointee = union(std.wasm.ExternalKind) {
            function: Wasm.ObjectFunctionIndex,
            table: Wasm.ObjectTableIndex,
            memory: Wasm.ObjectMemory.Index,
            global: Wasm.ObjectGlobalIndex,
        };
    };

    /// Index into `func_imports`.
    const FuncImportIndex = enum(u32) {
        _,

        fn ptr(index: FuncImportIndex, ss: *const ScratchSpace) *FunctionImport {
            return &ss.func_imports.items[@intFromEnum(index)];
        }
    };

    /// Index into `global_imports`.
    const GlobalImportIndex = enum(u32) {
        _,

        fn ptr(index: GlobalImportIndex, ss: *const ScratchSpace) *GlobalImport {
            return &ss.global_imports.items[@intFromEnum(index)];
        }
    };

    /// Index into `table_imports`.
    const TableImportIndex = enum(u32) {
        _,

        fn ptr(index: TableImportIndex, ss: *const ScratchSpace) *TableImport {
            return &ss.table_imports.items[@intFromEnum(index)];
        }
    };

    /// Index into `func_types`.
    const FuncTypeIndex = enum(u32) {
        _,

        fn ptr(index: FuncTypeIndex, ss: *const ScratchSpace) *Wasm.FunctionType.Index {
            return &ss.func_types.items[@intFromEnum(index)];
        }
    };

    pub fn deinit(ss: *ScratchSpace, gpa: Allocator) void {
        ss.exports.deinit(gpa);
        ss.func_types.deinit(gpa);
        ss.func_type_indexes.deinit(gpa);
        ss.func_imports.deinit(gpa);
        ss.global_imports.deinit(gpa);
        ss.table_imports.deinit(gpa);
        ss.symbol_table.deinit(gpa);
        ss.segment_info.deinit(gpa);
        ss.* = undefined;
    }

    fn clear(ss: *ScratchSpace) void {
        ss.exports.clearRetainingCapacity();
        ss.func_types.clearRetainingCapacity();
        ss.func_type_indexes.clearRetainingCapacity();
        ss.func_imports.clearRetainingCapacity();
        ss.global_imports.clearRetainingCapacity();
        ss.table_imports.clearRetainingCapacity();
        ss.symbol_table.clearRetainingCapacity();
        ss.segment_info.clearRetainingCapacity();
    }
};

pub fn parse(
    wasm: *Wasm,
    bytes: []const u8,
    path: Path,
    archive_member_name: ?[]const u8,
    host_name: Wasm.OptionalString,
    ss: *ScratchSpace,
    must_link: bool,
    gc_sections: bool,
) anyerror!Object {
    const comp = wasm.base.comp;
    const gpa = comp.gpa;
    const diags = &comp.link_diags;

    var pos: usize = 0;

    if (!std.mem.eql(u8, bytes[0..std.wasm.magic.len], &std.wasm.magic)) return error.BadObjectMagic;
    pos += std.wasm.magic.len;

    const version = std.mem.readInt(u32, bytes[pos..][0..4], .little);
    pos += 4;

    const data_segment_start: u32 = @intCast(wasm.object_data_segments.items.len);
    const custom_segment_start: u32 = @intCast(wasm.object_custom_segments.entries.len);
    const functions_start: u32 = @intCast(wasm.object_functions.items.len);
    const tables_start: u32 = @intCast(wasm.object_tables.items.len);
    const memories_start: u32 = @intCast(wasm.object_memories.items.len);
    const globals_start: u32 = @intCast(wasm.object_globals.items.len);
    const init_funcs_start: u32 = @intCast(wasm.object_init_funcs.items.len);
    const comdats_start: u32 = @intCast(wasm.object_comdats.items.len);
    const function_imports_start: u32 = @intCast(wasm.object_function_imports.entries.len);
    const global_imports_start: u32 = @intCast(wasm.object_global_imports.entries.len);
    const table_imports_start: u32 = @intCast(wasm.object_table_imports.entries.len);
    const data_imports_start: u32 = @intCast(wasm.object_data_imports.entries.len);
    const local_section_index_base = wasm.object_total_sections;
    const object_index: Wasm.ObjectIndex = @enumFromInt(wasm.objects.items.len);
    const source_location: Wasm.SourceLocation = .fromObject(object_index, wasm);

    ss.clear();

    var start_function: Wasm.OptionalObjectFunctionIndex = .none;
    var opt_features: ?Wasm.Feature.Set = null;
    var saw_linking_section = false;
    var has_tls = false;
    var table_import_symbol_count: usize = 0;
    var code_section_index: ?Wasm.ObjectSectionIndex = null;
    var global_section_index: ?Wasm.ObjectSectionIndex = null;
    var data_section_index: ?Wasm.ObjectSectionIndex = null;
    while (pos < bytes.len) : (wasm.object_total_sections += 1) {
        const section_index: Wasm.ObjectSectionIndex = @enumFromInt(wasm.object_total_sections);

        const section_tag: std.wasm.Section = @enumFromInt(bytes[pos]);
        pos += 1;

        const len, pos = readLeb(u32, bytes, pos);
        const section_end = pos + len;
        switch (section_tag) {
            .custom => {
                const section_name, pos = readBytes(bytes, pos);
                if (std.mem.eql(u8, section_name, "linking")) {
                    saw_linking_section = true;
                    const section_version, pos = readLeb(u32, bytes, pos);
                    log.debug("link meta data version: {d}", .{section_version});
                    if (section_version != 2) return error.UnsupportedVersion;
                    while (pos < section_end) {
                        const sub_type, pos = readLeb(u8, bytes, pos);
                        log.debug("found subsection: {s}", .{@tagName(@as(SubsectionType, @enumFromInt(sub_type)))});
                        const payload_len, pos = readLeb(u32, bytes, pos);
                        if (payload_len == 0) break;

                        const count, pos = readLeb(u32, bytes, pos);

                        switch (@as(SubsectionType, @enumFromInt(sub_type))) {
                            .segment_info => {
                                for (try ss.segment_info.addManyAsSlice(gpa, count)) |*segment| {
                                    const name, pos = readBytes(bytes, pos);
                                    const alignment, pos = readLeb(u32, bytes, pos);
                                    const flags_u32, pos = readLeb(u32, bytes, pos);
                                    const flags: SegmentInfo.Flags = @bitCast(flags_u32);
                                    const tls = flags.tls or
                                        // Supports legacy object files that specified
                                        // being TLS by the name instead of the TLS flag.
                                        std.mem.startsWith(u8, name, ".tdata") or
                                        std.mem.startsWith(u8, name, ".tbss");
                                    has_tls = has_tls or tls;
                                    segment.* = .{
                                        .name = try wasm.internString(name),
                                        .flags = .{
                                            .strings = flags.strings,
                                            .tls = tls,
                                            .alignment = @enumFromInt(alignment),
                                            .retain = flags.retain,
                                        },
                                    };
                                }
                            },
                            .init_funcs => {
                                for (try wasm.object_init_funcs.addManyAsSlice(gpa, count)) |*func| {
                                    const priority, pos = readLeb(u32, bytes, pos);
                                    const symbol_index, pos = readLeb(u32, bytes, pos);
                                    if (symbol_index > ss.symbol_table.items.len)
                                        return diags.failParse(path, "init_funcs before symbol table", .{});
                                    const sym = &ss.symbol_table.items[symbol_index];
                                    if (sym.pointee != .function) {
                                        return diags.failParse(path, "init_func symbol '{s}' not a function", .{
                                            sym.name.slice(wasm).?,
                                        });
                                    } else if (sym.flags.undefined) {
                                        return diags.failParse(path, "init_func symbol '{s}' is an import", .{
                                            sym.name.slice(wasm).?,
                                        });
                                    }
                                    func.* = .{
                                        .priority = priority,
                                        .function_index = sym.pointee.function,
                                    };
                                }
                            },
                            .comdat_info => {
                                for (try wasm.object_comdats.addManyAsSlice(gpa, count)) |*comdat| {
                                    const name, pos = readBytes(bytes, pos);
                                    const flags, pos = readLeb(u32, bytes, pos);
                                    if (flags != 0) return error.UnexpectedComdatFlags;
                                    const symbol_count, pos = readLeb(u32, bytes, pos);
                                    const start_off: u32 = @intCast(wasm.object_comdat_symbols.len);
                                    try wasm.object_comdat_symbols.ensureUnusedCapacity(gpa, symbol_count);
                                    for (0..symbol_count) |_| {
                                        const kind, pos = readEnum(Wasm.Comdat.Symbol.Type, bytes, pos);
                                        const index, pos = readLeb(u32, bytes, pos);
                                        if (true) @panic("TODO rebase index depending on kind");
                                        wasm.object_comdat_symbols.appendAssumeCapacity(.{
                                            .kind = kind,
                                            .index = index,
                                        });
                                    }
                                    comdat.* = .{
                                        .name = try wasm.internString(name),
                                        .flags = flags,
                                        .symbols = .{
                                            .off = start_off,
                                            .len = @intCast(wasm.object_comdat_symbols.len - start_off),
                                        },
                                    };
                                }
                            },
                            .symbol_table => {
                                for (try ss.symbol_table.addManyAsSlice(gpa, count)) |*symbol| {
                                    const tag, pos = readEnum(Symbol.Tag, bytes, pos);
                                    const flags, pos = readLeb(u32, bytes, pos);
                                    symbol.* = .{
                                        .flags = @bitCast(flags),
                                        .name = .none,
                                        .pointee = undefined,
                                    };
                                    symbol.flags.initZigSpecific(must_link, gc_sections);

                                    switch (tag) {
                                        .data => {
                                            const name, pos = readBytes(bytes, pos);
                                            const interned_name = try wasm.internString(name);
                                            symbol.name = interned_name.toOptional();
                                            if (symbol.flags.undefined) {
                                                symbol.pointee = .data_import;
                                            } else {
                                                const segment_index, pos = readLeb(u32, bytes, pos);
                                                const segment_offset, pos = readLeb(u32, bytes, pos);
                                                const size, pos = readLeb(u32, bytes, pos);
                                                try wasm.object_datas.append(gpa, .{
                                                    .segment = @enumFromInt(data_segment_start + segment_index),
                                                    .offset = segment_offset,
                                                    .size = size,
                                                    .name = interned_name,
                                                    .flags = symbol.flags,
                                                });
                                                symbol.pointee = .{
                                                    .data = @enumFromInt(wasm.object_datas.items.len - 1),
                                                };
                                            }
                                        },
                                        .section => {
                                            const local_section, pos = readLeb(u32, bytes, pos);
                                            const section: Wasm.ObjectSectionIndex = @enumFromInt(local_section_index_base + local_section);
                                            symbol.pointee = .{ .section = section };
                                        },

                                        .function => {
                                            const local_index, pos = readLeb(u32, bytes, pos);
                                            if (symbol.flags.undefined) {
                                                const function_import: ScratchSpace.FuncImportIndex = @enumFromInt(local_index);
                                                symbol.pointee = .{ .function_import = function_import };
                                                if (symbol.flags.explicit_name) {
                                                    const name, pos = readBytes(bytes, pos);
                                                    symbol.name = (try wasm.internString(name)).toOptional();
                                                } else {
                                                    symbol.name = function_import.ptr(ss).name.toOptional();
                                                }
                                            } else {
                                                symbol.pointee = .{ .function = @enumFromInt(functions_start + (local_index - ss.func_imports.items.len)) };
                                                const name, pos = readBytes(bytes, pos);
                                                symbol.name = (try wasm.internString(name)).toOptional();
                                            }
                                        },
                                        .global => {
                                            const local_index, pos = readLeb(u32, bytes, pos);
                                            if (symbol.flags.undefined) {
                                                const global_import: ScratchSpace.GlobalImportIndex = @enumFromInt(local_index);
                                                symbol.pointee = .{ .global_import = global_import };
                                                if (symbol.flags.explicit_name) {
                                                    const name, pos = readBytes(bytes, pos);
                                                    symbol.name = (try wasm.internString(name)).toOptional();
                                                } else {
                                                    symbol.name = global_import.ptr(ss).name.toOptional();
                                                }
                                            } else {
                                                symbol.pointee = .{ .global = @enumFromInt(globals_start + (local_index - ss.global_imports.items.len)) };
                                                const name, pos = readBytes(bytes, pos);
                                                symbol.name = (try wasm.internString(name)).toOptional();
                                            }
                                        },
                                        .table => {
                                            const local_index, pos = readLeb(u32, bytes, pos);
                                            if (symbol.flags.undefined) {
                                                table_import_symbol_count += 1;
                                                const table_import: ScratchSpace.TableImportIndex = @enumFromInt(local_index);
                                                symbol.pointee = .{ .table_import = table_import };
                                                if (symbol.flags.explicit_name) {
                                                    const name, pos = readBytes(bytes, pos);
                                                    symbol.name = (try wasm.internString(name)).toOptional();
                                                } else {
                                                    symbol.name = table_import.ptr(ss).name.toOptional();
                                                }
                                            } else {
                                                symbol.pointee = .{ .table = @enumFromInt(tables_start + (local_index - ss.table_imports.items.len)) };
                                                const name, pos = readBytes(bytes, pos);
                                                symbol.name = (try wasm.internString(name)).toOptional();
                                            }
                                        },
                                        else => {
                                            log.debug("unrecognized symbol type tag: {x}", .{@intFromEnum(tag)});
                                            return error.UnrecognizedSymbolType;
                                        },
                                    }
                                }
                            },
                        }
                    }
                } else if (std.mem.startsWith(u8, section_name, "reloc.")) {
                    // 'The "reloc." custom sections must come after the "linking" custom section'
                    if (!saw_linking_section) return error.RelocBeforeLinkingSection;

                    // "Relocation sections start with an identifier specifying
                    // which section they apply to, and must be sequenced in
                    // the module after that section."
                    // "Relocation sections can only target code, data and custom sections."
                    const local_section, pos = readLeb(u32, bytes, pos);
                    const count, pos = readLeb(u32, bytes, pos);
                    const section: Wasm.ObjectSectionIndex = @enumFromInt(local_section_index_base + local_section);

                    log.debug("found {d} relocations for section={d}", .{ count, section });

                    var prev_offset: u32 = 0;
                    try wasm.object_relocations.ensureUnusedCapacity(gpa, count);
                    for (0..count) |_| {
                        const tag: RelocationType = @enumFromInt(bytes[pos]);
                        pos += 1;
                        const offset, pos = readLeb(u32, bytes, pos);
                        const index, pos = readLeb(u32, bytes, pos);

                        if (offset < prev_offset)
                            return diags.failParse(path, "relocation entries not sorted by offset", .{});
                        prev_offset = offset;

                        const sym = &ss.symbol_table.items[index];

                        switch (tag) {
                            .memory_addr_leb,
                            .memory_addr_sleb,
                            .memory_addr_i32,
                            .memory_addr_rel_sleb,
                            .memory_addr_leb64,
                            .memory_addr_sleb64,
                            .memory_addr_i64,
                            .memory_addr_rel_sleb64,
                            .memory_addr_tls_sleb,
                            .memory_addr_locrel_i32,
                            .memory_addr_tls_sleb64,
                            => {
                                const addend: i32, pos = readLeb(i32, bytes, pos);
                                wasm.object_relocations.appendAssumeCapacity(switch (sym.pointee) {
                                    .data => |data| .{
                                        .tag = .fromType(tag),
                                        .offset = offset,
                                        .pointee = .{ .data = data },
                                        .addend = addend,
                                    },
                                    .data_import => .{
                                        .tag = .fromTypeImport(tag),
                                        .offset = offset,
                                        .pointee = .{ .symbol_name = sym.name.unwrap().? },
                                        .addend = addend,
                                    },
                                    else => unreachable,
                                });
                            },
                            .function_offset_i32, .function_offset_i64 => {
                                const addend: i32, pos = readLeb(i32, bytes, pos);
                                wasm.object_relocations.appendAssumeCapacity(switch (sym.pointee) {
                                    .function => .{
                                        .tag = .fromType(tag),
                                        .offset = offset,
                                        .pointee = .{ .function = sym.pointee.function },
                                        .addend = addend,
                                    },
                                    .function_import => .{
                                        .tag = .fromTypeImport(tag),
                                        .offset = offset,
                                        .pointee = .{ .symbol_name = sym.name.unwrap().? },
                                        .addend = addend,
                                    },
                                    else => unreachable,
                                });
                            },
                            .section_offset_i32 => {
                                const addend: i32, pos = readLeb(i32, bytes, pos);
                                wasm.object_relocations.appendAssumeCapacity(.{
                                    .tag = .section_offset_i32,
                                    .offset = offset,
                                    .pointee = .{ .section = sym.pointee.section },
                                    .addend = addend,
                                });
                            },
                            .type_index_leb => {
                                wasm.object_relocations.appendAssumeCapacity(.{
                                    .tag = .type_index_leb,
                                    .offset = offset,
                                    .pointee = .{ .type_index = ss.func_types.items[index] },
                                    .addend = undefined,
                                });
                            },
                            .function_index_leb,
                            .function_index_i32,
                            .table_index_sleb,
                            .table_index_i32,
                            .table_index_sleb64,
                            .table_index_i64,
                            .table_index_rel_sleb,
                            .table_index_rel_sleb64,
                            => {
                                wasm.object_relocations.appendAssumeCapacity(switch (sym.pointee) {
                                    .function => .{
                                        .tag = .fromType(tag),
                                        .offset = offset,
                                        .pointee = .{ .function = sym.pointee.function },
                                        .addend = undefined,
                                    },
                                    .function_import => .{
                                        .tag = .fromTypeImport(tag),
                                        .offset = offset,
                                        .pointee = .{ .symbol_name = sym.name.unwrap().? },
                                        .addend = undefined,
                                    },
                                    else => unreachable,
                                });
                            },
                            .global_index_leb, .global_index_i32 => {
                                wasm.object_relocations.appendAssumeCapacity(switch (sym.pointee) {
                                    .global => .{
                                        .tag = .fromType(tag),
                                        .offset = offset,
                                        .pointee = .{ .global = sym.pointee.global },
                                        .addend = undefined,
                                    },
                                    .global_import => .{
                                        .tag = .fromTypeImport(tag),
                                        .offset = offset,
                                        .pointee = .{ .symbol_name = sym.name.unwrap().? },
                                        .addend = undefined,
                                    },
                                    else => unreachable,
                                });
                            },

                            .table_number_leb => {
                                wasm.object_relocations.appendAssumeCapacity(switch (sym.pointee) {
                                    .table => .{
                                        .tag = .fromType(tag),
                                        .offset = offset,
                                        .pointee = .{ .table = sym.pointee.table },
                                        .addend = undefined,
                                    },
                                    .table_import => .{
                                        .tag = .fromTypeImport(tag),
                                        .offset = offset,
                                        .pointee = .{ .symbol_name = sym.name.unwrap().? },
                                        .addend = undefined,
                                    },
                                    else => unreachable,
                                });
                            },
                            .event_index_leb => return diags.failParse(path, "unsupported relocation: R_WASM_EVENT_INDEX_LEB", .{}),
                        }
                    }

                    try wasm.object_relocations_table.putNoClobber(gpa, section, .{
                        .off = @intCast(wasm.object_relocations.len - count),
                        .len = count,
                    });
                } else if (std.mem.eql(u8, section_name, "target_features")) {
                    opt_features, pos = try parseFeatures(wasm, bytes, pos, path);
                } else if (std.mem.startsWith(u8, section_name, ".debug")) {
                    const debug_content = bytes[pos..section_end];
                    pos = section_end;

                    const data_off: u32 = @intCast(wasm.string_bytes.items.len);
                    try wasm.string_bytes.appendSlice(gpa, debug_content);

                    try wasm.object_custom_segments.put(gpa, section_index, .{
                        .payload = .{
                            .off = @enumFromInt(data_off),
                            .len = @intCast(debug_content.len),
                        },
                        .flags = .{},
                        .section_name = try wasm.internString(section_name),
                    });
                } else {
                    pos = section_end;
                }
            },
            .type => {
                const func_types_len, pos = readLeb(u32, bytes, pos);
                for (try ss.func_types.addManyAsSlice(gpa, func_types_len)) |*func_type| {
                    if (bytes[pos] != std.wasm.function_type) return error.ExpectedFuncType;
                    pos += 1;

                    const params, pos = readBytes(bytes, pos);
                    const returns, pos = readBytes(bytes, pos);
                    func_type.* = try wasm.addFuncType(.{
                        .params = .fromString(try wasm.internString(params)),
                        .returns = .fromString(try wasm.internString(returns)),
                    });
                }
            },
            .import => {
                const imports_len, pos = readLeb(u32, bytes, pos);
                for (0..imports_len) |_| {
                    const module_name, pos = readBytes(bytes, pos);
                    const name, pos = readBytes(bytes, pos);
                    const kind, pos = readEnum(std.wasm.ExternalKind, bytes, pos);
                    const interned_module_name = try wasm.internString(module_name);
                    const interned_name = try wasm.internString(name);
                    switch (kind) {
                        .function => {
                            const function, pos = readLeb(u32, bytes, pos);
                            try ss.func_imports.append(gpa, .{
                                .module_name = interned_module_name,
                                .name = interned_name,
                                .function_index = @enumFromInt(function),
                            });
                        },
                        .memory => {
                            const limits, pos = readLimits(bytes, pos);
                            const gop = try wasm.object_memory_imports.getOrPut(gpa, interned_name);
                            if (gop.found_existing) {
                                if (gop.value_ptr.module_name != interned_module_name) {
                                    var err = try diags.addErrorWithNotes(2);
                                    try err.addMsg("memory '{s}' mismatching module names", .{name});
                                    gop.value_ptr.source_location.addNote(&err, "module '{s}' here", .{
                                        gop.value_ptr.module_name.slice(wasm),
                                    });
                                    source_location.addNote(&err, "module '{s}' here", .{module_name});
                                }
                                // TODO error for mismatching flags
                                gop.value_ptr.limits_min = @min(gop.value_ptr.limits_min, limits.min);
                                gop.value_ptr.limits_max = @max(gop.value_ptr.limits_max, limits.max);
                            } else {
                                gop.value_ptr.* = .{
                                    .module_name = interned_module_name,
                                    .limits_min = limits.min,
                                    .limits_max = limits.max,
                                    .limits_has_max = limits.flags.has_max,
                                    .limits_is_shared = limits.flags.is_shared,
                                    .source_location = source_location,
                                };
                            }
                        },
                        .global => {
                            const valtype, pos = readEnum(std.wasm.Valtype, bytes, pos);
                            const mutable = bytes[pos] == 0x01;
                            pos += 1;
                            try ss.global_imports.append(gpa, .{
                                .name = interned_name,
                                .valtype = valtype,
                                .mutable = mutable,
                                .module_name = interned_module_name,
                            });
                        },
                        .table => {
                            const ref_type, pos = readEnum(std.wasm.RefType, bytes, pos);
                            const limits, pos = readLimits(bytes, pos);
                            try ss.table_imports.append(gpa, .{
                                .name = interned_name,
                                .module_name = interned_module_name,
                                .limits_min = limits.min,
                                .limits_max = limits.max,
                                .limits_has_max = limits.flags.has_max,
                                .limits_is_shared = limits.flags.is_shared,
                                .ref_type = ref_type,
                            });
                        },
                    }
                }
            },
            .function => {
                const functions_len, pos = readLeb(u32, bytes, pos);
                for (try ss.func_type_indexes.addManyAsSlice(gpa, functions_len)) |*func_type_index| {
                    const i, pos = readLeb(u32, bytes, pos);
                    func_type_index.* = @enumFromInt(i);
                }
            },
            .table => {
                const tables_len, pos = readLeb(u32, bytes, pos);
                for (try wasm.object_tables.addManyAsSlice(gpa, tables_len)) |*table| {
                    const ref_type, pos = readEnum(std.wasm.RefType, bytes, pos);
                    const limits, pos = readLimits(bytes, pos);
                    table.* = .{
                        .name = .none,
                        .module_name = .none,
                        .flags = .{
                            .ref_type = .from(ref_type),
                            .limits_has_max = limits.flags.has_max,
                            .limits_is_shared = limits.flags.is_shared,
                        },
                        .limits_min = limits.min,
                        .limits_max = limits.max,
                    };
                }
            },
            .memory => {
                const memories_len, pos = readLeb(u32, bytes, pos);
                for (try wasm.object_memories.addManyAsSlice(gpa, memories_len)) |*memory| {
                    const limits, pos = readLimits(bytes, pos);
                    memory.* = .{
                        .name = .none,
                        .flags = .{
                            .limits_has_max = limits.flags.has_max,
                            .limits_is_shared = limits.flags.is_shared,
                        },
                        .limits_min = limits.min,
                        .limits_max = limits.max,
                    };
                }
            },
            .global => {
                if (global_section_index != null)
                    return diags.failParse(path, "object has more than one global section", .{});
                global_section_index = section_index;

                const section_start = pos;
                const globals_len, pos = readLeb(u32, bytes, pos);
                for (try wasm.object_globals.addManyAsSlice(gpa, globals_len)) |*global| {
                    const valtype, pos = readEnum(std.wasm.Valtype, bytes, pos);
                    const mutable = bytes[pos] == 0x01;
                    pos += 1;
                    const init_start = pos;
                    const expr, pos = try readInit(wasm, bytes, pos);
                    global.* = .{
                        .name = .none,
                        .flags = .{
                            .global_type = .{
                                .valtype = .from(valtype),
                                .mutable = mutable,
                            },
                        },
                        .expr = expr,
                        .object_index = object_index,
                        .offset = @intCast(init_start - section_start),
                        .size = @intCast(pos - init_start),
                    };
                }
            },
            .@"export" => {
                const exports_len, pos = readLeb(u32, bytes, pos);
                // Read into scratch space, and then later add this data as if
                // it were extra symbol table entries, but allow merging with
                // existing symbol table data if the name matches.
                for (try ss.exports.addManyAsSlice(gpa, exports_len)) |*exp| {
                    const name, pos = readBytes(bytes, pos);
                    const kind: std.wasm.ExternalKind = @enumFromInt(bytes[pos]);
                    pos += 1;
                    const index, pos = readLeb(u32, bytes, pos);
                    exp.* = .{
                        .name = try wasm.internString(name),
                        .pointee = switch (kind) {
                            .function => .{ .function = @enumFromInt(functions_start + (index - ss.func_imports.items.len)) },
                            .table => .{ .table = @enumFromInt(tables_start + (index - ss.table_imports.items.len)) },
                            .memory => .{ .memory = @enumFromInt(memories_start + index) },
                            .global => .{ .global = @enumFromInt(globals_start + (index - ss.global_imports.items.len)) },
                        },
                    };
                }
            },
            .start => {
                const index, pos = readLeb(u32, bytes, pos);
                start_function = @enumFromInt(functions_start + index);
            },
            .element => {
                log.warn("unimplemented: element section in {} {?s}", .{ path, archive_member_name });
                pos = section_end;
            },
            .code => {
                if (code_section_index != null)
                    return diags.failParse(path, "object has more than one code section", .{});
                code_section_index = section_index;

                const start = pos;
                const count, pos = readLeb(u32, bytes, pos);
                for (try wasm.object_functions.addManyAsSlice(gpa, count)) |*elem| {
                    const code_len, pos = readLeb(u32, bytes, pos);
                    const offset: u32 = @intCast(pos - start);
                    const payload = try wasm.addRelocatableDataPayload(bytes[pos..][0..code_len]);
                    pos += code_len;
                    elem.* = .{
                        .flags = .{}, // populated from symbol table
                        .name = .none, // populated from symbol table
                        .type_index = undefined, // populated from func_types
                        .code = payload,
                        .offset = offset,
                        .object_index = object_index,
                    };
                }
            },
            .data => {
                if (data_section_index != null)
                    return diags.failParse(path, "object has more than one data section", .{});
                data_section_index = section_index;

                const section_start = pos;
                const count, pos = readLeb(u32, bytes, pos);
                for (try wasm.object_data_segments.addManyAsSlice(gpa, count)) |*elem| {
                    const flags, pos = readEnum(DataSegmentFlags, bytes, pos);
                    if (flags == .active_memidx) {
                        const memidx, pos = readLeb(u32, bytes, pos);
                        if (memidx != 0) return diags.failParse(path, "data section uses mem index {d}", .{memidx});
                    }
                    //const expr, pos = if (flags != .passive) try readInit(wasm, bytes, pos) else .{ .none, pos };
                    if (flags != .passive) pos = try skipInit(bytes, pos);
                    const data_len, pos = readLeb(u32, bytes, pos);
                    const segment_start = pos;
                    const payload = try wasm.addRelocatableDataPayload(bytes[pos..][0..data_len]);
                    pos += data_len;
                    elem.* = .{
                        .payload = payload,
                        .name = .none, // Populated from segment_info
                        .flags = .{
                            .is_passive = flags == .passive,
                        }, // Remainder populated from segment_info
                        .offset = @intCast(segment_start - section_start),
                        .object_index = object_index,
                    };
                }
            },
            else => pos = section_end,
        }
        if (pos != section_end) return error.MalformedSection;
    }
    if (!saw_linking_section) return error.MissingLinkingSection;

    const target_features = comp.root_mod.resolved_target.result.cpu.features;

    if (has_tls) {
        if (!std.Target.wasm.featureSetHas(target_features, .atomics))
            return diags.failParse(path, "object has TLS segment but target CPU feature atomics is disabled", .{});
        if (!std.Target.wasm.featureSetHas(target_features, .bulk_memory))
            return diags.failParse(path, "object has TLS segment but target CPU feature bulk_memory is disabled", .{});
    }

    const features = opt_features orelse return error.MissingFeatures;
    for (features.slice(wasm)) |feat| {
        log.debug("feature: {s}{s}", .{ @tagName(feat.prefix), @tagName(feat.tag) });
        switch (feat.prefix) {
            .invalid => unreachable,
            .@"-" => switch (feat.tag) {
                .@"shared-mem" => if (comp.config.shared_memory) {
                    return diags.failParse(path, "object forbids shared-mem but compilation enables it", .{});
                },
                else => {
                    const f = feat.tag.toCpuFeature().?;
                    if (std.Target.wasm.featureSetHas(target_features, f)) {
                        return diags.failParse(
                            path,
                            "object forbids {s} but specified target features include {s}",
                            .{ @tagName(feat.tag), @tagName(f) },
                        );
                    }
                },
            },
            .@"+", .@"=" => switch (feat.tag) {
                .@"shared-mem" => if (!comp.config.shared_memory) {
                    return diags.failParse(path, "object requires shared-mem but compilation disables it", .{});
                },
                else => {
                    const f = feat.tag.toCpuFeature().?;
                    if (!std.Target.wasm.featureSetHas(target_features, f)) {
                        return diags.failParse(
                            path,
                            "object requires {s} but specified target features exclude {s}",
                            .{ @tagName(feat.tag), @tagName(f) },
                        );
                    }
                },
            },
        }
    }

    // Apply function type information.
    for (ss.func_type_indexes.items, wasm.object_functions.items[functions_start..]) |func_type, *func| {
        func.type_index = func_type.ptr(ss).*;
    }

    // Apply symbol table information.
    for (ss.symbol_table.items) |symbol| switch (symbol.pointee) {
        .function_import => |index| {
            const ptr = index.ptr(ss);
            const name = symbol.name.unwrap() orelse ptr.name;
            if (symbol.flags.binding == .local) {
                diags.addParseError(path, "local symbol '{s}' references import", .{name.slice(wasm)});
                continue;
            }
            const gop = try wasm.object_function_imports.getOrPut(gpa, name);
            const fn_ty_index = ptr.function_index.ptr(ss).*;
            if (gop.found_existing) {
                if (gop.value_ptr.type != fn_ty_index) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching function signatures", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "imported as {} here", .{
                        gop.value_ptr.type.fmt(wasm),
                    });
                    source_location.addNote(&err, "imported as {} here", .{fn_ty_index.fmt(wasm)});
                    continue;
                }
                if (gop.value_ptr.module_name != ptr.module_name.toOptional()) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching module names", .{name.slice(wasm)});
                    if (gop.value_ptr.module_name.slice(wasm)) |module_name| {
                        gop.value_ptr.source_location.addNote(&err, "module '{s}' here", .{module_name});
                    } else {
                        gop.value_ptr.source_location.addNote(&err, "no module here", .{});
                    }
                    source_location.addNote(&err, "module '{s}' here", .{ptr.module_name.slice(wasm)});
                    continue;
                }
                if (gop.value_ptr.name != ptr.name) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching import names", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "imported as '{s}' here", .{gop.value_ptr.name.slice(wasm)});
                    source_location.addNote(&err, "imported as '{s}' here", .{ptr.name.slice(wasm)});
                    continue;
                }
            } else {
                gop.value_ptr.* = .{
                    .flags = symbol.flags,
                    .module_name = ptr.module_name.toOptional(),
                    .name = ptr.name,
                    .source_location = source_location,
                    .resolution = .unresolved,
                    .type = fn_ty_index,
                };
            }
        },
        .global_import => |index| {
            const ptr = index.ptr(ss);
            const name = symbol.name.unwrap() orelse ptr.name;
            if (symbol.flags.binding == .local) {
                diags.addParseError(path, "local symbol '{s}' references import", .{name.slice(wasm)});
                continue;
            }
            const gop = try wasm.object_global_imports.getOrPut(gpa, name);
            if (gop.found_existing) {
                const existing_ty = gop.value_ptr.type();
                if (ptr.valtype != existing_ty.valtype) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching global types", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "type {s} here", .{@tagName(existing_ty.valtype)});
                    source_location.addNote(&err, "type {s} here", .{@tagName(ptr.valtype)});
                    continue;
                }
                if (ptr.mutable != existing_ty.mutable) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching global mutability", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "{s} here", .{
                        if (existing_ty.mutable) "mutable" else "not mutable",
                    });
                    source_location.addNote(&err, "{s} here", .{
                        if (ptr.mutable) "mutable" else "not mutable",
                    });
                    continue;
                }
                if (gop.value_ptr.module_name != ptr.module_name.toOptional()) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching module names", .{name.slice(wasm)});
                    if (gop.value_ptr.module_name.slice(wasm)) |module_name| {
                        gop.value_ptr.source_location.addNote(&err, "module '{s}' here", .{module_name});
                    } else {
                        gop.value_ptr.source_location.addNote(&err, "no module here", .{});
                    }
                    source_location.addNote(&err, "module '{s}' here", .{ptr.module_name.slice(wasm)});
                    continue;
                }
                if (gop.value_ptr.name != ptr.name) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching import names", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "imported as '{s}' here", .{gop.value_ptr.name.slice(wasm)});
                    source_location.addNote(&err, "imported as '{s}' here", .{ptr.name.slice(wasm)});
                    continue;
                }
            } else {
                gop.value_ptr.* = .{
                    .flags = symbol.flags,
                    .module_name = ptr.module_name.toOptional(),
                    .name = ptr.name,
                    .source_location = source_location,
                    .resolution = .unresolved,
                };
                gop.value_ptr.flags.global_type = .{
                    .valtype = .from(ptr.valtype),
                    .mutable = ptr.mutable,
                };
            }
        },
        .table_import => |index| {
            const ptr = index.ptr(ss);
            const name = symbol.name.unwrap() orelse ptr.name;
            if (symbol.flags.binding == .local) {
                diags.addParseError(path, "local symbol '{s}' references import", .{name.slice(wasm)});
                continue;
            }
            const gop = try wasm.object_table_imports.getOrPut(gpa, name);
            if (gop.found_existing) {
                const existing_reftype = gop.value_ptr.flags.ref_type.to();
                if (ptr.ref_type != existing_reftype) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching table reftypes", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "{s} here", .{@tagName(existing_reftype)});
                    source_location.addNote(&err, "{s} here", .{@tagName(ptr.ref_type)});
                    continue;
                }
                if (gop.value_ptr.module_name != ptr.module_name) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching module names", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "module '{s}' here", .{
                        gop.value_ptr.module_name.slice(wasm),
                    });
                    source_location.addNote(&err, "module '{s}' here", .{ptr.module_name.slice(wasm)});
                    continue;
                }
                if (gop.value_ptr.name != ptr.name) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching import names", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "imported as '{s}' here", .{gop.value_ptr.name.slice(wasm)});
                    source_location.addNote(&err, "imported as '{s}' here", .{ptr.name.slice(wasm)});
                    continue;
                }
                if (symbol.flags.binding == .strong) gop.value_ptr.flags.binding = .strong;
                if (!symbol.flags.visibility_hidden) gop.value_ptr.flags.visibility_hidden = false;
                if (symbol.flags.no_strip) gop.value_ptr.flags.no_strip = true;
            } else {
                gop.value_ptr.* = .{
                    .flags = symbol.flags,
                    .module_name = ptr.module_name,
                    .name = ptr.name,
                    .source_location = source_location,
                    .resolution = .unresolved,
                    .limits_min = ptr.limits_min,
                    .limits_max = ptr.limits_max,
                };
                gop.value_ptr.flags.limits_has_max = ptr.limits_has_max;
                gop.value_ptr.flags.limits_is_shared = ptr.limits_is_shared;
                gop.value_ptr.flags.ref_type = .from(ptr.ref_type);
            }
        },
        .data_import => {
            const name = symbol.name.unwrap().?;
            if (symbol.flags.binding == .local) {
                diags.addParseError(path, "local symbol '{s}' references import", .{name.slice(wasm)});
                continue;
            }
            const gop = try wasm.object_data_imports.getOrPut(gpa, name);
            if (!gop.found_existing) gop.value_ptr.* = .{
                .flags = symbol.flags,
                .source_location = source_location,
                .resolution = .unresolved,
            };
        },
        .function => |index| {
            assert(!symbol.flags.undefined);
            const ptr = index.ptr(wasm);
            ptr.name = symbol.name;
            ptr.flags = symbol.flags;
            if (symbol.flags.binding == .local) continue; // No participation in symbol resolution.
            const name = symbol.name.unwrap().?;
            const gop = try wasm.object_function_imports.getOrPut(gpa, name);
            if (gop.found_existing) {
                if (gop.value_ptr.type != ptr.type_index) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("function signature mismatch: {s}", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "exported as {} here", .{
                        ptr.type_index.fmt(wasm),
                    });
                    const word = if (gop.value_ptr.resolution == .unresolved) "imported" else "exported";
                    source_location.addNote(&err, "{s} as {} here", .{ word, gop.value_ptr.type.fmt(wasm) });
                    continue;
                }
                if (gop.value_ptr.resolution == .unresolved or gop.value_ptr.flags.binding == .weak) {
                    // Intentional: if they're both weak, take the last one.
                    gop.value_ptr.source_location = source_location;
                    gop.value_ptr.module_name = host_name;
                    gop.value_ptr.resolution = .fromObjectFunction(wasm, index);
                    gop.value_ptr.flags = symbol.flags;
                    continue;
                }
                if (ptr.flags.binding == .weak) {
                    // Keep the existing one.
                    continue;
                }
                var err = try diags.addErrorWithNotes(2);
                try err.addMsg("symbol collision: {s}", .{name.slice(wasm)});
                gop.value_ptr.source_location.addNote(&err, "exported as {} here", .{ptr.type_index.fmt(wasm)});
                source_location.addNote(&err, "exported as {} here", .{gop.value_ptr.type.fmt(wasm)});
                continue;
            } else {
                gop.value_ptr.* = .{
                    .flags = symbol.flags,
                    .module_name = host_name,
                    .name = name,
                    .source_location = source_location,
                    .resolution = .fromObjectFunction(wasm, index),
                    .type = ptr.type_index,
                };
            }
        },
        .global => |index| {
            assert(!symbol.flags.undefined);
            const ptr = index.ptr(wasm);
            ptr.name = symbol.name;
            ptr.flags = symbol.flags;
            if (symbol.flags.binding == .local) continue; // No participation in symbol resolution.
            const name = symbol.name.unwrap().?;
            const new_ty = ptr.type();
            const gop = try wasm.object_global_imports.getOrPut(gpa, name);
            if (gop.found_existing) {
                const existing_ty = gop.value_ptr.type();
                if (new_ty.valtype != existing_ty.valtype) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching global types", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "type {s} here", .{@tagName(existing_ty.valtype)});
                    source_location.addNote(&err, "type {s} here", .{@tagName(new_ty.valtype)});
                    continue;
                }
                if (new_ty.mutable != existing_ty.mutable) {
                    var err = try diags.addErrorWithNotes(2);
                    try err.addMsg("symbol '{s}' mismatching global mutability", .{name.slice(wasm)});
                    gop.value_ptr.source_location.addNote(&err, "{s} here", .{
                        if (existing_ty.mutable) "mutable" else "not mutable",
                    });
                    source_location.addNote(&err, "{s} here", .{
                        if (new_ty.mutable) "mutable" else "not mutable",
                    });
                    continue;
                }
                if (gop.value_ptr.resolution == .unresolved or gop.value_ptr.flags.binding == .weak) {
                    // Intentional: if they're both weak, take the last one.
                    gop.value_ptr.source_location = source_location;
                    gop.value_ptr.module_name = host_name;
                    gop.value_ptr.resolution = .fromObjectGlobal(wasm, index);
                    gop.value_ptr.flags = symbol.flags;
                    continue;
                }
                if (ptr.flags.binding == .weak) {
                    // Keep the existing one.
                    continue;
                }
                var err = try diags.addErrorWithNotes(2);
                try err.addMsg("symbol collision: {s}", .{name.slice(wasm)});
                gop.value_ptr.source_location.addNote(&err, "exported as {s} here", .{@tagName(existing_ty.valtype)});
                source_location.addNote(&err, "exported as {s} here", .{@tagName(new_ty.valtype)});
                continue;
            } else {
                gop.value_ptr.* = .{
                    .flags = symbol.flags,
                    .module_name = .none,
                    .name = name,
                    .source_location = source_location,
                    .resolution = .fromObjectGlobal(wasm, index),
                };
                gop.value_ptr.flags.global_type = .{
                    .valtype = .from(new_ty.valtype),
                    .mutable = new_ty.mutable,
                };
            }
        },
        .table => |i| {
            assert(!symbol.flags.undefined);
            const ptr = i.ptr(wasm);
            ptr.name = symbol.name;
            ptr.flags = symbol.flags;
        },
        .data => |index| {
            assert(!symbol.flags.undefined);
            const ptr = index.ptr(wasm);
            const name = ptr.name;
            assert(name.toOptional() == symbol.name);
            ptr.flags = symbol.flags;
            if (symbol.flags.binding == .local) continue; // No participation in symbol resolution.
            const gop = try wasm.object_data_imports.getOrPut(gpa, name);
            if (gop.found_existing) {
                if (gop.value_ptr.resolution == .unresolved or gop.value_ptr.flags.binding == .weak) {
                    // Intentional: if they're both weak, take the last one.
                    gop.value_ptr.source_location = source_location;
                    gop.value_ptr.resolution = .fromObjectDataIndex(wasm, index);
                    gop.value_ptr.flags = symbol.flags;
                    continue;
                }
                if (ptr.flags.binding == .weak) {
                    // Keep the existing one.
                    continue;
                }
                var err = try diags.addErrorWithNotes(2);
                try err.addMsg("symbol collision: {s}", .{name.slice(wasm)});
                gop.value_ptr.source_location.addNote(&err, "exported here", .{});
                source_location.addNote(&err, "exported here", .{});
                continue;
            } else {
                gop.value_ptr.* = .{
                    .flags = symbol.flags,
                    .source_location = source_location,
                    .resolution = .fromObjectDataIndex(wasm, index),
                };
            }
        },
        .section => |i| {
            // Name is provided by the section directly; symbol table does not have it.
            //const ptr = i.ptr(wasm);
            //ptr.flags = symbol.flags;
            _ = i;
            if (symbol.flags.undefined and symbol.flags.binding == .local) {
                const name = symbol.name.slice(wasm).?;
                diags.addParseError(path, "local symbol '{s}' references import", .{name});
            }
        },
    };

    // Apply export section info. This is done after the symbol table above so
    // that the symbol table can take precedence, overriding the export name.
    for (ss.exports.items) |*exp| {
        switch (exp.pointee) {
            inline .function, .table, .memory, .global => |index| {
                const ptr = index.ptr(wasm);
                ptr.name = exp.name.toOptional();
                ptr.flags.exported = true;
            },
        }
    }

    // Apply segment_info.
    const data_segments = wasm.object_data_segments.items[data_segment_start..];
    if (data_segments.len != ss.segment_info.items.len) {
        return diags.failParse(path, "expected {d} segment_info entries; found {d}", .{
            data_segments.len, ss.segment_info.items.len,
        });
    }
    for (data_segments, ss.segment_info.items) |*data, info| {
        data.name = info.name.toOptional();
        data.flags = .{
            .is_passive = data.flags.is_passive,
            .strings = info.flags.strings,
            .tls = info.flags.tls,
            .retain = info.flags.retain,
            .alignment = info.flags.alignment,
        };
    }

    // Check for indirect function table in case of an MVP object file.
    legacy_indirect_function_table: {
        // If there is a symbol for each import table, this is not a legacy object file.
        if (ss.table_imports.items.len == table_import_symbol_count) break :legacy_indirect_function_table;
        if (table_import_symbol_count != 0) {
            return diags.failParse(path, "expected a table entry symbol for each of the {d} table(s), but instead got {d} symbols.", .{
                ss.table_imports.items.len, table_import_symbol_count,
            });
        }
        // MVP object files cannot have any table definitions, only imports
        // (for the indirect function table).
        const tables = wasm.object_tables.items[tables_start..];
        if (tables.len > 0) {
            return diags.failParse(path, "table definition without representing table symbols", .{});
        }
        if (ss.table_imports.items.len != 1) {
            return diags.failParse(path, "found more than one table import, but no representing table symbols", .{});
        }
        const table_import_name = ss.table_imports.items[0].name;
        if (table_import_name != wasm.preloaded_strings.__indirect_function_table) {
            return diags.failParse(path, "non-indirect function table import '{s}' is missing a corresponding symbol", .{
                table_import_name.slice(wasm),
            });
        }
        const ptr = wasm.object_table_imports.getPtr(table_import_name).?;
        ptr.flags = .{
            .undefined = true,
            .no_strip = true,
        };
    }

    for (wasm.object_init_funcs.items[init_funcs_start..]) |init_func| {
        const func = init_func.function_index.ptr(wasm);
        const params = func.type_index.ptr(wasm).params.slice(wasm);
        if (params.len != 0) diags.addError("constructor function '{s}' has non-empty parameter list", .{
            func.name.slice(wasm).?,
        });
    }

    const functions_len: u32 = @intCast(wasm.object_functions.items.len - functions_start);
    if (functions_len > 0 and code_section_index == null)
        return diags.failParse(path, "code section missing ({d} functions)", .{functions_len});

    return .{
        .version = version,
        .path = path,
        .archive_member_name = try wasm.internOptionalString(archive_member_name),
        .start_function = start_function,
        .features = features,
        .functions = .{
            .off = functions_start,
            .len = functions_len,
        },
        .function_imports = .{
            .off = function_imports_start,
            .len = @intCast(wasm.object_function_imports.entries.len - function_imports_start),
        },
        .global_imports = .{
            .off = global_imports_start,
            .len = @intCast(wasm.object_global_imports.entries.len - global_imports_start),
        },
        .table_imports = .{
            .off = table_imports_start,
            .len = @intCast(wasm.object_table_imports.entries.len - table_imports_start),
        },
        .data_imports = .{
            .off = data_imports_start,
            .len = @intCast(wasm.object_data_imports.entries.len - data_imports_start),
        },
        .init_funcs = .{
            .off = init_funcs_start,
            .len = @intCast(wasm.object_init_funcs.items.len - init_funcs_start),
        },
        .comdats = .{
            .off = comdats_start,
            .len = @intCast(wasm.object_comdats.items.len - comdats_start),
        },
        .custom_segments = .{
            .off = custom_segment_start,
            .len = @intCast(wasm.object_custom_segments.entries.len - custom_segment_start),
        },
        .code_section_index = code_section_index,
        .global_section_index = global_section_index,
        .data_section_index = data_section_index,
        .is_included = must_link,
    };
}

/// Based on the "features" custom section, parses it into a list of
/// features that tell the linker what features were enabled and may be mandatory
/// to be able to link.
fn parseFeatures(
    wasm: *Wasm,
    bytes: []const u8,
    start_pos: usize,
    path: Path,
) error{ OutOfMemory, LinkFailure }!struct { Wasm.Feature.Set, usize } {
    const gpa = wasm.base.comp.gpa;
    const diags = &wasm.base.comp.link_diags;
    const features_len, var pos = readLeb(u32, bytes, start_pos);
    // This temporary allocation could be avoided by using the string_bytes buffer as a scratch space.
    const feature_buffer = try gpa.alloc(Wasm.Feature, features_len);
    defer gpa.free(feature_buffer);
    for (feature_buffer) |*feature| {
        const prefix: Wasm.Feature.Prefix = switch (bytes[pos]) {
            '-' => .@"-",
            '+' => .@"+",
            '=' => .@"=",
            else => |b| return diags.failParse(path, "invalid feature prefix: 0x{x}", .{b}),
        };
        pos += 1;
        const name, pos = readBytes(bytes, pos);
        const tag = std.meta.stringToEnum(Wasm.Feature.Tag, name) orelse {
            return diags.failParse(path, "unrecognized wasm feature in object: {s}", .{name});
        };
        feature.* = .{
            .prefix = prefix,
            .tag = tag,
        };
    }
    std.mem.sortUnstable(Wasm.Feature, feature_buffer, {}, Wasm.Feature.lessThan);

    return .{
        .fromString(try wasm.internString(@ptrCast(feature_buffer))),
        pos,
    };
}

fn readLeb(comptime T: type, bytes: []const u8, pos: usize) struct { T, usize } {
    var fbr = std.io.fixedBufferStream(bytes[pos..]);
    return .{
        switch (@typeInfo(T).int.signedness) {
            .signed => std.leb.readIleb128(T, fbr.reader()) catch unreachable,
            .unsigned => std.leb.readUleb128(T, fbr.reader()) catch unreachable,
        },
        pos + fbr.pos,
    };
}

fn readBytes(bytes: []const u8, start_pos: usize) struct { []const u8, usize } {
    const len, const pos = readLeb(u32, bytes, start_pos);
    return .{
        bytes[pos..][0..len],
        pos + len,
    };
}

fn readEnum(comptime T: type, bytes: []const u8, pos: usize) struct { T, usize } {
    const Tag = @typeInfo(T).@"enum".tag_type;
    const int, const new_pos = readLeb(Tag, bytes, pos);
    return .{ @enumFromInt(int), new_pos };
}

fn readLimits(bytes: []const u8, start_pos: usize) struct { std.wasm.Limits, usize } {
    const flags: std.wasm.Limits.Flags = @bitCast(bytes[start_pos]);
    const min, const max_pos = readLeb(u32, bytes, start_pos + 1);
    const max, const end_pos = if (flags.has_max) readLeb(u32, bytes, max_pos) else .{ 0, max_pos };
    return .{ .{
        .flags = flags,
        .min = min,
        .max = max,
    }, end_pos };
}

fn readInit(wasm: *Wasm, bytes: []const u8, pos: usize) !struct { Wasm.Expr, usize } {
    const end_pos = try skipInit(bytes, pos); // one after the end opcode
    return .{ try wasm.addExpr(bytes[pos..end_pos]), end_pos };
}

pub fn exprEndPos(bytes: []const u8, pos: usize) error{InvalidInitOpcode}!usize {
    const opcode = bytes[pos];
    return switch (@as(std.wasm.Opcode, @enumFromInt(opcode))) {
        .i32_const => readLeb(i32, bytes, pos + 1)[1],
        .i64_const => readLeb(i64, bytes, pos + 1)[1],
        .f32_const => pos + 5,
        .f64_const => pos + 9,
        .global_get => readLeb(u32, bytes, pos + 1)[1],
        else => return error.InvalidInitOpcode,
    };
}

fn skipInit(bytes: []const u8, pos: usize) !usize {
    const end_pos = try exprEndPos(bytes, pos);
    const op, const final_pos = readEnum(std.wasm.Opcode, bytes, end_pos);
    if (op != .end) return error.InitExprMissingEnd;
    return final_pos;
}
