Skip to content

Commit

Permalink
Add support for Swift export
Browse files Browse the repository at this point in the history
Minor hack: use wasm stdlib to avoid downloading the whole K/N distribution
sbogolepov committed Dec 2, 2024
1 parent 3b1f866 commit 39e8501
Showing 19 changed files with 474 additions and 3 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -78,6 +78,7 @@ dependencies {
implementation(libs.kotlin.core)
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")
14 changes: 14 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ jackson = "2.14.0"
hamcrest = "2.2"
compose = "1.7.0"
gradle-develocity = "3.17.5"
caffeine = "2.9.3"

[libraries]
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
@@ -38,6 +39,17 @@ kotlin-base-fe10-analysis = { group = "org.jetbrains.kotlin", name = "base-fe10-
kotlin-compiler-ide = { group = "org.jetbrains.kotlin", name = "kotlin-compiler-for-ide", version.ref = "kotlinIdeVersion" }
kotlin-idea = { group = "org.jetbrains.kotlin", name = "idea", version.ref = "kotlinIdeVersionWithSuffix" }
kotlin-core = { group = "org.jetbrains.kotlin", name = "core", version.ref = "kotlinIdeVersionWithSuffix" }
analysis-api-standalone-for-ide = { group = "org.jetbrains.kotlin", name = "analysis-api-standalone-for-ide", version.ref = "kotlin" }
high-level-api-for-ide = { group = "org.jetbrains.kotlin", name = "high-level-api-for-ide", version.ref = "kotlin" }
high-level-api-fir-for-ide = { group = "org.jetbrains.kotlin", name = "high-level-api-fir-for-ide", version.ref = "kotlin" }
high-level-api-impl-base-for-ide = { group = "org.jetbrains.kotlin", name = "high-level-api-impl-base-for-ide", version.ref = "kotlin" }
low-level-api-fir-for-ide = { group = "org.jetbrains.kotlin", name = "low-level-api-fir-for-ide", version.ref = "kotlin" }
symbol-light-classes-for-ide = { group = "org.jetbrains.kotlin", name = "symbol-light-classes-for-ide", version.ref = "kotlin" }
analysis-api-platform-interface-for-ide = { group = "org.jetbrains.kotlin", name = "analysis-api-platform-interface-for-ide", version.ref = "kotlin" }
sir = { group = "org.jetbrains.kotlin", name = "sir", version.ref = "kotlin" }
sir-providers = { group = "org.jetbrains.kotlin", name = "sir-providers", version.ref = "kotlin" }
sir-light-classes = { group = "org.jetbrains.kotlin", name = "sir-light-classes", version.ref = "kotlin" }
sir-printer = { group = "org.jetbrains.kotlin", name = "sir-printer", version.ref = "kotlin" }
kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" }
@@ -64,6 +76,8 @@ compose-material = { group = "org.jetbrains.compose.material", name = "material"
compose-components-resources = { group = "org.jetbrains.compose.components", name = "components-resources", version.ref = "compose" }
kotlin-serialization-plugin = {group= "org.jetbrains.kotlin", name="kotlin-serialization-compiler-plugin", version.ref = "kotlin"}
gradle-develocity = {group = "com.gradle", name= "develocity-gradle-plugin", version.ref = "gradle-develocity"}
caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeine" }


[bundles]
kotlin-stdlib = ["kotlin-stdlib", "kotlin-stdlib-jdk7", "kotlin-stdlib-jdk8"]
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -16,4 +16,5 @@ plugins {
include(":executors")
include(":indexation")
include(":common")
include(":dependencies")
include(":dependencies")
include(":swift-export-playground")
Original file line number Diff line number Diff line change
@@ -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<KtFile>): 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())
}
}
Original file line number Diff line number Diff line change
@@ -34,6 +34,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_EXPORT -> kotlinProjectExecutor.convertToSwift(project).let {
// TODO: A hack to avoid changing the return type of the function.
object : TranslationResultWithJsCode(it.swiftCode, it.compilerDiagnostics, it.exception) {}
}
}
}

Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ class KotlinPlaygroundRestController(private val kotlinProjectExecutor: KotlinPr
debugInfo = false,
)
ProjectType.JUNIT -> kotlinProjectExecutor.test(project, addByteCode)
ProjectType.SWIFT_EXPORT -> kotlinProjectExecutor.convertToSwift(project)
}
}

