[Swift export] Add swift-export-playground module

To mitigate problems with artifacts versioning at Kotlin Playground,
we package all required dependencies under a single module.

^KT-68877
diff --git a/generators/sir-tests-generator/build.gradle.kts b/generators/sir-tests-generator/build.gradle.kts
index a074a7e..2cd6d4d 100644
--- a/generators/sir-tests-generator/build.gradle.kts
+++ b/generators/sir-tests-generator/build.gradle.kts
@@ -10,6 +10,7 @@
 dependencies {
     implementation(projectTests(":native:swift:sir-compiler-bridge"))
     implementation(projectTests(":native:swift:swift-export-standalone"))
+    implementation(projectTests(":native:swift:swift-export-playground"))
     implementation(projectTests(":generators:test-generator"))
     runtimeOnly(projectTests(":analysis:analysis-test-framework"))
     runtimeOnly(libs.junit.jupiter.api)
diff --git a/generators/sir-tests-generator/main/org/jetbrains/kotlin/generators/tests/native/swift/sir/GenerateSirTests.kt b/generators/sir-tests-generator/main/org/jetbrains/kotlin/generators/tests/native/swift/sir/GenerateSirTests.kt
index 827a850..6927443 100644
--- a/generators/sir-tests-generator/main/org/jetbrains/kotlin/generators/tests/native/swift/sir/GenerateSirTests.kt
+++ b/generators/sir-tests-generator/main/org/jetbrains/kotlin/generators/tests/native/swift/sir/GenerateSirTests.kt
@@ -11,7 +11,7 @@
 import org.jetbrains.kotlin.konan.test.blackbox.support.group.UseStandardTestCaseGroupProvider
 import org.jetbrains.kotlin.sir.bridge.AbstractKotlinSirBridgeTest
 import org.jetbrains.kotlin.swiftexport.standalone.AbstractKlibBasedSwiftRunnerTest
-
+import org.jetbrains.kotlin.swiftexport.playground.AbstractPlaygroundTranslatorTest
 
 fun main() {
     System.setProperty("java.awt.headless", "true")
@@ -40,5 +40,15 @@
                 model("", extension = null, recursive = false)
             }
         }
+        testGroup(
+            "native/swift/swift-export-playground/tests-gen/",
+            "native/swift/swift-export-playground/testData"
+        ) {
+            testClass<AbstractPlaygroundTranslatorTest>(
+                suiteTestClassName = "SwiftExportPlaygroundTestGenerated",
+            ) {
+                model("")
+            }
+        }
     }
 }
