using System.Text; using NubLang.Syntax; namespace NubLang.Diagnostics; public class Diagnostic { public class DiagnosticBuilder { private readonly DiagnosticSeverity _severity; private readonly string _message; private SourceSpan? _span; private string? _help; public DiagnosticBuilder(DiagnosticSeverity severity, string message) { _severity = severity; _message = message; } public DiagnosticBuilder At(SyntaxNode? node) { if (node != null) { _span = SourceSpan.Merge(node.Tokens.Select(x => x.Span)); } return this; } public DiagnosticBuilder At(Token? token) { if (token != null) { At(token.Span); } return this; } public DiagnosticBuilder At(SourceSpan? span) { if (span != null) { _span = span; } return this; } public DiagnosticBuilder At(string filePath, int line, int column) { _span = new SourceSpan(filePath, new SourceLocation(line, column), new SourceLocation(line, column)); return this; } public DiagnosticBuilder WithHelp(string help) { _help = help; return this; } public Diagnostic Build() => new(_severity, _message, _help, _span); } public static DiagnosticBuilder Error(string message) => new(DiagnosticSeverity.Error, message); public static DiagnosticBuilder Warning(string message) => new(DiagnosticSeverity.Warning, message); public static DiagnosticBuilder Info(string message) => new(DiagnosticSeverity.Info, message); public DiagnosticSeverity Severity { get; } public string Message { get; } public string? Help { get; } public SourceSpan? Span { get; } private Diagnostic(DiagnosticSeverity severity, string message, string? help, SourceSpan? span) { Severity = severity; Message = message; Help = help; Span = span; } public string FormatANSI() { try { var sb = new StringBuilder(); sb.Append(Severity switch { DiagnosticSeverity.Error => ConsoleColors.Colorize("error", ConsoleColors.Bold + ConsoleColors.Red), DiagnosticSeverity.Warning => ConsoleColors.Colorize("warning", ConsoleColors.Bold + ConsoleColors.Yellow), DiagnosticSeverity.Info => ConsoleColors.Colorize("info", ConsoleColors.Bold + ConsoleColors.Blue), _ => ConsoleColors.Colorize("unknown", ConsoleColors.Bold + ConsoleColors.White) }); if (Span.HasValue) { sb.Append(ConsoleColors.Colorize($" at {Span.Value}", ConsoleColors.Faint)); } sb.Append(": "); sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite)); if (Span.HasValue) { sb.AppendLine(); var text = File.ReadAllText(Span.Value.FilePath); var lines = text.Split('\n'); var startLine = Span.Value.Start.Line; var endLine = Span.Value.End.Line; const int CONTEXT_LINES = 3; var contextStartLine = Math.Max(1, startLine - CONTEXT_LINES); var contextEndLine = Math.Min(lines.Length, endLine + CONTEXT_LINES); var numberPadding = contextEndLine.ToString().Length; var codePadding = lines.Skip(contextStartLine - 1).Take(contextEndLine - contextStartLine + 1).Max(x => x.Length); sb.Append('╭'); sb.Append(new string('─', numberPadding + 2)); sb.Append('┬'); sb.Append(new string('─', codePadding + 2)); sb.Append('╮'); sb.AppendLine(); var tokenizer = new Tokenizer(Span.Value.FilePath, text); tokenizer.Tokenize(); for (var i = contextStartLine; i <= contextEndLine; i++) { var line = lines[i - 1]; sb.Append("│ "); sb.Append(i.ToString().PadRight(numberPadding)); sb.Append(" │ "); sb.Append(ApplySyntaxHighlighting(line.PadRight(codePadding), i, tokenizer.Tokens)); sb.Append(" │"); sb.AppendLine(); if (i >= startLine && i <= endLine) { var markerStartColumn = 1; var markerEndColumn = line.Length; if (i == startLine) { markerStartColumn = Math.Min(Span.Value.Start.Column, 1); } if (i == endLine) { markerEndColumn = Math.Min(Span.Value.End.Column, line.Length); } var markerLength = markerEndColumn - markerStartColumn; var marker = new string('^', markerLength); var markerColor = Severity switch { DiagnosticSeverity.Info => ConsoleColors.Blue, DiagnosticSeverity.Warning => ConsoleColors.Yellow, DiagnosticSeverity.Error => ConsoleColors.Red, _ => ConsoleColors.White }; sb.Append("│ "); sb.Append(new string(' ', numberPadding)); sb.Append(" │ "); sb.Append(new string(' ', markerStartColumn - 1)); sb.Append(ConsoleColors.Colorize(marker, markerColor)); sb.Append(new string(' ', codePadding - markerEndColumn + 1)); sb.Append(" │"); sb.AppendLine(); } } sb.Append('╰'); sb.Append(new string('─', numberPadding + 2)); sb.Append('┴'); sb.Append(new string('─', codePadding + 2)); sb.Append('╯'); } if (Help != null) { sb.AppendLine(); sb.Append(ConsoleColors.Colorize($"help: {Help}", ConsoleColors.Cyan)); } return sb.ToString(); } catch (Exception e) { return ConsoleColors.Colorize("Failed to generate error message", ConsoleColors.Red); } } private static string ApplySyntaxHighlighting(string line, int lineNumber, List tokens) { var sb = new StringBuilder(); var lineTokens = tokens .Where(t => t.Span.Start.Line == lineNumber) .OrderBy(t => t.Span.Start.Column) .ToList(); if (lineTokens.Count == 0) { return line; } var currentColumn = 1; foreach (var token in lineTokens) { var tokenStart = token.Span.Start.Column; var tokenEnd = token.Span.End.Column; if (tokenStart > currentColumn) { var beforeToken = line.Substring(currentColumn - 1, tokenStart - currentColumn); sb.Append(beforeToken); } var tokenLength = tokenEnd - tokenStart; if (tokenStart - 1 + tokenLength <= line.Length) { var tokenText = line.Substring(tokenStart - 1, tokenLength); var coloredToken = ColorizeToken(token, tokenText); sb.Append(coloredToken); } currentColumn = tokenEnd; } if (currentColumn <= line.Length) { var remaining = line[(currentColumn - 1)..]; sb.Append(remaining); } return sb.ToString(); } private static string ColorizeToken(Token token, string tokenText) { switch (token) { case IdentifierToken: { return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightWhite); } case StringLiteralToken: { return ConsoleColors.Colorize(tokenText, ConsoleColors.Green); } case IntLiteralToken: case FloatLiteralToken: case BoolLiteralToken: { return ConsoleColors.Colorize(tokenText, ConsoleColors.Magenta); } case SymbolToken symbolToken: { switch (symbolToken.Symbol) { case Symbol.Func: case Symbol.Return: case Symbol.If: case Symbol.Else: case Symbol.While: case Symbol.Break: case Symbol.Continue: case Symbol.Struct: case Symbol.Let: case Symbol.Calls: case Symbol.For: case Symbol.Extern: { return ConsoleColors.Colorize(tokenText, ConsoleColors.Bold + ConsoleColors.Blue); } case Symbol.Assign: case Symbol.Bang: case Symbol.Equal: case Symbol.NotEqual: case Symbol.LessThan: case Symbol.LessThanOrEqual: case Symbol.GreaterThan: case Symbol.GreaterThanOrEqual: case Symbol.Plus: case Symbol.Minus: case Symbol.Star: case Symbol.ForwardSlash: case Symbol.Caret: case Symbol.Ampersand: { return ConsoleColors.Colorize(tokenText, ConsoleColors.Yellow); } case Symbol.Colon: case Symbol.OpenParen: case Symbol.CloseParen: case Symbol.OpenBrace: case Symbol.CloseBrace: case Symbol.OpenBracket: case Symbol.CloseBracket: case Symbol.Comma: case Symbol.Period: case Symbol.Semi: { return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightBlack); } } break; } } return tokenText; } } public enum DiagnosticSeverity { Info, Warning, Error } public static class ConsoleColors { public const string Reset = "\e[0m"; public const string Bold = "\e[1m"; public const string Faint = "\e[2m"; public const string Italic = "\e[3m"; public const string Underline = "\e[4m"; public const string SlowBlink = "\e[5m"; public const string RapidBlink = "\e[6m"; public const string SwapBgAndFg = "\e[7m"; public const string Conceal = "\e[8m"; public const string CrossedOut = "\e[9m"; public const string DefaultFont = "\e[10m"; public const string AltFont1 = "\e[11m"; public const string AltFont2 = "\e[12m"; public const string AltFont3 = "\e[13m"; public const string AltFont4 = "\e[14m"; public const string AltFont5 = "\e[15m"; public const string AltFont6 = "\e[16m"; public const string AltFont7 = "\e[17m"; public const string AltFont8 = "\e[18m"; public const string AltFont9 = "\e[19m"; public const string Black = "\e[30m"; public const string Red = "\e[31m"; public const string Green = "\e[32m"; public const string Yellow = "\e[33m"; public const string Blue = "\e[34m"; public const string Magenta = "\e[35m"; public const string Cyan = "\e[36m"; public const string White = "\e[37m"; public const string BrightBlack = "\e[90m"; public const string BrightRed = "\e[91m"; public const string BrightGreen = "\e[92m"; public const string BrightYellow = "\e[93m"; public const string BrightBlue = "\e[94m"; public const string BrightMagenta = "\e[95m"; public const string BrightCyan = "\e[96m"; public const string BrightWhite = "\e[97m"; private static bool IsColorSupported() { var term = Environment.GetEnvironmentVariable("TERM"); var colorTerm = Environment.GetEnvironmentVariable("COLORTERM"); return !string.IsNullOrEmpty(term) || !string.IsNullOrEmpty(colorTerm) || !Console.IsOutputRedirected; } public static string Colorize(string text, string color) { return IsColorSupported() ? $"{color}{text}{Reset}" : text; } }