This commit is contained in:
nub31
2025-05-24 21:40:48 +02:00
parent 30b3df626a
commit 43766f6d97
10 changed files with 403 additions and 20 deletions

View 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();
}

View File

@@ -1,6 +1,6 @@
namespace Nub.Lang.Frontend.Lexing; 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; public string Documentation { get; } = documentation;
} }

View File

@@ -1,6 +1,6 @@
namespace Nub.Lang.Frontend.Lexing; 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; public string Value { get; } = value;
} }

View File

@@ -55,13 +55,13 @@ public class Lexer
}; };
private string _src = null!; private string _src = null!;
private string _filePath = null!; private SourceFile _sourceFile;
private int _index; private int _index;
public List<Token> Lex(string src, string filePath) public List<Token> Lex(string src, SourceFile sourceFile)
{ {
_src = src; _src = src;
_filePath = filePath; _sourceFile = sourceFile;
_index = 0; _index = 0;
List<Token> tokens = []; List<Token> tokens = [];
@@ -121,7 +121,7 @@ public class Lexer
if (documentation != null) if (documentation != null)
{ {
return new DocumentationToken(_filePath, startIndex, _index, documentation); return new DocumentationToken(_sourceFile, startIndex, _index, documentation);
} }
ConsumeWhitespace(); ConsumeWhitespace();
@@ -144,20 +144,20 @@ public class Lexer
if (Keywords.TryGetValue(buffer, out var keywordSymbol)) 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)) 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") 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)) 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 // TODO: Revisit this
@@ -215,7 +215,7 @@ public class Lexer
Next(); 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)) if (Chars.TryGetValue(current, out var charSymbol))
{ {
Next(); Next();
return new SymbolToken(_filePath, startIndex, _index, charSymbol); return new SymbolToken(_sourceFile, startIndex, _index, charSymbol);
} }
if (current == '"') if (current == '"')
@@ -248,7 +248,7 @@ public class Lexer
Next(); 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}"); throw new Exception($"Unknown character {current}");

View File

@@ -1,6 +1,6 @@
namespace Nub.Lang.Frontend.Lexing; 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 NubType Type { get; } = type;
public string Value { get; } = value; public string Value { get; } = value;

View File

@@ -1,6 +1,6 @@
namespace Nub.Lang.Frontend.Lexing; 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; public Modifier Modifier { get; } = modifier;
} }

View File

@@ -1,6 +1,6 @@
namespace Nub.Lang.Frontend.Lexing; 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; public Symbol Symbol { get; } = symbol;
} }

View File

@@ -1,8 +1,8 @@
namespace Nub.Lang.Frontend.Lexing; 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 StartIndex { get; } = startIndex;
public int EndIndex { get; } = endIndex; public int EndIndex { get; } = endIndex;
} }

View File

@@ -69,7 +69,7 @@ internal static class Program
foreach (var filePath in filePaths) foreach (var filePath in filePaths)
{ {
var src = File.ReadAllText(filePath); 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); var module = Parser.ParseModule(tokens, rootFilePath);

View 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;
}