restructure
This commit is contained in:
178
src/Nub.Lang/Diagnostics/ConsoleColors.cs
Normal file
178
src/Nub.Lang/Diagnostics/ConsoleColors.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Text;
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Diagnostics;
|
||||
|
||||
public static class ConsoleColors
|
||||
{
|
||||
public const string Reset = "\e[0m";
|
||||
public const string Bold = "\e[1m";
|
||||
public const string Faint = "\e[2m";
|
||||
public const string Italic = "\e[3m";
|
||||
public const string Underline = "\e[4m";
|
||||
public const string SlowBlink = "\e[5m";
|
||||
public const string RapidBlink = "\e[6m";
|
||||
public const string SwapBgAndFg = "\e[7m";
|
||||
public const string Conceal = "\e[8m";
|
||||
public const string CrossedOut = "\e[9m";
|
||||
|
||||
public const string DefaultFont = "\e[10m";
|
||||
public const string AltFont1 = "\e[11m";
|
||||
public const string AltFont2 = "\e[12m";
|
||||
public const string AltFont3 = "\e[13m";
|
||||
public const string AltFont4 = "\e[14m";
|
||||
public const string AltFont5 = "\e[15m";
|
||||
public const string AltFont6 = "\e[16m";
|
||||
public const string AltFont7 = "\e[17m";
|
||||
public const string AltFont8 = "\e[18m";
|
||||
public const string AltFont9 = "\e[19m";
|
||||
|
||||
public const string Black = "\e[30m";
|
||||
public const string Red = "\e[31m";
|
||||
public const string Green = "\e[32m";
|
||||
public const string Yellow = "\e[33m";
|
||||
public const string Blue = "\e[34m";
|
||||
public const string Magenta = "\e[35m";
|
||||
public const string Cyan = "\e[36m";
|
||||
public const string White = "\e[37m";
|
||||
|
||||
public const string BrightBlack = "\e[90m";
|
||||
public const string BrightRed = "\e[91m";
|
||||
public const string BrightGreen = "\e[92m";
|
||||
public const string BrightYellow = "\e[93m";
|
||||
public const string BrightBlue = "\e[94m";
|
||||
public const string BrightMagenta = "\e[95m";
|
||||
public const string BrightCyan = "\e[96m";
|
||||
public const string BrightWhite = "\e[97m";
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private static string GetTokenColor(Token token)
|
||||
{
|
||||
switch (token)
|
||||
{
|
||||
case DocumentationToken:
|
||||
return Faint;
|
||||
case IdentifierToken:
|
||||
return White;
|
||||
case LiteralToken literal:
|
||||
return literal.Kind switch
|
||||
{
|
||||
LiteralKind.String => Green,
|
||||
LiteralKind.Integer or LiteralKind.Float => BrightBlue,
|
||||
LiteralKind.Bool => Blue,
|
||||
_ => White
|
||||
};
|
||||
case ModifierToken:
|
||||
return White;
|
||||
case SymbolToken symbol:
|
||||
switch (symbol.Symbol)
|
||||
{
|
||||
case Symbol.If:
|
||||
case Symbol.Else:
|
||||
case Symbol.While:
|
||||
case Symbol.Break:
|
||||
case Symbol.Continue:
|
||||
case Symbol.Return:
|
||||
return Magenta;
|
||||
case Symbol.Func:
|
||||
case Symbol.Struct:
|
||||
case Symbol.Namespace:
|
||||
case Symbol.Let:
|
||||
case Symbol.Alloc:
|
||||
return Blue;
|
||||
case Symbol.Assign:
|
||||
case Symbol.Bang:
|
||||
case Symbol.Equal:
|
||||
case Symbol.NotEqual:
|
||||
case Symbol.LessThan:
|
||||
case Symbol.LessThanOrEqual:
|
||||
case Symbol.GreaterThan:
|
||||
case Symbol.GreaterThanOrEqual:
|
||||
case Symbol.Plus:
|
||||
case Symbol.Minus:
|
||||
case Symbol.Star:
|
||||
case Symbol.ForwardSlash:
|
||||
case Symbol.Caret:
|
||||
case Symbol.Ampersand:
|
||||
return White;
|
||||
case Symbol.Semicolon:
|
||||
case Symbol.Colon:
|
||||
case Symbol.Comma:
|
||||
case Symbol.Period:
|
||||
case Symbol.DoubleColon:
|
||||
return Faint;
|
||||
case Symbol.OpenParen:
|
||||
case Symbol.CloseParen:
|
||||
case Symbol.OpenBrace:
|
||||
case Symbol.CloseBrace:
|
||||
case Symbol.OpenBracket:
|
||||
case Symbol.CloseBracket:
|
||||
return Yellow;
|
||||
default:
|
||||
return White;
|
||||
}
|
||||
default:
|
||||
return White;
|
||||
}
|
||||
}
|
||||
|
||||
public static string ColorizeSource(string source)
|
||||
{
|
||||
var sourceText = new SourceText(string.Empty, source);
|
||||
var tokens = Lexer.Tokenize(sourceText).Value;
|
||||
var result = new StringBuilder();
|
||||
var lastCharIndex = 0;
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var tokenStartIndex = GetCharacterIndex(sourceText, token.Span.Start);
|
||||
var tokenEndIndex = GetCharacterIndex(sourceText, token.Span.End);
|
||||
|
||||
if (tokenStartIndex > lastCharIndex)
|
||||
{
|
||||
var between = sourceText.Content.Substring(lastCharIndex, tokenStartIndex - lastCharIndex);
|
||||
result.Append(between);
|
||||
}
|
||||
|
||||
var tokenText = sourceText.Content.Substring(tokenStartIndex, tokenEndIndex - tokenStartIndex);
|
||||
|
||||
result.Append(Colorize(tokenText, GetTokenColor(token)));
|
||||
lastCharIndex = tokenEndIndex;
|
||||
}
|
||||
|
||||
if (lastCharIndex < sourceText.Content.Length)
|
||||
{
|
||||
var remaining = sourceText.Content[lastCharIndex..];
|
||||
result.Append(Colorize(remaining, Faint));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static int GetCharacterIndex(SourceText sourceText, SourceLocation location)
|
||||
{
|
||||
var lines = sourceText.Content.Split('\n');
|
||||
var index = 0;
|
||||
|
||||
for (var i = 0; i < location.Line - 1 && i < lines.Length; i++)
|
||||
{
|
||||
index += lines[i].Length + 1;
|
||||
}
|
||||
|
||||
index += location.Column - 1;
|
||||
|
||||
return Math.Min(index, sourceText.Content.Length);
|
||||
}
|
||||
}
|
||||
215
src/Nub.Lang/Diagnostics/Diagnostic.cs
Normal file
215
src/Nub.Lang/Diagnostics/Diagnostic.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System.Text;
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing;
|
||||
|
||||
namespace Nub.Lang.Diagnostics;
|
||||
|
||||
public class Diagnostic
|
||||
{
|
||||
public class DiagnosticBuilder
|
||||
{
|
||||
private readonly DiagnosticSeverity _severity;
|
||||
private readonly string _message;
|
||||
private string? _help;
|
||||
private SourceSpan? _sourceSpan;
|
||||
|
||||
public DiagnosticBuilder(DiagnosticSeverity severity, string message)
|
||||
{
|
||||
_severity = severity;
|
||||
_message = message;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder At(Token token)
|
||||
{
|
||||
_sourceSpan = token.Span;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder At(Node node)
|
||||
{
|
||||
_sourceSpan = SourceSpan.Merge(node.Tokens.Select(t => t.Span));
|
||||
return this;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder At(SourceSpan span)
|
||||
{
|
||||
_sourceSpan = span;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DiagnosticBuilder WithHelp(string help)
|
||||
{
|
||||
_help = help;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Diagnostic Build() => new(_severity, _message, _sourceSpan, _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 SourceSpan? Span { get; }
|
||||
public string? Help { get; }
|
||||
|
||||
private Diagnostic(DiagnosticSeverity severity, string message, SourceSpan? span, string? help)
|
||||
{
|
||||
Severity = severity;
|
||||
Message = message;
|
||||
Span = span;
|
||||
Help = help;
|
||||
}
|
||||
|
||||
public string Format()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var severityText = GetSeverityText(Severity);
|
||||
sb.Append(severityText);
|
||||
|
||||
if (Span.HasValue)
|
||||
{
|
||||
var locationText = $" at {Span.Value.Text.Path}:{Span}";
|
||||
sb.Append(ConsoleColors.Colorize(locationText, ConsoleColors.Faint));
|
||||
}
|
||||
|
||||
sb.Append(": ");
|
||||
sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite));
|
||||
|
||||
if (Span.HasValue)
|
||||
{
|
||||
sb.AppendLine();
|
||||
AppendSourceContext(sb, Span.Value, Severity);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Help))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.Append(ConsoleColors.Colorize($"help: {Help}", ConsoleColors.Cyan));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetSeverityText(DiagnosticSeverity severity)
|
||||
{
|
||||
return severity switch
|
||||
{
|
||||
DiagnosticSeverity.Error => ConsoleColors.Colorize("error", ConsoleColors.Bold + ConsoleColors.Red),
|
||||
DiagnosticSeverity.Warning => ConsoleColors.Colorize("warning", ConsoleColors.Bold + ConsoleColors.Yellow),
|
||||
DiagnosticSeverity.Info => ConsoleColors.Colorize("info", ConsoleColors.Bold + ConsoleColors.Blue),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(severity), severity, "Unknown diagnostic severity")
|
||||
};
|
||||
}
|
||||
|
||||
private static void AppendSourceContext(StringBuilder sb, SourceSpan span, DiagnosticSeverity severity)
|
||||
{
|
||||
var lines = span.Text.Content.Split('\n');
|
||||
var startLine = span.Start.Line;
|
||||
var endLine = span.End.Line;
|
||||
|
||||
const int contextLines = 3;
|
||||
|
||||
var lineNumWidth = Math.Min(endLine + contextLines, lines.Length).ToString().Length;
|
||||
|
||||
var contextStart = Math.Max(1, startLine - contextLines);
|
||||
var contextEnd = Math.Min(lines.Length, endLine + contextLines);
|
||||
|
||||
var contextWidth = 0;
|
||||
for (var i = contextStart; i <= contextEnd; i++)
|
||||
{
|
||||
if (lines[i - 1].Length > contextWidth)
|
||||
{
|
||||
contextWidth = lines[i - 1].Length;
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(ConsoleColors.Colorize('╭' + new string('─', lineNumWidth + 2) + '┬' + new string('─', contextWidth + 2) + '╮', ConsoleColors.Faint));
|
||||
|
||||
for (var lineNum = contextStart; lineNum < startLine; lineNum++)
|
||||
{
|
||||
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth);
|
||||
}
|
||||
|
||||
for (var lineNum = startLine; lineNum <= endLine; lineNum++)
|
||||
{
|
||||
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth);
|
||||
AppendErrorIndicators(sb, span, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth, severity);
|
||||
}
|
||||
|
||||
for (var lineNum = endLine + 1; lineNum <= contextEnd; lineNum++)
|
||||
{
|
||||
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth, contextWidth);
|
||||
}
|
||||
|
||||
sb.Append(ConsoleColors.Colorize('╰' + new string('─', lineNumWidth + 2) + '┴' + new string('─', contextWidth + 2) + '╯', ConsoleColors.Faint));
|
||||
}
|
||||
|
||||
private static void AppendContextLine(StringBuilder sb, int lineNum, string line, int lineNumWidth, int contextWidth)
|
||||
{
|
||||
sb.Append(ConsoleColors.Colorize('│' + " ", ConsoleColors.Faint));
|
||||
var lineNumStr = lineNum.ToString().PadLeft(lineNumWidth);
|
||||
sb.Append(ConsoleColors.Colorize(lineNumStr, ConsoleColors.Faint));
|
||||
sb.Append(ConsoleColors.Colorize(" │ ", ConsoleColors.Faint));
|
||||
sb.Append(ConsoleColors.ColorizeSource(line.PadRight(contextWidth)));
|
||||
sb.Append(ConsoleColors.Colorize(" " + '│', ConsoleColors.Faint));
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
private static void AppendErrorIndicators(StringBuilder sb, SourceSpan span, int lineNum, string line, int lineNumWidth, int contextWidth, DiagnosticSeverity severity)
|
||||
{
|
||||
var color = severity switch
|
||||
{
|
||||
DiagnosticSeverity.Info => ConsoleColors.Blue,
|
||||
DiagnosticSeverity.Warning => ConsoleColors.Yellow,
|
||||
DiagnosticSeverity.Error => ConsoleColors.Red,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null)
|
||||
};
|
||||
|
||||
sb.Append(ConsoleColors.Colorize('│' + " ", ConsoleColors.Faint));
|
||||
sb.Append(new string(' ', lineNumWidth));
|
||||
sb.Append(ConsoleColors.Colorize(" │ ", ConsoleColors.Faint));
|
||||
var indicators = GetIndicatorsForLine(span, lineNum, line);
|
||||
sb.Append(ConsoleColors.Colorize(indicators.PadRight(contextWidth), color));
|
||||
sb.Append(ConsoleColors.Colorize(" " + '│', ConsoleColors.Faint));
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
private static string GetIndicatorsForLine(SourceSpan span, int lineNum, string line)
|
||||
{
|
||||
const char indicator = '^';
|
||||
|
||||
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);
|
||||
|
||||
return new string(' ', startCol) + new string(indicator, length);
|
||||
}
|
||||
|
||||
if (lineNum == span.Start.Line)
|
||||
{
|
||||
var startCol = Math.Max(0, span.Start.Column - 1);
|
||||
return new string(' ', startCol) + new string(indicator, Math.Max(0, line.Length - startCol));
|
||||
}
|
||||
|
||||
if (lineNum == span.End.Line)
|
||||
{
|
||||
var endCol = Math.Min(line.Length, span.End.Column - 1);
|
||||
return new string(indicator, Math.Max(0, endCol));
|
||||
}
|
||||
|
||||
return new string(indicator, line.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public enum DiagnosticSeverity
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
20
src/Nub.Lang/Diagnostics/DiagnosticsResult.cs
Normal file
20
src/Nub.Lang/Diagnostics/DiagnosticsResult.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Nub.Lang.Diagnostics;
|
||||
|
||||
public class DiagnosticsResult(List<Diagnostic> diagnostics)
|
||||
{
|
||||
public bool HasErrors => diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);
|
||||
|
||||
public void PrintAllDiagnostics()
|
||||
{
|
||||
foreach (var diagnostic in diagnostics)
|
||||
{
|
||||
Console.Error.WriteLine(diagnostic.Format());
|
||||
Console.Error.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DiagnosticsResult<TResult>(List<Diagnostic> diagnostics, TResult value) : DiagnosticsResult(diagnostics)
|
||||
{
|
||||
public TResult Value { get; } = value;
|
||||
}
|
||||
43
src/Nub.Lang/Frontend/DefinitionTable.cs
Normal file
43
src/Nub.Lang/Frontend/DefinitionTable.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Nub.Lang.Frontend.Parsing;
|
||||
using Nub.Lang.Frontend.Parsing.Definitions;
|
||||
|
||||
namespace Nub.Lang.Frontend;
|
||||
|
||||
public class DefinitionTable
|
||||
{
|
||||
private readonly IEnumerable<CompilationUnit> _compilationUnits;
|
||||
|
||||
public DefinitionTable(IEnumerable<CompilationUnit> compilationUnits)
|
||||
{
|
||||
_compilationUnits = compilationUnits;
|
||||
}
|
||||
|
||||
public Optional<IFuncSignature> LookupFunc(string @namespace, string name)
|
||||
{
|
||||
var definition = _compilationUnits
|
||||
.Where(c => c.Namespace == @namespace)
|
||||
.SelectMany(c => c.Definitions)
|
||||
.OfType<IFuncSignature>()
|
||||
.SingleOrDefault(f => f.Name == name);
|
||||
|
||||
return Optional.OfNullable(definition);
|
||||
}
|
||||
|
||||
public Optional<StructDefinitionNode> LookupStruct(string @namespace, string name)
|
||||
{
|
||||
var definition = _compilationUnits
|
||||
.Where(c => c.Namespace == @namespace)
|
||||
.SelectMany(c => c.Definitions)
|
||||
.OfType<StructDefinitionNode>()
|
||||
.SingleOrDefault(f => f.Name == name);
|
||||
|
||||
return Optional.OfNullable(definition);
|
||||
}
|
||||
|
||||
public IEnumerable<StructDefinitionNode> GetStructs()
|
||||
{
|
||||
return _compilationUnits
|
||||
.SelectMany(c => c.Definitions)
|
||||
.OfType<StructDefinitionNode>();
|
||||
}
|
||||
}
|
||||
1396
src/Nub.Lang/Frontend/Generation/QBEGenerator.cs
Normal file
1396
src/Nub.Lang/Frontend/Generation/QBEGenerator.cs
Normal file
File diff suppressed because it is too large
Load Diff
6
src/Nub.Lang/Frontend/Lexing/DocumentationToken.cs
Normal file
6
src/Nub.Lang/Frontend/Lexing/DocumentationToken.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Nub.Lang.Frontend.Lexing;
|
||||
|
||||
public class DocumentationToken(SourceSpan span, string documentation) : Token(span)
|
||||
{
|
||||
public string Documentation { get; } = documentation;
|
||||
}
|
||||
6
src/Nub.Lang/Frontend/Lexing/IdentifierToken.cs
Normal file
6
src/Nub.Lang/Frontend/Lexing/IdentifierToken.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Nub.Lang.Frontend.Lexing;
|
||||
|
||||
public class IdentifierToken(SourceSpan span, string value) : Token(span)
|
||||
{
|
||||
public string Value { get; } = value;
|
||||
}
|
||||
278
src/Nub.Lang/Frontend/Lexing/Lexer.cs
Normal file
278
src/Nub.Lang/Frontend/Lexing/Lexer.cs
Normal file
@@ -0,0 +1,278 @@
|
||||
using Nub.Lang.Diagnostics;
|
||||
|
||||
namespace Nub.Lang.Frontend.Lexing;
|
||||
|
||||
public static class Lexer
|
||||
{
|
||||
private static readonly Dictionary<string, Symbol> Keywords = new()
|
||||
{
|
||||
["namespace"] = Symbol.Namespace,
|
||||
["func"] = Symbol.Func,
|
||||
["if"] = Symbol.If,
|
||||
["else"] = Symbol.Else,
|
||||
["while"] = Symbol.While,
|
||||
["break"] = Symbol.Break,
|
||||
["continue"] = Symbol.Continue,
|
||||
["return"] = Symbol.Return,
|
||||
["alloc"] = Symbol.Alloc,
|
||||
["struct"] = Symbol.Struct,
|
||||
["let"] = Symbol.Let,
|
||||
["calls"] = Symbol.Calls,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, Modifier> Modifiers = new()
|
||||
{
|
||||
["export"] = Modifier.Export,
|
||||
["extern"] = Modifier.Extern,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<char[], Symbol> Chians = new()
|
||||
{
|
||||
[['=', '=']] = Symbol.Equal,
|
||||
[['!', '=']] = Symbol.NotEqual,
|
||||
[['<', '=']] = Symbol.LessThanOrEqual,
|
||||
[['>', '=']] = Symbol.GreaterThanOrEqual,
|
||||
[[':', ':']] = Symbol.DoubleColon,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<char, Symbol> Chars = new()
|
||||
{
|
||||
[';'] = Symbol.Semicolon,
|
||||
[':'] = Symbol.Colon,
|
||||
['('] = Symbol.OpenParen,
|
||||
[')'] = Symbol.CloseParen,
|
||||
['{'] = Symbol.OpenBrace,
|
||||
['}'] = Symbol.CloseBrace,
|
||||
['['] = Symbol.OpenBracket,
|
||||
[']'] = Symbol.CloseBracket,
|
||||
[','] = Symbol.Comma,
|
||||
['.'] = Symbol.Period,
|
||||
['='] = Symbol.Assign,
|
||||
['<'] = Symbol.LessThan,
|
||||
['>'] = Symbol.GreaterThan,
|
||||
['+'] = Symbol.Plus,
|
||||
['-'] = Symbol.Minus,
|
||||
['*'] = Symbol.Star,
|
||||
['/'] = Symbol.ForwardSlash,
|
||||
['!'] = Symbol.Bang,
|
||||
['^'] = Symbol.Caret,
|
||||
['&'] = Symbol.Ampersand,
|
||||
};
|
||||
|
||||
private static SourceText _sourceText;
|
||||
private static int _index;
|
||||
|
||||
public static DiagnosticsResult<List<Token>> Tokenize(SourceText sourceText)
|
||||
{
|
||||
_sourceText = sourceText;
|
||||
_index = 0;
|
||||
|
||||
List<Token> tokens = [];
|
||||
while (ParseToken().TryGetValue(out var token))
|
||||
{
|
||||
tokens.Add(token);
|
||||
}
|
||||
|
||||
return new DiagnosticsResult<List<Token>>([], tokens);
|
||||
}
|
||||
|
||||
private static void ConsumeWhitespace()
|
||||
{
|
||||
while (Peek().TryGetValue(out var character) && char.IsWhiteSpace(character))
|
||||
{
|
||||
Next();
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<Token> ParseToken()
|
||||
{
|
||||
ConsumeWhitespace();
|
||||
var startIndex = _index;
|
||||
|
||||
if (!Peek().TryGetValue(out var current))
|
||||
{
|
||||
return Optional<Token>.Empty();
|
||||
}
|
||||
|
||||
if (current == '/' && Peek(1).TryGetValue(out var nextChar) && nextChar == '/')
|
||||
{
|
||||
Next();
|
||||
Next();
|
||||
|
||||
if (Peek().TryGetValue(out var thirdChar) && thirdChar == '/')
|
||||
{
|
||||
Next();
|
||||
var buffer = string.Empty;
|
||||
while (Peek().TryGetValue(out var character) && character != '\n')
|
||||
{
|
||||
buffer += character;
|
||||
Next();
|
||||
}
|
||||
Next();
|
||||
return new DocumentationToken(CreateSpan(startIndex), buffer);
|
||||
}
|
||||
|
||||
while (Peek().TryGetValue(out var character) && character != '\n')
|
||||
{
|
||||
Next();
|
||||
}
|
||||
Next();
|
||||
return ParseToken();
|
||||
}
|
||||
|
||||
if (char.IsLetter(current) || current == '_')
|
||||
{
|
||||
var buffer = string.Empty;
|
||||
|
||||
while (Peek().TryGetValue(out var next) && (char.IsLetterOrDigit(next) || next == '_'))
|
||||
{
|
||||
buffer += next;
|
||||
Next();
|
||||
}
|
||||
|
||||
if (Keywords.TryGetValue(buffer, out var keywordSymbol))
|
||||
{
|
||||
return new SymbolToken(CreateSpan(startIndex), keywordSymbol);
|
||||
}
|
||||
|
||||
if (Modifiers.TryGetValue(buffer, out var modifer))
|
||||
{
|
||||
return new ModifierToken(CreateSpan(startIndex), modifer);
|
||||
}
|
||||
|
||||
if (buffer is "true" or "false")
|
||||
{
|
||||
return new LiteralToken(CreateSpan(startIndex), LiteralKind.Bool, buffer);
|
||||
}
|
||||
|
||||
return new IdentifierToken(CreateSpan(startIndex), buffer);
|
||||
}
|
||||
|
||||
if (char.IsDigit(current))
|
||||
{
|
||||
var isFloat = false;
|
||||
var buffer = string.Empty;
|
||||
|
||||
while (Peek().TryGetValue(out var next))
|
||||
{
|
||||
if (next == '.')
|
||||
{
|
||||
if (isFloat)
|
||||
{
|
||||
throw new Exception("More than one period found in float literal");
|
||||
}
|
||||
|
||||
isFloat = true;
|
||||
buffer += next;
|
||||
Next();
|
||||
}
|
||||
else if (char.IsDigit(next))
|
||||
{
|
||||
buffer += next;
|
||||
Next();
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new LiteralToken(CreateSpan(startIndex), isFloat ? LiteralKind.Float : LiteralKind.Integer, buffer);
|
||||
}
|
||||
|
||||
// TODO: Revisit this
|
||||
foreach (var chain in Chians)
|
||||
{
|
||||
if (current != chain.Key[0]) continue;
|
||||
|
||||
for (var i = 1; i < chain.Key.Length; i++)
|
||||
{
|
||||
var c = Peek(i);
|
||||
if (!c.HasValue || c.Value != chain.Key[i]) break;
|
||||
|
||||
if (i == chain.Key.Length - 1)
|
||||
{
|
||||
for (var j = 0; j <= i; j++)
|
||||
{
|
||||
Next();
|
||||
}
|
||||
|
||||
return new SymbolToken(CreateSpan(startIndex), chain.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Chars.TryGetValue(current, out var charSymbol))
|
||||
{
|
||||
Next();
|
||||
return new SymbolToken(CreateSpan(startIndex), charSymbol);
|
||||
}
|
||||
|
||||
if (current == '"')
|
||||
{
|
||||
Next();
|
||||
var buffer = string.Empty;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (!Peek().TryGetValue(out var next))
|
||||
{
|
||||
throw new Exception("Unclosed string literal");
|
||||
}
|
||||
|
||||
if (next == '"')
|
||||
{
|
||||
Next();
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += next;
|
||||
Next();
|
||||
}
|
||||
|
||||
return new LiteralToken(CreateSpan(startIndex), LiteralKind.String, buffer);
|
||||
}
|
||||
|
||||
throw new Exception($"Unknown character {current}");
|
||||
}
|
||||
|
||||
private static SourceLocation CreateLocation(int index)
|
||||
{
|
||||
var line = 1;
|
||||
var column = 1;
|
||||
for (var i = 0; i < Math.Min(index, _sourceText.Content.Length - 1); i++)
|
||||
{
|
||||
if (_sourceText.Content[i] == '\n')
|
||||
{
|
||||
column = 1;
|
||||
line += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
column += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return new SourceLocation(line, column);
|
||||
}
|
||||
|
||||
private static SourceSpan CreateSpan(int startIndex)
|
||||
{
|
||||
return new SourceSpan(_sourceText, CreateLocation(startIndex), CreateLocation(_index));
|
||||
}
|
||||
|
||||
private static Optional<char> Peek(int offset = 0)
|
||||
{
|
||||
if (_index + offset < _sourceText.Content.Length)
|
||||
{
|
||||
return _sourceText.Content[_index + offset];
|
||||
}
|
||||
|
||||
return Optional<char>.Empty();
|
||||
}
|
||||
|
||||
private static void Next()
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
15
src/Nub.Lang/Frontend/Lexing/LiteralToken.cs
Normal file
15
src/Nub.Lang/Frontend/Lexing/LiteralToken.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Nub.Lang.Frontend.Lexing;
|
||||
|
||||
public class LiteralToken(SourceSpan span, LiteralKind kind, string value) : Token(span)
|
||||
{
|
||||
public LiteralKind Kind { get; } = kind;
|
||||
public string Value { get; } = value;
|
||||
}
|
||||
|
||||
public enum LiteralKind
|
||||
{
|
||||
Integer,
|
||||
Float,
|
||||
String,
|
||||
Bool
|
||||
}
|
||||
12
src/Nub.Lang/Frontend/Lexing/ModifierToken.cs
Normal file
12
src/Nub.Lang/Frontend/Lexing/ModifierToken.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Nub.Lang.Frontend.Lexing;
|
||||
|
||||
public class ModifierToken(SourceSpan span, Modifier modifier) : Token(span)
|
||||
{
|
||||
public Modifier Modifier { get; } = modifier;
|
||||
}
|
||||
|
||||
public enum Modifier
|
||||
{
|
||||
Extern,
|
||||
Export
|
||||
}
|
||||
47
src/Nub.Lang/Frontend/Lexing/SymbolToken.cs
Normal file
47
src/Nub.Lang/Frontend/Lexing/SymbolToken.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
namespace Nub.Lang.Frontend.Lexing;
|
||||
|
||||
public class SymbolToken(SourceSpan span, Symbol symbol) : Token(span)
|
||||
{
|
||||
public Symbol Symbol { get; } = symbol;
|
||||
}
|
||||
|
||||
public enum Symbol
|
||||
{
|
||||
Func,
|
||||
Return,
|
||||
If,
|
||||
Else,
|
||||
While,
|
||||
Break,
|
||||
Continue,
|
||||
Semicolon,
|
||||
Colon,
|
||||
OpenParen,
|
||||
CloseParen,
|
||||
OpenBrace,
|
||||
CloseBrace,
|
||||
OpenBracket,
|
||||
CloseBracket,
|
||||
Comma,
|
||||
Period,
|
||||
Assign,
|
||||
Bang,
|
||||
Equal,
|
||||
NotEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
ForwardSlash,
|
||||
Struct,
|
||||
Caret,
|
||||
Ampersand,
|
||||
DoubleColon,
|
||||
Namespace,
|
||||
Let,
|
||||
Alloc,
|
||||
Calls
|
||||
}
|
||||
6
src/Nub.Lang/Frontend/Lexing/Token.cs
Normal file
6
src/Nub.Lang/Frontend/Lexing/Token.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Nub.Lang.Frontend.Lexing;
|
||||
|
||||
public abstract class Token(SourceSpan span)
|
||||
{
|
||||
public SourceSpan Span { get; } = span;
|
||||
}
|
||||
9
src/Nub.Lang/Frontend/Parsing/CompilationUnit.cs
Normal file
9
src/Nub.Lang/Frontend/Parsing/CompilationUnit.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Nub.Lang.Frontend.Parsing.Definitions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing;
|
||||
|
||||
public class CompilationUnit(string @namespace, List<DefinitionNode> definitions)
|
||||
{
|
||||
public string Namespace { get; } = @namespace;
|
||||
public List<DefinitionNode> Definitions { get; } = definitions;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Definitions;
|
||||
|
||||
public abstract class DefinitionNode(IReadOnlyList<Token> tokens, Optional<string> documentation, string @namespace) : Node(tokens)
|
||||
{
|
||||
public Optional<string> Documentation { get; } = documentation;
|
||||
public string Namespace { get; set; } = @namespace;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Statements;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Definitions;
|
||||
|
||||
public class FuncParameter(string name, NubType type)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public NubType Type { get; } = type;
|
||||
|
||||
public override string ToString() => $"{Name}: {Type}";
|
||||
}
|
||||
|
||||
public interface IFuncSignature
|
||||
{
|
||||
public string Name { get; }
|
||||
public List<FuncParameter> Parameters { get; }
|
||||
public NubType ReturnType { get; }
|
||||
|
||||
public string ToString() => $"{Name}({string.Join(", ", Parameters.Select(p => p.ToString()))}){": " + ReturnType}";
|
||||
}
|
||||
|
||||
public class LocalFuncDefinitionNode(IReadOnlyList<Token> tokens, Optional<string> documentation, string @namespace, string name, List<FuncParameter> parameters, BlockNode body, NubType returnType, bool exported) : DefinitionNode(tokens, documentation, @namespace), IFuncSignature
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public List<FuncParameter> Parameters { get; } = parameters;
|
||||
public BlockNode Body { get; } = body;
|
||||
public NubType ReturnType { get; } = returnType;
|
||||
public bool Exported { get; } = exported;
|
||||
|
||||
public override string ToString() => $"{Name}({string.Join(", ", Parameters.Select(p => p.ToString()))}){": " + ReturnType}";
|
||||
}
|
||||
|
||||
public class ExternFuncDefinitionNode(IReadOnlyList<Token> tokens, Optional<string> documentation, string @namespace, string name, string callName, List<FuncParameter> parameters, NubType returnType) : DefinitionNode(tokens, documentation, @namespace), IFuncSignature
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public string CallName { get; } = callName;
|
||||
public List<FuncParameter> Parameters { get; } = parameters;
|
||||
public NubType ReturnType { get; } = returnType;
|
||||
|
||||
public override string ToString() => $"{Name}({string.Join(", ", Parameters.Select(p => p.ToString()))}){": " + ReturnType}";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Definitions;
|
||||
|
||||
public class StructField(string name, NubType type, Optional<ExpressionNode> value)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public NubType Type { get; } = type;
|
||||
public Optional<ExpressionNode> Value { get; } = value;
|
||||
}
|
||||
|
||||
public class StructDefinitionNode(IReadOnlyList<Token> tokens, Optional<string> documentation, string @namespace, string name, List<StructField> fields) : DefinitionNode(tokens, documentation, @namespace)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public List<StructField> Fields { get; } = fields;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class AddressOfNode(IReadOnlyList<Token> tokens, LValueNode expression) : ExpressionNode(tokens)
|
||||
{
|
||||
public LValueNode Expression { get; } = expression;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Definitions;
|
||||
using Nub.Lang.Frontend.Parsing.Statements;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class AnonymousFuncNode(IReadOnlyList<Token> tokens, List<FuncParameter> parameters, BlockNode body, NubType returnType) : ExpressionNode(tokens)
|
||||
{
|
||||
public List<FuncParameter> Parameters { get; } = parameters;
|
||||
public BlockNode Body { get; } = body;
|
||||
public NubType ReturnType { get; } = returnType;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class ArrayIndexAccessNode(IReadOnlyList<Token> tokens, ExpressionNode array, ExpressionNode index) : LValueNode(tokens)
|
||||
{
|
||||
public ExpressionNode Array { get; } = array;
|
||||
public ExpressionNode Index { get; } = index;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class ArrayInitializerNode(IReadOnlyList<Token> tokens, ExpressionNode capacity, NubType elementType) : ExpressionNode(tokens)
|
||||
{
|
||||
public ExpressionNode Capacity { get; } = capacity;
|
||||
public NubType ElementType { get; } = elementType;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class BinaryExpressionNode(IReadOnlyList<Token> tokens, ExpressionNode left, BinaryExpressionOperator @operator, ExpressionNode right) : ExpressionNode(tokens)
|
||||
{
|
||||
public ExpressionNode Left { get; } = left;
|
||||
public BinaryExpressionOperator Operator { get; } = @operator;
|
||||
public ExpressionNode Right { get; } = right;
|
||||
}
|
||||
|
||||
public enum BinaryExpressionOperator
|
||||
{
|
||||
Equal,
|
||||
NotEqual,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Plus,
|
||||
Minus,
|
||||
Multiply,
|
||||
Divide
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class DereferenceNode(IReadOnlyList<Token> tokens, ExpressionNode expression) : LValueNode(tokens)
|
||||
{
|
||||
public ExpressionNode Expression { get; } = expression;
|
||||
}
|
||||
16
src/Nub.Lang/Frontend/Parsing/Expressions/ExpressionNode.cs
Normal file
16
src/Nub.Lang/Frontend/Parsing/Expressions/ExpressionNode.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public abstract class ExpressionNode(IReadOnlyList<Token> tokens) : Node(tokens)
|
||||
{
|
||||
private NubType? _type;
|
||||
public NubType Type
|
||||
{
|
||||
get => _type ?? throw new Exception("Tried to access expression type before type was populated");
|
||||
set => _type = value;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class LValueNode(IReadOnlyList<Token> tokens) : ExpressionNode(tokens);
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class FixedArrayInitializerNode(IReadOnlyList<Token> tokens, NubType elementType, int capacity) : ExpressionNode(tokens)
|
||||
{
|
||||
public NubType ElementType { get; } = elementType;
|
||||
public int Capacity { get; } = capacity;
|
||||
}
|
||||
11
src/Nub.Lang/Frontend/Parsing/Expressions/FuncCallNode.cs
Normal file
11
src/Nub.Lang/Frontend/Parsing/Expressions/FuncCallNode.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class FuncCallNode(IReadOnlyList<Token> tokens, ExpressionNode expression, List<ExpressionNode> parameters) : ExpressionNode(tokens)
|
||||
{
|
||||
public ExpressionNode Expression = expression;
|
||||
public List<ExpressionNode> Parameters { get; } = parameters;
|
||||
|
||||
public override string ToString() => $"{Expression}({string.Join(", ", Parameters)})";
|
||||
}
|
||||
11
src/Nub.Lang/Frontend/Parsing/Expressions/IdentifierNode.cs
Normal file
11
src/Nub.Lang/Frontend/Parsing/Expressions/IdentifierNode.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class IdentifierNode(IReadOnlyList<Token> tokens, Optional<string> @namespace, string name) : LValueNode(tokens)
|
||||
{
|
||||
public Optional<string> Namespace { get; } = @namespace;
|
||||
public string Name { get; } = name;
|
||||
|
||||
public override string ToString() => Namespace.HasValue ? $"{Namespace.Value}::{Name}" : Name;
|
||||
}
|
||||
9
src/Nub.Lang/Frontend/Parsing/Expressions/LiteralNode.cs
Normal file
9
src/Nub.Lang/Frontend/Parsing/Expressions/LiteralNode.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class LiteralNode(IReadOnlyList<Token> tokens, string literal, LiteralKind kind) : ExpressionNode(tokens)
|
||||
{
|
||||
public string Literal { get; } = literal;
|
||||
public LiteralKind Kind { get; } = kind;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class MemberAccessNode(IReadOnlyList<Token> tokens, ExpressionNode expression, string member) : LValueNode(tokens)
|
||||
{
|
||||
public ExpressionNode Expression { get; } = expression;
|
||||
public string Member { get; } = member;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class StructInitializerNode(IReadOnlyList<Token> tokens, NubStructType structType, Dictionary<string, ExpressionNode> initializers) : ExpressionNode(tokens)
|
||||
{
|
||||
public NubStructType StructType { get; } = structType;
|
||||
public Dictionary<string, ExpressionNode> Initializers { get; } = initializers;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
public class UnaryExpressionNode(IReadOnlyList<Token> tokens, UnaryExpressionOperator @operator, ExpressionNode operand) : ExpressionNode(tokens)
|
||||
{
|
||||
public UnaryExpressionOperator Operator { get; } = @operator;
|
||||
public ExpressionNode Operand { get; } = operand;
|
||||
}
|
||||
|
||||
public enum UnaryExpressionOperator
|
||||
{
|
||||
Negate,
|
||||
Invert
|
||||
}
|
||||
8
src/Nub.Lang/Frontend/Parsing/Node.cs
Normal file
8
src/Nub.Lang/Frontend/Parsing/Node.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing;
|
||||
|
||||
public abstract class Node(IReadOnlyList<Token> tokens)
|
||||
{
|
||||
public IReadOnlyList<Token> Tokens { get; set; } = tokens;
|
||||
}
|
||||
936
src/Nub.Lang/Frontend/Parsing/Parser.cs
Normal file
936
src/Nub.Lang/Frontend/Parsing/Parser.cs
Normal file
@@ -0,0 +1,936 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Nub.Lang.Diagnostics;
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Definitions;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
using Nub.Lang.Frontend.Parsing.Statements;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing;
|
||||
|
||||
public static class Parser
|
||||
{
|
||||
private static string _namespace = null!;
|
||||
private static List<Diagnostic> _diagnostics = [];
|
||||
private static List<Token> _tokens = [];
|
||||
private static int _index;
|
||||
|
||||
public static DiagnosticsResult<CompilationUnit?> ParseFile(List<Token> tokens)
|
||||
{
|
||||
_tokens = tokens;
|
||||
_namespace = null!;
|
||||
_diagnostics = [];
|
||||
_index = 0;
|
||||
|
||||
try
|
||||
{
|
||||
ExpectSymbol(Symbol.Namespace);
|
||||
var @namespace = ExpectIdentifier();
|
||||
_namespace = @namespace.Value;
|
||||
}
|
||||
catch (ParseException ex)
|
||||
{
|
||||
_diagnostics.Add(ex.Diagnostic);
|
||||
return new DiagnosticsResult<CompilationUnit?>(_diagnostics, null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
List<DefinitionNode> definitions = [];
|
||||
|
||||
while (Peek().HasValue)
|
||||
{
|
||||
var definition = ParseDefinition();
|
||||
definitions.Add(definition);
|
||||
}
|
||||
|
||||
return new DiagnosticsResult<CompilationUnit?>(_diagnostics, new CompilationUnit(_namespace, definitions));
|
||||
}
|
||||
catch (ParseException ex)
|
||||
{
|
||||
_diagnostics.Add(ex.Diagnostic);
|
||||
RecoverToNextDefinition();
|
||||
}
|
||||
|
||||
return new DiagnosticsResult<CompilationUnit?>(_diagnostics, null);
|
||||
}
|
||||
|
||||
private static DefinitionNode ParseDefinition()
|
||||
{
|
||||
var startIndex = _index;
|
||||
List<ModifierToken> modifiers = [];
|
||||
|
||||
List<string> documentationParts = [];
|
||||
while (_index < _tokens.Count && _tokens[_index] is DocumentationToken commentToken)
|
||||
{
|
||||
documentationParts.Add(commentToken.Documentation);
|
||||
_index++;
|
||||
}
|
||||
|
||||
var documentation = documentationParts.Count == 0 ? null : string.Join('\n', documentationParts);
|
||||
|
||||
while (TryExpectModifier(out var modifier))
|
||||
{
|
||||
modifiers.Add(modifier);
|
||||
}
|
||||
|
||||
var keyword = ExpectSymbol();
|
||||
return keyword.Symbol switch
|
||||
{
|
||||
Symbol.Func => ParseFuncDefinition(startIndex, modifiers, Optional.OfNullable(documentation)),
|
||||
Symbol.Struct => ParseStruct(startIndex, modifiers, Optional.OfNullable(documentation)),
|
||||
_ => 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 static DefinitionNode ParseFuncDefinition(int startIndex, List<ModifierToken> modifiers, Optional<string> documentation)
|
||||
{
|
||||
var name = ExpectIdentifier();
|
||||
List<FuncParameter> parameters = [];
|
||||
|
||||
ExpectSymbol(Symbol.OpenParen);
|
||||
|
||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||
{
|
||||
parameters.Add(ParseFuncParameter());
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
var returnType = TryExpectSymbol(Symbol.Colon) ? ParseType() : new NubVoidType();
|
||||
|
||||
var isExtern = modifiers.RemoveAll(x => x.Modifier == Modifier.Extern) > 0;
|
||||
if (isExtern)
|
||||
{
|
||||
if (modifiers.Count != 0)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
var callName = name.Value;
|
||||
|
||||
if (TryExpectSymbol(Symbol.Calls))
|
||||
{
|
||||
callName = ExpectIdentifier().Value;
|
||||
}
|
||||
|
||||
return new ExternFuncDefinitionNode(GetTokensForNode(startIndex), documentation, _namespace, name.Value, callName, parameters, returnType);
|
||||
}
|
||||
|
||||
var body = ParseBlock();
|
||||
var exported = modifiers.RemoveAll(x => x.Modifier == Modifier.Export) > 0;
|
||||
|
||||
if (modifiers.Count != 0)
|
||||
{
|
||||
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, _namespace, name.Value, parameters, body, returnType, exported);
|
||||
}
|
||||
|
||||
private static StructDefinitionNode ParseStruct(int startIndex, List<ModifierToken> _, Optional<string> documentation)
|
||||
{
|
||||
var name = ExpectIdentifier().Value;
|
||||
|
||||
ExpectSymbol(Symbol.OpenBrace);
|
||||
|
||||
List<StructField> variables = [];
|
||||
|
||||
while (!TryExpectSymbol(Symbol.CloseBrace))
|
||||
{
|
||||
var variableName = ExpectIdentifier().Value;
|
||||
ExpectSymbol(Symbol.Colon);
|
||||
var variableType = ParseType();
|
||||
|
||||
var variableValue = Optional<ExpressionNode>.Empty();
|
||||
|
||||
if (TryExpectSymbol(Symbol.Assign))
|
||||
{
|
||||
variableValue = ParseExpression();
|
||||
}
|
||||
|
||||
variables.Add(new StructField(variableName, variableType, variableValue));
|
||||
}
|
||||
|
||||
return new StructDefinitionNode(GetTokensForNode(startIndex), documentation, _namespace, name, variables);
|
||||
}
|
||||
|
||||
private static FuncParameter ParseFuncParameter()
|
||||
{
|
||||
var name = ExpectIdentifier();
|
||||
ExpectSymbol(Symbol.Colon);
|
||||
var type = ParseType();
|
||||
|
||||
return new FuncParameter(name.Value, type);
|
||||
}
|
||||
|
||||
private static StatementNode ParseStatement()
|
||||
{
|
||||
var startIndex = _index;
|
||||
if (!Peek().TryGetValue(out var token))
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error("Unexpected end of file while parsing statement")
|
||||
.At(_tokens.Last())
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (token is SymbolToken symbol)
|
||||
{
|
||||
switch (symbol.Symbol)
|
||||
{
|
||||
case Symbol.Return:
|
||||
return ParseReturn(startIndex);
|
||||
case Symbol.If:
|
||||
return ParseIf(startIndex);
|
||||
case Symbol.While:
|
||||
return ParseWhile(startIndex);
|
||||
case Symbol.Let:
|
||||
return ParseVariableDeclaration(startIndex);
|
||||
case Symbol.Break:
|
||||
return ParseBreak(startIndex);
|
||||
case Symbol.Continue:
|
||||
return ParseContinue(startIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return ParseStatementExpression(startIndex);
|
||||
}
|
||||
|
||||
private static StatementNode ParseStatementExpression(int startIndex)
|
||||
{
|
||||
var expr = ParseExpression();
|
||||
|
||||
if (Peek().TryGetValue(out var token))
|
||||
{
|
||||
if (token is SymbolToken symbol)
|
||||
{
|
||||
switch (symbol.Symbol)
|
||||
{
|
||||
case Symbol.Assign:
|
||||
{
|
||||
switch (expr)
|
||||
{
|
||||
case MemberAccessNode memberAccess:
|
||||
{
|
||||
Next();
|
||||
var value = ParseExpression();
|
||||
return new MemberAssignmentNode(GetTokensForNode(startIndex), memberAccess, value);
|
||||
}
|
||||
case ArrayIndexAccessNode arrayIndexAccess:
|
||||
{
|
||||
Next();
|
||||
var value = ParseExpression();
|
||||
return new ArrayIndexAssignmentNode(GetTokensForNode(startIndex), arrayIndexAccess, value);
|
||||
}
|
||||
case IdentifierNode identifier:
|
||||
{
|
||||
Next();
|
||||
var value = ParseExpression();
|
||||
return new VariableAssignmentNode(GetTokensForNode(startIndex), identifier, value);
|
||||
}
|
||||
case DereferenceNode dereference:
|
||||
{
|
||||
Next();
|
||||
var value = ParseExpression();
|
||||
return new DereferenceAssignmentNode(GetTokensForNode(startIndex), dereference, value);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StatementExpressionNode(GetTokensForNode(startIndex), expr);
|
||||
}
|
||||
|
||||
private static VariableDeclarationNode ParseVariableDeclaration(int startIndex)
|
||||
{
|
||||
ExpectSymbol(Symbol.Let);
|
||||
var name = ExpectIdentifier().Value;
|
||||
var type = Optional<NubType>.Empty();
|
||||
if (TryExpectSymbol(Symbol.Colon))
|
||||
{
|
||||
type = ParseType();
|
||||
}
|
||||
|
||||
var value = Optional<ExpressionNode>.Empty();
|
||||
if (TryExpectSymbol(Symbol.Assign))
|
||||
{
|
||||
value = ParseExpression();
|
||||
}
|
||||
|
||||
return new VariableDeclarationNode(GetTokensForNode(startIndex), name, type, value);
|
||||
}
|
||||
|
||||
private static StatementNode ParseBreak(int startIndex)
|
||||
{
|
||||
ExpectSymbol(Symbol.Break);
|
||||
Next();
|
||||
return new BreakNode(GetTokensForNode(startIndex));
|
||||
}
|
||||
|
||||
private static StatementNode ParseContinue(int startIndex)
|
||||
{
|
||||
ExpectSymbol(Symbol.Continue);
|
||||
return new ContinueNode(GetTokensForNode(startIndex));
|
||||
}
|
||||
|
||||
private static ReturnNode ParseReturn(int startIndex)
|
||||
{
|
||||
ExpectSymbol(Symbol.Return);
|
||||
var value = Optional<ExpressionNode>.Empty();
|
||||
if (!TryExpectSymbol(Symbol.Semicolon))
|
||||
{
|
||||
value = ParseExpression();
|
||||
}
|
||||
|
||||
return new ReturnNode(GetTokensForNode(startIndex), value);
|
||||
}
|
||||
|
||||
private static IfNode ParseIf(int startIndex)
|
||||
{
|
||||
ExpectSymbol(Symbol.If);
|
||||
var condition = ParseExpression();
|
||||
var body = ParseBlock();
|
||||
|
||||
var elseStatement = Optional<Variant<IfNode, BlockNode>>.Empty();
|
||||
if (TryExpectSymbol(Symbol.Else))
|
||||
{
|
||||
var newStartIndex = _index;
|
||||
elseStatement = TryExpectSymbol(Symbol.If)
|
||||
? (Variant<IfNode, BlockNode>)ParseIf(newStartIndex)
|
||||
: (Variant<IfNode, BlockNode>)ParseBlock();
|
||||
}
|
||||
|
||||
return new IfNode(GetTokensForNode(startIndex), condition, body, elseStatement);
|
||||
}
|
||||
|
||||
private static WhileNode ParseWhile(int startIndex)
|
||||
{
|
||||
ExpectSymbol(Symbol.While);
|
||||
var condition = ParseExpression();
|
||||
var body = ParseBlock();
|
||||
return new WhileNode(GetTokensForNode(startIndex), condition, body);
|
||||
}
|
||||
|
||||
private static ExpressionNode ParseExpression(int precedence = 0)
|
||||
{
|
||||
var startIndex = _index;
|
||||
var left = ParsePrimaryExpression();
|
||||
|
||||
while (true)
|
||||
{
|
||||
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);
|
||||
|
||||
left = new BinaryExpressionNode(GetTokensForNode(startIndex), left, op.Value, right);
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private static int GetBinaryOperatorPrecedence(BinaryExpressionOperator binaryExpressionOperator)
|
||||
{
|
||||
return binaryExpressionOperator switch
|
||||
{
|
||||
BinaryExpressionOperator.Multiply => 3,
|
||||
BinaryExpressionOperator.Divide => 3,
|
||||
BinaryExpressionOperator.Plus => 2,
|
||||
BinaryExpressionOperator.Minus => 2,
|
||||
BinaryExpressionOperator.GreaterThan => 1,
|
||||
BinaryExpressionOperator.GreaterThanOrEqual => 1,
|
||||
BinaryExpressionOperator.LessThan => 1,
|
||||
BinaryExpressionOperator.LessThanOrEqual => 1,
|
||||
BinaryExpressionOperator.Equal => 0,
|
||||
BinaryExpressionOperator.NotEqual => 0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(binaryExpressionOperator), binaryExpressionOperator, null)
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetBinaryOperator(Symbol symbol, [NotNullWhen(true)] out BinaryExpressionOperator? binaryExpressionOperator)
|
||||
{
|
||||
switch (symbol)
|
||||
{
|
||||
case Symbol.Equal:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.Equal;
|
||||
return true;
|
||||
case Symbol.NotEqual:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.NotEqual;
|
||||
return true;
|
||||
case Symbol.LessThan:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.LessThan;
|
||||
return true;
|
||||
case Symbol.LessThanOrEqual:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.LessThanOrEqual;
|
||||
return true;
|
||||
case Symbol.GreaterThan:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.GreaterThan;
|
||||
return true;
|
||||
case Symbol.GreaterThanOrEqual:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.GreaterThanOrEqual;
|
||||
return true;
|
||||
case Symbol.Plus:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.Plus;
|
||||
return true;
|
||||
case Symbol.Minus:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.Minus;
|
||||
return true;
|
||||
case Symbol.Star:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.Multiply;
|
||||
return true;
|
||||
case Symbol.ForwardSlash:
|
||||
binaryExpressionOperator = BinaryExpressionOperator.Divide;
|
||||
return true;
|
||||
default:
|
||||
binaryExpressionOperator = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ExpressionNode ParsePrimaryExpression()
|
||||
{
|
||||
var startIndex = _index;
|
||||
ExpressionNode expr;
|
||||
|
||||
var token = ExpectToken();
|
||||
switch (token)
|
||||
{
|
||||
case LiteralToken literal:
|
||||
{
|
||||
expr = new LiteralNode(GetTokensForNode(startIndex), literal.Value, literal.Kind);
|
||||
break;
|
||||
}
|
||||
case IdentifierToken identifier:
|
||||
{
|
||||
var @namespace = Optional<string>.Empty();
|
||||
var name = identifier.Value;
|
||||
if (TryExpectSymbol(Symbol.DoubleColon))
|
||||
{
|
||||
@namespace = identifier.Value;
|
||||
name = ExpectIdentifier().Value;
|
||||
}
|
||||
|
||||
expr = new IdentifierNode(GetTokensForNode(startIndex), @namespace, name);
|
||||
break;
|
||||
}
|
||||
case SymbolToken symbolToken:
|
||||
{
|
||||
switch (symbolToken.Symbol)
|
||||
{
|
||||
case Symbol.Func:
|
||||
{
|
||||
List<FuncParameter> parameters = [];
|
||||
ExpectSymbol(Symbol.OpenParen);
|
||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||
{
|
||||
var parameter = ParseFuncParameter();
|
||||
parameters.Add(parameter);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
var returnType = TryExpectSymbol(Symbol.Colon) ? ParseType() : new NubVoidType();
|
||||
|
||||
var body = ParseBlock();
|
||||
|
||||
expr = new AnonymousFuncNode(GetTokensForNode(startIndex), parameters, body, returnType);
|
||||
break;
|
||||
}
|
||||
case Symbol.OpenParen:
|
||||
{
|
||||
var expression = ParseExpression();
|
||||
ExpectSymbol(Symbol.CloseParen);
|
||||
expr = expression;
|
||||
break;
|
||||
}
|
||||
case Symbol.Ampersand:
|
||||
{
|
||||
var expression = ParsePrimaryExpression();
|
||||
if (expression is not LValueNode lValue)
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error("& symbol can only be used on lvalues")
|
||||
.At(expression)
|
||||
.Build());
|
||||
}
|
||||
|
||||
expr = new AddressOfNode(GetTokensForNode(startIndex), lValue);
|
||||
break;
|
||||
}
|
||||
case Symbol.Minus:
|
||||
{
|
||||
var expression = ParsePrimaryExpression();
|
||||
expr = new UnaryExpressionNode(GetTokensForNode(startIndex), UnaryExpressionOperator.Negate, expression);
|
||||
break;
|
||||
}
|
||||
case Symbol.Bang:
|
||||
{
|
||||
var expression = ParsePrimaryExpression();
|
||||
expr = new UnaryExpressionNode(GetTokensForNode(startIndex), UnaryExpressionOperator.Invert, expression);
|
||||
break;
|
||||
}
|
||||
case Symbol.OpenBracket:
|
||||
{
|
||||
if (Peek().TryGetValue(out var capacityToken) && capacityToken is LiteralToken { Kind: LiteralKind.Integer } literalToken)
|
||||
{
|
||||
var capacity = int.Parse(literalToken.Value);
|
||||
Next();
|
||||
ExpectSymbol(Symbol.CloseBracket);
|
||||
var elementType = ParseType();
|
||||
|
||||
if (capacity > 0)
|
||||
{
|
||||
expr = new FixedArrayInitializerNode(GetTokensForNode(startIndex), elementType, capacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error("Fixed array size must be a positive integer")
|
||||
.WithHelp("Use a positive integer literal for the array size")
|
||||
.At(literalToken)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var capacity = ParseExpression();
|
||||
ExpectSymbol(Symbol.CloseBracket);
|
||||
var type = ParseType();
|
||||
|
||||
expr = new ArrayInitializerNode(GetTokensForNode(startIndex), capacity, type);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Symbol.Alloc:
|
||||
{
|
||||
var type = ParseType();
|
||||
Dictionary<string, ExpressionNode> initializers = [];
|
||||
ExpectSymbol(Symbol.OpenBrace);
|
||||
while (!TryExpectSymbol(Symbol.CloseBrace))
|
||||
{
|
||||
var name = ExpectIdentifier().Value;
|
||||
ExpectSymbol(Symbol.Assign);
|
||||
var value = ParseExpression();
|
||||
initializers.Add(name, value);
|
||||
}
|
||||
|
||||
if (type is not NubStructType structType)
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Cannot alloc type '{type}'")
|
||||
.At(symbolToken)
|
||||
.Build());
|
||||
}
|
||||
|
||||
expr = new StructInitializerNode(GetTokensForNode(startIndex), structType, initializers);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Unexpected symbol '{symbolToken.Symbol}' in expression")
|
||||
.WithHelp("Expected literal, identifier, or '(' to start expression")
|
||||
.At(symbolToken)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
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 static ExpressionNode ParsePostfixOperators(int startIndex, ExpressionNode expr)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (TryExpectSymbol(Symbol.Caret))
|
||||
{
|
||||
expr = new DereferenceNode(GetTokensForNode(startIndex), expr);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryExpectSymbol(Symbol.Period))
|
||||
{
|
||||
var structMember = ExpectIdentifier().Value;
|
||||
expr = new MemberAccessNode(GetTokensForNode(startIndex), expr, structMember);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryExpectSymbol(Symbol.OpenBracket))
|
||||
{
|
||||
var index = ParseExpression();
|
||||
ExpectSymbol(Symbol.CloseBracket);
|
||||
expr = new ArrayIndexAccessNode(GetTokensForNode(startIndex), expr, index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryExpectSymbol(Symbol.OpenParen))
|
||||
{
|
||||
var parameters = new List<ExpressionNode>();
|
||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||
{
|
||||
parameters.Add(ParseExpression());
|
||||
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 FuncCallNode(GetTokensForNode(startIndex), expr, parameters);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private static BlockNode ParseBlock()
|
||||
{
|
||||
var startIndex = _index;
|
||||
ExpectSymbol(Symbol.OpenBrace);
|
||||
List<StatementNode> statements = [];
|
||||
while (Peek().HasValue && !TryExpectSymbol(Symbol.CloseBrace))
|
||||
{
|
||||
try
|
||||
{
|
||||
statements.Add(ParseStatement());
|
||||
}
|
||||
catch (ParseException ex)
|
||||
{
|
||||
_diagnostics.Add(ex.Diagnostic);
|
||||
RecoverToNextStatement();
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockNode(GetTokensForNode(startIndex), statements);
|
||||
}
|
||||
|
||||
private static NubType ParseType()
|
||||
{
|
||||
if (TryExpectIdentifier(out var name))
|
||||
{
|
||||
if (name.Value == "any")
|
||||
{
|
||||
return new NubAnyType();
|
||||
}
|
||||
|
||||
if (name.Value == "void")
|
||||
{
|
||||
return new NubVoidType();
|
||||
}
|
||||
|
||||
if (NubPrimitiveType.TryParse(name.Value, out var primitiveTypeKind))
|
||||
{
|
||||
return new NubPrimitiveType(primitiveTypeKind.Value);
|
||||
}
|
||||
|
||||
var @namespace = _namespace;
|
||||
if (TryExpectSymbol(Symbol.DoubleColon))
|
||||
{
|
||||
@namespace = ExpectIdentifier().Value;
|
||||
}
|
||||
|
||||
if (@namespace == null)
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Struct '{name.Value}' does not belong to a namespace")
|
||||
.WithHelp("Make sure you have specified a namespace at the top of the file")
|
||||
.At(name)
|
||||
.Build());
|
||||
}
|
||||
|
||||
return new NubStructType(@namespace , name.Value);
|
||||
}
|
||||
|
||||
if (TryExpectSymbol(Symbol.Caret))
|
||||
{
|
||||
var baseType = ParseType();
|
||||
return new NubPointerType(baseType);
|
||||
}
|
||||
|
||||
if (TryExpectSymbol(Symbol.Func))
|
||||
{
|
||||
ExpectSymbol(Symbol.OpenParen);
|
||||
List<NubType> parameters = [];
|
||||
while (!TryExpectSymbol(Symbol.CloseParen))
|
||||
{
|
||||
var parameter = ParseType();
|
||||
parameters.Add(parameter);
|
||||
if (!TryExpectSymbol(Symbol.Comma) && Peek().TryGetValue(out var nextToken) && nextToken is not SymbolToken { Symbol: Symbol.CloseParen })
|
||||
{
|
||||
_diagnostics.Add(Diagnostic
|
||||
.Warning("Missing comma between func type arguments")
|
||||
.WithHelp("Add a ',' to separate arguments")
|
||||
.At(nextToken)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
var returnType = TryExpectSymbol(Symbol.Colon) ? ParseType() : new NubVoidType();
|
||||
|
||||
return new NubFuncType(returnType, parameters);
|
||||
}
|
||||
|
||||
if (TryExpectSymbol(Symbol.OpenBracket))
|
||||
{
|
||||
if (Peek().TryGetValue(out var token) && token is LiteralToken { Kind: LiteralKind.Integer, Value: var sizeValue })
|
||||
{
|
||||
Next();
|
||||
ExpectSymbol(Symbol.CloseBracket);
|
||||
var baseType = ParseType();
|
||||
|
||||
var size = int.Parse(sizeValue);
|
||||
|
||||
if (size > 0)
|
||||
{
|
||||
return new NubFixedArrayType(baseType, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new UnreachableException();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ExpectSymbol(Symbol.CloseBracket);
|
||||
var baseType = ParseType();
|
||||
return new NubArrayType(baseType);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Peek().TryGetValue(out var peekToken))
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error("Unexpected end of file while parsing type")
|
||||
.WithHelp("Expected a type name")
|
||||
.At(_tokens.Last())
|
||||
.Build());
|
||||
}
|
||||
|
||||
throw new ParseException(Diagnostic
|
||||
.Error("Invalid type syntax")
|
||||
.WithHelp("Expected type name, '^' for pointer, or '[]' for array")
|
||||
.At(peekToken)
|
||||
.Build());
|
||||
}
|
||||
|
||||
private static Token ExpectToken()
|
||||
{
|
||||
if (!Peek().TryGetValue(out var token))
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error("Unexpected end of file")
|
||||
.WithHelp("Expected more tokens to complete the syntax")
|
||||
.At(_tokens.Last())
|
||||
.Build());
|
||||
}
|
||||
|
||||
Next();
|
||||
return token;
|
||||
}
|
||||
|
||||
private static SymbolToken ExpectSymbol()
|
||||
{
|
||||
var token = ExpectToken();
|
||||
if (token is not SymbolToken symbol)
|
||||
{
|
||||
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 static void ExpectSymbol(Symbol expectedSymbol)
|
||||
{
|
||||
var token = ExpectSymbol();
|
||||
if (token.Symbol != expectedSymbol)
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Expected '{expectedSymbol}', but found '{token.Symbol}'")
|
||||
.WithHelp($"Insert '{expectedSymbol}' here")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryExpectSymbol(Symbol symbol)
|
||||
{
|
||||
if (Peek() is { Value: SymbolToken symbolToken } && symbolToken.Symbol == symbol)
|
||||
{
|
||||
Next();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryExpectModifier([NotNullWhen(true)] out ModifierToken? modifier)
|
||||
{
|
||||
if (Peek() is { Value: ModifierToken modifierToken })
|
||||
{
|
||||
modifier = modifierToken;
|
||||
Next();
|
||||
return true;
|
||||
}
|
||||
|
||||
modifier = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryExpectIdentifier([NotNullWhen(true)] out IdentifierToken? identifier)
|
||||
{
|
||||
if (Peek() is { Value: IdentifierToken identifierToken })
|
||||
{
|
||||
identifier = identifierToken;
|
||||
Next();
|
||||
return true;
|
||||
}
|
||||
|
||||
identifier = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IdentifierToken ExpectIdentifier()
|
||||
{
|
||||
var token = ExpectToken();
|
||||
if (token is not IdentifierToken identifier)
|
||||
{
|
||||
throw new ParseException(Diagnostic
|
||||
.Error($"Expected identifier, but found {token.GetType().Name}")
|
||||
.WithHelp("Provide a valid identifier name here")
|
||||
.At(token)
|
||||
.Build());
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private static void RecoverToNextDefinition()
|
||||
{
|
||||
while (Peek().HasValue)
|
||||
{
|
||||
var token = Peek().Value;
|
||||
if (token is SymbolToken { Symbol: Symbol.Func or Symbol.Struct } or ModifierToken)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Next();
|
||||
}
|
||||
}
|
||||
|
||||
private static 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.Let or Symbol.Break or Symbol.Continue
|
||||
})
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Next();
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<Token> Peek(int offset = 0)
|
||||
{
|
||||
var peekIndex = _index + offset;
|
||||
while (peekIndex < _tokens.Count && _tokens[peekIndex] is DocumentationToken)
|
||||
{
|
||||
peekIndex++;
|
||||
}
|
||||
|
||||
if (peekIndex < _tokens.Count)
|
||||
{
|
||||
return _tokens[peekIndex];
|
||||
}
|
||||
|
||||
return Optional<Token>.Empty();
|
||||
}
|
||||
|
||||
private static void Next()
|
||||
{
|
||||
while (_index < _tokens.Count && _tokens[_index] is DocumentationToken)
|
||||
{
|
||||
_index++;
|
||||
}
|
||||
|
||||
_index++;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<Token> GetTokensForNode(int startIndex)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class ArrayIndexAssignmentNode(IReadOnlyList<Token> tokens, ArrayIndexAccessNode arrayIndexAccess, ExpressionNode value) : StatementNode(tokens)
|
||||
{
|
||||
public ArrayIndexAccessNode ArrayIndexAccess { get; } = arrayIndexAccess;
|
||||
public ExpressionNode Value { get; } = value;
|
||||
}
|
||||
8
src/Nub.Lang/Frontend/Parsing/Statements/BlockNode.cs
Normal file
8
src/Nub.Lang/Frontend/Parsing/Statements/BlockNode.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class BlockNode(IReadOnlyList<Token> tokens, List<StatementNode> statements) : Node(tokens)
|
||||
{
|
||||
public List<StatementNode> Statements { get; } = statements;
|
||||
}
|
||||
5
src/Nub.Lang/Frontend/Parsing/Statements/BreakNode.cs
Normal file
5
src/Nub.Lang/Frontend/Parsing/Statements/BreakNode.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class BreakNode(IReadOnlyList<Token> tokens) : StatementNode(tokens);
|
||||
5
src/Nub.Lang/Frontend/Parsing/Statements/ContinueNode.cs
Normal file
5
src/Nub.Lang/Frontend/Parsing/Statements/ContinueNode.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class ContinueNode(IReadOnlyList<Token> tokens) : StatementNode(tokens);
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class DereferenceAssignmentNode(IReadOnlyList<Token> tokens, DereferenceNode dereference, ExpressionNode value) : StatementNode(tokens)
|
||||
{
|
||||
public DereferenceNode Dereference { get; } = dereference;
|
||||
public ExpressionNode Value { get; } = value;
|
||||
}
|
||||
11
src/Nub.Lang/Frontend/Parsing/Statements/IfNode.cs
Normal file
11
src/Nub.Lang/Frontend/Parsing/Statements/IfNode.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class IfNode(IReadOnlyList<Token> tokens, ExpressionNode condition, BlockNode body, Optional<Variant<IfNode, BlockNode>> @else) : StatementNode(tokens)
|
||||
{
|
||||
public ExpressionNode Condition { get; } = condition;
|
||||
public BlockNode Body { get; } = body;
|
||||
public Optional<Variant<IfNode, BlockNode>> Else { get; } = @else;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class MemberAssignmentNode(IReadOnlyList<Token> tokens, MemberAccessNode expression, ExpressionNode value) : StatementNode(tokens)
|
||||
{
|
||||
public MemberAccessNode MemberAccess { get; } = expression;
|
||||
public ExpressionNode Value { get; } = value;
|
||||
}
|
||||
9
src/Nub.Lang/Frontend/Parsing/Statements/ReturnNode.cs
Normal file
9
src/Nub.Lang/Frontend/Parsing/Statements/ReturnNode.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class ReturnNode(IReadOnlyList<Token> tokens, Optional<ExpressionNode> value) : StatementNode(tokens)
|
||||
{
|
||||
public Optional<ExpressionNode> Value { get; } = value;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class StatementExpressionNode(IReadOnlyList<Token> tokens, ExpressionNode expression) : StatementNode(tokens)
|
||||
{
|
||||
public ExpressionNode Expression { get; } = expression;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public abstract class StatementNode(IReadOnlyList<Token> tokens) : Node(tokens);
|
||||
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class VariableAssignmentNode(IReadOnlyList<Token> tokens, IdentifierNode identifier, ExpressionNode value) : StatementNode(tokens)
|
||||
{
|
||||
public IdentifierNode Identifier { get; } = identifier;
|
||||
public ExpressionNode Value { get; } = value;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
using Nub.Lang.Frontend.Typing;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class VariableDeclarationNode(IReadOnlyList<Token> tokens, string name, Optional<NubType> explicitType, Optional<ExpressionNode> value) : StatementNode(tokens)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public Optional<NubType> ExplicitType { get; } = explicitType;
|
||||
public Optional<ExpressionNode> Value { get; } = value;
|
||||
}
|
||||
10
src/Nub.Lang/Frontend/Parsing/Statements/WhileNode.cs
Normal file
10
src/Nub.Lang/Frontend/Parsing/Statements/WhileNode.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
|
||||
namespace Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
public class WhileNode(IReadOnlyList<Token> tokens, ExpressionNode condition, BlockNode body) : StatementNode(tokens)
|
||||
{
|
||||
public ExpressionNode Condition { get; } = condition;
|
||||
public BlockNode Body { get; } = body;
|
||||
}
|
||||
271
src/Nub.Lang/Frontend/Typing/NubType.cs
Normal file
271
src/Nub.Lang/Frontend/Typing/NubType.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Nub.Lang.Frontend.Typing;
|
||||
|
||||
public abstract class NubType
|
||||
{
|
||||
public static bool IsCompatibleWith(NubType sourceType, NubType targetType)
|
||||
{
|
||||
if (targetType is NubAnyType || sourceType.Equals(targetType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sourceType is NubFixedArrayType fixedArray && targetType is NubArrayType array && IsCompatibleWith(fixedArray.ElementType, array.ElementType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsInteger => this is NubPrimitiveType
|
||||
{
|
||||
Kind: PrimitiveTypeKind.I8
|
||||
or PrimitiveTypeKind.I16
|
||||
or PrimitiveTypeKind.I32
|
||||
or PrimitiveTypeKind.I64
|
||||
or PrimitiveTypeKind.U8
|
||||
or PrimitiveTypeKind.U16
|
||||
or PrimitiveTypeKind.U32
|
||||
or PrimitiveTypeKind.U64
|
||||
};
|
||||
|
||||
public bool IsFloat32 => this is NubPrimitiveType
|
||||
{
|
||||
Kind: PrimitiveTypeKind.F32
|
||||
};
|
||||
|
||||
public bool IsFloat64 => this is NubPrimitiveType
|
||||
{
|
||||
Kind: PrimitiveTypeKind.F64
|
||||
};
|
||||
|
||||
public bool IsNumber => IsFloat32 || IsFloat64 || IsInteger;
|
||||
|
||||
public abstract override bool Equals(object? obj);
|
||||
public abstract override int GetHashCode();
|
||||
public abstract override string ToString();
|
||||
}
|
||||
|
||||
public class NubFuncType(NubType returnType, List<NubType> parameters) : NubType
|
||||
{
|
||||
public NubType ReturnType { get; } = returnType;
|
||||
public List<NubType> Parameters { get; } = parameters;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is NubFuncType other && other.ReturnType.Equals(ReturnType) && other.Parameters.SequenceEqual(Parameters);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(ReturnType, Parameters);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"func({string.Join(", ", Parameters)}): {ReturnType}";
|
||||
}
|
||||
}
|
||||
|
||||
public class NubStructType(string @namespace, string name) : NubType
|
||||
{
|
||||
public string Namespace { get; } = @namespace;
|
||||
public string Name { get; } = name;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is NubStructType other && other.Namespace == Namespace && other.Name == Name;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Namespace, Name);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Namespace}::{Name}";
|
||||
}
|
||||
}
|
||||
|
||||
public class NubPointerType(NubType baseType) : NubType
|
||||
{
|
||||
public NubType BaseType { get; } = baseType;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is NubPointerType other && BaseType.Equals(other.BaseType);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(BaseType);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "^" + BaseType;
|
||||
}
|
||||
}
|
||||
|
||||
public class NubArrayType(NubType elementType) : NubType
|
||||
{
|
||||
public NubType ElementType { get; } = elementType;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is NubArrayType other)
|
||||
{
|
||||
return ElementType.Equals(other.ElementType);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(ElementType);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "[]" + ElementType;
|
||||
}
|
||||
}
|
||||
|
||||
public class NubFixedArrayType(NubType elementType, int capacity) : NubType
|
||||
{
|
||||
public NubType ElementType { get; } = elementType;
|
||||
public int Capacity { get; } = capacity;
|
||||
|
||||
public override string ToString() => $"[{Capacity}]{ElementType}";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is NubFixedArrayType other && ElementType.Equals(other.ElementType) && Capacity == other.Capacity;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(ElementType, Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
public class NubAnyType : NubType
|
||||
{
|
||||
public override string ToString() => "any";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is NubAnyType;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetType().GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public class NubVoidType : NubType
|
||||
{
|
||||
public override string ToString() => "void";
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is NubVoidType;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetType().GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public class NubPrimitiveType(PrimitiveTypeKind kind) : NubType
|
||||
{
|
||||
public PrimitiveTypeKind Kind { get; } = kind;
|
||||
|
||||
public static NubPrimitiveType I64 => new(PrimitiveTypeKind.I64);
|
||||
public static NubPrimitiveType I32 => new(PrimitiveTypeKind.I32);
|
||||
public static NubPrimitiveType I16 => new(PrimitiveTypeKind.I16);
|
||||
public static NubPrimitiveType I8 => new(PrimitiveTypeKind.I8);
|
||||
|
||||
public static NubPrimitiveType U64 => new(PrimitiveTypeKind.U64);
|
||||
public static NubPrimitiveType U32 => new(PrimitiveTypeKind.U32);
|
||||
public static NubPrimitiveType U16 => new(PrimitiveTypeKind.U16);
|
||||
public static NubPrimitiveType U8 => new(PrimitiveTypeKind.U8);
|
||||
|
||||
public static NubPrimitiveType F64 => new(PrimitiveTypeKind.F64);
|
||||
public static NubPrimitiveType F32 => new(PrimitiveTypeKind.F32);
|
||||
|
||||
public static NubPrimitiveType Bool => new(PrimitiveTypeKind.Bool);
|
||||
|
||||
public static bool TryParse(string s, [NotNullWhen(true)] out PrimitiveTypeKind? kind)
|
||||
{
|
||||
kind = s switch
|
||||
{
|
||||
"i64" => PrimitiveTypeKind.I64,
|
||||
"i32" => PrimitiveTypeKind.I32,
|
||||
"i16" => PrimitiveTypeKind.I16,
|
||||
"i8" => PrimitiveTypeKind.I8,
|
||||
"u64" => PrimitiveTypeKind.U64,
|
||||
"u32" => PrimitiveTypeKind.U32,
|
||||
"u16" => PrimitiveTypeKind.U16,
|
||||
"u8" => PrimitiveTypeKind.U8,
|
||||
"f64" => PrimitiveTypeKind.F64,
|
||||
"f32" => PrimitiveTypeKind.F32,
|
||||
"bool" => PrimitiveTypeKind.Bool,
|
||||
_ => null
|
||||
};
|
||||
|
||||
return kind != null;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is NubPrimitiveType other && other.Kind == Kind;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Kind);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Kind switch
|
||||
{
|
||||
PrimitiveTypeKind.I8 => "i8",
|
||||
PrimitiveTypeKind.I16 => "i16",
|
||||
PrimitiveTypeKind.I32 => "i32",
|
||||
PrimitiveTypeKind.I64 => "i64",
|
||||
|
||||
PrimitiveTypeKind.U8 => "u8",
|
||||
PrimitiveTypeKind.U16 => "u16",
|
||||
PrimitiveTypeKind.U32 => "u32",
|
||||
PrimitiveTypeKind.U64 => "u64",
|
||||
|
||||
PrimitiveTypeKind.F32 => "f32",
|
||||
PrimitiveTypeKind.F64 => "f64",
|
||||
|
||||
PrimitiveTypeKind.Bool => "bool",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), Kind, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum PrimitiveTypeKind
|
||||
{
|
||||
I64,
|
||||
I32,
|
||||
I16,
|
||||
I8,
|
||||
U64,
|
||||
U32,
|
||||
U16,
|
||||
U8,
|
||||
F64,
|
||||
F32,
|
||||
Bool
|
||||
}
|
||||
670
src/Nub.Lang/Frontend/Typing/TypeChecker.cs
Normal file
670
src/Nub.Lang/Frontend/Typing/TypeChecker.cs
Normal file
@@ -0,0 +1,670 @@
|
||||
using System.Diagnostics;
|
||||
using Nub.Lang.Diagnostics;
|
||||
using Nub.Lang.Frontend.Lexing;
|
||||
using Nub.Lang.Frontend.Parsing;
|
||||
using Nub.Lang.Frontend.Parsing.Definitions;
|
||||
using Nub.Lang.Frontend.Parsing.Expressions;
|
||||
using Nub.Lang.Frontend.Parsing.Statements;
|
||||
|
||||
namespace Nub.Lang.Frontend.Typing;
|
||||
|
||||
public static class TypeChecker
|
||||
{
|
||||
private static CompilationUnit _compilationUnit = null!;
|
||||
private static DefinitionTable _definitionTable = null!;
|
||||
|
||||
private static Dictionary<string, NubType> _variables = new();
|
||||
private static List<Diagnostic> _diagnostics = [];
|
||||
private static NubType? _currentFunctionReturnType;
|
||||
private static Queue<AnonymousFuncNode> _anonymousFunctions = [];
|
||||
|
||||
public static DiagnosticsResult Check(CompilationUnit compilationUnit, DefinitionTable definitionTable)
|
||||
{
|
||||
_compilationUnit = compilationUnit;
|
||||
_definitionTable = definitionTable;
|
||||
|
||||
_variables = new Dictionary<string, NubType>();
|
||||
_diagnostics = [];
|
||||
_currentFunctionReturnType = null;
|
||||
_anonymousFunctions = [];
|
||||
|
||||
foreach (var structDef in compilationUnit.Definitions.OfType<StructDefinitionNode>())
|
||||
{
|
||||
CheckStructDef(structDef);
|
||||
}
|
||||
|
||||
foreach (var funcDef in compilationUnit.Definitions.OfType<LocalFuncDefinitionNode>())
|
||||
{
|
||||
CheckFuncDef(funcDef.Parameters, funcDef.Body, funcDef.ReturnType);
|
||||
}
|
||||
|
||||
while (_anonymousFunctions.TryDequeue(out var func))
|
||||
{
|
||||
CheckFuncDef(func.Parameters, func.Body, func.ReturnType);
|
||||
|
||||
}
|
||||
|
||||
return new DiagnosticsResult(_diagnostics);
|
||||
}
|
||||
|
||||
private static void CheckStructDef(StructDefinitionNode structDef)
|
||||
{
|
||||
var fields = new Dictionary<string, NubType>();
|
||||
foreach (var field in structDef.Fields)
|
||||
{
|
||||
if (fields.ContainsKey(field.Name))
|
||||
{
|
||||
ReportError($"Duplicate field '{field.Name}' in struct '{structDef.Name}'", structDef);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (field.Value.HasValue)
|
||||
{
|
||||
var fieldType = CheckExpression(field.Value.Value, field.Type);
|
||||
if (fieldType != null && !fieldType.Equals(field.Type))
|
||||
{
|
||||
ReportError("Default field initializer does not match the defined type", field.Value.Value);
|
||||
}
|
||||
}
|
||||
|
||||
fields[field.Name] = field.Type;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckFuncDef(List<FuncParameter> parameters, BlockNode body, NubType returnType)
|
||||
{
|
||||
_variables.Clear();
|
||||
_currentFunctionReturnType = returnType;
|
||||
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
_variables[param.Name] = param.Type;
|
||||
}
|
||||
|
||||
CheckBlock(body);
|
||||
}
|
||||
|
||||
private static void CheckBlock(BlockNode block)
|
||||
{
|
||||
foreach (var statement in block.Statements)
|
||||
{
|
||||
CheckStatement(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckStatement(StatementNode statement)
|
||||
{
|
||||
switch (statement)
|
||||
{
|
||||
case ArrayIndexAssignmentNode arrayIndexAssignment:
|
||||
CheckArrayIndexAssignment(arrayIndexAssignment);
|
||||
break;
|
||||
case VariableAssignmentNode variableAssignment:
|
||||
CheckVariableAssignment(variableAssignment);
|
||||
break;
|
||||
case VariableDeclarationNode variableDeclaration:
|
||||
CheckVariableVariableDeclaration(variableDeclaration);
|
||||
break;
|
||||
case IfNode ifNode:
|
||||
CheckIf(ifNode);
|
||||
break;
|
||||
case MemberAssignmentNode memberAssignment:
|
||||
CheckMemberAssignment(memberAssignment);
|
||||
break;
|
||||
case WhileNode whileNode:
|
||||
CheckWhile(whileNode);
|
||||
break;
|
||||
case ReturnNode returnNode:
|
||||
CheckReturn(returnNode);
|
||||
break;
|
||||
case StatementExpressionNode statementExpression:
|
||||
CheckExpression(statementExpression.Expression);
|
||||
break;
|
||||
case BreakNode:
|
||||
case ContinueNode:
|
||||
break;
|
||||
case DereferenceAssignmentNode dereferenceAssignment:
|
||||
CheckDereferenceAssignment(dereferenceAssignment);
|
||||
break;
|
||||
default:
|
||||
ReportError($"Unsupported statement type: {statement.GetType().Name}", statement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckMemberAssignment(MemberAssignmentNode memberAssignment)
|
||||
{
|
||||
var memberType = CheckExpression(memberAssignment.MemberAccess);
|
||||
if (memberType == null) return;
|
||||
var valueType = CheckExpression(memberAssignment.Value, memberType);
|
||||
if (valueType == null) return;
|
||||
|
||||
if (!NubType.IsCompatibleWith(memberType, valueType))
|
||||
{
|
||||
ReportError($"'{valueType}' is not assignable to member of type '{memberType}'", memberAssignment);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckArrayIndexAssignment(ArrayIndexAssignmentNode arrayIndexAssignment)
|
||||
{
|
||||
var itemType = CheckExpression(arrayIndexAssignment.ArrayIndexAccess);
|
||||
if (itemType == null) return;
|
||||
var valueType = CheckExpression(arrayIndexAssignment.Value, itemType);
|
||||
if (valueType == null) return;
|
||||
|
||||
if (!NubType.IsCompatibleWith(itemType, valueType))
|
||||
{
|
||||
ReportError($"'{valueType}' is not assignable to array of type '{itemType}'", arrayIndexAssignment);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckVariableAssignment(VariableAssignmentNode variableAssignment)
|
||||
{
|
||||
if (!_variables.TryGetValue(variableAssignment.Identifier.Name, out var variable))
|
||||
{
|
||||
ReportError($"Variable '{variableAssignment.Identifier}' is not declared", variableAssignment);
|
||||
return;
|
||||
}
|
||||
|
||||
var valueType = CheckExpression(variableAssignment.Value, variable);
|
||||
if (valueType == null) return;
|
||||
|
||||
if (!NubType.IsCompatibleWith(variableAssignment.Value.Type, variable))
|
||||
{
|
||||
ReportError($"Cannot assign expression of type '{variableAssignment.Value.Type}' to variable '{variableAssignment.Identifier}' with type '{variable}'", variableAssignment);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckVariableVariableDeclaration(VariableDeclarationNode variableDeclaration)
|
||||
{
|
||||
NubType? type = null;
|
||||
|
||||
if (_variables.TryGetValue(variableDeclaration.Name, out var variable))
|
||||
{
|
||||
ReportError($"Cannot redeclare variable '{variable}'", variableDeclaration);
|
||||
}
|
||||
|
||||
if (variableDeclaration.Value.HasValue)
|
||||
{
|
||||
var valueType = CheckExpression(variableDeclaration.Value.Value, variableDeclaration.ExplicitType.Value);
|
||||
if (valueType == null) return;
|
||||
type = valueType;
|
||||
}
|
||||
|
||||
if (variableDeclaration.ExplicitType.HasValue)
|
||||
{
|
||||
type = variableDeclaration.ExplicitType.Value;
|
||||
}
|
||||
|
||||
if (variableDeclaration.ExplicitType.HasValue && variableDeclaration.Value.HasValue)
|
||||
{
|
||||
if (!NubType.IsCompatibleWith(variableDeclaration.ExplicitType.Value, variableDeclaration.Value.Value.Type))
|
||||
{
|
||||
ReportError(
|
||||
$"Cannot assign expression of type '{variableDeclaration.Value.Value.Type}' to variable '{variableDeclaration.Name}' with type '{variableDeclaration.ExplicitType.Value}'",
|
||||
variableDeclaration);
|
||||
}
|
||||
}
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
ReportError($"Cannot implicitly get type of variable '{variableDeclaration.Name}'", variableDeclaration);
|
||||
return;
|
||||
}
|
||||
|
||||
_variables[variableDeclaration.Name] = type;
|
||||
}
|
||||
|
||||
private static NubType? CheckDereference(DereferenceNode dereference)
|
||||
{
|
||||
var exprType = CheckExpression(dereference.Expression);
|
||||
if (exprType == null) return null;
|
||||
|
||||
if (exprType is not NubPointerType nubPointerType)
|
||||
{
|
||||
ReportError($"Cannot dereference a non-pointer type {exprType}", dereference);
|
||||
return null;
|
||||
}
|
||||
|
||||
return nubPointerType.BaseType;
|
||||
}
|
||||
|
||||
private static NubType CheckFixedInitializerArray(FixedArrayInitializerNode fixedArrayInitializer)
|
||||
{
|
||||
return new NubFixedArrayType(fixedArrayInitializer.ElementType, fixedArrayInitializer.Capacity);
|
||||
}
|
||||
|
||||
private static NubType? CheckFuncCall(FuncCallNode funcCall)
|
||||
{
|
||||
var identType = CheckExpression(funcCall.Expression);
|
||||
if (identType is not NubFuncType funcType)
|
||||
{
|
||||
ReportError("Cannot call function on non-function type", funcCall);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (funcCall.Parameters.Count != funcType.Parameters.Count)
|
||||
{
|
||||
ReportError($"{funcType} expects {funcType.Parameters.Count} arguments, but was called with {funcType.Parameters.Count} arguments", funcCall);
|
||||
}
|
||||
|
||||
for (var i = 0; i < funcCall.Parameters.Count; i++)
|
||||
{
|
||||
var parameter = funcCall.Parameters[i];
|
||||
var parameterType = CheckExpression(parameter);
|
||||
if (parameterType == null) return null;
|
||||
|
||||
if (!NubType.IsCompatibleWith(parameterType, funcType.Parameters[i]))
|
||||
{
|
||||
ReportError($"'{parameterType}' does not match expected type {funcType.Parameters[i]}", funcCall);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return funcType.ReturnType;
|
||||
}
|
||||
|
||||
private static void CheckIf(IfNode ifNode)
|
||||
{
|
||||
var conditionType = CheckExpression(ifNode.Condition, NubPrimitiveType.Bool);
|
||||
if (conditionType != null && !conditionType.Equals(NubPrimitiveType.Bool))
|
||||
{
|
||||
ReportError($"If condition must be a boolean expression, got '{conditionType}'", ifNode.Condition);
|
||||
}
|
||||
|
||||
CheckBlock(ifNode.Body);
|
||||
|
||||
if (ifNode.Else.HasValue)
|
||||
{
|
||||
var elseValue = ifNode.Else.Value;
|
||||
elseValue.Match(CheckIf, CheckBlock);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckWhile(WhileNode whileNode)
|
||||
{
|
||||
var conditionType = CheckExpression(whileNode.Condition, NubPrimitiveType.Bool);
|
||||
if (conditionType != null && !conditionType.Equals(NubPrimitiveType.Bool))
|
||||
{
|
||||
ReportError($"While condition must be a boolean expression, got '{conditionType}'", whileNode.Condition);
|
||||
}
|
||||
|
||||
CheckBlock(whileNode.Body);
|
||||
}
|
||||
|
||||
private static void CheckReturn(ReturnNode returnNode)
|
||||
{
|
||||
if (returnNode.Value.HasValue)
|
||||
{
|
||||
var returnType = CheckExpression(returnNode.Value.Value, _currentFunctionReturnType);
|
||||
if (returnType == null) return;
|
||||
|
||||
if (_currentFunctionReturnType == null)
|
||||
{
|
||||
ReportError("Cannot return a value from a function with no return type", returnNode.Value.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NubType.IsCompatibleWith(returnType, _currentFunctionReturnType))
|
||||
{
|
||||
ReportError($"Return value of type '{returnType}' is not compatible with function return type '{_currentFunctionReturnType}'", returnNode.Value.Value);
|
||||
}
|
||||
}
|
||||
else if (_currentFunctionReturnType != null)
|
||||
{
|
||||
ReportError($"Function must return a value of type '{_currentFunctionReturnType}'", returnNode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckDereferenceAssignment(DereferenceAssignmentNode dereferenceAssignment)
|
||||
{
|
||||
var dereferenceType = CheckExpression(dereferenceAssignment.Dereference);
|
||||
if (dereferenceType == null) return;
|
||||
var valueType = CheckExpression(dereferenceAssignment.Value, dereferenceType);
|
||||
if (valueType == null) return;
|
||||
|
||||
if (!NubType.IsCompatibleWith(dereferenceType, valueType))
|
||||
{
|
||||
ReportError($"'{valueType}' is not assignable to type '{dereferenceType}'", dereferenceAssignment);
|
||||
}
|
||||
}
|
||||
|
||||
private static NubType? CheckExpression(ExpressionNode expression, NubType? expectedType = null)
|
||||
{
|
||||
var resultType = expression switch
|
||||
{
|
||||
AddressOfNode addressOf => CheckAddressOf(addressOf),
|
||||
AnonymousFuncNode anonymousFunc => CheckAnonymousFunc(anonymousFunc),
|
||||
ArrayIndexAccessNode arrayIndex => CheckArrayIndex(arrayIndex),
|
||||
ArrayInitializerNode arrayInitializer => CheckArrayInitializer(arrayInitializer),
|
||||
LiteralNode literal => CheckLiteral(literal, expectedType),
|
||||
IdentifierNode identifier => CheckIdentifier(identifier),
|
||||
BinaryExpressionNode binaryExpr => CheckBinaryExpression(binaryExpr),
|
||||
DereferenceNode dereference => CheckDereference(dereference),
|
||||
FixedArrayInitializerNode fixedArray => CheckFixedInitializerArray(fixedArray),
|
||||
FuncCallNode funcCallExpr => CheckFuncCall(funcCallExpr),
|
||||
StructInitializerNode structInit => CheckStructInitializer(structInit),
|
||||
UnaryExpressionNode unaryExpression => CheckUnaryExpression(unaryExpression),
|
||||
MemberAccessNode memberAccess => CheckMemberAccess(memberAccess),
|
||||
_ => throw new UnreachableException()
|
||||
};
|
||||
|
||||
if (resultType != null)
|
||||
{
|
||||
expression.Type = resultType;
|
||||
}
|
||||
|
||||
return resultType;
|
||||
}
|
||||
|
||||
private static NubType CheckAnonymousFunc(AnonymousFuncNode anonymousFunc)
|
||||
{
|
||||
_anonymousFunctions.Enqueue(anonymousFunc);
|
||||
return new NubFuncType(anonymousFunc.ReturnType, anonymousFunc.Parameters.Select(p => p.Type).ToList());
|
||||
}
|
||||
|
||||
private static NubType? CheckLiteral(LiteralNode literal, NubType? expectedType = null)
|
||||
{
|
||||
if (expectedType != null)
|
||||
{
|
||||
if (expectedType.IsNumber && literal.Kind is not LiteralKind.Integer and not LiteralKind.Float)
|
||||
{
|
||||
ReportError("Expression expects a numeric literal", literal);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expectedType.IsInteger && literal.Kind == LiteralKind.Float)
|
||||
{
|
||||
if (literal.Kind == LiteralKind.Float)
|
||||
{
|
||||
ReportWarning("Float used in integer context. Everything after the '.' will be ignored", literal);
|
||||
}
|
||||
}
|
||||
|
||||
return expectedType;
|
||||
}
|
||||
|
||||
return literal.Kind switch
|
||||
{
|
||||
LiteralKind.Integer => NubPrimitiveType.I64,
|
||||
LiteralKind.Float => NubPrimitiveType.F64,
|
||||
LiteralKind.String => new NubArrayType(NubPrimitiveType.U8),
|
||||
LiteralKind.Bool => NubPrimitiveType.Bool,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
private static NubType? CheckArrayIndex(ArrayIndexAccessNode arrayIndexAccess)
|
||||
{
|
||||
var expressionType = CheckExpression(arrayIndexAccess.Array);
|
||||
if (expressionType == null) return null;
|
||||
var indexType = CheckExpression(arrayIndexAccess.Index, NubPrimitiveType.U64);
|
||||
if (indexType is { IsInteger: false })
|
||||
{
|
||||
ReportError("Array index type must be a number", arrayIndexAccess.Index);
|
||||
}
|
||||
|
||||
if (expressionType is NubArrayType arrayType)
|
||||
{
|
||||
return arrayType.ElementType;
|
||||
}
|
||||
|
||||
if (expressionType is NubFixedArrayType fixedArrayType)
|
||||
{
|
||||
return fixedArrayType.ElementType;
|
||||
}
|
||||
|
||||
ReportError($"Cannot access index of non-array type {expressionType}", arrayIndexAccess.Array);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static NubType CheckArrayInitializer(ArrayInitializerNode arrayInitializer)
|
||||
{
|
||||
var capacityType = CheckExpression(arrayInitializer.Capacity, NubPrimitiveType.U64);
|
||||
if (capacityType is { IsInteger: false })
|
||||
{
|
||||
ReportError("Array capacity type must be an integer", arrayInitializer.Capacity);
|
||||
}
|
||||
|
||||
return new NubArrayType(arrayInitializer.ElementType);
|
||||
}
|
||||
|
||||
private static NubType? CheckIdentifier(IdentifierNode identifier)
|
||||
{
|
||||
var definition = _definitionTable.LookupFunc(identifier.Namespace.Or(_compilationUnit.Namespace), identifier.Name);
|
||||
if (definition.HasValue)
|
||||
{
|
||||
return new NubFuncType(definition.Value.ReturnType, definition.Value.Parameters.Select(p => p.Type).ToList());
|
||||
}
|
||||
|
||||
if (!identifier.Namespace.HasValue)
|
||||
{
|
||||
return _variables[identifier.Name];
|
||||
}
|
||||
|
||||
ReportError($"Identifier '{identifier}' not found", identifier);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static NubType? CheckAddressOf(AddressOfNode addressOf)
|
||||
{
|
||||
var exprType = CheckExpression(addressOf.Expression);
|
||||
if (exprType == null) return null;
|
||||
|
||||
if (addressOf.Expression is not (IdentifierNode or MemberAccessNode))
|
||||
{
|
||||
ReportError($"Cannot take the address of {exprType}", addressOf.Expression);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new NubPointerType(exprType);
|
||||
}
|
||||
|
||||
private static NubType? CheckBinaryExpression(BinaryExpressionNode binaryExpr)
|
||||
{
|
||||
var leftType = CheckExpression(binaryExpr.Left);
|
||||
var rightType = CheckExpression(binaryExpr.Right);
|
||||
|
||||
if (leftType == null || rightType == null) return null;
|
||||
|
||||
if (!leftType.Equals(rightType))
|
||||
{
|
||||
ReportError($"Left '{leftType}' and right '{rightType}' side of the binary expression must be the same type", binaryExpr);
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (binaryExpr.Operator)
|
||||
{
|
||||
case BinaryExpressionOperator.Equal:
|
||||
case BinaryExpressionOperator.NotEqual:
|
||||
return NubPrimitiveType.Bool;
|
||||
case BinaryExpressionOperator.GreaterThan:
|
||||
case BinaryExpressionOperator.GreaterThanOrEqual:
|
||||
case BinaryExpressionOperator.LessThan:
|
||||
case BinaryExpressionOperator.LessThanOrEqual:
|
||||
if (!IsNumeric(leftType))
|
||||
{
|
||||
ReportError($"Comparison operators require numeric operands, got '{leftType}' and '{rightType}'", binaryExpr);
|
||||
return null;
|
||||
}
|
||||
|
||||
return NubPrimitiveType.Bool;
|
||||
case BinaryExpressionOperator.Plus:
|
||||
case BinaryExpressionOperator.Minus:
|
||||
case BinaryExpressionOperator.Multiply:
|
||||
case BinaryExpressionOperator.Divide:
|
||||
if (!IsNumeric(leftType))
|
||||
{
|
||||
ReportError($"Arithmetic operators require numeric operands, got '{leftType}' and '{rightType}'", binaryExpr);
|
||||
return null;
|
||||
}
|
||||
|
||||
return leftType;
|
||||
default:
|
||||
ReportError($"Unsupported binary operator: {binaryExpr.Operator}", binaryExpr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static NubType? CheckStructInitializer(StructInitializerNode structInit)
|
||||
{
|
||||
var initialized = new HashSet<string>();
|
||||
|
||||
var defOpt = _definitionTable.LookupStruct(structInit.StructType.Namespace, structInit.StructType.Name);
|
||||
if (!defOpt.TryGetValue(out var definition))
|
||||
{
|
||||
ReportError($"Struct type '{structInit.StructType.Name}' is not defined", structInit);
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var initializer in structInit.Initializers)
|
||||
{
|
||||
var definitionField = definition.Fields.FirstOrDefault(f => f.Name == initializer.Key);
|
||||
if (definitionField == null)
|
||||
{
|
||||
ReportError($"Field '{initializer.Key}' does not exist in struct '{structInit.StructType.Name}'", initializer.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
var initializerType = CheckExpression(initializer.Value, definitionField.Type);
|
||||
if (initializerType != null && !NubType.IsCompatibleWith(initializerType, definitionField.Type))
|
||||
{
|
||||
ReportError($"Cannot initialize field '{initializer.Key}' of type '{definitionField.Type}' with expression of type '{initializerType}'", initializer.Value);
|
||||
}
|
||||
|
||||
initialized.Add(initializer.Key);
|
||||
}
|
||||
|
||||
foreach (var field in definition.Fields.Where(f => f.Value.HasValue))
|
||||
{
|
||||
initialized.Add(field.Name);
|
||||
}
|
||||
|
||||
foreach (var field in definition.Fields)
|
||||
{
|
||||
if (!initialized.Contains(field.Name))
|
||||
{
|
||||
ReportError($"Struct field '{field.Name}' is not initialized on type '{structInit.StructType.Name}'", structInit);
|
||||
}
|
||||
}
|
||||
|
||||
return structInit.StructType;
|
||||
}
|
||||
|
||||
private static NubType? CheckUnaryExpression(UnaryExpressionNode unaryExpression)
|
||||
{
|
||||
var operandType = CheckExpression(unaryExpression.Operand);
|
||||
if (operandType == null) return null;
|
||||
|
||||
switch (unaryExpression.Operator)
|
||||
{
|
||||
case UnaryExpressionOperator.Negate:
|
||||
{
|
||||
if (operandType.Equals(NubPrimitiveType.I8) ||
|
||||
operandType.Equals(NubPrimitiveType.I16) ||
|
||||
operandType.Equals(NubPrimitiveType.I32) ||
|
||||
operandType.Equals(NubPrimitiveType.I64) ||
|
||||
operandType.Equals(NubPrimitiveType.F32) ||
|
||||
operandType.Equals(NubPrimitiveType.F64))
|
||||
{
|
||||
return operandType;
|
||||
}
|
||||
|
||||
ReportError($"Cannot negate non-numeric type {operandType}", unaryExpression.Operand);
|
||||
return null;
|
||||
}
|
||||
case UnaryExpressionOperator.Invert:
|
||||
{
|
||||
if (!operandType.Equals(NubPrimitiveType.Bool))
|
||||
{
|
||||
ReportError($"Cannot invert non-boolean type {operandType}", unaryExpression.Operand);
|
||||
return null;
|
||||
}
|
||||
|
||||
return operandType;
|
||||
}
|
||||
default:
|
||||
{
|
||||
ReportError($"Unsupported unary operator: {unaryExpression.Operator}", unaryExpression);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static NubType? CheckMemberAccess(MemberAccessNode memberAccess)
|
||||
{
|
||||
var expressionType = CheckExpression(memberAccess.Expression);
|
||||
if (expressionType == null) return null;
|
||||
|
||||
switch (expressionType)
|
||||
{
|
||||
case NubArrayType:
|
||||
{
|
||||
if (memberAccess.Member == "count")
|
||||
{
|
||||
return NubPrimitiveType.I64;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case NubStructType structType:
|
||||
{
|
||||
var defOpt = _definitionTable.LookupStruct(structType.Namespace, structType.Name);
|
||||
if (!defOpt.TryGetValue(out var definition))
|
||||
{
|
||||
ReportError($"Struct type '{structType.Name}' is not defined", memberAccess);
|
||||
return null;
|
||||
}
|
||||
|
||||
var field = definition.Fields.FirstOrDefault(f => f.Name == memberAccess.Member);
|
||||
if (field == null)
|
||||
{
|
||||
ReportError($"Field '{memberAccess.Member}' does not exist in struct '{structType.Name}'", memberAccess);
|
||||
return null;
|
||||
}
|
||||
|
||||
return field.Type;
|
||||
}
|
||||
}
|
||||
|
||||
ReportError($"Cannot access member '{memberAccess.Member}' on type '{expressionType}'", memberAccess);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ReportError(string message, Node node)
|
||||
{
|
||||
var diagnostic = Diagnostic.Error(message).At(node).Build();
|
||||
_diagnostics.Add(diagnostic);
|
||||
}
|
||||
|
||||
private static void ReportWarning(string message, Node node)
|
||||
{
|
||||
var diagnostic = Diagnostic.Warning(message).At(node).Build();
|
||||
_diagnostics.Add(diagnostic);
|
||||
}
|
||||
|
||||
private static bool IsNumeric(NubType type)
|
||||
{
|
||||
if (type is not NubPrimitiveType primitiveType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (primitiveType.Kind)
|
||||
{
|
||||
case PrimitiveTypeKind.I8:
|
||||
case PrimitiveTypeKind.I16:
|
||||
case PrimitiveTypeKind.I32:
|
||||
case PrimitiveTypeKind.I64:
|
||||
case PrimitiveTypeKind.U8:
|
||||
case PrimitiveTypeKind.U16:
|
||||
case PrimitiveTypeKind.U32:
|
||||
case PrimitiveTypeKind.U64:
|
||||
case PrimitiveTypeKind.F32:
|
||||
case PrimitiveTypeKind.F64:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/Nub.Lang/Nub.Lang.csproj
Normal file
10
src/Nub.Lang/Nub.Lang.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
77
src/Nub.Lang/Optional.cs
Normal file
77
src/Nub.Lang/Optional.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Nub.Lang;
|
||||
|
||||
public readonly struct Optional
|
||||
{
|
||||
public static Optional<TValue> Empty<TValue>() => new();
|
||||
|
||||
/// <summary>
|
||||
/// Alias for creating an Optional<TValue> which allows for implicit types
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static Optional<TValue> OfNullable<TValue>(TValue? value)
|
||||
{
|
||||
return value ?? Optional<TValue>.Empty();
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct Optional<TValue>
|
||||
{
|
||||
public static Optional<TValue> Empty() => new();
|
||||
|
||||
public static Optional<TValue> OfNullable(TValue? value)
|
||||
{
|
||||
return value ?? Empty();
|
||||
}
|
||||
|
||||
public Optional()
|
||||
{
|
||||
Value = default;
|
||||
HasValue = false;
|
||||
}
|
||||
|
||||
public Optional(TValue value)
|
||||
{
|
||||
Value = value;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public TValue? Value { get; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Value))]
|
||||
public bool HasValue { get; }
|
||||
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Value))]
|
||||
public bool TryGetValue([NotNullWhen(true)] out TValue? value)
|
||||
{
|
||||
if (HasValue)
|
||||
{
|
||||
value = Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public TValue GetValue()
|
||||
{
|
||||
return Value ?? throw new InvalidOperationException("Value is not set");
|
||||
}
|
||||
|
||||
public static implicit operator Optional<TValue>(TValue value) => new(value);
|
||||
|
||||
public TValue Or(TValue other)
|
||||
{
|
||||
if (HasValue)
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
|
||||
return other;
|
||||
}
|
||||
}
|
||||
274
src/Nub.Lang/Source.cs
Normal file
274
src/Nub.Lang/Source.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Nub.Lang;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a location in source code with line and column information.
|
||||
/// Lines and columns are 1-based to match typical editor conventions.
|
||||
/// </summary>
|
||||
public readonly struct SourceLocation : IEquatable<SourceLocation>, IComparable<SourceLocation>
|
||||
{
|
||||
public SourceLocation(int line, int column)
|
||||
{
|
||||
if (line < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(line), "Line must be >= 1");
|
||||
}
|
||||
|
||||
if (column < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(column), "Column must be >= 1");
|
||||
}
|
||||
|
||||
Line = line;
|
||||
Column = column;
|
||||
}
|
||||
|
||||
public int Line { get; }
|
||||
public int Column { get; }
|
||||
|
||||
public int CompareTo(SourceLocation other)
|
||||
{
|
||||
var lineComparison = Line.CompareTo(other.Line);
|
||||
if (lineComparison == 0)
|
||||
{
|
||||
return Column.CompareTo(other.Column);
|
||||
}
|
||||
|
||||
return lineComparison;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Line}:{Column}";
|
||||
}
|
||||
|
||||
public bool Equals(SourceLocation other)
|
||||
{
|
||||
return Line == other.Line && Column == other.Column;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceLocation other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Line, Column);
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceLocation left, SourceLocation right) => left.Equals(right);
|
||||
public static bool operator !=(SourceLocation left, SourceLocation right) => !left.Equals(right);
|
||||
public static bool operator <(SourceLocation left, SourceLocation right) => left.CompareTo(right) < 0;
|
||||
public static bool operator >(SourceLocation left, SourceLocation right) => left.CompareTo(right) > 0;
|
||||
public static bool operator <=(SourceLocation left, SourceLocation right) => left.CompareTo(right) <= 0;
|
||||
public static bool operator >=(SourceLocation left, SourceLocation right) => left.CompareTo(right) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents source text with a name (typically filename) and content.
|
||||
/// Equality is based on both name and content for better semantics.
|
||||
/// </summary>
|
||||
public struct SourceText : IEquatable<SourceText>
|
||||
{
|
||||
private int _lines = -1;
|
||||
|
||||
public SourceText(string path, string content)
|
||||
{
|
||||
Path = path ?? throw new ArgumentNullException(nameof(path));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public string Content { get; }
|
||||
|
||||
public int LineCount()
|
||||
{
|
||||
if (_lines == -1)
|
||||
{
|
||||
_lines = Content.Split('\n').Length + 1;
|
||||
}
|
||||
|
||||
return _lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific line from the source text (1-based).
|
||||
/// </summary>
|
||||
public string GetLine(int lineNumber)
|
||||
{
|
||||
if (lineNumber < 1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(lineNumber));
|
||||
}
|
||||
|
||||
var lines = Content.Split('\n');
|
||||
return lineNumber <= lines.Length ? lines[lineNumber - 1] : string.Empty;
|
||||
}
|
||||
|
||||
public bool Equals(SourceText other)
|
||||
{
|
||||
return Path == other.Path && Content == other.Content;
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceText other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Path, Content);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Path;
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceText left, SourceText right) => left.Equals(right);
|
||||
public static bool operator !=(SourceText left, SourceText right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a span of source code from a start to end location within a source text.
|
||||
/// </summary>
|
||||
public readonly struct SourceSpan : IEquatable<SourceSpan>
|
||||
{
|
||||
public SourceSpan(SourceText text, SourceLocation start, SourceLocation end)
|
||||
{
|
||||
if (start > end)
|
||||
{
|
||||
throw new ArgumentException("Start location cannot be after end location");
|
||||
}
|
||||
|
||||
if (end.Line > text.LineCount() || end.Line == text.LineCount() && end.Column > text.GetLine(text.LineCount()).Length + 1)
|
||||
{
|
||||
throw new ArgumentException("End location cannot be after text end location");
|
||||
}
|
||||
|
||||
Text = text;
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
public SourceText Text { get; }
|
||||
public SourceLocation Start { get; }
|
||||
public SourceLocation End { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this span represents a single point (start == end).
|
||||
/// </summary>
|
||||
public bool IsEmpty => Start == End;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this span is contained within a single line.
|
||||
/// </summary>
|
||||
public bool IsSingleLine => Start.Line == End.Line;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text content covered by this span.
|
||||
/// </summary>
|
||||
public string GetText()
|
||||
{
|
||||
if (IsEmpty)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var lines = Text.Content.Split('\n');
|
||||
|
||||
if (IsSingleLine)
|
||||
{
|
||||
var line = lines[Start.Line - 1];
|
||||
var startCol = Math.Min(Start.Column - 1, line.Length);
|
||||
var endCol = Math.Min(End.Column - 1, line.Length);
|
||||
return line.Substring(startCol, Math.Max(0, endCol - startCol));
|
||||
}
|
||||
|
||||
var result = new List<string>();
|
||||
for (var i = Start.Line - 1; i < Math.Min(End.Line, lines.Length); i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
if (i == Start.Line - 1)
|
||||
{
|
||||
result.Add(line[Math.Min(Start.Column - 1, line.Length)..]);
|
||||
}
|
||||
else if (i == End.Line - 1)
|
||||
{
|
||||
result.Add(line[..Math.Min(End.Column - 1, line.Length)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join("\n", result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges multiple source spans from the same file into a single span.
|
||||
/// The result spans from the earliest start to the latest end.
|
||||
/// </summary>
|
||||
public static SourceSpan Merge(IEnumerable<SourceSpan> spans)
|
||||
{
|
||||
var spanArray = spans.ToArray();
|
||||
if (spanArray.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Cannot merge empty collection of spans", nameof(spans));
|
||||
}
|
||||
|
||||
var firstText = spanArray[0].Text;
|
||||
if (spanArray.Any(s => !s.Text.Equals(firstText)))
|
||||
{
|
||||
throw new ArgumentException("All spans must be from the same source text", nameof(spans));
|
||||
}
|
||||
|
||||
var minStart = spanArray.Min(s => s.Start);
|
||||
var maxEnd = spanArray.Max(s => s.End);
|
||||
|
||||
return new SourceSpan(firstText, minStart, maxEnd);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (IsEmpty)
|
||||
{
|
||||
return $"{Start}";
|
||||
}
|
||||
|
||||
if (IsSingleLine)
|
||||
{
|
||||
return Start.Column == End.Column ? $"{Start}" : $"{Start.Line}:{Start.Column}-{End.Column}";
|
||||
}
|
||||
|
||||
return $"{Start}-{End}";
|
||||
}
|
||||
|
||||
public bool Equals(SourceSpan other)
|
||||
{
|
||||
return Text.Equals(other.Text) && Start.Equals(other.Start) && End.Equals(other.End);
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is SourceSpan other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Text, Start, End);
|
||||
}
|
||||
|
||||
public static bool operator ==(SourceSpan left, SourceSpan right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SourceSpan left, SourceSpan right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
49
src/Nub.Lang/Variant.cs
Normal file
49
src/Nub.Lang/Variant.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace Nub.Lang;
|
||||
|
||||
public readonly struct Variant<T1, T2> where T1 : notnull where T2 : notnull
|
||||
{
|
||||
public Variant()
|
||||
{
|
||||
throw new InvalidOperationException("Variant must be initialized with a value");
|
||||
}
|
||||
|
||||
public Variant(T1 value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public Variant(T2 value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
private readonly object _value;
|
||||
|
||||
public void Match(Action<T1> on1, Action<T2> on2)
|
||||
{
|
||||
switch (_value)
|
||||
{
|
||||
case T1 v1:
|
||||
on1(v1);
|
||||
break;
|
||||
case T2 v2:
|
||||
on2(v2);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidCastException();
|
||||
}
|
||||
}
|
||||
|
||||
public T Match<T>(Func<T1, T> on1, Func<T2, T> on2)
|
||||
{
|
||||
return _value switch
|
||||
{
|
||||
T1 v1 => on1(v1),
|
||||
T2 v2 => on2(v2),
|
||||
_ => throw new InvalidCastException()
|
||||
};
|
||||
}
|
||||
|
||||
public static implicit operator Variant<T1, T2>(T1 value) => new(value);
|
||||
public static implicit operator Variant<T1, T2>(T2 value) => new(value);
|
||||
}
|
||||
Reference in New Issue
Block a user