namespace Compiler; public sealed class Diagnostic(DiagnosticSeverity severity, string message, string? help, FileInfo? file) { public static DiagnosticBuilder Info(string message) => new DiagnosticBuilder(DiagnosticSeverity.Info, message); public static DiagnosticBuilder Warning(string message) => new DiagnosticBuilder(DiagnosticSeverity.Warning, message); public static DiagnosticBuilder Error(string message) => new DiagnosticBuilder(DiagnosticSeverity.Error, message); public readonly DiagnosticSeverity Severity = severity; public readonly string Message = message; public readonly string? Help = help; public readonly FileInfo? File = file; } public sealed class DiagnosticBuilder(DiagnosticSeverity severity, string message) { private FileInfo? file; private string? help; public DiagnosticBuilder At(string fileName, int line, int column, int length) { file = new FileInfo(fileName, line, column, length); return this; } public DiagnosticBuilder At(string fileName, Token? token) { if (token != null) { At(fileName, token.Line, token.Column, token.Length); } return this; } public DiagnosticBuilder At(string fileName, Node? node) { if (node != null && node.Tokens.Count != 0) { // todo(nub31): Calculate length based on last token At(fileName, node.Tokens[0]); } return this; } public DiagnosticBuilder WithHelp(string helpMessage) { help = helpMessage; return this; } public Diagnostic Build() { return new Diagnostic(severity, message, help, file); } } public sealed class FileInfo(string file, int line, int column, int length) { public readonly string File = file; public readonly int Line = line; public readonly int Column = column; public readonly int Length = length; } public enum DiagnosticSeverity { Info, Warning, Error, } public sealed class CompileException(Diagnostic diagnostic) : Exception { public readonly Diagnostic Diagnostic = diagnostic; } public static class DiagnosticFormatter { public static void Print(Diagnostic diagnostic, TextWriter writer) { var (label, color) = diagnostic.Severity switch { DiagnosticSeverity.Info => ("info", Ansi.Cyan), DiagnosticSeverity.Warning => ("warning", Ansi.Yellow), DiagnosticSeverity.Error => ("error", Ansi.Red), _ => ("unknown", Ansi.Reset), }; writer.Write(color); writer.Write(label); writer.Write(Ansi.Reset); writer.Write(": "); writer.WriteLine(diagnostic.Message); if (diagnostic.File is null) return; var file = diagnostic.File; var lineNumberWidth = diagnostic.File.Line.ToString().Length; writer.WriteLine($" {new string(' ', lineNumberWidth)}{file.File}:{file.Line}:{file.Column}"); writer.WriteLine($"{new string(' ', lineNumberWidth)} | "); var sourceLine = TryReadLine(file.File, file.Line); if (sourceLine != null) { writer.Write($"{file.Line.ToString().PadLeft(lineNumberWidth)} | "); writer.WriteLine(sourceLine); writer.Write(new string(' ', lineNumberWidth)); writer.Write(" | "); writer.Write(new string(' ', file.Column - 1)); writer.Write(color); writer.Write(new string('^', Math.Max(1, file.Length))); writer.WriteLine(Ansi.Reset); } writer.WriteLine($"{new string(' ', lineNumberWidth)} |"); if (!string.IsNullOrWhiteSpace(diagnostic.Help)) { writer.WriteLine($" = help: {diagnostic.Help}"); } } private static string? TryReadLine(string file, int line) { try { using var reader = new StreamReader(file); for (var i = 1; i < line; i++) { if (reader.ReadLine() == null) return null; } return reader.ReadLine(); } catch { return null; } } private static class Ansi { public const string Reset = "\e[0m"; public const string Red = "\e[31m"; public const string Yellow = "\e[33m"; public const string Cyan = "\e[36m"; } }