Diagnostics

This commit is contained in:
nub31
2025-07-23 00:43:50 +02:00
parent fd9fc6da66
commit bf89fe02d3
7 changed files with 193 additions and 45 deletions

View File

@@ -1,5 +1,11 @@
func main(args: []cstring): i64 func main(args: []cstring): i64
{ {
puts("test") puts("test") %%%
return 0 return 0
} }

View File

@@ -79,11 +79,11 @@ foreach (var file in options.Files)
foreach (var file in options.Files) foreach (var file in options.Files)
{ {
var tokenizer = new Tokenizer(file.GetText()); var tokenizer = new Tokenizer(file);
var tokens = tokenizer.Tokenize();
var parser = new Parser(); var parser = new Parser();
var syntaxTree = parser.Parse(tokens); var syntaxTree = parser.Parse(tokenizer.Tokenize());
diagnostics.AddRange(tokenizer.GetDiagnostics());
diagnostics.AddRange(parser.GetDiagnostics()); diagnostics.AddRange(parser.GetDiagnostics());
syntaxTrees.Add(syntaxTree); syntaxTrees.Add(syntaxTree);

View File

@@ -27,3 +27,15 @@ public class SourceFile
public static bool operator ==(SourceFile? left, SourceFile? right) => Equals(left, right); public static bool operator ==(SourceFile? left, SourceFile? right) => Equals(left, right);
public static bool operator !=(SourceFile? left, SourceFile? right) => !Equals(left, right); public static bool operator !=(SourceFile? left, SourceFile? right) => !Equals(left, right);
} }
public class SourceFileSpan
{
public SourceFileSpan(SourceFile sourceFile, SourceSpan span)
{
SourceFile = sourceFile;
Span = span;
}
public SourceFile SourceFile { get; }
public SourceSpan Span { get; }
}

View File

@@ -18,17 +18,6 @@ public readonly struct SourceLocation : IEquatable<SourceLocation>
return $"{Line}:{Column}"; return $"{Line}:{Column}";
} }
public int CompareTo(SourceLocation other)
{
var lineComparison = Line.CompareTo(other.Line);
if (lineComparison == 0)
{
return Column.CompareTo(other.Column);
}
return lineComparison;
}
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
return obj is SourceLocation other && Equals(other); return obj is SourceLocation other && Equals(other);

View File

