diff --git a/compiler/.gitignore b/compiler/.gitignore new file mode 100644 index 0000000..c6cc67a --- /dev/null +++ b/compiler/.gitignore @@ -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 \ No newline at end of file diff --git a/compiler/.idea/.idea.Compiler/.idea/.gitignore b/compiler/.idea/.idea.Compiler/.idea/.gitignore new file mode 100644 index 0000000..cda1cf4 --- /dev/null +++ b/compiler/.idea/.idea.Compiler/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/projectSettingsUpdater.xml +/contentModel.xml +/.idea.Compiler.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/compiler/.idea/.idea.Compiler/.idea/.name b/compiler/.idea/.idea.Compiler/.idea/.name new file mode 100644 index 0000000..b92b7a3 --- /dev/null +++ b/compiler/.idea/.idea.Compiler/.idea/.name @@ -0,0 +1 @@ +Compiler \ No newline at end of file diff --git a/compiler/.idea/.idea.Compiler/.idea/encodings.xml b/compiler/.idea/.idea.Compiler/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/compiler/.idea/.idea.Compiler/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/compiler/.idea/.idea.Compiler/.idea/indexLayout.xml b/compiler/.idea/.idea.Compiler/.idea/indexLayout.xml new file mode 100644 index 0000000..2135b43 --- /dev/null +++ b/compiler/.idea/.idea.Compiler/.idea/indexLayout.xml @@ -0,0 +1,10 @@ + + + + + Runtime + + + + + \ No newline at end of file diff --git a/compiler/.idea/.idea.Compiler/.idea/vcs.xml b/compiler/.idea/.idea.Compiler/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/compiler/.idea/.idea.Compiler/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/compiler/Compiler.sln b/compiler/Compiler.sln new file mode 100644 index 0000000..292d374 --- /dev/null +++ b/compiler/Compiler.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NubLang", "NubLang\NubLang.csproj", "{5047E21F-590D-4CB3-AFF3-064316485009}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NubLang.CLI", "NubLang.CLI\NubLang.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 diff --git a/compiler/NubLang.CLI/Archive.cs b/compiler/NubLang.CLI/Archive.cs new file mode 100644 index 0000000..59919c5 --- /dev/null +++ b/compiler/NubLang.CLI/Archive.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; + +namespace NubLang.CLI; + +public static class Archive +{ + public static async Task Invoke(string fileName, params IEnumerable objectFiles) + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo("ar", ["rcs", fileName, ..objectFiles]) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + process.Start(); + await process.WaitForExitAsync(); + + var errors = await process.StandardError.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(errors)) + { + await Console.Error.WriteLineAsync("ar error when archiving:\n" + errors); + } + + return process.ExitCode == 0; + } +} \ No newline at end of file diff --git a/compiler/NubLang.CLI/GCC.cs b/compiler/NubLang.CLI/GCC.cs new file mode 100644 index 0000000..51519ab --- /dev/null +++ b/compiler/NubLang.CLI/GCC.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; + +namespace NubLang.CLI; + +public static class GCC +{ + public static async Task Assemble(string asmPath, string outPath) + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo("x86_64-elf-as", ["-nostartfiles", "-o", outPath, asmPath]) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + process.Start(); + + await process.WaitForExitAsync(); + + var errors = await process.StandardError.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(errors)) + { + await Console.Error.WriteLineAsync("gcc error when assembling:\n" + errors); + } + + return process.ExitCode == 0; + } +} \ No newline at end of file diff --git a/compiler/NubLang.CLI/NubLang.CLI.csproj b/compiler/NubLang.CLI/NubLang.CLI.csproj new file mode 100644 index 0000000..0550d0f --- /dev/null +++ b/compiler/NubLang.CLI/NubLang.CLI.csproj @@ -0,0 +1,16 @@ + + + + nubc + Exe + net9.0 + enable + enable + true + + + + + + + diff --git a/compiler/NubLang.CLI/Options.cs b/compiler/NubLang.CLI/Options.cs new file mode 100644 index 0000000..acef2bc --- /dev/null +++ b/compiler/NubLang.CLI/Options.cs @@ -0,0 +1,9 @@ +using NubLang.Code; + +namespace NubLang.CLI; + +public class Options +{ + public string? OutputPath { get; set; } + public List Files { get; } = []; +} \ No newline at end of file diff --git a/compiler/NubLang.CLI/Program.cs b/compiler/NubLang.CLI/Program.cs new file mode 100644 index 0000000..8421840 --- /dev/null +++ b/compiler/NubLang.CLI/Program.cs @@ -0,0 +1,138 @@ +using System.Reflection; +using NubLang.CLI; +using NubLang.Code; +using NubLang.Diagnostics; +using NubLang.Generation.QBE; +using NubLang.Parsing; +using NubLang.Parsing.Syntax; +using NubLang.Tokenization; +using NubLang.TypeChecking; +using Module = NubLang.TypeChecking.Module; + +var options = new Options(); + +for (var i = 0; i < args.Length; i++) +{ + var arg = args[i]; + switch (arg) + { + case "-o": + { + ++i; + if (i >= args.Length) + { + return 1; + } + + options.OutputPath = args[i]; + break; + } + default: + { + options.Files.Add(new SourceFile(arg)); + break; + } + } +} + +foreach (var file in options.Files) +{ + if (!File.Exists(file.Path)) + { + Console.Error.WriteLine($"File '{file}' does not exist"); + return 1; + } +} + +var diagnostics = new List(); + +var syntaxTrees = new List(); +foreach (var file in options.Files) +{ + var tokenizer = new Tokenizer(file); + var tokens = tokenizer.Tokenize().ToList(); + diagnostics.AddRange(tokenizer.GetDiagnostics()); + + var parser = new Parser(); + var syntaxTree = parser.Parse(tokens); + + diagnostics.AddRange(parser.GetDiagnostics()); + + syntaxTrees.Add(syntaxTree); +} + +var moduleSignatures = ModuleSignature.CollectFromSyntaxTrees(syntaxTrees); +var modules = Module.CollectFromSyntaxTrees(syntaxTrees); + +var typedModules = new List(); + +foreach (var module in modules) +{ + var typeChecker = new TypeChecker(module, moduleSignatures); + var typedModule = typeChecker.CheckModule(); + diagnostics.AddRange(typeChecker.GetDiagnostics()); + typedModules.Add(typedModule); +} + +foreach (var diagnostic in diagnostics) +{ + Console.Error.WriteLine(diagnostic.FormatANSI()); +} + +if (diagnostics.Any(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)) +{ + return 1; +} + +var objectFiles = new List(); + +for (var i = 0; i < typedModules.Count; i++) +{ + var typedModule = typedModules[i]; + var outFileName = Path.Combine(".build", Path.ChangeExtension(options.Files[i].Path, null)); + + var outFileDir = Path.GetDirectoryName(outFileName); + if (!string.IsNullOrEmpty(outFileDir)) + { + Directory.CreateDirectory(outFileDir); + } + + var generator = new QBEGenerator(typedModule, moduleSignatures); + var ssa = generator.Emit(); + + var ssaFilePath = Path.ChangeExtension(outFileName, "ssa"); + File.WriteAllText(ssaFilePath, ssa); + + var asmFilePath = Path.ChangeExtension(outFileName, "s"); + var qbeSuccess = await QBE.Invoke(ssaFilePath, asmFilePath); + + if (!qbeSuccess) + { + return 1; + } + + var objFilePath = Path.ChangeExtension(outFileName, "o"); + var asmSuccess = await GCC.Assemble(asmFilePath, objFilePath); + + if (!asmSuccess) + { + return 1; + } + + objectFiles.Add(objFilePath); +} + +var outPath = options.OutputPath ?? Path.Combine("out.a"); +var outDir = Path.GetDirectoryName(outPath); +if (!string.IsNullOrEmpty(outDir)) +{ + Directory.CreateDirectory(outDir); +} + +var archiveResult = await Archive.Invoke(outPath, objectFiles); +if (!archiveResult) +{ + return 1; +} + +return 0; \ No newline at end of file diff --git a/compiler/NubLang.CLI/QBE.cs b/compiler/NubLang.CLI/QBE.cs new file mode 100644 index 0000000..52e37c5 --- /dev/null +++ b/compiler/NubLang.CLI/QBE.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; + +namespace NubLang.CLI; + +public static class QBE +{ + public static async Task Invoke(string ssaPath, string outPath) + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo("qbe", ["-o", outPath, ssaPath]) + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + process.Start(); + + await process.WaitForExitAsync(); + + var errors = await process.StandardError.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(errors)) + { + await Console.Error.WriteLineAsync("qbe error:\n" + errors); + } + + return process.ExitCode == 0; + } +} \ No newline at end of file diff --git a/compiler/NubLang/Code/SourceFile.cs b/compiler/NubLang/Code/SourceFile.cs new file mode 100644 index 0000000..48370c4 --- /dev/null +++ b/compiler/NubLang/Code/SourceFile.cs @@ -0,0 +1,41 @@ +namespace NubLang.Code; + +public class SourceFile +{ + private string? _content; + + public SourceFile(string path) + { + Path = path ?? throw new ArgumentNullException(nameof(path)); + } + + public string Path { get; } + + public string GetText() => _content ??= File.ReadAllText(Path); + public override string ToString() => Path; + + public override bool Equals(object? obj) + { + return obj is SourceFile other && other.Path == Path; + } + + public override int GetHashCode() + { + return HashCode.Combine(typeof(SourceFile), Path); + } + + public static bool operator ==(SourceFile? left, SourceFile? right) => Equals(left, right); + public static bool operator !=(SourceFile? left, SourceFile? right) => !Equals(left, right); +} + +public class SourceFileSpan +{ + public SourceFileSpan(SourceFile sourceFile, SourceSpan span) + { + SourceFile = sourceFile; + Span = span; + } + + public SourceFile SourceFile { get; } + public SourceSpan Span { get; } +} \ No newline at end of file diff --git a/compiler/NubLang/Code/SourceLocation.cs b/compiler/NubLang/Code/SourceLocation.cs new file mode 100644 index 0000000..38bf900 --- /dev/null +++ b/compiler/NubLang/Code/SourceLocation.cs @@ -0,0 +1,42 @@ +namespace NubLang.Code; + +public readonly struct SourceLocation : IEquatable +{ + public static SourceLocation Zero => new(0, 0); + + public SourceLocation(int line, int column) + { + Line = line; + Column = column; + } + + public int Line { get; } + public int Column { get; } + + public override string ToString() + { + return $"{Line}:{Column}"; + } + + public override bool Equals(object? obj) + { + return obj is SourceLocation other && Equals(other); + } + + public bool Equals(SourceLocation other) + { + return Line == other.Line && Column == other.Column; + } + + public override int GetHashCode() + { + return HashCode.Combine(typeof(SourceLocation), Line, Column); + } + + public static bool operator ==(SourceLocation left, SourceLocation right) => Equals(left, right); + public static bool operator !=(SourceLocation left, SourceLocation right) => !Equals(left, right); + public static bool operator <(SourceLocation left, SourceLocation right) => left.Line < right.Line || (left.Line == right.Line && left.Column < right.Column); + public static bool operator >(SourceLocation left, SourceLocation right) => left.Line > right.Line || (left.Line == right.Line && left.Column > right.Column); + public static bool operator <=(SourceLocation left, SourceLocation right) => left.Line <= right.Line || (left.Line == right.Line && left.Column <= right.Column); + public static bool operator >=(SourceLocation left, SourceLocation right) => left.Line >= right.Line || (left.Line == right.Line && left.Column >= right.Column); +} \ No newline at end of file diff --git a/compiler/NubLang/Code/SourceSpan.cs b/compiler/NubLang/Code/SourceSpan.cs new file mode 100644 index 0000000..3e7a89b --- /dev/null +++ b/compiler/NubLang/Code/SourceSpan.cs @@ -0,0 +1,57 @@ +namespace NubLang.Code; + +public readonly struct SourceSpan : IEquatable +{ + public static SourceSpan Zero => new(SourceLocation.Zero, SourceLocation.Zero); + + public static SourceSpan Merge(params IEnumerable spans) + { + var spanArray = spans as SourceSpan[] ?? spans.ToArray(); + + if (spanArray.Length == 0) + { + return Zero; + } + + var minStart = spanArray.Min(s => s.Start); + var maxEnd = spanArray.Max(s => s.End); + + return new SourceSpan(minStart, maxEnd); + } + + public SourceSpan(SourceLocation start, SourceLocation end) + { + if (start > end) + { + throw new ArgumentException("Start location cannot be after end location"); + } + + Start = start; + End = end; + } + + public SourceLocation Start { get; } + public SourceLocation End { get; } + + public override string ToString() + { + if (Start == End) + { + return $"{Start}"; + } + + if (Start.Line == End.Line) + { + return Start.Column == End.Column ? $"{Start}" : $"{Start.Line}:{Start.Column}-{End.Column}"; + } + + return $"{Start}-{End}"; + } + + public bool Equals(SourceSpan other) => Start == other.Start && End == other.End; + public override bool Equals(object? obj) => obj is SourceSpan other && Equals(other); + public override int GetHashCode() => HashCode.Combine(typeof(SourceSpan), Start, End); + + public static bool operator ==(SourceSpan left, SourceSpan right) => Equals(left, right); + public static bool operator !=(SourceSpan left, SourceSpan right) => !Equals(left, right); +} \ No newline at end of file diff --git a/compiler/NubLang/Diagnostics/ConsoleColors.cs b/compiler/NubLang/Diagnostics/ConsoleColors.cs new file mode 100644 index 0000000..1a6f7a3 --- /dev/null +++ b/compiler/NubLang/Diagnostics/ConsoleColors.cs @@ -0,0 +1,56 @@ +namespace NubLang.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; + } +} \ No newline at end of file diff --git a/compiler/NubLang/Diagnostics/Diagnostic.cs b/compiler/NubLang/Diagnostics/Diagnostic.cs new file mode 100644 index 0000000..b1fdd5f --- /dev/null +++ b/compiler/NubLang/Diagnostics/Diagnostic.cs @@ -0,0 +1,325 @@ +using System.Text; +using NubLang.Code; +using NubLang.Parsing.Syntax; +using NubLang.Tokenization; + +namespace NubLang.Diagnostics; + +public class Diagnostic +{ + public class DiagnosticBuilder + { + private readonly DiagnosticSeverity _severity; + private readonly string _message; + private SourceFileSpan? _fileSpan; + private string? _help; + + public DiagnosticBuilder(DiagnosticSeverity severity, string message) + { + _severity = severity; + _message = message; + } + + public DiagnosticBuilder At(SyntaxNode? node) + { + if (node != null) + { + var first = node.Tokens.FirstOrDefault(); + if (first?.FileSpan != null) + { + var span = SourceSpan.Merge(node.Tokens.Select(x => x.FileSpan?.Span ?? SourceSpan.Zero)); + At(new SourceFileSpan(first.FileSpan.SourceFile, span)); + } + } + + return this; + } + + public DiagnosticBuilder At(Token? token) + { + if (token != null) + { + At(token.FileSpan); + } + + return this; + } + + public DiagnosticBuilder At(SourceFileSpan? fileSpan) + { + if (fileSpan != null) + { + _fileSpan = fileSpan; + } + + return this; + } + + public DiagnosticBuilder WithHelp(string help) + { + _help = help; + return this; + } + + public Diagnostic Build() => new(_severity, _message, _help, _fileSpan); + } + + 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 string? Help { get; } + public SourceFileSpan? FileSpan { get; } + + private Diagnostic(DiagnosticSeverity severity, string message, string? help, SourceFileSpan? fileSpan) + { + Severity = severity; + Message = message; + Help = help; + FileSpan = fileSpan; + } + + public string FormatANSI() + { + var sb = new StringBuilder(); + + sb.Append(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), + _ => ConsoleColors.Colorize("unknown", ConsoleColors.Bold + ConsoleColors.White) + }); + + if (FileSpan != null) + { + sb.Append(ConsoleColors.Colorize($" at {FileSpan.SourceFile.Path}:{FileSpan.Span}", ConsoleColors.Faint)); + } + + sb.Append(": "); + sb.Append(ConsoleColors.Colorize(Message, ConsoleColors.BrightWhite)); + + if (FileSpan != null) + { + sb.AppendLine(); + var text = FileSpan.SourceFile.GetText(); + + var lines = text.Split('\n'); + + var startLine = FileSpan.Span.Start.Line; + var endLine = FileSpan.Span.End.Line; + + const int CONTEXT_LINES = 3; + + var contextStartLine = Math.Max(1, startLine - CONTEXT_LINES); + var contextEndLine = Math.Min(lines.Length, endLine + CONTEXT_LINES); + + var numberPadding = contextEndLine.ToString().Length; + var codePadding = lines.Skip(contextStartLine - 1).Take(contextEndLine - contextStartLine).Max(x => x.Length); + + sb.Append('╭'); + sb.Append(new string('─', numberPadding + 2)); + sb.Append('┬'); + sb.Append(new string('─', codePadding + 2)); + sb.Append('╮'); + sb.AppendLine(); + + var tokenizer = new Tokenizer(FileSpan.SourceFile); + var tokens = tokenizer.Tokenize().ToList(); + + for (var i = contextStartLine; i <= contextEndLine; i++) + { + var line = lines[i - 1]; + + sb.Append("│ "); + sb.Append(i.ToString().PadRight(numberPadding)); + sb.Append(" │ "); + sb.Append(ApplySyntaxHighlighting(line.PadRight(codePadding), i, tokens)); + sb.Append(" │"); + sb.AppendLine(); + + if (i >= startLine && i <= endLine) + { + var markerStartColumn = 1; + var markerEndColumn = line.Length + 1; + + if (i == startLine) + { + markerStartColumn = FileSpan.Span.Start.Column; + } + + if (i == endLine) + { + markerEndColumn = FileSpan.Span.End.Column; + } + + var markerLength = markerEndColumn - markerStartColumn; + var marker = new string('^', markerLength); + + var markerColor = Severity switch + { + DiagnosticSeverity.Info => ConsoleColors.Blue, + DiagnosticSeverity.Warning => ConsoleColors.Yellow, + DiagnosticSeverity.Error => ConsoleColors.Red, + _ => ConsoleColors.White + }; + + sb.Append("│ "); + sb.Append(new string(' ', numberPadding)); + sb.Append(" │ "); + sb.Append(new string(' ', markerStartColumn - 1)); + sb.Append(ConsoleColors.Colorize(marker, markerColor)); + sb.Append(new string(' ', codePadding - markerEndColumn + 1)); + sb.Append(" │"); + sb.AppendLine(); + } + } + + sb.Append('╰'); + sb.Append(new string('─', numberPadding + 2)); + sb.Append('┴'); + sb.Append(new string('─', codePadding + 2)); + sb.Append('╯'); + } + + if (Help != null) + { + sb.AppendLine(); + sb.Append(ConsoleColors.Colorize($"help: {Help}", ConsoleColors.Cyan)); + } + + return sb.ToString(); + } + + private static string ApplySyntaxHighlighting(string line, int lineNumber, List tokens) + { + var sb = new StringBuilder(); + var lineTokens = tokens + .Where(t => t.FileSpan.Span.Start.Line == lineNumber) + .OrderBy(t => t.FileSpan.Span.Start.Column) + .ToList(); + + if (lineTokens.Count == 0) + { + return line; + } + + var currentColumn = 1; + + foreach (var token in lineTokens) + { + var tokenStart = token.FileSpan.Span.Start.Column; + var tokenEnd = token.FileSpan.Span.End.Column; + + if (tokenStart > currentColumn) + { + var beforeToken = line.Substring(currentColumn - 1, tokenStart - currentColumn); + sb.Append(beforeToken); + } + + var tokenLength = tokenEnd - tokenStart; + if (tokenStart - 1 + tokenLength <= line.Length) + { + var tokenText = line.Substring(tokenStart - 1, tokenLength); + + var coloredToken = ColorizeToken(token, tokenText); + sb.Append(coloredToken); + } + + currentColumn = tokenEnd; + } + + if (currentColumn <= line.Length) + { + var remaining = line[(currentColumn - 1)..]; + sb.Append(remaining); + } + + return sb.ToString(); + } + + private static string ColorizeToken(Token token, string tokenText) + { + switch (token) + { + case IdentifierToken: + { + return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightWhite); + } + case LiteralToken literal: + { + if (literal.Kind == LiteralKind.String) + { + return ConsoleColors.Colorize(tokenText, ConsoleColors.Green); + } + + return ConsoleColors.Colorize(tokenText, ConsoleColors.Magenta); + } + case SymbolToken symbolToken: + { + switch (symbolToken.Symbol) + { + case Symbol.Func: + case Symbol.Return: + case Symbol.If: + case Symbol.Else: + case Symbol.While: + case Symbol.Break: + case Symbol.Continue: + case Symbol.Struct: + case Symbol.Let: + case Symbol.Calls: + case Symbol.Interface: + case Symbol.For: + case Symbol.Extern: + { + return ConsoleColors.Colorize(tokenText, ConsoleColors.Bold + ConsoleColors.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 ConsoleColors.Colorize(tokenText, ConsoleColors.Yellow); + } + case Symbol.Colon: + case Symbol.OpenParen: + case Symbol.CloseParen: + case Symbol.OpenBrace: + case Symbol.CloseBrace: + case Symbol.OpenBracket: + case Symbol.CloseBracket: + case Symbol.Comma: + case Symbol.Period: + case Symbol.Semi: + { + return ConsoleColors.Colorize(tokenText, ConsoleColors.BrightBlack); + } + } + + break; + } + } + + return tokenText; + } +} + +public enum DiagnosticSeverity +{ + Info, + Warning, + Error +} \ No newline at end of file diff --git a/compiler/NubLang/Generation/QBE/QBEGenerator.cs b/compiler/NubLang/Generation/QBE/QBEGenerator.cs new file mode 100644 index 0000000..6040310 --- /dev/null +++ b/compiler/NubLang/Generation/QBE/QBEGenerator.cs @@ -0,0 +1,1439 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text; +using NubLang.Tokenization; +using NubLang.TypeChecking; +using NubLang.TypeChecking.Node; + +namespace NubLang.Generation.QBE; + +public class QBEGenerator +{ + private readonly QBEWriter _writer; + private readonly TypedModule _module; + private readonly IReadOnlyDictionary _moduleSignatures; + + private readonly List _cStringLiterals = []; + private readonly List _stringLiterals = []; + private readonly Stack _breakLabels = []; + private readonly Stack _continueLabels = []; + private int _tmpIndex; + private int _labelIndex; + private int _cStringLiteralIndex; + private int _stringLiteralIndex; + private bool _codeIsReachable = true; + + public QBEGenerator(TypedModule module, IReadOnlyDictionary moduleSignatures) + { + _module = module; + _moduleSignatures = moduleSignatures; + _writer = new QBEWriter(); + } + + public string Emit() + { + _cStringLiterals.Clear(); + _stringLiterals.Clear(); + _breakLabels.Clear(); + _continueLabels.Clear(); + _tmpIndex = 0; + _labelIndex = 0; + _cStringLiteralIndex = 0; + _stringLiteralIndex = 0; + _codeIsReachable = true; + + foreach (var (module, signature) in _moduleSignatures) + { + foreach (var structType in signature.StructTypes) + { + EmitStructType(module, structType); + _writer.NewLine(); + } + } + + foreach (var structDef in _module.Definitions.OfType()) + { + EmitStructDefinition(structDef); + _writer.NewLine(); + } + + foreach (var funcDef in _module.Definitions.OfType()) + { + EmitFuncDefinition(funcDef); + _writer.NewLine(); + } + + // foreach (var structDef in _module.Definitions.OfType().Where(x => x.InterfaceImplementations.Count > 0)) + // { + // _writer.Write($"data {StructVtableName(_module.Name, structDef.Name)} = {{ "); + // + // foreach (var interfaceImplementation in structDef.InterfaceImplementations) + // { + // var interfaceDef = _definitionTable.LookupInterface(interfaceImplementation.Name); + // foreach (var func in interfaceDef.Functions) + // { + // _writer.Write($"l {StructFuncName(_module.Name, structDef.Name, func.Name)}, "); + // } + // } + // + // _writer.WriteLine("}"); + // } + + foreach (var cStringLiteral in _cStringLiterals) + { + _writer.WriteLine($"data {cStringLiteral.Name} = {{ b \"{cStringLiteral.Value}\", b 0 }}"); + } + + foreach (var stringLiteral in _stringLiterals) + { + var bytes = Encoding.UTF8.GetBytes(stringLiteral.Value).Select(b => $"b {b}"); + _writer.WriteLine($"data {stringLiteral.Name} = {{ l {stringLiteral.Value.Length}, {string.Join(", ", bytes)} }}"); + } + + return _writer.ToString(); + } + + private static string QBEAssign(TypeNode type) + { + if (type.IsSimpleType(out var simpleType, out _)) + { + return simpleType.StorageSize switch + { + StorageSize.I8 or StorageSize.U8 or StorageSize.I16 or StorageSize.U16 or StorageSize.I32 or StorageSize.U32 => "=w", + StorageSize.I64 or StorageSize.U64 => "=l", + StorageSize.F32 => "=s", + StorageSize.F64 => "=d", + _ => throw new ArgumentOutOfRangeException(nameof(simpleType.StorageSize)) + }; + } + + return "=l"; + } + + private void EmitStore(TypeNode type, string value, string destination) + { + string store; + + if (type.IsSimpleType(out var simpleType, out _)) + { + store = simpleType.StorageSize switch + { + StorageSize.I8 or StorageSize.U8 => "storeb", + StorageSize.I16 or StorageSize.U16 => "storeh", + StorageSize.I32 or StorageSize.U32 => "storew", + StorageSize.I64 or StorageSize.U64 => "storel", + StorageSize.F32 => "stores", + StorageSize.F64 => "stored", + _ => throw new ArgumentOutOfRangeException(nameof(simpleType.StorageSize)) + }; + } + else + { + store = "storel"; + } + + _writer.Indented($"{store} {value}, {destination}"); + } + + private string EmitLoad(TypeNode type, string from) + { + string load; + + if (type.IsSimpleType(out var simpleType, out _)) + { + load = simpleType.StorageSize switch + { + StorageSize.I64 or StorageSize.U64 => "loadl", + StorageSize.I32 or StorageSize.U32 => "loadw", + StorageSize.I16 => "loadsh", + StorageSize.I8 => "loadsb", + StorageSize.U16 => "loaduh", + StorageSize.U8 => "loadub", + StorageSize.F64 => "loadd", + StorageSize.F32 => "loads", + _ => throw new ArgumentOutOfRangeException(nameof(simpleType.StorageSize)) + }; + } + else + { + load = "loadl"; + } + + var into = TmpName(); + + _writer.Indented($"{into} {QBEAssign(type)} {load} {from}"); + + return into; + } + + private void EmitMemset(string destination, int value, string length) + { + var count = TmpName(); + _writer.Indented($"{count} =l copy 0"); + + var loopLabel = LabelName(); + _writer.WriteLine(loopLabel); + + var continueLabel = LabelName(); + var doneLabel = LabelName(); + var condition = TmpName(); + _writer.Indented($"{condition} =w cultl {count}, {length}"); + _writer.Indented($"jnz {condition}, {continueLabel}, {doneLabel}"); + + _writer.WriteLine(continueLabel); + + var destinationAddress = TmpName(); + _writer.Indented($"{destinationAddress} =l add {destination}, {count}"); + + _writer.Indented($"storeb {value}, {destinationAddress}"); + + _writer.Indented($"{count} =l add {count}, 1"); + _writer.Indented($"jmp {loopLabel}"); + + _writer.WriteLine(doneLabel); + } + + private void EmitMemcpy(string source, string destination, string length) + { + var count = TmpName(); + _writer.Indented($"{count} =l copy 0"); + + var loopLabel = LabelName(); + _writer.WriteLine(loopLabel); + + var continueLabel = LabelName(); + var doneLabel = LabelName(); + var condition = TmpName(); + _writer.Indented($"{condition} =w cultl {count}, {length}"); + _writer.Indented($"jnz {condition}, {continueLabel}, {doneLabel}"); + + _writer.WriteLine(continueLabel); + + var sourceAddress = TmpName(); + _writer.Indented($"{sourceAddress} =l add {source}, {count}"); + + var destinationAddress = TmpName(); + _writer.Indented($"{destinationAddress} =l add {destination}, {count}"); + + var value = TmpName(); + _writer.Indented($"{value} =w loadub {sourceAddress}"); + _writer.Indented($"storeb {value}, {destinationAddress}"); + + _writer.Indented($"{count} =l add {count}, 1"); + _writer.Indented($"jmp {loopLabel}"); + + _writer.WriteLine(doneLabel); + } + + private string EmitArraySizeInBytes(ArrayTypeNode type, string array) + { + var size = TmpName(); + _writer.Indented($"{size} =l loadl {array}"); + _writer.Indented($"{size} =l mul {size}, {SizeOf(type.ElementType)}"); + _writer.Indented($"{size} =l add {size}, 8"); + return size; + } + + private string EmitCStringSizeInBytes(string cstring) + { + var count = TmpName(); + _writer.Indented($"{count} =l copy 0"); + + var loopLabel = LabelName(); + _writer.WriteLine(loopLabel); + + var address = TmpName(); + _writer.Indented($"{address} =l add {cstring}, {count}"); + + var value = TmpName(); + _writer.Indented($"{value} =w loadub {address}"); + + var notZeroLabel = LabelName(); + var zeroLabel = LabelName(); + _writer.Indented($"jnz {value}, {notZeroLabel}, {zeroLabel}"); + _writer.WriteLine(notZeroLabel); + _writer.Indented($"{count} =l add {count}, 1"); + _writer.Indented($"jmp {loopLabel}"); + _writer.WriteLine(zeroLabel); + + return count; + } + + private string EmitStringSizeInBytes(string nubstring) + { + var size = TmpName(); + _writer.Indented($"{size} =l loadl {nubstring}"); + _writer.Indented($"{size} =l add {size}, 8"); + return size; + } + + private void EmitCopyInto(ExpressionNode source, string destination) + { + // Simple types are passed in registers and can therefore just be stored + if (source.Type.IsSimpleType(out var simpleType, out var complexType)) + { + var value = EmitExpression(source); + EmitStore(simpleType, value, destination); + return; + } + + // Structs and interfaces has known sizes at compile time + if (complexType is StructTypeNode or InterfaceTypeNode) + { + var value = EmitExpression(source); + _writer.Indented($"blit {value}, {destination}, {SizeOf(complexType)}"); + } + // The rest of the complex types has unknown sizes + else + { + var value = EmitExpression(source); + var size = complexType switch + { + ArrayTypeNode arrayType => EmitArraySizeInBytes(arrayType, value), + CStringTypeNode => EmitCStringSizeInBytes(value), + StringTypeNode => EmitStringSizeInBytes(value), + _ => throw new ArgumentOutOfRangeException(nameof(source.Type)) + }; + + var buffer = TmpName(); + _writer.Indented($"{buffer} =l alloc8 {size}"); + EmitMemcpy(value, buffer, size); + EmitStore(complexType, buffer, destination); + } + } + + private string EmitCopy(ExpressionNode source) + { + // Allowlist for types which are safe to not copy + if (source is ArrayInitializerNode or StructInitializerNode or ConvertToInterfaceNode or LiteralNode) + { + return EmitExpression(source); + } + + // Simple types are passed in registers and therefore always copied + if (source.Type.IsSimpleType(out _, out var complexType)) + { + return EmitExpression(source); + } + + // For the rest, we figure out the size of the type and shallow copy them + var value = EmitExpression(source); + var destination = TmpName(); + + // Structs and interfaces has known sizes at compile time + if (complexType is StructTypeNode or InterfaceTypeNode) + { + var size = SizeOf(complexType); + _writer.Indented($"{destination} =l alloc8 {size}"); + _writer.Indented($"blit {value}, {destination}, {size}"); + } + // The rest of the complex types has unknown sizes + else + { + var size = complexType switch + { + ArrayTypeNode arrayType => EmitArraySizeInBytes(arrayType, value), + CStringTypeNode => EmitCStringSizeInBytes(value), + StringTypeNode => EmitStringSizeInBytes(value), + _ => throw new ArgumentOutOfRangeException(nameof(source.Type)) + }; + + _writer.Indented($"{destination} =l alloc8 {size}"); + EmitMemcpy(value, destination, size); + } + + return destination; + } + + // Utility to create QBE type names for function parameters and return types + private string FuncQBETypeName(TypeNode type) + { + if (type.IsSimpleType(out var simpleType, out var complexType)) + { + return simpleType.StorageSize switch + { + StorageSize.I64 or StorageSize.U64 => "l", + StorageSize.I32 or StorageSize.U32 => "w", + StorageSize.I16 => "sh", + StorageSize.I8 => "sb", + StorageSize.U16 => "uh", + StorageSize.U8 => "ub", + StorageSize.F64 => "d", + StorageSize.F32 => "s", + _ => throw new ArgumentOutOfRangeException() + }; + } + + if (complexType is StructTypeNode structType) + { + return StructTypeName(structType.Module, structType.Name); + } + + return "l"; + } + + private void EmitFuncDefinition(FuncNode funcDef) + { + if (funcDef.Body == null) return; + + _labelIndex = 0; + _tmpIndex = 0; + + _writer.Write("export function "); + + if (funcDef.Signature.ReturnType is not VoidTypeNode) + { + _writer.Write(FuncQBETypeName(funcDef.Signature.ReturnType) + ' '); + } + + _writer.Write(FuncName(_module.Name, funcDef.Name)); + + _writer.Write("("); + foreach (var parameter in funcDef.Signature.Parameters) + { + _writer.Write(FuncQBETypeName(parameter.Type) + $" %{parameter.Name}"); + } + + _writer.WriteLine(") {"); + _writer.WriteLine("@start"); + + EmitBlock(funcDef.Body); + + // Implicit return for void functions if no explicit return has been set + if (funcDef.Signature.ReturnType is VoidTypeNode && funcDef.Body.Statements.LastOrDefault() is not ReturnNode) + { + _writer.Indented("ret"); + } + + _writer.WriteLine("}"); + } + + private void EmitStructDefinition(StructNode structDef) + { + var type = TypeResolver.ResolveStructType(_module.Name, structDef.Name, _moduleSignatures); + + // _writer.WriteLine($"export function {StructCtorName(_module.Name, structDef.Name)}() {{"); + // _writer.WriteLine("@start"); + // _writer.Indented($"%struct =l alloc8 {SizeOf(type)}"); + // // todo(nub31): Finish constructor + // _writer.Indented("ret %struct"); + // _writer.WriteLine("}"); + + foreach (var function in structDef.Functions) + { + _labelIndex = 0; + _tmpIndex = 0; + + _writer.NewLine(); + _writer.Write("export function "); + + if (function.Signature.ReturnType is not VoidTypeNode) + { + _writer.Write(FuncQBETypeName(function.Signature.ReturnType) + ' '); + } + + _writer.Write(StructFuncName(_module.Name, structDef.Name, function.Name)); + + _writer.Write("(l %this, "); + foreach (var parameter in function.Signature.Parameters) + { + _writer.Write(FuncQBETypeName(parameter.Type) + $" %{parameter.Name}, "); + } + + _writer.WriteLine(") {"); + _writer.WriteLine("@start"); + + EmitBlock(function.Body); + + // Implicit return for void functions if no explicit return has been set + if (function.Signature.ReturnType is VoidTypeNode && function.Body.Statements.LastOrDefault() is not ReturnNode) + { + _writer.Indented("ret"); + } + + _writer.WriteLine("}"); + } + } + + private void EmitStructType(string module, StructTypeNode structType) + { + _writer.WriteLine($"type {StructTypeName(module, structType.Name)} = {{ "); + + foreach (var field in structType.Fields) + { + _writer.Indented($"{StructDefQBEType(field.Type)},"); + } + + _writer.WriteLine("}"); + return; + + string StructDefQBEType(TypeNode type) + { + if (type.IsSimpleType(out var simpleType, out var complexType)) + { + return simpleType.StorageSize switch + { + StorageSize.I64 or StorageSize.U64 => "l", + StorageSize.I32 or StorageSize.U32 => "w", + StorageSize.I16 or StorageSize.U16 => "h", + StorageSize.I8 or StorageSize.U8 => "b", + StorageSize.F64 => "d", + StorageSize.F32 => "s", + _ => throw new ArgumentOutOfRangeException() + }; + } + + if (complexType is StructTypeNode childStructType) + { + return StructTypeName(childStructType.Module, childStructType.Name); + } + + return "l"; + } + } + + private void EmitBlock(BlockNode block) + { + foreach (var statement in block.Statements) + { + if (_codeIsReachable) + { + EmitStatement(statement); + } + } + + _codeIsReachable = true; + } + + private void EmitStatement(StatementNode statement) + { + // var tokens = statement.Tokens.ToArray(); + // if (tokens.Length != 0) + // { + // _writer.WriteLine($"dbgloc {tokens[0].FileSpan.Span.Start.Line}"); + // } + + switch (statement) + { + case AssignmentNode assignment: + EmitAssignment(assignment); + break; + case BreakNode: + EmitBreak(); + break; + case ContinueNode: + EmitContinue(); + break; + case IfNode ifStatement: + EmitIf(ifStatement); + break; + case ReturnNode @return: + EmitReturn(@return); + break; + case StatementExpressionNode statementExpression: + EmitExpression(statementExpression.Expression); + break; + case VariableDeclarationNode variableDeclaration: + EmitVariableDeclaration(variableDeclaration); + break; + case WhileNode whileStatement: + EmitWhile(whileStatement); + break; + default: + throw new ArgumentOutOfRangeException(nameof(statement)); + } + } + + private void EmitAssignment(AssignmentNode assignment) + { + EmitCopyInto(assignment.Value, EmitAddressOfLValue(assignment.Target)); + } + + private void EmitBreak() + { + _writer.Indented($"jmp {_breakLabels.Peek()}"); + _codeIsReachable = false; + } + + private void EmitContinue() + { + _writer.Indented($"jmp {_continueLabels.Peek()}"); + _codeIsReachable = false; + } + + private void EmitIf(IfNode ifStatement) + { + var trueLabel = LabelName(); + var falseLabel = LabelName(); + var endLabel = LabelName(); + + var result = EmitExpression(ifStatement.Condition); + _writer.Indented($"jnz {result}, {trueLabel}, {falseLabel}"); + _writer.WriteLine(trueLabel); + EmitBlock(ifStatement.Body); + _writer.Indented($"jmp {endLabel}"); + _writer.WriteLine(falseLabel); + if (ifStatement.Else.HasValue) + { + ifStatement.Else.Value.Match(EmitIf, EmitBlock); + } + + _writer.WriteLine(endLabel); + } + + private void EmitReturn(ReturnNode @return) + { + if (@return.Value.HasValue) + { + var result = EmitExpression(@return.Value.Value); + _writer.Indented($"ret {result}"); + } + else + { + _writer.Indented("ret"); + } + } + + private void EmitVariableDeclaration(VariableDeclarationNode variableDeclaration) + { + var name = $"%{variableDeclaration.Name}"; + _writer.Indented($"{name} =l alloc8 {SizeOf(variableDeclaration.Type)}"); + + if (variableDeclaration.Assignment.HasValue) + { + EmitCopyInto(variableDeclaration.Assignment.Value, name); + } + } + + private void EmitWhile(WhileNode whileStatement) + { + var conditionLabel = LabelName(); + var iterationLabel = LabelName(); + var endLabel = LabelName(); + + _breakLabels.Push(endLabel); + _continueLabels.Push(conditionLabel); + + _writer.Indented($"jmp {conditionLabel}"); + _writer.WriteLine(iterationLabel); + EmitBlock(whileStatement.Body); + _writer.WriteLine(conditionLabel); + var result = EmitExpression(whileStatement.Condition); + _writer.Indented($"jnz {result}, {iterationLabel}, {endLabel}"); + _writer.WriteLine(endLabel); + + _continueLabels.Pop(); + _breakLabels.Pop(); + } + + private string EmitExpression(ExpressionNode expression) + { + // var tokens = expression.Tokens.ToArray(); + // if (tokens.Length != 0) + // { + // _writer.WriteLine($"dbgloc {tokens[0].FileSpan.Span.Start.Line}"); + // } + + return expression switch + { + ArrayInitializerNode arrayInitializer => EmitArrayInitializer(arrayInitializer), + StructInitializerNode structInitializer => EmitStructInitializer(structInitializer), + AddressOfNode addressOf => EmitAddressOf(addressOf), + DereferenceNode dereference => EmitDereference(dereference), + BinaryExpressionNode binary => EmitBinaryExpression(binary), + FuncCallNode funcCall => EmitFuncCall(funcCall), + InterfaceFuncCallNode interfaceFuncCall => EmitInterfaceFuncCall(interfaceFuncCall), + ConvertToInterfaceNode convertToInterface => EmitConvertToInterface(convertToInterface), + ConvertIntNode convertInt => EmitConvertInt(convertInt), + ConvertFloatNode convertFloat => EmitConvertFloat(convertFloat), + VariableIdentifierNode identifier => EmitVariableIdentifier(identifier), + FuncIdentifierNode funcIdentifier => EmitFuncIdentifier(funcIdentifier), + FuncParameterIdentifierNode funcParameterIdentifier => EmitParameterFuncIdentifier(funcParameterIdentifier), + LiteralNode literal => EmitLiteral(literal), + UnaryExpressionNode unaryExpression => EmitUnaryExpression(unaryExpression), + StructFieldAccessNode structFieldAccess => EmitStructFieldAccess(structFieldAccess), + StructFuncCallNode structFuncCall => EmitStructFuncCall(structFuncCall), + ArrayIndexAccessNode arrayIndex => EmitArrayIndexAccess(arrayIndex), + _ => throw new ArgumentOutOfRangeException(nameof(expression)) + }; + } + + private string EmitFuncIdentifier(FuncIdentifierNode funcIdent) + { + // todo(nub31): Support for extern funcs + return FuncName(funcIdent.Module, funcIdent.Name); + } + + private string EmitVariableIdentifier(VariableIdentifierNode variableIdent) + { + var address = EmitAddressOfVariableIdent(variableIdent); + + return variableIdent.Type.IsSimpleType(out _, out _) + ? EmitLoad(variableIdent.Type, address) + : address; + } + + private string EmitParameterFuncIdentifier(FuncParameterIdentifierNode funcParameterIdent) + { + return "%" + funcParameterIdent.Name; + } + + private string EmitArrayIndexAccess(ArrayIndexAccessNode arrayIndexAccess) + { + // var address = EmitAddressOfArrayIndexAccess(arrayIndexAccess); + // if (arrayIndexAccess.Type is StructTypeNode) + // { + // return address; + // } + // + // return EmitLoad(arrayIndexAccess.Type, address); + throw new NotImplementedException(); + } + + private string EmitArrayInitializer(ArrayInitializerNode arrayInitializer) + { + var capacity = EmitExpression(arrayInitializer.Capacity); + var elementSize = SizeOf(arrayInitializer.ElementType); + + var capacityInBytes = TmpName(); + _writer.Indented($"{capacityInBytes} =l mul {capacity}, {elementSize}"); + var totalSize = TmpName(); + _writer.Indented($"{totalSize} =l add {capacityInBytes}, 8"); + + var arrayPointer = TmpName(); + _writer.Indented($"{arrayPointer} =l alloc8 {totalSize}"); + _writer.Indented($"storel {capacity}, {arrayPointer}"); + + var dataPointer = TmpName(); + _writer.Indented($"{dataPointer} =l add {arrayPointer}, 8"); + EmitMemset(dataPointer, 0, capacityInBytes); + + return arrayPointer; + } + + private string EmitDereference(DereferenceNode dereference) + { + var address = EmitExpression(dereference.Expression); + if (dereference.Type is StructTypeNode) + { + return address; + } + + return EmitLoad(dereference.Type, address); + } + + private string EmitAddressOf(AddressOfNode addressOf) + { + return EmitAddressOfLValue(addressOf.LValue); + } + + private string EmitAddressOfLValue(LValueExpressionNode addressOf) + { + return addressOf switch + { + ArrayIndexAccessNode arrayIndexAccess => EmitAddressOfArrayIndexAccess(arrayIndexAccess), + StructFieldAccessNode structFieldAccess => EmitAddressOfStructFieldAccess(structFieldAccess), + VariableIdentifierNode variableIdent => EmitAddressOfVariableIdent(variableIdent), + _ => throw new ArgumentOutOfRangeException(nameof(addressOf)) + }; + } + + private string EmitAddressOfArrayIndexAccess(ArrayIndexAccessNode arrayIndexAccess) + { + var array = EmitExpression(arrayIndexAccess.Target); + var index = EmitExpression(arrayIndexAccess.Index); + + var elementType = ((ArrayTypeNode)arrayIndexAccess.Target.Type).ElementType; + + var offset = TmpName(); + _writer.Indented($"{offset} =l mul {index}, {SizeOf(elementType)}"); + _writer.Indented($"{offset} =l add {offset}, 8"); + _writer.Indented($"{offset} =l add {array}, {offset}"); + return offset; + } + + private string EmitAddressOfStructFieldAccess(StructFieldAccessNode structFieldAccess) + { + var target = EmitExpression(structFieldAccess.Target); + + var structType = TypeResolver.ResolveStructType(structFieldAccess.StructType.Module, structFieldAccess.StructType.Name, _moduleSignatures); + var offset = OffsetOf(structType, structFieldAccess.Field); + + var address = TmpName(); + _writer.Indented($"{address} =l add {target}, {offset}"); + return address; + } + + private string EmitAddressOfVariableIdent(VariableIdentifierNode variableIdent) + { + return "%" + variableIdent.Name; + } + + private string EmitBinaryExpression(BinaryExpressionNode binaryExpression) + { + var left = EmitExpression(binaryExpression.Left); + var right = EmitExpression(binaryExpression.Right); + + var outputName = TmpName(); + + var instruction = EmitBinaryInstructionForOperator(binaryExpression.Operator, binaryExpression.Left.Type); + + _writer.Indented($"{outputName} {QBEAssign(binaryExpression.Left.Type)} {instruction} {left}, {right}"); + return outputName; + } + + private static string EmitBinaryInstructionForOperator(BinaryOperator op, TypeNode type) + { + return op switch + { + BinaryOperator.RightShift => type switch + { + IntTypeNode { Signed: true } => "sar", + IntTypeNode { Signed: false } => "shr", + _ => throw new NotSupportedException($"Right shift not supported for type '{type}'") + }, + BinaryOperator.BitwiseAnd => "and", + BinaryOperator.BitwiseOr => "or", + BinaryOperator.BitwiseXor => "xor", + BinaryOperator.LeftShift => "shl", + BinaryOperator.Divide => type switch + { + IntTypeNode { Signed: true } => "div", + IntTypeNode { Signed: false } => "udiv", + FloatTypeNode => "div", + _ => throw new NotSupportedException($"Division not supported for type '{type}'") + }, + BinaryOperator.Modulo => type switch + { + IntTypeNode { Signed: true } => "rem", + IntTypeNode { Signed: false } => "urem", + _ => throw new NotSupportedException($"Modulo not supported for type '{type}'") + }, + BinaryOperator.Plus => "add", + BinaryOperator.Minus => "sub", + BinaryOperator.Multiply => "mul", + BinaryOperator.Equal => type switch + { + IntTypeNode intType => intType.Width switch + { + <= 32 => "ceqw", + 64 => "ceql", + _ => throw new ArgumentOutOfRangeException() + }, + FloatTypeNode floatType => floatType.Width switch + { + 32 => "ceqs", + 64 => "ceqd", + _ => throw new ArgumentOutOfRangeException() + }, + _ => throw new NotSupportedException($"Equality comparison not supported for type '{type}'") + }, + BinaryOperator.NotEqual => type switch + { + IntTypeNode intType => intType.Width switch + { + <= 32 => "cnew", + 64 => "cnel", + _ => throw new ArgumentOutOfRangeException() + }, + FloatTypeNode floatType => floatType.Width switch + { + 32 => "cnes", + 64 => "cned", + _ => throw new ArgumentOutOfRangeException() + }, + _ => throw new NotSupportedException($"Inequality comparison not supported for type '{type}'") + }, + BinaryOperator.LessThan => type switch + { + IntTypeNode { Signed: true } intType => intType.Width switch + { + <= 32 => "csltw", + 64 => "csltl", + _ => throw new ArgumentOutOfRangeException() + }, + IntTypeNode { Signed: false } intType => intType.Width switch + { + <= 32 => "cultw", + 64 => "cultl", + _ => throw new ArgumentOutOfRangeException() + }, + FloatTypeNode floatType => floatType.Width switch + { + 32 => "clts", + 64 => "cltd", + _ => throw new ArgumentOutOfRangeException() + }, + _ => throw new NotSupportedException($"Less than comparison not supported for type '{type}'") + }, + BinaryOperator.LessThanOrEqual => type switch + { + IntTypeNode { Signed: true } intType => intType.Width switch + { + <= 32 => "cslew", + 64 => "cslel", + _ => throw new ArgumentOutOfRangeException() + }, + IntTypeNode { Signed: false } intType => intType.Width switch + { + <= 32 => "culew", + 64 => "culel", + _ => throw new ArgumentOutOfRangeException() + }, + FloatTypeNode floatType => floatType.Width switch + { + 32 => "cles", + 64 => "cled", + _ => throw new ArgumentOutOfRangeException() + }, + _ => throw new NotSupportedException($"Less than or equal comparison not supported for type '{type}'") + }, + BinaryOperator.GreaterThan => type switch + { + IntTypeNode { Signed: true } intType => intType.Width switch + { + <= 32 => "csgtw", + 64 => "csgtl", + _ => throw new ArgumentOutOfRangeException() + }, + IntTypeNode { Signed: false } intType => intType.Width switch + { + <= 32 => "cugtw", + 64 => "cugtl", + _ => throw new ArgumentOutOfRangeException() + }, + FloatTypeNode floatType => floatType.Width switch + { + 32 => "cgts", + 64 => "cgtd", + _ => throw new ArgumentOutOfRangeException() + }, + _ => throw new NotSupportedException($"Greater than comparison not supported for type '{type}'") + }, + BinaryOperator.GreaterThanOrEqual => type switch + { + IntTypeNode { Signed: true } intType => intType.Width switch + { + <= 32 => "csgew", + 64 => "csgel", + _ => throw new ArgumentOutOfRangeException() + }, + IntTypeNode { Signed: false } intType => intType.Width switch + { + <= 32 => "cugew", + 64 => "cugel", + _ => throw new ArgumentOutOfRangeException() + }, + FloatTypeNode floatType => floatType.Width switch + { + 32 => "cges", + 64 => "cged", + _ => throw new ArgumentOutOfRangeException() + }, + _ => throw new NotSupportedException($"Greater than or equal comparison not supported for type '{type}'") + }, + // todo(nub31): Implement short circuiting + BinaryOperator.LogicalAnd => "and", + BinaryOperator.LogicalOr => "or", + _ => throw new ArgumentOutOfRangeException(nameof(op)) + }; + } + + private string EmitLiteral(LiteralNode literal) + { + switch (literal.Kind) + { + case LiteralKind.Integer: + { + if (literal.Type is FloatTypeNode { Width: 32 }) + { + var value = float.Parse(literal.Value, CultureInfo.InvariantCulture); + var bits = BitConverter.SingleToInt32Bits(value); + return bits.ToString(); + } + + if (literal.Type is FloatTypeNode { Width: 64 }) + { + var value = double.Parse(literal.Value, CultureInfo.InvariantCulture); + var bits = BitConverter.DoubleToInt64Bits(value); + return bits.ToString(); + } + + if (literal.Type is IntTypeNode) + { + return literal.Value; + } + + break; + } + case LiteralKind.Float: + { + if (literal.Type is IntTypeNode) + { + return literal.Value.Split(".").First(); + } + + if (literal.Type is FloatTypeNode { Width: 32 }) + { + var value = float.Parse(literal.Value, CultureInfo.InvariantCulture); + var bits = BitConverter.SingleToInt32Bits(value); + return bits.ToString(); + } + + if (literal.Type is FloatTypeNode { Width: 64 }) + { + var value = double.Parse(literal.Value, CultureInfo.InvariantCulture); + var bits = BitConverter.DoubleToInt64Bits(value); + return bits.ToString(); + } + + break; + } + case LiteralKind.String: + { + if (literal.Type is StringTypeNode) + { + var stringLiteral = new StringLiteral(literal.Value, StringName()); + _stringLiterals.Add(stringLiteral); + return stringLiteral.Name; + } + + if (literal.Type is CStringTypeNode) + { + var cStringLiteral = new CStringLiteral(literal.Value, CStringName()); + _cStringLiterals.Add(cStringLiteral); + return cStringLiteral.Name; + } + + break; + } + case LiteralKind.Bool: + { + if (literal.Type is BoolTypeNode) + { + return bool.Parse(literal.Value) ? "1" : "0"; + } + + break; + } + } + + throw new NotSupportedException($"Cannot create literal of kind '{literal.Kind}' for type {literal.Type}"); + } + + private string EmitStructInitializer(StructInitializerNode structInitializer) + { + var destination = TmpName(); + var size = SizeOf(structInitializer.StructType); + _writer.Indented($"{destination} =l alloc8 {size}"); + + foreach (var (field, value) in structInitializer.Initializers) + { + var offset = TmpName(); + _writer.Indented($"{offset} =l add {destination}, {OffsetOf(structInitializer.StructType, field)}"); + EmitCopyInto(value, offset); + } + + return destination; + } + + private string EmitUnaryExpression(UnaryExpressionNode unaryExpression) + { + var operand = EmitExpression(unaryExpression.Operand); + var outputName = TmpName(); + + switch (unaryExpression.Operator) + { + case UnaryOperator.Negate: + { + switch (unaryExpression.Operand.Type) + { + case IntTypeNode { Signed: true, Width: 64 }: + _writer.Indented($"{outputName} =l neg {operand}"); + return outputName; + case IntTypeNode { Signed: true, Width: 8 or 16 or 32 }: + _writer.Indented($"{outputName} =w neg {operand}"); + return outputName; + case FloatTypeNode { Width: 64 }: + _writer.Indented($"{outputName} =d neg {operand}"); + return outputName; + case FloatTypeNode { Width: 32 }: + _writer.Indented($"{outputName} =s neg {operand}"); + return outputName; + } + + break; + } + case UnaryOperator.Invert: + { + switch (unaryExpression.Operand.Type) + { + case BoolTypeNode: + _writer.Indented($"{outputName} =w xor {operand}, 1"); + return outputName; + } + + break; + } + default: + { + throw new ArgumentOutOfRangeException(); + } + } + + throw new NotSupportedException($"Unary operator {unaryExpression.Operator} for type {unaryExpression.Operand.Type} not supported"); + } + + private string EmitStructFieldAccess(StructFieldAccessNode structFieldAccess) + { + var address = EmitAddressOfStructFieldAccess(structFieldAccess); + if (structFieldAccess.Type is StructTypeNode) + { + return address; + } + + return EmitLoad(structFieldAccess.Type, address); + } + + private string EmitStructFuncCall(StructFuncCallNode structFuncCall) + { + var func = StructFuncName(structFuncCall.StructType.Module, structFuncCall.StructType.Name, structFuncCall.Name); + + var thisParameter = EmitExpression(structFuncCall.StructExpression); + + List parameterStrings = [$"l {thisParameter}"]; + + foreach (var parameter in structFuncCall.Parameters) + { + var copy = EmitCopy(parameter); + parameterStrings.Add($"{FuncQBETypeName(parameter.Type)} {copy}"); + } + + if (structFuncCall.Type is VoidTypeNode) + { + _writer.Indented($"call {func}({string.Join(", ", parameterStrings)})"); + return string.Empty; + } + else + { + var outputName = TmpName(); + _writer.Indented($"{outputName} {QBEAssign(structFuncCall.Type)} call {func}({string.Join(", ", parameterStrings)})"); + return outputName; + } + } + + private string EmitInterfaceFuncCall(InterfaceFuncCallNode interfaceFuncCall) + { + var target = EmitExpression(interfaceFuncCall.InterfaceExpression); + + var functionIndex = interfaceFuncCall.InterfaceType.Functions.ToList().FindIndex(x => x.Name == interfaceFuncCall.Name); + var offset = functionIndex * 8; + + var vtable = TmpName(); + _writer.Indented($"{vtable} =l loadl {target}"); + + var funcOffset = TmpName(); + _writer.Indented($"{funcOffset} =l add {vtable}, {offset}"); + + var func = TmpName(); + _writer.Indented($"{func} =l loadl {funcOffset}"); + + var data = TmpName(); + _writer.Indented($"{data} =l add {target}, 8"); + _writer.Indented($"{data} =l loadl {data}"); + + List parameterStrings = [$"l {data}"]; + + foreach (var parameter in interfaceFuncCall.Parameters) + { + var copy = EmitCopy(parameter); + parameterStrings.Add($"{FuncQBETypeName(parameter.Type)} {copy}"); + } + + if (interfaceFuncCall.Type is VoidTypeNode) + { + _writer.Indented($"call {func}({string.Join(", ", parameterStrings)})"); + return string.Empty; + } + else + { + var outputName = TmpName(); + _writer.Indented($"{outputName} {QBEAssign(interfaceFuncCall.Type)} call {func}({string.Join(", ", parameterStrings)})"); + return outputName; + } + } + + private string EmitConvertToInterface(ConvertToInterfaceNode convertToInterface) + { + var implementation = EmitExpression(convertToInterface.Implementation); + + var vtableOffset = 0; + foreach (var interfaceImplementation in convertToInterface.StructType.InterfaceImplementations) + { + if (interfaceImplementation == convertToInterface.InterfaceType) + { + break; + } + + vtableOffset += interfaceImplementation.Functions.Count * 8; + } + + var destination = TmpName(); + _writer.Indented($"{destination} =l alloc8 {SizeOf(convertToInterface.InterfaceType)}"); + + var interfaceVtablePointer = TmpName(); + _writer.Indented($"{interfaceVtablePointer} =l add {StructVtableName(convertToInterface.StructType.Module, convertToInterface.StructType.Name)}, {vtableOffset}"); + _writer.Indented($"storel {interfaceVtablePointer}, {destination}"); + + var objectPointer = TmpName(); + _writer.Indented($"{objectPointer} =l add {destination}, 8"); + _writer.Indented($"storel {implementation}, {objectPointer}"); + + return destination; + } + + private string EmitConvertInt(ConvertIntNode convertInt) + { + var value = EmitExpression(convertInt.Value); + + if (convertInt.ValueType.Width >= convertInt.TargetType.Width) + { + return value; + } + + var method = convertInt.ValueType.Signed switch + { + true => convertInt.ValueType.Width switch + { + 8 => "extsb", + 16 => "extsh", + 32 => "extsw", + _ => throw new ArgumentOutOfRangeException() + }, + false => convertInt.ValueType.Width switch + { + 8 => "extub", + 16 => "extuh", + 32 => "extuw", + _ => throw new ArgumentOutOfRangeException() + } + }; + + var result = TmpName(); + _writer.Indented($"{result} {QBEAssign(convertInt.TargetType)} {method} {value}"); + return result; + } + + private string EmitConvertFloat(ConvertFloatNode convertFloat) + { + var value = EmitExpression(convertFloat.Value); + + if (convertFloat.ValueType.Width == convertFloat.TargetType.Width) + { + return value; + } + + var method = convertFloat.ValueType.Width switch + { + 32 => "exts", + 64 => "truncd", + _ => throw new ArgumentOutOfRangeException() + }; + + var result = TmpName(); + _writer.Indented($"{result} {QBEAssign(convertFloat.TargetType)} {method} {value}"); + return result; + } + + private string EmitFuncCall(FuncCallNode funcCall) + { + var funcPointer = EmitExpression(funcCall.Expression); + + var parameterStrings = new List(); + + foreach (var parameter in funcCall.Parameters) + { + var copy = EmitCopy(parameter); + parameterStrings.Add($"{FuncQBETypeName(parameter.Type)} {copy}"); + } + + if (funcCall.Type is VoidTypeNode) + { + _writer.Indented($"call {funcPointer}({string.Join(", ", parameterStrings)})"); + return string.Empty; + } + else + { + var outputName = TmpName(); + _writer.Indented($"{outputName} {QBEAssign(funcCall.Type)} call {funcPointer}({string.Join(", ", parameterStrings)})"); + return outputName; + } + } + + private static int SizeOf(TypeNode type) + { + return type switch + { + SimpleTypeNode simple => simple.StorageSize switch + { + StorageSize.Void => 0, + StorageSize.I8 or StorageSize.U8 => 1, + StorageSize.I16 or StorageSize.U16 => 2, + StorageSize.I32 or StorageSize.U32 or StorageSize.F32 => 4, + StorageSize.I64 or StorageSize.U64 or StorageSize.F64 => 8, + _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown storage size: {simple.StorageSize}") + }, + CStringTypeNode => 8, + StringTypeNode => 8, + ArrayTypeNode => 8, + StructTypeNode structType => CalculateStructSize(structType), + InterfaceTypeNode => 16, + _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type.GetType()}") + }; + } + + private static int CalculateStructSize(StructTypeNode structType) + { + var offset = 0; + + var fields = new List(structType.Fields.Count); + + foreach (var field in structType.Fields) + { + var fieldAlignment = AlignmentOf(field.Type); + offset = AlignTo(offset, fieldAlignment); + offset += SizeOf(field.Type); + } + + var structAlignment = CalculateStructAlignment(structType); + return AlignTo(offset, structAlignment); + } + + private static int AlignmentOf(TypeNode type) + { + return type switch + { + SimpleTypeNode simple => simple.StorageSize switch + { + StorageSize.Void => 1, + StorageSize.I8 or StorageSize.U8 => 1, + StorageSize.I16 or StorageSize.U16 => 2, + StorageSize.I32 or StorageSize.U32 or StorageSize.F32 => 4, + StorageSize.I64 or StorageSize.U64 or StorageSize.F64 => 8, + _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown storage size: {simple.StorageSize}") + }, + CStringTypeNode => 8, + StringTypeNode => 8, + ArrayTypeNode => 8, + StructTypeNode structType => CalculateStructAlignment(structType), + InterfaceTypeNode => 8, + _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown type: {type.GetType()}") + }; + } + + private static int CalculateStructAlignment(StructTypeNode structType) + { + var maxAlignment = 1; + + if (structType.InterfaceImplementations.Any()) + { + maxAlignment = Math.Max(maxAlignment, 8); + } + + foreach (var field in structType.Fields) + { + var fieldAlignment = AlignmentOf(field.Type); + maxAlignment = Math.Max(maxAlignment, fieldAlignment); + } + + return maxAlignment; + } + + private static int AlignTo(int offset, int alignment) + { + return (offset + alignment - 1) & ~(alignment - 1); + } + + private static int OffsetOf(StructTypeNode structDef, string member) + { + var offset = 0; + + foreach (var field in structDef.Fields) + { + if (field.Name == member) + { + return offset; + } + + var fieldAlignment = AlignmentOf(field.Type); + + offset = AlignTo(offset, fieldAlignment); + offset += SizeOf(field.Type); + } + + throw new UnreachableException($"Member '{member}' not found in struct"); + } + + #region Naming utilities + + private string TmpName() + { + return $"%t{++_tmpIndex}"; + } + + private string LabelName() + { + return $"@l{++_labelIndex}"; + } + + private string CStringName() + { + return $"$cstring{++_cStringLiteralIndex}"; + } + + private string StringName() + { + return $"$string{++_stringLiteralIndex}"; + } + + private string FuncName(string module, string name) + { + var type = TypeResolver.ResolveFunctionType(module, name, _moduleSignatures); + var symbol = type.ExternSymbol ?? $"{module}.{name}"; + return "$" + symbol; + } + + private string StructTypeName(string module, string name) + { + return $":{module}.{name}"; + } + + private string StructFuncName(string module, string structName, string funcName) + { + return $"${module}.{structName}_func.{funcName}"; + } + + private string StructCtorName(string module, string structName) + { + return $"${module}.{structName}_ctor"; + } + + private string StructVtableName(string module, string structName) + { + return $"${module}.{structName}_vtable"; + } + + #endregion +} + +public class StringLiteral(string value, string name) +{ + public string Value { get; } = value; + public string Name { get; } = name; +} + +public class CStringLiteral(string value, string name) +{ + public string Value { get; } = value; + public string Name { get; } = name; +} \ No newline at end of file diff --git a/compiler/NubLang/Generation/QBE/QBEWriter.cs b/compiler/NubLang/Generation/QBE/QBEWriter.cs new file mode 100644 index 0000000..2ff8668 --- /dev/null +++ b/compiler/NubLang/Generation/QBE/QBEWriter.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace NubLang.Generation.QBE; + +internal class QBEWriter +{ + private readonly StringBuilder _builder = new(); + + public void Indented(string value) + { + _builder.Append('\t'); + _builder.AppendLine(value); + } + + public void Comment(string comment) + { + _builder.AppendLine("# " + comment); + } + + public void WriteLine(string text) + { + _builder.AppendLine(text); + } + + public void Write(string text) + { + _builder.Append(text); + } + + public void NewLine() + { + _builder.AppendLine(); + } + + public override string ToString() + { + return _builder.ToString(); + } +} \ No newline at end of file diff --git a/compiler/NubLang/NubLang.csproj b/compiler/NubLang/NubLang.csproj new file mode 100644 index 0000000..b682a68 --- /dev/null +++ b/compiler/NubLang/NubLang.csproj @@ -0,0 +1,10 @@ + + + + net9.0 + enable + enable + true + + + diff --git a/compiler/NubLang/Optional.cs b/compiler/NubLang/Optional.cs new file mode 100644 index 0000000..a467b5b --- /dev/null +++ b/compiler/NubLang/Optional.cs @@ -0,0 +1,82 @@ +using System.Diagnostics.CodeAnalysis; + +namespace NubLang; + +public static class Optional +{ + public static Optional Empty() => new(); + + /// + /// Alias for creating an Optional which allows for implicit types + /// + public static Optional OfNullable(TValue? value) + { + return value ?? Optional.Empty(); + } + + /// + /// Converts a nullable type to an Optional + /// + public static Optional ToOptional(this TValue? value) + { + return OfNullable(value); + } +} + +public readonly struct Optional +{ + public static Optional Empty() => new(); + + public static Optional 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 value) => new(value); + + public TValue Or(TValue other) + { + if (HasValue) + { + return Value; + } + + return other; + } +} \ No newline at end of file diff --git a/compiler/NubLang/Parsing/Parser.cs b/compiler/NubLang/Parsing/Parser.cs new file mode 100644 index 0000000..3766e34 --- /dev/null +++ b/compiler/NubLang/Parsing/Parser.cs @@ -0,0 +1,883 @@ +using System.Diagnostics.CodeAnalysis; +using NubLang.Diagnostics; +using NubLang.Parsing.Syntax; +using NubLang.Tokenization; + +namespace NubLang.Parsing; + +public sealed class Parser +{ + private readonly List _diagnostics = []; + private IReadOnlyList _tokens = []; + private int _tokenIndex; + private string _moduleName = string.Empty; + + private Token? CurrentToken => _tokenIndex < _tokens.Count ? _tokens[_tokenIndex] : null; + private bool HasToken => CurrentToken != null; + + public IReadOnlyList GetDiagnostics() + { + return _diagnostics; + } + + public SyntaxTree Parse(IReadOnlyList tokens) + { + _diagnostics.Clear(); + _tokens = tokens; + _tokenIndex = 0; + _moduleName = string.Empty; + + var metadata = ParseMetadata(); + var definitions = ParseDefinitions(); + + return new SyntaxTree(definitions, metadata); + } + + private SyntaxTreeMetadata ParseMetadata() + { + var imports = new List(); + + try + { + ExpectSymbol(Symbol.Module); + _moduleName = ExpectLiteral(LiteralKind.String).Value; + + while (TryExpectSymbol(Symbol.Import)) + { + imports.Add(ExpectIdentifier().Value); + } + } + catch (ParseException e) + { + _diagnostics.Add(e.Diagnostic); + while (HasToken) + { + if (CurrentToken is SymbolToken { Symbol: Symbol.Module or Symbol.Import }) + { + break; + } + + Next(); + } + } + + return new SyntaxTreeMetadata(_moduleName, imports); + } + + private List ParseDefinitions() + { + var definitions = new List(); + + while (HasToken) + { + try + { + var startIndex = _tokenIndex; + var exported = TryExpectSymbol(Symbol.Export); + + if (TryExpectSymbol(Symbol.Extern)) + { + var externSymbol = ExpectLiteral(LiteralKind.String).Value; + ExpectSymbol(Symbol.Func); + definitions.Add(ParseFunc(startIndex, exported, externSymbol)); + continue; + } + + var keyword = ExpectSymbol(); + var definition = keyword.Symbol switch + { + Symbol.Func => ParseFunc(startIndex, exported, null), + Symbol.Struct => ParseStruct(startIndex, exported), + Symbol.Interface => ParseInterface(startIndex, exported), + _ => throw new ParseException(Diagnostic + .Error($"Expected 'func', 'struct' or 'interface' but found '{keyword.Symbol}'") + .WithHelp("Valid definition keywords are 'func', 'struct' and 'interface'") + .At(keyword) + .Build()) + }; + + definitions.Add(definition); + } + catch (ParseException e) + { + _diagnostics.Add(e.Diagnostic); + while (HasToken) + { + if (CurrentToken is SymbolToken { Symbol: Symbol.Extern or Symbol.Func or Symbol.Struct or Symbol.Interface }) + { + break; + } + + Next(); + } + } + } + + return definitions; + } + + private FuncSignatureSyntax ParseFuncSignature() + { + var startIndex = _tokenIndex; + List parameters = []; + + ExpectSymbol(Symbol.OpenParen); + + while (!TryExpectSymbol(Symbol.CloseParen)) + { + parameters.Add(ParseFuncParameter()); + + if (!TryExpectSymbol(Symbol.Comma)) + { + ExpectSymbol(Symbol.CloseParen); + break; + } + } + + var returnType = TryExpectSymbol(Symbol.Colon) ? ParseType() : new VoidTypeSyntax([]); + + return new FuncSignatureSyntax(GetTokens(startIndex), parameters, returnType); + } + + private FuncParameterSyntax ParseFuncParameter() + { + var startIndex = _tokenIndex; + var name = ExpectIdentifier(); + ExpectSymbol(Symbol.Colon); + var type = ParseType(); + + return new FuncParameterSyntax(GetTokens(startIndex), name.Value, type); + } + + private FuncSyntax ParseFunc(int startIndex, bool exported, string? externSymbol) + { + var name = ExpectIdentifier(); + var signature = ParseFuncSignature(); + + BlockSyntax? body = null; + if (CurrentToken is SymbolToken { Symbol: Symbol.OpenBrace }) + { + Next(); + body = ParseBlock(); + } + + return new FuncSyntax(GetTokens(startIndex), name.Value, exported, externSymbol, signature, body); + } + + private DefinitionSyntax ParseStruct(int startIndex, bool exported) + { + var name = ExpectIdentifier(); + var interfaceImplementations = new List(); + + if (TryExpectSymbol(Symbol.Colon)) + { + do + { + var interfaceType = ParseType(); + interfaceImplementations.Add(interfaceType); + } while (TryExpectSymbol(Symbol.Comma)); + } + + ExpectSymbol(Symbol.OpenBrace); + + List fields = []; + List funcs = []; + + var fieldIndex = 0; + + while (!TryExpectSymbol(Symbol.CloseBrace)) + { + var memberStartIndex = _tokenIndex; + + if (TryExpectSymbol(Symbol.Func)) + { + var funcName = ExpectIdentifier().Value; + var funcSignature = ParseFuncSignature(); + var funcBody = ParseBlock(); + + funcs.Add(new StructFuncSyntax(GetTokens(memberStartIndex), funcName, funcSignature, funcBody)); + } + else + { + var fieldName = ExpectIdentifier().Value; + ExpectSymbol(Symbol.Colon); + var fieldType = ParseType(); + + var fieldValue = Optional.Empty(); + + if (TryExpectSymbol(Symbol.Assign)) + { + fieldValue = ParseExpression(); + } + + fields.Add(new StructFieldSyntax(GetTokens(memberStartIndex), fieldIndex++, fieldName, fieldType, fieldValue)); + } + } + + return new StructSyntax(GetTokens(startIndex), name.Value, exported, fields, funcs, interfaceImplementations); + } + + private InterfaceSyntax ParseInterface(int startIndex, bool exported) + { + var name = ExpectIdentifier(); + + ExpectSymbol(Symbol.OpenBrace); + + List functions = []; + + while (!TryExpectSymbol(Symbol.CloseBrace)) + { + var funcStartIndex = _tokenIndex; + + ExpectSymbol(Symbol.Func); + + var funcName = ExpectIdentifier().Value; + var signature = ParseFuncSignature(); + + functions.Add(new InterfaceFuncSyntax(GetTokens(funcStartIndex), funcName, signature)); + } + + return new InterfaceSyntax(GetTokens(startIndex), name.Value, exported, functions); + } + + private StatementSyntax ParseStatement() + { + if (CurrentToken is SymbolToken symbol) + { + switch (symbol.Symbol) + { + case Symbol.Return: + return ParseReturn(); + case Symbol.If: + return ParseIf(); + case Symbol.While: + return ParseWhile(); + case Symbol.Let: + return ParseVariableDeclaration(); + case Symbol.Break: + return ParseBreak(); + case Symbol.Continue: + return ParseContinue(); + } + } + + return ParseStatementExpression(); + } + + private StatementSyntax ParseStatementExpression() + { + var startIndex = _tokenIndex; + var expr = ParseExpression(); + + if (TryExpectSymbol(Symbol.Assign)) + { + var value = ParseExpression(); + return new AssignmentSyntax(GetTokens(startIndex), expr, value); + } + + return new StatementExpressionSyntax(GetTokens(startIndex), expr); + } + + private VariableDeclarationSyntax ParseVariableDeclaration() + { + var startIndex = _tokenIndex; + ExpectSymbol(Symbol.Let); + var name = ExpectIdentifier().Value; + + var explicitType = Optional.Empty(); + if (TryExpectSymbol(Symbol.Colon)) + { + explicitType = ParseType(); + } + + var assignment = Optional.Empty(); + if (TryExpectSymbol(Symbol.Assign)) + { + assignment = ParseExpression(); + } + + return new VariableDeclarationSyntax(GetTokens(startIndex), name, explicitType, assignment); + } + + private StatementSyntax ParseBreak() + { + var startIndex = _tokenIndex; + ExpectSymbol(Symbol.Break); + return new BreakSyntax(GetTokens(startIndex)); + } + + private StatementSyntax ParseContinue() + { + var startIndex = _tokenIndex; + ExpectSymbol(Symbol.Continue); + return new ContinueSyntax(GetTokens(startIndex)); + } + + private ReturnSyntax ParseReturn() + { + var startIndex = _tokenIndex; + ExpectSymbol(Symbol.Return); + + var value = Optional.Empty(); + + if (!TryExpectSymbol(Symbol.Semi)) + { + value = ParseExpression(); + } + + return new ReturnSyntax(GetTokens(startIndex), value); + } + + private IfSyntax ParseIf() + { + var startIndex = _tokenIndex; + ExpectSymbol(Symbol.If); + var condition = ParseExpression(); + var body = ParseBlock(); + + var elseStatement = Optional>.Empty(); + if (TryExpectSymbol(Symbol.Else)) + { + elseStatement = TryExpectSymbol(Symbol.If) + ? (Variant)ParseIf() + : (Variant)ParseBlock(); + } + + return new IfSyntax(GetTokens(startIndex), condition, body, elseStatement); + } + + private WhileSyntax ParseWhile() + { + var startIndex = _tokenIndex; + ExpectSymbol(Symbol.While); + var condition = ParseExpression(); + var body = ParseBlock(); + return new WhileSyntax(GetTokens(startIndex), condition, body); + } + + private ExpressionSyntax ParseExpression(int precedence = 0) + { + var startIndex = _tokenIndex; + var left = ParsePrimaryExpression(); + + while (CurrentToken is SymbolToken symbolToken && TryGetBinaryOperator(symbolToken.Symbol, out var op) && GetBinaryOperatorPrecedence(op.Value) >= precedence) + { + Next(); + var right = ParseExpression(GetBinaryOperatorPrecedence(op.Value) + 1); + left = new BinaryExpressionSyntax(GetTokens(startIndex), left, op.Value, right); + } + + return left; + } + + private static int GetBinaryOperatorPrecedence(BinaryOperatorSyntax operatorSyntax) + { + return operatorSyntax switch + { + BinaryOperatorSyntax.Multiply => 10, + BinaryOperatorSyntax.Divide => 10, + BinaryOperatorSyntax.Modulo => 10, + + BinaryOperatorSyntax.Plus => 9, + BinaryOperatorSyntax.Minus => 9, + + BinaryOperatorSyntax.LeftShift => 8, + BinaryOperatorSyntax.RightShift => 8, + + BinaryOperatorSyntax.GreaterThan => 7, + BinaryOperatorSyntax.GreaterThanOrEqual => 7, + BinaryOperatorSyntax.LessThan => 7, + BinaryOperatorSyntax.LessThanOrEqual => 7, + + BinaryOperatorSyntax.Equal => 7, + BinaryOperatorSyntax.NotEqual => 7, + + BinaryOperatorSyntax.BitwiseAnd => 6, + BinaryOperatorSyntax.BitwiseXor => 5, + BinaryOperatorSyntax.BitwiseOr => 4, + + BinaryOperatorSyntax.LogicalAnd => 3, + BinaryOperatorSyntax.LogicalOr => 2, + + _ => throw new ArgumentOutOfRangeException(nameof(operatorSyntax), operatorSyntax, null) + }; + } + + private bool TryGetBinaryOperator(Symbol symbol, [NotNullWhen(true)] out BinaryOperatorSyntax? binaryExpressionOperator) + { + switch (symbol) + { + case Symbol.Equal: + binaryExpressionOperator = BinaryOperatorSyntax.Equal; + return true; + case Symbol.NotEqual: + binaryExpressionOperator = BinaryOperatorSyntax.NotEqual; + return true; + case Symbol.LessThan: + binaryExpressionOperator = BinaryOperatorSyntax.LessThan; + return true; + case Symbol.LessThanOrEqual: + binaryExpressionOperator = BinaryOperatorSyntax.LessThanOrEqual; + return true; + case Symbol.GreaterThan: + binaryExpressionOperator = BinaryOperatorSyntax.GreaterThan; + return true; + case Symbol.GreaterThanOrEqual: + binaryExpressionOperator = BinaryOperatorSyntax.GreaterThanOrEqual; + return true; + case Symbol.And: + binaryExpressionOperator = BinaryOperatorSyntax.LogicalAnd; + return true; + case Symbol.Or: + binaryExpressionOperator = BinaryOperatorSyntax.LogicalOr; + return true; + case Symbol.Plus: + binaryExpressionOperator = BinaryOperatorSyntax.Plus; + return true; + case Symbol.Minus: + binaryExpressionOperator = BinaryOperatorSyntax.Minus; + return true; + case Symbol.Star: + binaryExpressionOperator = BinaryOperatorSyntax.Multiply; + return true; + case Symbol.ForwardSlash: + binaryExpressionOperator = BinaryOperatorSyntax.Divide; + return true; + case Symbol.Percent: + binaryExpressionOperator = BinaryOperatorSyntax.Modulo; + return true; + case Symbol.LeftShift: + binaryExpressionOperator = BinaryOperatorSyntax.LeftShift; + return true; + case Symbol.RightShift: + binaryExpressionOperator = BinaryOperatorSyntax.RightShift; + return true; + case Symbol.Ampersand: + binaryExpressionOperator = BinaryOperatorSyntax.BitwiseAnd; + return true; + case Symbol.Pipe: + binaryExpressionOperator = BinaryOperatorSyntax.BitwiseOr; + return true; + case Symbol.Caret: + binaryExpressionOperator = BinaryOperatorSyntax.BitwiseXor; + return true; + default: + binaryExpressionOperator = null; + return false; + } + } + + private ExpressionSyntax ParsePrimaryExpression() + { + var startIndex = _tokenIndex; + var token = ExpectToken(); + var expr = token switch + { + LiteralToken literal => new LiteralSyntax(GetTokens(startIndex), literal.Value, literal.Kind), + IdentifierToken identifier => new IdentifierSyntax(GetTokens(startIndex), Optional.Empty(), identifier.Value), + SymbolToken symbolToken => symbolToken.Symbol switch + { + Symbol.OpenParen => ParseParenthesizedExpression(), + Symbol.Minus => new UnaryExpressionSyntax(GetTokens(startIndex), UnaryOperatorSyntax.Negate, ParsePrimaryExpression()), + Symbol.Bang => new UnaryExpressionSyntax(GetTokens(startIndex), UnaryOperatorSyntax.Invert, ParsePrimaryExpression()), + Symbol.OpenBracket => ParseArrayInitializer(startIndex), + Symbol.OpenBrace => new StructInitializerSyntax(GetTokens(startIndex), Optional.Empty(), ParseStructInitializerBody()), + Symbol.Struct => ParseStructInitializer(startIndex), + _ => throw new ParseException(Diagnostic + .Error($"Unexpected symbol '{symbolToken.Symbol}' in expression") + .WithHelp("Expected '(', '-', '!', '[' or '{'") + .At(symbolToken) + .Build()) + }, + _ => throw new ParseException(Diagnostic + .Error($"Unexpected token '{token.GetType().Name}' in expression") + .WithHelp("Expected literal, identifier, or parenthesized expression") + .At(token) + .Build()) + }; + + return ParsePostfixOperators(expr); + } + + private ExpressionSyntax ParseParenthesizedExpression() + { + var expression = ParseExpression(); + ExpectSymbol(Symbol.CloseParen); + return expression; + } + + private ExpressionSyntax ParsePostfixOperators(ExpressionSyntax expr) + { + var startIndex = _tokenIndex; + while (HasToken) + { + if (TryExpectSymbol(Symbol.Ampersand)) + { + expr = new AddressOfSyntax(GetTokens(startIndex), expr); + continue; + } + + if (TryExpectSymbol(Symbol.Caret)) + { + expr = new DereferenceSyntax(GetTokens(startIndex), expr); + continue; + } + + if (TryExpectSymbol(Symbol.Period)) + { + var member = ExpectIdentifier().Value; + if (TryExpectSymbol(Symbol.OpenParen)) + { + var parameters = new List(); + + while (!TryExpectSymbol(Symbol.CloseParen)) + { + parameters.Add(ParseExpression()); + if (!TryExpectSymbol(Symbol.Comma)) + { + ExpectSymbol(Symbol.CloseParen); + break; + } + } + + expr = new DotFuncCallSyntax(GetTokens(startIndex), member, expr, parameters); + continue; + } + + expr = new StructFieldAccessSyntax(GetTokens(startIndex), expr, member); + continue; + } + + if (TryExpectSymbol(Symbol.OpenBracket)) + { + var index = ParseExpression(); + ExpectSymbol(Symbol.CloseBracket); + expr = new ArrayIndexAccessSyntax(GetTokens(startIndex), expr, index); + continue; + } + + if (TryExpectSymbol(Symbol.OpenParen)) + { + var parameters = new List(); + + while (!TryExpectSymbol(Symbol.CloseParen)) + { + parameters.Add(ParseExpression()); + if (!TryExpectSymbol(Symbol.Comma)) + { + ExpectSymbol(Symbol.CloseParen); + break; + } + } + + expr = new FuncCallSyntax(GetTokens(startIndex), expr, parameters); + continue; + } + + break; + } + + return expr; + } + + private ArrayInitializerSyntax ParseArrayInitializer(int startIndex) + { + var capacity = ParseExpression(); + ExpectSymbol(Symbol.CloseBracket); + var type = ParseType(); + return new ArrayInitializerSyntax(GetTokens(startIndex), capacity, type); + } + + private StructInitializerSyntax ParseStructInitializer(int startIndex) + { + var type = Optional.Empty(); + if (!TryExpectSymbol(Symbol.OpenBrace)) + { + type = ParseType(); + ExpectSymbol(Symbol.OpenBrace); + } + + var initializers = ParseStructInitializerBody(); + + return new StructInitializerSyntax(GetTokens(startIndex), type, initializers); + } + + private Dictionary ParseStructInitializerBody() + { + Dictionary initializers = []; + while (!TryExpectSymbol(Symbol.CloseBrace)) + { + var name = ExpectIdentifier().Value; + ExpectSymbol(Symbol.Assign); + var value = ParseExpression(); + initializers.Add(name, value); + } + + return initializers; + } + + private BlockSyntax ParseBlock() + { + var startIndex = _tokenIndex; + List statements = []; + while (!TryExpectSymbol(Symbol.CloseBrace)) + { + try + { + statements.Add(ParseStatement()); + } + catch (ParseException ex) + { + _diagnostics.Add(ex.Diagnostic); + Next(); + } + } + + return new BlockSyntax(GetTokens(startIndex), statements); + } + + private TypeSyntax ParseType() + { + var startIndex = _tokenIndex; + if (TryExpectIdentifier(out var name)) + { + if (name.Value[0] == 'u' && int.TryParse(name.Value[1..], out var size)) + { + if (size is not 8 and not 16 and not 32 and not 64) + { + throw new ParseException(Diagnostic + .Error("Arbitrary uint size is not supported") + .WithHelp("Use u8, u16, u32 or u64") + .At(name) + .Build()); + } + + return new IntTypeSyntax(GetTokens(startIndex), false, size); + } + + if (name.Value[0] == 'i' && int.TryParse(name.Value[1..], out size)) + { + if (size is not 8 and not 16 and not 32 and not 64) + { + throw new ParseException(Diagnostic + .Error("Arbitrary int size is not supported") + .WithHelp("Use i8, i16, i32 or i64") + .At(name) + .Build()); + } + + return new IntTypeSyntax(GetTokens(startIndex), true, size); + } + + if (name.Value[0] == 'f' && int.TryParse(name.Value[1..], out size)) + { + if (size is not 32 and not 64) + { + throw new ParseException(Diagnostic + .Error("Arbitrary float size is not supported") + .WithHelp("Use f32 or f64") + .At(name) + .Build()); + } + + return new FloatTypeSyntax(GetTokens(startIndex), size); + } + + return name.Value switch + { + "void" => new VoidTypeSyntax(GetTokens(startIndex)), + "string" => new StringTypeSyntax(GetTokens(startIndex)), + "cstring" => new CStringTypeSyntax(GetTokens(startIndex)), + "bool" => new BoolTypeSyntax(GetTokens(startIndex)), + _ => new CustomTypeSyntax(GetTokens(startIndex), _moduleName, name.Value) + }; + } + + if (TryExpectSymbol(Symbol.Caret)) + { + var baseType = ParseType(); + return new PointerTypeSyntax(GetTokens(startIndex), baseType); + } + + if (TryExpectSymbol(Symbol.Func)) + { + ExpectSymbol(Symbol.OpenParen); + + List parameters = []; + while (!TryExpectSymbol(Symbol.CloseParen)) + { + parameters.Add(ParseType()); + if (!TryExpectSymbol(Symbol.Comma)) + { + ExpectSymbol(Symbol.CloseParen); + break; + } + } + + var returnType = TryExpectSymbol(Symbol.Colon) + ? ParseType() + : new VoidTypeSyntax([]); + + return new FuncTypeSyntax(GetTokens(startIndex), parameters, returnType); + } + + if (TryExpectSymbol(Symbol.OpenBracket)) + { + ExpectSymbol(Symbol.CloseBracket); + var baseType = ParseType(); + return new ArrayTypeSyntax(GetTokens(startIndex), baseType); + } + + throw new ParseException(Diagnostic + .Error("Invalid type syntax") + .WithHelp("Expected type name, '^' for pointer, or '[]' for array") + .At(CurrentToken) + .Build()); + } + + private Token ExpectToken() + { + if (!HasToken) + { + throw new ParseException(Diagnostic + .Error("Unexpected end of file") + .WithHelp("Expected more tokens to complete the syntax") + .At(_tokens[^1]) + .Build()); + } + + var token = CurrentToken!; + 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 (CurrentToken is SymbolToken symbolToken && symbolToken.Symbol == symbol) + { + Next(); + return true; + } + + return false; + } + + private bool TryExpectIdentifier([NotNullWhen(true)] out IdentifierToken? identifier) + { + if (CurrentToken is IdentifierToken identifierToken) + { + identifier = identifierToken; + 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 LiteralToken ExpectLiteral() + { + var token = ExpectToken(); + if (token is not LiteralToken identifier) + { + throw new ParseException(Diagnostic + .Error($"Expected literal, but found {token.GetType().Name}") + .WithHelp("Provide a valid literal name here") + .At(token) + .Build()); + } + + return identifier; + } + + private LiteralToken ExpectLiteral(LiteralKind kind) + { + var literal = ExpectLiteral(); + if (literal.Kind != kind) + { + throw new ParseException(Diagnostic + .Error($"Expected {kind} literal, but found {literal.Kind}") + .WithHelp($"Provide a {kind} literal name here") + .At(literal) + .Build()); + } + + return literal; + } + + private bool TryExpectLiteral(LiteralKind kind, [NotNullWhen(true)] out LiteralToken? literal) + { + if (CurrentToken is LiteralToken identifierToken) + { + literal = identifierToken; + Next(); + return true; + } + + literal = null; + return false; + } + + private void Next() + { + _tokenIndex++; + } + + private IEnumerable GetTokens(int tokenStartIndex) + { + return _tokens.Skip(tokenStartIndex).Take(_tokenIndex - tokenStartIndex); + } +} + +public class ParseException : Exception +{ + public Diagnostic Diagnostic { get; } + + public ParseException(Diagnostic diagnostic) : base(diagnostic.Message) + { + Diagnostic = diagnostic; + } +} \ No newline at end of file diff --git a/compiler/NubLang/Parsing/Syntax/DefinitionSyntax.cs b/compiler/NubLang/Parsing/Syntax/DefinitionSyntax.cs new file mode 100644 index 0000000..2c220ac --- /dev/null +++ b/compiler/NubLang/Parsing/Syntax/DefinitionSyntax.cs @@ -0,0 +1,21 @@ +using NubLang.Tokenization; + +namespace NubLang.Parsing.Syntax; + +public abstract record DefinitionSyntax(IEnumerable Tokens, string Name, bool Exported) : SyntaxNode(Tokens); + +public record FuncParameterSyntax(IEnumerable Tokens, string Name, TypeSyntax Type) : SyntaxNode(Tokens); + +public record FuncSignatureSyntax(IEnumerable Tokens, IReadOnlyList Parameters, TypeSyntax ReturnType) : SyntaxNode(Tokens); + +public record FuncSyntax(IEnumerable Tokens, string Name, bool Exported, string? ExternSymbol, FuncSignatureSyntax Signature, BlockSyntax? Body) : DefinitionSyntax(Tokens, Name, Exported); + +public record StructFieldSyntax(IEnumerable Tokens, int Index, string Name, TypeSyntax Type, Optional Value) : SyntaxNode(Tokens); + +public record StructFuncSyntax(IEnumerable Tokens, string Name, FuncSignatureSyntax Signature, BlockSyntax Body) : SyntaxNode(Tokens); + +public record StructSyntax(IEnumerable Tokens, string Name, bool Exported, IReadOnlyList Fields, IReadOnlyList Functions, IReadOnlyList InterfaceImplementations) : DefinitionSyntax(Tokens, Name, Exported); + +public record InterfaceFuncSyntax(IEnumerable Tokens, string Name, FuncSignatureSyntax Signature) : SyntaxNode(Tokens); + +public record InterfaceSyntax(IEnumerable Tokens, string Name, bool Exported, IReadOnlyList Functions) : DefinitionSyntax(Tokens, Name, Exported); \ No newline at end of file diff --git a/compiler/NubLang/Parsing/Syntax/ExpressionSyntax.cs b/compiler/NubLang/Parsing/Syntax/ExpressionSyntax.cs new file mode 100644 index 0000000..f5f4a30 --- /dev/null +++ b/compiler/NubLang/Parsing/Syntax/ExpressionSyntax.cs @@ -0,0 +1,57 @@ +using NubLang.Tokenization; + +namespace NubLang.Parsing.Syntax; + +public enum UnaryOperatorSyntax +{ + Negate, + Invert +} + +public enum BinaryOperatorSyntax +{ + Equal, + NotEqual, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + LogicalAnd, + LogicalOr, + Plus, + Minus, + Multiply, + Divide, + Modulo, + LeftShift, + RightShift, + BitwiseAnd, + BitwiseXor, + BitwiseOr, +} + +public abstract record ExpressionSyntax(IEnumerable Tokens) : SyntaxNode(Tokens); + +public record BinaryExpressionSyntax(IEnumerable Tokens, ExpressionSyntax Left, BinaryOperatorSyntax Operator, ExpressionSyntax Right) : ExpressionSyntax(Tokens); + +public record UnaryExpressionSyntax(IEnumerable Tokens, UnaryOperatorSyntax Operator, ExpressionSyntax Operand) : ExpressionSyntax(Tokens); + +public record FuncCallSyntax(IEnumerable Tokens, ExpressionSyntax Expression, IReadOnlyList Parameters) : ExpressionSyntax(Tokens); + +public record DotFuncCallSyntax(IEnumerable Tokens, string Name, ExpressionSyntax ThisParameter, IReadOnlyList Parameters) : ExpressionSyntax(Tokens); + +public record IdentifierSyntax(IEnumerable Tokens, Optional Module, string Name) : ExpressionSyntax(Tokens); + +public record ArrayInitializerSyntax(IEnumerable Tokens, ExpressionSyntax Capacity, TypeSyntax ElementType) : ExpressionSyntax(Tokens); + +public record ArrayIndexAccessSyntax(IEnumerable Tokens, ExpressionSyntax Target, ExpressionSyntax Index) : ExpressionSyntax(Tokens); + +public record AddressOfSyntax(IEnumerable Tokens, ExpressionSyntax Expression) : ExpressionSyntax(Tokens); + +public record LiteralSyntax(IEnumerable Tokens, string Value, LiteralKind Kind) : ExpressionSyntax(Tokens); + +public record StructFieldAccessSyntax(IEnumerable Tokens, ExpressionSyntax Target, string Member) : ExpressionSyntax(Tokens); + +public record StructInitializerSyntax(IEnumerable Tokens, Optional StructType, Dictionary Initializers) : ExpressionSyntax(Tokens); + +public record DereferenceSyntax(IEnumerable Tokens, ExpressionSyntax Expression) : ExpressionSyntax(Tokens); \ No newline at end of file diff --git a/compiler/NubLang/Parsing/Syntax/StatementSyntax.cs b/compiler/NubLang/Parsing/Syntax/StatementSyntax.cs new file mode 100644 index 0000000..acf1d8c --- /dev/null +++ b/compiler/NubLang/Parsing/Syntax/StatementSyntax.cs @@ -0,0 +1,21 @@ +using NubLang.Tokenization; + +namespace NubLang.Parsing.Syntax; + +public abstract record StatementSyntax(IEnumerable Tokens) : SyntaxNode(Tokens); + +public record StatementExpressionSyntax(IEnumerable Tokens, ExpressionSyntax Expression) : StatementSyntax(Tokens); + +public record ReturnSyntax(IEnumerable Tokens, Optional Value) : StatementSyntax(Tokens); + +public record AssignmentSyntax(IEnumerable Tokens, ExpressionSyntax Target, ExpressionSyntax Value) : StatementSyntax(Tokens); + +public record IfSyntax(IEnumerable Tokens, ExpressionSyntax Condition, BlockSyntax Body, Optional> Else) : StatementSyntax(Tokens); + +public record VariableDeclarationSyntax(IEnumerable Tokens, string Name, Optional ExplicitType, Optional Assignment) : StatementSyntax(Tokens); + +public record ContinueSyntax(IEnumerable Tokens) : StatementSyntax(Tokens); + +public record BreakSyntax(IEnumerable Tokens) : StatementSyntax(Tokens); + +public record WhileSyntax(IEnumerable Tokens, ExpressionSyntax Condition, BlockSyntax Body) : StatementSyntax(Tokens); \ No newline at end of file diff --git a/compiler/NubLang/Parsing/Syntax/SyntaxNode.cs b/compiler/NubLang/Parsing/Syntax/SyntaxNode.cs new file mode 100644 index 0000000..7435d18 --- /dev/null +++ b/compiler/NubLang/Parsing/Syntax/SyntaxNode.cs @@ -0,0 +1,11 @@ +using NubLang.Tokenization; + +namespace NubLang.Parsing.Syntax; + +public abstract record SyntaxNode(IEnumerable Tokens); + +public record SyntaxTreeMetadata(string? ModuleName, IReadOnlyList Imports); + +public record SyntaxTree(IReadOnlyList Definitions, SyntaxTreeMetadata Metadata); + +public record BlockSyntax(IEnumerable Tokens, IReadOnlyList Statements) : SyntaxNode(Tokens); \ No newline at end of file diff --git a/compiler/NubLang/Parsing/Syntax/TypeSyntax.cs b/compiler/NubLang/Parsing/Syntax/TypeSyntax.cs new file mode 100644 index 0000000..29ff911 --- /dev/null +++ b/compiler/NubLang/Parsing/Syntax/TypeSyntax.cs @@ -0,0 +1,25 @@ +using NubLang.Tokenization; + +namespace NubLang.Parsing.Syntax; + +public abstract record TypeSyntax(IEnumerable Tokens) : SyntaxNode(Tokens); + +public record FuncTypeSyntax(IEnumerable Tokens, IReadOnlyList Parameters, TypeSyntax ReturnType) : TypeSyntax(Tokens); + +public record PointerTypeSyntax(IEnumerable Tokens, TypeSyntax BaseType) : TypeSyntax(Tokens); + +public record VoidTypeSyntax(IEnumerable Tokens) : TypeSyntax(Tokens); + +public record IntTypeSyntax(IEnumerable Tokens, bool Signed, int Width) : TypeSyntax(Tokens); + +public record FloatTypeSyntax(IEnumerable Tokens, int Width) : TypeSyntax(Tokens); + +public record BoolTypeSyntax(IEnumerable Tokens) : TypeSyntax(Tokens); + +public record StringTypeSyntax(IEnumerable Tokens) : TypeSyntax(Tokens); + +public record CStringTypeSyntax(IEnumerable Tokens) : TypeSyntax(Tokens); + +public record ArrayTypeSyntax(IEnumerable Tokens, TypeSyntax BaseType) : TypeSyntax(Tokens); + +public record CustomTypeSyntax(IEnumerable Tokens, string Module, string Name) : TypeSyntax(Tokens); \ No newline at end of file diff --git a/compiler/NubLang/Tokenization/Token.cs b/compiler/NubLang/Tokenization/Token.cs new file mode 100644 index 0000000..f5ec1cd --- /dev/null +++ b/compiler/NubLang/Tokenization/Token.cs @@ -0,0 +1,82 @@ +using NubLang.Code; + +namespace NubLang.Tokenization; + +public abstract class Token(SourceFileSpan fileSpan) +{ + public SourceFileSpan FileSpan { get; } = fileSpan; +} + +public class IdentifierToken(SourceFileSpan fileSpan, string value) : Token(fileSpan) +{ + public string Value { get; } = value; +} + +public class LiteralToken(SourceFileSpan fileSpan, LiteralKind kind, string value) : Token(fileSpan) +{ + public LiteralKind Kind { get; } = kind; + public string Value { get; } = value; +} + +public enum LiteralKind +{ + Integer, + Float, + String, + Bool +} + +public class SymbolToken(SourceFileSpan fileSpan, Symbol symbol) : Token(fileSpan) +{ + public Symbol Symbol { get; } = symbol; +} + +public enum Symbol +{ + Func, + Return, + If, + Else, + While, + Break, + Continue, + Colon, + OpenParen, + CloseParen, + OpenBrace, + CloseBrace, + OpenBracket, + CloseBracket, + Comma, + Period, + Assign, + Bang, + Equal, + NotEqual, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, + Plus, + Minus, + Star, + ForwardSlash, + Struct, + Caret, + Ampersand, + Let, + Calls, + Interface, + For, + Extern, + Semi, + Percent, + LeftShift, + RightShift, + Pipe, + And, + Or, + Module, + Import, + Export, +} \ No newline at end of file diff --git a/compiler/NubLang/Tokenization/Tokenizer.cs b/compiler/NubLang/Tokenization/Tokenizer.cs new file mode 100644 index 0000000..c629bf9 --- /dev/null +++ b/compiler/NubLang/Tokenization/Tokenizer.cs @@ -0,0 +1,266 @@ +using NubLang.Code; +using NubLang.Diagnostics; + +namespace NubLang.Tokenization; + +public sealed class Tokenizer +{ + private static readonly Dictionary Keywords = new() + { + ["func"] = Symbol.Func, + ["if"] = Symbol.If, + ["else"] = Symbol.Else, + ["while"] = Symbol.While, + ["break"] = Symbol.Break, + ["continue"] = Symbol.Continue, + ["return"] = Symbol.Return, + ["struct"] = Symbol.Struct, + ["let"] = Symbol.Let, + ["calls"] = Symbol.Calls, + ["interface"] = Symbol.Interface, + ["for"] = Symbol.For, + ["extern"] = Symbol.Extern, + ["module"] = Symbol.Module, + ["export"] = Symbol.Export, + ["import"] = Symbol.Import, + }; + + private static readonly Dictionary Symbols = new() + { + [['=', '=']] = Symbol.Equal, + [['!', '=']] = Symbol.NotEqual, + [['<', '=']] = Symbol.LessThanOrEqual, + [['>', '=']] = Symbol.GreaterThanOrEqual, + [['<', '<']] = Symbol.LeftShift, + [['>', '>']] = Symbol.RightShift, + [['&', '&']] = Symbol.And, + [['|', '|']] = Symbol.Or, + [[':']] = 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, + [[';']] = Symbol.Semi, + [['%']] = Symbol.Percent, + [['|']] = Symbol.Pipe, + }; + + private static readonly (char[] Pattern, Symbol Symbol)[] OrderedSymbols = Symbols + .OrderByDescending(kvp => kvp.Key.Length) + .Select(kvp => (kvp.Key, kvp.Value)) + .ToArray(); + + private readonly SourceFile _sourceFile; + private readonly List _diagnostics = []; + private int _index; + + public Tokenizer(SourceFile sourceFile) + { + _sourceFile = sourceFile; + } + + public IReadOnlyList GetDiagnostics() => _diagnostics; + + public IEnumerable Tokenize() + { + _index = 0; + + while (Peek().TryGetValue(out var current)) + { + if (char.IsWhiteSpace(current)) + { + Next(); + continue; + } + + if (current == '/' && Peek(1).TryGetValue(out var nextChar) && nextChar == '/') + { + while (Peek().TryGetValue(out var ch) && ch != '\n') + { + Next(); + } + + continue; + } + + var tokenStartIndex = _index; + + 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)) + { + yield return new SymbolToken(GetSourceFileSpan(tokenStartIndex), keywordSymbol); + continue; + } + + if (buffer is "true" or "false") + { + yield return new LiteralToken(GetSourceFileSpan(tokenStartIndex), LiteralKind.Bool, buffer); + continue; + } + + yield return new IdentifierToken(GetSourceFileSpan(tokenStartIndex), buffer); + continue; + } + + 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; + } + } + + yield return new LiteralToken(GetSourceFileSpan(tokenStartIndex), isFloat ? LiteralKind.Float : LiteralKind.Integer, buffer); + continue; + } + + 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(); + } + + yield return new LiteralToken(GetSourceFileSpan(tokenStartIndex), LiteralKind.String, buffer); + continue; + } + + var foundMatch = false; + foreach (var (pattern, symbol) in OrderedSymbols) + { + for (var i = 0; i < pattern.Length; i++) + { + var c = Peek(i); + if (!c.HasValue || c.Value != pattern[i]) break; + + if (i == pattern.Length - 1) + { + for (var j = 0; j <= i; j++) + { + Next(); + } + + yield return new SymbolToken(GetSourceFileSpan(tokenStartIndex), symbol); + foundMatch = true; + break; + } + } + + if (foundMatch) + { + break; + } + } + + if (foundMatch) + { + continue; + } + + _diagnostics.Add(Diagnostic.Error($"Unknown token '{current}'").At(GetSourceFileSpan(tokenStartIndex)).Build()); + Next(); + } + } + + private Optional Peek(int offset = 0) + { + if (_index + offset < _sourceFile.GetText().Length) + { + return _sourceFile.GetText()[_index + offset]; + } + + return Optional.Empty(); + } + + private void Next() + { + _index++; + } + + private SourceFileSpan GetSourceFileSpan(int tokenStartIndex) + { + var start = CalculateSourceLocation(tokenStartIndex); + var end = CalculateSourceLocation(_index); + return new SourceFileSpan(_sourceFile, new SourceSpan(start, end)); + } + + private SourceLocation CalculateSourceLocation(int index) + { + var line = 1; + var column = 1; + + for (var i = 0; i < index && i < _sourceFile.GetText().Length; i++) + { + if (_sourceFile.GetText()[i] == '\n') + { + line++; + column = 1; + } + else + { + column++; + } + } + + return new SourceLocation(line, column); + } +} \ No newline at end of file diff --git a/compiler/NubLang/TypeChecking/Module.cs b/compiler/NubLang/TypeChecking/Module.cs new file mode 100644 index 0000000..5a39eeb --- /dev/null +++ b/compiler/NubLang/TypeChecking/Module.cs @@ -0,0 +1,171 @@ +using NubLang.Parsing.Syntax; +using NubLang.TypeChecking.Node; + +namespace NubLang.TypeChecking; + +public class Module +{ + public static IReadOnlyList CollectFromSyntaxTrees(IReadOnlyList syntaxTrees) + { + var modules = new Dictionary(); + + foreach (var syntaxTree in syntaxTrees) + { + var name = syntaxTree.Metadata.ModuleName; + if (name == null) + { + continue; + } + + if (!modules.TryGetValue(name, out var module)) + { + module = new Module(name, syntaxTree.Metadata.Imports); + modules[name] = module; + } + + foreach (var definition in syntaxTree.Definitions) + { + module.AddDefinition(definition); + } + } + + return modules.Values.ToList(); + } + + private readonly List _definitions = []; + + public Module(string name, IReadOnlyList imports) + { + Name = name; + Imports = imports; + } + + public string Name { get; } + public IReadOnlyList Imports { get; } + + public IReadOnlyList Definitions => _definitions; + + private void AddDefinition(DefinitionSyntax syntax) + { + _definitions.Add(syntax); + } +} + +public class TypedModule +{ + public TypedModule(string name, IReadOnlyList definitions) + { + Name = name; + Definitions = definitions; + } + + public string Name { get; } + public IReadOnlyList Definitions { get; } +} + +public class ModuleSignature +{ + public static IReadOnlyDictionary CollectFromSyntaxTrees(IReadOnlyList syntaxTrees) + { + var modules = new Dictionary(); + + foreach (var syntaxTree in syntaxTrees) + { + var moduleName = syntaxTree.Metadata.ModuleName; + if (moduleName == null) + { + continue; + } + + if (!modules.TryGetValue(moduleName, out var module)) + { + module = new ModuleSignature(); + modules[moduleName] = module; + } + + foreach (var def in syntaxTree.Definitions) + { + if (!def.Exported) continue; + + switch (def) + { + case FuncSyntax funcDef: + { + var parameters = funcDef.Signature.Parameters.Select(p => TypeResolver.ResolveType(p.Type, modules)).ToList(); + var returnType = TypeResolver.ResolveType(funcDef.Signature.ReturnType, modules); + + var type = new FuncTypeNode(parameters, returnType, funcDef.ExternSymbol); + module._functions.Add(funcDef.Name, type); + break; + } + case InterfaceSyntax interfaceDef: + { + var functions = new List(); + for (var i = 0; i < interfaceDef.Functions.Count; i++) + { + var function = interfaceDef.Functions[i]; + var parameters = function.Signature.Parameters.Select(p => TypeResolver.ResolveType(p.Type, modules)).ToList(); + var returnType = TypeResolver.ResolveType(function.Signature.ReturnType, modules); + functions.Add(new InterfaceTypeFunc(function.Name, new FuncTypeNode(parameters, returnType), i)); + } + + var type = new InterfaceTypeNode(moduleName, interfaceDef.Name, functions); + module._interfaces.Add(type); + break; + } + case StructSyntax structDef: + { + var fields = new List(); + foreach (var field in structDef.Fields) + { + fields.Add(new StructTypeField(field.Name, TypeResolver.ResolveType(field.Type, modules), field.Index, field.Value.HasValue)); + } + + var functions = new List(); + foreach (var function in structDef.Functions) + { + var parameters = function.Signature.Parameters.Select(p => TypeResolver.ResolveType(p.Type, modules)).ToList(); + var returnType = TypeResolver.ResolveType(function.Signature.ReturnType, modules); + functions.Add(new StructTypeFunc(function.Name, new FuncTypeNode(parameters, returnType))); + } + + var interfaceImplementations = new List(); + foreach (var interfaceImplementation in structDef.InterfaceImplementations) + { + if (interfaceImplementation is not CustomTypeSyntax customType) + { + throw new Exception("Interface implementation is not a custom type"); + } + + var resolvedType = TypeResolver.ResolveCustomType(customType.Module, customType.Name, modules); + if (resolvedType is not InterfaceTypeNode interfaceType) + { + throw new Exception("Interface implementation is not a interface"); + } + + interfaceImplementations.Add(interfaceType); + } + + var type = new StructTypeNode(moduleName, structDef.Name, fields, functions, interfaceImplementations); + module._structs.Add(type); + break; + } + default: + { + throw new ArgumentOutOfRangeException(nameof(def)); + } + } + } + } + + return modules; + } + + private readonly List _structs = []; + private readonly List _interfaces = []; + private readonly Dictionary _functions = []; + + public IReadOnlyList StructTypes => _structs; + public IReadOnlyList InterfaceTypes => _interfaces; + public IReadOnlyDictionary Functions => _functions; +} \ No newline at end of file diff --git a/compiler/NubLang/TypeChecking/Node/DefinitionNode.cs b/compiler/NubLang/TypeChecking/Node/DefinitionNode.cs new file mode 100644 index 0000000..2169f79 --- /dev/null +++ b/compiler/NubLang/TypeChecking/Node/DefinitionNode.cs @@ -0,0 +1,19 @@ +namespace NubLang.TypeChecking.Node; + +public abstract record DefinitionNode : Node; + +public record FuncParameterNode(string Name, TypeNode Type) : Node; + +public record FuncSignatureNode(IReadOnlyList Parameters, TypeNode ReturnType) : Node; + +public record FuncNode(string Name, FuncSignatureNode Signature, BlockNode? Body) : DefinitionNode; + +public record StructFieldNode(int Index, string Name, TypeNode Type, Optional Value) : Node; + +public record StructFuncNode(string Name, FuncSignatureNode Signature, BlockNode Body) : Node; + +public record StructNode(string Name, IReadOnlyList Fields, IReadOnlyList Functions, IReadOnlyList InterfaceImplementations) : DefinitionNode; + +public record InterfaceFuncNode(string Name, FuncSignatureNode Signature) : Node; + +public record InterfaceNode(string Name, IReadOnlyList Functions) : DefinitionNode; \ No newline at end of file diff --git a/compiler/NubLang/TypeChecking/Node/ExpressionNode.cs b/compiler/NubLang/TypeChecking/Node/ExpressionNode.cs new file mode 100644 index 0000000..df480cd --- /dev/null +++ b/compiler/NubLang/TypeChecking/Node/ExpressionNode.cs @@ -0,0 +1,72 @@ +using NubLang.Tokenization; + +namespace NubLang.TypeChecking.Node; + +public enum UnaryOperator +{ + Negate, + Invert +} + +public enum BinaryOperator +{ + Equal, + NotEqual, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + LogicalAnd, + LogicalOr, + Plus, + Minus, + Multiply, + Divide, + Modulo, + LeftShift, + RightShift, + BitwiseAnd, + BitwiseXor, + BitwiseOr +} + +public abstract record ExpressionNode(TypeNode Type) : Node; + +public abstract record LValueExpressionNode(TypeNode Type) : RValueExpressionNode(Type); +public abstract record RValueExpressionNode(TypeNode Type) : ExpressionNode(Type); + +public record BinaryExpressionNode(TypeNode Type, ExpressionNode Left, BinaryOperator Operator, ExpressionNode Right) : RValueExpressionNode(Type); + +public record UnaryExpressionNode(TypeNode Type, UnaryOperator Operator, ExpressionNode Operand) : RValueExpressionNode(Type); + +public record FuncCallNode(TypeNode Type, ExpressionNode Expression, IReadOnlyList Parameters) : RValueExpressionNode(Type); + +public record StructFuncCallNode(TypeNode Type, string Name, StructTypeNode StructType, ExpressionNode StructExpression, IReadOnlyList Parameters) : RValueExpressionNode(Type); + +public record InterfaceFuncCallNode(TypeNode Type, string Name, InterfaceTypeNode InterfaceType, ExpressionNode InterfaceExpression, IReadOnlyList Parameters) : RValueExpressionNode(Type); + +public record VariableIdentifierNode(TypeNode Type, string Name) : LValueExpressionNode(Type); + +public record FuncParameterIdentifierNode(TypeNode Type, string Name) : RValueExpressionNode(Type); + +public record FuncIdentifierNode(TypeNode Type, string Module, string Name) : RValueExpressionNode(Type); + +public record ArrayInitializerNode(TypeNode Type, ExpressionNode Capacity, TypeNode ElementType) : RValueExpressionNode(Type); + +public record ArrayIndexAccessNode(TypeNode Type, ExpressionNode Target, ExpressionNode Index) : LValueExpressionNode(Type); + +public record AddressOfNode(TypeNode Type, LValueExpressionNode LValue) : RValueExpressionNode(Type); + +public record LiteralNode(TypeNode Type, string Value, LiteralKind Kind) : RValueExpressionNode(Type); + +public record StructFieldAccessNode(TypeNode Type, StructTypeNode StructType, ExpressionNode Target, string Field) : LValueExpressionNode(Type); + +public record StructInitializerNode(StructTypeNode StructType, Dictionary Initializers) : RValueExpressionNode(StructType); + +public record DereferenceNode(TypeNode Type, ExpressionNode Expression) : RValueExpressionNode(Type); + +public record ConvertToInterfaceNode(TypeNode Type, InterfaceTypeNode InterfaceType, StructTypeNode StructType, ExpressionNode Implementation) : RValueExpressionNode(Type); + +public record ConvertIntNode(TypeNode Type, ExpressionNode Value, IntTypeNode ValueType, IntTypeNode TargetType) : RValueExpressionNode(Type); + +public record ConvertFloatNode(TypeNode Type, ExpressionNode Value, FloatTypeNode ValueType, FloatTypeNode TargetType) : RValueExpressionNode(Type); diff --git a/compiler/NubLang/TypeChecking/Node/Node.cs b/compiler/NubLang/TypeChecking/Node/Node.cs new file mode 100644 index 0000000..4b75960 --- /dev/null +++ b/compiler/NubLang/TypeChecking/Node/Node.cs @@ -0,0 +1,5 @@ +namespace NubLang.TypeChecking.Node; + +public abstract record Node; + +public record BlockNode(IReadOnlyList Statements) : Node; \ No newline at end of file diff --git a/compiler/NubLang/TypeChecking/Node/StatementNode.cs b/compiler/NubLang/TypeChecking/Node/StatementNode.cs new file mode 100644 index 0000000..bd58462 --- /dev/null +++ b/compiler/NubLang/TypeChecking/Node/StatementNode.cs @@ -0,0 +1,19 @@ +namespace NubLang.TypeChecking.Node; + +public record StatementNode : Node; + +public record StatementExpressionNode(ExpressionNode Expression) : StatementNode; + +public record ReturnNode(Optional Value) : StatementNode; + +public record AssignmentNode(LValueExpressionNode Target, ExpressionNode Value) : StatementNode; + +public record IfNode(ExpressionNode Condition, BlockNode Body, Optional> Else) : StatementNode; + +public record VariableDeclarationNode(string Name, Optional Assignment, TypeNode Type) : StatementNode; + +public record ContinueNode : StatementNode; + +public record BreakNode : StatementNode; + +public record WhileNode(ExpressionNode Condition, BlockNode Body) : StatementNode; \ No newline at end of file diff --git a/compiler/NubLang/TypeChecking/Node/TypeNode.cs b/compiler/NubLang/TypeChecking/Node/TypeNode.cs new file mode 100644 index 0000000..121d533 --- /dev/null +++ b/compiler/NubLang/TypeChecking/Node/TypeNode.cs @@ -0,0 +1,233 @@ +using System.Diagnostics.CodeAnalysis; + +namespace NubLang.TypeChecking.Node; + +public abstract class TypeNode : IEquatable +{ + public bool IsSimpleType([NotNullWhen(true)] out SimpleTypeNode? simpleType, [NotNullWhen(false)] out ComplexTypeNode? complexType) + { + if (this is SimpleTypeNode st) + { + complexType = null; + simpleType = st; + return true; + } + + if (this is ComplexTypeNode ct) + { + complexType = ct; + simpleType = null; + return false; + } + + throw new ArgumentException($"Type {this} is not a simple type nor a complex type"); + } + + public override bool Equals(object? obj) => obj is TypeNode other && Equals(other); + + public abstract bool Equals(TypeNode? other); + public abstract override int GetHashCode(); + public abstract override string ToString(); + + public static bool operator ==(TypeNode? left, TypeNode? right) => Equals(left, right); + public static bool operator !=(TypeNode? left, TypeNode? right) => !Equals(left, right); +} + +public enum StorageSize +{ + Void, + I8, + I16, + I32, + I64, + U8, + U16, + U32, + U64, + F32, + F64 +} + +public abstract class SimpleTypeNode : TypeNode +{ + public abstract StorageSize StorageSize { get; } +} + +#region Simple types + +public class IntTypeNode(bool signed, int width) : SimpleTypeNode +{ + public bool Signed { get; } = signed; + public int Width { get; } = width; + + public override StorageSize StorageSize => Signed switch + { + true => Width switch + { + 8 => StorageSize.I8, + 16 => StorageSize.I16, + 32 => StorageSize.I32, + 64 => StorageSize.I64, + _ => throw new ArgumentOutOfRangeException(nameof(Width)) + }, + false => Width switch + { + 8 => StorageSize.U8, + 16 => StorageSize.U16, + 32 => StorageSize.U32, + 64 => StorageSize.U64, + _ => throw new ArgumentOutOfRangeException(nameof(Width)) + } + }; + + public override string ToString() => $"{(Signed ? "i" : "u")}{Width}"; + public override bool Equals(TypeNode? other) => other is IntTypeNode @int && @int.Width == Width && @int.Signed == Signed; + public override int GetHashCode() => HashCode.Combine(typeof(IntTypeNode), Signed, Width); +} + +public class FloatTypeNode(int width) : SimpleTypeNode +{ + public int Width { get; } = width; + + public override StorageSize StorageSize => Width switch + { + 32 => StorageSize.F32, + 64 => StorageSize.F64, + _ => throw new ArgumentOutOfRangeException(nameof(Width)) + }; + + public override string ToString() => $"f{Width}"; + public override bool Equals(TypeNode? other) => other is FloatTypeNode @int && @int.Width == Width; + public override int GetHashCode() => HashCode.Combine(typeof(FloatTypeNode), Width); +} + +public class BoolTypeNode : SimpleTypeNode +{ + public override StorageSize StorageSize => StorageSize.U8; + + public override string ToString() => "bool"; + public override bool Equals(TypeNode? other) => other is BoolTypeNode; + public override int GetHashCode() => HashCode.Combine(typeof(BoolTypeNode)); +} + +public class FuncTypeNode(IReadOnlyList parameters, TypeNode returnType, string? externSymbol = null) : SimpleTypeNode +{ + public string? ExternSymbol { get; } = externSymbol; + public IReadOnlyList Parameters { get; } = parameters; + public TypeNode ReturnType { get; } = returnType; + + public override StorageSize StorageSize => StorageSize.U64; + + public override string ToString() => $"func({string.Join(", ", Parameters)}): {ReturnType}"; + public override bool Equals(TypeNode? other) => other is FuncTypeNode func && ReturnType.Equals(func.ReturnType) && Parameters.SequenceEqual(func.Parameters); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(typeof(FuncTypeNode)); + hash.Add(ReturnType); + foreach (var param in Parameters) + { + hash.Add(param); + } + + return hash.ToHashCode(); + } +} + +public class PointerTypeNode(TypeNode baseType) : SimpleTypeNode +{ + public TypeNode BaseType { get; } = baseType; + + public override StorageSize StorageSize => StorageSize.U64; + + public override string ToString() => "^" + BaseType; + public override bool Equals(TypeNode? other) => other is PointerTypeNode pointer && BaseType.Equals(pointer.BaseType); + public override int GetHashCode() => HashCode.Combine(typeof(PointerTypeNode), BaseType); +} + +public class VoidTypeNode : SimpleTypeNode +{ + public override StorageSize StorageSize => StorageSize.Void; + + public override string ToString() => "void"; + public override bool Equals(TypeNode? other) => other is VoidTypeNode; + public override int GetHashCode() => HashCode.Combine(typeof(VoidTypeNode)); +} + +#endregion + +public abstract class ComplexTypeNode : TypeNode; + +#region Complex types + +public class CStringTypeNode : ComplexTypeNode +{ + public override string ToString() => "cstring"; + public override bool Equals(TypeNode? other) => other is CStringTypeNode; + public override int GetHashCode() => HashCode.Combine(typeof(CStringTypeNode)); +} + +public class StringTypeNode : ComplexTypeNode +{ + public override string ToString() => "string"; + public override bool Equals(TypeNode? other) => other is StringTypeNode; + public override int GetHashCode() => HashCode.Combine(typeof(StringTypeNode)); +} + +public class StructTypeField(string name, TypeNode type, int index, bool hasDefaultValue) +{ + public string Name { get; } = name; + public TypeNode Type { get; } = type; + public int Index { get; } = index; + public bool HasDefaultValue { get; } = hasDefaultValue; +} + +public class StructTypeFunc(string name, FuncTypeNode type) +{ + public string Name { get; } = name; + public FuncTypeNode Type { get; } = type; +} + +public class StructTypeNode(string module, string name, IReadOnlyList fields, IReadOnlyList functions, IReadOnlyList interfaceImplementations) : ComplexTypeNode +{ + public string Module { get; } = module; + public string Name { get; } = name; + public IReadOnlyList Fields { get; set; } = fields; + public IReadOnlyList Functions { get; set; } = functions; + public IReadOnlyList InterfaceImplementations { get; set; } = interfaceImplementations; + + public override string ToString() => Name; + public override bool Equals(TypeNode? other) => other is StructTypeNode structType && Name == structType.Name && Module == structType.Module; + public override int GetHashCode() => HashCode.Combine(typeof(StructTypeNode), Name); +} + +public class InterfaceTypeFunc(string name, FuncTypeNode type, int index) +{ + public string Name { get; } = name; + public FuncTypeNode Type { get; } = type; + public int Index { get; } = index; +} + +public class InterfaceTypeNode(string module, string name, IReadOnlyList functions) : ComplexTypeNode +{ + public string Module { get; } = module; + public string Name { get; } = name; + public IReadOnlyList Functions { get; set; } = functions; + + public override string ToString() => Name; + public override bool Equals(TypeNode? other) => other is InterfaceTypeNode interfaceType && Name == interfaceType.Name && Module == interfaceType.Module; + public override int GetHashCode() => HashCode.Combine(typeof(InterfaceTypeNode), Name); +} + +public class ArrayTypeNode(TypeNode elementType) : ComplexTypeNode +{ + public TypeNode ElementType { get; } = elementType; + + public override string ToString() => "[]" + ElementType; + + public override bool Equals(TypeNode? other) => other is ArrayTypeNode array && ElementType.Equals(array.ElementType); + public override int GetHashCode() => HashCode.Combine(typeof(ArrayTypeNode), ElementType); +} + +#endregion \ No newline at end of file diff --git a/compiler/NubLang/TypeChecking/TypeChecker.cs b/compiler/NubLang/TypeChecking/TypeChecker.cs new file mode 100644 index 0000000..972a6d7 --- /dev/null +++ b/compiler/NubLang/TypeChecking/TypeChecker.cs @@ -0,0 +1,456 @@ +using NubLang.Diagnostics; +using NubLang.Parsing.Syntax; +using NubLang.Tokenization; +using NubLang.TypeChecking.Node; + +namespace NubLang.TypeChecking; + +public sealed class TypeChecker +{ + private readonly Module _currentModule; + private readonly IReadOnlyDictionary _moduleSignatures; + + private readonly Stack _scopes = []; + private readonly Stack _funcReturnTypes = []; + private readonly List _diagnostics = []; + + private Scope Scope => _scopes.Peek(); + + public TypeChecker(Module currentModule, IReadOnlyDictionary moduleSignatures) + { + _currentModule = currentModule; + _moduleSignatures = moduleSignatures.Where(x => currentModule.Imports.Contains(x.Key) || _currentModule.Name == x.Key).ToDictionary(); + } + + public IReadOnlyList GetDiagnostics() => _diagnostics; + + public TypedModule CheckModule() + { + _diagnostics.Clear(); + _scopes.Clear(); + + var definitions = new List(); + + foreach (var definition in _currentModule.Definitions) + { + try + { + definitions.Add(CheckDefinition(definition)); + } + catch (TypeCheckerException e) + { + _diagnostics.Add(e.Diagnostic); + } + } + + return new TypedModule(_currentModule.Name, definitions); + } + + private DefinitionNode CheckDefinition(DefinitionSyntax node) + { + return node switch + { + InterfaceSyntax definition => CheckInterfaceDefinition(definition), + FuncSyntax definition => CheckFuncDefinition(definition), + StructSyntax definition => CheckStructDefinition(definition), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + } + + private InterfaceNode CheckInterfaceDefinition(InterfaceSyntax node) + { + throw new NotImplementedException(); + } + + private StructNode CheckStructDefinition(StructSyntax node) + { + var fields = new List(); + foreach (var field in node.Fields) + { + var value = Optional.Empty(); + if (field.Value.HasValue) + { + value = CheckExpression(field.Value.Value); + } + + fields.Add(new StructFieldNode(field.Index, field.Name, ResolveType(field.Type), value)); + } + + var functions = new List(); + foreach (var function in node.Functions) + { + var scope = new Scope(); + // todo(nub31): Add this parameter + foreach (var parameter in function.Signature.Parameters) + { + scope.Declare(new Identifier(parameter.Name, ResolveType(parameter.Type), IdentifierKind.FunctionParameter)); + } + + _funcReturnTypes.Push(ResolveType(function.Signature.ReturnType)); + var body = CheckBlock(function.Body, scope); + _funcReturnTypes.Pop(); + functions.Add(new StructFuncNode(function.Name, CheckFuncSignature(function.Signature), body)); + } + + var interfaceImplementations = new List(); + foreach (var interfaceImplementation in node.InterfaceImplementations) + { + var type = ResolveType(interfaceImplementation); + if (type is not InterfaceTypeNode interfaceType) + { + _diagnostics.Add(Diagnostic.Error($"Struct {node.Name} cannot implement non-struct type {interfaceImplementation}").At(interfaceImplementation).Build()); + continue; + } + + interfaceImplementations.Add(interfaceType); + } + + return new StructNode(node.Name, fields, functions, interfaceImplementations); + } + + private FuncNode CheckFuncDefinition(FuncSyntax node) + { + var scope = new Scope(); + foreach (var parameter in node.Signature.Parameters) + { + scope.Declare(new Identifier(parameter.Name, ResolveType(parameter.Type), IdentifierKind.FunctionParameter)); + } + + BlockNode? body = null; + if (node.Body != null) + { + _funcReturnTypes.Push(ResolveType(node.Signature.ReturnType)); + body = CheckBlock(node.Body, scope); + _funcReturnTypes.Pop(); + } + + return new FuncNode(node.Name, CheckFuncSignature(node.Signature), body); + } + + private StatementNode CheckStatement(StatementSyntax node) + { + return node switch + { + AssignmentSyntax statement => CheckAssignment(statement), + BreakSyntax => new BreakNode(), + ContinueSyntax => new ContinueNode(), + IfSyntax statement => CheckIf(statement), + ReturnSyntax statement => CheckReturn(statement), + StatementExpressionSyntax statement => CheckStatementExpression(statement), + VariableDeclarationSyntax statement => CheckVariableDeclaration(statement), + WhileSyntax statement => CheckWhile(statement), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + } + + private StatementNode CheckAssignment(AssignmentSyntax statement) + { + throw new NotImplementedException(); + } + + private IfNode CheckIf(IfSyntax statement) + { + throw new NotImplementedException(); + } + + private ReturnNode CheckReturn(ReturnSyntax statement) + { + var value = Optional.Empty(); + + if (statement.Value.HasValue) + { + value = CheckExpression(statement.Value.Value, _funcReturnTypes.Peek()); + } + + return new ReturnNode(value); + } + + private StatementExpressionNode CheckStatementExpression(StatementExpressionSyntax statement) + { + return new StatementExpressionNode(CheckExpression(statement.Expression)); + } + + private VariableDeclarationNode CheckVariableDeclaration(VariableDeclarationSyntax statement) + { + TypeNode? type = null; + ExpressionNode? assignmentNode = null; + + if (statement.ExplicitType.TryGetValue(out var explicitType)) + { + type = ResolveType(explicitType); + } + + if (statement.Assignment.TryGetValue(out var assignment)) + { + assignmentNode = CheckExpression(assignment, type); + type ??= assignmentNode.Type; + } + + if (type == null) + { + throw new TypeCheckerException(Diagnostic.Error($"Cannot infer type of variable {statement.Name}").At(statement).Build()); + } + + Scope.Declare(new Identifier(statement.Name, type, IdentifierKind.Variable)); + + return new VariableDeclarationNode(statement.Name, Optional.OfNullable(assignmentNode), type); + } + + private WhileNode CheckWhile(WhileSyntax statement) + { + throw new NotImplementedException(); + } + + private FuncSignatureNode CheckFuncSignature(FuncSignatureSyntax statement) + { + var parameters = new List(); + foreach (var parameter in statement.Parameters) + { + parameters.Add(new FuncParameterNode(parameter.Name, ResolveType(parameter.Type))); + } + + return new FuncSignatureNode(parameters, ResolveType(statement.ReturnType)); + } + + private ExpressionNode CheckExpression(ExpressionSyntax node, TypeNode? expectedType = null) + { + var result = node switch + { + AddressOfSyntax expression => CheckAddressOf(expression), + ArrayIndexAccessSyntax expression => CheckArrayIndexAccess(expression), + ArrayInitializerSyntax expression => CheckArrayInitializer(expression), + BinaryExpressionSyntax expression => CheckBinaryExpression(expression), + DereferenceSyntax expression => CheckDereference(expression), + DotFuncCallSyntax expression => CheckDotFuncCall(expression), + FuncCallSyntax expression => CheckFuncCall(expression), + IdentifierSyntax expression => CheckIdentifier(expression), + LiteralSyntax expression => CheckLiteral(expression, expectedType), + StructFieldAccessSyntax expression => CheckStructFieldAccess(expression), + StructInitializerSyntax expression => CheckStructInitializer(expression, expectedType), + UnaryExpressionSyntax expression => CheckUnaryExpression(expression), + _ => throw new ArgumentOutOfRangeException(nameof(node)) + }; + + if (expectedType == null || result.Type == expectedType) + { + return result; + } + + if (result.Type is StructTypeNode structType && expectedType is InterfaceTypeNode interfaceType) + { + return new ConvertToInterfaceNode(interfaceType, interfaceType, structType, result); + } + + if (result.Type is IntTypeNode sourceIntType && expectedType is IntTypeNode targetIntType) + { + if (sourceIntType.Signed == targetIntType.Signed && sourceIntType.Width < targetIntType.Width) + { + return new ConvertIntNode(targetIntType, result, sourceIntType, targetIntType); + } + } + + if (result.Type is FloatTypeNode sourceFloatType && expectedType is FloatTypeNode targetFloatType) + { + if (sourceFloatType.Width < targetFloatType.Width) + { + return new ConvertFloatNode(targetFloatType, result, sourceFloatType, targetFloatType); + } + } + + throw new TypeCheckerException(Diagnostic.Error($"Cannot convert {result.Type} to {expectedType}").At(node).Build()); + } + + private AddressOfNode CheckAddressOf(AddressOfSyntax expression) + { + throw new NotImplementedException(); + } + + private ArrayIndexAccessNode CheckArrayIndexAccess(ArrayIndexAccessSyntax expression) + { + throw new NotImplementedException(); + } + + private ArrayInitializerNode CheckArrayInitializer(ArrayInitializerSyntax expression) + { + throw new NotImplementedException(); + } + + private BinaryExpressionNode CheckBinaryExpression(BinaryExpressionSyntax expression) + { + throw new NotImplementedException(); + } + + private DereferenceNode CheckDereference(DereferenceSyntax expression) + { + throw new NotImplementedException(); + } + + private FuncCallNode CheckFuncCall(FuncCallSyntax expression) + { + var accessor = CheckExpression(expression.Expression); + if (accessor.Type is not FuncTypeNode funcType) + { + throw new TypeCheckerException(Diagnostic.Error($"Cannot call non-function type {accessor.Type}").At(expression.Expression).Build()); + } + + if (expression.Parameters.Count != funcType.Parameters.Count) + { + throw new TypeCheckerException(Diagnostic.Error($"Function {funcType} expects {funcType.Parameters} but got {expression.Parameters.Count} parameters").At(expression.Expression).Build()); + } + + var parameters = new List(); + for (var i = 0; i < expression.Parameters.Count; i++) + { + var parameter = expression.Parameters[i]; + var expectedType = funcType.Parameters[i]; + + var parameterExpression = CheckExpression(parameter, expectedType); + if (parameterExpression.Type != expectedType) + { + throw new Exception($"Parameter {i + 1} does not match the type {expectedType} for function {funcType}"); + } + + parameters.Add(parameterExpression); + } + + return new FuncCallNode(funcType.ReturnType, accessor, parameters); + } + + private ExpressionNode CheckDotFuncCall(DotFuncCallSyntax expression) + { + throw new NotImplementedException(); + } + + private ExpressionNode CheckIdentifier(IdentifierSyntax expression) + { + // If the identifier does not have a module specified, first check if a local variable or function parameter with that identifier exists + if (!expression.Module.TryGetValue(out var moduleName)) + { + var scopeIdent = Scope.Lookup(expression.Name); + if (scopeIdent != null) + { + switch (scopeIdent.Kind) + { + case IdentifierKind.Variable: + { + return new VariableIdentifierNode(scopeIdent.Type, expression.Name); + } + case IdentifierKind.FunctionParameter: + { + return new FuncParameterIdentifierNode(scopeIdent.Type, expression.Name); + } + default: + { + throw new ArgumentOutOfRangeException(); + } + } + } + } + + moduleName ??= _currentModule.Name; + if (_moduleSignatures.TryGetValue(moduleName, out var module)) + { + if (module.Functions.TryGetValue(expression.Name, out var function)) + { + return new FuncIdentifierNode(function, moduleName, expression.Name); + } + } + + throw new TypeCheckerException(Diagnostic.Error($"Identifier {expression.Name} not found").At(expression).Build()); + } + + private LiteralNode CheckLiteral(LiteralSyntax expression, TypeNode? expectedType) + { + // todo(nub31): Check if the types can actually be represented as another one. For example, an int should be passed when a string is expected + var type = expectedType ?? expression.Kind switch + { + LiteralKind.Integer => new IntTypeNode(true, 64), + LiteralKind.Float => new FloatTypeNode(64), + LiteralKind.String => new StringTypeNode(), + LiteralKind.Bool => new BoolTypeNode(), + _ => throw new ArgumentOutOfRangeException() + }; + + return new LiteralNode(type, expression.Value, expression.Kind); + } + + private StructFieldAccessNode CheckStructFieldAccess(StructFieldAccessSyntax expression) + { + throw new NotImplementedException(); + } + + private StructInitializerNode CheckStructInitializer(StructInitializerSyntax expression, TypeNode? expectedType) + { + throw new NotImplementedException(); + } + + private UnaryExpressionNode CheckUnaryExpression(UnaryExpressionSyntax expression) + { + throw new NotImplementedException(); + } + + private BlockNode CheckBlock(BlockSyntax node, Scope? scope = null) + { + var statements = new List(); + + _scopes.Push(scope ?? Scope.SubScope()); + + foreach (var statement in node.Statements) + { + statements.Add(CheckStatement(statement)); + } + + _scopes.Pop(); + + return new BlockNode(statements); + } + + private TypeNode ResolveType(TypeSyntax fieldType) + { + return TypeResolver.ResolveType(fieldType, _moduleSignatures); + } +} + +public enum IdentifierKind +{ + Variable, + FunctionParameter +} + +public record Identifier(string Name, TypeNode Type, IdentifierKind Kind); + +public class Scope(Scope? parent = null) +{ + private readonly List _variables = []; + + public Identifier? Lookup(string name) + { + var variable = _variables.FirstOrDefault(x => x.Name == name); + if (variable != null) + { + return variable; + } + + return parent?.Lookup(name); + } + + public void Declare(Identifier identifier) + { + _variables.Add(identifier); + } + + public Scope SubScope() + { + return new Scope(this); + } +} + +public class TypeCheckerException : Exception +{ + public Diagnostic Diagnostic { get; } + + public TypeCheckerException(Diagnostic diagnostic) : base(diagnostic.Message) + { + Diagnostic = diagnostic; + } +} \ No newline at end of file diff --git a/compiler/NubLang/TypeResolver.cs b/compiler/NubLang/TypeResolver.cs new file mode 100644 index 0000000..6bbdec2 --- /dev/null +++ b/compiler/NubLang/TypeResolver.cs @@ -0,0 +1,95 @@ +using NubLang.Parsing.Syntax; +using NubLang.TypeChecking; +using NubLang.TypeChecking.Node; + +namespace NubLang; + +public static class TypeResolver +{ + public static TypeNode ResolveType(TypeSyntax type, IReadOnlyDictionary modules) + { + return type switch + { + BoolTypeSyntax => new BoolTypeNode(), + CStringTypeSyntax => new CStringTypeNode(), + IntTypeSyntax i => new IntTypeNode(i.Signed, i.Width), + CustomTypeSyntax c => ResolveCustomType(c.Module, c.Name, modules), + FloatTypeSyntax f => new FloatTypeNode(f.Width), + FuncTypeSyntax func => new FuncTypeNode(func.Parameters.Select(x => ResolveType(x, modules)).ToList(), ResolveType(func.ReturnType, modules)), + ArrayTypeSyntax arr => new ArrayTypeNode(ResolveType(arr.BaseType, modules)), + PointerTypeSyntax ptr => new PointerTypeNode(ResolveType(ptr.BaseType, modules)), + StringTypeSyntax => new StringTypeNode(), + VoidTypeSyntax => new VoidTypeNode(), + _ => throw new NotSupportedException($"Unknown type syntax: {type}") + }; + } + + public static TypeNode ResolveCustomType(string moduleName, string typeName, IReadOnlyDictionary modules) + { + if (!modules.TryGetValue(moduleName, out var module)) + { + throw new Exception("Module not found: " + moduleName); + } + + var structType = module.StructTypes.FirstOrDefault(x => x.Name == typeName); + if (structType != null) + { + return structType; + } + + var interfaceType = module.InterfaceTypes.FirstOrDefault(x => x.Name == typeName); + if (interfaceType != null) + { + return interfaceType; + } + + throw new Exception($"Type {typeName} not found in module {moduleName}"); + } + + public static StructTypeNode ResolveStructType(string moduleName, string structName, IReadOnlyDictionary modules) + { + if (!modules.TryGetValue(moduleName, out var module)) + { + throw new Exception("Module not found: " + moduleName); + } + + var structType = module.StructTypes.FirstOrDefault(x => x.Name == structName); + if (structType != null) + { + return structType; + } + + throw new Exception($"Struct type {structName} not found in module {moduleName}"); + } + + public static InterfaceTypeNode ResolveInterfaceType(string moduleName, string interfaceName, IReadOnlyDictionary modules) + { + if (!modules.TryGetValue(moduleName, out var module)) + { + throw new Exception("Module not found: " + moduleName); + } + + var structType = module.InterfaceTypes.FirstOrDefault(x => x.Name == interfaceName); + if (structType != null) + { + return structType; + } + + throw new Exception($"Interface type {interfaceName} not found in module {moduleName}"); + } + + public static FuncTypeNode ResolveFunctionType(string moduleName, string funcName, IReadOnlyDictionary modules) + { + if (!modules.TryGetValue(moduleName, out var module)) + { + throw new Exception("Module not found: " + moduleName); + } + + if (module.Functions.TryGetValue(funcName, out var funcType)) + { + return funcType; + } + + throw new Exception($"Function type {funcName} not found in module {moduleName}"); + } +} \ No newline at end of file diff --git a/compiler/NubLang/Variant.cs b/compiler/NubLang/Variant.cs new file mode 100644 index 0000000..c9898b8 --- /dev/null +++ b/compiler/NubLang/Variant.cs @@ -0,0 +1,75 @@ +using System.Diagnostics.CodeAnalysis; + +namespace NubLang; + +public readonly struct Variant 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 on1, Action on2) + { + switch (_value) + { + case T1 v1: + on1(v1); + break; + case T2 v2: + on2(v2); + break; + default: + throw new InvalidCastException(); + } + } + + public T Match(Func on1, Func on2) + { + return _value switch + { + T1 v1 => on1(v1), + T2 v2 => on2(v2), + _ => throw new InvalidCastException() + }; + } + + public bool IsCase1([NotNullWhen(true)] out T1? value) + { + if (_value is T1 converted) + { + value = converted; + return true; + } + + value = default; + return false; + } + + public bool IsCase2([NotNullWhen(true)] out T2? value) + { + if (_value is T2 converted) + { + value = converted; + return true; + } + + value = default; + return false; + } + + public static implicit operator Variant(T1 value) => new(value); + public static implicit operator Variant(T2 value) => new(value); +} \ No newline at end of file diff --git a/example/main.nub b/example/main.nub new file mode 100644 index 0000000..1586d8c --- /dev/null +++ b/example/main.nub @@ -0,0 +1,9 @@ +module "main" + +extern "puts" func puts(text: cstring) + +extern "main" func main(args: []cstring): i64 +{ + puts("test") + return 0 +} diff --git a/example/x86_64.s b/example/x86_64.s new file mode 100644 index 0000000..cded159 --- /dev/null +++ b/example/x86_64.s @@ -0,0 +1,10 @@ +.intel_syntax noprefix + +.text +.globl _start +_start: + mov rdi, rsp + call main.main + mov rdi, rax + mov rax, 60 + syscall diff --git a/vscode/.gitignore b/vscode/.gitignore new file mode 100644 index 0000000..c5e82d7 --- /dev/null +++ b/vscode/.gitignore @@ -0,0 +1 @@ +bin \ No newline at end of file diff --git a/vscode/language-configuration.json b/vscode/language-configuration.json new file mode 100644 index 0000000..538ffc1 --- /dev/null +++ b/vscode/language-configuration.json @@ -0,0 +1,29 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": ["/*", "*/"] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"] + ], + "indentationRules": { + "increaseIndentPattern": "^((?!\\/\\*).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$", + "decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]\\)].*$" + } +} diff --git a/vscode/makefile b/vscode/makefile new file mode 100644 index 0000000..f2b1655 --- /dev/null +++ b/vscode/makefile @@ -0,0 +1,6 @@ +vscode: + mkdir -p bin + npx vsce package -o bin/nub-lang.vsix + +clean: + rm -r bin \ No newline at end of file diff --git a/vscode/package.json b/vscode/package.json new file mode 100644 index 0000000..a988628 --- /dev/null +++ b/vscode/package.json @@ -0,0 +1,40 @@ +{ + "name": "nub-lang", + "displayName": "Nub Language Support", + "description": "Syntax highlighting for Nub programming language", + "version": "0.0.0", + "publisher": "nub31", + "repository": { + "type": "git", + "url": "https://git.oliste.no/nub31/nub-lang" + }, + "license": "MIT", + "engines": { + "vscode": "^1.74.0" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "languages": [ + { + "id": "nub", + "aliases": [ + "Nub", + "nub" + ], + "extensions": [ + ".nub" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "nub", + "scopeName": "source.nub", + "path": "./syntaxes/nub.tmLanguage.json" + } + ] + } +} diff --git a/vscode/syntaxes/nub.tmLanguage.json b/vscode/syntaxes/nub.tmLanguage.json new file mode 100644 index 0000000..4a4780c --- /dev/null +++ b/vscode/syntaxes/nub.tmLanguage.json @@ -0,0 +1,300 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Nub", + "scopeName": "source.nub", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#keywords" + }, + { + "include": "#modifiers" + }, + { + "include": "#types" + }, + { + "include": "#strings" + }, + { + "include": "#numbers" + }, + { + "include": "#operators" + }, + { + "include": "#function-definition" + }, + { + "include": "#struct-definition" + }, + { + "include": "#function-call" + }, + { + "include": "#identifiers" + } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.double-slash.nub", + "begin": "//", + "end": "$" + }, + { + "name": "comment.block.nub", + "begin": "/\\*", + "end": "\\*/" + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.control.nub", + "match": "\\b(if|else|while|break|continue|return|let)\\b" + }, + { + "name": "keyword.other.nub", + "match": "\\b(namespace|func|struct|interface)\\b" + } + ] + }, + "modifiers": { + "patterns": [ + { + "name": "storage.modifier.nub", + "match": "\\b(export|extern|calls)\\b" + } + ] + }, + "types": { + "patterns": [ + { + "include": "#function-type" + }, + { + "name": "storage.type.primitive.nub", + "match": "\\b(i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|bool|string|cstring|void|any)\\b" + }, + { + "name": "storage.type.array.nub", + "match": "\\[\\]" + }, + { + "name": "storage.type.pointer.nub", + "match": "\\^" + } + ] + }, + "function-type": { + "patterns": [ + { + "begin": "\\b(func)\\s*\\(", + "beginCaptures": { + "1": { + "name": "storage.type.function.nub" + } + }, + "end": "(?<=\\))(?:\\s*:\\s*([^\\s,;{}()]+))?", + "endCaptures": { + "1": { + "name": "storage.type.nub" + } + }, + "patterns": [ + { + "include": "#function-type-parameters" + } + ] + } + ] + }, + "function-type-parameters": { + "patterns": [ + { + "match": "\\.\\.\\.", + "name": "keyword.operator.variadic.nub" + }, + { + "include": "#types" + }, + { + "match": ",", + "name": "punctuation.separator.nub" + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.nub", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "name": "constant.character.escape.nub", + "match": "\\\\(n|t|r|\\\\|\"|')" + }, + { + "name": "constant.character.escape.nub", + "match": "\\\\[0-7]{1,3}" + }, + { + "name": "constant.character.escape.nub", + "match": "\\\\x[0-9A-Fa-f]{1,2}" + } + ] + }, + { + "name": "string.quoted.single.nub", + "begin": "'", + "end": "'", + "patterns": [ + { + "name": "constant.character.escape.nub", + "match": "\\\\(n|t|r|\\\\|\"|')" + } + ] + } + ] + }, + "numbers": { + "patterns": [ + { + "name": "constant.numeric.float.nub", + "match": "\\b\\d+\\.\\d*([eE][+-]?\\d+)?[fF]?\\b" + }, + { + "name": "constant.numeric.integer.decimal.nub", + "match": "\\b\\d+\\b" + }, + { + "name": "constant.numeric.integer.hexadecimal.nub", + "match": "\\b0[xX][0-9A-Fa-f]+\\b" + }, + { + "name": "constant.numeric.integer.binary.nub", + "match": "\\b0[bB][01]+\\b" + } + ] + }, + "operators": { + "patterns": [ + { + "name": "keyword.operator.assignment.nub", + "match": "=" + }, + { + "name": "keyword.operator.comparison.nub", + "match": "(==|!=|<=|>=|<|>)" + }, + { + "name": "keyword.operator.arithmetic.nub", + "match": "(\\+|\\-|\\*|/)" + }, + { + "name": "keyword.operator.logical.nub", + "match": "(&&|\\|\\||!)" + }, + { + "name": "keyword.operator.address.nub", + "match": "&" + }, + { + "name": "keyword.operator.dereference.nub", + "match": "\\^" + }, + { + "name": "keyword.operator.member-access.nub", + "match": "\\." + } + ] + }, + "function-definition": { + "patterns": [ + { + "begin": "\\b(global\\s+|extern\\s+)?(func)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(", + "beginCaptures": { + "1": { + "name": "storage.modifier.nub" + }, + "2": { + "name": "keyword.other.nub" + }, + "3": { + "name": "entity.name.function.nub" + } + }, + "end": "\\)", + "patterns": [ + { + "include": "#function-parameters" + } + ] + } + ] + }, + "struct-definition": { + "patterns": [ + { + "match": "\\b(struct)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\b", + "captures": { + "1": { + "name": "keyword.other.nub" + }, + "2": { + "name": "entity.name.type.struct.nub" + } + } + } + ] + }, + "function-parameters": { + "patterns": [ + { + "match": "\\.\\.\\.", + "name": "keyword.operator.variadic.nub" + }, + { + "match": "([a-zA-Z_][a-zA-Z0-9_]*)\\s*:\\s*", + "captures": { + "1": { + "name": "variable.parameter.nub" + } + } + }, + { + "include": "#types" + }, + { + "include": "#identifiers" + } + ] + }, + "function-call": { + "patterns": [ + { + "match": "([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()", + "captures": { + "1": { + "name": "entity.name.function.call.nub" + } + } + } + ] + }, + "identifiers": { + "patterns": [ + { + "name": "variable.other.nub", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + } + ] + } + } +} \ No newline at end of file