diff --git a/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs b/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs index c761e85..0f547ac 100644 --- a/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs +++ b/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs @@ -38,7 +38,7 @@ public class Diagnostic { if (!node.Tokens.Any()) { - throw new ArgumentException("Node has no tokens"); + throw new ArgumentException("Node has no tokens", nameof(node)); } _sourceFile = node.Tokens[0].SourceFile; @@ -85,22 +85,15 @@ public class Diagnostic { var sb = new StringBuilder(); - var severityText = 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), - _ => "diagnostic" - }; - - + var severityText = GetSeverityText(Severity); sb.Append(severityText); + if (SourceFile.HasValue) { var locationText = $" at {SourceFile.Value.Path}:{Span}"; sb.Append(ConsoleColors.Colorize(locationText, ConsoleColors.Gray)); } - + sb.Append(": "); sb.AppendLine(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite)); @@ -119,59 +112,72 @@ public class Diagnostic return sb.ToString(); } - private void AppendSourceContext(StringBuilder sb, SourceFile sourceFile, SourceSpan span) + private static string GetSeverityText(DiagnosticSeverity severity) + { + return 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), + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, "Unknown diagnostic severity") + }; + } + + private static void AppendSourceContext(StringBuilder sb, SourceFile sourceFile, SourceSpan span) { var lines = sourceFile.Content.Split('\n'); var startLine = span.Start.Line; var endLine = span.End.Line; - const int CONTEXT_LINES = 3; + const int contextLines = 3; - var maxLineNum = Math.Min(endLine + CONTEXT_LINES, lines.Length); - var lineNumWidth = maxLineNum.ToString().Length; - - var contextStart = Math.Max(1, startLine - CONTEXT_LINES); - for (var lineNum = contextStart; lineNum < startLine; lineNum++) + if (startLine < 1 || startLine > lines.Length || endLine < 1 || endLine > lines.Length) { - if (lineNum <= lines.Length) - { - AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth); - } + return; } - for (var lineNum = startLine; lineNum <= endLine && lineNum <= lines.Length; lineNum++) + var maxLineNum = Math.Min(endLine + contextLines, lines.Length); + var lineNumWidth = maxLineNum.ToString().Length; + + var contextStart = Math.Max(1, startLine - contextLines); + + for (var lineNum = contextStart; lineNum < startLine; lineNum++) + { + AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth); + } + + for (var lineNum = startLine; lineNum <= endLine; lineNum++) { var line = lines[lineNum - 1]; AppendContextLine(sb, lineNum, line, lineNumWidth); AppendErrorIndicators(sb, span, lineNum, line, lineNumWidth); } - var contextEnd = Math.Min(lines.Length, endLine + CONTEXT_LINES); + var contextEnd = Math.Min(lines.Length, endLine + contextLines); for (var lineNum = endLine + 1; lineNum <= contextEnd; lineNum++) { - if (lineNum <= lines.Length) - { - AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth); - } + AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth); } } - private void AppendContextLine(StringBuilder sb, int lineNum, string line, int lineNumWidth) + private static void AppendContextLine(StringBuilder sb, int lineNum, string line, int lineNumWidth) { var lineNumStr = lineNum.ToString().PadLeft(lineNumWidth); - sb.Append(ConsoleColors.Colorize(lineNumStr, ConsoleColors.Gray)); sb.Append(ConsoleColors.Colorize(" | ", ConsoleColors.Gray)); sb.AppendLine(line); } - private void AppendErrorIndicators(StringBuilder sb, SourceSpan span, int lineNum, string line, int lineNumWidth) + private static void AppendErrorIndicators(StringBuilder sb, SourceSpan span, int lineNum, string line, int lineNumWidth) { sb.Append(new string(' ', lineNumWidth + 3)); + var indicators = GetIndicatorsForLine(span, lineNum, line); + sb.AppendLine(ConsoleColors.Colorize(indicators, ConsoleColors.Red)); + } - const char ERROR_INDICATOR = '^'; - - string indicators; + private static string GetIndicatorsForLine(SourceSpan span, int lineNum, string line) + { + const char indicator = '^'; if (lineNum == span.Start.Line && lineNum == span.End.Line) { @@ -179,27 +185,21 @@ public class Diagnostic var endCol = Math.Min(line.Length, span.End.Column - 1); var length = Math.Max(1, endCol - startCol); - var spaces = new string(' ', startCol); - var carets = new string(ERROR_INDICATOR, length); - indicators = spaces + carets; - } - else if (lineNum == span.Start.Line) - { - var startCol = Math.Max(0, span.Start.Column - 1); - var spaces = new string(' ', startCol); - var carets = new string(ERROR_INDICATOR, line.Length - startCol); - indicators = spaces + carets; - } - else if (lineNum == span.End.Line) - { - var endCol = Math.Min(line.Length, span.End.Column - 1); - indicators = new string(ERROR_INDICATOR, endCol); - } - else - { - indicators = new string(ERROR_INDICATOR, line.Length); + return new string(' ', startCol) + new string(indicator, length); } - sb.AppendLine(ConsoleColors.Colorize(indicators, ConsoleColors.Red)); + if (lineNum == span.Start.Line) + { + var startCol = Math.Max(0, span.Start.Column - 1); + return new string(' ', startCol) + new string(indicator, Math.Max(0, line.Length - startCol)); + } + + if (lineNum == span.End.Line) + { + var endCol = Math.Min(line.Length, span.End.Column - 1); + return new string(indicator, Math.Max(0, endCol)); + } + + return new string(indicator, line.Length); } } \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs b/src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs index 5b48737..8a7ce89 100644 --- a/src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs +++ b/src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Nub.Lang.Frontend.Lexing; using Nub.Lang.Frontend.Parsing; @@ -9,13 +10,10 @@ public readonly struct SourceFile(string path, string content) public string Content { get; } = content; } -public readonly struct SourceLocation(int line, int column, int index) +public readonly struct SourceLocation(int line, int column) { public int Line { get; } = line; public int Column { get; } = column; - public int Index { get; } = index; - - public override string ToString() => $"{Line}:{Column}"; } public readonly struct SourceSpan(SourceLocation start, SourceLocation end) @@ -23,35 +21,61 @@ public readonly struct SourceSpan(SourceLocation start, SourceLocation end) public SourceLocation Start { get; } = start; public SourceLocation End { get; } = end; - public override string ToString() => $"{Start}-{End}"; + public override string ToString() + { + if (Start.Line == End.Line) + { + if (Start.Column == End.Column) + { + return $"{Start.Line}:{Start.Column}"; + } + + return $"{Start.Line}:{Start.Column}-{End.Column}"; + } + + return $"{Start.Line}:{Start.Column}-{End.Line}:{End.Column}"; + } } public static class SourceLocationCalculator { + private static int[] GetLineStarts(string content) + { + var lineStarts = new List { 0 }; + + for (var i = 0; i < content.Length; i++) + { + if (content[i] == '\n') + { + lineStarts.Add(i + 1); + } + } + + return lineStarts.ToArray(); + } + private static SourceLocation IndexToLocation(string content, int index) { if (index < 0 || index > content.Length) { - throw new ArgumentOutOfRangeException(nameof(index)); + throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} is out of range for content of length {content.Length}"); } - var line = 1; - var column = 1; + var lineStarts = GetLineStarts(content); - for (var i = 0; i < index && i < content.Length; i++) + var line = Array.BinarySearch(lineStarts, index); + if (line < 0) { - if (content[i] == '\n') - { - line++; - column = 1; - } - else - { - column++; - } + line = ~line - 1; } - return new SourceLocation(line, column, index); + if (line < lineStarts.Length - 1 && lineStarts[line + 1] == index && index < content.Length && content[index] == '\n') + { + line++; + } + + var column = index - lineStarts[line] + 1; + return new SourceLocation(line + 1, column); } public static SourceSpan GetSpan(Token token) @@ -65,7 +89,7 @@ public static class SourceLocationCalculator { if (!node.Tokens.Any()) { - throw new ArgumentException("Node has no tokens"); + throw new ArgumentException("Node has no tokens", nameof(node)); } var firstToken = node.Tokens[0]; @@ -76,4 +100,4 @@ public static class SourceLocationCalculator return new SourceSpan(start, end); } -} +} \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs b/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs index 824e4a8..3481f86 100644 --- a/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs +++ b/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs @@ -569,9 +569,10 @@ public class Parser if (!Peek().TryGetValue(out var token)) { - throw new ParseException(Diagnostic.Error("Unexpected end of file while parsing type") - .At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last())) + throw new ParseException(Diagnostic + .Error("Unexpected end of file while parsing type") .WithHelp("Expected a type name") + .At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last())) .Build()); } @@ -586,9 +587,10 @@ public class Parser { if (!Peek().TryGetValue(out var token)) { - throw new ParseException(Diagnostic.Error("Unexpected end of file") - .At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last())) + throw new ParseException(Diagnostic + .Error("Unexpected end of file") .WithHelp("Expected more tokens to complete the syntax") + .At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last())) .Build()); } @@ -669,6 +671,7 @@ public class Parser throw new ParseException(Diagnostic .Error($"Expected identifier, but found {token.GetType().Name}") .WithHelp("Provide a valid identifier name here") + .At(token) .Build()); }