...
This commit is contained in:
376
src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs
Normal file
376
src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using System.Text;
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a source location with line and column information
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a span of source code
|
||||
/// </summary>
|
||||
public readonly struct SourceSpan(SourceLocation start, SourceLocation end)
|
||||
{
|
||||
public SourceLocation Start { get; } = start;
|
||||
public SourceLocation End { get; } = end;
|
||||
|
||||
public override string ToString() => $"{Start}-{End}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity levels for diagnostics
|
||||
/// </summary>
|
||||
public enum DiagnosticSeverity
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a diagnostic message with source location
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for converting indices to line/column positions
|
||||
/// </summary>
|
||||
public static class SourceLocationCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a character index to line/column position
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the source span for a token
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the source span for a node (from first to last token)
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating diagnostic messages
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats diagnostic messages for display
|
||||
/// </summary>
|
||||
public class DiagnosticFormatter
|
||||
{
|
||||
private readonly DiagnosticFormatterOptions _options;
|
||||
|
||||
public DiagnosticFormatter(DiagnosticFormatterOptions? options = null)
|
||||
{
|
||||
_options = options ?? new DiagnosticFormatterOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a diagnostic as a string
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format multiple diagnostics
|
||||
/// </summary>
|
||||
public string Format(IEnumerable<Diagnostic> 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]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for diagnostic formatting
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for convenient error reporting
|
||||
/// </summary>
|
||||
public static class ErrorReportingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an error diagnostic for a token
|
||||
/// </summary>
|
||||
public static Diagnostic Error(this Token token, string message, string? help = null) =>
|
||||
DiagnosticBuilder.Error(message).At(token).WithHelp(help).Build();
|
||||
|
||||
/// <summary>
|
||||
/// Create a warning diagnostic for a token
|
||||
/// </summary>
|
||||
public static Diagnostic Warning(this Token token, string message, string? help = null) =>
|
||||
DiagnosticBuilder.Warning(message).At(token).WithHelp(help).Build();
|
||||
|
||||
/// <summary>
|
||||
/// Create an info diagnostic for a token
|
||||
/// </summary>
|
||||
public static Diagnostic Info(this Token token, string message, string? help = null) =>
|
||||
DiagnosticBuilder.Info(message).At(token).WithHelp(help).Build();
|
||||
|
||||
/// <summary>
|
||||
/// Create an error diagnostic for a node
|
||||
/// </summary>
|
||||
public static Diagnostic Error(this Node node, string message, string? help = null) =>
|
||||
DiagnosticBuilder.Error(message).At(node).WithHelp(help).Build();
|
||||
|
||||
/// <summary>
|
||||
/// Create a warning diagnostic for a node
|
||||
/// </summary>
|
||||
public static Diagnostic Warning(this Node node, string message, string? help = null) =>
|
||||
DiagnosticBuilder.Warning(message).At(node).WithHelp(help).Build();
|
||||
|
||||
/// <summary>
|
||||
/// Create an info diagnostic for a node
|
||||
/// </summary>
|
||||
public static Diagnostic Info(this Node node, string message, string? help = null) =>
|
||||
DiagnosticBuilder.Info(message).At(node).WithHelp(help).Build();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -55,13 +55,13 @@ public class Lexer
|
||||
};
|
||||
|
||||
private string _src = null!;
|
||||
private string _filePath = null!;
|
||||
private SourceFile _sourceFile;
|
||||
private int _index;
|
||||
|
||||
public List<Token> Lex(string src, string filePath)
|
||||
public List<Token> Lex(string src, SourceFile sourceFile)
|
||||
{
|
||||
_src = src;
|
||||
_filePath = filePath;
|
||||
_sourceFile = sourceFile;
|
||||
_index = 0;
|
||||
|
||||
List<Token> 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}");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
7
src/compiler/Nub.Lang/SourceFile.cs
Normal file
7
src/compiler/Nub.Lang/SourceFile.cs
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user