This commit is contained in:
nub31
2025-05-27 10:22:51 +02:00
parent b182b100cf
commit f659cfa7fd
55 changed files with 0 additions and 0 deletions

34
src/lang/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Common IntelliJ Platform excludes
# User specific
**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/httpRequests/
# Sensitive or high-churn files
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
*.suo
*.user
.vs/
[Bb]in/
[Oo]bj/
_UpgradeReport_Files/
[Pp]ackages/
Thumbs.db
Desktop.ini
.DS_Store

13
src/lang/.idea/.idea.Nub.Lang/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/projectSettingsUpdater.xml
/.idea.Nub.Lang.iml
/modules.xml
/contentModel.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1 @@
Nub.Lang

View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders>
<Path>../../example</Path>
<Path>../core</Path>
</attachedFolders>
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>nub</AssemblyName>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nub.Lang\Nub.Lang.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,57 @@
using Nub.Lang.Backend;
using Nub.Lang.Frontend.Diagnostics;
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Parsing;
using Nub.Lang.Frontend.Typing;
if (args.Length != 1)
{
Console.Error.WriteLine("Usage: nub <input-dir>");
Console.Error.WriteLine("Example: nub src");
return 1;
}
var srcDir = Path.GetFullPath(args[0]);
if (!Directory.Exists(srcDir))
{
Console.Error.WriteLine($"Error: Input directory '{srcDir}' does not exist.");
return 1;
}
var error = false;
var lexer = new Lexer();
var parser = new Parser();
var typeChecker = new TypeChecker();
List<SourceFile> files = [];
foreach (var file in Directory.EnumerateFiles(srcDir, "*.nub", SearchOption.AllDirectories))
{
var content = File.ReadAllText(file);
var tokenizeResult = lexer.Tokenize(new SourceText(file, content));
tokenizeResult.PrintAllDiagnostics();
error = error || tokenizeResult.HasErrors;
var parseResult = parser.ParseModule(tokenizeResult.Value);
parseResult.PrintAllDiagnostics();
error = error || parseResult.HasErrors;
if (parseResult.Value != null)
{
files.Add(parseResult.Value);
}
}
var typeCheckResult = typeChecker.TypeCheck(files);
typeCheckResult.PrintAllDiagnostics();
error = error || typeCheckResult.HasErrors;
if (error) return 1;
var generator = new Generator();
var result = generator.Generate(files);
Console.Out.Write(result);
return 0;

22
src/lang/Nub.Lang.sln Normal file
View File

@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nub.Lang", "Nub.Lang\Nub.Lang.csproj", "{5047E21F-590D-4CB3-AFF3-064316485009}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nub.Lang.CLI", "Nub.Lang.CLI\Nub.Lang.CLI.csproj", "{A22F17ED-FA17-45AB-92BA-CD02C28B3524}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5047E21F-590D-4CB3-AFF3-064316485009}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5047E21F-590D-4CB3-AFF3-064316485009}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5047E21F-590D-4CB3-AFF3-064316485009}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5047E21F-590D-4CB3-AFF3-064316485009}.Release|Any CPU.Build.0 = Release|Any CPU
{A22F17ED-FA17-45AB-92BA-CD02C28B3524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A22F17ED-FA17-45AB-92BA-CD02C28B3524}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A22F17ED-FA17-45AB-92BA-CD02C28B3524}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A22F17ED-FA17-45AB-92BA-CD02C28B3524}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
namespace Nub.Lang.Frontend.Diagnostics;
public static class ConsoleColors
{
public const string Reset = "\e[0m";
public const string Bold = "\e[1m";
public const string Red = "\e[31m";
public const string Yellow = "\e[33m";
public const string Blue = "\e[34m";
public const string Cyan = "\e[36m";
public const string White = "\e[37m";
public const string BrightWhite = "\e[97m";
public const string Gray = "\e[90m";
public static bool IsColorSupported()
{
var term = Environment.GetEnvironmentVariable("TERM");
var colorTerm = Environment.GetEnvironmentVariable("COLORTERM");
return !string.IsNullOrEmpty(term) || !string.IsNullOrEmpty(colorTerm) || !Console.IsOutputRedirected;
}
public static string Colorize(string text, string color)
{
return IsColorSupported() ? $"{color}{text}{Reset}" : text;
}
}

View File

