diff --git a/src/check/snapshot.zig b/src/check/snapshot.zig index 2ed61f8e66..b77776c4eb 100644 --- a/src/check/snapshot.zig +++ b/src/check/snapshot.zig @@ -656,11 +656,11 @@ pub const Store = struct { break :blk self.snapshots.sliceVars(fn_args)[0]; }; - try self.writeWithContext(idx, .General, dispatcher); + try self.writeWithContext(dispatcher, .General, idx); _ = try self.buf.writer().write("."); _ = try self.buf.writer().write(self.idents.getText(constraint.fn_name)); _ = try self.buf.writer().write(" : "); - try self.writeWithContext(idx, .General, constraint.fn_content); + try self.writeWithContext(constraint.fn_content, .General, idx); } _ = try self.buf.writer().write("]"); } @@ -1048,15 +1048,77 @@ pub const Store = struct { } /// Append a constraint to the list, if it doesn't already exist + /// Deduplicates based on method name and dispatcher type variable fn appendStaticDispatchConstraint(self: *Self, constraint_to_add: SnapshotStaticDispatchConstraint) std.mem.Allocator.Error!void { + // Extract dispatcher (first arg) identity from the constraint to add + const add_dispatcher = self.getDispatcherIdentity(constraint_to_add.fn_content); + for (self.static_dispatch_constraints.items) |constraint| { - if (constraint.fn_name == constraint_to_add.fn_name and constraint.fn_content == constraint_to_add.fn_content) { - return; + if (constraint.fn_name == constraint_to_add.fn_name) { + // Same method name - check if dispatcher identity is the same + const existing_dispatcher = self.getDispatcherIdentity(constraint.fn_content); + if (dispatcherIdentitiesEqual(add_dispatcher, existing_dispatcher)) { + return; // Duplicate constraint + } + // Also check fn_content directly for backwards compatibility + if (constraint.fn_content == constraint_to_add.fn_content) { + return; + } } } _ = try self.static_dispatch_constraints.append(constraint_to_add); } + /// Dispatcher identity for deduplication - either a Var (for flex), an Ident.Idx (for rigid), or recursive + const DispatcherIdentity = union(enum) { + flex_var: Var, + rigid_name: Ident.Idx, + recursive: void, // All recursive types are equivalent for deduplication + }; + + /// Get the dispatcher (first argument) identity from a function content + fn getDispatcherIdentity(self: *Self, fn_content: SnapshotContentIdx) ?DispatcherIdentity { + const content = self.snapshots.getContent(fn_content); + if (content != .structure) return null; + + const fn_args = switch (content.structure) { + .fn_effectful => |func| func.args, + .fn_pure => |func| func.args, + .fn_unbound => |func| func.args, + else => return null, + }; + + if (fn_args.len() == 0) return null; + + const first_arg_idx = self.snapshots.sliceVars(fn_args)[0]; + const first_arg_content = self.snapshots.getContent(first_arg_idx); + + return switch (first_arg_content) { + .flex => |flex| DispatcherIdentity{ .flex_var = flex.var_ }, + .rigid => |rigid| DispatcherIdentity{ .rigid_name = rigid.name }, + .recursive => DispatcherIdentity{ .recursive = {} }, + else => null, + }; + } + + fn dispatcherIdentitiesEqual(a: ?DispatcherIdentity, b: ?DispatcherIdentity) bool { + if (a == null or b == null) return false; + return switch (a.?) { + .flex_var => |a_var| switch (b.?) { + .flex_var => |b_var| a_var == b_var, + else => false, + }, + .rigid_name => |a_name| switch (b.?) { + .rigid_name => |b_name| a_name == b_name, + else => false, + }, + .recursive => switch (b.?) { + .recursive => true, // All recursive types are equal + else => false, + }, + }; + } + /// Generate a name for a flex var that may appear multiple times in the type fn writeFlexVarName(self: *Self, flex_var: Var, _: SnapshotContentIdx, context: TypeContext, root_idx: SnapshotContentIdx) std.mem.Allocator.Error!void { // Check if we've seen this flex var before. @@ -1464,11 +1526,11 @@ pub const SnapshotWriter = struct { break :blk self.snapshots.sliceVars(fn_args)[0]; }; - try self.writeWithContext(idx, .General, dispatcher); + try self.writeWithContext(dispatcher, .General, idx); _ = try self.buf.writer().write("."); _ = try self.buf.writer().write(self.idents.getText(constraint.fn_name)); _ = try self.buf.writer().write(" : "); - try self.writeWithContext(idx, .General, constraint.fn_content); + try self.writeWithContext(constraint.fn_content, .General, idx); } _ = try self.buf.writer().write("]"); } @@ -1856,15 +1918,77 @@ pub const SnapshotWriter = struct { } /// Append a constraint to the list, if it doesn't already exist + /// Deduplicates based on method name and dispatcher type variable fn appendStaticDispatchConstraint(self: *Self, constraint_to_add: SnapshotStaticDispatchConstraint) std.mem.Allocator.Error!void { + // Extract dispatcher (first arg) identity from the constraint to add + const add_dispatcher = self.getDispatcherIdentity(constraint_to_add.fn_content); + for (self.static_dispatch_constraints.items) |constraint| { - if (constraint.fn_name == constraint_to_add.fn_name and constraint.fn_content == constraint_to_add.fn_content) { - return; + if (constraint.fn_name == constraint_to_add.fn_name) { + // Same method name - check if dispatcher identity is the same + const existing_dispatcher = self.getDispatcherIdentity(constraint.fn_content); + if (dispatcherIdentitiesEqual(add_dispatcher, existing_dispatcher)) { + return; // Duplicate constraint + } + // Also check fn_content directly for backwards compatibility + if (constraint.fn_content == constraint_to_add.fn_content) { + return; + } } } _ = try self.static_dispatch_constraints.append(constraint_to_add); } + /// Dispatcher identity for deduplication - either a Var (for flex), an Ident.Idx (for rigid), or recursive + const DispatcherIdentity = union(enum) { + flex_var: Var, + rigid_name: Ident.Idx, + recursive: void, // All recursive types are equivalent for deduplication + }; + + /// Get the dispatcher (first argument) identity from a function content + fn getDispatcherIdentity(self: *Self, fn_content: SnapshotContentIdx) ?DispatcherIdentity { + const content = self.snapshots.getContent(fn_content); + if (content != .structure) return null; + + const fn_args = switch (content.structure) { + .fn_effectful => |func| func.args, + .fn_pure => |func| func.args, + .fn_unbound => |func| func.args, + else => return null, + }; + + if (fn_args.len() == 0) return null; + + const first_arg_idx = self.snapshots.sliceVars(fn_args)[0]; + const first_arg_content = self.snapshots.getContent(first_arg_idx); + + return switch (first_arg_content) { + .flex => |flex| DispatcherIdentity{ .flex_var = flex.var_ }, + .rigid => |rigid| DispatcherIdentity{ .rigid_name = rigid.name }, + .recursive => DispatcherIdentity{ .recursive = {} }, + else => null, + }; + } + + fn dispatcherIdentitiesEqual(a: ?DispatcherIdentity, b: ?DispatcherIdentity) bool { + if (a == null or b == null) return false; + return switch (a.?) { + .flex_var => |a_var| switch (b.?) { + .flex_var => |b_var| a_var == b_var, + else => false, + }, + .rigid_name => |a_name| switch (b.?) { + .rigid_name => |b_name| a_name == b_name, + else => false, + }, + .recursive => switch (b.?) { + .recursive => true, // All recursive types are equal + else => false, + }, + }; + } + /// Generate a name for a flex var that may appear multiple times in the type fn writeFlexVarName(self: *Self, flex_var: Var, _: SnapshotContentIdx, context: TypeContext, root_idx: SnapshotContentIdx) std.mem.Allocator.Error!void { // Check if we've seen this flex var before. diff --git a/src/reporting/renderer.zig b/src/reporting/renderer.zig index d7bd0d9131..328d3798ce 100644 --- a/src/reporting/renderer.zig +++ b/src/reporting/renderer.zig @@ -298,8 +298,8 @@ fn renderElementToTerminal(element: DocumentElement, writer: *std.Io.Writer, pal try writer.writeAll(" │ "); try writer.writeAll(palette.reset); - // Print spaces up to the start column - try source_region.printSpaces(writer, region.start_column - 1); + // Print leading whitespace, preserving tabs from the source line + try source_region.printLeadingWhitespace(writer, line, region.start_column); // Print the underline try writer.writeAll(color); @@ -370,9 +370,15 @@ fn renderElementToTerminal(element: DocumentElement, writer: *std.Io.Writer, pal var col_position: u32 = 1; for (data.underline_regions) |underline| { if (underline.start_line == line_num and underline.start_line == underline.end_line) { - // Print spaces up to the start column + // Print whitespace up to the start column if (underline.start_column > col_position) { - try source_region.printSpaces(writer, underline.start_column - col_position); + if (col_position == 1) { + // First underline: preserve tabs from source + try source_region.printLeadingWhitespace(writer, line, underline.start_column); + } else { + // Subsequent underlines: just use spaces + try source_region.printSpaces(writer, underline.start_column - col_position); + } } // Print the underline diff --git a/src/reporting/source_region.zig b/src/reporting/source_region.zig index 65f2a41795..c59cee1d44 100644 --- a/src/reporting/source_region.zig +++ b/src/reporting/source_region.zig @@ -32,6 +32,40 @@ pub fn printSpaces(writer: anytype, count: u32) !void { } } +/// Print leading whitespace from source line, preserving tabs. +/// Copies exact whitespace characters (tabs/spaces) from the line, +/// and uses spaces for non-whitespace characters. +/// This ensures underlines align correctly when the source contains tabs. +/// +/// Parameters: +/// - writer: The output writer +/// - line: The source line text +/// - target_column: The 1-based column to print up to (exclusive) +pub fn printLeadingWhitespace(writer: anytype, line: []const u8, target_column: u32) !void { + if (target_column <= 1) return; + + const chars_to_print = target_column - 1; + var i: u32 = 0; + while (i < chars_to_print) : (i += 1) { + if (i < line.len) { + const char = line[i]; + if (char == '\t') { + // Preserve tabs exactly + try writer.writeAll("\t"); + } else if (char == ' ') { + // Preserve spaces exactly + try writer.writeAll(" "); + } else { + // Non-whitespace: use a space to maintain width + try writer.writeAll(" "); + } + } else { + // Past end of line: use spaces + try writer.writeAll(" "); + } + } +} + // ===== TESTS ===== test "calculateLineNumberWidth" {