@@ -1,4 +1,6 @@
using System.Text; using System.Text;
using NubLang.Code;
using NubLang.Tokenization;
namespace NubLang.Diagnostics; namespace NubLang.Diagnostics;
@@ -8,6 +10,7 @@ public class Diagnostic
{ {
private readonly DiagnosticSeverity _severity; private readonly DiagnosticSeverity _severity;
private readonly string _message; private readonly string _message;
private SourceFileSpan? _fileSpan;
private string? _help; private string? _help;
public DiagnosticBuilder(DiagnosticSeverity severity, string message) public DiagnosticBuilder(DiagnosticSeverity severity, string message)
@@ -16,13 +19,29 @@ public class Diagnostic
_message = message; _message = message;
} }
public DiagnosticBuilder At(Token token)
{
At(token.FileSpan);
return this;
}
public DiagnosticBuilder At(SourceFileSpan? fileSpan)
{
if (fileSpan != null)
{
_fileSpan = fileSpan;
}
return this;
}
public DiagnosticBuilder WithHelp(string help) public DiagnosticBuilder WithHelp(string help)
{ {
_help = help; _help = help;
return this; return this;
} }
public Diagnostic Build() => new(_severity, _message, _help); public Diagnostic Build() => new(_severity, _message, _help, _fileSpan);
} }
public static DiagnosticBuilder Error(string message) => new(DiagnosticSeverity.Error, message); public static DiagnosticBuilder Error(string message) => new(DiagnosticSeverity.Error, message);
@@ -32,25 +51,104 @@ public class Diagnostic
public DiagnosticSeverity Severity { get; } public DiagnosticSeverity Severity { get; }
public string Message { get; } public string Message { get; }
public string? Help { get; } public string? Help { get; }
public SourceFileSpan? FileSpan { get; }
private Diagnostic(DiagnosticSeverity severity, string message, string? help) private Diagnostic(DiagnosticSeverity severity, string message, string? help, SourceFileSpan? fileSpan)
{ {
Severity = severity; Severity = severity;
Message = message; Message = message;
Help = help; Help = help;
FileSpan = fileSpan;
} }
public string FormatANSI() public string FormatANSI()
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
var severityText = GetSeverityText(Severity); sb.Append(Severity switch
sb.Append(severityText); {
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),
_ => ConsoleColors.Colorize("unknown", ConsoleColors.Bold + ConsoleColors.White)
});
sb.Append(": "); sb.Append(": ");
sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite)); sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite));
if (!string.IsNullOrEmpty(Help)) if (FileSpan != null)
{
sb.AppendLine();
var text = FileSpan.SourceFile.GetText();
var lines = text.Split('\n');
var startLine = FileSpan.Span.Start.Line;
var endLine = FileSpan.Span.End.Line;
const int CONTEXT_LINES = 3;
var contextStartLine = Math.Max(1, startLine - CONTEXT_LINES);
var contextEndLine = Math.Min(lines.Length, endLine + CONTEXT_LINES);
var numberPadding = contextEndLine.ToString().Length;
var codePadding = lines.Skip(contextStartLine - 1).Take(contextEndLine - contextStartLine).Max(x => x.Length);
sb.Append('╭');
sb.Append(new string('─', numberPadding + 2));
sb.Append('┬');
sb.Append(new string('─', codePadding + 2));
sb.Append('╮');
sb.AppendLine();
for (var i = contextStartLine; i <= contextEndLine; i++)
{
var line = lines[i - 1];
sb.Append("│ ");
sb.Append(i.ToString().PadRight(numberPadding));
sb.Append(" │ ");
sb.Append(line.PadRight(codePadding));
sb.Append(" │");
sb.AppendLine();
if (i >= startLine && i <= endLine)
{
var markerStartColumn = 1;
var markerEndColumn = line.Length + 1;
if (i == startLine)
{
markerStartColumn = FileSpan.Span.Start.Column;
}
if (i == endLine)
{
markerEndColumn = FileSpan.Span.End.Column;
}
var markerLength = markerEndColumn - markerStartColumn;
var marker = new string('^', markerLength);
sb.Append("│ ");
sb.Append(new string(' ', numberPadding));
sb.Append(" │ ");
sb.Append(new string(' ', markerStartColumn - 1));
sb.Append(ConsoleColors.Colorize(marker, ConsoleColors.Red));
sb.Append(new string(' ', codePadding - markerEndColumn + 1));
sb.Append(" │");
sb.AppendLine();
}
}
sb.Append('╰');
sb.Append(new string('─', numberPadding + 2));
sb.Append('┴');
sb.Append(new string('─', codePadding + 2));
sb.Append('╯');
}
if (Help != null)
{ {
sb.AppendLine(); sb.AppendLine();
sb.Append(ConsoleColors.Colorize($"help: {Help}", ConsoleColors.Cyan)); sb.Append(ConsoleColors.Colorize($"help: {Help}", ConsoleColors.Cyan));
@@ -58,17 +156,6 @@ public class Diagnostic
return sb.ToString(); return sb.ToString();
} }
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")
};
}
} }
public enum DiagnosticSeverity public enum DiagnosticSeverity

View File

@@ -1,13 +1,18 @@
namespace NubLang.Tokenization; using NubLang.Code;
public abstract class Token; namespace NubLang.Tokenization;
public class IdentifierToken(string value) : Token public abstract class Token(SourceFileSpan? fileSpan)
{
public SourceFileSpan? FileSpan { get; } = fileSpan;
}
public class IdentifierToken(SourceFileSpan? fileSpan, string value) : Token(fileSpan)
{ {
public string Value { get; } = value; public string Value { get; } = value;
} }
public class LiteralToken(LiteralKind kind, string value) : Token public class LiteralToken(SourceFileSpan? fileSpan, LiteralKind kind, string value) : Token(fileSpan)
{ {
public LiteralKind Kind { get; } = kind; public LiteralKind Kind { get; } = kind;
public string Value { get; } = value; public string Value { get; } = value;
@@ -21,7 +26,7 @@ public enum LiteralKind
Bool Bool
} }
public class SymbolToken(Symbol symbol) : Token public class SymbolToken(SourceFileSpan? fileSpan, Symbol symbol) : Token(fileSpan)
{ {
public Symbol Symbol { get; } = symbol; public Symbol Symbol { get; } = symbol;
} }