@@ -0,0 +1,205 @@
using System.Text;
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Parsing;
namespace Nub.Lang.Frontend.Diagnostics;
public enum DiagnosticSeverity
{
Info,
Warning,
Error
}
public class Diagnostic
{
public class DiagnosticBuilder
{
private readonly DiagnosticSeverity _severity;
private readonly string _message;
private SourceText? _sourceFile;
private SourceSpan? _span;
private string? _help;
public DiagnosticBuilder(DiagnosticSeverity severity, string message)
{
_severity = severity;
_message = message;
}
public DiagnosticBuilder At(Token token)
{
_sourceFile = token.SourceText;
_span = SourceLocationCalculator.GetSpan(token);
return this;
}
public DiagnosticBuilder At(Node node)
{
if (!node.Tokens.Any())
{
throw new ArgumentException("Node has no tokens", nameof(node));
}
_sourceFile = node.Tokens[0].SourceText;
_span = SourceLocationCalculator.GetSpan(node);
return this;
}
public DiagnosticBuilder At(SourceText sourceText, SourceSpan span)
{
_sourceFile = sourceText;
_span = span;
return this;
}
public DiagnosticBuilder WithHelp(string help)
{
_help = help;
return this;
}
public Diagnostic Build() => new(_severity, _message, _sourceFile, _span, _help);
}
public static DiagnosticBuilder Error(string message) => new(DiagnosticSeverity.Error, message);
public static DiagnosticBuilder Warning(string message) => new(DiagnosticSeverity.Warning, message);
public static DiagnosticBuilder Info(string message) => new(DiagnosticSeverity.Info, message);
public DiagnosticSeverity Severity { get; }
public string Message { get; }
public SourceText? SourceFile { get; }
public SourceSpan? Span { get; }
public string? Help { get; }
private Diagnostic(DiagnosticSeverity severity, string message, SourceText? sourceFile, SourceSpan? span, string? help)
{
Severity = severity;
Message = message;
SourceFile = sourceFile;
Span = span;
Help = help;
}
public string Format()
{
var sb = new StringBuilder();
var severityText = GetSeverityText(Severity);
sb.Append(severityText);
if (SourceFile.HasValue)
{
var locationText = $" at {SourceFile.Value.Path}:{Span}";
sb.Append(ConsoleColors.Colorize(locationText, ConsoleColors.Gray));
}
sb.Append(": ");
sb.AppendLine(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite));
if (SourceFile.HasValue && Span.HasValue)
{
AppendSourceContext(sb, SourceFile.Value, Span.Value);
}
if (!string.IsNullOrEmpty(Help))
{
sb.AppendLine();
var helpText = $"help: {Help}";
sb.AppendLine(ConsoleColors.Colorize(helpText, ConsoleColors.Cyan));
}
return sb.ToString();
}
private 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, SourceText sourceText, SourceSpan span)
{
var lines = sourceText.Content.Split('\n');
var startLine = span.Start.Line;
var endLine = span.End.Line;
const int contextLines = 3;
if (startLine < 1 || startLine > lines.Length || endLine < 1 || endLine > lines.Length)
{
return;
}
var maxLineNum = Math.Min(endLine + contextLines, lines.Length);
var lineNumWidth = maxLineNum.ToString().Length;
var contextStart = Math.Max(1, startLine - contextLines);
for (var lineNum = contextStart; lineNum < startLine; lineNum++)
{
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth);
}
for (var lineNum = startLine; lineNum <= endLine; lineNum++)
{
var line = lines[lineNum - 1];
AppendContextLine(sb, lineNum, line, lineNumWidth);
AppendErrorIndicators(sb, span, lineNum, line, lineNumWidth);
}
var contextEnd = Math.Min(lines.Length, endLine + contextLines);
for (var lineNum = endLine + 1; lineNum <= contextEnd; lineNum++)
{
AppendContextLine(sb, lineNum, lines[lineNum - 1], lineNumWidth);
}
}
private static void AppendContextLine(StringBuilder sb, int lineNum, string line, int lineNumWidth)
{
var lineNumStr = lineNum.ToString().PadLeft(lineNumWidth);
sb.Append(ConsoleColors.Colorize(lineNumStr, ConsoleColors.Gray));
sb.Append(ConsoleColors.Colorize(" | ", ConsoleColors.Gray));
sb.AppendLine(line);
}
private static void AppendErrorIndicators(StringBuilder sb, SourceSpan span, int lineNum, string line, int lineNumWidth)
{
sb.Append(new string(' ', lineNumWidth + 3));
var indicators = GetIndicatorsForLine(span, lineNum, line);
sb.AppendLine(ConsoleColors.Colorize(indicators, ConsoleColors.Red));
}
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);
}
}

View File

@@ -0,0 +1,19 @@
namespace Nub.Lang.Frontend.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());
}
}
}
public class DiagnosticsResult<TResult>(List<Diagnostic> diagnostics, TResult value) : DiagnosticsResult(diagnostics)
{
public TResult Value { get; } = value;
}

View File

@@ -0,0 +1,102 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Parsing;
namespace Nub.Lang.Frontend.Diagnostics;
public readonly struct SourceText(string path, string content)
{
public string Path { get; } = path;
public string Content { get; } = content;
}
public readonly struct SourceLocation(int line, int column)
{
public int Line { get; } = line;
public int Column { get; } = column;
}
public readonly struct SourceSpan(SourceLocation start, SourceLocation end)
{
public SourceLocation Start { get; } = start;
public SourceLocation End { get; } = end;
public override string ToString()
{
if (Start.Line == End.Line)
{
if (Start.Column == End.Column)
{
return $"{Start.Line}:{Start.Column}";
}
return $"{Start.Line}:{Start.Column}-{End.Column}";
}
return $"{Start.Line}:{Start.Column}-{End.Line}:{End.Column}";
}
}
public static class SourceLocationCalculator
{
private static int[] GetLineStarts(string content)
{
var lineStarts = new List<int> { 0 };
for (var i = 0; i < content.Length; i++)
{
if (content[i] == '\n')
{
lineStarts.Add(i + 1);
}
}
return lineStarts.ToArray();
}
private static SourceLocation IndexToLocation(string content, int index)
{
if (index < 0 || index > content.Length)
{
throw new ArgumentOutOfRangeException(nameof(index), $"Index {index} is out of range for content of length {content.Length}");
}
var lineStarts = GetLineStarts(content);
var line = Array.BinarySearch(lineStarts, index);
if (line < 0)
{
line = ~line - 1;
}
if (line < lineStarts.Length - 1 && lineStarts[line + 1] == index && index < content.Length && content[index] == '\n')
{
line++;
}
var column = index - lineStarts[line] + 1;
return new SourceLocation(line + 1, column);
}
public static SourceSpan GetSpan(Token token)
{
var start = IndexToLocation(token.SourceText.Content, token.StartIndex);
var end = IndexToLocation(token.SourceText.Content, token.EndIndex);
return new SourceSpan(start, end);
}
public static SourceSpan GetSpan(Node node)
{
if (!node.Tokens.Any())
{
throw new ArgumentException("Node has no tokens", nameof(node));
}
var firstToken = node.Tokens[0];
var lastToken = node.Tokens[^1];
var start = IndexToLocation(firstToken.SourceText.Content, firstToken.StartIndex);
var end = IndexToLocation(lastToken.SourceText.Content, lastToken.EndIndex);
return new SourceSpan(start, end);
}
}

View File

@@ -0,0 +1,8 @@
using Nub.Lang.Frontend.Diagnostics;
namespace Nub.Lang.Frontend.Lexing;
public class DocumentationToken(SourceText sourceText, int startIndex, int endIndex, string documentation) : Token(sourceText, startIndex, endIndex)
{
public string Documentation { get; } = documentation;
}

View File