8 changes: 8 additions & 0 deletions src/main/kotlin/com/compiler/server/model/ExecutionResult.kt
Original file line number Diff line number Diff line change
@@ -89,6 +89,14 @@ class JunitExecutionResult(
jvmBytecode: String? = null,
) : JvmExecutionResult(compilerDiagnostics, exception, jvmBytecode)

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("&amp;lt;".toRegex(), "<")
.replace("&amp;gt;".toRegex(), ">")
.replace("\r", "")
Original file line number Diff line number Diff line change
@@ -3,5 +3,6 @@ package com.compiler.server.model
enum class KotlinTranslatableCompiler {
JS,
WASM,
COMPOSE_WASM
COMPOSE_WASM,
SWIFT_EXPORT,
}
4 changes: 3 additions & 1 deletion src/main/kotlin/com/compiler/server/model/Project.kt
Original file line number Diff line number Diff line change
@@ -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

Original file line number Diff line number Diff line change
@@ -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<Completion> {
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,
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ class ResourceE2ECompileTest : BaseResourceCompileTest {
ProjectType.JAVA -> JvmExecutionResult::class.java
ProjectType.JS, ProjectType.CANVAS, ProjectType.JS_IR -> TranslationJSResult::class.java
ProjectType.WASM, ProjectType.COMPOSE_WASM -> TranslationWasmResult::class.java
ProjectType.SWIFT_EXPORT -> SwiftExportResult::class.java
}
val result = RestTemplate().postForObject(
"${getHost()}$url", HttpEntity(body, headers), resultClass
110 changes: 110 additions & 0 deletions src/test/kotlin/com/compiler/server/SwiftConverterTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.compiler.server

import com.compiler.server.base.BaseExecutorTest
import org.junit.jupiter.api.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals

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)
}

@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 {
stub()
}
""".trimIndent()
)

@Test
fun `class declaration`() = exactTest(
input = "public class MyClass { public fun A() {}}",
expected = """
import KotlinRuntime
public final class MyClass : KotlinRuntime.KotlinBase {
public override init() {
stub()
}
public override init(
__externalRCRef: Swift.UInt
) {
stub()
}
public func A() -> Swift.Void {
stub()
}
}
""".trimIndent()
)

@Test
fun `simple packages`() = exactTest(
input = """
package foo.bar
val myProperty: Int = 42
""".trimIndent(),
expected = """
@_exported import pkg
public extension pkg.foo.bar {
public static var myProperty: Swift.Int32 {
get {
stub()
}
}
}
""".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 {
stub()
}
""".trimIndent()
)

@Test
fun `unsupported type declaration`() = exactTest(
input = """
interface Foo
fun produceFoo(): Foo = TODO()
""".trimIndent(),
expected = """
public func produceFoo() -> Swift.Never {
stub()
}
""".trimIndent()
)
}
2 changes: 2 additions & 0 deletions src/test/kotlin/com/compiler/server/base/BaseExecutorTest.kt
Original file line number Diff line number Diff line change
@@ -65,6 +65,8 @@ class BaseExecutorTest {

fun translateToJsIr(@Language("kotlin") code: String) = testRunner.translateToJsIr(code)

fun translateToSwift(code: String) = testRunner.translateToSwift(code)

fun runWithException(@Language("kotlin") code: String, contains: String, message: String? = null, addByteCode: Boolean = false) =
testRunner.runWithException(code, contains, message, addByteCode)

Original file line number Diff line number Diff line change
@@ -75,6 +75,11 @@ class TestProjectRunner {
)
}

fun translateToSwift(code: String): SwiftExportResult {
val project = generateSingleProject(text = code, projectType = ProjectType.SWIFT_EXPORT)
return kotlinProjectExecutor.convertToSwift(project)
}

