Commit 5ba5a2c133

Luuk de Gram <luuk@degram.dev>
2024-02-12 17:12:53
wasm: integrate linker errors with `Compilation`
Rather than using the logger, we now emit proper 'compiler'-errors just like the ELF and MachO linkers with notes. We now also support emitting multiple errors before quiting the linking process in certain phases, such as symbol resolution. This means we will print all symbols which were resolved incorrectly, rather than the first one we encounter.
1 parent 5ef8321
Changed files (4)
src/link/Wasm/Archive.zig
@@ -1,14 +1,3 @@
-const Archive = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const fs = std.fs;
-const log = std.log.scoped(.archive);
-const mem = std.mem;
-
-const Allocator = mem.Allocator;
-const Object = @import("Object.zig");
-
 file: fs.File,
 name: []const u8,
 
@@ -151,10 +140,7 @@ fn parseTableOfContents(archive: *Archive, allocator: Allocator, reader: anytype
     const sym_tab = try allocator.alloc(u8, sym_tab_size - 4 - (4 * num_symbols));
     defer allocator.free(sym_tab);
 
-    reader.readNoEof(sym_tab) catch {
-        log.err("incomplete symbol table: expected symbol table of length 0x{x}", .{sym_tab.len});
-        return error.MalformedArchive;
-    };
+    reader.readNoEof(sym_tab) catch return error.IncompleteSymbolTable;
 
     var i: usize = 0;
     var pos: usize = 0;
@@ -178,12 +164,10 @@ fn parseTableOfContents(archive: *Archive, allocator: Allocator, reader: anytype
 fn parseNameTable(archive: *Archive, allocator: Allocator, reader: anytype) !void {
     const header: ar_hdr = try reader.readStruct(ar_hdr);
     if (!mem.eql(u8, &header.ar_fmag, ARFMAG)) {
-        log.err("invalid header delimiter: expected '{s}', found '{s}'", .{ ARFMAG, header.ar_fmag });
-        return error.MalformedArchive;
+        return error.InvalidHeaderDelimiter;
     }
     if (!mem.eql(u8, header.ar_name[0..2], "//")) {
-        log.err("invalid archive. Long name table missing", .{});
-        return error.MalformedArchive;
+        return error.MissingTableName;
     }
     const table_size = try header.size();
     const long_file_names = try allocator.alloc(u8, table_size);
@@ -194,7 +178,8 @@ fn parseNameTable(archive: *Archive, allocator: Allocator, reader: anytype) !voi
 
 /// From a given file offset, starts reading for a file header.
 /// When found, parses the object file into an `Object` and returns it.
-pub fn parseObject(archive: Archive, allocator: Allocator, file_offset: u32) !Object {
+pub fn parseObject(archive: Archive, wasm_file: *const Wasm, file_offset: u32) !Object {
+    const gpa = wasm_file.base.comp.gpa;
     try archive.file.seekTo(file_offset);
     const reader = archive.file.reader();
     const header = try reader.readStruct(ar_hdr);
@@ -202,22 +187,33 @@ pub fn parseObject(archive: Archive, allocator: Allocator, file_offset: u32) !Ob
     try archive.file.seekTo(0);
 
     if (!mem.eql(u8, &header.ar_fmag, ARFMAG)) {
-        log.err("invalid header delimiter: expected '{s}', found '{s}'", .{ ARFMAG, header.ar_fmag });
-        return error.MalformedArchive;
+        return error.InvalidHeaderDelimiter;
     }
 
     const object_name = try archive.parseName(header);
     const name = name: {
         var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
         const path = try std.os.realpath(archive.name, &buffer);
-        break :name try std.fmt.allocPrint(allocator, "{s}({s})", .{ path, object_name });
+        break :name try std.fmt.allocPrint(gpa, "{s}({s})", .{ path, object_name });
     };
-    defer allocator.free(name);
+    defer gpa.free(name);
 
     const object_file = try std.fs.cwd().openFile(archive.name, .{});
     errdefer object_file.close();
 
     const object_file_size = try header.size();
     try object_file.seekTo(current_offset);
-    return Object.create(allocator, object_file, name, object_file_size);
+    return Object.create(wasm_file, object_file, name, object_file_size);
 }
+
+const std = @import("std");
+const assert = std.debug.assert;
+const fs = std.fs;
+const log = std.log.scoped(.archive);
+const mem = std.mem;
+
+const Allocator = mem.Allocator;
+const Object = @import("Object.zig");
+const Wasm = @import("../Wasm.zig");
+
+const Archive = @This();
src/link/Wasm/Object.zig
@@ -127,7 +127,8 @@ pub const InitError = error{NotObjectFile} || ParseError || std.fs.File.ReadErro
 /// This also parses and verifies the object file.
 /// When a max size is given, will only parse up to the given size,
 /// else will read until the end of the file.
-pub fn create(gpa: Allocator, file: std.fs.File, name: []const u8, maybe_max_size: ?usize) InitError!Object {
+pub fn create(wasm_file: *const Wasm, file: std.fs.File, name: []const u8, maybe_max_size: ?usize) InitError!Object {
+    const gpa = wasm_file.base.comp.gpa;
     var object: Object = .{
         .file = file,
         .path = try gpa.dupe(u8, name),
@@ -151,7 +152,7 @@ pub fn create(gpa: Allocator, file: std.fs.File, name: []const u8, maybe_max_siz
     }
     var fbs = std.io.fixedBufferStream(file_contents);
 
-    try object.parse(gpa, fbs.reader(), &is_object_file);
+    try object.parse(gpa, wasm_file, fbs.reader(), &is_object_file);
     errdefer object.deinit(gpa);
     if (!is_object_file) return error.NotObjectFile;
 
@@ -224,7 +225,7 @@ pub fn findImport(object: *const Object, sym: Symbol) types.Import {
 /// we initialize a new table symbol that corresponds to that import and return that symbol.
 ///
 /// When the object file is *NOT* MVP, we return `null`.
-fn checkLegacyIndirectFunctionTable(object: *Object) !?Symbol {
+fn checkLegacyIndirectFunctionTable(object: *Object, wasm_file: *const Wasm) !?Symbol {
     var table_count: usize = 0;
     for (object.symtable) |sym| {
         if (sym.tag == .table) table_count += 1;
@@ -234,21 +235,27 @@ fn checkLegacyIndirectFunctionTable(object: *Object) !?Symbol {
     if (object.imported_tables_count == table_count) return null;
 
     if (table_count != 0) {
-        log.err("Expected a table entry symbol for each of the {d} table(s), but instead got {d} symbols.", .{
+        var err = try wasm_file.addErrorWithNotes(1);
+        try err.addMsg(wasm_file, "Expected a table entry symbol for each of the {d} table(s), but instead got {d} symbols.", .{
             object.imported_tables_count,
             table_count,
         });
+        try err.addNote(wasm_file, "defined in '{s}'", .{object.path});
         return error.MissingTableSymbols;
     }
 
     // MVP object files cannot have any table definitions, only imports (for the indirect function table).
     if (object.tables.len > 0) {
-        log.err("Unexpected table definition without representing table symbols.", .{});
+        var err = try wasm_file.addErrorWithNotes(1);
+        try err.addMsg(wasm_file, "Unexpected table definition without representing table symbols.", .{});
+        try err.addNote(wasm_file, "defined in '{s}'", .{object.path});
         return error.UnexpectedTable;
     }
 
     if (object.imported_tables_count != 1) {
-        log.err("Found more than one table import, but no representing table symbols", .{});
+        var err = try wasm_file.addErrorWithNotes(1);
+        try err.addMsg(wasm_file, "Found more than one table import, but no representing table symbols", .{});
+        try err.addNote(wasm_file, "defined in '{s}'", .{object.path});
         return error.MissingTableSymbols;
     }
 
@@ -259,7 +266,9 @@ fn checkLegacyIndirectFunctionTable(object: *Object) !?Symbol {
     } else unreachable;
 
     if (!std.mem.eql(u8, object.string_table.get(table_import.name), "__indirect_function_table")) {
-        log.err("Non-indirect function table import '{s}' is missing a corresponding symbol", .{object.string_table.get(table_import.name)});
+        var err = try wasm_file.addErrorWithNotes(1);
+        try err.addMsg(wasm_file, "Non-indirect function table import '{s}' is missing a corresponding symbol", .{object.string_table.get(table_import.name)});
+        try err.addNote(wasm_file, "defined in '{s}'", .{object.path});
         return error.MissingTableSymbols;
     }
 
@@ -312,8 +321,8 @@ pub const ParseError = error{
     UnknownFeature,
 };
 
-fn parse(object: *Object, gpa: Allocator, reader: anytype, is_object_file: *bool) Parser(@TypeOf(reader)).Error!void {
-    var parser = Parser(@TypeOf(reader)).init(object, reader);
+fn parse(object: *Object, gpa: Allocator, wasm_file: *const Wasm, reader: anytype, is_object_file: *bool) Parser(@TypeOf(reader)).Error!void {
+    var parser = Parser(@TypeOf(reader)).init(object, wasm_file, reader);
     return parser.parseObject(gpa, is_object_file);
 }
 
@@ -325,9 +334,11 @@ fn Parser(comptime ReaderType: type) type {
         reader: std.io.CountingReader(ReaderType),
         /// Object file we're building
         object: *Object,
+        /// Read-only reference to the WebAssembly linker
+        wasm_file: *const Wasm,
 
-        fn init(object: *Object, reader: ReaderType) ObjectParser {
-            return .{ .object = object, .reader = std.io.countingReader(reader) };
+        fn init(object: *Object, wasm_file: *const Wasm, reader: ReaderType) ObjectParser {
+            return .{ .object = object, .wasm_file = wasm_file, .reader = std.io.countingReader(reader) };
         }
 
         /// Verifies that the first 4 bytes contains \0Asm
@@ -585,7 +596,9 @@ fn Parser(comptime ReaderType: type) type {
                 try reader.readNoEof(name);
 
                 const tag = types.known_features.get(name) orelse {
-                    log.err("Object file contains unknown feature: {s}", .{name});
+                    var err = try parser.wasm_file.addErrorWithNotes(1);
+                    try err.addMsg(parser.wasm_file, "Object file contains unknown feature: {s}", .{name});
+                    try err.addNote(parser.wasm_file, "defined in '{s}'", .{parser.object.path});
                     return error.UnknownFeature;
                 };
                 feature.* = .{
@@ -754,7 +767,7 @@ fn Parser(comptime ReaderType: type) type {
 
                     // we found all symbols, check for indirect function table
                     // in case of an MVP object file
-                    if (try parser.object.checkLegacyIndirectFunctionTable()) |symbol| {
+                    if (try parser.object.checkLegacyIndirectFunctionTable(parser.wasm_file)) |symbol| {
                         try symbols.append(symbol);
                         log.debug("Found legacy indirect function table. Created symbol", .{});
                     }
src/link/Wasm/ZigObject.zig
@@ -1213,6 +1213,6 @@ const StringTable = @import("../StringTable.zig");
 const Symbol = @import("Symbol.zig");
 const Type = @import("../../type.zig").Type;
 const TypedValue = @import("../../TypedValue.zig");
-const Value = @import("../../value.zig").Value;
+const Value = @import("../../Value.zig");
 const Wasm = @import("../Wasm.zig");
 const ZigObject = @This();
src/link/Wasm.zig
@@ -655,9 +655,14 @@ fn parseObjectFile(wasm: *Wasm, path: []const u8) !bool {
     errdefer obj_file.close();
 
     const gpa = wasm.base.comp.gpa;
-    var object = Object.create(gpa, obj_file, path, null) catch |err| switch (err) {
+    var object = Object.create(wasm, obj_file, path, null) catch |err| switch (err) {
         error.InvalidMagicByte, error.NotObjectFile => return false,
-        else => |e| return e,
+        else => |e| {
+            var err_note = try wasm.addErrorWithNotes(1);
+            try err_note.addMsg(wasm, "Failed parsing object file: {s}", .{@errorName(e)});
+            try err_note.addNote(wasm, "while parsing '{s}'", .{path});
+            return error.FlushFailure;
+        },
     };
     errdefer object.deinit(gpa);
     object.index = @enumFromInt(wasm.files.len);
@@ -708,7 +713,12 @@ fn parseArchive(wasm: *Wasm, path: []const u8, force_load: bool) !bool {
             archive.deinit(gpa);
             return false;
         },
-        else => |e| return e,
+        else => |e| {
+            var err_note = try wasm.addErrorWithNotes(1);
+            try err_note.addMsg(wasm, "Failed parsing archive: {s}", .{@errorName(e)});
+            try err_note.addNote(wasm, "while parsing archive {s}", .{path});
+            return error.FlushFailure;
+        },
     };
 
     if (!force_load) {
@@ -730,7 +740,12 @@ fn parseArchive(wasm: *Wasm, path: []const u8, force_load: bool) !bool {
     }
 
     for (offsets.keys()) |file_offset| {
-        var object = try archive.parseObject(gpa, file_offset);
+        var object = archive.parseObject(wasm, file_offset) catch |e| {
+            var err_note = try wasm.addErrorWithNotes(1);
+            try err_note.addMsg(wasm, "Failed parsing object: {s}", .{@errorName(e)});
+            try err_note.addNote(wasm, "while parsing object in archive {s}", .{path});
+            return error.FlushFailure;
+        };
         object.index = @enumFromInt(wasm.files.len);
         try wasm.files.append(gpa, .{ .object = object });
         try wasm.objects.append(gpa, object.index);
@@ -764,9 +779,9 @@ fn resolveSymbolsInObject(wasm: *Wasm, file_index: File.Index) !void {
 
         if (symbol.isLocal()) {
             if (symbol.isUndefined()) {
-                log.err("Local symbols are not allowed to reference imports", .{});
-                log.err("  symbol '{s}' defined in '{s}'", .{ sym_name, obj_file.path() });
-                return error.UndefinedLocal;
+                var err = try wasm.addErrorWithNotes(1);
+                try err.addMsg(wasm, "Local symbols are not allowed to reference imports", .{});
+                try err.addNote(wasm, "symbol '{s}' defined in '{s}'", .{ sym_name, obj_file.path() });
             }
             try wasm.resolved_symbols.putNoClobber(gpa, location, {});
             continue;
@@ -801,10 +816,10 @@ fn resolveSymbolsInObject(wasm: *Wasm, file_index: File.Index) !void {
                     break :outer; // existing is weak, while new one isn't. Replace it.
                 }
                 // both are defined and weak, we have a symbol collision.
-                log.err("symbol '{s}' defined multiple times", .{sym_name});
-                log.err("  first definition in '{s}'", .{existing_file_path});
-                log.err("  next definition in '{s}'", .{obj_file.path()});
-                return error.SymbolCollision;
+                var err = try wasm.addErrorWithNotes(2);
+                try err.addMsg(wasm, "symbol '{s}' defined multiple times", .{sym_name});
+                try err.addNote(wasm, "first definition in '{s}'", .{existing_file_path});
+                try err.addNote(wasm, "next definition in '{s}'", .{obj_file.path()});
             }
 
             try wasm.discarded.put(gpa, location, existing_loc);
@@ -812,10 +827,10 @@ fn resolveSymbolsInObject(wasm: *Wasm, file_index: File.Index) !void {
         }
 
         if (symbol.tag != existing_sym.tag) {
-            log.err("symbol '{s}' mismatching types '{s}' and '{s}'", .{ sym_name, @tagName(symbol.tag), @tagName(existing_sym.tag) });
-            log.err("  first definition in '{s}'", .{existing_file_path});
-            log.err("  next definition in '{s}'", .{obj_file.path()});
-            return error.SymbolMismatchingType;
+            var err = try wasm.addErrorWithNotes(2);
+            try err.addMsg(wasm, "symbol '{s}' mismatching types '{s}' and '{s}'", .{ sym_name, @tagName(symbol.tag), @tagName(existing_sym.tag) });
+            try err.addNote(wasm, "first definition in '{s}'", .{existing_file_path});
+            try err.addNote(wasm, "next definition in '{s}'", .{obj_file.path()});
         }
 
         if (existing_sym.isUndefined() and symbol.isUndefined()) {
@@ -832,14 +847,14 @@ fn resolveSymbolsInObject(wasm: *Wasm, file_index: File.Index) !void {
                 const imp = obj_file.import(sym_index);
                 const module_name = obj_file.string(imp.module_name);
                 if (!mem.eql(u8, existing_name, module_name)) {
-                    log.err("symbol '{s}' module name mismatch. Expected '{s}', but found '{s}'", .{
+                    var err = try wasm.addErrorWithNotes(2);
+                    try err.addMsg(wasm, "symbol '{s}' module name mismatch. Expected '{s}', but found '{s}'", .{
                         sym_name,
                         existing_name,
                         module_name,
                     });
-                    log.err("  first definition in '{s}'", .{existing_file_path});
-                    log.err("  next definition in '{s}'", .{obj_file.path()});
-                    return error.ModuleNameMismatch;
+                    try err.addNote(wasm, "first definition in '{s}'", .{existing_file_path});
+                    try err.addNote(wasm, "next definition in '{s}'", .{obj_file.path()});
                 }
             }
 
@@ -852,10 +867,10 @@ fn resolveSymbolsInObject(wasm: *Wasm, file_index: File.Index) !void {
             const existing_ty = wasm.getGlobalType(existing_loc);
             const new_ty = wasm.getGlobalType(location);
             if (existing_ty.mutable != new_ty.mutable or existing_ty.valtype != new_ty.valtype) {
-                log.err("symbol '{s}' mismatching global types", .{sym_name});
-                log.err("  first definition in '{s}'", .{existing_file_path});
-                log.err("  next definition in '{s}'", .{obj_file.path()});
-                return error.GlobalTypeMismatch;
+                var err = try wasm.addErrorWithNotes(2);
+                try err.addMsg(wasm, "symbol '{s}' mismatching global types", .{sym_name});
+                try err.addNote(wasm, "first definition in '{s}'", .{existing_file_path});
+                try err.addNote(wasm, "next definition in '{s}'", .{obj_file.path()});
             }
         }
 
@@ -863,11 +878,11 @@ fn resolveSymbolsInObject(wasm: *Wasm, file_index: File.Index) !void {
             const existing_ty = wasm.getFunctionSignature(existing_loc);
             const new_ty = wasm.getFunctionSignature(location);
             if (!existing_ty.eql(new_ty)) {
-                log.err("symbol '{s}' mismatching function signatures.", .{sym_name});
-                log.err("  expected signature {}, but found signature {}", .{ existing_ty, new_ty });
-                log.err("  first definition in '{s}'", .{existing_file_path});
-                log.err("  next definition in '{s}'", .{obj_file.path()});
-                return error.FunctionSignatureMismatch;
+                var err = try wasm.addErrorWithNotes(3);
+                try err.addMsg(wasm, "symbol '{s}' mismatching function signatures.", .{sym_name});
+                try err.addNote(wasm, "expected signature {}, but found signature {}", .{ existing_ty, new_ty });
+                try err.addNote(wasm, "first definition in '{s}'", .{existing_file_path});
+                try err.addNote(wasm, "next definition in '{s}'", .{obj_file.path()});
             }
         }
 
@@ -914,7 +929,12 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {
             // Symbol is found in unparsed object file within current archive.
             // Parse object and and resolve symbols again before we check remaining
             // undefined symbols.
-            var object = try archive.parseObject(gpa, offset.items[0]);
+            var object = archive.parseObject(wasm, offset.items[0]) catch |e| {
+                var err_note = try wasm.addErrorWithNotes(1);
+                try err_note.addMsg(wasm, "Failed parsing object: {s}", .{@errorName(e)});
+                try err_note.addNote(wasm, "while parsing object in archive {s}", .{archive.name});
+                return error.FlushFailure;
+            };
             object.index = @enumFromInt(wasm.files.len);
             try wasm.files.append(gpa, .{ .object = object });
             try wasm.objects.append(gpa, object.index);
@@ -1214,20 +1234,21 @@ fn validateFeatures(
             allowed[used_index] = is_enabled;
             emit_features_count.* += @intFromBool(is_enabled);
         } else if (is_enabled and !allowed[used_index]) {
-            log.err("feature '{}' not allowed, but used by linked object", .{@as(types.Feature.Tag, @enumFromInt(used_index))});
-            log.err("  defined in '{s}'", .{wasm.files.items(.data)[used_set >> 1].object.path});
+            var err = try wasm.addErrorWithNotes(1);
+            try err.addMsg(wasm, "feature '{}' not allowed, but used by linked object", .{@as(types.Feature.Tag, @enumFromInt(used_index))});
+            try err.addNote(wasm, "defined in '{s}'", .{wasm.files.items(.data)[used_set >> 1].object.path});
             valid_feature_set = false;
         }
     }
 
     if (!valid_feature_set) {
-        return error.InvalidFeatureSet;
+        return error.FlushFailure;
     }
 
     if (shared_memory) {
         const disallowed_feature = disallowed[@intFromEnum(types.Feature.Tag.shared_mem)];
         if (@as(u1, @truncate(disallowed_feature)) != 0) {
-            log.err(
+            try wasm.addErrorWithoutNotes(
                 "shared-memory is disallowed by '{s}' because it wasn't compiled with 'atomics' and 'bulk-memory' features enabled",
                 .{wasm.files.items(.data)[disallowed_feature >> 1].object.path},
             );
@@ -1236,7 +1257,7 @@ fn validateFeatures(
 
         for ([_]types.Feature.Tag{ .atomics, .bulk_memory }) |feature| {
             if (!allowed[@intFromEnum(feature)]) {
-                log.err("feature '{}' is not used but is required for shared-memory", .{feature});
+                try wasm.addErrorWithoutNotes("feature '{}' is not used but is required for shared-memory", .{feature});
             }
         }
     }
@@ -1244,7 +1265,7 @@ fn validateFeatures(
     if (has_tls) {
         for ([_]types.Feature.Tag{ .atomics, .bulk_memory }) |feature| {
             if (!allowed[@intFromEnum(feature)]) {
-                log.err("feature '{}' is not used but is required for thread-local storage", .{feature});
+                try wasm.addErrorWithoutNotes("feature '{}' is not used but is required for thread-local storage", .{feature});
             }
         }
     }
@@ -1257,9 +1278,10 @@ fn validateFeatures(
             // from here a feature is always used
             const disallowed_feature = disallowed[@intFromEnum(feature.tag)];
             if (@as(u1, @truncate(disallowed_feature)) != 0) {
-                log.err("feature '{}' is disallowed, but used by linked object", .{feature.tag});
-                log.err("  disallowed by '{s}'", .{wasm.files.items(.data)[disallowed_feature >> 1].object.path});
-                log.err("  used in '{s}'", .{object.path});
+                var err = try wasm.addErrorWithNotes(2);
+                try err.addMsg(wasm, "feature '{}' is disallowed, but used by linked object", .{feature.tag});
+                try err.addNote(wasm, "disallowed by '{s}'", .{wasm.files.items(.data)[disallowed_feature >> 1].object.path});
+                try err.addNote(wasm, "used in '{s}'", .{object.path});
                 valid_feature_set = false;
             }
 
@@ -1270,16 +1292,17 @@ fn validateFeatures(
         for (required, 0..) |required_feature, feature_index| {
             const is_required = @as(u1, @truncate(required_feature)) != 0;
             if (is_required and !object_used_features[feature_index]) {
-                log.err("feature '{}' is required but not used in linked object", .{@as(types.Feature.Tag, @enumFromInt(feature_index))});
-                log.err("  required by '{s}'", .{wasm.files.items(.data)[required_feature >> 1].object.path});
-                log.err("  missing in '{s}'", .{object.path});
+                var err = try wasm.addErrorWithNotes(2);
+                try err.addMsg(wasm, "feature '{}' is required but not used in linked object", .{@as(types.Feature.Tag, @enumFromInt(feature_index))});
+                try err.addNote(wasm, "required by '{s}'", .{wasm.files.items(.data)[required_feature >> 1].object.path});
+                try err.addNote(wasm, "missing in '{s}'", .{object.path});
                 valid_feature_set = false;
             }
         }
     }
 
     if (!valid_feature_set) {
-        return error.InvalidFeatureSet;
+        return error.FlushFailure;
     }
 
     to_emit.* = allowed;
@@ -1350,12 +1373,13 @@ fn checkUndefinedSymbols(wasm: *const Wasm) !void {
             else
                 wasm.name;
             const symbol_name = undef.getName(wasm);
-            log.err("could not resolve undefined symbol '{s}'", .{symbol_name});
-            log.err("  defined in '{s}'", .{file_name});
+            var err = try wasm.addErrorWithNotes(1);
+            try err.addMsg(wasm, "could not resolve undefined symbol '{s}'", .{symbol_name});
+            try err.addNote(wasm, "defined in '{s}'", .{file_name});
         }
     }
     if (found_undefined_symbols) {
-        return error.UndefinedSymbol;
+        return error.FlushFailure;
     }
 }
 
@@ -1728,8 +1752,7 @@ fn setupInitFunctions(wasm: *Wasm) !void {
                 break :ty object.func_types[func.type_index];
             };
             if (ty.params.len != 0) {
-                log.err("constructor functions cannot take arguments: '{s}'", .{object.string_table.get(symbol.name)});
-                return error.InvalidInitFunc;
+                try wasm.addErrorWithoutNotes("constructor functions cannot take arguments: '{s}'", .{object.string_table.get(symbol.name)});
             }
             log.debug("appended init func '{s}'\n", .{object.string_table.get(symbol.name)});
             wasm.init_funcs.appendAssumeCapacity(.{
@@ -2108,7 +2131,7 @@ fn setupExports(wasm: *Wasm) !void {
 
         for (force_exp_names) |exp_name| {
             const loc = wasm.findGlobalSymbol(exp_name) orelse {
-                log.err("could not export '{s}', symbol not found", .{exp_name});
+                try wasm.addErrorWithoutNotes("could not export '{s}', symbol not found", .{exp_name});
                 failed_exports = true;
                 continue;
             };
@@ -2118,7 +2141,7 @@ fn setupExports(wasm: *Wasm) !void {
         }
 
         if (failed_exports) {
-            return error.MissingSymbol;
+            return error.FlushFailure;
         }
     }
 
@@ -2164,14 +2187,14 @@ fn setupStart(wasm: *Wasm) !void {
     const entry_name = wasm.entry_name orelse return;
 
     const symbol_loc = wasm.findGlobalSymbol(entry_name) orelse {
-        log.err("Entry symbol '{s}' missing, use '-fno-entry' to suppress", .{entry_name});
-        return error.MissingSymbol;
+        try wasm.addErrorWithoutNotes("Entry symbol '{s}' missing, use '-fno-entry' to suppress", .{entry_name});
+        return error.FlushFailure;
     };
 
     const symbol = symbol_loc.getSymbol(wasm);
     if (symbol.tag != .function) {
-        log.err("Entry symbol '{s}' is not a function", .{entry_name});
-        return error.InvalidEntryKind;
+        try wasm.addErrorWithoutNotes("Entry symbol '{s}' is not a function", .{entry_name});
+        return error.FlushFailure;
     }
 
     // Ensure the symbol is exported so host environment can access it
@@ -2274,16 +2297,13 @@ fn setupMemory(wasm: *Wasm) !void {
 
     if (wasm.initial_memory) |initial_memory| {
         if (!std.mem.isAlignedGeneric(u64, initial_memory, page_size)) {
-            log.err("Initial memory must be {d}-byte aligned", .{page_size});
-            return error.MissAlignment;
+            try wasm.addErrorWithoutNotes("Initial memory must be {d}-byte aligned", .{page_size});
         }
         if (memory_ptr > initial_memory) {
-            log.err("Initial memory too small, must be at least {d} bytes", .{memory_ptr});
-            return error.MemoryTooSmall;
+            try wasm.addErrorWithoutNotes("Initial memory too small, must be at least {d} bytes", .{memory_ptr});
         }
         if (initial_memory > max_memory_allowed) {
-            log.err("Initial memory exceeds maximum memory {d}", .{max_memory_allowed});
-            return error.MemoryTooBig;
+            try wasm.addErrorWithoutNotes("Initial memory exceeds maximum memory {d}", .{max_memory_allowed});
         }
         memory_ptr = initial_memory;
     }
@@ -2300,16 +2320,13 @@ fn setupMemory(wasm: *Wasm) !void {
 
     if (wasm.max_memory) |max_memory| {
         if (!std.mem.isAlignedGeneric(u64, max_memory, page_size)) {
-            log.err("Maximum memory must be {d}-byte aligned", .{page_size});
-            return error.MissAlignment;
+            try wasm.addErrorWithoutNotes("Maximum memory must be {d}-byte aligned", .{page_size});
         }
         if (memory_ptr > max_memory) {
-            log.err("Maxmimum memory too small, must be at least {d} bytes", .{memory_ptr});
-            return error.MemoryTooSmall;
+            try wasm.addErrorWithoutNotes("Maxmimum memory too small, must be at least {d} bytes", .{memory_ptr});
         }
         if (max_memory > max_memory_allowed) {
-            log.err("Maximum memory exceeds maxmium amount {d}", .{max_memory_allowed});
-            return error.MemoryTooBig;
+            try wasm.addErrorWithoutNotes("Maximum memory exceeds maxmium amount {d}", .{max_memory_allowed});
         }
         wasm.memories.limits.max = @as(u32, @intCast(max_memory / page_size));
         wasm.memories.limits.setFlag(.WASM_LIMITS_FLAG_HAS_MAX);
@@ -2412,7 +2429,9 @@ pub fn getMatchingSegment(wasm: *Wasm, file_index: File.Index, symbol_index: Sym
                     break :blk index;
                 };
             } else {
-                log.err("found unknown section '{s}'", .{section_name});
+                var err = try wasm.addErrorWithNotes(1);
+                try err.addMsg(wasm, "found unknown section '{s}'", .{section_name});
+                try err.addNote(wasm, "defined in '{s}'", .{obj_file.path()});
                 return error.UnexpectedValue;
             }
         },
@@ -2529,18 +2548,22 @@ pub fn flushModule(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node)
     if (wasm.zig_object_index != .null) {
         try wasm.resolveSymbolsInObject(wasm.zig_object_index);
     }
+    if (comp.link_errors.items.len > 0) return error.FlushFailure;
     for (wasm.objects.items) |object_index| {
         try wasm.resolveSymbolsInObject(object_index);
     }
+    if (comp.link_errors.items.len > 0) return error.FlushFailure;
 
     var emit_features_count: u32 = 0;
     var enabled_features: [@typeInfo(types.Feature.Tag).Enum.fields.len]bool = undefined;
     try wasm.validateFeatures(&enabled_features, &emit_features_count);
     try wasm.resolveSymbolsInArchives();
+    if (comp.link_errors.items.len > 0) return error.FlushFailure;
     try wasm.resolveLazySymbols();
     try wasm.checkUndefinedSymbols();
 
     try wasm.setupInitFunctions();
+    if (comp.link_errors.items.len > 0) return error.FlushFailure;
     try wasm.setupStart();
 
     try wasm.markReferences();
@@ -2549,6 +2572,7 @@ pub fn flushModule(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node)
     try wasm.mergeTypes();
     try wasm.allocateAtoms();
     try wasm.setupMemory();
+    if (comp.link_errors.items.len > 0) return error.FlushFailure;
     wasm.allocateVirtualAddresses();
     wasm.mapFunctionTable();
     try wasm.initializeCallCtorsFunction();
@@ -2558,6 +2582,7 @@ pub fn flushModule(wasm: *Wasm, arena: Allocator, prog_node: *std.Progress.Node)
     try wasm.setupStartSection();
     try wasm.setupExports();
     try wasm.writeToFile(enabled_features, emit_features_count, arena);
+    if (comp.link_errors.items.len > 0) return error.FlushFailure;
 }
 
 /// Writes the WebAssembly in-memory module to the file
@@ -2955,7 +2980,7 @@ fn writeToFile(
                 }) catch unreachable;
                 try emitBuildIdSection(&binary_bytes, str);
             },
-            else => |mode| log.err("build-id '{s}' is not supported for WASM", .{@tagName(mode)}),
+            else => |mode| try wasm.addErrorWithoutNotes("build-id '{s}' is not supported for WebAssembly", .{@tagName(mode)}),
         }
 
         var debug_bytes = std.ArrayList(u8).init(gpa);
@@ -4043,3 +4068,57 @@ fn defaultEntrySymbolName(wasi_exec_model: std.builtin.WasiExecModel) []const u8
         .command => "_start",
     };
 }
+
+const ErrorWithNotes = struct {
+    /// Allocated index in comp.link_errors array.
+    index: usize,
+
+    /// Next available note slot.
+    note_slot: usize = 0,
+
+    pub fn addMsg(
+        err: ErrorWithNotes,
+        wasm_file: *const Wasm,
+        comptime format: []const u8,
+        args: anytype,
+    ) error{OutOfMemory}!void {
+        const comp = wasm_file.base.comp;
+        const gpa = comp.gpa;
+        const err_msg = &comp.link_errors.items[err.index];
+        err_msg.msg = try std.fmt.allocPrint(gpa, format, args);
+    }
+
+    pub fn addNote(
+        err: *ErrorWithNotes,
+        wasm_file: *const Wasm,
+        comptime format: []const u8,
+        args: anytype,
+    ) error{OutOfMemory}!void {
+        const comp = wasm_file.base.comp;
+        const gpa = comp.gpa;
+        const err_msg = &comp.link_errors.items[err.index];
+        err_msg.notes[err.note_slot] = .{ .msg = try std.fmt.allocPrint(gpa, format, args) };
+        err.note_slot += 1;
+    }
+};
+
+pub fn addErrorWithNotes(wasm: *const Wasm, note_count: usize) error{OutOfMemory}!ErrorWithNotes {
+    const comp = wasm.base.comp;
+    const gpa = comp.gpa;
+    try comp.link_errors.ensureUnusedCapacity(gpa, 1);
+    return wasm.addErrorWithNotesAssumeCapacity(note_count);
+}
+
+pub fn addErrorWithoutNotes(wasm: *const Wasm, comptime fmt: []const u8, args: anytype) !void {
+    const err = try wasm.addErrorWithNotes(0);
+    try err.addMsg(wasm, fmt, args);
+}
+
+fn addErrorWithNotesAssumeCapacity(wasm: *const Wasm, note_count: usize) error{OutOfMemory}!ErrorWithNotes {
+    const comp = wasm.base.comp;
+    const gpa = comp.gpa;
+    const index = comp.link_errors.items.len;
+    const err = comp.link_errors.addOneAssumeCapacity();
+    err.* = .{ .msg = undefined, .notes = try gpa.alloc(link.File.ErrorMsg, note_count) };
+    return .{ .index = index };
+}