@@ -0,0 +1,8 @@
using Nub.Lang.Frontend.Diagnostics;
namespace Nub.Lang.Frontend.Lexing;
public class IdentifierToken(SourceText sourceText, int startIndex, int endIndex, string value) : Token(sourceText, startIndex, endIndex)
{
public string Value { get; } = value;
}

View File

@@ -0,0 +1,273 @@
using Nub.Lang.Frontend.Diagnostics;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Lexing;
public 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,
["new"] = Symbol.New,
["struct"] = Symbol.Struct,
};
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 SourceText _sourceText;
private int _index;
public 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 void ConsumeWhitespace()
{
while (Peek().TryGetValue(out var character) && char.IsWhiteSpace(character))
{
Next();
}
}
private Optional<Token> ParseToken()
{
ConsumeWhitespace();
var startIndex = _index;
string? documentation = null;
while (Peek() is { Value: '/' } && Peek(1) is { Value: '/' })
{
Next();
Next();
if (Peek() is { Value: '/' })
{
Next();
while (Peek().TryGetValue(out var character))
{
Next();
documentation += character;
if (character == '\n')
{
break;
}
}
}
else
{
while (Peek().TryGetValue(out var character))
{
Next();
if (character == '\n')
{
break;
}
}
}
}
if (documentation != null)
{
return new DocumentationToken(_sourceText, startIndex, _index, documentation);
}
ConsumeWhitespace();
startIndex = _index;
if (!Peek().TryGetValue(out var current))
{
return Optional<Token>.Empty();
}
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(_sourceText, startIndex, _index, keywordSymbol);
}
if (Modifiers.TryGetValue(buffer, out var modifer))
{
return new ModifierToken(_sourceText, startIndex, _index, modifer);
}
if (buffer is "true" or "false")
{
return new LiteralToken(_sourceText, startIndex, _index, NubPrimitiveType.Bool, buffer);
}
return new IdentifierToken(_sourceText, startIndex, _index, 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 if (next == 'f')
{
isFloat = true;
Next();
break;
}
else
{
break;
}
}
return new LiteralToken(_sourceText, startIndex, _index, isFloat ? NubPrimitiveType.F64 : NubPrimitiveType.I64, 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(_sourceText, startIndex, _index, chain.Value);
}
}
}
if (Chars.TryGetValue(current, out var charSymbol))
{
Next();
return new SymbolToken(_sourceText, startIndex, _index, 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(_sourceText, startIndex, _index, NubPrimitiveType.String, buffer);
}
throw new Exception($"Unknown character {current}");
}
private Optional<char> Peek(int offset = 0)
{
if (_index + offset < _sourceText.Content.Length)
{
return _sourceText.Content[_index + offset];
}
return Optional<char>.Empty();
}
private void Next()
{
_index++;
}
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Diagnostics;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Lexing;
public class LiteralToken(SourceText sourceText, int startIndex, int endIndex, NubType type, string value) : Token(sourceText, startIndex, endIndex)
{
public NubType Type { get; } = type;
public string Value { get; } = value;
}

View File

@@ -0,0 +1,14 @@
using Nub.Lang.Frontend.Diagnostics;
namespace Nub.Lang.Frontend.Lexing;
public class ModifierToken(SourceText sourceText, int startIndex, int endIndex, Modifier modifier) : Token(sourceText, startIndex, endIndex)
{
public Modifier Modifier { get; } = modifier;
}
public enum Modifier
{
Extern,
Export
}

View File

@@ -0,0 +1,47 @@
using Nub.Lang.Frontend.Diagnostics;
namespace Nub.Lang.Frontend.Lexing;
public class SymbolToken(SourceText sourceText, int startIndex, int endIndex, Symbol symbol) : Token(sourceText, startIndex, endIndex)
{
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,
New,
Struct,
Caret,
Ampersand,
DoubleColon,
Namespace,
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Diagnostics;
namespace Nub.Lang.Frontend.Lexing;
public abstract class Token(SourceText sourceText, int startIndex, int endIndex)
{
public SourceText SourceText { get; } = sourceText;
public int StartIndex { get; } = startIndex;
public int EndIndex { get; } = endIndex;
}

View File

@@ -0,0 +1,8 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class AddressOfNode(IReadOnlyList<Token> tokens, ExpressionNode expression) : ExpressionNode(tokens)
{
public ExpressionNode Expression { get; } = expression;
}

View File

@@ -0,0 +1,9 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class ArrayIndexNode(IReadOnlyList<Token> tokens, ExpressionNode expression, ExpressionNode index) : ExpressionNode(tokens)
{
public ExpressionNode Expression { get; } = expression;
public ExpressionNode Index { get; } = index;
}

View File

@@ -0,0 +1,24 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
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
}

View File

@@ -0,0 +1,8 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class BlockNode(IReadOnlyList<Token> tokens, List<StatementNode> statements) : Node(tokens)
{
public List<StatementNode> Statements { get; } = statements;
}

View File

@@ -0,0 +1,5 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class BreakNode(IReadOnlyList<Token> tokens) : StatementNode(tokens);

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
public class CastNode(IReadOnlyList<Token> tokens, NubType targetType, ExpressionNode expression) : ExpressionNode(tokens)
{
public NubType TargetType { get; } = targetType;
public ExpressionNode Expression { get; } = expression;
}

View File

@@ -0,0 +1,5 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class ContinueNode(IReadOnlyList<Token> tokens) : StatementNode(tokens);

View File

@@ -0,0 +1,8 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public abstract class DefinitionNode(IReadOnlyList<Token> tokens, Optional<string> documentation) : Node(tokens)
{
public Optional<string> Documentation { get; set; } = documentation;
}

View File

@@ -0,0 +1,8 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class DereferenceNode(IReadOnlyList<Token> tokens, ExpressionNode expression) : ExpressionNode(tokens)
{
public ExpressionNode Expression { get; } = expression;
}

View File

@@ -0,0 +1,14 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
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;
}
}

View File

