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