All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.backend.wasm.lower.JsInteropFunctionsLowering.kt Maven / Gradle / Ivy

There is a newer version: 2.1.0-RC
Show newest version
/*
 * 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.DeclarationTransformer
import org.jetbrains.kotlin.backend.common.ir.addDispatchReceiver
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.ir2wasm.isBuiltInWasmRefType
import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExternalType
import org.jetbrains.kotlin.backend.wasm.ir2wasm.toJsStringLiteral
import org.jetbrains.kotlin.backend.wasm.utils.getJsFunAnnotation
import org.jetbrains.kotlin.backend.wasm.utils.getWasmImportDescriptor
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName
import org.jetbrains.kotlin.ir.backend.js.utils.isJsExport
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.IrBody
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
import org.jetbrains.kotlin.ir.expressions.impl.IrInstanceInitializerCallImpl
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.config.WasmTarget
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.util.OperatorNameConventions

/**
 * Create wrappers for external and @JsExport functions when type adaptation is needed
 */
class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationTransformer {
    val builtIns = context.irBuiltIns
    val symbols = context.wasmSymbols
    val jsRelatedSymbols get() = context.wasmSymbols.jsRelatedSymbols
    val adapters get() = jsRelatedSymbols.jsInteropAdapters

    val additionalDeclarations = mutableListOf()
    lateinit var currentParent: IrDeclarationParent

    override fun transformFlat(declaration: IrDeclaration): List? {
        if (context.configuration.get(JSConfigurationKeys.WASM_TARGET, WasmTarget.JS) == WasmTarget.WASI) return null

        if (declaration.isFakeOverride) return null
        if (declaration !is IrSimpleFunction) return null
        val isExported = declaration.isJsExport()
        val isExternal = declaration.isExternal || declaration.getJsFunAnnotation() != null
        if (declaration.isPropertyAccessor) return null
        if (declaration.parent !is IrPackageFragment) return null
        if (!isExported && !isExternal) return null
        if (declaration.getWasmImportDescriptor() != null) 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}" }
        if (context.mapping.wasmNestedExternalToNewTopLevelFunction.keys.contains(declaration)) return null

        additionalDeclarations.clear()
        currentParent = declaration.parent
        val newDeclarations = if (isExternal)
            transformExternalFunction(declaration)
        else
            transformExportFunction(declaration)

