Skip to content

Commit 39e8501

Browse files
committed
Add support for Swift export
Minor hack: use wasm stdlib to avoid downloading the whole K/N distribution
1 parent 3b1f866 commit 39e8501

File tree

19 files changed

+474
-3
lines changed

19 files changed

+474
-3
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ dependencies {
7878
implementation(libs.kotlin.core)
7979
implementation(project(":executors", configuration = "default"))
8080
implementation(project(":common", configuration = "default"))
81+
implementation(project(":swift-export-playground", configuration = "default"))
8182

8283
testImplementation("org.springframework.boot:spring-boot-starter-test") {
8384
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")

gradle/libs.versions.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jackson = "2.14.0"
2020
hamcrest = "2.2"
2121
compose = "1.7.0"
2222
gradle-develocity = "3.17.5"
23+
caffeine = "2.9.3"
2324

2425
[libraries]
2526
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-
3839
kotlin-compiler-ide = { group = "org.jetbrains.kotlin", name = "kotlin-compiler-for-ide", version.ref = "kotlinIdeVersion" }
3940
kotlin-idea = { group = "org.jetbrains.kotlin", name = "idea", version.ref = "kotlinIdeVersionWithSuffix" }
4041
kotlin-core = { group = "org.jetbrains.kotlin", name = "core", version.ref = "kotlinIdeVersionWithSuffix" }
42+
analysis-api-standalone-for-ide = { group = "org.jetbrains.kotlin", name = "analysis-api-standalone-for-ide", version.ref = "kotlin" }
43+
high-level-api-for-ide = { group = "org.jetbrains.kotlin", name = "high-level-api-for-ide", version.ref = "kotlin" }
44+
high-level-api-fir-for-ide = { group = "org.jetbrains.kotlin", name = "high-level-api-fir-for-ide", version.ref = "kotlin" }
45+
high-level-api-impl-base-for-ide = { group = "org.jetbrains.kotlin", name = "high-level-api-impl-base-for-ide", version.ref = "kotlin" }
46+
low-level-api-fir-for-ide = { group = "org.jetbrains.kotlin", name = "low-level-api-fir-for-ide", version.ref = "kotlin" }
47+
symbol-light-classes-for-ide = { group = "org.jetbrains.kotlin", name = "symbol-light-classes-for-ide", version.ref = "kotlin" }
48+
analysis-api-platform-interface-for-ide = { group = "org.jetbrains.kotlin", name = "analysis-api-platform-interface-for-ide", version.ref = "kotlin" }
49+
sir = { group = "org.jetbrains.kotlin", name = "sir", version.ref = "kotlin" }
50+
sir-providers = { group = "org.jetbrains.kotlin", name = "sir-providers", version.ref = "kotlin" }
51+
sir-light-classes = { group = "org.jetbrains.kotlin", name = "sir-light-classes", version.ref = "kotlin" }
52+
sir-printer = { group = "org.jetbrains.kotlin", name = "sir-printer", version.ref = "kotlin" }
4153
kotlinx-coroutines-core-jvm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-jvm", version.ref = "kotlinx-coroutines" }
4254
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" }
4355
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"
6476
compose-components-resources = { group = "org.jetbrains.compose.components", name = "components-resources", version.ref = "compose" }
6577
kotlin-serialization-plugin = {group= "org.jetbrains.kotlin", name="kotlin-serialization-compiler-plugin", version.ref = "kotlin"}
6678
gradle-develocity = {group = "com.gradle", name= "develocity-gradle-plugin", version.ref = "gradle-develocity"}
79+
caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeine" }
80+
6781

6882
[bundles]
6983
kotlin-stdlib = ["kotlin-stdlib", "kotlin-stdlib-jdk7", "kotlin-stdlib-jdk8"]

settings.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ plugins {
1616
include(":executors")
1717
include(":indexation")
1818
include(":common")
19-
include(":dependencies")
19+
include(":dependencies")
20+
include(":swift-export-playground")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.compiler.server.compiler.components
2+
3+
import com.compiler.server.model.CompilerDiagnostics
4+
import com.compiler.server.model.SwiftExportResult
5+
import com.compiler.server.model.toExceptionDescriptor
6+
import component.KotlinEnvironment
7+
import org.jetbrains.kotlin.psi.KtFile
8+
import org.springframework.stereotype.Component
9+
import runSwiftExport
10+
import java.nio.file.Path
11+
12+
@Component
13+
class SwiftExportTranslator(
14+
private val kotlinEnvironment: KotlinEnvironment,
15+
) {
16+
fun translate(files: List<KtFile>): SwiftExportResult = try {
17+
usingTempDirectory { tempDirectory ->
18+
val ioFiles = files.writeToIoFiles(tempDirectory)
19+
val stdlib = kotlinEnvironment.WASM_LIBRARIES.singleOrNull { "stdlib" in it }
20+
val swiftCode = runSwiftExport(
21+
sourceFile = ioFiles.first(),
22+
stdlibPath = stdlib?.let { Path.of(it) },
23+
)
24+
SwiftExportResult(
25+
compilerDiagnostics = CompilerDiagnostics(emptyMap()),
26+
swiftCode = swiftCode
27+
)
28+
}
29+
} catch (e: Exception) {
30+
SwiftExportResult(swiftCode = "", exception = e.toExceptionDescriptor())
31+
}
32+
}

src/main/kotlin/com/compiler/server/controllers/CompilerRestController.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class CompilerRestController(private val kotlinProjectExecutor: KotlinProjectExe
3434
KotlinTranslatableCompiler.JS -> kotlinProjectExecutor.convertToJsIr(project)
3535
KotlinTranslatableCompiler.WASM -> kotlinProjectExecutor.convertToWasm(project, debugInfo)
3636
KotlinTranslatableCompiler.COMPOSE_WASM -> kotlinProjectExecutor.convertToWasm(project, debugInfo)
37+
KotlinTranslatableCompiler.SWIFT_EXPORT -> kotlinProjectExecutor.convertToSwift(project).let {
38+
// TODO: A hack to avoid changing the return type of the function.
39+
object : TranslationResultWithJsCode(it.swiftCode, it.compilerDiagnostics, it.exception) {}
40+
}
3741
}
3842
}
3943

src/main/kotlin/com/compiler/server/controllers/KotlinPlaygroundRestController.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class KotlinPlaygroundRestController(private val kotlinProjectExecutor: KotlinPr
5151
debugInfo = false,
5252
)
5353
ProjectType.JUNIT -> kotlinProjectExecutor.test(project, addByteCode)
54+
ProjectType.SWIFT_EXPORT -> kotlinProjectExecutor.convertToSwift(project)
5455
}
5556
}
5657

