[Wasm] Lower complex external declarations

- Properties
- Member functions
- Object declarations
- Constructors
- Classes with instance checks
This commit is contained in:
Svyatoslav Kuzmich
2021-11-10 14:58:29 +03:00
parent c0761bf34a
commit 03c352c11e
7 changed files with 497 additions and 6 deletions
@@ -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
@@ -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
}
@@ -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<IrDeclaration>()
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
}
}
})
}
}
@@ -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)
@@ -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)
}
}
}
@@ -45,6 +45,15 @@ class JsMapping(val state: JsMappingState) : DefaultMapping(state) {
// Wasm mappings
val wasmJsInteropFunctionToWrapper =
state.newDeclarationToDeclarationMapping<IrSimpleFunction, IrSimpleFunction>()
val wasmNestedExternalToNewTopLevelFunction =
state.newDeclarationToDeclarationMapping<IrFunction, IrSimpleFunction>()
val wasmExternalObjectToGetInstanceFunction =
state.newDeclarationToDeclarationMapping<IrClass, IrSimpleFunction>()
val wasmExternalClassToInstanceCheck =
state.newDeclarationToDeclarationMapping<IrClass, IrSimpleFunction>()
}
+146 -2
View File
@@ -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"
}