        return (newDeclarations ?: listOf(declaration)) + additionalDeclarations
    }

    private fun doubleIfNumber(possiblyNumber: IrType): IrType {
        val isNullable = possiblyNumber.isNullable()
        val notNullType = possiblyNumber.makeNotNull()
        if (notNullType != builtIns.numberType) return possiblyNumber
        return if (isNullable) builtIns.doubleType.makeNullable() else builtIns.doubleType
    }

    /**
     *  external fun foo(x: KotlinType): KotlinType
     *
     *  ->
     *
     *  external fun foo(x: JsType): JsType
     *  fun foo__externalAdapter(x: KotlinType): KotlinType = adaptResult(foo(adaptParameter(x)));
     */
    fun transformExternalFunction(function: IrSimpleFunction): List? {
        // External functions with default parameter values are already processed by
        // [ComplexExternalDeclarationsToTopLevelFunctionsLowering]
        if (function.valueParameters.any { it.defaultValue != null })
            return null

        // Patch function types for Number parameters as double
        function.returnType = doubleIfNumber(function.returnType)

        val valueParametersAdapters = function.valueParameters.map { parameter ->
            val varargElementType = parameter.varargElementType
            if (varargElementType != null) {
                CopyToJsArrayAdapter(parameter.type, varargElementType)
            } else {
                parameter.type.kotlinToJsAdapterIfNeeded(isReturn = false)
            }
        }
        val resultAdapter =
            function.returnType.jsToKotlinAdapterIfNeeded(isReturn = true)

        if (resultAdapter == null && valueParametersAdapters.all { it == null })
            return null

        val newFun = context.irFactory.createStaticFunctionWithReceivers(
            function.parent,
            name = Name.identifier(function.name.asStringStripSpecialMarkers() + "__externalAdapter"),
            function,
            remapMultiFieldValueClassStructure = context::remapMultiFieldValueClassStructure
        )

        function.valueParameters.forEachIndexed { index, newParameter ->
            val adapter = valueParametersAdapters[index]
            if (adapter != null) {
                newParameter.type = adapter.toType
            }
        }
        resultAdapter?.let {
            function.returnType = resultAdapter.fromType
        }

        val builder = context.createIrBuilder(newFun.symbol)
        newFun.body = createAdapterFunctionBody(builder, newFun, function, valueParametersAdapters, resultAdapter)
        newFun.annotations = emptyList()

        context.mapping.wasmJsInteropFunctionToWrapper[function] = newFun
        return listOf(function, newFun)
    }

    /**
     *  @JsExport
     *  fun foo(x: KotlinType): KotlinType {  }
     *
     *  ->
     *
     *  @JsExport
     *  @JsName("foo")
     *  fun foo__JsExportAdapter(x: JsType): JsType =
     *      adaptResult(foo(adaptParameter(x)));
     *
     *  fun foo(x: KotlinType): KotlinType {  }
     */
    fun transformExportFunction(function: IrSimpleFunction): List? {
        val valueParametersAdapters = function.valueParameters.map {
            it.type.jsToKotlinAdapterIfNeeded(isReturn = false)
        }
        val resultAdapter =
            function.returnType.kotlinToJsAdapterIfNeeded(isReturn = true)

        if (resultAdapter == null && valueParametersAdapters.all { it == null })
            return null

        val newFun = context.irFactory.createStaticFunctionWithReceivers(
            function.parent,
            name = Name.identifier(function.name.asStringStripSpecialMarkers() + "__JsExportAdapter"),
            function,
            remapMultiFieldValueClassStructure = context::remapMultiFieldValueClassStructure
        )

        newFun.valueParameters.forEachIndexed { index, newParameter ->
            val adapter = valueParametersAdapters[index]
            if (adapter != null) {
                newParameter.type = adapter.fromType
            }
        }
        resultAdapter?.let {
            newFun.returnType = resultAdapter.toType
        }

        // Delegate new function to old function:
        val builder: DeclarationIrBuilder = context.createIrBuilder(newFun.symbol)
        newFun.body = createAdapterFunctionBody(builder, newFun, function, valueParametersAdapters, resultAdapter)

        newFun.annotations += builder.irCallConstructor(jsRelatedSymbols.jsNameConstructor, typeArguments = emptyList()).also {
            it.putValueArgument(0, builder.irString(function.getJsNameOrKotlinName().identifier))
        }
        function.annotations = function.annotations.filter { it.symbol != jsRelatedSymbols.jsExportConstructor }

        return listOf(function, newFun)
    }

    private fun createAdapterFunctionBody(
        builder: DeclarationIrBuilder,
        function: IrSimpleFunction,
        functionToCall: IrSimpleFunction,
        valueParametersAdapters: List,
        resultAdapter: InteropTypeAdapter?
    ) = builder.irBlockBody {
        +irReturn(
            irCall(functionToCall).let { call ->
                for ((index, valueParameter) in function.valueParameters.withIndex()) {
                    val get = irGet(valueParameter)
                    call.putValueArgument(index, valueParametersAdapters[index].adaptIfNeeded(get, builder))
                }
                resultAdapter.adaptIfNeeded(call, builder)
            }
        )
    }

    val primitivesToExternRefAdapters: Map by lazy {
        mapOf(
            builtIns.byteType to adapters.kotlinByteToExternRefAdapter,
            builtIns.shortType to adapters.kotlinShortToExternRefAdapter,
            builtIns.charType to adapters.kotlinCharToExternRefAdapter,
            builtIns.intType to adapters.kotlinIntToExternRefAdapter,
            builtIns.longType to adapters.kotlinLongToExternRefAdapter,
            builtIns.floatType to adapters.kotlinFloatToExternRefAdapter,
            builtIns.doubleType to adapters.kotlinDoubleToExternRefAdapter,
        ).mapValues { FunctionBasedAdapter(it.value.owner) }
    }

    private fun IrType.kotlinToJsAdapterIfNeeded(isReturn: Boolean): InteropTypeAdapter? {
        if (isReturn && this == builtIns.unitType)
            return null

        if (this == builtIns.nothingType)
            return null

        if (!isNullable()) {
            return kotlinToJsAdapterIfNeededNotNullable(isReturn)
        }

        val notNullType = makeNotNull()

        if (notNullType == builtIns.numberType) {
            return NullOrAdapter(
                CombineAdapter(
                    FunctionBasedAdapter(adapters.kotlinDoubleToExternRefAdapter.owner),
                    FunctionBasedAdapter(adapters.numberToDoubleAdapter.owner)
                )
            )
        }

        val primitiveToExternRefAdapter = primitivesToExternRefAdapters[notNullType]

        val typeAdapter = primitiveToExternRefAdapter
            ?: notNullType.kotlinToJsAdapterIfNeededNotNullable(isReturn)
            ?: return null

        return NullOrAdapter(typeAdapter)
    }

    private fun IrType.kotlinToJsAdapterIfNeededNotNullable(isReturn: Boolean): InteropTypeAdapter? {
        if (isReturn && this == builtIns.unitType)
            return null

        if (this == builtIns.nothingType)
            return null

        when (this) {
            builtIns.stringType -> return FunctionBasedAdapter(adapters.kotlinToJsStringAdapter.owner)
            builtIns.booleanType -> return FunctionBasedAdapter(adapters.kotlinBooleanToExternRefAdapter.owner)
            builtIns.anyType -> return FunctionBasedAdapter(adapters.kotlinToJsAnyAdapter.owner)
            builtIns.numberType -> return FunctionBasedAdapter(adapters.numberToDoubleAdapter.owner)

            builtIns.byteType,
            builtIns.shortType,
            builtIns.charType,
            builtIns.intType,
            builtIns.longType,
            builtIns.floatType,
            builtIns.doubleType,
            context.wasmSymbols.voidType ->
                return null

        }

        if (isExternalType(this))
            return null

        if (isBuiltInWasmRefType(this))
            return null

        if (this is IrSimpleType && this.isFunction()) {
            val functionTypeInfo = FunctionTypeInfo(this, toJs = true)

            // Kotlin's closures are objects that implement FunctionN interface.
            // JavaScript can receive opaque reference to them but cannot call them directly.
            // Thus, we export helper "caller" method that JavaScript will use to call kotlin closures:
            //
            //     @JsExport
            //     fun __callFunction_(f: structref, p1: JsType1, p2: JsType2, ...): JsTypeRes {
            //          return adapt(
            //              cast(f).invoke(
            //                  adapt(p1),
            //                  adapt(p2),
            //                  ...
            //               )
            //          )
            //     }
            //
            context.closureCallExports.getOrPut(functionTypeInfo.signatureString) {
                createKotlinClosureCaller(functionTypeInfo)
            }

            // Converter functions creates new JavaScript closures that delegate to Kotlin closures
            // using above-mentioned "caller" export:
            //
            //     @JsFun("""(f) => {
            //        (p1, p2, ...) => .__callFunction_(f, p1, p2, ...)
            //     }""")
            //     external fun __convertKotlinClosureToJsClosure_(f: structref): ExternalRef
            //
            val kotlinToJsClosureConvertor = context.kotlinClosureToJsConverters.getOrPut(functionTypeInfo.signatureString) {
                createKotlinToJsClosureConvertor(functionTypeInfo)
            }
            return FunctionBasedAdapter(kotlinToJsClosureConvertor)
        }

        return SendKotlinObjectToJsAdapter(this)
    }

    private fun createNullableAdapter(notNullType: IrType, isPrimitive: Boolean, valueAdapter: InteropTypeAdapter?): InteropTypeAdapter? {
        return if (isPrimitive) { //nullable primitive should be checked and adapt to target type
            val externRefToPrimitiveAdapter = when (notNullType) {
                builtIns.floatType -> adapters.externRefToKotlinFloatAdapter.owner
                builtIns.doubleType -> adapters.externRefToKotlinDoubleAdapter.owner
                builtIns.longType -> adapters.externRefToKotlinLongAdapter.owner
                builtIns.booleanType -> adapters.externRefToKotlinBooleanAdapter.owner
                else -> adapters.externRefToKotlinIntAdapter.owner
            }
            val externalToPrimitiveAdapter = FunctionBasedAdapter(externRefToPrimitiveAdapter)
            NullOrAdapter(
                adapter = valueAdapter?.let { CombineAdapter(it, externalToPrimitiveAdapter) } ?: externalToPrimitiveAdapter
            )
        } else { //nullable reference should not be checked
            val nullableValueAdapter = valueAdapter?.let(::NullOrAdapter)
            val undefinedToNullAdapter = FunctionBasedAdapter(adapters.jsCheckIsNullOrUndefinedAdapter.owner)
            nullableValueAdapter
                ?.let { CombineAdapter(it, undefinedToNullAdapter) }
                ?: undefinedToNullAdapter
        }
    }

    private fun createNotNullAdapter(notNullType: IrType, isPrimitive: Boolean, valueAdapter: InteropTypeAdapter?): InteropTypeAdapter? {
        // !nullable primitive checked by wasm signature
        if (isPrimitive) return valueAdapter

        // !nullable reference should be null checked
        // notNullAdapter((undefined -> null)!!)
        val nullCheckedValueAdapter = valueAdapter?.let(::CheckNotNullAndAdapter)
            ?: CheckNotNullNoAdapter(notNullType)

        // kotlin types could not take undefined value so just take null-checked value
        if (!isExternalType(notNullType)) return nullCheckedValueAdapter

        // js value should convert undefined into null and the null-checked
        return CombineAdapter(
            outerAdapter = nullCheckedValueAdapter,
            innerAdapter = FunctionBasedAdapter(adapters.jsCheckIsNullOrUndefinedAdapter.owner)
        )
    }

    private fun IrType.jsToKotlinAdapterIfNeeded(isReturn: Boolean): InteropTypeAdapter? {
        if (isReturn && this == builtIns.unitType)
            return null

        val notNullType = makeNotNull()
        val valueAdapter = notNullType.jsToKotlinAdapterIfNeededNotNullable(isReturn)
        val isPrimitive = valueAdapter?.fromType?.isPrimitiveType() ?: notNullType.isPrimitiveType()

        return if (isNullable())
            createNullableAdapter(notNullType, isPrimitive, valueAdapter)
        else
            createNotNullAdapter(notNullType, isPrimitive, valueAdapter)
    }

    private fun IrType.jsToKotlinAdapterIfNeededNotNullable(isReturn: Boolean): InteropTypeAdapter? {
        if (isReturn && (this == builtIns.unitType || this == builtIns.nothingType))
            return null

        when (this) {
            builtIns.stringType -> return FunctionBasedAdapter(adapters.jsToKotlinStringAdapter.owner)
            builtIns.anyType -> return FunctionBasedAdapter(adapters.jsToKotlinAnyAdapter.owner)
            builtIns.byteType -> return FunctionBasedAdapter(adapters.jsToKotlinByteAdapter.owner)
            builtIns.shortType -> return FunctionBasedAdapter(adapters.jsToKotlinShortAdapter.owner)
            builtIns.charType -> return FunctionBasedAdapter(adapters.jsToKotlinCharAdapter.owner)

            builtIns.booleanType,
            builtIns.intType,
            builtIns.longType,
            builtIns.floatType,
            builtIns.doubleType,
            context.wasmSymbols.voidType ->
                return null
        }

        if (isExternalType(this))
            return null

        if (isBuiltInWasmRefType(this))
            return null

        if (this is IrSimpleType && this.isFunction()) {
            val functionTypeInfo = FunctionTypeInfo(this, toJs = false)

            // JavaScript's closures are external references that cannot be called directly in WebAssembly.
            // Thus, we import helper "caller" method that WebAssembly will use to call JS closures:
            //
            //     @JsFun("(f, p0, p1, ...) => f(p0, p1, ...)")
            //     external fun __callJsClosure_(f: ExternalRef, p0: JsType1, p1: JsType2, ...): JsResType
            //
            val jsClosureCaller = context.jsClosureCallers.getOrPut(functionTypeInfo.signatureString) {
                createJsClosureCaller(functionTypeInfo)
            }

            // Converter functions creates new Kotlin closure that delegate to JS closure
            // using above-mentioned "caller" import:
            //
            //     fun __convertJsClosureToKotlinClosure_(f: ExternalRef) : FunctionN =
            //       { p0: KotlinType1, p1: KotlinType2, ... ->
            //          adapt(__callJsClosure_(f, adapt(p0), adapt(p1), ..))
            //       }
            //
            val jsToKotlinClosure = context.jsToKotlinClosures.getOrPut(functionTypeInfo.signatureString) {
                createJsToKotlinClosureConverter(functionTypeInfo, jsClosureCaller)
            }
            return FunctionBasedAdapter(jsToKotlinClosure)
        }

        return ReceivingKotlinObjectFromJsAdapter(this)
    }

    private fun createKotlinClosureCaller(info: FunctionTypeInfo): IrSimpleFunction {
        val result = context.irFactory.buildFun {
            name = Name.identifier("__callFunction_${info.signatureString}")
            returnType = info.adaptedResultType
        }
        result.parent = currentParent
        result.addValueParameter {
            name = Name.identifier("f")
            type = context.wasmSymbols.wasmStructRefType
        }
        var count = 0
        info.adaptedParameterTypes.forEach { type ->
            result.addValueParameter {
                this.name = Name.identifier("p" + count++.toString())
                this.type = type
            }
        }
        val builder = context.createIrBuilder(result.symbol)

        result.body = builder.irBlockBody {
            val invokeFun = info.functionType.classOrNull!!.owner.functions.single { it.name == Name.identifier("invoke") }
            val callInvoke = irCall(invokeFun.symbol, info.originalResultType).also { call ->
                call.dispatchReceiver =
                    ReceivingKotlinObjectFromJsAdapter(invokeFun.dispatchReceiverParameter!!.type)
                        .adapt(irGet(result.valueParameters[0]), builder)

                for (i in info.adaptedParameterTypes.indices) {
                    call.putValueArgument(i, info.parametersAdapters[i].adaptIfNeeded(irGet(result.valueParameters[i + 1]), builder))
                }
            }
            +irReturn(info.resultAdapter.adaptIfNeeded(callInvoke, builder))
        }

        // TODO find out a better way to export the such declarations only when it's required. Also, fix building roots for DCE, then.
        result.annotations += builder.irCallConstructor(jsRelatedSymbols.jsExportConstructor, typeArguments = emptyList())
        additionalDeclarations += result
        return result
    }

    private fun createKotlinToJsClosureConvertor(info: FunctionTypeInfo): IrSimpleFunction {
        val result = context.irFactory.buildFun {
            name = Name.identifier("__convertKotlinClosureToJsClosure_${info.signatureString}")
            returnType = jsRelatedSymbols.jsAnyType
            isExternal = true
        }
        result.parent = currentParent
        result.addValueParameter {
            name = Name.identifier("f")
            type = context.wasmSymbols.wasmStructRefType
        }
        val builder = context.createIrBuilder(result.symbol)
        // TODO: Cache created JS closures
        val arity = info.parametersAdapters.size
        val jsCode = buildString {
            append("(f) => (")
            appendParameterList(arity)
            append(") => wasmExports[")
            append("__callFunction_${info.signatureString}".toJsStringLiteral())
            append("](f, ")
            appendParameterList(arity)
            append(")")
        }

        result.annotations += builder.irCallConstructor(jsRelatedSymbols.jsFunConstructor, typeArguments = emptyList()).also {
            it.putValueArgument(0, builder.irString(jsCode))
        }

        additionalDeclarations += result
        return result
    }

    private fun createJsToKotlinClosureConverter(
        info: FunctionTypeInfo,
        jsClosureCaller: IrSimpleFunction,
    ): IrSimpleFunction {
        val functionType = info.functionType
        val result = context.irFactory.buildFun {
            name = Name.identifier("__convertJsClosureToKotlinClosure_${info.signatureString}")
            returnType = functionType
        }
        result.parent = currentParent
        result.addValueParameter {
            name = Name.identifier("f")
            type = jsRelatedSymbols.jsAnyType
        }

        val closureClass = context.irFactory.buildClass {
            name = Name.identifier("__JsClosureToKotlinClosure_${info.signatureString}")
        }.apply {
            createImplicitParameterDeclarationWithWrappedDescriptor()
            superTypes = listOf(functionType)
            parent = currentParent
        }

        val closureClassField = closureClass.addField {
            name = Name.identifier("jsClosure")
            type = jsRelatedSymbols.jsAnyType
            visibility = DescriptorVisibilities.PRIVATE
            isFinal = true
        }

        val closureClassConstructor = closureClass.addConstructor {
            isPrimary = true
        }.apply {
            val parameter = addValueParameter {
                name = closureClassField.name
                type = closureClassField.type
            }
            body = context.createIrBuilder(symbol).irBlockBody(startOffset, endOffset) {
                +irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single())
                +irSetField(irGet(closureClass.thisReceiver!!), closureClassField, irGet(parameter))
                +IrInstanceInitializerCallImpl(startOffset, endOffset, closureClass.symbol, context.irBuiltIns.unitType)
            }
        }

        closureClass.addFunction {
            name = Name.identifier("invoke")
            returnType = info.originalResultType
        }.apply {
            addDispatchReceiver { type = closureClass.defaultType }
            info.originalParameterTypes.forEachIndexed { index, irType ->
                addValueParameter {
                    name = Name.identifier("p$index")
                    type = irType
                }
            }
            val lambdaBuilder = context.createIrBuilder(symbol)
            body = lambdaBuilder.irBlockBody {
                val jsClosureCallerCall = irCall(jsClosureCaller)
                jsClosureCallerCall.putValueArgument(0, irGetField(irGet(dispatchReceiverParameter!!), closureClassField))
                for ((adapterIndex, paramAdapter) in info.parametersAdapters.withIndex()) {
                    jsClosureCallerCall.putValueArgument(
                        adapterIndex + 1,
                        paramAdapter.adaptIfNeeded(
                            irGet(valueParameters[adapterIndex]),
                            lambdaBuilder
                        )
                    )
                }
                +irReturn(info.resultAdapter.adaptIfNeeded(jsClosureCallerCall, lambdaBuilder))
            }

            overriddenSymbols =
                overriddenSymbols + functionType.classOrNull!!.functions.single { it.owner.name == Name.identifier("invoke") }
        }

        val builder = context.createIrBuilder(result.symbol)
        result.body = builder.irBlockBody {
            +irReturn(irCall(closureClassConstructor).also { it.putValueArgument(0, irGet(result.valueParameters[0])) })
        }

        additionalDeclarations += closureClass
        additionalDeclarations += result
        return result
    }

    private fun createJsClosureCaller(info: FunctionTypeInfo): IrSimpleFunction {
        val result = context.irFactory.buildFun {
            name = Name.identifier("__callJsClosure_${info.signatureString}")
            returnType = info.adaptedResultType
            isExternal = true
        }
        result.parent = currentParent
        result.addValueParameter {
            name = Name.identifier("f")
            type = jsRelatedSymbols.jsAnyType
        }
        val arity = info.adaptedParameterTypes.size
        repeat(arity) { paramIndex ->
            result.addValueParameter {
                name = Name.identifier("p$paramIndex")
                type = info.adaptedParameterTypes[paramIndex]
            }
        }
        val builder = context.createIrBuilder(result.symbol)
        val jsFun = buildString {
            append("(f, ")
            appendParameterList(arity)
            append(") => f(")
            appendParameterList(arity)
            append(")")
        }

        result.annotations += builder.irCallConstructor(jsRelatedSymbols.jsFunConstructor, typeArguments = emptyList()).also {
            it.putValueArgument(0, builder.irString(jsFun))
        }

        additionalDeclarations += result
        return result
    }

    inner class FunctionTypeInfo(val functionType: IrSimpleType, toJs: Boolean) {
        init {
            require(functionType.arguments.all { it is IrTypeProjection }) {
                "Star projection is not supported in function type interop ${functionType.render()}"
            }
        }

        val originalParameterTypes: List =
            functionType.arguments.dropLast(1).map { (it as IrTypeProjection).type }

        val originalResultType: IrType =
            (functionType.arguments.last() as IrTypeProjection).type

        val parametersAdapters: List =
            originalParameterTypes.map { parameterType ->
                if (toJs)
                    parameterType.jsToKotlinAdapterIfNeeded(isReturn = false)
                else
                    parameterType.kotlinToJsAdapterIfNeeded(isReturn = false)
            }

        val resultAdapter: InteropTypeAdapter? =
            if (toJs)
                originalResultType.kotlinToJsAdapterIfNeeded(isReturn = true)
            else
                originalResultType.jsToKotlinAdapterIfNeeded(isReturn = true)

        val adaptedParameterTypes: List =
            originalParameterTypes.zip(parametersAdapters).map { (parameterType, adapter) ->
                (if (toJs) adapter?.fromType else adapter?.toType) ?: parameterType
            }

        val adaptedResultType: IrType =
            (if (toJs) resultAdapter?.toType else resultAdapter?.fromType) ?: originalResultType

        val signatureString: String = jsInteropNotNullTypeSignature(this)
    }

    private fun jsInteropNotNullTypeSignature(type: JsInteropFunctionsLowering.FunctionTypeInfo): String {
        val parameterTypes = type.originalParameterTypes.joinToString(separator = ",") { jsInteropTypeSignature(it) }
        val resultType = jsInteropTypeSignature(type.originalResultType)
        return "(($parameterTypes)->$resultType)"
    }

    private fun jsInteropNotNullTypeSignature(type: IrType): String {
        if (isExternalType(type)) {
            return "Js"
        }
        require(type is IrSimpleType)
        if (type.isFunction()) {
            return jsInteropNotNullTypeSignature(FunctionTypeInfo(type, true))
        }
        val klass = type.classOrNull ?: error("Unsupported JS interop type: ${type.render()}")
        if (klass.owner.packageFqName == FqName("kotlin")) {
            return klass.owner.name.identifier
        }
        error("Unsupported JS interop type: ${type.render()}")
    }

    private fun jsInteropTypeSignature(type: IrType): String {
        return if (type.isNullable()) {
            jsInteropNotNullTypeSignature(type.makeNotNull()) + "?"
        } else {
            jsInteropNotNullTypeSignature(type)
        }
    }

    interface InteropTypeAdapter {
        val fromType: IrType
        val toType: IrType
        fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression
    }

    fun InteropTypeAdapter?.adaptIfNeeded(expression: IrExpression, builder: IrBuilderWithScope): IrExpression =
        this?.adapt(expression, builder) ?: expression

    /**
     * Adapter implemented as a single function call
     */
    class FunctionBasedAdapter(
        private val function: IrSimpleFunction,
    ) : InteropTypeAdapter {
        override val fromType = function.valueParameters[0].type
        override val toType = function.returnType
        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            val call = builder.irCall(function)
            call.putValueArgument(0, expression)
            return call
        }
    }

    class CombineAdapter(
        private val outerAdapter: InteropTypeAdapter,
        private val innerAdapter: InteropTypeAdapter,
    ) : InteropTypeAdapter {
        override val fromType = innerAdapter.fromType
        override val toType = outerAdapter.toType
        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            return outerAdapter.adapt(innerAdapter.adapt(expression, builder), builder)
        }
    }

    /**
     * Current V8 Wasm GC mandates structref type instead of structs and arrays
     */
    inner class SendKotlinObjectToJsAdapter(
        override val fromType: IrType
    ) : InteropTypeAdapter {
        override val toType: IrType = context.wasmSymbols.wasmStructRefType
        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            return builder.irReinterpretCast(expression, toType)
        }
    }

    /**
     * Current V8 Wasm GC mandates structref type instead of structs and arrays
     */
    inner class ReceivingKotlinObjectFromJsAdapter(
        override val toType: IrType
    ) : InteropTypeAdapter {
        override val fromType: IrType = context.wasmSymbols.wasmStructRefType
        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            val call = builder.irCall(context.wasmSymbols.refCastNull)
            call.putValueArgument(0, expression)
            call.putTypeArgument(0, toType)
            return call
        }
    }

    /**
     * Current V8 Wasm GC mandates structref type instead of structs and arrays
     */

    /**
     * Effectively `value!!`
     */
    inner class CheckNotNullNoAdapter(type: IrType) : InteropTypeAdapter {
        override val fromType: IrType = type.makeNullable()
        override val toType: IrType = type.makeNotNull()
        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            return builder.irComposite {
                val tmp = irTemporary(expression)
                +irIfNull(
                    type = toType,
                    subject = irGet(tmp),
                    thenPart = builder.irCall(symbols.throwNullPointerException),
                    elsePart = irGet(tmp)
                )
            }
        }
    }

    /**
     * Effectively `value?.let { adapter(it) }`
     */
    inner class NullOrAdapter(
        private val adapter: InteropTypeAdapter
    ) : InteropTypeAdapter {
        override val fromType: IrType = adapter.fromType.makeNullable()
        override val toType: IrType = adapter.toType.makeNullable()
        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            return builder.irComposite {
                val tmp = irTemporary(expression)
                +irIfNull(
                    type = toType,
                    subject = irGet(tmp),
                    thenPart = irNull(toType),
                    elsePart = irImplicitCast(adapter.adapt(irGet(tmp), builder), toType)
                )
            }
        }
    }

    /**
     * Effectively `adapter(value!!)`
     */
    inner class CheckNotNullAndAdapter(
        private val adapter: InteropTypeAdapter
    ) : InteropTypeAdapter {
        override val fromType: IrType = adapter.fromType.makeNullable()
        override val toType: IrType = adapter.toType
        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            return builder.irComposite {
                val temp = irTemporary(expression)
                +irIfNull(
                    type = toType,
                    subject = irGet(temp),
                    thenPart = irCall(this@JsInteropFunctionsLowering.context.wasmSymbols.throwNullPointerException),
                    elsePart = adapter.adapt(irImplicitCast(irGet(temp), adapter.fromType.makeNotNull()), builder),
                )
            }
        }
    }

    /**
     * Vararg parameter adapter
     */
    inner class CopyToJsArrayAdapter(
        override val fromType: IrType,
        private val fromElementType: IrType,
    ) : InteropTypeAdapter {
        override val toType: IrType =
            jsRelatedSymbols.jsAnyType

        private val elementAdapter =
            primitivesToExternRefAdapters[fromElementType]
                ?: fromElementType.kotlinToJsAdapterIfNeeded(false)

        private val arrayClass = fromType.classOrNull!!
        private val getMethod = arrayClass.getSimpleFunction("get")!!.owner
        private val sizeMethod = arrayClass.getPropertyGetter("size")!!.owner

        override fun adapt(expression: IrExpression, builder: IrBuilderWithScope): IrExpression {
            return builder.irComposite {
                val originalArrayVar = irTemporary(expression)

                //  val newJsArray = []
                //  var index = 0
                //  while(index != size) {
                //      newJsArray.push(adapt(originalArray[index]));
                //      index++
                //  }
                val newJsArrayVar = irTemporary(irCall(jsRelatedSymbols.newJsArray))
                val indexVar = irTemporary(irInt(0), isMutable = true)
                val arraySizeVar = irTemporary(irCall(sizeMethod).apply { dispatchReceiver = irGet(originalArrayVar) })

                +irWhile().apply {
                    condition = irNotEquals(irGet(indexVar), irGet(arraySizeVar))
                    body = irBlock {
                        val adaptedValue = elementAdapter.adaptIfNeeded(
                            irImplicitCast(
                                irCall(getMethod).apply {
                                    dispatchReceiver = irGet(originalArrayVar)
                                    putValueArgument(0, irGet(indexVar))
                                },
                                fromElementType
                            ),
                            this@irBlock
                        )
                        +irCall(jsRelatedSymbols.jsArrayPush).apply {
                            putValueArgument(0, irGet(newJsArrayVar))
                            putValueArgument(1, adaptedValue)
                        }
                        val inc = indexVar.type.getClass()!!.functions.single { it.name == OperatorNameConventions.INC }
                        +irSet(
                            indexVar,
                            irCallOp(inc.symbol, indexVar.type, irGet(indexVar)),
                            origin = IrStatementOrigin.PREFIX_INCR
                        )
                    }
                }
                +irGet(newJsArrayVar)
            }
        }
    }
}

internal fun StringBuilder.appendParameterList(size: Int, name: String = "p", isEnd: Boolean = true) =
    repeat(size) {
        append(name)
        append(it)
        if (!isEnd || it + 1 < size)
            append(", ")
    }

/**
 * Redirect calls to external and @JsExport functions to created wrappers
 */
class JsInteropFunctionCallsLowering(val context: WasmBackendContext) : BodyLoweringPass {
    override fun lower(irBody: IrBody, container: IrDeclaration) {
        if (context.configuration.get(JSConfigurationKeys.WASM_TARGET, WasmTarget.JS) == WasmTarget.WASI) return
        irBody.transformChildrenVoid(object : IrElementTransformerVoid() {
            override fun visitCall(expression: IrCall): IrExpression {
                expression.transformChildrenVoid()
                val newFun: IrSimpleFunction? = context.mapping.wasmJsInteropFunctionToWrapper[expression.symbol.owner]
                return if (newFun != null && container != newFun) {
                    irCall(expression, newFun)
                } else {
                    expression
                }
            }
        })
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy