From a9e4b86e786099c5e3fbce61b9c90cfe293e512d Mon Sep 17 00:00:00 2001 From: nub31 Date: Thu, 23 Oct 2025 12:14:10 +0200 Subject: [PATCH] ... --- compiler/NubLang.LSP/AstExtensions.cs | 355 ++++++++++++++++++ compiler/NubLang.LSP/DiagnosticsPublisher.cs | 4 +- compiler/NubLang.LSP/HoverHandler.cs | 175 ++++++++- .../NubLang.LSP/TextDocumentSyncHandler.cs | 2 +- compiler/NubLang.LSP/WorkspaceManager.cs | 39 +- compiler/NubLang/Ast/TypeChecker.cs | 38 +- vscode-lsp/package.json | 8 +- vscode-lsp/src/extension.ts | 4 +- vscode-lsp/syntaxes/nub.tmLanguage.json | 2 +- 9 files changed, 601 insertions(+), 26 deletions(-) create mode 100644 compiler/NubLang.LSP/AstExtensions.cs diff --git a/compiler/NubLang.LSP/AstExtensions.cs b/compiler/NubLang.LSP/AstExtensions.cs new file mode 100644 index 0000000..d5b1c22 --- /dev/null +++ b/compiler/NubLang.LSP/AstExtensions.cs @@ -0,0 +1,355 @@ +using NubLang.Ast; + +namespace NubLang.LSP; + +public static class AstExtensions +{ + public static IEnumerable EnumerateDescendantsAndSelf(this Node node) + { + yield return node; + + switch (node) + { + case FuncNode func: + { + foreach (var n in func.Prototype.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + if (func.Body != null) + { + foreach (var n in func.Body.EnumerateDescendantsAndSelf()) + { + yield return n; + } + } + + break; + } + case FuncPrototypeNode proto: + { + foreach (var n in proto.Parameters.SelectMany(param => param.EnumerateDescendantsAndSelf())) + { + yield return n; + } + + break; + } + case BlockNode block: + { + foreach (var n in block.Statements.SelectMany(stmt => stmt.EnumerateDescendantsAndSelf())) + { + yield return n; + } + + break; + } + case StatementFuncCallNode stmtCall: + { + foreach (var n in stmtCall.FuncCall.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ReturnNode { Value: not null } ret: + { + foreach (var n in ret.Value.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case AssignmentNode assign: + { + foreach (var n in assign.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in assign.Value.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case IfNode ifNode: + { + foreach (var n in ifNode.Condition.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in ifNode.Body.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + if (ifNode.Else.HasValue) + { + if (ifNode.Else.Value.IsCase1(out var elseIfNode)) + { + foreach (var n in elseIfNode.EnumerateDescendantsAndSelf()) + { + yield return n; + } + } + else if (ifNode.Else.Value.IsCase2(out var elseNode)) + { + foreach (var n in elseNode.EnumerateDescendantsAndSelf()) + { + yield return n; + } + } + } + + break; + } + case VariableDeclarationNode decl: + { + if (decl.Assignment != null) + { + foreach (var n in decl.Assignment.EnumerateDescendantsAndSelf()) + { + yield return n; + } + } + + break; + } + case WhileNode whileNode: + { + foreach (var n in whileNode.Condition.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in whileNode.Body.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ForSliceNode forSlice: + { + foreach (var n in forSlice.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in forSlice.Body.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ForConstArrayNode forConst: + { + foreach (var n in forConst.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in forConst.Body.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case DeferNode defer: + { + foreach (var n in defer.Statement.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case BinaryExpressionNode bin: + { + foreach (var n in bin.Left.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in bin.Right.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case UnaryExpressionNode unary: + { + foreach (var n in unary.Operand.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case FuncCallNode call: + { + foreach (var n in call.Expression.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in call.Parameters.SelectMany(param => param.EnumerateDescendantsAndSelf())) + { + yield return n; + } + + break; + } + case ArrayInitializerNode arrInit: + { + foreach (var n in arrInit.Values.SelectMany(val => val.EnumerateDescendantsAndSelf())) + { + yield return n; + } + + break; + } + case ConstArrayInitializerNode constArrInit: + { + foreach (var n in constArrInit.Values.SelectMany(val => val.EnumerateDescendantsAndSelf())) + { + yield return n; + } + + break; + } + case ArrayIndexAccessNode arrIndex: + { + foreach (var n in arrIndex.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in arrIndex.Index.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ConstArrayIndexAccessNode constArrIndex: + { + foreach (var n in constArrIndex.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in constArrIndex.Index.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case SliceIndexAccessNode sliceIndex: + { + foreach (var n in sliceIndex.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + foreach (var n in sliceIndex.Index.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case AddressOfNode addr: + { + foreach (var n in addr.LValue.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case StructFieldAccessNode field: + { + foreach (var n in field.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case StructInitializerNode structInit: + { + foreach (var n in structInit.Initializers.SelectMany(kv => kv.Value.EnumerateDescendantsAndSelf())) + { + yield return n; + } + + break; + } + case DereferenceNode deref: + { + foreach (var n in deref.Target.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ConvertIntNode convInt: + { + foreach (var n in convInt.Value.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ConvertFloatNode convFloat: + { + foreach (var n in convFloat.Value.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ConvertCStringToStringNode convStr: + { + foreach (var n in convStr.Value.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case FloatToIntBuiltinNode ftoi: + { + foreach (var n in ftoi.Value.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + case ConstArrayToSliceNode constSlice: + { + foreach (var n in constSlice.Array.EnumerateDescendantsAndSelf()) + { + yield return n; + } + + break; + } + } + } +} \ No newline at end of file diff --git a/compiler/NubLang.LSP/DiagnosticsPublisher.cs b/compiler/NubLang.LSP/DiagnosticsPublisher.cs index 1c8faa0..7364396 100644 --- a/compiler/NubLang.LSP/DiagnosticsPublisher.cs +++ b/compiler/NubLang.LSP/DiagnosticsPublisher.cs @@ -35,9 +35,9 @@ public class DiagnosticsPublisher Diagnostics.DiagnosticSeverity.Error => DiagnosticSeverity.Error, _ => null }, - Message = $"{nubDiagnostic.Message}\nhelp: {nubDiagnostic.Help}", + Message = $"{nubDiagnostic.Message}\n{(nubDiagnostic.Help == null ? "" : $"help: {nubDiagnostic.Help}")}", Range = nubDiagnostic.Span.HasValue - ? new Range(nubDiagnostic.Span.Value.Start.Line, nubDiagnostic.Span.Value.Start.Column, nubDiagnostic.Span.Value.End.Line, nubDiagnostic.Span.Value.Start.Column) + ? new Range(nubDiagnostic.Span.Value.Start.Line - 1, nubDiagnostic.Span.Value.Start.Column - 1, nubDiagnostic.Span.Value.End.Line - 1, nubDiagnostic.Span.Value.End.Column - 1) : new Range(), }; } diff --git a/compiler/NubLang.LSP/HoverHandler.cs b/compiler/NubLang.LSP/HoverHandler.cs index d0a5c72..3567df7 100644 --- a/compiler/NubLang.LSP/HoverHandler.cs +++ b/compiler/NubLang.LSP/HoverHandler.cs @@ -1,28 +1,195 @@ +using System.Globalization; +using NubLang.Ast; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; namespace NubLang.LSP; -internal class HoverHandler : HoverHandlerBase +internal class HoverHandler(WorkspaceManager workspaceManager) : HoverHandlerBase { protected override HoverRegistrationOptions CreateRegistrationOptions(HoverCapability capability, ClientCapabilities clientCapabilities) { return new HoverRegistrationOptions { - DocumentSelector = TextDocumentSelector.ForLanguage("nublang") + DocumentSelector = TextDocumentSelector.ForLanguage("nub") }; } - public override async Task Handle(HoverParams request, CancellationToken cancellationToken) + public override Task Handle(HoverParams request, CancellationToken cancellationToken) { + return Task.FromResult(HandleSync(request)); + } + + private Hover? HandleSync(HoverParams request) + { + var compilationUnit = workspaceManager.GetCompilationUnit(request.TextDocument.Uri); + if (compilationUnit == null) + { + return null; + } + + var line = request.Position.Line; + var character = request.Position.Character; + + var hoveredNode = compilationUnit.Functions + .SelectMany(x => x.EnumerateDescendantsAndSelf()) + .Where(n => IsHoveringOverNode(n, line, character)) + .OrderBy(n => n.Tokens.First().Span.Start.Line) + .ThenBy(n => n.Tokens.First().Span.Start.Column) + .LastOrDefault(); + + if (hoveredNode == null) + { + return null; + } + + var message = CreateMessage(hoveredNode, compilationUnit); + if (message == null) + { + return null; + } + return new Hover { Contents = new MarkedStringsOrMarkupContent(new MarkupContent { - Value = "# uwu", + Value = message, Kind = MarkupKind.Markdown, }) }; } + + private static string? CreateMessage(Node hoveredNode, CompilationUnit compilationUnit) + { + return hoveredNode switch + { + FuncNode funcNode => CreateFuncPrototypeNodeMessage(funcNode.Prototype), + FuncPrototypeNode funcPrototypeNode => CreateFuncPrototypeNodeMessage(funcPrototypeNode), + FuncIdentifierNode funcIdentifierNode => CreateFuncIdentifierNodeMessage(funcIdentifierNode, compilationUnit), + FuncParameterNode funcParameterNode => CreateVariableIdentifierNodeMessage(funcParameterNode.Name, funcParameterNode.Type), + VariableIdentifierNode variableIdentifierNode => CreateVariableIdentifierNodeMessage(variableIdentifierNode.Name, variableIdentifierNode.Type), + VariableDeclarationNode variableDeclarationNode => CreateVariableIdentifierNodeMessage(variableDeclarationNode.Name, variableDeclarationNode.Type), + CStringLiteralNode cStringLiteralNode => CreateLiteralNodeMessage(cStringLiteralNode.Type, '"' + cStringLiteralNode.Value + '"'), + StringLiteralNode stringLiteralNode => CreateLiteralNodeMessage(stringLiteralNode.Type, '"' + stringLiteralNode.Value + '"'), + BoolLiteralNode boolLiteralNode => CreateLiteralNodeMessage(boolLiteralNode.Type, boolLiteralNode.Value.ToString()), + Float32LiteralNode float32LiteralNode => CreateLiteralNodeMessage(float32LiteralNode.Type, float32LiteralNode.Value.ToString(CultureInfo.InvariantCulture)), + Float64LiteralNode float64LiteralNode => CreateLiteralNodeMessage(float64LiteralNode.Type, float64LiteralNode.Value.ToString(CultureInfo.InvariantCulture)), + I8LiteralNode i8LiteralNode => CreateLiteralNodeMessage(i8LiteralNode.Type, i8LiteralNode.Value.ToString()), + I16LiteralNode i16LiteralNode => CreateLiteralNodeMessage(i16LiteralNode.Type, i16LiteralNode.Value.ToString()), + I32LiteralNode i32LiteralNode => CreateLiteralNodeMessage(i32LiteralNode.Type, i32LiteralNode.Value.ToString()), + I64LiteralNode i64LiteralNode => CreateLiteralNodeMessage(i64LiteralNode.Type, i64LiteralNode.Value.ToString()), + U8LiteralNode u8LiteralNode => CreateLiteralNodeMessage(u8LiteralNode.Type, u8LiteralNode.Value.ToString()), + U16LiteralNode u16LiteralNode => CreateLiteralNodeMessage(u16LiteralNode.Type, u16LiteralNode.Value.ToString()), + U32LiteralNode u32LiteralNode => CreateLiteralNodeMessage(u32LiteralNode.Type, u32LiteralNode.Value.ToString()), + U64LiteralNode u64LiteralNode => CreateLiteralNodeMessage(u64LiteralNode.Type, u64LiteralNode.Value.ToString()), + // Expressions can have a generic fallback showing the resulting type + ExpressionNode expressionNode => CreateGenericExpressionNodeMessage(expressionNode), + // Explicit null returns, can be removed when the default is null instead of the debug type + BlockNode => null, + StatementNode statementNode => CreateGenericStatementNodeMessage(statementNode), + _ => hoveredNode.GetType().Name + }; + } + + private static string CreateLiteralNodeMessage(NubType type, string value) + { + return $""" + **Literal** `{type}` + ```nub + {value}: {type} + ``` + """; + } + + private static string CreateVariableIdentifierNodeMessage(string name, NubType type) + { + return $""" + **Variable** `{name}` + ```nub + {name}: {type} + ``` + """; + } + + private static string CreateFuncIdentifierNodeMessage(FuncIdentifierNode funcIdentifierNode, CompilationUnit compilationUnit) + { + var func = compilationUnit.ImportedFunctions.FirstOrDefault(x => x.Module == funcIdentifierNode.Module && x.Name == funcIdentifierNode.Name); + if (func == null) + { + return $""" + **Function** `{funcIdentifierNode.Module}::{funcIdentifierNode.Name}` + ```nub + // Declaration not found + ``` + """; + } + + return CreateFuncPrototypeNodeMessage(func); + } + + private static string CreateFuncPrototypeNodeMessage(FuncPrototypeNode funcPrototypeNode) + { + var parameterText = string.Join(", ", funcPrototypeNode.Parameters.Select(x => $"{x.Name}: {x.Type}")); + var externText = funcPrototypeNode.ExternSymbol != null ? $"extern \"{funcPrototypeNode.ExternSymbol}\" " : ""; + + return $""" + **Function** `{funcPrototypeNode.Name}` + ```nub + {externText}func {funcPrototypeNode.Name}({parameterText}): {funcPrototypeNode.ReturnType} + ``` + """; + } + + private static string CreateGenericStatementNodeMessage(StatementNode statementNode) + { + return $"**Statement** `{statementNode.GetType().Name}`"; + } + + private static string CreateGenericExpressionNodeMessage(ExpressionNode expressionNode) + { + return $""" + **Expression** `{expressionNode.GetType().Name}` + ```nub + {expressionNode.Type} + ``` + """; + } + + private static bool IsHoveringOverNode(Node node, int line, int character) + { + if (node.Tokens.Count == 0) + { + return false; + } + + var start = node.Tokens.First().Span.Start; + var end = node.Tokens.Last().Span.End; + + var startLine = start.Line - 1; + var startChar = start.Column - 1; + var endLine = end.Line - 1; + var endChar = end.Column - 1; + + if (line < startLine || line > endLine) return false; + + if (line > startLine && line < endLine) return true; + + if (startLine == endLine) + { + return character >= startChar && character <= endChar; + } + + if (line == startLine) + { + return character >= startChar; + } + + if (line == endLine) + { + return character <= endChar; + } + + return false; + } } \ No newline at end of file diff --git a/compiler/NubLang.LSP/TextDocumentSyncHandler.cs b/compiler/NubLang.LSP/TextDocumentSyncHandler.cs index 54714b4..decb6c2 100644 --- a/compiler/NubLang.LSP/TextDocumentSyncHandler.cs +++ b/compiler/NubLang.LSP/TextDocumentSyncHandler.cs @@ -10,7 +10,7 @@ internal class TextDocumentSyncHandler(WorkspaceManager workspaceManager) : Text { public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri) { - return new TextDocumentAttributes(uri, "nublang"); + return new TextDocumentAttributes(uri, "nub"); } public override Task Handle(DidOpenTextDocumentParams request, CancellationToken cancellationToken) diff --git a/compiler/NubLang.LSP/WorkspaceManager.cs b/compiler/NubLang.LSP/WorkspaceManager.cs index 07a3824..da3f48e 100644 --- a/compiler/NubLang.LSP/WorkspaceManager.cs +++ b/compiler/NubLang.LSP/WorkspaceManager.cs @@ -1,3 +1,4 @@ +using NubLang.Ast; using NubLang.Syntax; using OmniSharp.Extensions.LanguageServer.Protocol; @@ -5,9 +6,10 @@ namespace NubLang.LSP; public class WorkspaceManager(DiagnosticsPublisher diagnosticsPublisher) { - private readonly Dictionary _files = new(); + private readonly Dictionary _syntaxTrees = new(); + private readonly Dictionary _compilationUnits = new(); - public void UpdateFile(DocumentUri path) + public void UpdateFile(DocumentUri path, bool typeCheck = true) { var text = File.ReadAllText(path.GetFileSystemPath()); var tokenizer = new Tokenizer(path.GetFileSystemPath(), text); @@ -19,11 +21,40 @@ public class WorkspaceManager(DiagnosticsPublisher diagnosticsPublisher) var result = parser.Parse(tokenizer.Tokens); diagnosticsPublisher.Publish(path, parser.Diagnostics); - _files[path] = result; + _syntaxTrees[path] = result; + + if (typeCheck) + { + TypeCheck(); + } + } + + private void TypeCheck() + { + var modules = Module.Collect(_syntaxTrees.Select(x => x.Value).ToList()); + + foreach (var (path, syntaxTree) in _syntaxTrees) + { + var typeChecker = new TypeChecker(syntaxTree, modules); + var result = typeChecker.Check(); + diagnosticsPublisher.Publish(path, typeChecker.Diagnostics); + _compilationUnits[path] = result; + } } public void RemoveFile(Uri path) { - _files.Remove(path); + _syntaxTrees.Remove(path); + _compilationUnits.Remove(path); + } + + public Dictionary GetCompilationUnits() + { + return _compilationUnits; + } + + public CompilationUnit? GetCompilationUnit(DocumentUri path) + { + return _compilationUnits.GetValueOrDefault(path); } } \ No newline at end of file diff --git a/compiler/NubLang/Ast/TypeChecker.cs b/compiler/NubLang/Ast/TypeChecker.cs index 81c95be..f5b3f20 100644 --- a/compiler/NubLang/Ast/TypeChecker.cs +++ b/compiler/NubLang/Ast/TypeChecker.cs @@ -679,7 +679,7 @@ public sealed class TypeChecker return new FuncCallNode(expression.Tokens, funcType.ReturnType, accessor, parameters); } - private ExpressionNode CheckIdentifier(ExpressionSyntax expression, string moduleName, string name) + private ExpressionNode? CheckIdentifier(ExpressionSyntax expression, string moduleName, string name) { if (!_importedModules.TryGetValue(moduleName, out var module)) { @@ -707,10 +707,7 @@ public sealed class TypeChecker return new EnumReferenceIntermediateNode(expression.Tokens, moduleName, name); } - throw new TypeCheckerException(Diagnostic - .Error($"No exported symbol {name} not found in module {moduleName}") - .At(expression) - .Build()); + return null; } private ExpressionNode CheckLocalIdentifier(LocalIdentifierSyntax expression, NubType? _) @@ -722,13 +719,31 @@ public sealed class TypeChecker return new VariableIdentifierNode(expression.Tokens, scopeIdent.Type, expression.Name); } - return CheckIdentifier(expression, Scope.Module, expression.Name); + var ident = CheckIdentifier(expression, Scope.Module, expression.Name); + if (ident == null) + { + throw new TypeCheckerException(Diagnostic + .Error($"There is no identifier named {expression.Name}") + .At(expression) + .Build()); + } + + return ident; } private ExpressionNode CheckModuleIdentifier(ModuleIdentifierSyntax expression, NubType? _) { // note(nub31): Unlike local identifiers, module identifiers does not look for local variables - return CheckIdentifier(expression, expression.Module, expression.Name); + var ident = CheckIdentifier(expression, expression.Module, expression.Name); + if (ident == null) + { + throw new TypeCheckerException(Diagnostic + .Error($"Module {expression.Module} does not export a member named {expression.Name}") + .At(expression) + .Build()); + } + + return ident; } private ExpressionNode CheckStringLiteral(StringLiteralSyntax expression, NubType? expectedType) @@ -912,7 +927,14 @@ public sealed class TypeChecker var statements = new List(); foreach (var statement in node.Statements) { - statements.Add(CheckStatement(statement)); + try + { + statements.Add(CheckStatement(statement)); + } + catch (TypeCheckerException e) + { + Diagnostics.Add(e.Diagnostic); + } } return new BlockNode(node.Tokens, statements); diff --git a/vscode-lsp/package.json b/vscode-lsp/package.json index 3c9f56b..3cfe9de 100644 --- a/vscode-lsp/package.json +++ b/vscode-lsp/package.json @@ -1,7 +1,7 @@ { - "name": "nublang", + "name": "nub", "displayName": "Nub Language Support", - "description": "Language server client for nublang", + "description": "Language server client for nub lang", "version": "0.0.0", "publisher": "nub31", "repository": { @@ -18,7 +18,7 @@ "contributes": { "languages": [ { - "id": "nublang", + "id": "nub", "extensions": [ ".nub" ], @@ -27,7 +27,7 @@ ], "grammars": [ { - "language": "nublang", + "language": "nub", "scopeName": "source.nub", "path": "./syntaxes/nub.tmLanguage.json" } diff --git a/vscode-lsp/src/extension.ts b/vscode-lsp/src/extension.ts index 91ed391..58a4de7 100644 --- a/vscode-lsp/src/extension.ts +++ b/vscode-lsp/src/extension.ts @@ -7,8 +7,8 @@ export function activate(context: ExtensionContext) { const serverExecutable = '/home/oliste/repos/nub-lang/compiler/NubLang.LSP/bin/Debug/net9.0/NubLang.LSP'; client = new LanguageClient( - 'nublang', - 'nublang client', + 'nub', + 'nub lsp client', { run: { command: serverExecutable, diff --git a/vscode-lsp/syntaxes/nub.tmLanguage.json b/vscode-lsp/syntaxes/nub.tmLanguage.json index 39a8a26..17de754 100644 --- a/vscode-lsp/syntaxes/nub.tmLanguage.json +++ b/vscode-lsp/syntaxes/nub.tmLanguage.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "nublang", + "name": "nub", "scopeName": "source.nub", "patterns": [ {