const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const build_config = @import("../build_config.zig"); /// A diagnostic message from parsing. This is used to provide additional /// human-friendly warnings and errors about the parsed data. /// /// All of the memory for the diagnostic is allocated from the arena /// associated with the config structure. If an arena isn't available /// then diagnostics are not supported. pub const Diagnostic = struct { location: Location = .none, key: [:0]const u8 = "", message: [:0]const u8, /// Write the full user-friendly diagnostic message to the writer. pub fn write(self: *const Diagnostic, writer: anytype) !void { switch (self.location) { .none => {}, .cli => |index| try writer.print("cli:{}:", .{index}), .file => |file| try writer.print( "{s}:{}:", .{ file.path, file.line }, ), } if (self.key.len > 0) { try writer.print("{s}: ", .{self.key}); } else if (self.location != .none) { try writer.print(" ", .{}); } try writer.print("{s}", .{self.message}); } }; /// The possible locations for a diagnostic message. This is used /// to provide context for the message. pub const Location = union(enum) { none, cli: usize, file: struct { path: []const u8, line: usize, }, pub const Key = @typeInfo(Location).Union.tag_type.?; pub fn fromIter(iter: anytype) Location { const Iter = t: { const T = @TypeOf(iter); break :t switch (@typeInfo(T)) { .Pointer => |v| v.child, .Struct => T, else => return .none, }; }; if (!@hasDecl(Iter, "location")) return .none; return iter.location() orelse .none; } }; /// A list of diagnostics. The "_diagnostics" field must be this type /// for diagnostics to be supported. If this field is an incorrect type /// a compile-time error will be raised. /// /// This is implemented as a simple wrapper around an array list /// so that we can inject some logic around adding diagnostics /// and potentially in the future structure them differently. pub const DiagnosticList = struct { /// The list of diagnostics. list: std.ArrayListUnmanaged(Diagnostic) = .{}, /// Precomputed data for diagnostics. This is used specifically /// when we build libghostty so that we can precompute the messages /// and return them via the C API without allocating memory at /// call time. precompute: Precompute = precompute_init, const precompute_enabled = switch (build_config.artifact) { // We enable precompute for tests so that the logic is // semantically analyzed and run. .exe, .wasm_module => builtin.is_test, // We specifically want precompute for libghostty. .lib => true, }; const Precompute = if (precompute_enabled) struct { messages: std.ArrayListUnmanaged([:0]const u8) = .{}, } else void; const precompute_init: Precompute = if (precompute_enabled) .{} else {}; pub fn append( self: *DiagnosticList, alloc: Allocator, diag: Diagnostic, ) Allocator.Error!void { try self.list.append(alloc, diag); errdefer _ = self.list.pop(); if (comptime precompute_enabled) { var buf = std.ArrayList(u8).init(alloc); defer buf.deinit(); try diag.write(buf.writer()); const owned: [:0]const u8 = try buf.toOwnedSliceSentinel(0); errdefer alloc.free(owned); try self.precompute.messages.append(alloc, owned); errdefer _ = self.precompute.messages.pop(); assert(self.precompute.messages.items.len == self.list.items.len); } } pub fn empty(self: *const DiagnosticList) bool { return self.list.items.len == 0; } pub fn items(self: *const DiagnosticList) []const Diagnostic { return self.list.items; } /// Returns true if there are any diagnostics for the given /// location type. pub fn containsLocation( self: *const DiagnosticList, location: Location.Key, ) bool { for (self.list.items) |diag| { if (diag.location == location) return true; } return false; } };