fun runWithException(
@Language("kotlin")
code: String,
1 change: 1 addition & 0 deletions swift-export-playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An implementation of Swift export for Kotlin Playground.
32 changes: 32 additions & 0 deletions swift-export-playground/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
plugins {
kotlin("jvm")
}

repositories {
mavenCentral()
// For Analysis API components
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies")
}

dependencies {
implementation(libs.kotlin.compiler)

// Analysis API components which are required for the Swift export
implementation(libs.analysis.api.standalone.`for`.ide) { isTransitive = false }
implementation(libs.high.level.api.`for`.ide) { isTransitive = false }
implementation(libs.high.level.api.fir.`for`.ide) { isTransitive = false }
implementation(libs.high.level.api.impl.base.`for`.ide) { isTransitive = false }
implementation(libs.low.level.api.fir.`for`.ide) { isTransitive = false }
implementation(libs.symbol.light.classes.`for`.ide) { isTransitive = false }
implementation(libs.analysis.api.platform.`interface`.`for`.ide) { isTransitive = false }
implementation(libs.caffeine)

// Swift export not-yet-published dependencies.
implementation(libs.sir) { isTransitive = false }
implementation(libs.sir.providers) { isTransitive = false }
implementation(libs.sir.light.classes) { isTransitive = false }
implementation(libs.sir.printer) { isTransitive = false }

testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
}
42 changes: 42 additions & 0 deletions swift-export-playground/src/main/kotlin/PlaygroundSirSession.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.sir.SirModule

import org.jetbrains.kotlin.sir.providers.SirSession
import org.jetbrains.kotlin.sir.providers.SirTrampolineDeclarationsProvider
import org.jetbrains.kotlin.sir.providers.SirTypeProvider
import org.jetbrains.kotlin.sir.providers.impl.*
import org.jetbrains.kotlin.sir.providers.utils.UnsupportedDeclarationReporter
import org.jetbrains.sir.lightclasses.SirDeclarationFromKtSymbolProvider

internal class PlaygroundSirSession(
ktModule: KaModule,
moduleForPackageEnums: SirModule,
unsupportedDeclarationReporter: UnsupportedDeclarationReporter,
targetPackageFqName: FqName?,
) : SirSession {
override val declarationNamer = SirDeclarationNamerImpl()
override val moduleProvider = SirSingleModuleProvider("Playground")
override val declarationProvider = CachingSirDeclarationProvider(
declarationsProvider = SirDeclarationFromKtSymbolProvider(
ktModule = ktModule,
sirSession = sirSession,
)
)
override val enumGenerator = SirEnumGeneratorImpl(moduleForPackageEnums)
override val parentProvider = SirParentProviderImpl(
sirSession = sirSession,
packageEnumGenerator = enumGenerator,
)
override val typeProvider = SirTypeProviderImpl(
errorTypeStrategy = SirTypeProvider.ErrorTypeStrategy.ErrorType,
unsupportedTypeStrategy = SirTypeProvider.ErrorTypeStrategy.ErrorType,
sirSession = sirSession,
)
override val visibilityChecker = SirVisibilityCheckerImpl(unsupportedDeclarationReporter)
override val childrenProvider = SirDeclarationChildrenProviderImpl(
sirSession = sirSession,
)

override val trampolineDeclarationsProvider: SirTrampolineDeclarationsProvider = SirTrampolineDeclarationsProviderImpl(sirSession, targetPackageFqName)
}
89 changes: 89 additions & 0 deletions swift-export-playground/src/main/kotlin/Runner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
import org.jetbrains.kotlin.analysis.api.standalone.buildStandaloneAnalysisAPISession
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.SirFunctionBody
import org.jetbrains.kotlin.sir.SirModule
import org.jetbrains.kotlin.sir.SirMutableDeclarationContainer
import org.jetbrains.kotlin.sir.builder.buildModule
import org.jetbrains.kotlin.sir.providers.utils.SimpleUnsupportedDeclarationReporter
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 pkgModule = buildModule {
name = "pkg"
}
val unsupportedDeclarationReporter = SimpleUnsupportedDeclarationReporter()
val sirSession = PlaygroundSirSession(ktModule, pkgModule, unsupportedDeclarationReporter, targetPackageFqName = null)
val sirModule: SirModule = with(sirSession) {
ktModule.sirModule().also {
sources.flatMap { file ->
file.symbol.fileScope.extractDeclarations(useSiteSession)
}.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,
emptyBodyStub = SirFunctionBody(
listOf("stub()")
)
)
}
}

private fun collectModuleAndSources(
sourceRoot: Path,
kotlinModuleName: String,
stdlibPath: Path?,
): Pair<KaModule, List<KtFile>> {
val analysisAPISession = buildStandaloneAnalysisAPISession {
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<KtFile>()
}
110 changes: 110 additions & 0 deletions swift-export-playground/src/test/kotlin/Tests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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 {
stub()
}
"""
)

@Test
fun `class declaration`() = testSources(
"""
class A
""".trimIndent(),
"""
import KotlinRuntime
public final class A : KotlinRuntime.KotlinBase {
public override init() {
stub()
}
public override init(
__externalRCRef: Swift.UInt
) {
stub()
}
}
""".trimIndent()
)

@Test
fun `object declaration`() = testSources(
"""
object O
""".trimIndent(),
"""
import KotlinRuntime
public final class O : KotlinRuntime.KotlinBase {
public static var shared: Playground.O {
get {
stub()
}
}
private override init() {
stub()
}
public override init(
__externalRCRef: Swift.UInt
) {
stub()
}
}
""".trimIndent()
)

@Test
fun `typealias to basic type declaration`() = testSources(
"""
typealias MyInt = Int
""".trimIndent(),
"""
public typealias MyInt = Swift.Int32
""".trimIndent()
)

@Test
fun `strings and chars`() = testSources(
"""
fun produceString(): String = "hello"
fun firstChar(str: String): Char = str.first()
""".trimIndent(),
"""
public func firstChar(
str: Swift.String
) -> Swift.Unicode.UTF16.CodeUnit {
stub()
}
public func produceString() -> Swift.String {
stub()
}
""".trimIndent()
)
}

0 comments on commit 39e8501

Please sign in to comment.