diff --git a/native/swift/swift-export-playground/README.md b/native/swift/swift-export-playground/README.md
new file mode 100644
index 0000000..d40cd59
--- /dev/null
+++ b/native/swift/swift-export-playground/README.md
@@ -0,0 +1,5 @@
+# Swift Export for Kotlin Playground
+
+Swift export is accessible at https://play.kotlinlang.org to provide an easy way for folks to check out the generated API.  
+This modules packages all required dependencies into a single artifact with a pretty dumb API surface: 
+take Kotlin file, return Swift API for it.
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/build.gradle.kts b/native/swift/swift-export-playground/build.gradle.kts
new file mode 100644
index 0000000..f92708d
--- /dev/null
+++ b/native/swift/swift-export-playground/build.gradle.kts
@@ -0,0 +1,62 @@
+
+plugins {
+    kotlin("jvm")
+    id("jps-compatible")
+}
+
+description = "High-level Swift export API for Kotlin Playground"
+
+kotlin {
+    explicitApi()
+}
+
+dependencies {
+    compileOnly(kotlinStdlib())
+
+    implementation(project(":native:swift:sir"))
+    implementation(project(":native:swift:sir-light-classes"))
+    implementation(project(":native:swift:sir-printer"))
+    implementation(project(":native:swift:sir-providers"))
+
+    implementation(project(":analysis:analysis-api"))
+    implementation(project(":analysis:analysis-api-standalone"))
+
+    testApi(platform(libs.junit.bom))
+    testRuntimeOnly(libs.junit.jupiter.engine)
+    testImplementation(libs.junit.jupiter.api)
+
+    testRuntimeOnly(projectTests(":analysis:low-level-api-fir"))
+    testRuntimeOnly(projectTests(":analysis:analysis-api-impl-base"))
+    testImplementation(projectTests(":analysis:analysis-api-fir"))
+    testImplementation(projectTests(":analysis:analysis-test-framework"))
+    testImplementation(projectTests(":compiler:tests-common"))
+    testImplementation(projectTests(":compiler:tests-common-new"))
+
+    if (!kotlinBuildProperties.isInJpsBuildIdeaSync) {
+        testApi(projectTests(":native:native.tests"))
+    }
+}
+
+sourceSets {
+    "main" { projectDefault() }
+    "test" {
+        projectDefault()
+        generatedTestDir()
+    }
+}
+
+val testDataDir = projectDir.resolve("testData")
+
+val test by nativeTest("test", null) {
+    inputs.dir(testDataDir)
+    workingDir = rootDir
+    useJUnitPlatform { }
+}
+
+testsJar()
+
+if (kotlinBuildProperties.isSwiftExportPluginPublishingEnabled) {
+    publish()
+}
+
+runtimeJar(rewriteDefaultJarDepsToShadedCompiler())
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/src/org/jetbrains/kotlin/swiftexport/playground/PlaygroundSirSession.kt b/native/swift/swift-export-playground/src/org/jetbrains/kotlin/swiftexport/playground/PlaygroundSirSession.kt
new file mode 100644
index 0000000..b91b6ec
--- /dev/null
+++ b/native/swift/swift-export-playground/src/org/jetbrains/kotlin/swiftexport/playground/PlaygroundSirSession.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.swiftexport.playground
+
+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,
+        )
+    )
+    private 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, enumGenerator)
+}
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/src/org/jetbrains/kotlin/swiftexport/playground/PlaygroundTranslator.kt b/native/swift/swift-export-playground/src/org/jetbrains/kotlin/swiftexport/playground/PlaygroundTranslator.kt
new file mode 100644
index 0000000..1d71829
--- /dev/null
+++ b/native/swift/swift-export-playground/src/org/jetbrains/kotlin/swiftexport/playground/PlaygroundTranslator.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.swiftexport.playground
+
+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
+
+/**
+ * [stdlibPath] is a path to stdlib.klib which is required to properly resolve references to declarations from the standard library.
+ */
+public class PlaygroundTranslator(
+    private val stdlibPath: Path?
+) {
+
+    /**
+     * Translate public API of the given [sourceFile] to Swift sources.
+     * [stdlibPath] is a path to stdlib.klib which is required to properly resolve references from [sourceFile].
+     */
+    public fun translate(sourceFile: Path): String {
+        val (ktModule, sources) = collectModuleAndSources(sourceFile, 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,
+        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 = "Playground"
+                        if (stdlib != null) {
+                            addRegularDependency(stdlib)
+                        }
+                    }
+                )
+            }
+        }
+
+        val (sourceModule, rawFiles) = analysisAPISession.modulesWithFiles.entries.single()
+        return sourceModule to rawFiles.filterIsInstance<KtFile>()
+    }
+}
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/test/org/jetbrains/kotlin/swiftexport/playground/AbstractPlaygroundTranslatorTest.kt b/native/swift/swift-export-playground/test/org/jetbrains/kotlin/swiftexport/playground/AbstractPlaygroundTranslatorTest.kt
new file mode 100644
index 0000000..d1cd7df
--- /dev/null
+++ b/native/swift/swift-export-playground/test/org/jetbrains/kotlin/swiftexport/playground/AbstractPlaygroundTranslatorTest.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.swiftexport.playground
+
+import com.intellij.testFramework.TestDataFile
+import org.jetbrains.kotlin.konan.target.Distribution
+import org.jetbrains.kotlin.test.KotlinTestUtils
+import kotlin.io.path.Path
+import kotlin.io.path.nameWithoutExtension
+
+abstract class AbstractPlaygroundTranslatorTest {
+    protected fun runTest(@TestDataFile testPath: String) {
+        val sourceFile = Path(testPath)
+        val actual = PlaygroundTranslator(Path(KotlinNativeDistribution.stdlibPath)).translate(sourceFile)
+        val expectedFile = sourceFile.resolveSibling("${sourceFile.nameWithoutExtension}.swift")
+        KotlinTestUtils.assertEqualsToFile(expectedFile, actual)
+    }
+}
+
+private object KotlinNativeDistribution {
+    val stdlibPath: String
+        get() = Distribution(System.getProperty("kotlin.internal.native.test.nativeHome")).stdlib
+}
diff --git a/native/swift/swift-export-playground/testData/invalid.kt b/native/swift/swift-export-playground/testData/invalid.kt
new file mode 100644
index 0000000..9660ff0
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/invalid.kt
@@ -0,0 +1,5 @@
+vax abracadabra
+
+val x: UndeclaredType = TODO()
+
+val y: () -> Unit = TODO()
diff --git a/native/swift/swift-export-playground/testData/invalid.swift b/native/swift/swift-export-playground/testData/invalid.swift
new file mode 100644
index 0000000..b3b2d16
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/invalid.swift
@@ -0,0 +1,10 @@
+public var x: ERROR_TYPE {
+    get {
+        stub()
+    }
+}
+public var y: Swift.Never {
+    get {
+        stub()
+    }
+}
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/testData/packaged.kt b/native/swift/swift-export-playground/testData/packaged.kt
new file mode 100644
index 0000000..c68bfee
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/packaged.kt
@@ -0,0 +1,3 @@
+package org.me.playground
+
+class Foo
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/testData/packaged.swift b/native/swift/swift-export-playground/testData/packaged.swift
new file mode 100644
index 0000000..d26cb63
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/packaged.swift
@@ -0,0 +1,15 @@
+@_exported import pkg
+import KotlinRuntime
+
+public extension pkg.org.me.playground {
+    public class Foo : KotlinRuntime.KotlinBase {
+        public override init() {
+            stub()
+        }
+        public override init(
+            __externalRCRef: Swift.UInt
+        ) {
+            stub()
+        }
+    }
+}
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/testData/smoke.kt b/native/swift/swift-export-playground/testData/smoke.kt
new file mode 100644
index 0000000..874783e
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/smoke.kt
@@ -0,0 +1,22 @@
+fun foo() { println("hello") }
+
+class A(
+    val boolProp: Boolean,
+    val intProp: Int,
+    val floatProp: Float,
+    val refProp: B,
+)
+
+class B
+
+class C {
+    class D {
+        class E {
+
+        }
+    }
+
+    fun method(): D.E = D.E()
+}
+
+typealias CDE = C.D.E
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/testData/smoke.swift b/native/swift/swift-export-playground/testData/smoke.swift
new file mode 100644
index 0000000..802061f
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/smoke.swift
@@ -0,0 +1,84 @@
+import KotlinRuntime
+
+public typealias CDE = Playground.C.D.E
+public class A : KotlinRuntime.KotlinBase {
+    public var boolProp: Swift.Bool {
+        get {
+            stub()
+        }
+    }
+    public var floatProp: Swift.Float {
+        get {
+            stub()
+        }
+    }
+    public var intProp: Swift.Int32 {
+        get {
+            stub()
+        }
+    }
+    public var refProp: Playground.B {
+        get {
+            stub()
+        }
+    }
+    public override init(
+        __externalRCRef: Swift.UInt
+    ) {
+        stub()
+    }
+    public init(
+        boolProp: Swift.Bool,
+        intProp: Swift.Int32,
+        floatProp: Swift.Float,
+        refProp: Playground.B
+    ) {
+        stub()
+    }
+}
+public class B : KotlinRuntime.KotlinBase {
+    public override init() {
+        stub()
+    }
+    public override init(
+        __externalRCRef: Swift.UInt
+    ) {
+        stub()
+    }
+}
+public class C : KotlinRuntime.KotlinBase {
+    public class D : KotlinRuntime.KotlinBase {
+        public class E : KotlinRuntime.KotlinBase {
+            public override init() {
+                stub()
+            }
+            public override init(
+                __externalRCRef: Swift.UInt
+            ) {
+                stub()
+            }
+        }
+        public override init() {
+            stub()
+        }
+        public override init(
+            __externalRCRef: Swift.UInt
+        ) {
+            stub()
+        }
+    }
+    public override init() {
+        stub()
+    }
+    public override init(
+        __externalRCRef: Swift.UInt
+    ) {
+        stub()
+    }
+    public func method() -> Playground.C.D.E {
+        stub()
+    }
+}
+public func foo() -> Swift.Void {
+    stub()
+}
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/testData/stdlib_usages.kt b/native/swift/swift-export-playground/testData/stdlib_usages.kt
new file mode 100644
index 0000000..2b75072
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/stdlib_usages.kt
@@ -0,0 +1,4 @@
+val ui: UInt = 0
+
+// Is not supported properly yet.
+val s: String = "x"
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/testData/stdlib_usages.swift b/native/swift/swift-export-playground/testData/stdlib_usages.swift
new file mode 100644
index 0000000..4e8e5e9b
--- /dev/null
+++ b/native/swift/swift-export-playground/testData/stdlib_usages.swift
@@ -0,0 +1,10 @@
+public var s: Swift.Never {
+    get {
+        stub()
+    }
+}
+public var ui: Swift.UInt32 {
+    get {
+        stub()
+    }
+}
\ No newline at end of file
diff --git a/native/swift/swift-export-playground/tests-gen/org/jetbrains/kotlin/swiftexport/playground/SwiftExportPlaygroundTestGenerated.java b/native/swift/swift-export-playground/tests-gen/org/jetbrains/kotlin/swiftexport/playground/SwiftExportPlaygroundTestGenerated.java
new file mode 100644
index 0000000..c5edfc0
--- /dev/null
+++ b/native/swift/swift-export-playground/tests-gen/org/jetbrains/kotlin/swiftexport/playground/SwiftExportPlaygroundTestGenerated.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.swiftexport.playground;
+
+import com.intellij.testFramework.TestDataPath;
+import org.jetbrains.kotlin.test.util.KtTestUtil;
+import org.jetbrains.kotlin.test.TestMetadata;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.native.swift.sir.GenerateSirTestsKt}. DO NOT MODIFY MANUALLY */
+@SuppressWarnings("all")
+@TestMetadata("native/swift/swift-export-playground/testData")
+@TestDataPath("$PROJECT_ROOT")
+public class SwiftExportPlaygroundTestGenerated extends AbstractPlaygroundTranslatorTest {
+  @Test
+  public void testAllFilesPresentInTestData() {
+    KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("native/swift/swift-export-playground/testData"), Pattern.compile("^(.+)\\.kt$"), null, true);
+  }
+
+  @Test
+  @TestMetadata("invalid.kt")
+  public void testInvalid() {
+    runTest("native/swift/swift-export-playground/testData/invalid.kt");
+  }
+
+  @Test
+  @TestMetadata("packaged.kt")
+  public void testPackaged() {
+    runTest("native/swift/swift-export-playground/testData/packaged.kt");
+  }
+
+  @Test
+  @TestMetadata("smoke.kt")
+  public void testSmoke() {
+    runTest("native/swift/swift-export-playground/testData/smoke.kt");
+  }
+
+  @Test
+  @TestMetadata("stdlib_usages.kt")
+  public void testStdlib_usages() {
+    runTest("native/swift/swift-export-playground/testData/stdlib_usages.kt");
+  }
+}
diff --git a/settings.gradle b/settings.gradle
index a8288bf..a372920 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -466,6 +466,7 @@
         ":native:swift:sir-compiler-bridge",
         ":native:swift:sir-providers",
         ":native:swift:swift-export-standalone",
+        ":native:swift:swift-export-playground",
         ":native:swift:swift-export-embeddable",
         ":generators:sir-tests-generator"