const std = @import("std"); const Span = @import("ast.zig").Span; pub const Level = enum { err, warn, note, }; pub const SourceLoc = struct { line: u32, col: u32, pub fn compute(source: []const u8, byte_offset: u32) SourceLoc { var line: u32 = 1; var col: u32 = 1; const end = @min(byte_offset, @as(u32, @intCast(source.len))); for (source[0..end]) |c| { if (c == '\n') { line += 1; col = 1; } else { col += 1; } } return .{ .line = line, .col = col }; } }; pub const Diagnostic = struct { level: Level, message: []const u8, span: ?Span, source_file: ?[]const u8 = null, }; pub const DiagnosticList = struct { items: std.ArrayList(Diagnostic) = .empty, allocator: std.mem.Allocator, source: []const u8, file_name: []const u8, current_source_file: ?[]const u8 = null, import_sources: ?*const std.StringHashMap([:0]const u8) = null, pub fn init(allocator: std.mem.Allocator, source: []const u8, file_name: []const u8) DiagnosticList { return .{ .allocator = allocator, .source = source, .file_name = file_name, }; } pub fn deinit(self: *DiagnosticList) void { self.items.deinit(self.allocator); } pub fn add(self: *DiagnosticList, level: Level, message: []const u8, span: ?Span) void { // Deduplicate: skip if same level+span+message already exists for (self.items.items) |d| { if (d.level == level and std.mem.eql(u8, d.message, message)) { const a = d.span orelse continue; const b = span orelse continue; if (a.start == b.start and a.end == b.end) return; } } self.items.append(self.allocator, .{ .level = level, .message = message, .span = span, .source_file = self.current_source_file, }) catch {}; } pub fn addFmt(self: *DiagnosticList, level: Level, span: ?Span, comptime fmt: []const u8, args: anytype) void { const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error"; self.add(level, message, span); } pub fn hasErrors(self: *const DiagnosticList) bool { for (self.items.items) |d| { if (d.level == .err) return true; } return false; } fn resolveSourceAndFile(self: *const DiagnosticList, d: Diagnostic) struct { source: []const u8, file_name: []const u8 } { if (d.source_file) |sf| { if (self.import_sources) |is| { if (is.get(sf)) |src| { return .{ .source = src, .file_name = sf }; } } } return .{ .source = self.source, .file_name = self.file_name }; } pub fn render(self: *const DiagnosticList, writer: anytype) !void { for (self.items.items) |d| { const level_str = switch (d.level) { .err => "error", .warn => "warning", .note => "note", }; if (d.span) |span| { const resolved = self.resolveSourceAndFile(d); const loc = SourceLoc.compute(resolved.source, span.start); try writer.print("{s}:{d}:{d}: {s}: {s}\n", .{ resolved.file_name, loc.line, loc.col, level_str, d.message }); } else { try writer.print("{s}: {s}: {s}\n", .{ self.file_name, level_str, d.message }); } } } pub fn renderDebug(self: *const DiagnosticList) void { for (self.items.items) |d| { const level_str = switch (d.level) { .err => "error", .warn => "warning", .note => "note", }; if (d.span) |span| { const resolved = self.resolveSourceAndFile(d); const loc = SourceLoc.compute(resolved.source, span.start); std.debug.print("{s}:{d}:{d}: {s}: {s}\n", .{ resolved.file_name, loc.line, loc.col, level_str, d.message }); } else { std.debug.print("{s}: {s}: {s}\n", .{ self.file_name, level_str, d.message }); } } } };