@@ -0,0 +1,10 @@
namespace Nub.Lang.Frontend.Parsing;
public class FuncCall(string @namespace, string name, List<ExpressionNode> parameters)
{
public string Namespace { get; } = @namespace;
public string Name { get; } = name;
public List<ExpressionNode> Parameters { get; } = parameters;
public override string ToString() => $"{Name}()";
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class FuncCallExpressionNode(IReadOnlyList<Token> tokens, FuncCall funcCall) : ExpressionNode(tokens)
{
public FuncCall FuncCall { get; } = funcCall;
public override string ToString() => FuncCall.ToString();
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class FuncCallStatementNode(IReadOnlyList<Token> tokens, FuncCall funcCall) : StatementNode(tokens)
{
public FuncCall FuncCall { get; } = funcCall;
public override string ToString() => FuncCall.ToString();
}

View File

@@ -0,0 +1,73 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
public class FuncParameter(string name, NubType type, bool variadic)
{
public string Name { get; } = name;
public NubType Type { get; } = type;
public bool Variadic { get; } = variadic;
public override string ToString() => $"{Name}: {Type}";
}
public interface IFuncSignature
{
public string Name { get; }
public List<FuncParameter> Parameters { get; }
public Optional<NubType> ReturnType { get; }
public bool SignatureMatches(string name, List<NubType> parameters)
{
if (Name != name) return false;
if (Parameters.Count == 0 && parameters.Count == 0) return true;
if (Parameters.Count > parameters.Count) return false;
for (var i = 0; i < parameters.Count; i++)
{
if (i > Parameters.Count)
{
if (Parameters[^1].Variadic)
{
if (!NubType.IsCompatibleWith(parameters[i], Parameters[^1].Type))
{
return false;
}
}
else
{
return false;
}
}
else if (!NubType.IsCompatibleWith(parameters[i], Parameters[i].Type))
{
return false;
}
}
return true;
}
public string ToString() => $"{Name}({string.Join(", ", Parameters.Select(p => p.ToString()))}){(ReturnType.HasValue ? ": " + ReturnType.Value : "")}";
}
public class LocalFuncDefinitionNode(IReadOnlyList<Token> tokens, Optional<string> documentation, string name, List<FuncParameter> parameters, BlockNode body, Optional<NubType> returnType, bool exported) : DefinitionNode(tokens, documentation), IFuncSignature
{
public string Name { get; } = name;
public List<FuncParameter> Parameters { get; } = parameters;
public BlockNode Body { get; } = body;
public Optional<NubType> ReturnType { get; } = returnType;
public bool Exported { get; } = exported;
public override string ToString() => $"{Name}({string.Join(", ", Parameters.Select(p => p.ToString()))}){(ReturnType.HasValue ? ": " + ReturnType.Value : "")}";
}
public class ExternFuncDefinitionNode(IReadOnlyList<Token> tokens, Optional<string> documentation, string name, List<FuncParameter> parameters, Optional<NubType> returnType) : DefinitionNode(tokens, documentation), IFuncSignature
{
public string Name { get; } = name;
public List<FuncParameter> Parameters { get; } = parameters;
public Optional<NubType> ReturnType { get; } = returnType;
public override string ToString() => $"{Name}({string.Join(", ", Parameters.Select(p => p.ToString()))}){(ReturnType.HasValue ? ": " + ReturnType.Value : "")}";
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class IdentifierNode(IReadOnlyList<Token> tokens, string identifier) : ExpressionNode(tokens)
{
public string Identifier { get; } = identifier;
public override string ToString() => Identifier;
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
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;
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
public class LiteralNode(IReadOnlyList<Token> tokens, string literal, NubType literalType) : ExpressionNode(tokens)
{
public string Literal { get; } = literal;
public NubType LiteralType { get; } = literalType;
}

View File

@@ -0,0 +1,9 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class MemberAccessNode(IReadOnlyList<Token> tokens, ExpressionNode expression, string member) : ExpressionNode(tokens)
{
public ExpressionNode Expression { get; } = expression;
public string Member { get; } = member;
}

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

View File

@@ -0,0 +1,830 @@
using System.Diagnostics.CodeAnalysis;
using Nub.Lang.Frontend.Diagnostics;
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
public class Parser
{
private List<Diagnostic> _diagnostics = [];
private List<Token> _tokens = [];
private int _index;
private string _namespace = string.Empty;
public DiagnosticsResult<SourceFile?> ParseModule(List<Token> tokens)
{
_diagnostics = [];
_tokens = tokens;
_index = 0;
_namespace = string.Empty;
try
{
ExpectSymbol(Symbol.Namespace);
_namespace = ExpectIdentifier().Value;
List<DefinitionNode> definitions = [];
while (Peek().HasValue)
{
definitions.Add(ParseDefinition());
}
return new DiagnosticsResult<SourceFile?>(_diagnostics, new SourceFile(_namespace, definitions));
}
catch (ParseException ex)
{
_diagnostics.Add(ex.Diagnostic);
RecoverToNextDefinition();
}
return new DiagnosticsResult<SourceFile?>(_diagnostics, null);
}
private 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 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 = Optional<NubType>.Empty();
if (TryExpectSymbol(Symbol.Colon))
{
returnType = ParseType();
}
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());
}
return new ExternFuncDefinitionNode(GetTokensForNode(startIndex), documentation, name.Value, 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, name.Value, parameters, body, returnType, exported);
}
private 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, name, variables);
}
private FuncParameter ParseFuncParameter()
{
var variadic = false;
if (TryExpectSymbol(Symbol.Period))
{
ExpectSymbol(Symbol.Period);
ExpectSymbol(Symbol.Period);
variadic = true;
}
var name = ExpectIdentifier();
ExpectSymbol(Symbol.Colon);
var type = ParseType();
return new FuncParameter(name.Value, type, variadic);
}
private StatementNode ParseStatement()
{
var startIndex = _index;
var token = ExpectToken();
switch (token)
{
case IdentifierToken identifier:
{
return ParseStatementIdentifier(startIndex, identifier);
}
case SymbolToken symbol:
{
return symbol.Symbol switch
{
Symbol.Return => ParseReturn(startIndex),
Symbol.If => ParseIf(startIndex),
Symbol.While => ParseWhile(startIndex),
Symbol.Break => new BreakNode(GetTokensForNode(startIndex)),
Symbol.Continue => new ContinueNode(GetTokensForNode(startIndex)),
_ => throw new ParseException(Diagnostic
.Error($"Unexpected symbol '{symbol.Symbol}' at start of statement")
.WithHelp("Expected identifier, 'return', 'if', 'while', 'break', or 'continue'")
.At(symbol)
.Build())
};
}
default:
{
throw new ParseException(Diagnostic
.Error($"Unexpected token '{token.GetType().Name}' at start of statement")
.WithHelp("Statements must start with an identifier or keyword")
.At(token)
.Build());
}
}
}
private StatementNode ParseStatementIdentifier(int startIndex, IdentifierToken identifier)
{
var symbol = ExpectSymbol();
switch (symbol.Symbol)
{
case Symbol.DoubleColon:
{
var name = ExpectIdentifier();
ExpectSymbol(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());
}
}
return new FuncCallStatementNode(GetTokensForNode(startIndex), new FuncCall(identifier.Value, name.Value, parameters));
}
case 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());
}
}
return new FuncCallStatementNode(GetTokensForNode(startIndex), new FuncCall(_namespace, identifier.Value, parameters));
}
case Symbol.Assign:
{
var value = ParseExpression();
return new VariableAssignmentNode(GetTokensForNode(startIndex), identifier.Value, Optional<NubType>.Empty(), value);
}
case Symbol.Colon:
{
var type = ParseType();
ExpectSymbol(Symbol.Assign);
var value = ParseExpression();
return new VariableAssignmentNode(GetTokensForNode(startIndex), identifier.Value, type, value);
}
default:
{
throw new ParseException(Diagnostic
.Error($"Unexpected symbol '{symbol.Symbol}' after identifier")
.WithHelp("Expected '(', '=', or ':' after identifier")
.At(symbol)
.Build());
}
}
}
private ReturnNode ParseReturn(int startIndex)
{
var value = Optional<ExpressionNode>.Empty();
if (!TryExpectSymbol(Symbol.Semicolon))
{
value = ParseExpression();
}
return new ReturnNode(GetTokensForNode(startIndex), value);
}
private IfNode ParseIf(int startIndex)
{
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 WhileNode ParseWhile(int startIndex)
{
var condition = ParseExpression();
var body = ParseBlock();
return new WhileNode(GetTokensForNode(startIndex), condition, body);
}
private 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 ExpressionNode ParsePrimaryExpression()
{
var startIndex = _index;
ExpressionNode expr;
var token = ExpectToken();
switch (token)
{
case LiteralToken literal:
{
expr = new LiteralNode(GetTokensForNode(startIndex), literal.Value, literal.Type);
break;
}
case IdentifierToken identifier:
{
var next = Peek();
switch (next.Value)
{
case SymbolToken { Symbol: Symbol.DoubleColon }:
{
Next();
var name = ExpectIdentifier();
ExpectSymbol(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 FuncCallExpressionNode(GetTokensForNode(startIndex), new FuncCall(identifier.Value, name.Value, parameters));
break;
}
case SymbolToken { Symbol: Symbol.OpenParen }:
{
Next();
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 FuncCallExpressionNode(GetTokensForNode(startIndex), new FuncCall(_namespace, identifier.Value, parameters));
break;
}
default:
{
expr = new IdentifierNode(GetTokensForNode(startIndex), identifier.Value);
break;
}
}
break;
}
case SymbolToken symbolToken:
{
switch (symbolToken.Symbol)
{
case Symbol.OpenParen:
{
var expression = ParseExpression();
ExpectSymbol(Symbol.CloseParen);
expr = expression;
break;
}
case Symbol.LessThan:
{
var type = ParseType();
ExpectSymbol(Symbol.GreaterThan);
ExpectSymbol(Symbol.OpenParen);
var expressionToCast = ParseExpression();
ExpectSymbol(Symbol.CloseParen);
expr = new CastNode(GetTokensForNode(startIndex), type, expressionToCast);
break;
}
case Symbol.New:
{
var type = ParseType();
switch (type)
{
case NubStructType structType:
{
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);
}
expr = new StructInitializerNode(GetTokensForNode(startIndex), structType, initializers);
break;
}
case NubArrayType arrayType:
{
throw new NotImplementedException();
}
default:
{
throw new ParseException(Diagnostic
.Error($"Cannot use new keyword on type {type}")
.At(symbolToken)
.Build());
}
}
break;
}
case Symbol.Ampersand:
{
var expression = ParsePrimaryExpression();
expr = new AddressOfNode(GetTokensForNode(startIndex), expression);
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;
}
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 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 ArrayIndexNode(GetTokensForNode(startIndex), expr, index);
continue;
}
break;
}
return expr;
}
private 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 NubType ParseType()
{
if (TryExpectIdentifier(out var name))
{
if (NubPrimitiveType.TryParse(name, out var primitiveTypeKind))
{
return new NubPrimitiveType(primitiveTypeKind.Value);
}
else
{
var @namespace = _namespace;
if (TryExpectSymbol(Symbol.DoubleColon))
{
@namespace = ExpectIdentifier().Value;
}
return new NubStructType(@namespace, name);
}
}
if (TryExpectSymbol(Symbol.Caret))
{
var baseType = ParseType();
return new NubPointerType(baseType);
}
if (TryExpectSymbol(Symbol.OpenBracket))
{
ExpectSymbol(Symbol.CloseBracket);
var baseType = ParseType();
return new NubArrayType(baseType);
}
if (!Peek().TryGetValue(out var token))
{
throw new ParseException(Diagnostic
.Error("Unexpected end of file while parsing type")
.WithHelp("Expected a type name")
.At(_tokens.Last().SourceText, SourceLocationCalculator.GetSpan(_tokens.Last()))
.Build());
}
throw new ParseException(Diagnostic
.Error("Invalid type syntax")
.WithHelp("Expected type name, '^' for pointer, or '[]' for array")
.At(token)
.Build());
}
private 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().SourceText, SourceLocationCalculator.GetSpan(_tokens.Last()))
.Build());
}
Next();
return token;
}
private 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 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 bool TryExpectSymbol(Symbol symbol)
{
if (Peek() is { Value: SymbolToken symbolToken } && symbolToken.Symbol == symbol)
{
Next();
return true;
}
return false;
}
private bool TryExpectModifier([NotNullWhen(true)] out ModifierToken? modifier)
{
if (Peek() is { Value: ModifierToken modifierToken })
{
modifier = modifierToken;
Next();
return true;
}
modifier = null;
return false;
}
private bool TryExpectIdentifier([NotNullWhen(true)] out string? identifier)
{
if (Peek() is { Value: IdentifierToken identifierToken })
{
identifier = identifierToken.Value;
Next();
return true;
}
identifier = null;
return false;
}
private 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 void RecoverToNextDefinition()
{
while (Peek().HasValue)
{
var token = Peek().Value;
if (token is SymbolToken { Symbol: Symbol.Func or Symbol.Struct })
{
break;
}
Next();
}
}
private void RecoverToNextStatement()
{
while (Peek().TryGetValue(out var token))
{
if (token is SymbolToken { Symbol: Symbol.CloseBrace } or IdentifierToken or SymbolToken
{
Symbol: Symbol.Return or Symbol.If or Symbol.While or Symbol.Break or Symbol.Continue
})
{
break;
}
Next();
}
}
private Optional<Token> Peek()
{
var peekIndex = _index;
while (peekIndex < _tokens.Count && _tokens[peekIndex] is DocumentationToken)
{
peekIndex++;
}
if (peekIndex < _tokens.Count)
{
return _tokens[peekIndex];
}
return Optional<Token>.Empty();
}
private void Next()
{
while (_index < _tokens.Count && _tokens[_index] is DocumentationToken)
{
_index++;
}
_index++;
}
private 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;
}
}

