From 43766f6d970f36e0c7eef03fdaa9663faf16f37e Mon Sep 17 00:00:00 2001 From: nub31 Date: Sat, 24 May 2025 21:40:48 +0200 Subject: [PATCH] ... --- .../Frontend/Diagnostics/Diagnostics.cs | 376 ++++++++++++++++++ .../Frontend/Lexing/DocumentationToken.cs | 2 +- .../Frontend/Lexing/IdentifierToken.cs | 2 +- .../Nub.Lang/Frontend/Lexing/Lexer.cs | 24 +- .../Nub.Lang/Frontend/Lexing/LiteralToken.cs | 2 +- .../Nub.Lang/Frontend/Lexing/ModifierToken.cs | 2 +- .../Nub.Lang/Frontend/Lexing/SymbolToken.cs | 2 +- .../Nub.Lang/Frontend/Lexing/Token.cs | 4 +- src/compiler/Nub.Lang/Program.cs | 2 +- src/compiler/Nub.Lang/SourceFile.cs | 7 + 10 files changed, 403 insertions(+), 20 deletions(-) create mode 100644 src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs create mode 100644 src/compiler/Nub.Lang/SourceFile.cs diff --git a/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs b/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs new file mode 100644 index 0000000..f545764 --- /dev/null +++ b/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs @@ -0,0 +1,376 @@ +using System.Text; +using Nub.Lang.Frontend.Lexing; +using Nub.Lang.Frontend.Parsing; + +namespace Nub.Lang.Frontend.Diagnostics; + +/// +/// Represents a source location with line and column information +/// +public readonly struct SourceLocation(int line, int column, int index) +{ + public int Line { get; } = line; + public int Column { get; } = column; + public int Index { get; } = index; + + public override string ToString() => $"{Line}:{Column}"; +} + +/// +/// Represents a span of source code +/// +public readonly struct SourceSpan(SourceLocation start, SourceLocation end) +{ + public SourceLocation Start { get; } = start; + public SourceLocation End { get; } = end; + + public override string ToString() => $"{Start}-{End}"; +} + +/// +/// Severity levels for diagnostics +/// +public enum DiagnosticSeverity +{ + Info, + Warning, + Error +} + +/// +/// Represents a diagnostic message with source location +/// +public class Diagnostic +{ + public DiagnosticSeverity Severity { get; } + public string Message { get; } + public SourceFile SourceFile { get; } + public SourceSpan Span { get; } + public string? Help { get; } + + public Diagnostic(DiagnosticSeverity severity, string message, SourceFile sourceFile, + SourceSpan span, string? help = null) + { + Severity = severity; + Message = message; + SourceFile = sourceFile; + Span = span; + Help = help; + } +} + +/// +/// Utility class for converting indices to line/column positions +/// +public static class SourceLocationCalculator +{ + /// + /// Convert a character index to line/column position + /// + public static SourceLocation IndexToLocation(string content, int index) + { + if (index < 0 || index > content.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + int line = 1; + int column = 1; + + for (int i = 0; i < index && i < content.Length; i++) + { + if (content[i] == '\n') + { + line++; + column = 1; + } + else + { + column++; + } + } + + return new SourceLocation(line, column, index); + } + + /// + /// Get the source span for a token + /// + public static SourceSpan GetSpan(Token token) + { + var start = IndexToLocation(token.SourceFile.Content, token.StartIndex); + var end = IndexToLocation(token.SourceFile.Content, token.EndIndex); + return new SourceSpan(start, end); + } + + /// + /// Get the source span for a node (from first to last token) + /// + public static SourceSpan GetSpan(Node node) + { + if (!node.Tokens.Any()) + throw new ArgumentException("Node has no tokens"); + + var firstToken = node.Tokens.First(); + var lastToken = node.Tokens.Last(); + + var start = IndexToLocation(firstToken.SourceFile.Content, firstToken.StartIndex); + var end = IndexToLocation(lastToken.SourceFile.Content, lastToken.EndIndex); + + return new SourceSpan(start, end); + } +} + +/// +/// Builder for creating diagnostic messages +/// +public class DiagnosticBuilder +{ + private DiagnosticSeverity _severity; + private string _message = string.Empty; + private SourceFile _sourceFile; + private SourceSpan _span; + private string? _help; + + public static DiagnosticBuilder Error(string message) => + new() { _severity = DiagnosticSeverity.Error, _message = message }; + + public static DiagnosticBuilder Warning(string message) => + new() { _severity = DiagnosticSeverity.Warning, _message = message }; + + public static DiagnosticBuilder Info(string message) => + new() { _severity = DiagnosticSeverity.Info, _message = message }; + + public DiagnosticBuilder At(Token token) + { + _sourceFile = token.SourceFile; + _span = SourceLocationCalculator.GetSpan(token); + return this; + } + + public DiagnosticBuilder At(Node node) + { + if (!node.Tokens.Any()) + throw new ArgumentException("Node has no tokens"); + + _sourceFile = node.Tokens.First().SourceFile; + _span = SourceLocationCalculator.GetSpan(node); + return this; + } + + public DiagnosticBuilder At(SourceFile sourceFile, SourceSpan span) + { + _sourceFile = sourceFile; + _span = span; + return this; + } + + public DiagnosticBuilder WithHelp(string help) + { + _help = help; + return this; + } + + public Diagnostic Build() => new(_severity, _message, _sourceFile, _span, _help); +} + +/// +/// Formats diagnostic messages for display +/// +public class DiagnosticFormatter +{ + private readonly DiagnosticFormatterOptions _options; + + public DiagnosticFormatter(DiagnosticFormatterOptions? options = null) + { + _options = options ?? new DiagnosticFormatterOptions(); + } + + /// + /// Format a diagnostic as a string + /// + public string Format(Diagnostic diagnostic) + { + var sb = new StringBuilder(); + + // Header line: severity, location, and message + sb.Append(GetSeverityPrefix(diagnostic.Severity)); + sb.Append($" at {diagnostic.SourceFile.Path}:{diagnostic.Span}: "); + sb.AppendLine(diagnostic.Message); + + // Show source context + if (_options.ShowSourceContext) + { + AppendSourceContext(sb, diagnostic); + } + + // Show help if available + if (!string.IsNullOrEmpty(diagnostic.Help)) + { + sb.AppendLine(); + sb.Append(_options.HelpPrefix); + sb.AppendLine(diagnostic.Help); + } + + return sb.ToString(); + } + + /// + /// Format multiple diagnostics + /// + public string Format(IEnumerable diagnostics) + { + var sb = new StringBuilder(); + var diagnosticList = diagnostics.ToList(); + + for (int i = 0; i < diagnosticList.Count; i++) + { + sb.Append(Format(diagnosticList[i])); + + if (i < diagnosticList.Count - 1) + { + sb.AppendLine(); + sb.AppendLine(); + } + } + + return sb.ToString(); + } + + private string GetSeverityPrefix(DiagnosticSeverity severity) => severity switch + { + DiagnosticSeverity.Error => _options.ErrorPrefix, + DiagnosticSeverity.Warning => _options.WarningPrefix, + DiagnosticSeverity.Info => _options.InfoPrefix, + _ => "diagnostic" + }; + + private void AppendSourceContext(StringBuilder sb, Diagnostic diagnostic) + { + var lines = diagnostic.SourceFile.Content.Split('\n'); + var startLine = diagnostic.Span.Start.Line; + var endLine = diagnostic.Span.End.Line; + + // Calculate line number width for padding + var maxLineNum = Math.Min(endLine + _options.ContextLines, lines.Length); + var lineNumWidth = maxLineNum.ToString().Length; + + // Show context before error + var contextStart = Math.Max(1, startLine - _options.ContextLines); + for (int lineNum = contextStart; lineNum < startLine; lineNum++) + { + if (lineNum <= lines.Length) + { + sb.AppendLine($"{lineNum.ToString().PadLeft(lineNumWidth)} | {lines[lineNum - 1]}"); + } + } + + // Show error lines with highlighting + for (int lineNum = startLine; lineNum <= endLine && lineNum <= lines.Length; lineNum++) + { + var line = lines[lineNum - 1]; + sb.AppendLine($"{lineNum.ToString().PadLeft(lineNumWidth)} | {line}"); + + // Add error indicators + if (_options.ShowErrorIndicators) + { + sb.Append(new string(' ', lineNumWidth + 3)); // Padding for line number + " | " + + if (lineNum == startLine && lineNum == endLine) + { + // Single line error + var startCol = Math.Max(0, diagnostic.Span.Start.Column - 1); + var endCol = Math.Min(line.Length, diagnostic.Span.End.Column - 1); + var length = Math.Max(1, endCol - startCol); + + sb.Append(new string(' ', startCol)); + sb.AppendLine(new string(_options.ErrorIndicatorChar, length)); + } + else if (lineNum == startLine) + { + // First line of multi-line error + var startCol = Math.Max(0, diagnostic.Span.Start.Column - 1); + sb.Append(new string(' ', startCol)); + sb.AppendLine(new string(_options.ErrorIndicatorChar, line.Length - startCol)); + } + else if (lineNum == endLine) + { + // Last line of multi-line error + var endCol = Math.Min(line.Length, diagnostic.Span.End.Column - 1); + sb.AppendLine(new string(_options.ErrorIndicatorChar, endCol)); + } + else + { + // Middle line of multi-line error + sb.AppendLine(new string(_options.ErrorIndicatorChar, line.Length)); + } + } + } + + // Show context after error + var contextEnd = Math.Min(lines.Length, endLine + _options.ContextLines); + for (int lineNum = endLine + 1; lineNum <= contextEnd; lineNum++) + { + if (lineNum <= lines.Length) + { + sb.AppendLine($"{lineNum.ToString().PadLeft(lineNumWidth)} | {lines[lineNum - 1]}"); + } + } + } +} + +/// +/// Configuration options for diagnostic formatting +/// +public class DiagnosticFormatterOptions +{ + public string ErrorPrefix { get; set; } = "error"; + public string WarningPrefix { get; set; } = "warning"; + public string InfoPrefix { get; set; } = "info"; + public string HelpPrefix { get; set; } = "help: "; + + public bool ShowSourceContext { get; set; } = true; + public bool ShowErrorIndicators { get; set; } = true; + public char ErrorIndicatorChar { get; set; } = '^'; + public int ContextLines { get; set; } = 2; +} + +/// +/// Extension methods for convenient error reporting +/// +public static class ErrorReportingExtensions +{ + /// + /// Create an error diagnostic for a token + /// + public static Diagnostic Error(this Token token, string message, string? help = null) => + DiagnosticBuilder.Error(message).At(token).WithHelp(help).Build(); + + /// + /// Create a warning diagnostic for a token + /// + public static Diagnostic Warning(this Token token, string message, string? help = null) => + DiagnosticBuilder.Warning(message).At(token).WithHelp(help).Build(); + + /// + /// Create an info diagnostic for a token + /// + public static Diagnostic Info(this Token token, string message, string? help = null) => + DiagnosticBuilder.Info(message).At(token).WithHelp(help).Build(); + + /// + /// Create an error diagnostic for a node + /// + public static Diagnostic Error(this Node node, string message, string? help = null) => + DiagnosticBuilder.Error(message).At(node).WithHelp(help).Build(); + + /// + /// Create a warning diagnostic for a node + /// + public static Diagnostic Warning(this Node node, string message, string? help = null) => + DiagnosticBuilder.Warning(message).At(node).WithHelp(help).Build(); + + /// + /// Create an info diagnostic for a node + /// + public static Diagnostic Info(this Node node, string message, string? help = null) => + DiagnosticBuilder.Info(message).At(node).WithHelp(help).Build(); +} \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs index 422374b..4fb09f2 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs @@ -1,6 +1,6 @@ namespace Nub.Lang.Frontend.Lexing; -public class DocumentationToken(string filePath, int startIndex, int endIndex, string documentation) : Token(filePath, startIndex, endIndex) +public class DocumentationToken(SourceFile sourceFile, int startIndex, int endIndex, string documentation) : Token(sourceFile, startIndex, endIndex) { public string Documentation { get; } = documentation; } \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs index 92b37c7..077a802 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs @@ -1,6 +1,6 @@ namespace Nub.Lang.Frontend.Lexing; -public class IdentifierToken(string filePath, int startIndex, int endIndex, string value) : Token(filePath, startIndex, endIndex) +public class IdentifierToken(SourceFile sourceFile, int startIndex, int endIndex, string value) : Token(sourceFile, startIndex, endIndex) { public string Value { get; } = value; } \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs b/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs index e868073..7439c20 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs @@ -55,13 +55,13 @@ public class Lexer }; private string _src = null!; - private string _filePath = null!; + private SourceFile _sourceFile; private int _index; - public List Lex(string src, string filePath) + public List Lex(string src, SourceFile sourceFile) { _src = src; - _filePath = filePath; + _sourceFile = sourceFile; _index = 0; List tokens = []; @@ -121,7 +121,7 @@ public class Lexer if (documentation != null) { - return new DocumentationToken(_filePath, startIndex, _index, documentation); + return new DocumentationToken(_sourceFile, startIndex, _index, documentation); } ConsumeWhitespace(); @@ -144,20 +144,20 @@ public class Lexer if (Keywords.TryGetValue(buffer, out var keywordSymbol)) { - return new SymbolToken(_filePath, startIndex, _index, keywordSymbol); + return new SymbolToken(_sourceFile, startIndex, _index, keywordSymbol); } if (Modifiers.TryGetValue(buffer, out var modifer)) { - return new ModifierToken(_filePath, startIndex, _index, modifer); + return new ModifierToken(_sourceFile, startIndex, _index, modifer); } if (buffer is "true" or "false") { - return new LiteralToken(_filePath, startIndex, _index, NubPrimitiveType.Bool, buffer); + return new LiteralToken(_sourceFile, startIndex, _index, NubPrimitiveType.Bool, buffer); } - return new IdentifierToken(_filePath, startIndex, _index, buffer); + return new IdentifierToken(_sourceFile, startIndex, _index, buffer); } if (char.IsDigit(current)) @@ -195,7 +195,7 @@ public class Lexer } } - return new LiteralToken(_filePath, startIndex, _index, isFloat ? NubPrimitiveType.F64 : NubPrimitiveType.I64, buffer); + return new LiteralToken(_sourceFile, startIndex, _index, isFloat ? NubPrimitiveType.F64 : NubPrimitiveType.I64, buffer); } // TODO: Revisit this @@ -215,7 +215,7 @@ public class Lexer Next(); } - return new SymbolToken(_filePath, startIndex, _index, chain.Value); + return new SymbolToken(_sourceFile, startIndex, _index, chain.Value); } } } @@ -223,7 +223,7 @@ public class Lexer if (Chars.TryGetValue(current, out var charSymbol)) { Next(); - return new SymbolToken(_filePath, startIndex, _index, charSymbol); + return new SymbolToken(_sourceFile, startIndex, _index, charSymbol); } if (current == '"') @@ -248,7 +248,7 @@ public class Lexer Next(); } - return new LiteralToken(_filePath, startIndex, _index, NubPrimitiveType.String, buffer); + return new LiteralToken(_sourceFile, startIndex, _index, NubPrimitiveType.String, buffer); } throw new Exception($"Unknown character {current}"); diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs index 4797197..ddd734e 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs @@ -1,6 +1,6 @@ namespace Nub.Lang.Frontend.Lexing; -public class LiteralToken(string filePath, int startIndex, int endIndex, NubType type, string value) : Token(filePath, startIndex, endIndex) +public class LiteralToken(SourceFile sourceFile, int startIndex, int endIndex, NubType type, string value) : Token(sourceFile, startIndex, endIndex) { public NubType Type { get; } = type; public string Value { get; } = value; diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs index 0bc95fb..7927c21 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs @@ -1,6 +1,6 @@ namespace Nub.Lang.Frontend.Lexing; -public class ModifierToken(string filePath, int startIndex, int endIndex, Modifier modifier) : Token(filePath, startIndex, endIndex) +public class ModifierToken(SourceFile sourceFile, int startIndex, int endIndex, Modifier modifier) : Token(sourceFile, startIndex, endIndex) { public Modifier Modifier { get; } = modifier; } diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs index b1aa398..72a7331 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs @@ -1,6 +1,6 @@ namespace Nub.Lang.Frontend.Lexing; -public class SymbolToken(string filePath, int startIndex, int endIndex, Symbol symbol) : Token(filePath, startIndex, endIndex) +public class SymbolToken(SourceFile sourceFile, int startIndex, int endIndex, Symbol symbol) : Token(sourceFile, startIndex, endIndex) { public Symbol Symbol { get; } = symbol; } diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs b/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs index 154083f..ccf30b4 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs @@ -1,8 +1,8 @@ namespace Nub.Lang.Frontend.Lexing; -public abstract class Token(string filePath, int startIndex, int endIndex) +public abstract class Token(SourceFile sourceFile, int startIndex, int endIndex) { - public string FilePath { get; } = filePath; + public SourceFile SourceFile { get; } = sourceFile; public int StartIndex { get; } = startIndex; public int EndIndex { get; } = endIndex; } \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Program.cs b/src/compiler/Nub.Lang/Program.cs index ede202f..ca697a6 100644 --- a/src/compiler/Nub.Lang/Program.cs +++ b/src/compiler/Nub.Lang/Program.cs @@ -69,7 +69,7 @@ internal static class Program foreach (var filePath in filePaths) { var src = File.ReadAllText(filePath); - tokens.AddRange(Lexer.Lex(src, filePath)); + tokens.AddRange(Lexer.Lex(src, new SourceFile(filePath, src))); } var module = Parser.ParseModule(tokens, rootFilePath); diff --git a/src/compiler/Nub.Lang/SourceFile.cs b/src/compiler/Nub.Lang/SourceFile.cs new file mode 100644 index 0000000..b5b2271 --- /dev/null +++ b/src/compiler/Nub.Lang/SourceFile.cs @@ -0,0 +1,7 @@ +namespace Nub.Lang; + +public readonly struct SourceFile(string path, string content) +{ + public string Path { get; } = path; + public string Content { get; } = content; +}