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
  • Loading branch information
sbogolepov committed Dec 2, 2024
1 parent 3b1f866 commit 39e8501
Show file tree
Hide file tree
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
Expand Up @@ -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")
Expand Down
14 changes: 14 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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" }
Expand All @@ -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"]
Expand Down
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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) {}
}
}
}

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

Expand Down
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
Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
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")
}
Loading

0 comments on commit 39e8501

Please sign in to comment.