View File

@@ -0,0 +1,8 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class ReturnNode(IReadOnlyList<Token> tokens, Optional<ExpressionNode> value) : StatementNode(tokens)
{
public Optional<ExpressionNode> Value { get; } = value;
}

View File

@@ -0,0 +1,7 @@
namespace Nub.Lang.Frontend.Parsing;
public class SourceFile(string @namespace, List<DefinitionNode> definitions)
{
public string Namespace { get; } = @namespace;
public List<DefinitionNode> Definitions { get; } = definitions;
}

View File

@@ -0,0 +1,5 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public abstract class StatementNode(IReadOnlyList<Token> tokens) : Node(tokens);

View File

@@ -0,0 +1,17 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
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 name, List<StructField> fields) : DefinitionNode(tokens, documentation)
{
public string Name { get; } = name;
public List<StructField> Fields { get; } = fields;
}

View File

@@ -0,0 +1,10 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
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;
}

View File

@@ -0,0 +1,15 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
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
}

View File

@@ -0,0 +1,11 @@
using Nub.Lang.Frontend.Lexing;
using Nub.Lang.Frontend.Typing;
namespace Nub.Lang.Frontend.Parsing;
public class VariableAssignmentNode(IReadOnlyList<Token> tokens, string name, Optional<NubType> explicitType, ExpressionNode value) : StatementNode(tokens)
{
public string Name { get; } = name;
public Optional<NubType> ExplicitType { get; } = explicitType;
public ExpressionNode Value { get; } = value;
}

