Commit 94e98bfe80

Prokop Randáček <prokop@rdck.dev>
2025-11-16 15:20:45
Dedupe types when printing error messages
1 parent aa4332f
src/Air/print.zig
@@ -363,7 +363,7 @@ const Writer = struct {
     }
 
     fn writeType(w: *Writer, s: *std.Io.Writer, ty: Type) !void {
-        return ty.print(s, w.pt);
+        return ty.print(s, w.pt, null);
     }
 
     fn writeTy(w: *Writer, s: *std.Io.Writer, inst: Air.Inst.Index) Error!void {
src/codegen/spirv/CodeGen.zig
@@ -1213,7 +1213,7 @@ fn resolveTypeName(cg: *CodeGen, ty: Type) ![]const u8 {
     const gpa = cg.module.gpa;
     var aw: std.Io.Writer.Allocating = .init(gpa);
     defer aw.deinit();
-    ty.print(&aw.writer, cg.pt) catch |err| switch (err) {
+    ty.print(&aw.writer, cg.pt, null) catch |err| switch (err) {
         error.WriteFailed => return error.OutOfMemory,
     };
     return try aw.toOwnedSlice();
src/codegen/llvm.zig
@@ -2697,7 +2697,7 @@ pub const Object = struct {
     fn allocTypeName(o: *Object, pt: Zcu.PerThread, ty: Type) Allocator.Error![:0]const u8 {
         var aw: std.Io.Writer.Allocating = .init(o.gpa);
         defer aw.deinit();
-        ty.print(&aw.writer, pt) catch |err| switch (err) {
+        ty.print(&aw.writer, pt, null) catch |err| switch (err) {
             error.WriteFailed => return error.OutOfMemory,
         };
         return aw.toOwnedSliceSentinel(0);
src/print_value.zig
@@ -66,7 +66,7 @@ pub fn print(
         .func_type,
         .error_set_type,
         .inferred_error_set_type,
-        => try Type.print(val.toType(), writer, pt),
+        => try Type.print(val.toType(), writer, pt, null),
         .undef => try writer.writeAll("undefined"),
         .simple_value => |simple_value| switch (simple_value) {
             .void => try writer.writeAll("{}"),
src/Sema.zig
@@ -2447,19 +2447,6 @@ fn failWithStructInitNotSupported(sema: *Sema, block: *Block, src: LazySrcLoc, t
     });
 }
 
-fn failWithErrorSetCodeMissing(
-    sema: *Sema,
-    block: *Block,
-    src: LazySrcLoc,
-    dest_err_set_ty: Type,
-    src_err_set_ty: Type,
-) CompileError {
-    const pt = sema.pt;
-    return sema.fail(block, src, "expected type '{f}', found type '{f}'", .{
-        dest_err_set_ty.fmt(pt), src_err_set_ty.fmt(pt),
-    });
-}
-
 pub fn failWithIntegerOverflow(sema: *Sema, block: *Block, src: LazySrcLoc, int_ty: Type, val: Value, vector_index: ?usize) CompileError {
     const pt = sema.pt;
     return sema.failWithOwnedErrorMsg(block, msg: {
@@ -2619,6 +2606,26 @@ pub fn errMsg(
     return Zcu.ErrorMsg.create(sema.gpa, src, format, args);
 }
 
+fn typeMismatchErrMsg(sema: *Sema, src: LazySrcLoc, expected: Type, found: Type) Allocator.Error!*Zcu.ErrorMsg {
+    const pt = sema.pt;
+    var cmp: Type.Comparison = try .init(&.{ expected, found }, pt);
+    defer cmp.deinit(pt);
+
+    const msg = try sema.errMsg(src, "expected type '{f}', found '{f}'", .{
+        cmp.fmtType(expected, pt),
+        cmp.fmtType(found, pt),
+    });
+    errdefer msg.destroy(sema.gpa);
+
+    for (cmp.type_dedupe_cache.keys(), cmp.type_dedupe_cache.values()) |ty, value| {
+        if (value == .dont_dedupe) continue;
+        const placeholder = value.dedupe;
+        try sema.errNote(src, msg, "{f} = {f}", .{ placeholder, ty.fmt(pt) });
+    }
+
+    return msg;
+}
+
 pub fn fail(
     sema: *Sema,
     block: *Block,
@@ -2635,6 +2642,14 @@ pub fn fail(
     return sema.failWithOwnedErrorMsg(block, err_msg);
 }
 
+fn failWithTypeMismatch(sema: *Sema, block: *Block, src: LazySrcLoc, expected: Type, found: Type) CompileError {
+    const err_msg = try sema.typeMismatchErrMsg(src, expected, found);
+    errdefer err_msg.destroy(sema.gpa);
+    try addDeclaredHereNote(sema, err_msg, expected);
+    try addDeclaredHereNote(sema, err_msg, found);
+    return sema.failWithOwnedErrorMsg(block, err_msg);
+}
+
 pub fn failWithOwnedErrorMsg(sema: *Sema, block: ?*Block, err_msg: *Zcu.ErrorMsg) error{ AnalysisFail, OutOfMemory } {
     @branchHint(.cold);
     const gpa = sema.gpa;
@@ -22933,7 +22948,7 @@ fn zirTruncate(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
     const operand_is_vector = operand_ty.zigTypeTag(zcu) == .vector;
     const dest_is_vector = dest_ty.zigTypeTag(zcu) == .vector;
     if (operand_is_vector != dest_is_vector) {
-        return sema.fail(block, operand_src, "expected type '{f}', found '{f}'", .{ dest_ty.fmt(pt), operand_ty.fmt(pt) });
+        return sema.failWithTypeMismatch(block, operand_src, dest_ty, operand_ty);
     }
 
     if (dest_scalar_ty.zigTypeTag(zcu) == .comptime_int) {
@@ -29167,7 +29182,7 @@ fn coerceExtra(
     }
 
     const msg = msg: {
-        const msg = try sema.errMsg(inst_src, "expected type '{f}', found '{f}'", .{ dest_ty.fmt(pt), inst_ty.fmt(pt) });
+        const msg = try sema.typeMismatchErrMsg(inst_src, dest_ty, inst_ty);
         errdefer msg.destroy(sema.gpa);
 
         if (!can_coerce_to) {
@@ -30780,9 +30795,7 @@ fn coerceEnumToUnion(
 
     const tag_ty = union_ty.unionTagType(zcu) orelse {
         const msg = msg: {
-            const msg = try sema.errMsg(inst_src, "expected type '{f}', found '{f}'", .{
-                union_ty.fmt(pt), inst_ty.fmt(pt),
-            });
+            const msg = try sema.typeMismatchErrMsg(inst_src, union_ty, inst_ty);
             errdefer msg.destroy(sema.gpa);
             try sema.errNote(union_ty_src, msg, "cannot coerce enum to untagged union", .{});
             try sema.addDeclaredHereNote(msg, union_ty);
@@ -30933,9 +30946,7 @@ fn coerceArrayLike(
     const dest_len = try sema.usizeCast(block, dest_ty_src, dest_ty.arrayLen(zcu));
     if (dest_len != inst_len) {
         const msg = msg: {
-            const msg = try sema.errMsg(inst_src, "expected type '{f}', found '{f}'", .{
-                dest_ty.fmt(pt), inst_ty.fmt(pt),
-            });
+            const msg = try sema.typeMismatchErrMsg(inst_src, dest_ty, inst_ty);
             errdefer msg.destroy(sema.gpa);
             try sema.errNote(dest_ty_src, msg, "destination has length {d}", .{dest_len});
             try sema.errNote(inst_src, msg, "source has length {d}", .{inst_len});
@@ -31018,9 +31029,7 @@ fn coerceTupleToArray(
 
     if (dest_len != inst_len) {
         const msg = msg: {
-            const msg = try sema.errMsg(inst_src, "expected type '{f}', found '{f}'", .{
-                dest_ty.fmt(pt), inst_ty.fmt(pt),
-            });
+            const msg = try sema.typeMismatchErrMsg(inst_src, dest_ty, inst_ty);
             errdefer msg.destroy(sema.gpa);
             try sema.errNote(dest_ty_src, msg, "destination has length {d}", .{dest_len});
             try sema.errNote(inst_src, msg, "source has length {d}", .{inst_len});
@@ -32719,12 +32728,12 @@ fn wrapErrorUnionSet(
                         break :ok;
                     },
                 }
-                return sema.failWithErrorSetCodeMissing(block, inst_src, dest_err_set_ty, inst_ty);
+                return sema.failWithTypeMismatch(block, inst_src, dest_err_set_ty, inst_ty);
             },
             else => switch (ip.indexToKey(dest_err_set_ty.toIntern())) {
                 .error_set_type => |error_set_type| ok: {
                     if (error_set_type.nameIndex(ip, expected_name) != null) break :ok;
-                    return sema.failWithErrorSetCodeMissing(block, inst_src, dest_err_set_ty, inst_ty);
+                    return sema.failWithTypeMismatch(block, inst_src, dest_err_set_ty, inst_ty);
                 },
                 .inferred_error_set_type => |func_index| ok: {
                     // We carefully do this in an order that avoids unnecessarily
@@ -32740,7 +32749,7 @@ fn wrapErrorUnionSet(
                         },
                     }
 
-                    return sema.failWithErrorSetCodeMissing(block, inst_src, dest_err_set_ty, inst_ty);
+                    return sema.failWithTypeMismatch(block, inst_src, dest_err_set_ty, inst_ty);
                 },
                 else => unreachable,
             },
src/Type.zig
@@ -141,7 +141,7 @@ const Format = struct {
     pt: Zcu.PerThread,
 
     fn default(f: Format, writer: *std.Io.Writer) std.Io.Writer.Error!void {
-        return print(f.ty, writer, f.pt);
+        return print(f.ty, writer, f.pt, null);
     }
 };
 
@@ -157,7 +157,17 @@ pub fn dump(start_type: Type, writer: *std.Io.Writer) std.Io.Writer.Error!void {
 
 /// Prints a name suitable for `@typeName`.
 /// TODO: take an `opt_sema` to pass to `fmtValue` when printing sentinels.
-pub fn print(ty: Type, writer: *std.Io.Writer, pt: Zcu.PerThread) std.Io.Writer.Error!void {
+pub fn print(ty: Type, writer: *std.Io.Writer, pt: Zcu.PerThread, ctx: ?*Comparison) std.Io.Writer.Error!void {
+    if (ctx) |c| {
+        const should_dedupe = shouldDedupeType(ty, c, pt) catch |err| switch (err) {
+            error.OutOfMemory => return error.WriteFailed,
+        };
+        switch (should_dedupe) {
+            .dont_dedupe => {},
+            .dedupe => |placeholder| return placeholder.format(writer),
+        }
+    }
+
     const zcu = pt.zcu;
     const ip = &zcu.intern_pool;
     switch (ip.indexToKey(ty.toIntern())) {
@@ -209,39 +219,39 @@ pub fn print(ty: Type, writer: *std.Io.Writer, pt: Zcu.PerThread) std.Io.Writer.
             if (info.flags.is_const) try writer.writeAll("const ");
             if (info.flags.is_volatile) try writer.writeAll("volatile ");
 
-            try print(Type.fromInterned(info.child), writer, pt);
+            try print(Type.fromInterned(info.child), writer, pt, ctx);
             return;
         },
         .array_type => |array_type| {
             if (array_type.sentinel == .none) {
                 try writer.print("[{d}]", .{array_type.len});
-                try print(Type.fromInterned(array_type.child), writer, pt);
+                try print(Type.fromInterned(array_type.child), writer, pt, ctx);
             } else {
                 try writer.print("[{d}:{f}]", .{
                     array_type.len,
                     Value.fromInterned(array_type.sentinel).fmtValue(pt),
                 });
-                try print(Type.fromInterned(array_type.child), writer, pt);
+                try print(Type.fromInterned(array_type.child), writer, pt, ctx);
             }
             return;
         },
         .vector_type => |vector_type| {
             try writer.print("@Vector({d}, ", .{vector_type.len});
-            try print(Type.fromInterned(vector_type.child), writer, pt);
+            try print(Type.fromInterned(vector_type.child), writer, pt, ctx);
             try writer.writeAll(")");
             return;
         },
         .opt_type => |child| {
             try writer.writeByte('?');
-            return print(Type.fromInterned(child), writer, pt);
+            return print(Type.fromInterned(child), writer, pt, ctx);
         },
         .error_union_type => |error_union_type| {
-            try print(Type.fromInterned(error_union_type.error_set_type), writer, pt);
+            try print(Type.fromInterned(error_union_type.error_set_type), writer, pt, ctx);
             try writer.writeByte('!');
             if (error_union_type.payload_type == .generic_poison_type) {
                 try writer.writeAll("anytype");
             } else {
-                try print(Type.fromInterned(error_union_type.payload_type), writer, pt);
+                try print(Type.fromInterned(error_union_type.payload_type), writer, pt, ctx);
             }
             return;
         },
@@ -323,7 +333,7 @@ pub fn print(ty: Type, writer: *std.Io.Writer, pt: Zcu.PerThread) std.Io.Writer.
             for (tuple.types.get(ip), tuple.values.get(ip), 0..) |field_ty, val, i| {
                 try writer.writeAll(if (i == 0) " " else ", ");
                 if (val != .none) try writer.writeAll("comptime ");
-                try print(Type.fromInterned(field_ty), writer, pt);
+                try print(Type.fromInterned(field_ty), writer, pt, ctx);
                 if (val != .none) try writer.print(" = {f}", .{Value.fromInterned(val).fmtValue(pt)});
             }
             try writer.writeAll(" }");
@@ -360,7 +370,7 @@ pub fn print(ty: Type, writer: *std.Io.Writer, pt: Zcu.PerThread) std.Io.Writer.
                 if (param_ty == .generic_poison_type) {
                     try writer.writeAll("anytype");
                 } else {
-                    try print(Type.fromInterned(param_ty), writer, pt);
+                    try print(Type.fromInterned(param_ty), writer, pt, ctx);
                 }
             }
             if (fn_info.is_var_args) {
@@ -387,13 +397,13 @@ pub fn print(ty: Type, writer: *std.Io.Writer, pt: Zcu.PerThread) std.Io.Writer.
             if (fn_info.return_type == .generic_poison_type) {
                 try writer.writeAll("anytype");
             } else {
-                try print(Type.fromInterned(fn_info.return_type), writer, pt);
+                try print(Type.fromInterned(fn_info.return_type), writer, pt, ctx);
             }
         },
         .anyframe_type => |child| {
             if (child == .none) return writer.writeAll("anyframe");
             try writer.writeAll("anyframe->");
-            return print(Type.fromInterned(child), writer, pt);
+            return print(Type.fromInterned(child), writer, pt, ctx);
         },
 
         // values, not types
@@ -4046,6 +4056,175 @@ pub fn isNullFromType(ty: Type, zcu: *const Zcu) ?bool {
     return null;
 }
 
+/// Recursively walks the type and marks for each subtype how many times it has been seen
+fn collectSubtypes(ty: Type, pt: Zcu.PerThread, visited: *std.AutoArrayHashMapUnmanaged(Type, u16)) error{OutOfMemory}!void {
+    const zcu = pt.zcu;
+    const ip = &zcu.intern_pool;
+
+    const gop = try visited.getOrPut(zcu.gpa, ty);
+    if (gop.found_existing) {
+        gop.value_ptr.* += 1;
+    } else {
+        gop.value_ptr.* = 1;
+    }
+
+    switch (ip.indexToKey(ty.toIntern())) {
+        .ptr_type => try collectSubtypes(Type.fromInterned(ty.ptrInfo(zcu).child), pt, visited),
+        .array_type => |array_type| try collectSubtypes(Type.fromInterned(array_type.child), pt, visited),
+        .vector_type => |vector_type| try collectSubtypes(Type.fromInterned(vector_type.child), pt, visited),
+        .opt_type => |child| try collectSubtypes(Type.fromInterned(child), pt, visited),
+        .error_union_type => |error_union_type| {
+            try collectSubtypes(Type.fromInterned(error_union_type.error_set_type), pt, visited);
+            if (error_union_type.payload_type != .generic_poison_type) {
+                try collectSubtypes(Type.fromInterned(error_union_type.payload_type), pt, visited);
+            }
+        },
+        .tuple_type => |tuple| {
+            for (tuple.types.get(ip)) |field_ty| {
+                try collectSubtypes(Type.fromInterned(field_ty), pt, visited);
+            }
+        },
+        .func_type => |fn_info| {
+            const param_types = fn_info.param_types.get(&zcu.intern_pool);
+            for (param_types) |param_ty| {
+                if (param_ty != .generic_poison_type) {
+                    try collectSubtypes(Type.fromInterned(param_ty), pt, visited);
+                }
+            }
+
+            if (fn_info.return_type != .generic_poison_type) {
+                try collectSubtypes(Type.fromInterned(fn_info.return_type), pt, visited);
+            }
+        },
+        .anyframe_type => |child| try collectSubtypes(Type.fromInterned(child), pt, visited),
+
+        // leaf types
+        .undef,
+        .inferred_error_set_type,
+        .error_set_type,
+        .struct_type,
+        .union_type,
+        .opaque_type,
+        .enum_type,
+        .simple_type,
+        .int_type,
+        => {},
+
+        // values, not types
+        .simple_value,
+        .variable,
+        .@"extern",
+        .func,
+        .int,
+        .err,
+        .error_union,
+        .enum_literal,
+        .enum_tag,
+        .empty_enum_value,
+        .float,
+        .ptr,
+        .slice,
+        .opt,
+        .aggregate,
+        .un,
+        // memoization, not types
+        .memoized_call,
+        => unreachable,
+    }
+}
+
+fn shouldDedupeType(ty: Type, ctx: *Comparison, pt: Zcu.PerThread) error{OutOfMemory}!Comparison.DedupeEntry {
+    if (ctx.type_occurrences.get(ty)) |occ| {
+        if (ctx.type_dedupe_cache.get(ty)) |cached| {
+            return cached;
+        }
+
+        var discarding: std.Io.Writer.Discarding = .init(&.{});
+
+        print(ty, &discarding.writer, pt, null) catch
+            unreachable; // we are writing into a discarding writer, it should never fail
+
+        const type_len: i32 = @intCast(discarding.count);
+
+        const placeholder_len: i32 = 3;
+        const min_saved_bytes: i32 = 10;
+
+        const saved_bytes = (type_len - placeholder_len) * (occ - 1);
+        const max_placeholders = 7; // T to Z
+        const should_dedupe = saved_bytes >= min_saved_bytes and ctx.placeholder_index < max_placeholders;
+
+        const entry: Comparison.DedupeEntry = if (should_dedupe) b: {
+            ctx.placeholder_index += 1;
+            break :b .{ .dedupe = .{ .index = ctx.placeholder_index - 1 } };
+        } else .dont_dedupe;
+
+        try ctx.type_dedupe_cache.put(pt.zcu.gpa, ty, entry);
+
+        return entry;
+    } else {
+        return .{ .dont_dedupe = {} };
+    }
+}
+
+/// The comparison recursively walks all types given and notes how many times
+/// each subtype occurs. It then while recursively printing decides for each
+/// subtype whether to print the type inline or create a placeholder based on
+/// the subtype length and number of occurences. Placeholders are then found by
+/// iterating `type_dedupe_cache` which caches the inline/placeholder decisions.
+pub const Comparison = struct {
+    type_occurrences: std.AutoArrayHashMapUnmanaged(Type, u16),
+    type_dedupe_cache: std.AutoArrayHashMapUnmanaged(Type, DedupeEntry),
+    placeholder_index: u8,
+
+    pub const Placeholder = struct {
+        index: u8,
+
+        pub fn format(p: Placeholder, writer: *std.Io.Writer) error{WriteFailed}!void {
+            return writer.print("<{c}>", .{p.index + 'T'});
+        }
+    };
+
+    pub const DedupeEntry = union(enum) {
+        dont_dedupe: void,
+        dedupe: Placeholder,
+    };
+
+    pub fn init(types: []const Type, pt: Zcu.PerThread) error{OutOfMemory}!Comparison {
+        var cmp: Comparison = .{
+            .type_occurrences = .empty,
+            .type_dedupe_cache = .empty,
+            .placeholder_index = 0,
+        };
+
+        errdefer cmp.deinit(pt);
+
+        for (types) |ty| {
+            try collectSubtypes(ty, pt, &cmp.type_occurrences);
+        }
+
+        return cmp;
+    }
+
+    pub fn deinit(cmp: *Comparison, pt: Zcu.PerThread) void {
+        const gpa = pt.zcu.gpa;
+        cmp.type_occurrences.deinit(gpa);
+        cmp.type_dedupe_cache.deinit(gpa);
+    }
+
+    pub fn fmtType(ctx: *Comparison, ty: Type, pt: Zcu.PerThread) Comparison.Formatter {
+        return .{ .ty = ty, .ctx = ctx, .pt = pt };
+    }
+    pub const Formatter = struct {
+        ty: Type,
+        ctx: *Comparison,
+        pt: Zcu.PerThread,
+
+        pub fn format(self: Comparison.Formatter, writer: anytype) error{WriteFailed}!void {
+            print(self.ty, writer, self.pt, self.ctx) catch return error.WriteFailed;
+        }
+    };
+};
+
 pub const @"u1": Type = .{ .ip_index = .u1_type };
 pub const @"u8": Type = .{ .ip_index = .u8_type };
 pub const @"u16": Type = .{ .ip_index = .u16_type };
test/cases/compile_errors/pointer_attributes_checked_when_coercing_pointer_to_anon_literal.zig
@@ -16,7 +16,8 @@ comptime {
 //
 // :2:29: error: expected type '[][]const u8', found '*const [2][]const u8'
 // :2:29: note: cast discards const qualifier
-// :6:31: error: expected type '*[2][]const u8', found '*const [2][]const u8'
+// :6:31: error: expected type '*<T>', found '*const <T>'
+// :6:31: note: <T> = [2][]const u8
 // :6:31: note: cast discards const qualifier
 // :11:19: error: expected type '*tmp.S', found '*const tmp.S'
 // :11:19: note: cast discards const qualifier
test/cases/compile_errors/type_dedupe.zig
@@ -0,0 +1,18 @@
+const SomeVeryLongName = struct {};
+
+fn foo(a: *SomeVeryLongName) void {
+    _ = a;
+}
+
+export fn entry() void {
+    const a: SomeVeryLongName = .{};
+
+    foo(a);
+}
+
+// error
+//
+// :10:9: error: expected type '*<T>', found '<T>'
+// :10:9: note: <T> = tmp.SomeVeryLongName
+// :1:26: note: struct declared here
+// :3:11: note: parameter type declared here
test/cases/compile_errors/type_mismatch_with_tuple_concatenation.zig
@@ -5,4 +5,5 @@ export fn entry() void {
 
 // error
 //
-// :3:11: error: expected type '@TypeOf(.{})', found 'struct { comptime comptime_int = 1, comptime comptime_int = 2, comptime comptime_int = 3 }'
+// :3:11: error: expected type '@TypeOf(.{})', found 'struct { comptime <T> = 1, comptime <T> = 2, comptime <T> = 3 }'
+// :3:11: note: <T> = comptime_int