src/main/kotlin/com/compiler/server/model/ExecutionResult.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ class JunitExecutionResult(
8989
jvmBytecode: String? = null,
9090
) : JvmExecutionResult(compilerDiagnostics, exception, jvmBytecode)
9191

92+
class SwiftExportResult(
93+
val swiftCode: String,
94+
override var exception: ExceptionDescriptor? = null,
95+
@field:JsonProperty("errors")
96+
override var compilerDiagnostics: CompilerDiagnostics = CompilerDiagnostics()
97+
) : ExecutionResult(compilerDiagnostics, exception)
98+
99+
92100
private fun unEscapeOutput(value: String) = value.replace("&amp;lt;".toRegex(), "<")
93101
.replace("&amp;gt;".toRegex(), ">")
94102
.replace("\r", "")

src/main/kotlin/com/compiler/server/model/KotlinTranslatableCompiler.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ package com.compiler.server.model
33
enum class KotlinTranslatableCompiler {
44
JS,
55
WASM,
6-
COMPOSE_WASM
6+
COMPOSE_WASM,
7+
SWIFT_EXPORT,
78
}

src/main/kotlin/com/compiler/server/model/Project.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ enum class ProjectType(@JsonValue val id: String) {
2020
CANVAS("canvas"),
2121
JS_IR("js-ir"),
2222
WASM("wasm"),
23-
COMPOSE_WASM("compose-wasm");
23+
COMPOSE_WASM("compose-wasm"),
24+
SWIFT_EXPORT("swift-export")
25+
;
2426

2527
fun isJvmRelated(): Boolean = this == JAVA || this == JUNIT
2628

src/main/kotlin/com/compiler/server/service/KotlinProjectExecutor.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class KotlinProjectExecutor(
1717
private val completionProvider: CompletionProvider,
1818
private val version: VersionInfo,
1919
private val kotlinToJSTranslator: KotlinToJSTranslator,
20+
private val swiftExportTranslator: SwiftExportTranslator,
2021
private val kotlinEnvironment: KotlinEnvironment,
2122
private val loggerDetailsStreamer: LoggerDetailsStreamer? = null,
2223
) {
@@ -52,6 +53,10 @@ class KotlinProjectExecutor(
5253
return convertWasmWithConverter(project, debugInfo, kotlinToJSTranslator::doTranslateWithWasm)
5354
}
5455

56+
fun convertToSwift(project: Project): SwiftExportResult {
57+
return convertSwiftWithConverter(project)
58+
}
59+
5560
fun complete(project: Project, line: Int, character: Int): List<Completion> {
5661
return kotlinEnvironment.environment {
5762
val file = getFilesFrom(project, it).first()
@@ -76,6 +81,7 @@ class KotlinProjectExecutor(
7681
project,
7782
debugInfo = false,
7883
).compilerDiagnostics
84+
ProjectType.SWIFT_EXPORT -> convertToSwift(project).compilerDiagnostics
7985
}
8086
} catch (e: Exception) {
8187
log.warn("Exception in getting highlight. Project: $project", e)
@@ -114,6 +120,15 @@ class KotlinProjectExecutor(
114120
}.also { logExecutionResult(project, it) }
115121
}
116122

123+
private fun convertSwiftWithConverter(
124+
project: Project,
125+
): SwiftExportResult {
126+
return kotlinEnvironment.environment { environment ->
127+
val files = getFilesFrom(project, environment).map { it.kotlinFile }
128+
swiftExportTranslator.translate(files)
129+
}.also { logExecutionResult(project, it) }
130+
}
131+
117132
private fun logExecutionResult(project: Project, executionResult: ExecutionResult) {
118133
loggerDetailsStreamer?.logExecutionResult(
119134
executionResult,

src/test/kotlin/com/compiler/server/ResourceE2ECompileTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ResourceE2ECompileTest : BaseResourceCompileTest {
3737
ProjectType.JAVA -> JvmExecutionResult::class.java
3838
ProjectType.JS, ProjectType.CANVAS, ProjectType.JS_IR -> TranslationJSResult::class.java
3939
ProjectType.WASM, ProjectType.COMPOSE_WASM -> TranslationWasmResult::class.java
40+
ProjectType.SWIFT_EXPORT -> SwiftExportResult::class.java
4041
}
4142
val result = RestTemplate().postForObject(
4243
"${getHost()}$url", HttpEntity(body, headers), resultClass
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.compiler.server
2+
3+
import com.compiler.server.base.BaseExecutorTest
4+
import org.junit.jupiter.api.Test
5+
import kotlin.test.assertContains
6+
import kotlin.test.assertEquals
7+
8+
class SwiftConverterTest : BaseExecutorTest() {
9+
10+
private fun exactTest(input: String, expected: String) {
11+
val actual = translateToSwift(input)
12+
assertEquals(expected, actual.swiftCode.trimEnd())
13+
}
14+
15+
private fun containsTest(input: String, expected: String) {
16+
val actual = translateToSwift(input)
17+
assertContains(actual.swiftCode.trimEnd(), expected)
18+
}
19+
20+
@Test
21+
fun basicSwiftExportTest() = containsTest(
22+
input = """
23+
fun main() {}
24+
""".trimIndent(),
25+
expected = "public func main() -> Swift.Void"
26+
)
27+
28+
@Test
29+
fun `use stdlib declaration`() = containsTest(
30+
input = "fun foo(): UInt = 42",
31+
expected = """
32+
public func foo() -> Swift.UInt32 {
33+
stub()
34+
}
35+
""".trimIndent()
36+
)
37+
38+
@Test
39+
fun `class declaration`() = exactTest(
40+
input = "public class MyClass { public fun A() {}}",
41+
expected = """
42+
import KotlinRuntime
43+
44+
public final class MyClass : KotlinRuntime.KotlinBase {
45+
public override init() {
46+
stub()
47+
}
48+
public override init(
49+
__externalRCRef: Swift.UInt
50+
) {
51+
stub()
52+
}
53+
public func A() -> Swift.Void {
54+
stub()
55+
}
56+
}
57+
""".trimIndent()
58+
)
59+
60+
@Test
61+
fun `simple packages`() = exactTest(
62+
input = """
63+
package foo.bar
64+
65+
val myProperty: Int = 42
66+
""".trimIndent(),
67+
expected = """
68+
@_exported import pkg
69+
70+
public extension pkg.foo.bar {
71+
public static var myProperty: Swift.Int32 {
72+
get {
73+
stub()
74+
}
75+
}
76+
}
77+
""".trimIndent()
78+
)
79+
80+
@Test
81+
fun `invalid code`() = exactTest(
82+
input = "abracadabra",
83+
expected = """
84+
""".trimIndent()
85+
)
86+
87+
@Test
88+
fun `more invalid code`() = exactTest(
89+
input = "fun foo(): Bar = error()",
90+
expected = """
91+
public func foo() -> ERROR_TYPE {
92+
stub()
93+
}
94+
""".trimIndent()
95+
)
96+
97+
@Test
98+
fun `unsupported type declaration`() = exactTest(
99+
input = """
100+
interface Foo
101+
102+
fun produceFoo(): Foo = TODO()
103+
""".trimIndent(),
104+
expected = """
105+
public func produceFoo() -> Swift.Never {
106+
stub()
107+
}
108+
""".trimIndent()
109+
)
110+
}

src/test/kotlin/com/compiler/server/base/BaseExecutorTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class BaseExecutorTest {
6565

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

68+
fun translateToSwift(code: String) = testRunner.translateToSwift(code)
69+
6870
fun runWithException(@Language("kotlin") code: String, contains: String, message: String? = null, addByteCode: Boolean = false) =
6971
testRunner.runWithException(code, contains, message, addByteCode)
7072

src/test/kotlin/com/compiler/server/generator/TestProjectRunner.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ class TestProjectRunner {
7575
)
7676
}
7777

78+
fun translateToSwift(code: String): SwiftExportResult {
79+
val project = generateSingleProject(text = code, projectType = ProjectType.SWIFT_EXPORT)
80+
return kotlinProjectExecutor.convertToSwift(project)
81+
}
82+
7883
fun runWithException(
7984
@Language("kotlin")
8085
code: String,

swift-export-playground/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An implementation of Swift export for Kotlin Playground.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
plugins {
2+
kotlin("jvm")
3+
}
4+
5+
repositories {
6+
mavenCentral()
7+
// For Analysis API components
8+
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies")
9+
}
10+
11+
dependencies {
12+
implementation(libs.kotlin.compiler)
13+
14+
// Analysis API components which are required for the Swift export
15+
implementation(libs.analysis.api.standalone.`for`.ide) { isTransitive = false }
16+
implementation(libs.high.level.api.`for`.ide) { isTransitive = false }
17+
implementation(libs.high.level.api.fir.`for`.ide) { isTransitive = false }
18+
implementation(libs.high.level.api.impl.base.`for`.ide) { isTransitive = false }
19+
implementation(libs.low.level.api.fir.`for`.ide) { isTransitive = false }
20+
implementation(libs.symbol.light.classes.`for`.ide) { isTransitive = false }
21+
implementation(libs.analysis.api.platform.`interface`.`for`.ide) { isTransitive = false }
22+
implementation(libs.caffeine)
23+
24+
// Swift export not-yet-published dependencies.
25+
implementation(libs.sir) { isTransitive = false }
26+
implementation(libs.sir.providers) { isTransitive = false }
27+
implementation(libs.sir.light.classes) { isTransitive = false }
28+
implementation(libs.sir.printer) { isTransitive = false }
29+
30+
testImplementation("junit:junit:4.13.2")
31+
testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
32+
}

0 commit comments

Comments
 (0)