diff --git a/build.gradle.kts b/build.gradle.kts index 759ab839..aa1872d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,6 +40,7 @@ allprojects { maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/bootstrap") maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/swift-export-experimental") } afterEvaluate { dependencies { @@ -74,6 +75,7 @@ dependencies { implementation("org.jetbrains.kotlin:core:231-$kotlinIdeVersion-$kotlinIdeVersionSuffix") implementation(project(":executors", configuration = "default")) implementation(project(":common", configuration = "default")) + implementation(project(":swift-export-playground", configuration = "default")) testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") diff --git a/gradle.properties b/gradle.properties index a439bca6..d89baab0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ systemProp.kotlinVersion=2.0.0-RC2 systemProp.kotlinIdeVersion=1.9.20-506 systemProp.kotlinIdeVersionSuffix=IJ8109.175 +systemProp.swiftExportVersion=2.0.20-dev-3080 systemProp.policy=executor.policy systemProp.indexes=indexes.json systemProp.indexesJs=indexesJs.json diff --git a/settings.gradle.kts b/settings.gradle.kts index aa2d316b..3ab5e135 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ include(":executors") include(":indexation") include(":common") include(":dependencies") +include(":swift-export-playground") pluginManagement { repositories { diff --git a/src/main/kotlin/com/compiler/server/compiler/components/SwiftExportTranslator.kt b/src/main/kotlin/com/compiler/server/compiler/components/SwiftExportTranslator.kt new file mode 100644 index 00000000..6c7e8909 --- /dev/null +++ b/src/main/kotlin/com/compiler/server/compiler/components/SwiftExportTranslator.kt @@ -0,0 +1,32 @@ +package com.compiler.server.compiler.components + +import com.compiler.server.model.CompilerDiagnostics +import com.compiler.server.model.SwiftExportResult +import com.compiler.server.model.toExceptionDescriptor +import component.KotlinEnvironment +import org.jetbrains.kotlin.psi.KtFile +import org.springframework.stereotype.Component +import runSwiftExport +import java.nio.file.Path + +@Component +class SwiftExportTranslator( + private val kotlinEnvironment: KotlinEnvironment, +) { + fun translate(files: List): SwiftExportResult = try { + usingTempDirectory { tempDirectory -> + val ioFiles = files.writeToIoFiles(tempDirectory) + val stdlib = kotlinEnvironment.WASM_LIBRARIES.singleOrNull { "stdlib" in it } + val swiftCode = runSwiftExport( + sourceFile = ioFiles.first(), + stdlibPath = stdlib?.let { Path.of(it) }, + ) + SwiftExportResult( + compilerDiagnostics = CompilerDiagnostics(emptyMap()), + swiftCode = swiftCode + ) + } + } catch (e: Exception) { + SwiftExportResult(swiftCode = "", exception = e.toExceptionDescriptor()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt b/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt index 2b646d8f..67c1e87c 100644 --- a/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt +++ b/src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt @@ -28,6 +28,10 @@ class CompilerRestController(private val kotlinProjectExecutor: KotlinProjectExe KotlinTranslatableCompiler.JS -> kotlinProjectExecutor.convertToJsIr(project) KotlinTranslatableCompiler.WASM -> kotlinProjectExecutor.convertToWasm(project, debugInfo) KotlinTranslatableCompiler.COMPOSE_WASM -> kotlinProjectExecutor.convertToWasm(project, debugInfo) + KotlinTranslatableCompiler.SWIFT -> kotlinProjectExecutor.convertToSwift(project).let { + // TODO: A hack to avoid changing the return type of the function. + object : TranslationResultWithJsCode(it.swiftCode, it.compilerDiagnostics, it.exception) {} + } } } diff --git a/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt b/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt index 737db2ce..ee952bbb 100644 --- a/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt +++ b/src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt @@ -50,6 +50,7 @@ class KotlinPlaygroundRestController(private val kotlinProjectExecutor: KotlinPr debugInfo = false, ) ProjectType.JUNIT -> kotlinProjectExecutor.test(project) + ProjectType.SWIFT_EXPORT -> kotlinProjectExecutor.convertToSwift(project) } } diff --git a/src/main/kotlin/com/compiler/server/model/ExecutionResult.kt b/src/main/kotlin/com/compiler/server/model/ExecutionResult.kt index 9d769120..f2a9af18 100644 --- a/src/main/kotlin/com/compiler/server/model/ExecutionResult.kt +++ b/src/main/kotlin/com/compiler/server/model/ExecutionResult.kt @@ -82,6 +82,14 @@ class JunitExecutionResult( override var compilerDiagnostics: CompilerDiagnostics = CompilerDiagnostics() ) : ExecutionResult(compilerDiagnostics, exception) +class SwiftExportResult( + val swiftCode: String, + override var exception: ExceptionDescriptor? = null, + @field:JsonProperty("errors") + override var compilerDiagnostics: CompilerDiagnostics = CompilerDiagnostics() +) : ExecutionResult(compilerDiagnostics, exception) + + private fun unEscapeOutput(value: String) = value.replace("&lt;".toRegex(), "<") .replace("&gt;".toRegex(), ">") .replace("\r", "") diff --git a/src/main/kotlin/com/compiler/server/model/KotlinTranslatableCompiler.kt b/src/main/kotlin/com/compiler/server/model/KotlinTranslatableCompiler.kt index 0f743056..4bc2dc8c 100644 --- a/src/main/kotlin/com/compiler/server/model/KotlinTranslatableCompiler.kt +++ b/src/main/kotlin/com/compiler/server/model/KotlinTranslatableCompiler.kt @@ -3,5 +3,6 @@ package com.compiler.server.model enum class KotlinTranslatableCompiler { JS, WASM, - COMPOSE_WASM + COMPOSE_WASM, + SWIFT, } \ No newline at end of file diff --git a/src/main/kotlin/com/compiler/server/model/Project.kt b/src/main/kotlin/com/compiler/server/model/Project.kt index b8afd6fe..dcb57cc4 100644 --- a/src/main/kotlin/com/compiler/server/model/Project.kt +++ b/src/main/kotlin/com/compiler/server/model/Project.kt @@ -20,7 +20,9 @@ enum class ProjectType(@JsonValue val id: String) { CANVAS("canvas"), JS_IR("js-ir"), WASM("wasm"), - COMPOSE_WASM("compose-wasm"); + COMPOSE_WASM("compose-wasm"), + SWIFT_EXPORT("swift-export") + ; fun isJvmRelated(): Boolean = this == JAVA || this == JUNIT diff --git a/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt b/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt index 327cf426..3e93d2e1 100644 --- a/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt +++ b/src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt @@ -17,6 +17,7 @@ class KotlinProjectExecutor( private val completionProvider: CompletionProvider, private val version: VersionInfo, private val kotlinToJSTranslator: KotlinToJSTranslator, + private val swiftExportTranslator: SwiftExportTranslator, private val kotlinEnvironment: KotlinEnvironment, private val loggerDetailsStreamer: LoggerDetailsStreamer? = null, ) { @@ -52,6 +53,10 @@ class KotlinProjectExecutor( return convertWasmWithConverter(project, debugInfo, kotlinToJSTranslator::doTranslateWithWasm) } + fun convertToSwift(project: Project): SwiftExportResult { + return convertSwiftWithConverter(project) + } + fun complete(project: Project, line: Int, character: Int): List { return kotlinEnvironment.environment { val file = getFilesFrom(project, it).first() @@ -76,6 +81,7 @@ class KotlinProjectExecutor( project, debugInfo = false, ).compilerDiagnostics + ProjectType.SWIFT_EXPORT -> convertToSwift(project).compilerDiagnostics } } catch (e: Exception) { log.warn("Exception in getting highlight. Project: $project", e) @@ -114,6 +120,15 @@ class KotlinProjectExecutor( }.also { logExecutionResult(project, it) } } + private fun convertSwiftWithConverter( + project: Project, + ): SwiftExportResult { + return kotlinEnvironment.environment { environment -> + val files = getFilesFrom(project, environment).map { it.kotlinFile } + swiftExportTranslator.translate(files) + }.also { logExecutionResult(project, it) } + } + private fun logExecutionResult(project: Project, executionResult: ExecutionResult) { loggerDetailsStreamer?.logExecutionResult( executionResult, diff --git a/src/test/kotlin/com/compiler/server/SwiftConverterTest.kt b/src/test/kotlin/com/compiler/server/SwiftConverterTest.kt new file mode 100644 index 00000000..d5019d96 --- /dev/null +++ b/src/test/kotlin/com/compiler/server/SwiftConverterTest.kt @@ -0,0 +1,99 @@ +package com.compiler.server + +import com.compiler.server.base.BaseExecutorTest +import org.junit.jupiter.api.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SwiftConverterTest : BaseExecutorTest() { + + private fun exactTest(input: String, expected: String) { + val actual = translateToSwift(input) + assertEquals(expected, actual.swiftCode.trimEnd()) + } + + private fun containsTest(input: String, expected: String) { + val actual = translateToSwift(input) + assertContains(actual.swiftCode.trimEnd(), expected) + } + + private fun shouldFailTest(input: String) { + val actual = translateToSwift(input) + assertTrue(actual.hasErrors()) + } + + @Test + fun basicSwiftExportTest() = containsTest( + input = """ + fun main() {} + """.trimIndent(), + expected = "public func main() -> Swift.Void" + ) + + @Test + fun `use stdlib declaration`() = containsTest( + input = "fun foo(): UInt = 42", + expected = """ + public func foo() -> Swift.UInt32 { + fatalError() + } + """.trimIndent() + ) + + @Test + fun `class declaration`() = exactTest( + input = "public class MyClass { public fun A() {}}", + expected = """ + import KotlinRuntime + + public class MyClass : KotlinRuntime.KotlinBase { + public override init() { + fatalError() + } + public func A() -> Swift.Void { + fatalError() + } + } + """.trimIndent() + ) + + @Test + fun `simple packages`() = exactTest( + input = """ + package foo.bar + + val myProperty: Int = 42 + """.trimIndent(), + expected = """ + public extension Playground.foo.bar { + public static var myProperty: Swift.Int32 { + get { + fatalError() + } + } + } + public enum foo { + public enum bar { + } + } + """.trimIndent() + ) + + @Test + fun `invalid code`() = exactTest( + input = "abracadabra", + expected = """ + """.trimIndent() + ) + + @Test + fun `more invalid code`() = exactTest( + input = "fun foo(): Bar = error()", + expected = """ + public func foo() -> ERROR_TYPE { + fatalError() + } + """.trimIndent() + ) +} \ No newline at end of file diff --git a/src/test/kotlin/com/compiler/server/base/BaseExecutorTest.kt b/src/test/kotlin/com/compiler/server/base/BaseExecutorTest.kt index 6ce60c7a..6ec2adeb 100644 --- a/src/test/kotlin/com/compiler/server/base/BaseExecutorTest.kt +++ b/src/test/kotlin/com/compiler/server/base/BaseExecutorTest.kt @@ -60,6 +60,8 @@ class BaseExecutorTest { fun translateToJsIr(code: String) = testRunner.translateToJsIr(code) + fun translateToSwift(code: String) = testRunner.translateToSwift(code) + fun runWithException(code: String, contains: String, message: String? = null) = testRunner.runWithException(code, contains, message) fun version() = testRunner.getVersion() diff --git a/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt b/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt index ec35e598..dffb0882 100644 --- a/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt +++ b/src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt @@ -63,6 +63,11 @@ class TestProjectRunner { ) } + fun translateToSwift(code: String): SwiftExportResult { + val project = generateSingleProject(text = code, projectType = ProjectType.SWIFT_EXPORT) + return kotlinProjectExecutor.convertToSwift(project) + } + fun runWithException(code: String, contains: String, message: String? = null): ExecutionResult { val project = generateSingleProject(text = code) val result = kotlinProjectExecutor.run(project) diff --git a/swift-export-playground/README.md b/swift-export-playground/README.md new file mode 100644 index 00000000..c26b5951 --- /dev/null +++ b/swift-export-playground/README.md @@ -0,0 +1 @@ +An implementation of Swift export for Kotlin Playground. \ No newline at end of file diff --git a/swift-export-playground/build.gradle.kts b/swift-export-playground/build.gradle.kts new file mode 100644 index 00000000..134130e2 --- /dev/null +++ b/swift-export-playground/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + kotlin("jvm") +} + +repositories { + mavenCentral() + // For Analysis API components + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies") + maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/swift-export-experimental") +} + +val kotlinVersion = rootProject.properties["systemProp.kotlinVersion"] +val swiftExportVersion = rootProject.properties["systemProp.swiftExportVersion"] + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-compiler:$kotlinVersion") + // For K/N Distribution class + implementation("org.jetbrains.kotlin:kotlin-native-utils:$kotlinVersion") + + // Analysis API components which are required for the Swift export + implementation("org.jetbrains.kotlin:analysis-api-standalone-for-ide:$kotlinVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:high-level-api-for-ide:$kotlinVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:high-level-api-fir-for-ide:$kotlinVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:high-level-api-impl-base-for-ide:$kotlinVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:low-level-api-fir-for-ide:$kotlinVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:symbol-light-classes-for-ide:$kotlinVersion") { isTransitive = false } + + // Swift export not-yet-published dependencies. + implementation("org.jetbrains.kotlin:sir:$swiftExportVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:sir-providers:$swiftExportVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:sir-light-classes:$swiftExportVersion") { isTransitive = false } + implementation("org.jetbrains.kotlin:sir-printer:$swiftExportVersion") { isTransitive = false } + + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion") +} \ No newline at end of file diff --git a/swift-export-playground/src/main/kotlin/PlaygroundSirSession.kt b/swift-export-playground/src/main/kotlin/PlaygroundSirSession.kt new file mode 100644 index 00000000..cc603c34 --- /dev/null +++ b/swift-export-playground/src/main/kotlin/PlaygroundSirSession.kt @@ -0,0 +1,31 @@ +import org.jetbrains.kotlin.analysis.project.structure.KtModule +import org.jetbrains.kotlin.sir.providers.SirSession +import org.jetbrains.kotlin.sir.providers.SirTypeProvider +import org.jetbrains.kotlin.sir.providers.impl.* +import org.jetbrains.sir.lightclasses.SirDeclarationFromKtSymbolProvider + +internal class PlaygroundSirSession( + ktModule: KtModule, +) : SirSession { + override val declarationNamer = SirDeclarationNamerImpl() + override val enumGenerator = SirEnumGeneratorImpl() + override val moduleProvider = SirSingleModuleProvider("Playground") + override val declarationProvider = CachingSirDeclarationProvider( + declarationsProvider = SirDeclarationFromKtSymbolProvider( + ktModule = ktModule, + sirSession = sirSession, + ) + ) + override val parentProvider = SirParentProviderImpl( + sirSession = sirSession, + ) + override val typeProvider = SirTypeProviderImpl( + errorTypeStrategy = SirTypeProvider.ErrorTypeStrategy.ErrorType, + unsupportedTypeStrategy = SirTypeProvider.ErrorTypeStrategy.ErrorType, + sirSession = sirSession, + ) + override val visibilityChecker = SirVisibilityCheckerImpl() + override val childrenProvider = SirDeclarationChildrenProviderImpl( + sirSession = sirSession, + ) +} \ No newline at end of file diff --git a/swift-export-playground/src/main/kotlin/Runner.kt b/swift-export-playground/src/main/kotlin/Runner.kt new file mode 100644 index 00000000..c74b0f0a --- /dev/null +++ b/swift-export-playground/src/main/kotlin/Runner.kt @@ -0,0 +1,81 @@ +import org.jetbrains.kotlin.analysis.api.KtAnalysisApiInternals +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.lifetime.KtLifetimeTokenProvider +import org.jetbrains.kotlin.analysis.api.standalone.KtAlwaysAccessibleLifetimeTokenProvider +import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession +import org.jetbrains.kotlin.analysis.project.structure.KtModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtLibraryModule +import org.jetbrains.kotlin.analysis.project.structure.builder.buildKtSourceModule +import org.jetbrains.kotlin.platform.konan.NativePlatforms +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.sir.SirModule +import org.jetbrains.kotlin.sir.SirMutableDeclarationContainer +import org.jetbrains.kotlin.sir.util.addChild +import org.jetbrains.sir.printer.SirAsSwiftSourcesPrinter +import java.nio.file.Path + +/** + * Translate public API of the given [sourceFile] to Swift. + * [stdlibPath] is a path to stdlib.klib which is required to properly resolve references from [sourceFile]. + */ +fun runSwiftExport( + sourceFile: Path, + stdlibPath: Path? +): String { + val (ktModule, sources) = collectModuleAndSources(sourceFile, "Playground", stdlibPath) + + return analyze(ktModule) { + val sirSession = PlaygroundSirSession(ktModule) + val sirModule: SirModule = with(sirSession) { + ktModule.sirModule().also { + sources.flatMap { file -> + file.getFileSymbol().getFileScope().extractDeclarations(analysisSession) + }.forEach { topLevelDeclaration -> + val parent = topLevelDeclaration.parent as? SirMutableDeclarationContainer + ?: error("top level declaration can contain only module or extension to package as a parent") + parent.addChild { topLevelDeclaration } + } + } + } + SirAsSwiftSourcesPrinter.print(sirModule, stableDeclarationsOrder = true, renderDocComments = true) + } +} + +@OptIn(KtAnalysisApiInternals::class) +private fun collectModuleAndSources( + sourceRoot: Path, + kotlinModuleName: String, + stdlibPath: Path?, +): Pair> { + val analysisAPISession = buildStandaloneAnalysisAPISession { + registerProjectService(KtLifetimeTokenProvider::class.java, KtAlwaysAccessibleLifetimeTokenProvider()) + + buildKtModuleProvider { + platform = NativePlatforms.unspecifiedNativePlatform + + val stdlib = stdlibPath?.let { + addModule( + buildKtLibraryModule { + addBinaryRoot(it) + platform = NativePlatforms.unspecifiedNativePlatform + libraryName = "stdlib" + } + ) + } + + addModule( + buildKtSourceModule { + addSourceRoot(sourceRoot) + platform = NativePlatforms.unspecifiedNativePlatform + moduleName = kotlinModuleName + if (stdlib != null) { + addRegularDependency(stdlib) + } + } + ) + } + } + + val (sourceModule, rawFiles) = analysisAPISession.modulesWithFiles.entries.single() + return sourceModule to rawFiles.filterIsInstance() +} \ No newline at end of file diff --git a/swift-export-playground/src/test/kotlin/Tests.kt b/swift-export-playground/src/test/kotlin/Tests.kt new file mode 100644 index 00000000..b7100ffb --- /dev/null +++ b/swift-export-playground/src/test/kotlin/Tests.kt @@ -0,0 +1,81 @@ +import kotlin.io.path.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class SwiftExportTests { + + private fun testSources(input: String, expect: String) { + val tempDir = createTempDirectory() + + val inputFormatted = input.trimIndent().trimEnd() + + val inputFile = (tempDir / "input.kt").also { it.writeText(inputFormatted) } + + val actual = runSwiftExport( + sourceFile = inputFile, + stdlibPath = null, + ) + val expectFormatted = expect.trimIndent().trimEnd() + + assertEquals(expectFormatted, actual) + } + + @Test + fun smoke() = testSources( + """ + fun foo(): Int = 5 + """, + """ + public func foo() -> Swift.Int32 { + fatalError() + } + """ + ) + + @Test + fun `class declaration`() = testSources( + """ + class A + """.trimIndent(), + """ + import KotlinRuntime + + public class A : KotlinRuntime.KotlinBase { + public override init() { + fatalError() + } + } + """.trimIndent() + ) + + @Test + fun `object declaration`() = testSources( + """ + object O + """.trimIndent(), + """ + import KotlinRuntime + + public class O : KotlinRuntime.KotlinBase { + public static var shared: Playground.O { + get { + fatalError() + } + } + private override init() { + fatalError() + } + } + """.trimIndent() + ) + + @Test + fun `typealias to basic type declaration`() = testSources( + """ + typealias MyInt = Int + """.trimIndent(), + """ + public typealias MyInt = Swift.Int32 + """.trimIndent() + ) +} \ No newline at end of file