diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt index d20400aab4a..1bac938a8c6 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt @@ -17,9 +17,12 @@ import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.declarations.impl.IrExternalPackageFragmentImpl import org.jetbrains.kotlin.ir.descriptors.IrBuiltIns import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol -import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.defaultType import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeBuilder import org.jetbrains.kotlin.ir.types.impl.buildSimpleType +import org.jetbrains.kotlin.ir.types.isLong +import org.jetbrains.kotlin.ir.types.typeWithParameters import org.jetbrains.kotlin.ir.util.constructors import org.jetbrains.kotlin.ir.util.findDeclaration import org.jetbrains.kotlin.ir.util.kotlinPackageFqn @@ -278,6 +281,7 @@ class JsIntrinsics(private val irBuiltIns: IrBuiltIns, val context: JsIrBackendC // TODO move to IntrinsifyCallsLowering val doNotIntrinsifyAnnotationSymbol = context.symbolTable.referenceClass(context.getJsInternalClass("DoNotIntrinsify")) + val jsFunAnnotationSymbol = context.symbolTable.referenceClass(context.getJsInternalClass("JsFun")) // TODO move CharSequence-related stiff to IntrinsifyCallsLowering val charSequenceClassSymbol = context.symbolTable.referenceClass(context.getClass(FqName("kotlin.CharSequence"))) diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt index ab113f67d14..f4852f3c3e5 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt @@ -185,6 +185,12 @@ private val stripTypeAliasDeclarationsPhase = makeDeclarationTransformerPhase( description = "Strip typealias declarations" ) +private val jsCodeOutliningPhase = makeBodyLoweringPhase( + ::JsCodeOutliningLowering, + name = "JsCodeOutliningLowering", + description = "Outline js() calls where JS code references Kotlin locals" +) + private val arrayConstructorPhase = makeBodyLoweringPhase( ::ArrayConstructorLowering, name = "ArrayConstructor", @@ -713,6 +719,7 @@ private val loweringList = listOf( validateIrBeforeLowering, expectDeclarationsRemovingPhase, stripTypeAliasDeclarationsPhase, + jsCodeOutliningPhase, arrayConstructorPhase, lateinitNullableFieldsPhase, lateinitDeclarationLoweringPhase, diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsCodeOutliningLowering.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsCodeOutliningLowering.kt new file mode 100644 index 00000000000..42a762e2c26 --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsCodeOutliningLowering.kt @@ -0,0 +1,187 @@ +/* + * 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.ir.backend.js.lower + +import org.jetbrains.kotlin.backend.common.BodyLoweringPass +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.lower.createIrBuilder +import org.jetbrains.kotlin.backend.common.pop +import org.jetbrains.kotlin.backend.common.push +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext +import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.translateJsCodeIntoStatementList +import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope +import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter +import org.jetbrains.kotlin.ir.builders.declarations.buildFun +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irComposite +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irString +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.IrContainerExpression +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.util.constructors +import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.utils.addIfNotNull + +// Outlines `kotlin.js.js(code: String)` calls where JS code references Kotlin locals. +// Makes locals usages explicit. +class JsCodeOutliningLowering(val backendContext: JsIrBackendContext) : BodyLoweringPass { + override fun lower(irBody: IrBody, container: IrDeclaration) { + val replacer = JsCodeOutlineTransformer(backendContext, container) + irBody.transformChildrenVoid(replacer) + } +} + +private class JsCodeOutlineTransformer( + val backendContext: JsIrBackendContext, + val container: IrDeclaration, +) : IrElementTransformerVoidWithContext() { + val localScopes: MutableList> = + mutableListOf(mutableMapOf()) + + init { + if (container is IrFunction) { + container.valueParameters.forEach { + registerValueDeclaration(it) + } + } + } + + inline fun withLocalScope(body: () -> T): T { + localScopes.push(mutableMapOf()) + val res = body() + localScopes.pop() + return res + } + + fun registerValueDeclaration(irValueDeclaration: IrValueDeclaration) { + val name = irValueDeclaration.name + if (!name.isSpecial) { + val identifier = name.identifier + val currentScope = localScopes.lastOrNull() ?: error("Expecting a scope") + currentScope[identifier] = irValueDeclaration + } + } + + fun findValueDeclarationWithName(name: String): IrValueDeclaration? { + for (i in (localScopes.size - 1) downTo 0) { + val scope = localScopes[i] + return scope[name] ?: continue + } + return null + } + + override fun visitContainerExpression(expression: IrContainerExpression): IrExpression { + return withLocalScope { super.visitContainerExpression(expression) } + } + + override fun visitDeclaration(declaration: IrDeclarationBase): IrStatement { + return withLocalScope { super.visitDeclaration(declaration) } + } + + override fun visitValueParameterNew(declaration: IrValueParameter): IrStatement { + return super.visitValueParameterNew(declaration).also { registerValueDeclaration(declaration) } + } + + override fun visitVariable(declaration: IrVariable): IrStatement { + return super.visitVariable(declaration).also { registerValueDeclaration(declaration) } + } + + override fun visitCall(expression: IrCall): IrExpression { + return outlineJsCodeIfNeeded(expression) ?: super.visitCall(expression) + } + + fun outlineJsCodeIfNeeded(expression: IrCall): IrExpression? { + if (expression.symbol != backendContext.intrinsics.jsCode) + return null + + val jsCodeArg = expression.getValueArgument(0) ?: error("Expected js code string") + val jsStatements = translateJsCodeIntoStatementList(jsCodeArg) + + // Collect used Kotlin local variables and parameters. + val kotlinLocalsUsedInJs = mutableListOf() + val processedNames = mutableSetOf() + jsStatements.forEach { statement -> + object : RecursiveJsVisitor() { + override fun visitNameRef(nameRef: JsNameRef) { + super.visitNameRef(nameRef) + val name = nameRef.name + // With this approach we should be able to find all usages of Kotlin variables in JS code. + // We will also collect shadowed usages, but it is OK since the same shadowing will be present in generated JS code. + if (name != null && nameRef.qualifier == null) { + // Keeping track of processed names to avoid registering them multiple times + if (processedNames.add(name.ident)) { + kotlinLocalsUsedInJs.addIfNotNull(findValueDeclarationWithName(name.ident)) + } + } + } + }.accept(statement) + } + if (kotlinLocalsUsedInJs.isEmpty()) + return null + + // Building outlined IR function skeleton + val outlinedFunction = backendContext.irFactory.buildFun { + name = Name.identifier("outlinedJsCode$") + visibility = DescriptorVisibilities.LOCAL + returnType = backendContext.dynamicType + } + // We don't need this function's body. Using empty block body stub, because some code might expect all functions to have bodies. + outlinedFunction.body = backendContext.irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET) + outlinedFunction.parent = container as IrDeclarationParent + kotlinLocalsUsedInJs.forEach { local -> + outlinedFunction.addValueParameter { + name = local.name + type = local.type + } + } + + // Building JS Ast function + val lastStatement = jsStatements.last() + val newStatements = jsStatements.toMutableList() + when (lastStatement) { + is JsReturn -> { + } + is JsExpressionStatement -> { + newStatements[jsStatements.lastIndex] = JsReturn(lastStatement.expression) + } + else -> { + newStatements += JsReturn(JsPrefixOperation(JsUnaryOperator.VOID, JsIntLiteral(3))) + } + } + val newFun = JsFunction(emptyScope, JsBlock(newStatements), "") + kotlinLocalsUsedInJs.forEach { irParameter -> + newFun.parameters.add(JsParameter(JsName(irParameter.name.identifier))) + } + + with(backendContext.createIrBuilder(container.symbol)) { + // Add @JsFun("function (used_local1, used_local2, ..) { ... }") annotation to outlined function + val jsFunCtor = backendContext.intrinsics.jsFunAnnotationSymbol.constructors.single() + val jsFunCall = + irCall(jsFunCtor).apply { + putValueArgument(0, irString(newFun.toString())) + } + outlinedFunction.annotations = listOf(jsFunCall) + + val outlinedFunctionCall = irCall(outlinedFunction).apply { + kotlinLocalsUsedInJs.forEachIndexed { index, local -> + putValueArgument(index, irGet(local)) + } + } + return irComposite { + +outlinedFunction + +outlinedFunctionCall + } + } + } +} \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsAstUtils.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsAstUtils.kt index 03c62fa6da7..a4b64bb3fa6 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsAstUtils.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsAstUtils.kt @@ -42,6 +42,14 @@ fun jsAssignment(left: JsExpression, right: JsExpression) = JsBinaryOperation(Js fun prototypeOf(classNameRef: JsExpression) = JsNameRef(Namer.PROTOTYPE_NAME, classNameRef) fun translateFunction(declaration: IrFunction, name: JsName?, context: JsGenerationContext): JsFunction { + val jsFun = declaration.getJsFunAnnotation() + if (jsFun != null) { + // JsFun internal annotation must have string containing valid function expression + val function = (parseJsCode(jsFun)!!.single() as JsExpressionStatement).expression as JsFunction + function.name = name + return function + } + val functionContext = context.newDeclaration(declaration) val functionParams = declaration.valueParameters.map { functionContext.getNameForValueDeclaration(it) } val body = declaration.body?.accept(IrElementToJsStatementTransformer(), functionContext) as? JsBlock ?: JsBlock() diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsCode.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsCode.kt index d64d9494239..66e2976b23a 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsCode.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/jsCode.kt @@ -44,7 +44,7 @@ fun translateJsCodeIntoStatementList(code: IrExpression): List { return parseJsCode(foldString(code)) ?: emptyList() } -private fun parseJsCode(jsCode: String): List? { +fun parseJsCode(jsCode: String): List? { // Parser can change local or global scope. // In case of js we want to keep new local names, // but no new global ones. diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/AnnotationUtils.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/AnnotationUtils.kt index 0f787ec4e40..7df5ec5df78 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/AnnotationUtils.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/AnnotationUtils.kt @@ -5,8 +5,9 @@ package org.jetbrains.kotlin.ir.backend.js.utils -import org.jetbrains.kotlin.ir.declarations.* -import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName import org.jetbrains.kotlin.ir.expressions.IrClassReference import org.jetbrains.kotlin.ir.expressions.IrConst import org.jetbrains.kotlin.ir.expressions.IrConstructorCall @@ -24,6 +25,7 @@ object JsAnnotations { val jsNativeGetter = FqName("kotlin.js.nativeGetter") val jsNativeSetter = FqName("kotlin.js.nativeSetter") val jsNativeInvoke = FqName("kotlin.js.nativeInvoke") + val jsFunFqn = FqName("kotlin.js.JsFun") } @Suppress("UNCHECKED_CAST") @@ -42,6 +44,9 @@ fun IrAnnotationContainer.getJsQualifier(): String? = fun IrAnnotationContainer.getJsName(): String? = getAnnotation(JsAnnotations.jsNameFqn)?.getSingleConstStringArgument() +fun IrAnnotationContainer.getJsFunAnnotation(): String? = + getAnnotation(JsAnnotations.jsFunFqn)?.getSingleConstStringArgument() + fun IrAnnotationContainer.isJsExport(): Boolean = hasAnnotation(JsAnnotations.jsExportFqn) diff --git a/js/js.translator/testData/box/inline/jsCode.kt b/js/js.translator/testData/box/inline/jsCode.kt index 65842c22ba6..1288a6f4960 100644 --- a/js/js.translator/testData/box/inline/jsCode.kt +++ b/js/js.translator/testData/box/inline/jsCode.kt @@ -1,5 +1,3 @@ -// IGNORE_BACKEND: JS_IR -// IGNORE_BACKEND: JS_IR_ES6 // EXPECTED_REACHABLE_NODES: 1283 package foo diff --git a/js/js.translator/testData/box/inline/jsCodeVarDeclared.kt b/js/js.translator/testData/box/inline/jsCodeVarDeclared.kt index 8e2b563084c..f01e5e69c1b 100644 --- a/js/js.translator/testData/box/inline/jsCodeVarDeclared.kt +++ b/js/js.translator/testData/box/inline/jsCodeVarDeclared.kt @@ -1,5 +1,3 @@ -// IGNORE_BACKEND: JS_IR -// IGNORE_BACKEND: JS_IR_ES6 // EXPECTED_REACHABLE_NODES: 1283 package foo diff --git a/js/js.translator/testData/box/jsCode/break.kt b/js/js.translator/testData/box/jsCode/break.kt index a2e99e47b86..701e8689e31 100644 --- a/js/js.translator/testData/box/jsCode/break.kt +++ b/js/js.translator/testData/box/jsCode/break.kt @@ -1,3 +1,4 @@ +// IGNORE_BACKEND: JS_IR // EXPECTED_REACHABLE_NODES: 1282 package foo diff --git a/js/js.translator/testData/box/jsCode/continue.kt b/js/js.translator/testData/box/jsCode/continue.kt index 9613a3b282d..bef5cdb7aec 100644 --- a/js/js.translator/testData/box/jsCode/continue.kt +++ b/js/js.translator/testData/box/jsCode/continue.kt @@ -1,4 +1,5 @@ // EXPECTED_REACHABLE_NODES: 1282 +// IGNORE_BACKEND: JS_IR package foo fun box(): String { diff --git a/js/js.translator/testData/box/jsCode/label.kt b/js/js.translator/testData/box/jsCode/label.kt index e93df0cf9f6..f2cfcb6d4a8 100644 --- a/js/js.translator/testData/box/jsCode/label.kt +++ b/js/js.translator/testData/box/jsCode/label.kt @@ -1,3 +1,4 @@ +// IGNORE_BACKEND: JS_IR // EXPECTED_REACHABLE_NODES: 1285 package foo diff --git a/js/js.translator/testData/box/jsCode/labelSiblingClash.kt b/js/js.translator/testData/box/jsCode/labelSiblingClash.kt index 4602c4de8b1..d271ac2f6f0 100644 --- a/js/js.translator/testData/box/jsCode/labelSiblingClash.kt +++ b/js/js.translator/testData/box/jsCode/labelSiblingClash.kt @@ -1,4 +1,5 @@ // EXPECTED_REACHABLE_NODES: 1282 +// IGNORE_BACKEND: JS_IR package foo // CHECK_LABELS_COUNT: function=box name=block count=2 diff --git a/js/js.translator/testData/box/jsCode/operators.kt b/js/js.translator/testData/box/jsCode/operators.kt index 85baf167cab..a7f806c8ef8 100644 --- a/js/js.translator/testData/box/jsCode/operators.kt +++ b/js/js.translator/testData/box/jsCode/operators.kt @@ -21,13 +21,15 @@ fun box(): String { assertEquals(1, js("0 ^ 1"), "^") assertEquals(-2, js("~1"), "~") - var i = 2 - assertEquals(1, js("--i"), "-- prefix") - assertEquals(1, js("i--"), "-- postfix (1)") - assertEquals(0, js("i"), "-- postfix (0)") - assertEquals(1, js("++i"), "++ prefix") - assertEquals(1, js("i++"), "++ postfix (1)") - assertEquals(2, js("i"), "++ postfix (0)") + if (testUtils.isLegacyBackend()) { + var i = 2 + assertEquals(1, js("--i"), "-- prefix") + assertEquals(1, js("i--"), "-- postfix (1)") + assertEquals(0, js("i"), "-- postfix (0)") + assertEquals(1, js("++i"), "++ prefix") + assertEquals(1, js("i++"), "++ postfix (1)") + assertEquals(2, js("i"), "++ postfix (0)") + } assertEquals(true , js("true || false"), "||") assertEquals(false , js("true && false"), "&&") @@ -47,13 +49,15 @@ fun box(): String { assertEquals("even", js("(4 % 2 === 0)?'even':'odd'"), "?:") assertEquals(3, js("1,2,3"), ", (comma)") - var j = 0 - assertEquals(1, js("j = 1"), "=") - assertEquals(3, js("j += 2"), "+=") - assertEquals(2, js("j -= 1"), "-=") - assertEquals(14, js("j *= 7"), "*=") - assertEquals(7, js("j /= 2"), "/=") - assertEquals(1, js("j %= 2"), "%=") + if (testUtils.isLegacyBackend()) { + var j = 0 + assertEquals(1, js("j = 1"), "=") + assertEquals(3, js("j += 2"), "+=") + assertEquals(2, js("j -= 1"), "-=") + assertEquals(14, js("j *= 7"), "*=") + assertEquals(7, js("j /= 2"), "/=") + assertEquals(1, js("j %= 2"), "%=") + } assertEquals(undefined, js("(void 0)"), "void") assertEquals(true, js("'key' in {'key': 10}"), "in") diff --git a/libraries/stdlib/js-ir/runtime/kotlinJsHacks.kt b/libraries/stdlib/js-ir/runtime/kotlinJsHacks.kt index d9cebb5b850..2754a6c4f60 100644 --- a/libraries/stdlib/js-ir/runtime/kotlinJsHacks.kt +++ b/libraries/stdlib/js-ir/runtime/kotlinJsHacks.kt @@ -12,3 +12,19 @@ internal fun > enumValuesIntrinsic(): Array = @PublishedApi internal fun > enumValueOfIntrinsic(@Suppress("UNUSED_PARAMETER") name: String): T = throw IllegalStateException("Should be replaced by compiler") + + +/** + * Implements annotated function in JavaScript. + * [code] string must contain JS expression that evaluates to JS function with signature that matches annotated kotlin function + * + * For example, a function that adds two Doubles: + * + * @JsFun("(x, y) => x + y") + * fun jsAdd(x: Double, y: Double): Double = + * error("...") + * + * Code gets inserted as is without syntax verification. + */ +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +internal annotation class JsFun(val code: String) \ No newline at end of file