diff --git a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt index 74235f37..da1ec894 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt @@ -16,7 +16,7 @@ internal fun compile(command: CompileCommand, controller: DiagnosticController) // attempt to parse each source file into an AST val fileNodes = buildList { for (source in sourceFiles) { - val context = controller.createContext(source) + val context = controller.getOrCreateContext(source) val tokenStream = Lexer.scan(source.content.reader(), context) if (context.hasErrors()) { diff --git a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt index 25ead32c..4605d1e5 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt @@ -19,7 +19,7 @@ internal fun dump(command: DumpCommand, terminal: Terminal, controller: Diagnost // attempt to parse each source file into an AST val fileNodes = buildList { for (source in sourceFiles) { - val context = controller.createContext(source) + val context = controller.getOrCreateContext(source) if (dumpAll || command.dumpTokens) { // create duplicate scan because sequence can only be iterated once diff --git a/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt b/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt index e4dae8ab..3a08465c 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt @@ -2,34 +2,11 @@ package tools.samt.cli import tools.samt.common.DiagnosticController import tools.samt.common.SourceFile +import tools.samt.common.collectSamtFiles +import tools.samt.common.readSamtSource import java.io.File -internal fun List.readSamtSourceFiles(controller: DiagnosticController): List { - val files = map { File(it) }.ifEmpty { gatherSamtFiles(controller.workingDirectoryAbsolutePath) } +internal fun List.readSamtSourceFiles(controller: DiagnosticController): List = + map { File(it) }.ifEmpty { collectSamtFiles(controller.workingDirectoryAbsolutePath) } + .readSamtSource(controller) - return buildList { - for (file in files) { - if (!file.exists()) { - controller.reportGlobalError("File '${file.canonicalPath}' does not exist") - continue - } - - if (!file.canRead()) { - controller.reportGlobalError("File '${file.canonicalPath}' cannot be read, bad file permissions?") - continue - } - - if (file.extension != "samt") { - controller.reportGlobalError("File '${file.canonicalPath}' must end in .samt") - continue - } - - add(SourceFile(file.canonicalPath, content = file.readText())) - } - } -} - -internal fun gatherSamtFiles(directory: String): List { - val dir = File(directory) - return dir.walkTopDown().filter { it.isFile && it.extension == "samt" }.toList() -} diff --git a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt index 641c7419..33486b04 100644 --- a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt +++ b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt @@ -84,7 +84,7 @@ class ASTPrinterTest { val filePath = "/tmp/ASTPrinterTest.samt" val sourceFile = SourceFile(filePath, source) val diagnosticController = DiagnosticController("/tmp") - val diagnosticContext = diagnosticController.createContext(sourceFile) + val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile) val stream = Lexer.scan(source.reader(), diagnosticContext) val fileTree = Parser.parse(sourceFile, stream, diagnosticContext) assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors") diff --git a/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt b/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt index e8b2596f..972d1e3d 100644 --- a/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt +++ b/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt @@ -45,7 +45,7 @@ class DiagnosticFormatterTest { val controller = DiagnosticController(baseDirectory) val source = "" val sourceFile = SourceFile(filePath, source) - val context = controller.createContext(sourceFile) + val context = controller.getOrCreateContext(sourceFile) context.error { message("some error") @@ -484,7 +484,7 @@ class DiagnosticFormatterTest { val filePath = Path("/tmp", "DiagnosticFormatterTest.samt").absolutePathString() val sourceFile = SourceFile(filePath, source) val diagnosticController = DiagnosticController(baseDirectory) - val diagnosticContext = diagnosticController.createContext(sourceFile) + val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile) val stream = Lexer.scan(source.reader(), diagnosticContext) val fileTree = Parser.parse(sourceFile, stream, diagnosticContext) assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors") diff --git a/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt index 9496f0c8..8e43e47e 100644 --- a/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt +++ b/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt @@ -49,7 +49,7 @@ class TokenPrinterTest { val filePath = "/tmp/TokenPrinterTest.samt" val sourceFile = SourceFile(filePath, source) val diagnosticController = DiagnosticController("/tmp") - val diagnosticContext = diagnosticController.createContext(sourceFile) + val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile) val stream = Lexer.scan(source.reader(), diagnosticContext) assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors") return stream diff --git a/common/src/main/kotlin/tools/samt/common/Diagnostics.kt b/common/src/main/kotlin/tools/samt/common/Diagnostics.kt index 7f0857bd..4a793c07 100644 --- a/common/src/main/kotlin/tools/samt/common/Diagnostics.kt +++ b/common/src/main/kotlin/tools/samt/common/Diagnostics.kt @@ -56,7 +56,7 @@ class DiagnosticController(val workingDirectoryAbsolutePath: String) { /** * Creates a new diagnostic context for the given source file or returns already existing one. * */ - fun createContext(source: SourceFile): DiagnosticContext { + fun getOrCreateContext(source: SourceFile): DiagnosticContext { val foundContext = contexts.find { it.source == source} if (foundContext != null) return foundContext return DiagnosticContext(source).also { contexts.add(it) } diff --git a/common/src/main/kotlin/tools/samt/common/Files.kt b/common/src/main/kotlin/tools/samt/common/Files.kt new file mode 100644 index 00000000..7c062f84 --- /dev/null +++ b/common/src/main/kotlin/tools/samt/common/Files.kt @@ -0,0 +1,31 @@ +package tools.samt.common + +import java.io.File + +fun List.readSamtSource(controller: DiagnosticController): List { + return buildList { + for (file in this@readSamtSource) { + if (!file.exists()) { + controller.reportGlobalError("File '${file.canonicalPath}' does not exist") + continue + } + + if (!file.canRead()) { + controller.reportGlobalError("File '${file.canonicalPath}' cannot be read, bad file permissions?") + continue + } + + if (file.extension != "samt") { + controller.reportGlobalError("File '${file.canonicalPath}' must end in .samt") + continue + } + + add(SourceFile(file.canonicalPath, content = file.readText())) + } + } +} + +fun collectSamtFiles(directory: String): List { + val dir = File(directory) + return dir.walkTopDown().filter { it.isFile && it.extension == "samt" }.toList() +} \ No newline at end of file diff --git a/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt b/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt index b879964f..9e421322 100644 --- a/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt +++ b/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt @@ -36,7 +36,7 @@ class DiagnosticsTest { package debug """.trimIndent() val sourceFile = SourceFile(sourcePath, sourceCode) - val context = controller.createContext(sourceFile) + val context = controller.getOrCreateContext(sourceFile) assertThrows("some fatal error") { context.fatal { @@ -65,7 +65,7 @@ class DiagnosticsTest { package debug """.trimIndent().trim() val sourceFile = SourceFile(sourcePath, sourceCode) - val context = controller.createContext(sourceFile) + val context = controller.getOrCreateContext(sourceFile) val importStatementStartOffset = FileOffset(0, 0, 0) val importStatementEndOffset = FileOffset(17, 0, 17) diff --git a/language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt b/language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt new file mode 100644 index 00000000..b60e85ee --- /dev/null +++ b/language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt @@ -0,0 +1,35 @@ +package tools.samt.ls + +import tools.samt.common.DiagnosticContext +import tools.samt.common.DiagnosticException +import tools.samt.common.SourceFile +import tools.samt.lexer.Lexer +import tools.samt.lexer.Token +import tools.samt.parser.FileNode +import tools.samt.parser.Parser + +class FileInfo( + val diagnosticContext: DiagnosticContext, + val sourceFile: SourceFile, + val tokens: List, + val fileNode: FileNode? = null +) + +fun parseFile(sourceFile: SourceFile): FileInfo { + val diagnosticContext = DiagnosticContext(sourceFile) + + val tokens = Lexer.scan(sourceFile.content.reader(), diagnosticContext).toList() + + if (diagnosticContext.hasErrors()) { + return FileInfo(diagnosticContext, sourceFile, tokens) + } + + val fileNode = try { + Parser.parse(sourceFile, tokens.asSequence(), diagnosticContext) + } catch (e: DiagnosticException) { + // error message is added to the diagnostic console, so it can be ignored here + return FileInfo(diagnosticContext, sourceFile, tokens) + } + + return FileInfo(diagnosticContext, sourceFile, tokens, fileNode) +} \ No newline at end of file diff --git a/language-server/src/main/kotlin/tools/samt/ls/Mapping.kt b/language-server/src/main/kotlin/tools/samt/ls/Mapping.kt new file mode 100644 index 00000000..0080379f --- /dev/null +++ b/language-server/src/main/kotlin/tools/samt/ls/Mapping.kt @@ -0,0 +1,34 @@ +package tools.samt.ls + +import org.eclipse.lsp4j.* +import tools.samt.common.DiagnosticMessage +import tools.samt.common.DiagnosticSeverity +import tools.samt.common.Location as SamtLocation + +fun DiagnosticMessage.toDiagnostic(): Diagnostic? { + val diagnostic = Diagnostic() + val primaryHighlight = this.highlights.firstOrNull() ?: return null + // TODO consider highlightBeginningOnly + diagnostic.range = primaryHighlight.location.toRange() + diagnostic.severity = when (severity) { + DiagnosticSeverity.Error -> org.eclipse.lsp4j.DiagnosticSeverity.Error + DiagnosticSeverity.Warning -> org.eclipse.lsp4j.DiagnosticSeverity.Warning + DiagnosticSeverity.Info -> org.eclipse.lsp4j.DiagnosticSeverity.Information + } + diagnostic.source = "samt" + diagnostic.message = message + diagnostic.relatedInformation = highlights.filter { it.message != null }.map { + DiagnosticRelatedInformation( + Location("file://${it.location.source.absolutePath}", it.location.toRange()), + it.message + ) + } + return diagnostic +} + +fun SamtLocation.toRange(): Range { + return Range( + Position(start.row, start.col), + Position(start.row, end.col) + ) +} \ No newline at end of file diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt index 6755a337..5713f1fb 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -2,6 +2,7 @@ package tools.samt.ls import org.eclipse.lsp4j.* import org.eclipse.lsp4j.services.* +import tools.samt.common.* import java.io.Closeable import java.util.concurrent.CompletableFuture import java.util.logging.Logger @@ -9,18 +10,23 @@ import kotlin.system.exitProcess class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { private lateinit var client: LanguageClient - private val textDocumentService = SamtTextDocumentService() private val logger = Logger.getLogger("SamtLanguageServer") + private val workspaces = mutableMapOf() + private val textDocumentService = SamtTextDocumentService(workspaces) override fun initialize(params: InitializeParams): CompletableFuture = CompletableFuture.supplyAsync { + buildSamtModel(params) val capabilities = ServerCapabilities().apply { - // TODO support pull-based diagnostics? diagnosticProvider = DiagnosticRegistrationOptions(true, false) setTextDocumentSync(TextDocumentSyncKind.Full) } InitializeResult(capabilities) } + override fun initialized(params: InitializedParams) { + pushDiagnostics() + } + override fun setTrace(params: SetTraceParams?) { // TODO } @@ -44,4 +50,31 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { override fun close() { shutdown().get() } + + private fun buildSamtModel(params: InitializeParams) { + params.workspaceFolders.forEach { + val path = it.uri.uriToPath() + workspaces[path] = buildWorkspace(path) + } + } + + private fun buildWorkspace(workspacePath: String): SamtWorkspace { + val diagnosticController = DiagnosticController(workspacePath) + val sourceFiles = collectSamtFiles(workspacePath).readSamtSource(diagnosticController) + val workspace = SamtWorkspace(diagnosticController) + sourceFiles.asSequence().map(::parseFile).forEach(workspace::add) + workspace.buildSemanticModel() + return workspace + } + + private fun pushDiagnostics() { + workspaces.values.flatMap { workspace -> + workspace.getAllMessages().map { (path, messages) -> + PublishDiagnosticsParams( + path.pathToUri(), + messages.map { it.toDiagnostic() } + ) + } + }.forEach(client::publishDiagnostics) + } } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt index 1de8aaa8..51e65420 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt @@ -4,48 +4,37 @@ import org.eclipse.lsp4j.* import org.eclipse.lsp4j.services.LanguageClient import org.eclipse.lsp4j.services.LanguageClientAware import org.eclipse.lsp4j.services.TextDocumentService -import tools.samt.common.DiagnosticController -import tools.samt.common.DiagnosticException -import tools.samt.common.DiagnosticMessage import tools.samt.common.SourceFile -import tools.samt.lexer.Lexer -import tools.samt.parser.Parser -import tools.samt.semantic.SemanticModelBuilder import java.util.logging.Logger -import kotlin.io.path.Path -import kotlin.io.path.readText -import tools.samt.common.DiagnosticSeverity as SamtSeverity -import tools.samt.common.Location as SamtLocation -class SamtTextDocumentService : TextDocumentService, LanguageClientAware { +class SamtTextDocumentService(private val workspaces: Map) : TextDocumentService, + LanguageClientAware { private lateinit var client: LanguageClient private val logger = Logger.getLogger("SamtTextDocumentService") override fun didOpen(params: DidOpenTextDocumentParams) { logger.info("Opened document ${params.textDocument.uri}") - - val uri = params.textDocument.uri.removePrefix("file://") - val messages = compile(uri, Path(uri).readText()) - - client.publishDiagnostics( - PublishDiagnosticsParams( - params.textDocument.uri, - messages.mapNotNull { it.toDiagnostic() }) - ) } override fun didChange(params: DidChangeTextDocumentParams) { logger.info("Changed document ${params.textDocument.uri}") - val uri = params.textDocument.uri.removePrefix("file://") - val newText = params.contentChanges.firstOrNull()?.text ?: Path(uri).readText() - val messages = compile(uri, newText) - - client.publishDiagnostics( - PublishDiagnosticsParams( - params.textDocument.uri, - messages.mapNotNull { it.toDiagnostic() }) - ) + val path = params.textDocument.uri.uriToPath() + val newText = params.contentChanges.single().text + val fileInfo = parseFile(SourceFile(path, newText)) + val workspaces = getWorkspaces(path) + + workspaces.forEach { workspace -> + workspace.add(fileInfo) + workspace.buildSemanticModel() + workspace.getAllMessages().forEach { (path, messages) -> + client.publishDiagnostics(PublishDiagnosticsParams( + path.pathToUri(), + messages.map { it.toDiagnostic() }, + params.textDocument.version + )) + } + } } override fun didClose(params: DidCloseTextDocumentParams) { @@ -56,62 +45,10 @@ class SamtTextDocumentService : TextDocumentService, LanguageClientAware { logger.info("Saved document ${params.textDocument.uri}") } - private fun SamtLocation.toRange(): Range { - return Range( - Position(start.row, start.col), - Position(start.row, end.col) - ) - } - - private fun DiagnosticMessage.toDiagnostic(): Diagnostic? { - val diagnostic = Diagnostic() - val primaryHighlight = this.highlights.firstOrNull() - if (primaryHighlight == null) { - logger.warning("Diagnostic message without location, how do I convert this???") - return null - } - // TODO consider highlightBeginningOnly - diagnostic.range = primaryHighlight.location.toRange() - diagnostic.severity = when (severity) { - SamtSeverity.Error -> DiagnosticSeverity.Error - SamtSeverity.Warning -> DiagnosticSeverity.Warning - SamtSeverity.Info -> DiagnosticSeverity.Information - } - diagnostic.source = "samt" - diagnostic.message = message - diagnostic.relatedInformation = highlights.filter { it.message != null }.map { - DiagnosticRelatedInformation( - Location("file://${it.location.source.absolutePath}", it.location.toRange()), - it.message - ) - } - return diagnostic - } - - private fun compile(uri: String, content: String): List { - val source = SourceFile(uri, content) - val controller = DiagnosticController("todo") - val context = controller.createContext(source) - - val tokenStream = Lexer.scan(source.content.reader(), context) - - if (context.hasErrors()) { - return context.messages - } - - val fileNode = try { - Parser.parse(source, tokenStream, context) - } catch (e: DiagnosticException) { - // error message is added to the diagnostic console, so it can be ignored here - return context.messages - } - - SemanticModelBuilder.build(listOf(fileNode), controller) - - return context.messages - } - override fun connect(client: LanguageClient) { this.client = client } + + private fun getWorkspaces(filePath: String): List = + workspaces.values.filter { filePath in it } } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt new file mode 100644 index 00000000..fa0d89f1 --- /dev/null +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt @@ -0,0 +1,38 @@ +package tools.samt.ls + +import tools.samt.common.DiagnosticController +import tools.samt.common.DiagnosticMessage +import tools.samt.semantic.Package +import tools.samt.semantic.SemanticModelBuilder + +class SamtWorkspace(private val parserController: DiagnosticController) : Iterable { + private val files = mutableMapOf() + private var samtPackage: Package? = null + private var semanticController: DiagnosticController = + DiagnosticController(parserController.workingDirectoryAbsolutePath) + + fun add(fileInfo: FileInfo) { + files[fileInfo.sourceFile.absolutePath] = fileInfo + } + + operator fun get(path: String): FileInfo? = files[path] + + override fun iterator(): Iterator = files.values.iterator() + + operator fun contains(path: String) = path in files + + fun buildSemanticModel() { + semanticController = DiagnosticController(parserController.workingDirectoryAbsolutePath) + samtPackage = SemanticModelBuilder.build(mapNotNull { it.fileNode }, semanticController) + } + + fun getMessages(path: String): List { + val fileInfo = files[path] ?: return emptyList() + return fileInfo.diagnosticContext.messages + + semanticController.getOrCreateContext(fileInfo.sourceFile).messages + } + + fun getAllMessages() = files.keys.associateWith { + getMessages(it) + } +} \ No newline at end of file diff --git a/language-server/src/main/kotlin/tools/samt/ls/Uri.kt b/language-server/src/main/kotlin/tools/samt/ls/Uri.kt new file mode 100644 index 00000000..339c7240 --- /dev/null +++ b/language-server/src/main/kotlin/tools/samt/ls/Uri.kt @@ -0,0 +1,7 @@ +package tools.samt.ls + +private const val FILE_PROTOCOL = "file://" + +fun String.uriToPath(): String = removePrefix(FILE_PROTOCOL) + +fun String.pathToUri(): String = if (startsWith(FILE_PROTOCOL)) this else "$FILE_PROTOCOL$this" \ No newline at end of file diff --git a/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt b/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt index 0f5431df..8e935698 100644 --- a/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt +++ b/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt @@ -39,9 +39,7 @@ class Lexer private constructor( skipBlanks() resetStartPosition() } - while (true) { - yield(EndOfFileToken(windowLocation())) - } + yield(EndOfFileToken(windowLocation())) } private fun readToken(): Token? = when { @@ -81,7 +79,7 @@ class Lexer private constructor( } } - private inline fun readStructureToken(factory: (location: Location) -> T): StructureToken { + private inline fun readStructureToken(factory: (location: Location) -> T): StructureToken { readNext() return factory(windowLocation()) } @@ -283,6 +281,7 @@ class Lexer private constructor( skipLineComment() return null } + '*' -> { skipCommentBlock() return null @@ -319,7 +318,13 @@ class Lexer private constructor( if (current == '*') { readNext() nestedCommentDepth++ - commentOpenerPositionStack.add(Location(diagnostic.source, currentCharacterPosition, currentPosition)) + commentOpenerPositionStack.add( + Location( + diagnostic.source, + currentCharacterPosition, + currentPosition + ) + ) } } @@ -404,24 +409,24 @@ class Lexer private constructor( companion object { val KEYWORDS: Map StaticToken> = mapOf( - "record" to { RecordToken(it) }, - "enum" to { EnumToken(it) }, - "service" to { ServiceToken(it) }, - "alias" to { AliasToken(it) }, - "package" to { PackageToken(it) }, - "import" to { ImportToken(it) }, - "provide" to { ProvideToken(it) }, - "consume" to { ConsumeToken(it) }, - "transport" to { TransportToken(it) }, - "implements" to { ImplementsToken(it) }, - "uses" to { UsesToken(it) }, - "extends" to { ExtendsToken(it) }, - "as" to { AsToken(it) }, - "async" to { AsyncToken(it) }, - "oneway" to { OnewayToken(it) }, - "raises" to { RaisesToken(it) }, - "true" to { TrueToken(it) }, - "false" to { FalseToken(it) }, + "record" to { RecordToken(it) }, + "enum" to { EnumToken(it) }, + "service" to { ServiceToken(it) }, + "alias" to { AliasToken(it) }, + "package" to { PackageToken(it) }, + "import" to { ImportToken(it) }, + "provide" to { ProvideToken(it) }, + "consume" to { ConsumeToken(it) }, + "transport" to { TransportToken(it) }, + "implements" to { ImplementsToken(it) }, + "uses" to { UsesToken(it) }, + "extends" to { ExtendsToken(it) }, + "as" to { AsToken(it) }, + "async" to { AsyncToken(it) }, + "oneway" to { OnewayToken(it) }, + "raises" to { RaisesToken(it) }, + "true" to { TrueToken(it) }, + "false" to { FalseToken(it) }, ) fun scan(reader: Reader, diagnostics: DiagnosticContext): Sequence { diff --git a/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt b/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt index 5684115b..1f925335 100644 --- a/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt +++ b/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt @@ -369,7 +369,7 @@ SAMT!""", stream.next() private fun readTokenStream(source: String): Pair, DiagnosticContext> { val sourceFile = SourceFile("/tmp/test", source) - val context = diagnosticController.createContext(sourceFile) + val context = diagnosticController.getOrCreateContext(sourceFile) return Pair(Lexer.scan(source.reader(), context).iterator(), context) } diff --git a/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt b/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt index a5602abc..82e484fb 100644 --- a/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt +++ b/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt @@ -822,7 +822,7 @@ class ParserTest { val filePath = "/tmp/ParserTest.samt" val sourceFile = SourceFile(filePath, source) val diagnosticController = DiagnosticController("/tmp") - val diagnosticContext = diagnosticController.createContext(sourceFile) + val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile) val stream = Lexer.scan(source.reader(), diagnosticContext) val fileTree = Parser.parse(sourceFile, stream, diagnosticContext) assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors") @@ -833,7 +833,7 @@ class ParserTest { val filePath = "/tmp/ParserTest.samt" val sourceFile = SourceFile(filePath, source) val diagnosticController = DiagnosticController("/tmp") - val diagnosticContext = diagnosticController.createContext(sourceFile) + val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile) val stream = Lexer.scan(source.reader(), diagnosticContext) val fileTree = Parser.parse(sourceFile, stream, diagnosticContext) assertTrue(diagnosticContext.hasErrors(), "Expected errors, but had no errors") @@ -844,7 +844,7 @@ class ParserTest { val filePath = "/tmp/ParserTest.samt" val sourceFile = SourceFile(filePath, source) val diagnosticController = DiagnosticController("/tmp") - val diagnosticContext = diagnosticController.createContext(sourceFile) + val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile) val stream = Lexer.scan(source.reader(), diagnosticContext) val exception = assertThrows { Parser.parse(sourceFile, stream, diagnosticContext) } assertTrue(diagnosticContext.hasErrors(), "Expected errors, but had no errors") diff --git a/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt b/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt index 66300a3e..0440a861 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt @@ -9,7 +9,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { is NumberNode -> expressionNode.value is WildcardNode -> null else -> { - controller.createContext(expressionNode.location.source).error { + controller.getOrCreateContext(expressionNode.location.source).error { message("Range constraint argument must be a valid number range") highlight("neither a number nor '*'", expressionNode.location) help("A valid constraint would be range(1..10.5) or range(1..*)") @@ -22,7 +22,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { val higher = resolveSide(expression.right) if (lower == null && higher == null) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Range constraint must have at least one valid number") highlight("invalid constraint", expression.location) help("A valid constraint would be range(1..10.5) or range(1..*)") @@ -33,7 +33,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { if (lower is Double && higher is Double && lower > higher || lower is Long && higher is Long && lower > higher ) { - controller.createContext(expression.location.source) + controller.getOrCreateContext(expression.location.source) .error { message("Range constraint must have a lower bound lower than the upper bound") highlight("invalid constraint", expression.location) @@ -54,7 +54,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { is WildcardNode -> null else -> { - controller.createContext(expressionNode.location.source).error { + controller.getOrCreateContext(expressionNode.location.source).error { message("Expected size constraint argument to be a whole number or wildcard") highlight("expected whole number or wildcard '*'", expressionNode.location) help("A valid constraint would be size(1..10), size(1..*) or size(*..10)") @@ -67,7 +67,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { val higher = resolveSide(expression.right) if (lower == null && higher == null) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Constraint parameters cannot both be wildcards") highlight("invalid constraint", expression.location) help("A valid constraint would be range(1..10.5) or range(1..*)") @@ -76,7 +76,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { } if (lower != null && higher != null && lower > higher) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Size constraint lower bound must be lower than or equal to the upper bound") highlight("invalid constraint", expression.location) } @@ -101,7 +101,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { is NumberNode -> ResolvedTypeReference.Constraint.Value(expression, expression.value) is BooleanNode -> ResolvedTypeReference.Constraint.Value(expression, expression.value) else -> { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Value constraint must be a string, integer, float or boolean") highlight("invalid constraint", expression.location) help("A valid constraint would be value(\"foo\"), value(42) or value(false)") @@ -124,7 +124,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { when (name) { "range" -> { if (expression.arguments.size != 1 || expression.arguments.firstOrNull() !is RangeExpressionNode) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Range constraint must have exactly one range argument") highlight("invalid constraint", expression.location) help("A valid constraint would be range(1..10.5)") @@ -136,7 +136,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { "size" -> { if (expression.arguments.size != 1 || expression.arguments.firstOrNull() !is RangeExpressionNode) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Size constraint must have exactly one size argument") highlight("invalid constraint", expression.location) help("A valid constraint would be size(1..10)") @@ -148,7 +148,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { "pattern" -> { if (expression.arguments.size != 1 || expression.arguments.firstOrNull() !is StringNode) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Pattern constraint must have exactly one string argument") highlight("invalid constraint", expression.location) help("A valid constraint would be pattern(\"a-z\")") @@ -160,7 +160,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { "value" -> { if (expression.arguments.size != 1) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("value constraint must have exactly one argument") highlight("invalid constraint", expression.location) } @@ -170,7 +170,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { } is String -> { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Constraint with name '${name}' does not exist") highlight("unknown constraint", expression.base.location) help("A valid constraint would be range(1..10.5), size(1..10), pattern(\"a-z\") or value(\"foo\")") @@ -203,7 +203,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { is StringNode -> return createPattern(expression) else -> Unit } - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Invalid constraint") highlight("invalid constraint", expression.location) } @@ -230,7 +230,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) { return if (validateConstraintMatches(constraint, baseType)) { constraint } else { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Constraint '${constraint.humanReadableName}' is not allowed for type '${baseType.humanReadableName}'") highlight("illegal constraint", expression.location) diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt index d1020a83..7d54a52d 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt @@ -12,7 +12,7 @@ import tools.samt.parser.* * - Resolve all references to types * - Resolve all references to their declarations in the AST * */ -class SemanticModelBuilder( +class SemanticModelBuilder private constructor( private val files: List, private val controller: DiagnosticController, ) { @@ -29,7 +29,7 @@ class SemanticModelBuilder( block() } else { val existingType = parentPackage.types.getValue(statement.name.name) - controller.createContext(statement.location.source).error { + controller.getOrCreateContext(statement.location.source).error { message("'${statement.name.name}' is already declared") highlight("duplicate declaration", statement.name.location) if (existingType is UserDefinedType) { @@ -49,7 +49,7 @@ class SemanticModelBuilder( val name = identifierGetter(item).name val existingLocation = existingItems.putIfAbsent(name, item.location) if (existingLocation != null) { - controller.createContext(item.location.source).error { + controller.getOrCreateContext(item.location.source).error { message("$what '$name' is defined more than once") highlight("duplicate declaration", identifierGetter(item).location) highlight("previous declaration", existingLocation) @@ -88,7 +88,7 @@ class SemanticModelBuilder( ensureNameIsAvailable(parentPackage, statement) { reportDuplicates(statement.fields, "Record field") { it.name } if (statement.extends.isNotEmpty()) { - controller.createContext(statement.location.source).error { + controller.getOrCreateContext(statement.location.source).error { message("Record extends are not yet supported") highlight("cannot extend other records", statement.extends.first().location) } @@ -129,7 +129,7 @@ class SemanticModelBuilder( is RequestResponseOperationNode -> { if (operation.isAsync) { - controller.createContext(operation.location.source).error { + controller.getOrCreateContext(operation.location.source).error { message("Async operations are not yet supported") highlight("unsupported async operation", operation.location) } @@ -179,7 +179,7 @@ class SemanticModelBuilder( } is TypeAliasNode -> { - controller.createContext(statement.location.source).error { + controller.getOrCreateContext(statement.location.source).error { message("Type aliases are not yet supported") highlight("unsupported feature", statement.location) } @@ -246,7 +246,7 @@ class SemanticModelBuilder( } null -> { - controller.createContext(component.location.source).error { + controller.getOrCreateContext(component.location.source).error { message("Could not resolve reference '${component.name}'") highlight("unresolved reference", component.location) } @@ -257,7 +257,7 @@ class SemanticModelBuilder( if (iterator.hasNext()) { // We resolved a non-package type but there are still components left - controller.createContext(component.location.source).error { + controller.getOrCreateContext(component.location.source).error { message("Type '${component.name}' is not a package, cannot access sub-types") highlight("must be a package", component.location) } @@ -285,7 +285,7 @@ class SemanticModelBuilder( file.imports.forEach { import -> fun addImportedType(name: String, type: Type) { putIfAbsent(name, type)?.let { existingType -> - controller.createContext(file.sourceFile).error { + controller.getOrCreateContext(file.sourceFile).error { message("Import '$name' conflicts with locally defined type with same name") highlight("conflicting import", import.location) if (existingType is UserDefinedType) { @@ -318,7 +318,7 @@ class SemanticModelBuilder( addImportedType(name, type) } } else { - controller.createContext(file.sourceFile).error { + controller.getOrCreateContext(file.sourceFile).error { message("Import '${import.name.name}.*' must point to a package and not a type") highlight( "illegal wildcard import", import.location, suggestChange = "import ${ @@ -338,7 +338,7 @@ class SemanticModelBuilder( // Add built-in types fun addBuiltIn(name: String, type: Type) { putIfAbsent(name, type)?.let { existingType -> - controller.createContext(file.sourceFile).error { + controller.getOrCreateContext(file.sourceFile).error { message("Type '$name' shadows built-in type with same name") if (existingType is UserDefinedType) { val definition = existingType.definition @@ -379,7 +379,7 @@ class SemanticModelBuilder( return ResolvedTypeReference(expression, it) } - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Type '${expression.name}' could not be resolved") highlight("unresolved type", expression.location) } @@ -403,14 +403,14 @@ class SemanticModelBuilder( } null -> { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Type '${expression.name}' could not be resolved") highlight("unresolved type", expression.location) } } else -> { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Type '${expression.components.first().name}' is not a package, cannot access sub-types") highlight("not a package", expression.components.first().location) } @@ -422,7 +422,7 @@ class SemanticModelBuilder( val baseType = resolveExpression(expression.base) val constraints = expression.arguments.mapNotNull { constraintBuilder.build(baseType.type, it) } if (baseType.constraints.isNotEmpty()) { - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Cannot have nested constraints") highlight("illegal nested constraint", expression.location) } @@ -460,7 +460,7 @@ class SemanticModelBuilder( } } } - controller.createContext(expression.location.source).error { + controller.getOrCreateContext(expression.location.source).error { message("Unsupported generic type") highlight(expression.location) help("Valid generic types are List and Map") @@ -470,7 +470,7 @@ class SemanticModelBuilder( is OptionalDeclarationNode -> { val baseType = resolveExpression(expression.base) if (baseType.isOptional) { - controller.createContext(expression.location.source).warn { + controller.getOrCreateContext(expression.location.source).warn { message("Type is already optional, ignoring '?'") highlight("already optional", expression.base.location) } @@ -481,7 +481,7 @@ class SemanticModelBuilder( is BooleanNode, is NumberNode, is StringNode, - -> controller.createContext(expression.location.source).error { + -> controller.getOrCreateContext(expression.location.source).error { message("Cannot use literal value as type") highlight("not a type expression", expression.location) } @@ -490,7 +490,7 @@ class SemanticModelBuilder( is ArrayNode, is RangeExpressionNode, is WildcardNode, - -> controller.createContext(expression.location.source).error { + -> controller.getOrCreateContext(expression.location.source).error { message("Invalid type expression") highlight("not a type expression", expression.location) } diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt index 8a8178c2..5b76a50e 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt @@ -27,7 +27,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont check(typeReference is ResolvedTypeReference) when (val type = typeReference.type) { is ServiceType -> { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { // error message applies to both record fields and return types message("Cannot use service '${type.name}' as type") highlight("service type not allowed here", typeReference.definition.location) @@ -35,14 +35,14 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont } is ProviderType -> { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Cannot use provider '${type.name}' as type") highlight("provider type not allowed here", typeReference.definition.location) } } is PackageType -> { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Cannot use package '${type.packageName}' as type") highlight("package type not allowed here", typeReference.definition.location) } @@ -64,7 +64,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont private inline fun checkServiceType(typeReference: TypeReference, block: (serviceType: ServiceType) -> Unit) { check(typeReference is ResolvedTypeReference) if (typeReference.constraints.isNotEmpty()) { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Cannot have constraints on service") for (constraint in typeReference.constraints) { highlight("illegal constraint", constraint.definition.location) @@ -72,7 +72,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont } } if (typeReference.isOptional) { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Cannot have optional service") highlight("illegal optional", typeReference.definition.location) } @@ -84,7 +84,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont is UnknownType -> Unit else -> { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Expected a service but got '${type.humanReadableName}'") highlight("illegal type", typeReference.definition.location) } @@ -95,7 +95,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont private inline fun checkProviderType(typeReference: TypeReference, block: (providerType: ProviderType) -> Unit) { check(typeReference is ResolvedTypeReference) if (typeReference.constraints.isNotEmpty()) { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Cannot have constraints on provider") for (constraint in typeReference.constraints) { highlight("illegal constraint", constraint.definition.location) @@ -103,7 +103,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont } } if (typeReference.isOptional) { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Cannot have optional provider") highlight("illegal optional", typeReference.definition.location) } @@ -115,7 +115,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont is UnknownType -> Unit else -> { - controller.createContext(typeReference.definition.location.source).error { + controller.getOrCreateContext(typeReference.definition.location.source).error { message("Expected a provider but got '${type.humanReadableName}'") highlight("illegal type", typeReference.definition.location) } @@ -144,7 +144,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont provider.implements.forEach { implements -> checkServiceType(implements.service) { type -> implementsTypes.putIfAbsent(type, implements.definition.location)?.let { existingLocation -> - controller.createContext(implements.definition.location.source).error { + controller.getOrCreateContext(implements.definition.location.source).error { message("Service '${type.name}' already implemented") highlight("duplicate implements", implements.definition.location) highlight("previous implements", existingLocation) @@ -160,7 +160,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont if (matchingOperation != null) { matchingOperation } else { - controller.createContext(provider.definition.location.source).error { + controller.getOrCreateContext(provider.definition.location.source).error { message("Operation '${serviceOperationName.name}' not found in service '${type.name}'") highlight("unknown operation", serviceOperationName.location) } @@ -178,7 +178,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont consumer.uses.forEach { uses -> checkServiceType(uses.service) { type -> usesTypes.putIfAbsent(type, uses.definition.location)?.let { existingLocation -> - controller.createContext(uses.definition.location.source).error { + controller.getOrCreateContext(uses.definition.location.source).error { message("Service '${type.name}' already used") highlight("duplicate uses", uses.definition.location) highlight("previous uses", existingLocation) @@ -189,7 +189,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont val matchingImplements = providerType.implements.find { (it.service as ResolvedTypeReference).type == type } if (matchingImplements == null) { - controller.createContext(uses.definition.location.source).error { + controller.getOrCreateContext(uses.definition.location.source).error { message("Service '${type.name}' is not implemented by provider '${providerType.name}'") highlight("unavailable service", uses.definition.serviceName.location) } @@ -205,12 +205,12 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont matchingOperation } else { if (type.operations.any { it.name == serviceOperationName.name }) { - controller.createContext(uses.definition.location.source).error { + controller.getOrCreateContext(uses.definition.location.source).error { message("Operation '${serviceOperationName.name}' in service '${type.name}' is not implemented by provider '${providerType.name}'") highlight("unavailable operation", serviceOperationName.location) } } else { - controller.createContext(uses.definition.location.source).error { + controller.getOrCreateContext(uses.definition.location.source).error { message("Operation '${serviceOperationName.name}' not found in service '${type.name}'") highlight("unknown operation", serviceOperationName.location) } diff --git a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt index 879375b4..d7341131 100644 --- a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt +++ b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt @@ -696,7 +696,7 @@ class SemanticModelTest { val fileTrees = sourceAndExpectedMessages.mapIndexed { index, (source) -> val filePath = "/tmp/SemanticModelTest-${index}.samt" val sourceFile = SourceFile(filePath, source) - val parseContext = diagnosticController.createContext(sourceFile) + val parseContext = diagnosticController.getOrCreateContext(sourceFile) val stream = Lexer.scan(source.reader(), parseContext) val fileTree = Parser.parse(sourceFile, stream, parseContext) assertFalse(parseContext.hasErrors(), "Expected no parse errors, but had errors: ${parseContext.messages}}")