View File

@@ -1,4 +1,7 @@
namespace NubLang.Tokenization; using NubLang.Code;
using NubLang.Diagnostics;
namespace NubLang.Tokenization;
public sealed class Tokenizer public sealed class Tokenizer
{ {
@@ -55,6 +58,8 @@ public sealed class Tokenizer
.ToArray(); .ToArray();
private readonly string _sourceText; private readonly string _sourceText;
private readonly SourceFile? _sourceFile;
private readonly List<Diagnostic> _diagnostics = [];
private int _index; private int _index;
public Tokenizer(string sourceText) public Tokenizer(string sourceText)
@@ -62,6 +67,14 @@ public sealed class Tokenizer
_sourceText = sourceText; _sourceText = sourceText;
} }
public Tokenizer(SourceFile sourceFile)
{
_sourceFile = sourceFile;
_sourceText = sourceFile.GetText();
}
public IReadOnlyList<Diagnostic> GetDiagnostics() => _diagnostics;
public IEnumerable<Token> Tokenize() public IEnumerable<Token> Tokenize()
{ {
_index = 0; _index = 0;
@@ -84,6 +97,8 @@ public sealed class Tokenizer
continue; continue;
} }
var tokenStartIndex = _index;
if (char.IsLetter(current) || current == '_') if (char.IsLetter(current) || current == '_')
{ {
var buffer = string.Empty; var buffer = string.Empty;
@@ -96,17 +111,17 @@ public sealed class Tokenizer
if (Keywords.TryGetValue(buffer, out var keywordSymbol)) if (Keywords.TryGetValue(buffer, out var keywordSymbol))
{ {
yield return new SymbolToken(keywordSymbol); yield return new SymbolToken(GetSourceFileSpan(tokenStartIndex), keywordSymbol);
continue; continue;
} }
if (buffer is "true" or "false") if (buffer is "true" or "false")
{ {
yield return new LiteralToken(LiteralKind.Bool, buffer); yield return new LiteralToken(GetSourceFileSpan(tokenStartIndex), LiteralKind.Bool, buffer);
continue; continue;
} }
yield return new IdentifierToken(buffer); yield return new IdentifierToken(GetSourceFileSpan(tokenStartIndex), buffer);
continue; continue;
} }
@@ -139,7 +154,7 @@ public sealed class Tokenizer
} }
} }
yield return new LiteralToken(isFloat ? LiteralKind.Float : LiteralKind.Integer, buffer); yield return new LiteralToken(GetSourceFileSpan(tokenStartIndex), isFloat ? LiteralKind.Float : LiteralKind.Integer, buffer);
continue; continue;
} }
@@ -165,7 +180,7 @@ public sealed class Tokenizer
Next(); Next();
} }
yield return new LiteralToken(LiteralKind.String, buffer); yield return new LiteralToken(GetSourceFileSpan(tokenStartIndex), LiteralKind.String, buffer);
continue; continue;
} }
@@ -184,7 +199,7 @@ public sealed class Tokenizer
Next(); Next();
} }
yield return new SymbolToken(symbol); yield return new SymbolToken(GetSourceFileSpan(tokenStartIndex), symbol);
foundMatch = true; foundMatch = true;
break; break;
} }
@@ -201,7 +216,8 @@ public sealed class Tokenizer
continue; continue;
} }
throw new Exception($"Unknown character {current}"); _diagnostics.Add(Diagnostic.Error($"Unknown token '{current}'").At(GetSourceFileSpan(tokenStartIndex)).Build());
Next();
} }
} }
@@ -219,4 +235,37 @@ public sealed class Tokenizer
{ {
_index++; _index++;
} }
private SourceFileSpan? GetSourceFileSpan(int tokenStartIndex)
{
if (_sourceFile != null)
{
var start = CalculateSourceLocation(tokenStartIndex);
var end = CalculateSourceLocation(_index + 1);
return new SourceFileSpan(_sourceFile, new SourceSpan(start, end));
}
return null;
}
private SourceLocation CalculateSourceLocation(int index)
{
var line = 1;
var column = 1;
for (var i = 0; i < index && i < _sourceText.Length; i++)
{
if (_sourceText[i] == '\n')
{
line++;
column = 1;
}
else
{
column++;
}
}
return new SourceLocation(line, column);
}
} }