...
This commit is contained in:
10
build.sh
10
build.sh
@@ -3,19 +3,15 @@ set -e
|
|||||||
|
|
||||||
mkdir -p out
|
mkdir -p out
|
||||||
|
|
||||||
echo "setup..."
|
|
||||||
|
|
||||||
dotnet publish -c Release src/compiler/Nub.Lang
|
dotnet publish -c Release src/compiler/Nub.Lang
|
||||||
|
|
||||||
echo "compiling..."
|
clear
|
||||||
|
|
||||||
nub example out/out.qbe
|
nub example out/out.qbe
|
||||||
|
|
||||||
nasm -g -felf64 src/runtime/runtime.asm -o out/runtime.o
|
nasm -g -felf64 src/runtime/runtime.asm -o out/runtime.o
|
||||||
|
|
||||||
qbe out/out.qbe > out/out.s
|
qbe out/out.qbe > out/out.s
|
||||||
|
|
||||||
gcc -c -g out/out.s -o out/out.o
|
gcc -c -g out/out.s -o out/out.o
|
||||||
|
gcc -nostartfiles -o out/program out/runtime.o out/out.o
|
||||||
gcc -nostartfiles -o out/program out/runtime.o out/out.o
|
|
||||||
|
|
||||||
echo "done..."
|
|
||||||
@@ -4,7 +4,7 @@ import c
|
|||||||
// Test2
|
// Test2
|
||||||
// Test3
|
// Test3
|
||||||
// Test4
|
// Test4
|
||||||
global func main(args: []string) {
|
global func main(args: f []string) {
|
||||||
i = 0
|
i = 0
|
||||||
printf("%d\n", args.count)
|
printf("%d\n", args.count)
|
||||||
while i < args.count {
|
while i < args.count {
|
||||||
|
|||||||
2
run.sh
2
run.sh
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
./clean.sh
|
./clean.sh
|
||||||
|
clear
|
||||||
./build.sh
|
./build.sh
|
||||||
./out/program
|
./out/program
|
||||||
echo "Process exited with status code $?"
|
echo "Process exited with status code $?"
|
||||||
|
|||||||
27
src/compiler/Nub.Lang/Frontend/Diagnostics/ConsoleColors.cs
Normal file
27
src/compiler/Nub.Lang/Frontend/Diagnostics/ConsoleColors.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
public static class ConsoleColors
|
||||||
|
{
|
||||||
|
public const string Reset = "\e[0m";
|
||||||
|
public const string Bold = "\e[1m";
|
||||||
|
|
||||||
|
public const string Red = "\e[31m";
|
||||||
|
public const string Yellow = "\e[33m";
|
||||||
|
public const string Blue = "\e[34m";
|
||||||
|
public const string Cyan = "\e[36m";
|
||||||
|
public const string White = "\e[37m";
|
||||||
|
public const string BrightWhite = "\e[97m";
|
||||||
|
public const string Gray = "\e[90m";
|
||||||
|
|
||||||
|
public static bool IsColorSupported()
|
||||||
|
{
|
||||||
|
var term = Environment.GetEnvironmentVariable("TERM");
|
||||||
|
var colorTerm = Environment.GetEnvironmentVariable("COLORTERM");
|
||||||
|
return !string.IsNullOrEmpty(term) || !string.IsNullOrEmpty(colorTerm) || !Console.IsOutputRedirected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Colorize(string text, string color)
|
||||||
|
{
|
||||||
|
return IsColorSupported() ? $"{color}{text}{Reset}" : text;
|
||||||
|
}
|
||||||
|
}
|
||||||
205
src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs
Normal file
205
src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Nub.Lang.Frontend.Lexing;
|
||||||
|
using Nub.Lang.Frontend.Parsing;
|
||||||
|
|
||||||
|
namespace Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
public enum DiagnosticSeverity
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Diagnostic
|
||||||
|
{
|
||||||
|
public class DiagnosticBuilder
|
||||||
|
{
|
||||||
|
private readonly DiagnosticSeverity _severity;
|
||||||
|
private readonly string _message;
|
||||||
|
private SourceFile? _sourceFile;
|
||||||
|
private SourceSpan? _span;
|
||||||
|
private string? _help;
|
||||||
|
|
||||||
|
public DiagnosticBuilder(DiagnosticSeverity severity, string message)
|
||||||
|
{
|
||||||
|
_severity = severity;
|
||||||
|
_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[0].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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DiagnosticBuilder Error(string message) => new(DiagnosticSeverity.Error, message);
|
||||||
|
public static DiagnosticBuilder Warning(string message) => new(DiagnosticSeverity.Warning, message);
|
||||||
|
public static DiagnosticBuilder Info(string message) => new(DiagnosticSeverity.Info, message);
|
||||||
|
|
||||||
|
public DiagnosticSeverity Severity { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
public SourceFile? SourceFile { get; }
|
||||||
|
public SourceSpan? Span { get; }
|
||||||
|
public string? Help { get; }
|
||||||
|
|
||||||
|
private Diagnostic(DiagnosticSeverity severity, string message, SourceFile? sourceFile, SourceSpan? span, string? help)
|
||||||
|
{
|
||||||
|
Severity = severity;
|
||||||
|
Message = message;
|
||||||
|
SourceFile = sourceFile;
|
||||||
|
Span = span;
|
||||||
|
Help = help;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Format()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
var severityText = 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),
|
||||||
|
_ => "diagnostic"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
sb.Append(severityText);
|
||||||
|
if (SourceFile.HasValue)
|
||||||
|
{
|
||||||
|
var locationText = $" at {SourceFile.Value.Path}:{Span}";
|
||||||
|
sb.Append(ConsoleColors.Colorize(locationText, ConsoleColors.Gray));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(": ");
|
||||||
|
sb.AppendLine(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite));
|
||||||
|
|
||||||
|
if (SourceFile.HasValue && Span.HasValue)
|
||||||
|
{
|
||||||
|
AppendSourceContext(sb, SourceFile.Value, Span.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Help))
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
var helpText = $"help: {Help}";
|
||||||
|
sb.AppendLine(ConsoleColors.Colorize(helpText, ConsoleColors.Cyan));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendSourceContext(StringBuilder sb, SourceFile sourceFile, SourceSpan span)
|
||||||
|
{
|
||||||
|
var lines = sourceFile.Content.Split('\n');
|
||||||
|
var startLine = span.Start.Line;
|
||||||
|
var endLine = span.End.Line;
|
||||||
|
|
||||||
|
const int CONTEXT_LINES = 3;
|
||||||
|
|
||||||
|
var maxLineNum = Math.Min(endLine + CONTEXT_LINES, lines.Length);
|
||||||
|
var lineNumWidth = maxLineNum.ToString().Length;
|
||||||
|
|
||||||
|
var contextStart = Math.Max(1, startLine - CONTEXT_LINES);
|
||||||
|
for (var lineNum = contextStart; lineNum < startLine; lineNum++)
|
||||||
|
{
|
||||||
|
if (lineNum <= lines.Length)
|
||||||
|
{
|
||||||
|
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var lineNum = startLine; lineNum <= endLine && lineNum <= lines.Length; lineNum++)
|
||||||
|
{
|
||||||
|
var line = lines[lineNum - 1];
|
||||||
|
AppendContextLine(sb, lineNum, line, lineNumWidth);
|
||||||
|
AppendErrorIndicators(sb, span, lineNum, line, lineNumWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextEnd = Math.Min(lines.Length, endLine + CONTEXT_LINES);
|
||||||
|
for (var lineNum = endLine + 1; lineNum <= contextEnd; lineNum++)
|
||||||
|
{
|
||||||
|
if (lineNum <= lines.Length)
|
||||||
|
{
|
||||||
|
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendContextLine(StringBuilder sb, int lineNum, string line, int lineNumWidth)
|
||||||
|
{
|
||||||
|
var lineNumStr = lineNum.ToString().PadLeft(lineNumWidth);
|
||||||
|
|
||||||
|
sb.Append(ConsoleColors.Colorize(lineNumStr, ConsoleColors.Gray));
|
||||||
|
sb.Append(ConsoleColors.Colorize(" | ", ConsoleColors.Gray));
|
||||||
|
sb.AppendLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendErrorIndicators(StringBuilder sb, SourceSpan span, int lineNum, string line, int lineNumWidth)
|
||||||
|
{
|
||||||
|
sb.Append(new string(' ', lineNumWidth + 3));
|
||||||
|
|
||||||
|
const char ERROR_INDICATOR = '^';
|
||||||
|
|
||||||
|
string indicators;
|
||||||
|
|
||||||
|
if (lineNum == span.Start.Line && lineNum == span.End.Line)
|
||||||
|
{
|
||||||
|
var startCol = Math.Max(0, span.Start.Column - 1);
|
||||||
|
var endCol = Math.Min(line.Length, span.End.Column - 1);
|
||||||
|
var length = Math.Max(1, endCol - startCol);
|
||||||
|
|
||||||
|
var spaces = new string(' ', startCol);
|
||||||
|
var carets = new string(ERROR_INDICATOR, length);
|
||||||
|
indicators = spaces + carets;
|
||||||
|
}
|
||||||
|
else if (lineNum == span.Start.Line)
|
||||||
|
{
|
||||||
|
var startCol = Math.Max(0, span.Start.Column - 1);
|
||||||
|
var spaces = new string(' ', startCol);
|
||||||
|
var carets = new string(ERROR_INDICATOR, line.Length - startCol);
|
||||||
|
indicators = spaces + carets;
|
||||||
|
}
|
||||||
|
else if (lineNum == span.End.Line)
|
||||||
|
{
|
||||||
|
var endCol = Math.Min(line.Length, span.End.Column - 1);
|
||||||
|
indicators = new string(ERROR_INDICATOR, endCol);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indicators = new string(ERROR_INDICATOR, line.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine(ConsoleColors.Colorize(indicators, ConsoleColors.Red));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
79
src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs
Normal file
79
src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using Nub.Lang.Frontend.Lexing;
|
||||||
|
using Nub.Lang.Frontend.Parsing;
|
||||||
|
|
||||||
|
namespace Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
public readonly struct SourceFile(string path, string content)
|
||||||
|
{
|
||||||
|
public string Path { get; } = path;
|
||||||
|
public string Content { get; } = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct SourceSpan(SourceLocation start, SourceLocation end)
|
||||||
|
{
|
||||||
|
public SourceLocation Start { get; } = start;
|
||||||
|
public SourceLocation End { get; } = end;
|
||||||
|
|
||||||
|
public override string ToString() => $"{Start}-{End}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SourceLocationCalculator
|
||||||
|
{
|
||||||
|
private static SourceLocation IndexToLocation(string content, int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index > content.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
var line = 1;
|
||||||
|
var column = 1;
|
||||||
|
|
||||||
|
for (var i = 0; i < index && i < content.Length; i++)
|
||||||
|
{
|
||||||
|
if (content[i] == '\n')
|
||||||
|
{
|
||||||
|
line++;
|
||||||
|
column = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
column++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SourceLocation(line, column, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SourceSpan GetSpan(Node node)
|
||||||
|
{
|
||||||
|
if (!node.Tokens.Any())
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Node has no tokens");
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstToken = node.Tokens[0];
|
||||||
|
var lastToken = node.Tokens[^1];
|
||||||
|
|
||||||
|
var start = IndexToLocation(firstToken.SourceFile.Content, firstToken.StartIndex);
|
||||||
|
var end = IndexToLocation(lastToken.SourceFile.Content, lastToken.EndIndex);
|
||||||
|
|
||||||
|
return new SourceSpan(start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
namespace Nub.Lang.Frontend.Lexing;
|
namespace Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
public class DocumentationToken(SourceFile sourceFile, int startIndex, int endIndex, string documentation) : Token(sourceFile, startIndex, endIndex)
|
public class DocumentationToken(SourceFile sourceFile, int startIndex, int endIndex, string documentation) : Token(sourceFile, startIndex, endIndex)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Nub.Lang.Frontend.Lexing;
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
public class IdentifierToken(SourceFile sourceFile, int startIndex, int endIndex, string value) : Token(sourceFile, startIndex, endIndex)
|
public class IdentifierToken(SourceFile sourceFile, int startIndex, int endIndex, string value) : Token(sourceFile, startIndex, endIndex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Nub.Lang.Frontend.Lexing;
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
public class Lexer
|
public class Lexer
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Nub.Lang.Frontend.Lexing;
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
public class LiteralToken(SourceFile sourceFile, int startIndex, int endIndex, NubType type, string value) : Token(sourceFile, startIndex, endIndex)
|
public class LiteralToken(SourceFile sourceFile, int startIndex, int endIndex, NubType type, string value) : Token(sourceFile, startIndex, endIndex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
namespace Nub.Lang.Frontend.Lexing;
|
namespace Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
public class ModifierToken(SourceFile sourceFile, int startIndex, int endIndex, Modifier modifier) : Token(sourceFile, startIndex, endIndex)
|
public class ModifierToken(SourceFile sourceFile, int startIndex, int endIndex, Modifier modifier) : Token(sourceFile, startIndex, endIndex)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Nub.Lang.Frontend.Lexing;
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
public class SymbolToken(SourceFile sourceFile, int startIndex, int endIndex, Symbol symbol) : Token(sourceFile, startIndex, endIndex)
|
public class SymbolToken(SourceFile sourceFile, int startIndex, int endIndex, Symbol symbol) : Token(sourceFile, startIndex, endIndex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Nub.Lang.Frontend.Lexing;
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
public abstract class Token(SourceFile sourceFile, int startIndex, int endIndex)
|
public abstract class Token(SourceFile sourceFile, int startIndex, int endIndex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
using Nub.Lang.Frontend.Lexing;
|
using Nub.Lang.Frontend.Lexing;
|
||||||
|
|
||||||
namespace Nub.Lang.Frontend.Parsing;
|
namespace Nub.Lang.Frontend.Parsing;
|
||||||
@@ -7,25 +8,37 @@ public class Parser
|
|||||||
{
|
{
|
||||||
private List<Token> _tokens = [];
|
private List<Token> _tokens = [];
|
||||||
private int _index;
|
private int _index;
|
||||||
|
private List<Diagnostic> _diagnostics = [];
|
||||||
|
|
||||||
|
public IReadOnlyList<Diagnostic> Diagnostics => _diagnostics;
|
||||||
|
|
||||||
public ModuleNode ParseModule(List<Token> tokens, string rootFilePath)
|
public ModuleNode ParseModule(List<Token> tokens, string rootFilePath)
|
||||||
{
|
{
|
||||||
_index = 0;
|
_index = 0;
|
||||||
_tokens = tokens;
|
_tokens = tokens;
|
||||||
|
_diagnostics = [];
|
||||||
|
|
||||||
List<DefinitionNode> definitions = [];
|
List<DefinitionNode> definitions = [];
|
||||||
List<string> imports = [];
|
List<string> imports = [];
|
||||||
|
|
||||||
while (Peek().HasValue)
|
while (Peek().HasValue)
|
||||||
{
|
{
|
||||||
if (TryExpectSymbol(Symbol.Import))
|
try
|
||||||
{
|
{
|
||||||
var name = ExpectIdentifier();
|
if (TryExpectSymbol(Symbol.Import))
|
||||||
imports.Add(name.Value);
|
{
|
||||||
|
var name = ExpectIdentifier();
|
||||||
|
imports.Add(name.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
definitions.Add(ParseDefinition());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
catch (ParseException ex)
|
||||||
{
|
{
|
||||||
definitions.Add(ParseDefinition());
|
_diagnostics.Add(ex.Diagnostic);
|
||||||
|
RecoverToNextDefinition();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +48,7 @@ public class Parser
|
|||||||
private DefinitionNode ParseDefinition()
|
private DefinitionNode ParseDefinition()
|
||||||
{
|
{
|
||||||
var startIndex = _index;
|
var startIndex = _index;
|
||||||
List<Modifier> modifiers = [];
|
List<ModifierToken> modifiers = [];
|
||||||
|
|
||||||
List<string> documentationParts = [];
|
List<string> documentationParts = [];
|
||||||
while (_index < _tokens.Count && _tokens[_index] is DocumentationToken commentToken)
|
while (_index < _tokens.Count && _tokens[_index] is DocumentationToken commentToken)
|
||||||
@@ -56,21 +69,35 @@ public class Parser
|
|||||||
{
|
{
|
||||||
Symbol.Func => ParseFuncDefinition(startIndex, modifiers, Optional.OfNullable(documentation)),
|
Symbol.Func => ParseFuncDefinition(startIndex, modifiers, Optional.OfNullable(documentation)),
|
||||||
Symbol.Struct => ParseStruct(startIndex, modifiers, Optional.OfNullable(documentation)),
|
Symbol.Struct => ParseStruct(startIndex, modifiers, Optional.OfNullable(documentation)),
|
||||||
_ => throw new Exception("Unexpected symbol: " + keyword.Symbol)
|
_ => throw new ParseException(Diagnostic
|
||||||
|
.Error($"Expected 'func' or 'struct', but found '{keyword.Symbol}'")
|
||||||
|
.WithHelp("Valid definition keywords are 'func' and 'struct'")
|
||||||
|
.At(keyword)
|
||||||
|
.Build())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private DefinitionNode ParseFuncDefinition(int startIndex, List<Modifier> modifiers, Optional<string> documentation)
|
private DefinitionNode ParseFuncDefinition(int startIndex, List<ModifierToken> modifiers, Optional<string> documentation)
|
||||||
{
|
{
|
||||||
var name = ExpectIdentifier();
|
var name = ExpectIdentifier();
|
||||||
List<FuncParameter> parameters = [];
|
List<FuncParameter> parameters = [];
|
||||||
|
|
||||||
ExpectSymbol(Symbol.OpenParen);
|
ExpectSymbol(Symbol.OpenParen);
|
||||||
|
|
||||||
if (!TryExpectSymbol(Symbol.CloseParen))
|
if (!TryExpectSymbol(Symbol.CloseParen))
|
||||||
{
|
{
|
||||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||||
{
|
{
|
||||||
parameters.Add(ParseFuncParameter());
|
parameters.Add(ParseFuncParameter());
|
||||||
TryExpectSymbol(Symbol.Comma);
|
|
||||||
|
if (!TryExpectSymbol(Symbol.Comma) && Peek().TryGetValue(out var token) && token is not SymbolToken { Symbol: Symbol.CloseParen })
|
||||||
|
{
|
||||||
|
_diagnostics.Add(Diagnostic
|
||||||
|
.Warning("Missing comma between function parameters")
|
||||||
|
.WithHelp("Add a ',' to separate parameters")
|
||||||
|
.At(token)
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,28 +107,37 @@ public class Parser
|
|||||||
returnType = ParseType();
|
returnType = ParseType();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modifiers.Remove(Modifier.Extern))
|
var isExtern = modifiers.RemoveAll(x => x.Modifier == Modifier.Extern) > 0;
|
||||||
|
if (isExtern)
|
||||||
{
|
{
|
||||||
if (modifiers.Count != 0)
|
if (modifiers.Count != 0)
|
||||||
{
|
{
|
||||||
throw new Exception($"Modifiers: {string.Join(", ", modifiers)} is not valid for an extern function");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Invalid modifier for extern function: {modifiers[0].Modifier}")
|
||||||
|
.WithHelp($"Extern functions cannot use the '{modifiers[0].Modifier}' modifier")
|
||||||
|
.At(modifiers[0])
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ExternFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, parameters, returnType);
|
return new ExternFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, parameters, returnType);
|
||||||
}
|
}
|
||||||
|
|
||||||
var body = ParseBlock();
|
var body = ParseBlock();
|
||||||
var global = modifiers.Remove(Modifier.Global);
|
var isGlobal = modifiers.RemoveAll(x => x.Modifier == Modifier.Global) > 0;
|
||||||
|
|
||||||
if (modifiers.Count != 0)
|
if (modifiers.Count != 0)
|
||||||
{
|
{
|
||||||
throw new Exception($"Modifiers: {string.Join(", ", modifiers)} is not valid for a local function");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Invalid modifiers for function: {modifiers[0].Modifier}")
|
||||||
|
.WithHelp($"Functions cannot use the '{modifiers[0].Modifier}' modifier")
|
||||||
|
.At(modifiers[0])
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LocalFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, parameters, body, returnType, global);
|
return new LocalFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, parameters, body, returnType, isGlobal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private StructDefinitionNode ParseStruct(int startIndex, List<Modifier> _, Optional<string> documentation)
|
private StructDefinitionNode ParseStruct(int startIndex, List<ModifierToken> _, Optional<string> documentation)
|
||||||
{
|
{
|
||||||
var name = ExpectIdentifier().Value;
|
var name = ExpectIdentifier().Value;
|
||||||
|
|
||||||
@@ -181,7 +217,11 @@ public class Parser
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
throw new Exception($"Unexpected symbol {symbol.Symbol}");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Unexpected symbol '{symbol.Symbol}' after identifier")
|
||||||
|
.WithHelp("Expected '(', '=', or ':' after identifier")
|
||||||
|
.At(symbol)
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,12 +234,20 @@ public class Parser
|
|||||||
Symbol.While => ParseWhile(startIndex),
|
Symbol.While => ParseWhile(startIndex),
|
||||||
Symbol.Break => new BreakNode(GetTokensForNode(startIndex)),
|
Symbol.Break => new BreakNode(GetTokensForNode(startIndex)),
|
||||||
Symbol.Continue => new ContinueNode(GetTokensForNode(startIndex)),
|
Symbol.Continue => new ContinueNode(GetTokensForNode(startIndex)),
|
||||||
_ => throw new Exception($"Unexpected symbol {symbol.Symbol}")
|
_ => throw new ParseException(Diagnostic
|
||||||
|
.Error($"Unexpected symbol '{symbol.Symbol}' at start of statement")
|
||||||
|
.WithHelp("Expected identifier, 'return', 'if', 'while', 'break', or 'continue'")
|
||||||
|
.At(symbol)
|
||||||
|
.Build())
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
throw new Exception($"Unexpected token type {token.GetType().Name}");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Unexpected token '{token.GetType().Name}' at start of statement")
|
||||||
|
.WithHelp("Statements must start with an identifier or keyword")
|
||||||
|
.At(token)
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +297,9 @@ public class Parser
|
|||||||
var token = Peek();
|
var token = Peek();
|
||||||
if (!token.HasValue || token.Value is not SymbolToken symbolToken || !TryGetBinaryOperator(symbolToken.Symbol, out var op) ||
|
if (!token.HasValue || token.Value is not SymbolToken symbolToken || !TryGetBinaryOperator(symbolToken.Symbol, out var op) ||
|
||||||
GetBinaryOperatorPrecedence(op.Value) < precedence)
|
GetBinaryOperatorPrecedence(op.Value) < precedence)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
Next();
|
Next();
|
||||||
var right = ParseExpression(GetBinaryOperatorPrecedence(op.Value) + 1);
|
var right = ParseExpression(GetBinaryOperatorPrecedence(op.Value) + 1);
|
||||||
@@ -343,7 +393,14 @@ public class Parser
|
|||||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||||
{
|
{
|
||||||
parameters.Add(ParseExpression());
|
parameters.Add(ParseExpression());
|
||||||
TryExpectSymbol(Symbol.Comma);
|
if (!TryExpectSymbol(Symbol.Comma) && Peek().TryGetValue(out var nextToken) && nextToken is not SymbolToken { Symbol: Symbol.CloseParen })
|
||||||
|
{
|
||||||
|
_diagnostics.Add(Diagnostic
|
||||||
|
.Warning("Missing comma between function arguments")
|
||||||
|
.WithHelp("Add a ',' to separate arguments")
|
||||||
|
.At(nextToken)
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expr = new FuncCallExpressionNode(GetTokensForNode(startIndex), new FuncCall(identifier.Value, parameters));
|
expr = new FuncCallExpressionNode(GetTokensForNode(startIndex), new FuncCall(identifier.Value, parameters));
|
||||||
@@ -415,7 +472,11 @@ public class Parser
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
throw new Exception($"Unknown symbol: {symbolToken.Symbol}");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Unexpected symbol '{symbolToken.Symbol}' in expression")
|
||||||
|
.WithHelp("Expected literal, identifier, or '(' to start expression")
|
||||||
|
.At(symbolToken)
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,14 +484,18 @@ public class Parser
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
throw new Exception($"Unexpected token type {token.GetType().Name}");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Unexpected token '{token.GetType().Name}' in expression")
|
||||||
|
.WithHelp("Expected literal, identifier, or parenthesized expression")
|
||||||
|
.At(token)
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParsePostfixOperators(startIndex, expr);
|
return ParsePostfixOperators(startIndex, expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExpressionNode ParsePostfixOperators(int startIndex, ExpressionNode expr)
|
private ExpressionNode ParsePostfixOperators(int startIndex, ExpressionNode expr)
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -468,7 +533,15 @@ public class Parser
|
|||||||
List<StatementNode> statements = [];
|
List<StatementNode> statements = [];
|
||||||
while (!TryExpectSymbol(Symbol.CloseBrace))
|
while (!TryExpectSymbol(Symbol.CloseBrace))
|
||||||
{
|
{
|
||||||
statements.Add(ParseStatement());
|
try
|
||||||
|
{
|
||||||
|
statements.Add(ParseStatement());
|
||||||
|
}
|
||||||
|
catch (ParseException ex)
|
||||||
|
{
|
||||||
|
_diagnostics.Add(ex.Diagnostic);
|
||||||
|
RecoverToNextStatement();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BlockNode(GetTokensForNode(startIndex), statements);
|
return new BlockNode(GetTokensForNode(startIndex), statements);
|
||||||
@@ -494,19 +567,33 @@ public class Parser
|
|||||||
return new NubArrayType(baseType);
|
return new NubArrayType(baseType);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Exception($"Unexpected token {Peek()} when parsing type");
|
if (!Peek().TryGetValue(out var token))
|
||||||
|
{
|
||||||
|
throw new ParseException(Diagnostic.Error("Unexpected end of file while parsing type")
|
||||||
|
.At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last()))
|
||||||
|
.WithHelp("Expected a type name")
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ParseException(Diagnostic
|
||||||
|
.Error("Invalid type syntax")
|
||||||
|
.WithHelp("Expected type name, '^' for pointer, or '[]' for array")
|
||||||
|
.At(token)
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Token ExpectToken()
|
private Token ExpectToken()
|
||||||
{
|
{
|
||||||
var token = Peek();
|
if (!Peek().TryGetValue(out var token))
|
||||||
if (!token.HasValue)
|
|
||||||
{
|
{
|
||||||
throw new Exception("Reached end of tokens");
|
throw new ParseException(Diagnostic.Error("Unexpected end of file")
|
||||||
|
.At(_tokens.Last().SourceFile, SourceLocationCalculator.GetSpan(_tokens.Last()))
|
||||||
|
.WithHelp("Expected more tokens to complete the syntax")
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
Next();
|
Next();
|
||||||
return token.Value;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SymbolToken ExpectSymbol()
|
private SymbolToken ExpectSymbol()
|
||||||
@@ -514,44 +601,56 @@ public class Parser
|
|||||||
var token = ExpectToken();
|
var token = ExpectToken();
|
||||||
if (token is not SymbolToken symbol)
|
if (token is not SymbolToken symbol)
|
||||||
{
|
{
|
||||||
throw new Exception($"Expected {nameof(SymbolToken)} but got {token.GetType().Name}");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Expected symbol, but found {token.GetType().Name}")
|
||||||
|
.WithHelp("This position requires a symbol like '(', ')', '{', '}', etc.")
|
||||||
|
.At(token)
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
return symbol;
|
return symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExpectSymbol(Symbol symbol)
|
private void ExpectSymbol(Symbol expectedSymbol)
|
||||||
{
|
{
|
||||||
var token = ExpectSymbol();
|
var token = ExpectSymbol();
|
||||||
if (token.Symbol != symbol)
|
if (token.Symbol != expectedSymbol)
|
||||||
{
|
{
|
||||||
throw new Exception($"Expected symbol {symbol} but got {token.Symbol}");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Expected '{expectedSymbol}', but found '{token.Symbol}'")
|
||||||
|
.WithHelp($"Insert '{expectedSymbol}' here")
|
||||||
|
.At(token)
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryExpectSymbol(Symbol symbol)
|
private bool TryExpectSymbol(Symbol symbol)
|
||||||
{
|
{
|
||||||
var result = Peek() is { HasValue: true, Value: SymbolToken symbolToken } && symbolToken.Symbol == symbol;
|
if (Peek() is { Value: SymbolToken symbolToken } && symbolToken.Symbol == symbol)
|
||||||
if (result) Next();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryExpectModifier(out Modifier modifier)
|
|
||||||
{
|
|
||||||
if (Peek() is { HasValue: true, Value: ModifierToken modifierToken })
|
|
||||||
{
|
{
|
||||||
modifier = modifierToken.Modifier;
|
|
||||||
Next();
|
Next();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifier = default;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryExpectModifier([NotNullWhen(true)] out ModifierToken? modifier)
|
||||||
|
{
|
||||||
|
if (Peek() is { Value: ModifierToken modifierToken })
|
||||||
|
{
|
||||||
|
modifier = modifierToken;
|
||||||
|
Next();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryExpectIdentifier([NotNullWhen(true)] out string? identifier)
|
private bool TryExpectIdentifier([NotNullWhen(true)] out string? identifier)
|
||||||
{
|
{
|
||||||
if (Peek() is { HasValue: true, Value: IdentifierToken identifierToken })
|
if (Peek() is { Value: IdentifierToken identifierToken })
|
||||||
{
|
{
|
||||||
identifier = identifierToken.Value;
|
identifier = identifierToken.Value;
|
||||||
Next();
|
Next();
|
||||||
@@ -567,12 +666,45 @@ public class Parser
|
|||||||
var token = ExpectToken();
|
var token = ExpectToken();
|
||||||
if (token is not IdentifierToken identifier)
|
if (token is not IdentifierToken identifier)
|
||||||
{
|
{
|
||||||
throw new Exception($"Expected {nameof(IdentifierToken)} but got {token.GetType().Name}");
|
throw new ParseException(Diagnostic
|
||||||
|
.Error($"Expected identifier, but found {token.GetType().Name}")
|
||||||
|
.WithHelp("Provide a valid identifier name here")
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
return identifier;
|
return identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RecoverToNextDefinition()
|
||||||
|
{
|
||||||
|
while (Peek().HasValue)
|
||||||
|
{
|
||||||
|
var token = Peek().Value;
|
||||||
|
if (token is SymbolToken { Symbol: Symbol.Func or Symbol.Struct })
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecoverToNextStatement()
|
||||||
|
{
|
||||||
|
while (Peek().TryGetValue(out var token))
|
||||||
|
{
|
||||||
|
if (token is SymbolToken { Symbol: Symbol.CloseBrace } or IdentifierToken or SymbolToken
|
||||||
|
{
|
||||||
|
Symbol: Symbol.Return or Symbol.If or Symbol.While or Symbol.Break or Symbol.Continue
|
||||||
|
})
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<Token> Peek()
|
private Optional<Token> Peek()
|
||||||
{
|
{
|
||||||
var peekIndex = _index;
|
var peekIndex = _index;
|
||||||
@@ -603,4 +735,14 @@ public class Parser
|
|||||||
{
|
{
|
||||||
return _tokens[startIndex..Math.Min(_index, _tokens.Count - 1)];
|
return _tokens[startIndex..Math.Min(_index, _tokens.Count - 1)];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParseException : Exception
|
||||||
|
{
|
||||||
|
public Diagnostic Diagnostic { get; }
|
||||||
|
|
||||||
|
public ParseException(Diagnostic diagnostic) : base(diagnostic.Message)
|
||||||
|
{
|
||||||
|
Diagnostic = diagnostic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Nub.Lang.Backend;
|
using Nub.Lang.Backend;
|
||||||
|
using Nub.Lang.Frontend.Diagnostics;
|
||||||
using Nub.Lang.Frontend.Lexing;
|
using Nub.Lang.Frontend.Lexing;
|
||||||
using Nub.Lang.Frontend.Parsing;
|
using Nub.Lang.Frontend.Parsing;
|
||||||
using Nub.Lang.Frontend.Typing;
|
using Nub.Lang.Frontend.Typing;
|
||||||
@@ -41,7 +42,20 @@ internal static class Program
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var modules = RunFrontend(input);
|
List<Diagnostic> diagnostics = [];
|
||||||
|
|
||||||
|
var modules = RunFrontend(input, diagnostics);
|
||||||
|
|
||||||
|
foreach (var diagnostic in diagnostics)
|
||||||
|
{
|
||||||
|
Console.WriteLine(diagnostic.Format());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
var definitions = modules.SelectMany(f => f.Definitions).ToList();
|
var definitions = modules.SelectMany(f => f.Definitions).ToList();
|
||||||
|
|
||||||
var typeChecker = new TypeChecker(definitions);
|
var typeChecker = new TypeChecker(definitions);
|
||||||
@@ -54,14 +68,14 @@ internal static class Program
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<ModuleNode> RunFrontend(string rootFilePath)
|
private static List<ModuleNode> RunFrontend(string rootFilePath, List<Diagnostic> diagnostics)
|
||||||
{
|
{
|
||||||
List<ModuleNode> modules = [];
|
List<ModuleNode> modules = [];
|
||||||
RunFrontend(rootFilePath, modules);
|
RunFrontend(rootFilePath, modules, diagnostics);
|
||||||
return modules;
|
return modules;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunFrontend(string rootFilePath, List<ModuleNode> modules)
|
private static void RunFrontend(string rootFilePath, List<ModuleNode> modules, List<Diagnostic> diagnostics)
|
||||||
{
|
{
|
||||||
var filePaths = Directory.EnumerateFiles(rootFilePath, "*.nub", SearchOption.TopDirectoryOnly);
|
var filePaths = Directory.EnumerateFiles(rootFilePath, "*.nub", SearchOption.TopDirectoryOnly);
|
||||||
|
|
||||||
@@ -73,6 +87,7 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
var module = Parser.ParseModule(tokens, rootFilePath);
|
var module = Parser.ParseModule(tokens, rootFilePath);
|
||||||
|
diagnostics.AddRange(Parser.Diagnostics);
|
||||||
modules.Add(module);
|
modules.Add(module);
|
||||||
|
|
||||||
foreach (var import in module.Imports)
|
foreach (var import in module.Imports)
|
||||||
@@ -80,7 +95,7 @@ internal static class Program
|
|||||||
var importPath = Path.GetFullPath(import, module.Path);
|
var importPath = Path.GetFullPath(import, module.Path);
|
||||||
if (modules.All(m => m.Path != importPath))
|
if (modules.All(m => m.Path != importPath))
|
||||||
{
|
{
|
||||||
RunFrontend(importPath, modules);
|
RunFrontend(importPath, modules, diagnostics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
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