From 94e98bfe80033439382bff22bcab02aaaa580dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Prokop=20Rand=C3=A1=C4=8Dek?= Date: Sun, 16 Nov 2025 14:20:45 +0000 Subject: [PATCH] Dedupe types when printing error messages --- src/Air/print.zig | 2 +- src/Sema.zig | 63 +++--- src/Type.zig | 205 ++++++++++++++++-- src/codegen/llvm.zig | 2 +- src/codegen/spirv/CodeGen.zig | 2 +- src/print_value.zig | 2 +- ..._when_coercing_pointer_to_anon_literal.zig | 3 +- test/cases/compile_errors/type_dedupe.zig | 18 ++ ...type_mismatch_with_tuple_concatenation.zig | 3 +- 9 files changed, 254 insertions(+), 46 deletions(-) create mode 100644 test/cases/compile_errors/type_dedupe.zig diff --git a/src/Air/print.zig b/src/Air/print.zig index ba167bbfd9..95c8a1fcda 100644 --- a/src/Air/print.zig +++ b/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 { diff --git a/src/Sema.zig b/src/Sema.zig index db87178a6a..5392ef54bd 100644 --- a/src/Sema.zig +++ b/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, }, diff --git a/src/Type.zig b/src/Type.zig index b111650e34..74a540298c 100644 --- a/src/Type.zig +++ b/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 }; diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 0df4cbc3d4..450e446d8a 100644 --- a/src/codegen/llvm.zig +++ b/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); diff --git a/src/codegen/spirv/CodeGen.zig b/src/codegen/spirv/CodeGen.zig index 6d87f37396..20aad56225 100644 --- a/src/codegen/spirv/CodeGen.zig +++ b/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(); diff --git a/src/print_value.zig b/src/print_value.zig index 79cb6e3f87..9a44e8d7a4 100644 --- a/src/print_value.zig +++ b/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("{}"), diff --git a/test/cases/compile_errors/pointer_attributes_checked_when_coercing_pointer_to_anon_literal.zig b/test/cases/compile_errors/pointer_attributes_checked_when_coercing_pointer_to_anon_literal.zig index 00693a87f0..12ecf98590 100644 --- a/test/cases/compile_errors/pointer_attributes_checked_when_coercing_pointer_to_anon_literal.zig +++ b/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 '*', found '*const ' +// :6:31: note: = [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 diff --git a/test/cases/compile_errors/type_dedupe.zig b/test/cases/compile_errors/type_dedupe.zig new file mode 100644 index 0000000000..d1787d0bfa --- /dev/null +++ b/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 '*', found '' +// :10:9: note: = tmp.SomeVeryLongName +// :1:26: note: struct declared here +// :3:11: note: parameter type declared here diff --git a/test/cases/compile_errors/type_mismatch_with_tuple_concatenation.zig b/test/cases/compile_errors/type_mismatch_with_tuple_concatenation.zig index 587ab48d15..bb6ee8ba33 100644 --- a/test/cases/compile_errors/type_mismatch_with_tuple_concatenation.zig +++ b/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 = 1, comptime = 2, comptime = 3 }' +// :3:11: note: = comptime_int