View File

@@ -0,0 +1,9 @@
using Nub.Lang.Frontend.Lexing;
namespace Nub.Lang.Frontend.Parsing;
public class WhileNode(IReadOnlyList<Token> tokens, ExpressionNode condition, BlockNode body) : StatementNode(tokens)
{
public ExpressionNode Condition { get; } = condition;
public BlockNode Body { get; } = body;
}

View File

@@ -0,0 +1,151 @@
using System.Diagnostics.CodeAnalysis;
namespace Nub.Lang.Frontend.Typing;
public abstract class NubType
{
protected NubType(string name)
{
Name = name;
}
public string Name { get; }
public static bool IsCompatibleWith(NubType sourceType, NubType targetType)
{
return targetType.Equals(NubPrimitiveType.Any) || sourceType.Equals(targetType);
}
public override bool Equals(object? obj) => obj is NubType item && Name.Equals(item.Name);
public override int GetHashCode() => HashCode.Combine(Name);
public override string ToString() => Name;
}
public class NubStructType(string name, string @namespace) : NubType(name)
{
public string Namespace { get; } = @namespace;
}
public class NubPointerType(NubType baseType) : NubType("^" + baseType)
{
public NubType BaseType { get; } = baseType;
public override bool Equals(object? obj)
{
if (obj is NubPointerType other)
{
return BaseType.Equals(other.BaseType);
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(), BaseType);
}
}
public class NubArrayType(NubType baseType) : NubType("[]" + baseType)
{
public NubType BaseType { get; } = baseType;
public override bool Equals(object? obj)
{
if (obj is NubArrayType other)
{
return BaseType.Equals(other.BaseType);
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(base.GetHashCode(), BaseType);
}
}
public class NubPrimitiveType(PrimitiveTypeKind kind) : NubType(KindToString(kind))
{
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 NubPrimitiveType String => new(PrimitiveTypeKind.String);
public static NubPrimitiveType Any => new(PrimitiveTypeKind.Any);
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,
"string" => PrimitiveTypeKind.String,
"any" => PrimitiveTypeKind.Any,
_ => null
};
return kind != null;
}
public static string KindToString(PrimitiveTypeKind kind)
{
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",
PrimitiveTypeKind.String => "string",
PrimitiveTypeKind.Any => "any",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
};
}
}
public enum PrimitiveTypeKind
{
I64,
I32,
I16,
I8,
U64,
U32,
U16,
U8,
F64,
F32,
Bool,
String,
Any
}

View File

