From 5281d4608c9842a8cea87981de8f011ef2464d34 Mon Sep 17 00:00:00 2001 From: "Mateus Felipe C. C. Pinto" Date: Fri, 16 Aug 2024 14:51:53 -0300 Subject: [PATCH] Initial LSP implementation Signed-off-by: Mateus Felipe C. C. Pinto --- bin/pinto.dart | 62 ++++++----- bin/pinto_server.dart | 77 ++++++++++++++ bin/src/analyzer.dart | 136 +++++++++++++++++++++++++ grammar.bnf | 5 +- lib/localization.dart | 3 + lib/src/ast/parser.dart | 42 +------- lib/src/ast/scanner.dart | 26 +++-- lib/src/ast/statement.dart | 4 +- lib/src/ast/token.dart | 12 ++- lib/src/compiler/compiler.dart | 4 +- lib/src/error.dart | 15 ++- lib/src/localization.dart | 117 +++++++++++++++++++++ lib/src/semantic/resolver.dart | 29 ++++-- lib/src/semantic/symbols_resolver.dart | 106 ++++++++++--------- lib/src/semantic/type.dart | 10 +- pubspec.yaml | 7 +- tool/generate_ast.dart | 48 ++++----- 17 files changed, 539 insertions(+), 164 deletions(-) create mode 100644 bin/pinto_server.dart create mode 100644 bin/src/analyzer.dart create mode 100644 lib/localization.dart create mode 100644 lib/src/localization.dart diff --git a/bin/pinto.dart b/bin/pinto.dart index beee605..a50a15a 100644 --- a/bin/pinto.dart +++ b/bin/pinto.dart @@ -1,14 +1,21 @@ import 'dart:convert'; import 'dart:io'; +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/src/dart/sdk/sdk.dart'; +import 'package:analyzer/src/util/sdk.dart'; import 'package:chalkdart/chalkstrings.dart'; import 'package:dart_style/dart_style.dart'; import 'package:exitcode/exitcode.dart'; import 'package:pinto/ast.dart'; import 'package:pinto/compiler.dart'; import 'package:pinto/error.dart'; +import 'package:pinto/localization.dart'; import 'package:pinto/semantic.dart'; +final _resourceProvider = PhysicalResourceProvider.INSTANCE; + Future main(List args) async { if (args.length == 1) { await runFile(args.single); @@ -23,7 +30,24 @@ Future runFile(String path) async { if (await file.exists()) { final fileString = file.readAsStringSync(); - final error = await run(fileString); + + stdout.writeln('Included paths: ${file.absolute.path}'); + + final analysisContextCollection = AnalysisContextCollection( + includedPaths: [file.absolute.path], + resourceProvider: _resourceProvider, + ); + + final sdk = FolderBasedDartSdk( + _resourceProvider, + _resourceProvider.getFolder(getSdkPath()), + ); + + final error = await run( + source: fileString, + analysisContextCollection: analysisContextCollection, + sdk: sdk, + ); switch (error) { case PintoError(): @@ -37,7 +61,11 @@ Future runFile(String path) async { } } -Future run(String source) async { +Future run({ + required String source, + required AnalysisContextCollection analysisContextCollection, + required AbstractDartSdk sdk, +}) async { final errorHandler = ErrorHandler(); final lineSplitter = LineSplitter(); // TODO(mateusfccp): Convert the handler into an interface and put this logic inside @@ -91,25 +119,7 @@ Future run(String source) async { ScanError() => '[${error.location.line}:${error.location.column}]:', }; - final errorMessage = switch (error) { - // Parse errors - ExpectError(:final expectation) => "Expected to find $expectation.", - ExpectAfterError(:final token, :final expectation, :final after) => "Expected to find $expectation after $after. Found '${token.lexeme}'.", - ExpectBeforeError(:final expectation, :final before) => "Expected to find $expectation before $before.", - - // Resolve errors - NoSymbolInScopeError(:final token) => "The symbol ${token.lexeme} was not found in the scope.", - TypeAlreadyDefinedError(:final token) => "The type parameter '${token.lexeme}' is already defined for this type. Try removing it or changing it's name.", - WrongNumberOfArgumentsError(:final token, argumentsCount: 1, expectedArgumentsCount: 0) => "The type '${token.lexeme}' don't accept arguments, but 1 argument was provided.", - WrongNumberOfArgumentsError(:final token, :final argumentsCount, expectedArgumentsCount: 0) => "The type '${token.lexeme}' don't accept arguments, but $argumentsCount arguments were provided.", - WrongNumberOfArgumentsError(:final token, argumentsCount: 0, :final expectedArgumentsCount) => "The type '${token.lexeme}' expects $expectedArgumentsCount arguments, but none was provided.", - WrongNumberOfArgumentsError(:final token, argumentsCount: 1, :final expectedArgumentsCount) => "The type '${token.lexeme}' expects $expectedArgumentsCount arguments, but 1 was provided.", - WrongNumberOfArgumentsError(:final token, :final argumentsCount, :final expectedArgumentsCount) => - "The type '${token.lexeme}' expects $expectedArgumentsCount arguments, but $argumentsCount were provided.", - - // Scan errors - UnexpectedCharacterError() => "Unexpected character '${error.character}'.", - }; + final errorMessage = messageFromError(error); final lineHint = switch (error) { ScanError() => getLineWithErrorPointer(error.location.line, error.location.column, 1), @@ -136,11 +146,15 @@ Future run(String source) async { final program = parser.parse(); + final symbolsResolver = SymbolsResolver( + resourceProvider: _resourceProvider, + analysisContextCollection: analysisContextCollection, + sdk: sdk, + ); + final resolver = Resolver( program: program, - symbolsResolver: SymbolsResolver( - projectRoot: Directory.current.path, - ), + symbolsResolver: symbolsResolver, errorHandler: errorHandler, ); diff --git a/bin/pinto_server.dart b/bin/pinto_server.dart new file mode 100644 index 0000000..74f34ac --- /dev/null +++ b/bin/pinto_server.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:lsp_server/lsp_server.dart'; + +import 'src/analyzer.dart'; + +void main() async { + final connection = Connection(stdin, stdout); + final analyzer = Analyzer(); + + connection.onInitialize((params) async { + if (params.workspaceFolders case final worksPaceFolders?) { + for (final folder in worksPaceFolders) { + final directory = Directory.fromUri(folder.uri); + analyzer.addFileSystemEntity(directory); + } + } else if (params.rootUri case final uri?) { + final directory = Directory.fromUri(uri); + analyzer.addFileSystemEntity(directory); + } else if (params.rootPath case final path?) { + final directory = Directory(path); + analyzer.addFileSystemEntity(directory); + } + + return InitializeResult( + capabilities: ServerCapabilities( + textDocumentSync: const Either2.t1(TextDocumentSyncKind.Full), + ), + ); + }); + + // Register a listener for when the client sends a notification when a text + // document was opened. + connection.onDidOpenTextDocument((parameters) async { + final file = File.fromUri(parameters.textDocument.uri); + analyzer.addFileSystemEntity(file); + + final diagnostics = await analyzer.analyze( + parameters.textDocument.uri.path, + parameters.textDocument.text, + ); + + // Send back an event notifying the client of issues we want them to render. + // To clear issues the server is responsible for sending an empty list. + connection.sendDiagnostics( + PublishDiagnosticsParams( + diagnostics: diagnostics, + uri: parameters.textDocument.uri, + ), + ); + }); + + // Register a listener for when the client sends a notification when a text + // document was changed. + connection.onDidChangeTextDocument((parameters) async { + final contentChanges = parameters.contentChanges; + final contentChange = TextDocumentContentChangeEvent2.fromJson( + contentChanges[contentChanges.length - 1].toJson() as Map, + ); + + final diagnostics = await analyzer.analyze( + parameters.textDocument.uri.path, + contentChange.text, + ); + + // Send back an event notifying the client of issues we want them to render. + // To clear issues the server is responsible for sending an empty list. + connection.sendDiagnostics( + PublishDiagnosticsParams( + diagnostics: diagnostics, + uri: parameters.textDocument.uri, + ), + ); + }); + + await connection.listen(); +} diff --git a/bin/src/analyzer.dart b/bin/src/analyzer.dart new file mode 100644 index 0000000..7bad5a3 --- /dev/null +++ b/bin/src/analyzer.dart @@ -0,0 +1,136 @@ +import 'dart:collection'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:analyzer/src/dart/sdk/sdk.dart'; +import 'package:analyzer/src/util/sdk.dart'; +import 'package:lsp_server/lsp_server.dart'; +import 'package:pinto/ast.dart'; +import 'package:pinto/error.dart'; +import 'package:pinto/localization.dart'; +import 'package:pinto/semantic.dart'; +import 'package:quiver/collection.dart'; + +final _resourceProvider = PhysicalResourceProvider.INSTANCE; + +final class Analyzer { + final _analysisEntities = SplayTreeSet(); + final _analysisCache = >{}; + + late AnalysisContextCollection _analysisContextCollection; + late AbstractDartSdk _sdk; + + Analyzer() { + _analysisContextCollection = AnalysisContextCollection( + includedPaths: [], + resourceProvider: _resourceProvider, + ); + + _sdk = FolderBasedDartSdk( + _resourceProvider, + _resourceProvider.getFolder(getSdkPath()), + ); + } + + void addFileSystemEntity(FileSystemEntity entity) { + final path = entity.absolute.path; + final analysisEntities = [..._analysisEntities]; + + for (final folder in analysisEntities) { + if (path.startsWith(folder)) { + return; + } else if (folder.startsWith(path)) { + _analysisEntities.remove(folder); + } + } + + _analysisEntities.add(path); + + if (!listsEqual([..._analysisEntities], analysisEntities)) { + _rebuildAnalysisContext(); + } + } + + void _rebuildAnalysisContext() { + _analysisContextCollection.dispose(); + + _analysisContextCollection = AnalysisContextCollection( + includedPaths: [..._analysisEntities], + resourceProvider: _resourceProvider, + ); + } + + Future> analyze(String path, String text) async { + final errorHandler = ErrorHandler(); + + final scanner = Scanner( + source: text, + errorHandler: errorHandler, + ); + + final tokens = scanner.scanTokens(); + + final parser = Parser( + tokens: tokens, + errorHandler: errorHandler, + ); + + final program = parser.parse(); + + final symbolsResolver = SymbolsResolver( + resourceProvider: _resourceProvider, + analysisContextCollection: _analysisContextCollection, + sdk: _sdk, + ); + + final resolver = Resolver( + program: program, + symbolsResolver: symbolsResolver, + errorHandler: errorHandler, + ); + + await resolver.resolve(); + + final diagnostics = [ + for (final error in errorHandler.errors) _diagnosticFromError(error), + ]; + + _analysisCache[path] = diagnostics; + + return diagnostics; + } +} + +Diagnostic _diagnosticFromError(PintoError error) { + final start = switch (error) { + ScanError(:final location) => Position( + character: location.column - 1, + line: location.line - 1, + ), + ParseError(:final token) || ResolveError(:final token) => Position( + character: token.column - token.lexeme.length, + line: token.line - 1, + ), + }; + + final end = switch (error) { + ScanError(:final location) => Position( + character: location.column, + line: location.line - 1, + ), + ParseError(:final token) || ResolveError(:final token) => Position( + character: token.column, + line: token.line - 1, + ), + }; + + final range = Range(start: start, end: end); + + final message = messageFromError(error); + + return Diagnostic( + message: message, + range: range, + ); +} diff --git a/grammar.bnf b/grammar.bnf index a22c860..dff342a 100644 --- a/grammar.bnf +++ b/grammar.bnf @@ -7,8 +7,9 @@ ::= * * ::= "import" ( | ) - ::= "@" - ::= ( "/" )* + ::= "@" + ::= + ::= ( "/" )* ::= "type" ( "(" ( "," )* ")" )? "=" ( "+" )* ::= ( "(" ( "," )* ")" )? diff --git a/lib/localization.dart b/lib/localization.dart new file mode 100644 index 0000000..de5f3aa --- /dev/null +++ b/lib/localization.dart @@ -0,0 +1,3 @@ +library; + +export 'src/localization.dart'; diff --git a/lib/src/ast/parser.dart b/lib/src/ast/parser.dart index 815b8cb..dbe5f3f 100644 --- a/lib/src/ast/parser.dart +++ b/lib/src/ast/parser.dart @@ -57,7 +57,7 @@ final class Parser { : ExpectationType.statement(statement: body[body.length - 1]), ), ); - + body.add(_typeDefinition()); } on ParseError { _synchronize(); @@ -137,49 +137,17 @@ final class Parser { } ImportStatement _import() { - final ImportType type; + final identifier = _consumeExpecting(TokenType.importIdentifier); - final String package; + final ImportType type; - if (_match(TokenType.at)) { + if (identifier.lexeme[0] == '@') { type = ImportType.dart; - - final packageIdentifier = _consumeAfter( - type: TokenType.identifier, - after: TokenType.at, - ); - - package = packageIdentifier.lexeme; } else { type = ImportType.package; - - final root = _consumeAfter( - type: TokenType.identifier, - after: TokenType.importKeyword, - ); - - final subdirectories = []; - - while (_match(TokenType.slash)) { - final subdirectory = _consumeAfter( - type: TokenType.identifier, - after: TokenType.slash, - ); - - subdirectories.add(subdirectory); - } - - if (subdirectories.isEmpty) { - subdirectories.add(root); - } - - package = [ - root.lexeme, - for (final subdirectory in subdirectories) subdirectory.lexeme, - ].join('/'); } - return ImportStatement(type, package); + return ImportStatement(type, identifier); } TypeDefinitionStatement _typeDefinition() { diff --git a/lib/src/ast/scanner.dart b/lib/src/ast/scanner.dart index b51c503..25cbe4b 100644 --- a/lib/src/ast/scanner.dart +++ b/lib/src/ast/scanner.dart @@ -50,7 +50,7 @@ final class Scanner { return switch (character) { '⊤' => _addToken(TokenType.verum), '⊥' => _addToken(TokenType.falsum), - '@' => _addToken(TokenType.at), + '@' => _identifier(true), '?' => _addToken(TokenType.eroteme), '(' => _addToken(TokenType.leftParenthesis), ')' => _addToken(TokenType.rightParenthesis), @@ -88,7 +88,7 @@ final class Scanner { void _character(String character) { if (_isIdentifierStart(character)) { - _identifier(); + _identifier(false); } else { _errorHandler?.emit( UnexpectedCharacterError( @@ -150,8 +150,6 @@ final class Scanner { String get _peek => _isAtEnd ? '\x00' : _source[_current]; - String get _peekNext => _current + 1 >= _source.length ? '\x00' : _source[_current + 1]; - bool _isIdentifierStart(String character) { assert(character.length == 1); return RegExp(r'[A-Za-z_$]').hasMatch(character); @@ -162,13 +160,29 @@ final class Scanner { return RegExp(r'[A-Za-z_$0-9]').hasMatch(character); } - void _identifier() { + void _identifier(bool isImportIdentifier) { while (_isIdentifierPart(_peek)) { _advance(); } + while (_peek == '/') { + isImportIdentifier = true; + + while (_isIdentifierPart(_peek)) { + _advance(); + } + } + final text = _source.substring(_start, _current); - final tokenType = _keywords[text] ?? TokenType.identifier; + final TokenType tokenType; + + if (isImportIdentifier) { + tokenType = TokenType.importIdentifier; + } else if (_keywords[text] case final keyword?) { + tokenType = keyword; + } else { + tokenType = TokenType.identifier; + } _addToken(tokenType); } diff --git a/lib/src/ast/statement.dart b/lib/src/ast/statement.dart index 9679f15..e47e264 100644 --- a/lib/src/ast/statement.dart +++ b/lib/src/ast/statement.dart @@ -15,12 +15,12 @@ abstract interface class StatementVisitor { final class ImportStatement implements Statement { const ImportStatement( this.type, - this.package, + this.identifier, ); final ImportType type; - final String package; + final Token identifier; @override R accept(StatementVisitor visitor) => diff --git a/lib/src/ast/token.dart b/lib/src/ast/token.dart index bbc6983..938aa80 100644 --- a/lib/src/ast/token.dart +++ b/lib/src/ast/token.dart @@ -52,7 +52,7 @@ enum TokenType { /// The falsum token (`⊥`). falsum, - /// The identifier token. + /// An identifier. /// /// pint°'s identifier follows Dart's one. The grammar is the following: /// @@ -63,6 +63,15 @@ enum TokenType { /// ``` identifier, + /// An import identifier. + /// + /// The import identifier follows the grammar: + /// + ///```bnf + /// ::= "@"? ( "/" )* + ///``` + importIdentifier, + /// The `import` keyword token. importKeyword, @@ -107,6 +116,7 @@ enum TokenType { eroteme => '?', falsum => '⊥', identifier => 'identifier', + importIdentifier => 'import identifier', importKeyword => 'import', leftBrace => '{', leftBracket => '[', diff --git a/lib/src/compiler/compiler.dart b/lib/src/compiler/compiler.dart index e683929..000dc17 100644 --- a/lib/src/compiler/compiler.dart +++ b/lib/src/compiler/compiler.dart @@ -41,8 +41,8 @@ final class Compiler with DefaultTypeLiteralVisitor implements AstVisitor< assert(_currentClass == null); final url = switch (statement.type) { - ImportType.dart => 'dart:${statement.package}', - ImportType.package => 'package:${statement.package}.dart', + ImportType.dart => 'dart:${statement.identifier.lexeme.substring(1)}', + ImportType.package => 'package:${statement.identifier.lexeme}.dart', }; _directives.add( diff --git a/lib/src/error.dart b/lib/src/error.dart index ce2189b..8d95460 100644 --- a/lib/src/error.dart +++ b/lib/src/error.dart @@ -104,15 +104,22 @@ sealed class ResolveError implements PintoError { Token get token; } -final class NoSymbolInScopeError implements ResolveError { - const NoSymbolInScopeError(this.token); +final class ImportedPackageNotAvailableError implements ResolveError { + const ImportedPackageNotAvailableError(this.token); @override final Token token; } -final class TypeAlreadyDefinedError implements ResolveError { - const TypeAlreadyDefinedError(this.token); +final class SymbolNotInScopeError implements ResolveError { + const SymbolNotInScopeError(this.token); + + @override + final Token token; +} + +final class TypeParameterAlreadyDefinedError implements ResolveError { + const TypeParameterAlreadyDefinedError(this.token); @override final Token token; diff --git a/lib/src/localization.dart b/lib/src/localization.dart new file mode 100644 index 0000000..562150a --- /dev/null +++ b/lib/src/localization.dart @@ -0,0 +1,117 @@ +import 'package:intl/intl.dart'; +import 'package:pinto/error.dart'; + +String messageFromError(PintoError error) { + return switch (error) { + // Parse errors + ExpectError error => expectError('${error.expectation}', error.token.lexeme), + ExpectAfterError error => expectAfterError('${error.expectation}', '${error.after}', error.token.lexeme), + ExpectBeforeError error => expectBeforeError('${error.expectation}', '${error.before}', error.token.lexeme), + + // Resolve errors + ImportedPackageNotAvailableError error => importedPackageNotAvailableError(error.token.lexeme), + SymbolNotInScopeError error => symbolNotInScopeError(error.token.lexeme), + TypeParameterAlreadyDefinedError error => typeParameterAlreadyDefinedError(error.token.lexeme), + WrongNumberOfArgumentsError error => wrongNumberOfArgumentsError(error.argumentsCount, error.expectedArgumentsCount, error.token.lexeme), + + // Scan errors + UnexpectedCharacterError error => unexpectedCharacterError(error.character), + }; +} + +// Parse errors + +String expectError(String expectation, String found) { + return Intl.message( + "Expected to find $expectation. Found '$found'.", + name: 'expectErrorMessage', + args: [expectation, found], + desc: 'The error message for unqualified parsing expectation', + ); +} + +String expectAfterError(String expectation, String after, String found) { + return Intl.message( + "Expected to find $expectation after $after. Found '$found'.", + name: 'expectAfterErrorMessage', + args: [expectation, after, found], + desc: 'The error message for expected element after the current token', + ); +} + +String expectBeforeError(String expectation, String before, String found) { + return Intl.message( + 'Expected to find $expectation before $before. Found $found.', + name: 'expectBeforeErrorMessage', + args: [expectation, before, found], + desc: 'The error message for expected element before the current token', + ); +} + +// Resolve errors + +String importedPackageNotAvailableError(String import) { + return Intl.message( + "The imported package '$import' does not exist.", + name: 'importedPackageNotAvailableErrorMessage', + args: [import], + desc: 'The error describing that the package that is being imported does ' + 'exist or was not fetched by `pub get`', + ); +} + +String symbolNotInScopeError(String symbol) { + return Intl.message( + "The symbol '$symbol' was not found in the scope.", + name: 'symbolNotInScopeErrorMessage', + args: [symbol], + desc: 'The error message describing a symbol that was not found in the scope.', + ); +} + +String typeParameterAlreadyDefinedError(String typeParameter) { + return Intl.message( + "The type parameter '$typeParameter' is already defined for this type. Try removing it or changing it's name.", + name: 'typeAlreadyDefinedErrorMessage', + args: [typeParameter], + desc: 'The error message describing that a symbol is already defined in the scope.', + ); +} + +String wrongNumberOfArgumentsError(int argumentsCount, int expectedArgumentsCount, String type) { + return Intl.message( + Intl.plural( + argumentsCount, + zero: Intl.plural( + expectedArgumentsCount, + one: "The type '$type' expects one argument, but none was provided.", + other: "The type '$type' expects $expectedArgumentsCount arguments, but none was provided.", + ), + one: Intl.plural( + expectedArgumentsCount, + zero: "The type '$type' don't accept arguments, but an argument was provided.", + other: "The type '$type' expects $expectedArgumentsCount arguments, but an was provided.", + ), + other: Intl.plural( + expectedArgumentsCount, + zero: "The type '$type' don't accept arguments, but $argumentsCount arguments were provided.", + one: "The type '$type' expects one argument, but $argumentsCount arguments were provided.", + other: "The type '$type' expects $expectedArgumentsCount, but $argumentsCount arguments were provided.", + ), + ), + name: 'wrongNumberOfArgumentsErrorMessage', + args: [argumentsCount, expectedArgumentsCount, type], + desc: 'The error message for when the number of type arguments passed to a type is different than the expected.', + ); +} + +// Scan error + +String unexpectedCharacterError(String character) { + return Intl.message( + "Unexpected character '$character'.", + name: 'unexpectedCharacterErrorMessage', + args: [character], + desc: "The error message from when the scanner finds a character that it's not supposed to scan.", + ); +} diff --git a/lib/src/semantic/resolver.dart b/lib/src/semantic/resolver.dart index af0574b..3d8a743 100644 --- a/lib/src/semantic/resolver.dart +++ b/lib/src/semantic/resolver.dart @@ -34,20 +34,23 @@ final class Resolver with DefaultTypeLiteralVisitor> implements Ast @override Future visitImportStatement(ImportStatement statement) async { - final symbols = await symbolsResolver.getSymbolsFor(statement: statement); + try { + final symbols = await symbolsResolver.getSymbolsForImportStatement(statement: statement); - for (final symbol in symbols) { - _environment.defineType(symbol); + for (final symbol in symbols) { + _environment.defineType(symbol); + } + } on ResolveError catch (error) { + _errorHandler?.emit(error); } } @override Future visitProgram(Program program) async { - // TODO(mateusfccp): maybe find a better place for this - final core = ImportStatement(ImportType.dart, 'core'); - await core.accept(this); + final core = DartSdkPackage(name: 'core'); await Future.wait([ + _resolvePackage(core), for (final import in program.imports) import.accept(this), ]); @@ -86,7 +89,7 @@ final class Resolver with DefaultTypeLiteralVisitor> implements Ast if (definedType is TypeParameterType) { _errorHandler?.emit( - TypeAlreadyDefinedError(typeParameter.identifier), + TypeParameterAlreadyDefinedError(typeParameter.identifier), ); } else { final type = TypeParameterType(name: typeParameter.identifier.lexeme); @@ -148,7 +151,7 @@ final class Resolver with DefaultTypeLiteralVisitor> implements Ast final baseType = _environment.getType(literal.literal.identifier.lexeme); if (baseType == null) { - throw NoSymbolInScopeError(literal.literal.identifier); + throw SymbolNotInScopeError(literal.literal.identifier); } else if (baseType is PolymorphicType) { final arguments = [ for (final parameter in literal.parameters) _resolveType(parameter), @@ -178,7 +181,7 @@ final class Resolver with DefaultTypeLiteralVisitor> implements Ast final type = _environment.getType(literal.identifier.lexeme); if (type == null) { - throw NoSymbolInScopeError(literal.identifier); + throw SymbolNotInScopeError(literal.identifier); } else if (type is PolymorphicType) { throw WrongNumberOfArgumentsError( token: literal.identifier, @@ -199,4 +202,12 @@ final class Resolver with DefaultTypeLiteralVisitor> implements Ast ); } } + + Future _resolvePackage(Package package) async { + final symbols = await symbolsResolver.getSymbolForPackage(package: package); + + for (final symbol in symbols) { + _environment.defineType(symbol); + } + } } diff --git a/lib/src/semantic/symbols_resolver.dart b/lib/src/semantic/symbols_resolver.dart index 6eba3ae..8cb945c 100644 --- a/lib/src/semantic/symbols_resolver.dart +++ b/lib/src/semantic/symbols_resolver.dart @@ -2,76 +2,49 @@ import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/results.dart'; -import 'package:analyzer/src/util/sdk.dart'; +import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/src/context/packages.dart' hide Package; import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/file_system/physical_file_system.dart'; import 'package:analyzer/src/dart/sdk/sdk.dart'; import 'package:pinto/ast.dart'; +import 'package:pinto/error.dart'; import 'type.dart'; final class SymbolsResolver { - SymbolsResolver({required this.projectRoot}) { - final resourceProvider = PhysicalResourceProvider.INSTANCE; - - _analysisContextCollection = AnalysisContextCollection( - includedPaths: [projectRoot], - resourceProvider: resourceProvider, - ); - - _sdk = FolderBasedDartSdk( - resourceProvider, - resourceProvider.getFolder(getSdkPath()), - ); - + SymbolsResolver({ + required this.resourceProvider, + required this.analysisContextCollection, + required this.sdk, + }) { _packages = findPackagesFrom( resourceProvider, - _analysisContextCollection.contexts.first.contextRoot.root, + analysisContextCollection.contexts.first.contextRoot.root, ); } - final String projectRoot; - - late AnalysisContextCollection _analysisContextCollection; - late AbstractDartSdk _sdk; + final ResourceProvider resourceProvider; + final AnalysisContextCollection analysisContextCollection; + final AbstractDartSdk sdk; late Packages _packages; - Future> getSymbolsFor({required ImportStatement statement}) async { - final String uri; - - switch (statement.type) { - case ImportType.dart: - uri = _sdk.mapDartUri('dart:${statement.package}')!.fullName; - case ImportType.package: - final segments = statement.package.split('/'); - final package = segments.first; + Future> getSymbolForPackage({required Package package}) async { + final uri = _getUriFromPackage(package); - final String file; - - if (segments.length == 1) { - file = '$package.dart'; - } else { - file = '${segments.skip(1).join('/')}.dart'; - } - - uri = '${_packages[package]!.libFolder}/$file'; + if (uri == null) { + throw _SymbolResolvingException(package); } - final library = await _analysisContextCollection.contexts.first.currentSession.getResolvedLibrary(uri); + final library = await analysisContextCollection.contexts.first.currentSession.getResolvedLibrary(uri); if (library is ResolvedLibraryResult) { - final source = switch (statement.type) { - ImportType.dart => DartSdkPackage(name: statement.package), - ImportType.package => ExternalPackage(name: statement.package), - }; return [ for (var element in library.element.exportNamespace.definedNames.values) if (element is InterfaceElement) if (element.typeParameters case List(isEmpty: false) && final typeParameters) PolymorphicType( name: element.name, - source: source, + source: package, arguments: [ for (final typeParameter in typeParameters) // TypeParameterType(name: typeParameter.name), @@ -80,11 +53,52 @@ final class SymbolsResolver { else MonomorphicType( name: element.name, - source: source, + source: package, ), ]; } else { - throw 'Heheheh'; + throw _SymbolResolvingException(package); + } + } + + Future> getSymbolsForImportStatement({required ImportStatement statement}) async { + final package = switch (statement.type) { + ImportType.dart => DartSdkPackage(name: statement.identifier.lexeme.substring(1)), + ImportType.package => ExternalPackage(name: statement.identifier.lexeme), + }; + + try { + return await getSymbolForPackage(package: package); + } on _SymbolResolvingException { + throw ImportedPackageNotAvailableError(statement.identifier); + } + } + + String? _getUriFromPackage(Package package) { + switch (package) { + case DartSdkPackage(:final name): + return sdk.mapDartUri('dart:$name')?.fullName; + case ExternalPackage(:final name): + final parts = name.split('/'); + final package = parts.first; + + final String file; + + if (parts.length == 1) { + file = '$package.dart'; + } else { + file = '${parts.skip(1).join('/')}.dart'; + } + + final folder = _packages[package]?.libFolder; + + return folder == null ? null : '$folder/$file'; } } } + +final class _SymbolResolvingException implements Exception { + const _SymbolResolvingException(this.package); + + final Package package; +} diff --git a/lib/src/semantic/type.dart b/lib/src/semantic/type.dart index 9c6e287..db2a5cb 100644 --- a/lib/src/semantic/type.dart +++ b/lib/src/semantic/type.dart @@ -1,6 +1,6 @@ -sealed class TypeSource {} +sealed class Package {} -final class DartSdkPackage implements TypeSource { +final class DartSdkPackage implements Package { const DartSdkPackage({required this.name}); final String name; @@ -18,7 +18,7 @@ final class DartSdkPackage implements TypeSource { String toString() => 'DartSdkPackage(name: $name)'; } -final class ExternalPackage implements TypeSource { +final class ExternalPackage implements Package { const ExternalPackage({required this.name}); final String name; @@ -59,7 +59,7 @@ final class MonomorphicType implements Type { final String name; - final TypeSource source; + final Package source; @override bool operator ==(Object other) { @@ -86,7 +86,7 @@ final class PolymorphicType implements Type { final String name; - final TypeSource source; + final Package source; final List arguments; diff --git a/pubspec.yaml b/pubspec.yaml index 3dbbc48..ac9b848 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: pinto description: The pint° programming language -version: 0.0.2 +version: 0.0.3 repository: https://github.com/mateusfccp/pinto environment: - sdk: ">=3.4.0 <4.0.0" + sdk: ">=3.5.0 <4.0.0" dependencies: analyzer: ^6.8.0 @@ -14,6 +14,8 @@ dependencies: dart_style: ^2.3.6 exitcode: ^1.0.1 freezed_annotation: ^2.4.4 + intl: ^0.19.0 + lsp_server: ^0.3.2 meta: ^1.15.0 path: ^1.9.0 quiver: ^3.2.1 @@ -27,3 +29,4 @@ dev_dependencies: executables: pinto: + pinto_server: diff --git a/tool/generate_ast.dart b/tool/generate_ast.dart index 61c02d0..2278d63 100644 --- a/tool/generate_ast.dart +++ b/tool/generate_ast.dart @@ -11,28 +11,6 @@ void main(List args) { } else { final [outputDir] = args; - _defineAst( - outputDir, - 'TypeLiteral', - { - 'Top': [], - 'Bottom': [], - 'List': [('TypeLiteral', 'literal')], - 'Set': [('TypeLiteral', 'literal')], - 'Map': [ - ('TypeLiteral', 'keyLiteral'), - ('TypeLiteral', 'valueLiteral'), - ], - 'Parameterized': [ - ('NamedTypeLiteral', 'literal'), - ('List', 'parameters'), - ], - 'Named': [('Token', 'identifier')], - 'Option': [('TypeLiteral', 'literal')], - }, - ['token.dart'], - ); - _defineAst( outputDir, 'Node', @@ -55,7 +33,7 @@ void main(List args) { { 'Import': [ ('ImportType', 'type'), - ('String', 'package'), + ('Token', 'identifier'), ], 'TypeDefinition': [ ('Token', 'name'), @@ -63,7 +41,29 @@ void main(List args) { ('List', 'variants'), ], }, - ['node.dart', '../import.dart', 'token.dart', 'type_literal.dart'], + ['node.dart', 'import.dart', 'token.dart', 'type_literal.dart'], + ); + + _defineAst( + outputDir, + 'TypeLiteral', + { + 'Top': [], + 'Bottom': [], + 'List': [('TypeLiteral', 'literal')], + 'Set': [('TypeLiteral', 'literal')], + 'Map': [ + ('TypeLiteral', 'keyLiteral'), + ('TypeLiteral', 'valueLiteral'), + ], + 'Parameterized': [ + ('NamedTypeLiteral', 'literal'), + ('List', 'parameters'), + ], + 'Named': [('Token', 'identifier')], + 'Option': [('TypeLiteral', 'literal')], + }, + ['token.dart'], ); } }