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