From be144a098931c4edc88db2aada26265762b2f1de Mon Sep 17 00:00:00 2001 From: nub31 Date: Sun, 25 May 2025 01:03:15 +0200 Subject: [PATCH] ... --- build.sh | 10 +- clean.sh | 1 + example/program.nub | 2 +- run.sh | 2 + .../Frontend/Diagnostics/ConsoleColors.cs | 27 ++ .../Frontend/Diagnostics/Diagnostic.cs | 205 ++++++++++ .../Frontend/Diagnostics/Diagnostics.cs | 376 ------------------ .../Frontend/Diagnostics/SourceFile.cs | 79 ++++ .../Frontend/Lexing/DocumentationToken.cs | 2 + .../Frontend/Lexing/IdentifierToken.cs | 4 +- .../Nub.Lang/Frontend/Lexing/Lexer.cs | 4 +- .../Nub.Lang/Frontend/Lexing/LiteralToken.cs | 4 +- .../Nub.Lang/Frontend/Lexing/ModifierToken.cs | 2 + .../Nub.Lang/Frontend/Lexing/SymbolToken.cs | 4 +- .../Nub.Lang/Frontend/Lexing/Token.cs | 4 +- .../Nub.Lang/Frontend/Parsing/Parser.cs | 230 +++++++++-- src/compiler/Nub.Lang/Program.cs | 25 +- src/compiler/Nub.Lang/SourceFile.cs | 7 - 18 files changed, 543 insertions(+), 445 deletions(-) create mode 100644 src/compiler/Nub.Lang/Frontend/Diagnostics/ConsoleColors.cs create mode 100644 src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs delete mode 100644 src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs create mode 100644 src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs delete mode 100644 src/compiler/Nub.Lang/SourceFile.cs diff --git a/build.sh b/build.sh index ec564b4..c4c6a61 100755 --- a/build.sh +++ b/build.sh @@ -3,19 +3,15 @@ set -e mkdir -p out -echo "setup..." - dotnet publish -c Release src/compiler/Nub.Lang -echo "compiling..." +clear nub example out/out.qbe nasm -g -felf64 src/runtime/runtime.asm -o out/runtime.o qbe out/out.qbe > out/out.s + gcc -c -g out/out.s -o out/out.o - -gcc -nostartfiles -o out/program out/runtime.o out/out.o - -echo "done..." +gcc -nostartfiles -o out/program out/runtime.o out/out.o \ No newline at end of file diff --git a/clean.sh b/clean.sh index 552d583..00ca15f 100755 --- a/clean.sh +++ b/clean.sh @@ -1,2 +1,3 @@ #!/bin/bash +set -e rm -rf out diff --git a/example/program.nub b/example/program.nub index d78d772..1a98306 100644 --- a/example/program.nub +++ b/example/program.nub @@ -4,7 +4,7 @@ import c // Test2 // Test3 // Test4 -global func main(args: []string) { +global func main(args: f []string) { i = 0 printf("%d\n", args.count) while i < args.count { diff --git a/run.sh b/run.sh index d6e850f..488dcc3 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e ./clean.sh +clear ./build.sh ./out/program echo "Process exited with status code $?" diff --git a/src/compiler/Nub.Lang/Frontend/Diagnostics/ConsoleColors.cs b/src/compiler/Nub.Lang/Frontend/Diagnostics/ConsoleColors.cs new file mode 100644 index 0000000..2222701 --- /dev/null +++ b/src/compiler/Nub.Lang/Frontend/Diagnostics/ConsoleColors.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs b/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs new file mode 100644 index 0000000..c761e85 --- /dev/null +++ b/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostic.cs @@ -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)); + } +} \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs b/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs deleted file mode 100644 index f545764..0000000 --- a/src/compiler/Nub.Lang/Frontend/Diagnostics/Diagnostics.cs +++ /dev/null @@ -1,376 +0,0 @@ -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/Diagnostics/SourceFile.cs b/src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs new file mode 100644 index 0000000..5b48737 --- /dev/null +++ b/src/compiler/Nub.Lang/Frontend/Diagnostics/SourceFile.cs @@ -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); + } +} diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs index 4fb09f2..d1f60f8 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/DocumentationToken.cs @@ -1,3 +1,5 @@ +using Nub.Lang.Frontend.Diagnostics; + namespace Nub.Lang.Frontend.Lexing; public class DocumentationToken(SourceFile sourceFile, int startIndex, int endIndex, string documentation) : Token(sourceFile, startIndex, endIndex) diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs index 077a802..b84520d 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/IdentifierToken.cs @@ -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) { diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs b/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs index 7439c20..af772d8 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/Lexer.cs @@ -1,4 +1,6 @@ -namespace Nub.Lang.Frontend.Lexing; +using Nub.Lang.Frontend.Diagnostics; + +namespace Nub.Lang.Frontend.Lexing; public class Lexer { diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs index ddd734e..976a53b 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/LiteralToken.cs @@ -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) { diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs index 7927c21..d4b2ca6 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/ModifierToken.cs @@ -1,3 +1,5 @@ +using Nub.Lang.Frontend.Diagnostics; + namespace Nub.Lang.Frontend.Lexing; public class ModifierToken(SourceFile sourceFile, int startIndex, int endIndex, Modifier modifier) : Token(sourceFile, startIndex, endIndex) diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs b/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs index 72a7331..13223c4 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/SymbolToken.cs @@ -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) { diff --git a/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs b/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs index ccf30b4..1bd7b94 100644 --- a/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs +++ b/src/compiler/Nub.Lang/Frontend/Lexing/Token.cs @@ -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) { diff --git a/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs b/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs index 24ecaf7..824e4a8 100644 --- a/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs +++ b/src/compiler/Nub.Lang/Frontend/Parsing/Parser.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Nub.Lang.Frontend.Diagnostics; using Nub.Lang.Frontend.Lexing; namespace Nub.Lang.Frontend.Parsing; @@ -7,25 +8,37 @@ public class Parser { private List _tokens = []; private int _index; + private List _diagnostics = []; + + public IReadOnlyList Diagnostics => _diagnostics; public ModuleNode ParseModule(List tokens, string rootFilePath) { _index = 0; _tokens = tokens; + _diagnostics = []; List definitions = []; List imports = []; while (Peek().HasValue) { - if (TryExpectSymbol(Symbol.Import)) + try { - var name = ExpectIdentifier(); - imports.Add(name.Value); + if (TryExpectSymbol(Symbol.Import)) + { + 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() { var startIndex = _index; - List modifiers = []; + List modifiers = []; List documentationParts = []; 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.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 modifiers, Optional documentation) + private DefinitionNode ParseFuncDefinition(int startIndex, List modifiers, Optional documentation) { var name = ExpectIdentifier(); List parameters = []; + ExpectSymbol(Symbol.OpenParen); + if (!TryExpectSymbol(Symbol.CloseParen)) { while (!TryExpectSymbol(Symbol.CloseParen)) { 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(); } - if (modifiers.Remove(Modifier.Extern)) + var isExtern = modifiers.RemoveAll(x => x.Modifier == Modifier.Extern) > 0; + if (isExtern) { 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); } var body = ParseBlock(); - var global = modifiers.Remove(Modifier.Global); + var isGlobal = modifiers.RemoveAll(x => x.Modifier == Modifier.Global) > 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 _, Optional documentation) + private StructDefinitionNode ParseStruct(int startIndex, List _, Optional documentation) { var name = ExpectIdentifier().Value; @@ -181,7 +217,11 @@ public class Parser } 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.Break => new BreakNode(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: { - 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(); if (!token.HasValue || token.Value is not SymbolToken symbolToken || !TryGetBinaryOperator(symbolToken.Symbol, out var op) || GetBinaryOperatorPrecedence(op.Value) < precedence) + { break; + } Next(); var right = ParseExpression(GetBinaryOperatorPrecedence(op.Value) + 1); @@ -343,7 +393,14 @@ public class Parser while (!TryExpectSymbol(Symbol.CloseParen)) { 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)); @@ -415,7 +472,11 @@ public class Parser } 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: { - 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); } - private ExpressionNode ParsePostfixOperators(int startIndex, ExpressionNode expr) + private ExpressionNode ParsePostfixOperators(int startIndex, ExpressionNode expr) { while (true) { @@ -468,7 +533,15 @@ public class Parser List statements = []; 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); @@ -494,19 +567,33 @@ public class Parser 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() { - var token = Peek(); - if (!token.HasValue) + if (!Peek().TryGetValue(out var token)) { - 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(); - return token.Value; + return token; } private SymbolToken ExpectSymbol() @@ -514,44 +601,56 @@ public class Parser var token = ExpectToken(); 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; } - private void ExpectSymbol(Symbol symbol) + private void ExpectSymbol(Symbol expectedSymbol) { 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) { - var result = Peek() is { HasValue: true, 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 }) + if (Peek() is { Value: SymbolToken symbolToken } && symbolToken.Symbol == symbol) { - modifier = modifierToken.Modifier; Next(); 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; } 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; Next(); @@ -567,12 +666,45 @@ public class Parser var token = ExpectToken(); 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; } + 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 Peek() { var peekIndex = _index; @@ -603,4 +735,14 @@ public class Parser { 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; + } } \ No newline at end of file diff --git a/src/compiler/Nub.Lang/Program.cs b/src/compiler/Nub.Lang/Program.cs index ca697a6..5537887 100644 --- a/src/compiler/Nub.Lang/Program.cs +++ b/src/compiler/Nub.Lang/Program.cs @@ -1,4 +1,5 @@ using Nub.Lang.Backend; +using Nub.Lang.Frontend.Diagnostics; using Nub.Lang.Frontend.Lexing; using Nub.Lang.Frontend.Parsing; using Nub.Lang.Frontend.Typing; @@ -41,7 +42,20 @@ internal static class Program return 1; } - var modules = RunFrontend(input); + List 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 typeChecker = new TypeChecker(definitions); @@ -54,14 +68,14 @@ internal static class Program return 0; } - private static List RunFrontend(string rootFilePath) + private static List RunFrontend(string rootFilePath, List diagnostics) { List modules = []; - RunFrontend(rootFilePath, modules); + RunFrontend(rootFilePath, modules, diagnostics); return modules; } - private static void RunFrontend(string rootFilePath, List modules) + private static void RunFrontend(string rootFilePath, List modules, List diagnostics) { var filePaths = Directory.EnumerateFiles(rootFilePath, "*.nub", SearchOption.TopDirectoryOnly); @@ -73,6 +87,7 @@ internal static class Program } var module = Parser.ParseModule(tokens, rootFilePath); + diagnostics.AddRange(Parser.Diagnostics); modules.Add(module); foreach (var import in module.Imports) @@ -80,7 +95,7 @@ internal static class Program var importPath = Path.GetFullPath(import, module.Path); if (modules.All(m => m.Path != importPath)) { - RunFrontend(importPath, modules); + RunFrontend(importPath, modules, diagnostics); } } } diff --git a/src/compiler/Nub.Lang/SourceFile.cs b/src/compiler/Nub.Lang/SourceFile.cs deleted file mode 100644 index b5b2271..0000000 --- a/src/compiler/Nub.Lang/SourceFile.cs +++ /dev/null @@ -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; -}