@@ -0,0 +1,621 @@
using Nub.Lang.Frontend.Parsing;
using Nub.Lang.Frontend.Diagnostics;
namespace Nub.Lang.Frontend.Typing;
public class TypeChecker
{
private Dictionary<string, NubType> _variables = new();
private List<SourceFile> _sourceFiles = [];
private List<Diagnostic> _diagnostics = [];
private NubType? _currentFunctionReturnType;
private bool _hasReturnStatement;
public DiagnosticsResult TypeCheck(List<SourceFile> sourceFiles)
{
_variables = new Dictionary<string, NubType>();
_diagnostics = [];
_currentFunctionReturnType = null;
_hasReturnStatement = false;
_sourceFiles = sourceFiles;
var externFuncDefinitions = _sourceFiles
.SelectMany(f => f.Definitions)
.OfType<ExternFuncDefinitionNode>()
.ToArray();
foreach (var funcName in externFuncDefinitions.Where(x => externFuncDefinitions.Count(y => x.Name == y.Name) > 1))
{
ReportError($"Extern function '{funcName}' has been declared more than once", funcName);
}
var exportedLocalFuncDefinitions = _sourceFiles
.SelectMany(f => f.Definitions)
.OfType<LocalFuncDefinitionNode>()
.Where(f => f.Exported)
.ToArray();
foreach (var funcName in exportedLocalFuncDefinitions.Where(x => exportedLocalFuncDefinitions.Count(y => x.Name == y.Name) > 1))
{
ReportError($"Exported function '{funcName}' has been declared more than once", funcName);
}
foreach (var structDef in _sourceFiles.SelectMany(f => f.Definitions).OfType<StructDefinitionNode>())
{
TypeCheckStructDef(structDef);
}
foreach (var funcDef in _sourceFiles.SelectMany(f => f.Definitions).OfType<LocalFuncDefinitionNode>())
{
TypeCheckFuncDef(funcDef);
}
return new DiagnosticsResult(_diagnostics);
}
private void TypeCheckStructDef(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 = TypeCheckExpression(field.Value.Value);
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 void TypeCheckFuncDef(LocalFuncDefinitionNode funcDef)
{
_variables.Clear();
_currentFunctionReturnType = funcDef.ReturnType.HasValue ? funcDef.ReturnType.Value : null;
_hasReturnStatement = false;
foreach (var param in funcDef.Parameters)
{
_variables[param.Name] = param.Type;
}
TypeCheckBlock(funcDef.Body);
if (_currentFunctionReturnType != null && !_hasReturnStatement)
{
ReportError($"Function '{funcDef.Name}' must return a value of type '{_currentFunctionReturnType}'", funcDef);
}
}
private void TypeCheckBlock(BlockNode block)
{
foreach (var statement in block.Statements)
{
TypeCheckStatement(statement);
}
}
private void TypeCheckStatement(StatementNode statement)
{
switch (statement)
{
case VariableAssignmentNode varAssign:
TypeCheckVariableAssignment(varAssign);
break;
case FuncCallStatementNode funcCall:
TypeCheckFuncCall(funcCall.FuncCall, funcCall);
break;
case IfNode ifNode:
TypeCheckIf(ifNode);
break;
case WhileNode whileNode:
TypeCheckWhile(whileNode);
break;
case ReturnNode returnNode:
TypeCheckReturn(returnNode);
break;
case BreakNode:
case ContinueNode:
break;
default:
ReportError($"Unsupported statement type: {statement.GetType().Name}", statement);
break;
}
}
private void TypeCheckVariableAssignment(VariableAssignmentNode varAssign)
{
var valueType = TypeCheckExpression(varAssign.Value);
if (valueType == null) return;
if (_variables.TryGetValue(varAssign.Name, out var existingVariable))
{
if (varAssign.ExplicitType.HasValue)
{
if (!NubType.IsCompatibleWith(existingVariable, varAssign.ExplicitType.Value))
{
ReportError($"Explicit type '{varAssign.ExplicitType.Value}' on variable '{varAssign.Name}' is not compatible with declared type '{existingVariable}'", varAssign);
return;
}
}
if (!NubType.IsCompatibleWith(valueType, existingVariable))
{
ReportError($"Cannot assign expression of type '{valueType}' to variable '{varAssign.Name}' of type '{existingVariable}'", varAssign);
}
}
else
{
if (varAssign.ExplicitType.HasValue)
{
var explicitType = varAssign.ExplicitType.Value;
if (!NubType.IsCompatibleWith(valueType, explicitType))
{
ReportError($"Cannot assign expression of type '{valueType}' to variable '{varAssign.Name}' of type '{explicitType}'", varAssign);
return;
}
_variables[varAssign.Name] = explicitType;
}
else
{
_variables[varAssign.Name] = valueType;
}
}
}
private NubType? TypeCheckDereference(DereferenceNode dereference)
{
var exprType = TypeCheckExpression(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 NubType? TypeCheckFuncCall(FuncCall funcCall, Node node)
{
List<NubType> parameterTypes = [];
foreach (var funcCallParameter in funcCall.Parameters)
{
var parameterType = TypeCheckExpression(funcCallParameter);
if (parameterType == null) return null;
parameterTypes.Add(parameterType);
}
var funcDefinition = LookupFuncSignature(funcCall.Namespace, funcCall.Name, parameterTypes);
if (funcDefinition == null)
{
ReportError($"Function '{funcCall.Name}' is not defined", node);
return null;
}
if (funcDefinition.Parameters.Take(funcDefinition.Parameters.Count - 1).Any(x => x.Variadic))
{
ReportError($"Function '{funcCall.Name}' has multiple variadic parameters", node);
return null;
}
for (var i = 0; i < funcCall.Parameters.Count; i++)
{
var argType = funcCall.Parameters[i].Type;
NubType paramType;
if (i < funcDefinition.Parameters.Count)
{
paramType = funcDefinition.Parameters[i].Type;
}
else if (funcDefinition.Parameters.LastOrDefault()?.Variadic ?? false)
{
paramType = funcDefinition.Parameters[^1].Type;
}
else
{
ReportError($"Function '{funcCall.Name}' does not take {funcCall.Parameters.Count} parameters", node);
continue;
}
if (!NubType.IsCompatibleWith(argType, paramType))
{
ReportError($"Parameter {i + 1} of function '{funcCall.Name}' expects type '{paramType}', but got '{argType}'", funcCall.Parameters[i]);
}
}
return funcDefinition.ReturnType.HasValue ? funcDefinition.ReturnType.Value : NubPrimitiveType.Any;
}
private void TypeCheckIf(IfNode ifNode)
{
var conditionType = TypeCheckExpression(ifNode.Condition);
if (conditionType != null && !conditionType.Equals(NubPrimitiveType.Bool))
{
ReportError($"If condition must be a boolean expression, got '{conditionType}'", ifNode.Condition);
}
TypeCheckBlock(ifNode.Body);
if (ifNode.Else.HasValue)
{
var elseValue = ifNode.Else.Value;
elseValue.Match(TypeCheckIf, TypeCheckBlock);
}
}
private void TypeCheckWhile(WhileNode whileNode)
{
var conditionType = TypeCheckExpression(whileNode.Condition);
if (conditionType != null && !conditionType.Equals(NubPrimitiveType.Bool))
{
ReportError($"While condition must be a boolean expression, got '{conditionType}'", whileNode.Condition);
}
TypeCheckBlock(whileNode.Body);
}
private void TypeCheckReturn(ReturnNode returnNode)
{
_hasReturnStatement = true;
if (returnNode.Value.HasValue)
{
var returnType = TypeCheckExpression(returnNode.Value.Value);
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 NubType? TypeCheckExpression(ExpressionNode expression)
{
var resultType = expression switch
{
AddressOfNode addressOf => TypeCheckAddressOf(addressOf),
ArrayIndexNode arrayIndex => TypeCheckArrayIndex(arrayIndex),
LiteralNode literal => literal.LiteralType,
IdentifierNode identifier => TypeCheckIdentifier(identifier),
BinaryExpressionNode binaryExpr => TypeCheckBinaryExpression(binaryExpr),
CastNode cast => TypeCheckCast(cast),
DereferenceNode dereference => TypeCheckDereference(dereference),
FuncCallExpressionNode funcCallExpr => TypeCheckFuncCall(funcCallExpr.FuncCall, funcCallExpr),
StructInitializerNode structInit => TypeCheckStructInitializer(structInit),
UnaryExpressionNode unaryExpression => TypeCheckUnaryExpression(unaryExpression),
MemberAccessNode memberAccess => TypeCheckMemberAccess(memberAccess),
_ => ReportUnsupportedExpression(expression)
};
if (resultType != null)
{
expression.Type = resultType;
}
return resultType;
}
private NubType? ReportUnsupportedExpression(ExpressionNode expression)
{
ReportError($"Unsupported expression type: {expression.GetType().Name}", expression);
return null;
}
private NubType? TypeCheckArrayIndex(ArrayIndexNode arrayIndex)
{
var expressionType = TypeCheckExpression(arrayIndex.Expression);
if (expressionType == null) return null;
if (expressionType is not NubArrayType arrayType)
{
ReportError($"Cannot access index of non-array type {expressionType}", arrayIndex.Expression);
return null;
}
var indexType = TypeCheckExpression(arrayIndex.Index);
if (indexType != null && !IsInteger(indexType))
{
ReportError("Array index type must be an integer", arrayIndex.Index);
}
return arrayType.BaseType;
}
private NubType? TypeCheckIdentifier(IdentifierNode identifier)
{
if (!_variables.TryGetValue(identifier.Identifier, out var varType))
{
ReportError($"Variable '{identifier.Identifier}' is not defined", identifier);
return null;
}
return varType;
}
private NubType? TypeCheckAddressOf(AddressOfNode addressOf)
{
var exprType = TypeCheckExpression(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 NubType? TypeCheckBinaryExpression(BinaryExpressionNode binaryExpr)
{
var leftType = TypeCheckExpression(binaryExpr.Left);
var rightType = TypeCheckExpression(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 NubType? TypeCheckCast(CastNode cast)
{
TypeCheckExpression(cast.Expression);
// TODO: Check if castable
return cast.TargetType;
}
private NubType? TypeCheckStructInitializer(StructInitializerNode structInit)
{
var initialized = new HashSet<string>();
var definition = LookupStructDefinition(structInit.StructType.Namespace, structInit.StructType.Name);
if (definition == null)
{
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 = TypeCheckExpression(initializer.Value);
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 NubType? TypeCheckUnaryExpression(UnaryExpressionNode unaryExpression)
{
var operandType = TypeCheckExpression(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 NubType? TypeCheckMemberAccess(MemberAccessNode memberAccess)
{
var expressionType = TypeCheckExpression(memberAccess.Expression);
if (expressionType == null) return null;
switch (expressionType)
{
case NubArrayType:
{
if (memberAccess.Member == "count")
{
return NubPrimitiveType.I64;
}
break;
}
case NubStructType structType:
{
var definition = LookupStructDefinition(structType.Namespace, structType.Name);
if (definition == null)
{
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 void ReportError(string message, Node node)
{
var diagnostic = Diagnostic.Error(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;
}
}
private static bool IsInteger(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:
return true;
default:
return false;
}
}
private IFuncSignature? LookupFuncSignature(string @namespace, string name, List<NubType> parameters)
{
return _sourceFiles
.Where(f => f.Namespace == @namespace)
.SelectMany(f => f.Definitions)
.OfType<IFuncSignature>()
.FirstOrDefault(f => f.SignatureMatches(name, parameters));
}
private StructDefinitionNode? LookupStructDefinition(string @namespace, string name)
{
return _sourceFiles
.Where(f => f.Namespace == @namespace)
.SelectMany(f => f.Definitions)
.OfType<StructDefinitionNode>()
.FirstOrDefault(d => d.Name == name);
}
}

View 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>

View File

@@ -0,0 +1,62 @@
using System.Diagnostics.CodeAnalysis;
namespace Nub.Lang;
public readonly struct Optional
{
public static Optional<TValue> Empty<TValue>() => new();
/// <summary>
/// Alias for creating an Optional&lt;TValue&gt; 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 static implicit operator Optional<TValue>(TValue value) => new(value);
}

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