diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt index 321532cf7c7..def400042b3 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt @@ -122,6 +122,18 @@ private val tailrecLoweringPhase = makeWasmModulePhase( description = "Replace `tailrec` call sites with equivalent loop" ) +private val complexExternalDeclarationsToTopLevelFunctionsLowering = makeWasmModulePhase( + ::ComplexExternalDeclarationsToTopLevelFunctionsLowering, + name = "ComplexExternalDeclarationsToTopLevelFunctionsLowering", + description = "Lower complex external declarations to top-level functions", +) + +private val complexExternalDeclarationsUsagesLowering = makeWasmModulePhase( + ::ComplexExternalDeclarationsUsageLowering, + name = "ComplexExternalDeclarationsUsageLowering", + description = "Lower usages of complex external declarations", +) + private val jsInteropFunctionsLowering = makeWasmModulePhase( ::JsInteropFunctionsLowering, name = "JsInteropFunctionsLowering", @@ -532,6 +544,9 @@ val wasmPhases = NamedCompilerPhase( delegateToPrimaryConstructorLoweringPhase then // Common prefix ends + complexExternalDeclarationsToTopLevelFunctionsLowering then + complexExternalDeclarationsUsagesLowering then + jsInteropFunctionsLowering then jsInteropFunctionCallsLowering then diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt index 10fb96205d0..93f091514d4 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt @@ -36,6 +36,10 @@ class DeclarationGenerator(val context: WasmModuleCodegenContext) : IrElementVis error("Unexpected element of type ${element::class}") } + override fun visitProperty(declaration: IrProperty) { + require(declaration.isExternal) + } + override fun visitTypeAlias(declaration: IrTypeAlias) { // Type aliases are not material } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt new file mode 100644 index 00000000000..bed26587b12 --- /dev/null +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/ComplexExternalDeclarationsToTopLevelFunctionsLowering.kt @@ -0,0 +1,301 @@ +/* + * Copyright 2010-2021 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.backend.wasm.lower + +import org.jetbrains.kotlin.backend.common.BodyLoweringPass +import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.common.lower.createIrBuilder +import org.jetbrains.kotlin.backend.wasm.WasmBackendContext +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName +import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget +import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter +import org.jetbrains.kotlin.ir.builders.declarations.buildFun +import org.jetbrains.kotlin.ir.builders.irCallConstructor +import org.jetbrains.kotlin.ir.builders.irString +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.ir.visitors.* +import org.jetbrains.kotlin.name.Name +import java.lang.StringBuilder + +/** + * Lower complex external declarations to top-level functions: + * - Property accessors + * - Member functions + * - Class constructors + * - Object declarations + * - Class instance checks + */ +class ComplexExternalDeclarationsToTopLevelFunctionsLowering(val context: WasmBackendContext) : FileLoweringPass { + lateinit var currentFile: IrFile + val addedDeclarations = mutableListOf() + + val externalFunToTopLevelMapping = + context.mapping.wasmNestedExternalToNewTopLevelFunction + + val externalObjectToGetInstanceFunction = + context.mapping.wasmExternalObjectToGetInstanceFunction + + override fun lower(irFile: IrFile) { + currentFile = irFile + for (declaration in irFile.declarations) { + if (declaration.isEffectivelyExternal()) { + processExternalDeclaration(declaration) + } + } + irFile.declarations += addedDeclarations + addedDeclarations.clear() + } + + fun processExternalDeclaration(declaration: IrDeclaration) { + declaration.acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + error("Unknown external element ${element::class}") + } + + override fun visitValueParameter(declaration: IrValueParameter) { + } + + override fun visitClass(declaration: IrClass) { + declaration.acceptChildrenVoid(this) + lowerExternalTopLevelClass(declaration) + } + + override fun visitProperty(declaration: IrProperty) { + processExternalProperty(declaration) + } + + override fun visitConstructor(declaration: IrConstructor) { + processExternalConstructor(declaration) + } + + override fun visitSimpleFunction(declaration: IrSimpleFunction) { + processExternalSimpleFunction(declaration) + } + }) + } + + fun lowerExternalTopLevelClass(klass: IrClass) { + if (klass.kind == ClassKind.OBJECT) + generateExternalObjectInstanceGetter(klass) + + if (klass.kind != ClassKind.INTERFACE) + generateInstanceCheckForExternalClass(klass) + } + + fun processExternalProperty(property: IrProperty) { + if (property.isFakeOverride) + return + + val propName: String = + property.getJsNameOrKotlinName().identifier + + property.getter?.let { getter -> + val dispatchReceiver = getter.dispatchReceiverParameter + val jsCode = + if (dispatchReceiver == null) + "() => $propName" + else + "(_this) => _this.$propName" + + val res = createExternalJsFunction( + property.name, + "_\$external_prop_getter", + resultType = getter.returnType, + jsCode = jsCode + ) + + if (dispatchReceiver != null) { + res.addValueParameter("_this", dispatchReceiver.type) + } + + externalFunToTopLevelMapping[getter] = res + } + + property.setter?.let { setter -> + val dispatchReceiver = setter.dispatchReceiverParameter + val jsCode = + if (dispatchReceiver == null) + "(v) => $propName = v" + else + "(_this, v) => _this.$propName = v" + + val res = createExternalJsFunction( + property.name, + "_\$external_prop_setter", + resultType = setter.returnType, + jsCode = jsCode + ) + + if (dispatchReceiver != null) { + res.addValueParameter("_this", dispatchReceiver.type) + } + res.addValueParameter("v", setter.valueParameters[0].type) + + externalFunToTopLevelMapping[setter] = res + } + } + + private fun StringBuilder.appendExternalClassReference(klass: IrClass) { + val parent = klass.parent + if (parent is IrClass) { + appendExternalClassReference(parent) + append('.') + } + append(klass.getJsNameOrKotlinName()) + } + + fun processExternalConstructor(constructor: IrConstructor) { + val klass = constructor.constructedClass + val jsCode = buildString { + append("(") + appendParameterList(constructor.valueParameters.size) + append(") => new ") + appendExternalClassReference(klass) + append("(") + appendParameterList(constructor.valueParameters.size) + append(")") + } + + val res = createExternalJsFunction( + klass.name, + "_\$external_constructor", + resultType = klass.defaultType, + jsCode = jsCode + ) + + constructor.valueParameters.forEach { res.addValueParameter(it.name, it.type) } + externalFunToTopLevelMapping[constructor] = res + } + + fun processExternalSimpleFunction(function: IrSimpleFunction) { + if (function.parent is IrPackageFragment) return + val jsName = function.getJsNameOrKotlinName() + val dispatchReceiver = function.dispatchReceiverParameter + require(dispatchReceiver != null) { + "Non top-level external function w/o dispatchReceiverParameter: ${function.fqNameWhenAvailable}" + } + + val jsCode = buildString { + append("(_this, ") + appendParameterList(function.valueParameters.size) + append(") => _this.") + append(jsName) + append("(") + appendParameterList(function.valueParameters.size) + append(")") + } + val res = createExternalJsFunction( + function.name, + "_\$external_member_fun", + resultType = function.returnType, + jsCode = jsCode + ) + res.addValueParameter("_this", dispatchReceiver.type) + function.valueParameters.forEach { res.addValueParameter(it.name, it.type) } + + externalFunToTopLevelMapping[function] = res + } + + fun generateExternalObjectInstanceGetter(obj: IrClass) { + context.mapping.wasmExternalObjectToGetInstanceFunction[obj] = createExternalJsFunction( + obj.name, + "_\$external_object_getInstance", + resultType = obj.defaultType, + jsCode = buildString { + append("() => ") + appendExternalClassReference(obj) + } + ) + } + + fun generateInstanceCheckForExternalClass(klass: IrClass) { + context.mapping.wasmExternalClassToInstanceCheck[klass] = createExternalJsFunction( + klass.name, + "_\$external_class_instanceof", + resultType = context.irBuiltIns.booleanType, + jsCode = buildString { + append("(x) => x instanceof ") + appendExternalClassReference(klass) + } + ).also { + it.addValueParameter("x", context.irBuiltIns.anyType) + } + } + + private fun createExternalJsFunction( + originalName: Name, + suffix: String, + resultType: IrType, + jsCode: String, + ): IrSimpleFunction { + val res = context.irFactory.buildFun { + name = Name.identifier(originalName.asStringStripSpecialMarkers() + suffix) + returnType = resultType + isExternal = true + } + val builder = context.createIrBuilder(res.symbol) + res.annotations += builder.irCallConstructor(context.wasmSymbols.jsFunConstructor, typeArguments = emptyList()).also { + it.putValueArgument(0, builder.irString(jsCode)) + } + res.parent = currentFile + addedDeclarations += res + return res + } +} + +/** + * Redirect usages of complex declarations to top-level functions + */ +class ComplexExternalDeclarationsUsageLowering(val context: WasmBackendContext) : BodyLoweringPass { + override fun lower(irBody: IrBody, container: IrDeclaration) { + irBody.transformChildrenVoid(object : IrElementTransformerVoid() { + override fun visitCall(expression: IrCall): IrExpression { + expression.transformChildrenVoid() + return transformCall(expression) + } + + override fun visitConstructorCall(expression: IrConstructorCall): IrExpression { + expression.transformChildrenVoid() + return transformCall(expression) + } + + override fun visitGetObjectValue(expression: IrGetObjectValue): IrExpression { + val externalGetInstance = context.mapping.wasmExternalObjectToGetInstanceFunction[expression.symbol.owner] + return if (externalGetInstance != null) { + IrCallImpl( + expression.startOffset, + expression.endOffset, + expression.type, + externalGetInstance.symbol, + valueArgumentsCount = 0, + typeArgumentsCount = 0 + ) + } else { + expression + } + } + + fun transformCall(call: IrFunctionAccessExpression): IrExpression { + val newFun: IrSimpleFunction? = + context.mapping.wasmNestedExternalToNewTopLevelFunction[call.symbol.owner.realOverrideTarget] + + return if (newFun != null) { + irCall(call, newFun, receiversAsArguments = true) + } else { + call + } + } + }) + } +} + + diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt index c0a81cd5ffe..42eb5f57233 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt @@ -20,7 +20,10 @@ import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName import org.jetbrains.kotlin.ir.builders.* import org.jetbrains.kotlin.ir.builders.declarations.* import org.jetbrains.kotlin.ir.declarations.* -import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.IrBody +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrStatementOriginImpl import org.jetbrains.kotlin.ir.expressions.impl.IrInstanceInitializerCallImpl import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.util.* @@ -53,6 +56,8 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT if (declaration !is IrSimpleFunction) return null val isExported = declaration.isExported() val isExternal = declaration.isExternal + if (declaration.isPropertyAccessor) return null + if (declaration.parent !is IrPackageFragment) return null if (!isExported && !isExternal) return null check(!(isExported && isExternal)) { "Exported external declarations are not supported: ${declaration.fqNameWhenAvailable}" } check(declaration.parent !is IrClass) { "Interop members are not supported: ${declaration.fqNameWhenAvailable}" } @@ -595,7 +600,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT } } -private fun StringBuilder.appendParameterList(size: Int) = +internal fun StringBuilder.appendParameterList(size: Int) = repeat(size) { append("p") append(it) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmTypeOperatorLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmTypeOperatorLowering.kt index 456ac17fec1..77ea6310319 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmTypeOperatorLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmTypeOperatorLowering.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.backend.common.lower.irNot import org.jetbrains.kotlin.backend.wasm.WasmBackendContext import org.jetbrains.kotlin.backend.wasm.ir2wasm.erasedUpperBound import org.jetbrains.kotlin.backend.wasm.ir2wasm.getRuntimeClass +import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.builders.* import org.jetbrains.kotlin.ir.declarations.IrClass @@ -151,9 +152,14 @@ class WasmBaseTypeOperatorTransformer(val context: WasmBackendContext) : IrEleme private fun generateTypeCheckNonNull(argument: IrExpression, toType: IrType): IrExpression { assert(!toType.isMarkedNullable()) + val classOrInterface = toType.eraseToClassOrInterface return when { - // TODO: Use instanceof for classes later - toType.eraseToClassOrInterface.isExternal -> builder.irTrue() + classOrInterface.isExternal -> { + if (classOrInterface.kind == ClassKind.INTERFACE) + builder.irTrue() + else + generateIsExternalClass(argument, classOrInterface) + } toType.isNothing() -> builder.irFalse() toType.isTypeParameter() -> generateTypeCheckWithTypeParameter(argument, toType) toType.isInterface() -> generateIsInterface(argument, toType) @@ -315,4 +321,11 @@ class WasmBaseTypeOperatorTransformer(val context: WasmBackendContext) : IrEleme putTypeArgument(0, toType) } } + + private fun generateIsExternalClass(argument: IrExpression, klass: IrClass): IrExpression { + val function = context.mapping.wasmJsInteropFunctionToWrapper[context.mapping.wasmExternalClassToInstanceCheck[klass]!!]!! + return builder.irCall(function).also { + it.putValueArgument(0, argument) + } + } } diff --git a/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/JsMapping.kt b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/JsMapping.kt index 2da271c9469..7ff4572d141 100644 --- a/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/JsMapping.kt +++ b/compiler/ir/serialization.js/src/org/jetbrains/kotlin/ir/backend/js/JsMapping.kt @@ -45,6 +45,15 @@ class JsMapping(val state: JsMappingState) : DefaultMapping(state) { // Wasm mappings val wasmJsInteropFunctionToWrapper = state.newDeclarationToDeclarationMapping() + + val wasmNestedExternalToNewTopLevelFunction = + state.newDeclarationToDeclarationMapping() + + val wasmExternalObjectToGetInstanceFunction = + state.newDeclarationToDeclarationMapping() + + val wasmExternalClassToInstanceCheck = + state.newDeclarationToDeclarationMapping() } diff --git a/compiler/testData/codegen/boxWasmJsInterop/externals.kt b/compiler/testData/codegen/boxWasmJsInterop/externals.kt index 47f1751f731..318153a5246 100644 --- a/compiler/testData/codegen/boxWasmJsInterop/externals.kt +++ b/compiler/testData/codegen/boxWasmJsInterop/externals.kt @@ -1,6 +1,9 @@ // FILE: externals.js function createObject() { - return {}; + return { + getXmethod() { return this.x; }, + setXmethod(v) { this.x = v; } + }; } function setX(obj, x) { @@ -11,15 +14,156 @@ function getX(obj) { return obj.x; } +const readOnlyProp = 123; +var mutableProp = "20"; + +class C1 { + constructor(a, b) { + this.a = a; + this.b = b; + } + + getA() { return this.a; } + setA(v) { this.a = v; } + + getB() { return this.b; } +} + +C1.Nested1 = class {} +C1.Nested1.Nested2 = class {} +C1.Nested1.Nested2.Nested3 = class { + constructor(x) { + this.x = x; + } + + foo() { return this.x + " from Nested 3"; } +} + +class C2 extends C1 { + constructor(a, b) { + super(a, b); + this.c = "C"; + } +} + + +C2.Object1 = { Object2: { Object3: { x: "C2.Object1.Object2.Object3.x" } } } + +const externalObj = { + x: "externalObj.x", + y: { x: "externalObj.y.x" }, + c: class { x = "(new externalObj.c()).x" } +} + + // FILE: externals.kt -external interface Obj +external interface Obj { + var x: Int + fun getXmethod(): Int + fun setXmethod(v: Int) +} external fun createObject(): Obj external fun setX(obj: Obj, x: Int) external fun getX(obj: Obj): Int +external val readOnlyProp: Int +external var mutableProp: String + +open external class C1 { + constructor(a: String, b: String) + var a: String + val b: String + + fun getA(): String + fun setA(x: String) + fun getB(): String + + class Nested1 { + class Nested2 { + class Nested3 { + constructor(x: String) + fun foo(): String + } + } + } +} + +external class C2 : C1 { + constructor(a: String, b: String) + + val c: String + + object Object1 { + object Object2 { + object Object3 { + val x: String + } + } + } +} + +external object externalObj { + val x: String + object y { + val x: String + } + class c { + val x: String + } +} + fun box(): String { val obj = createObject() setX(obj, 100) if (getX(obj) != 100) return "Fail 2" + + if (obj.x != 100) return "Fail 2.1" + obj.x = 200 + if (getX(obj) != 200) return "Fail 2.2" + val objXRef = obj::x + objXRef.set(300) + if (getX(obj) != 300 || obj.x != 300 || objXRef.get() != 300) return "Fail 2.3" + + if (obj.getXmethod() != 300) return "Fail 2.4" + obj.setXmethod(400) + if (obj.getXmethod() != 400 || getX(obj) != 400) return "Fail 2.5" + + if (readOnlyProp != 123) return "Fail 3" + if (::readOnlyProp.get() != 123) return "Fail 4" + if (mutableProp != "20") return "Fail 5" + mutableProp = "30" + if (mutableProp != "30") return "Fail 6" + (::mutableProp).set("40") + if (mutableProp != "40") return "Fail 7" + + val c1 = C1("A", "B") + if (c1.a != "A" || c1.b != "B") return "Fail 8" + if (c1.getA() != "A" || c1.getB() != "B") return "Fail 9" + c1.setA("A2") + if (c1.a != "A2") return "Fail 10" + c1.a = "A3" + if (c1.getA() != "A3") return "Fail 11" + val c2 = C2("A", "B") + if (c2.a != "A" || c2.b != "B" || c2.c != "C") return "Fail 12" + val c2_as_c1: C1 = c2 + if (c2_as_c1.a != "A" || c2_as_c1.b != "B") return "Fail 13" + + val nested3 = C1.Nested1.Nested2.Nested3("example") + if (nested3.foo() != "example from Nested 3") return "Fail 14" + + if (C2.Object1.Object2.Object3.x != "C2.Object1.Object2.Object3.x") return "Fail 15" + if (externalObj.x != "externalObj.x") return "Fail 16" + if (externalObj.y.x != "externalObj.y.x") return "Fail 17" + if (externalObj.c().x != "(new externalObj.c()).x") return "Fail 18" + + + if (c1 as Any !is C1) return "Fail 19" + if (c2 as Any !is C1) return "Fail 20" + if (c2 as Any !is C2) return "Fail 21" + if (externalObj.c() as Any !is externalObj.c) return "Fail 22" + if (10 as Any is C1) return "Fail 23" + if (c1 as Any is C2) return "Fail 24" + + return "OK" } \ No newline at end of file