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 index dbdd0746e22..c1dd2aee108 100644 --- 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 @@ -7,7 +7,6 @@ package org.jetbrains.kotlin.ir.backend.js.lower import org.jetbrains.kotlin.backend.common.* import org.jetbrains.kotlin.backend.common.lower.createIrBuilder -import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET @@ -132,41 +131,23 @@ private class JsCodeOutlineTransformer( if (expression.symbol != backendContext.intrinsics.jsCode) return null - val jsCodeArg = expression.getValueArgument(0) - ?: compilationException( - "Expected js code string", - expression - ) + val jsCodeArg = expression.getValueArgument(0) ?: compilationException("Expected js code string", expression) val jsStatements = translateJsCodeIntoStatementList(jsCodeArg, backendContext) ?: return null // 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) - } + val scope = JsScopesCollector().apply { acceptList(jsStatements) } + val localsUsageCollector = KotlinLocalsUsageCollector(scope, ::findValueDeclarationWithName).apply { acceptList(jsStatements) } + val kotlinLocalsUsedInJs = localsUsageCollector.usedLocals + 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 - origin = OUTLINED_ORIGIN + isExternal = true + origin = JsIrBackendContext.callableClosureOrigin } // 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) @@ -219,8 +200,78 @@ private class JsCodeOutlineTransformer( } } } +} - companion object { - object OUTLINED_ORIGIN : IrDeclarationOriginImpl("OUTLINED_ORIGIN") +class JsScopesCollector : RecursiveJsVisitor() { + private val functionsStack = mutableListOf(Scope(null)) + private val functionalScopes = mutableMapOf(null to functionsStack.first()) + + private class Scope(val parent: Scope?) { + private val variables = hashSetOf() + + fun add(variableName: String) { + variables.add(variableName) + } + + fun variableWithNameExists(variableName: String): Boolean { + return variables.contains(variableName) || + parent?.variableWithNameExists(variableName) == true + } } -} \ No newline at end of file + + override fun visitVars(x: JsVars) { + super.visitVars(x) + val currentScope = functionsStack.last() + x.vars.forEach { currentScope.add(it.name.ident) } + } + + override fun visitFunction(x: JsFunction) { + val parentScope = functionsStack.last() + val newScope = Scope(parentScope).apply { + val name = x.name?.ident + if (name != null) add(name) + x.parameters.forEach { add(it.name.ident) } + } + functionsStack.push(newScope) + functionalScopes[x] = newScope + super.visitFunction(x) + functionsStack.pop() + } + + fun varWithNameExistsInScopeOf(function: JsFunction?, variableName: String): Boolean { + return functionalScopes[function]!!.variableWithNameExists(variableName) + } +} + +private class KotlinLocalsUsageCollector( + private val scopeInfo: JsScopesCollector, + private val findValueDeclarationWithName: (String) -> IrValueDeclaration? +) : RecursiveJsVisitor() { + private val functionStack = mutableListOf(null) + private val processedNames = mutableSetOf() + private val kotlinLocalsUsedInJs = mutableListOf() + + val usedLocals: List + get() = kotlinLocalsUsedInJs + + override fun visitFunction(x: JsFunction) { + functionStack.push(x) + super.visitFunction(x) + functionStack.pop() + } + + override fun visitNameRef(nameRef: JsNameRef) { + super.visitNameRef(nameRef) + val name = nameRef.name.takeIf { nameRef.qualifier == null } ?: return + // 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. + // Keeping track of processed names to avoid registering them multiple times + if (processedNames.add(name.ident) && !name.isDeclaredInsideJsCode()) { + kotlinLocalsUsedInJs.addIfNotNull(findValueDeclarationWithName(name.ident)) + } + } + + private fun JsName.isDeclaredInsideJsCode(): Boolean { + return scopeInfo.varWithNameExistsInScopeOf(functionStack.peek(), ident) + } +} diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/FunctionWithJsFuncAnnotationInliner.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/FunctionWithJsFuncAnnotationInliner.kt new file mode 100644 index 00000000000..4874a430675 --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/FunctionWithJsFuncAnnotationInliner.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2010-2022 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.transformers.irToJs + +import org.jetbrains.kotlin.backend.common.compilationException +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.backend.js.utils.JsGenerationContext +import org.jetbrains.kotlin.ir.backend.js.utils.getJsFunAnnotation +import org.jetbrains.kotlin.js.backend.ast.* + +class FunctionWithJsFuncAnnotationInliner(private val jsFuncCall: IrCall, private val context: JsGenerationContext) { + private val function = getJsFunctionImplementation() + private val replacements = collectReplacementsForCall() + + fun generateResultStatement(): List { + return function.body.statements + .run { + SimpleJsCodeInliner(replacements) + .apply { acceptList(this@run) } + .withTemporaryVariablesForExpressions(this) + } + } + + private fun getJsFunctionImplementation(): JsFunction { + val code = jsFuncCall.symbol.owner.getJsFunAnnotation() ?: compilationException("JsFun annotation is expected", jsFuncCall) + val statements = parseJsCode(code) ?: compilationException("Cannot compute js code", jsFuncCall) + return statements.singleOrNull() + ?.let { it as? JsExpressionStatement } + ?.let { it.expression as? JsFunction } ?: compilationException("Provided js code is not a js function", jsFuncCall.symbol.owner) + } + + private fun collectReplacementsForCall(): Map { + val translatedArguments = Array(jsFuncCall.valueArgumentsCount) { + jsFuncCall.getValueArgument(it)!!.accept(IrElementToJsExpressionTransformer(), context) + } + return function.parameters + .mapIndexed { i, param -> param.name to translatedArguments[i] } + .toMap() + } +} + +private class SimpleJsCodeInliner(private val replacements: Map): RecursiveJsVisitor() { + private val temporaryNamesForExpressions = mutableMapOf() + + fun withTemporaryVariablesForExpressions(statements: List): List { + if (temporaryNamesForExpressions.isEmpty()) { + return statements + } + + val variableDeclarations = temporaryNamesForExpressions.map { JsVars(JsVars.JsVar(it.key, it.value)) } + return variableDeclarations + statements + } + + override fun visitNameRef(nameRef: JsNameRef) { + super.visitNameRef(nameRef) + if (nameRef.qualifier != null) return + nameRef.name = nameRef.name?.getReplacement() ?: return + } + + private fun JsName.declareNewTemporaryFor(expression: JsExpression): JsName { + return JsName(ident, true) + .also { temporaryNamesForExpressions[it] = expression } + } + + private fun JsName.getReplacement(): JsName? { + val expression = replacements[this] ?: return null + return when { + expression is JsNameRef && expression.qualifier == null -> expression.name!! + else -> declareNewTemporaryFor(expression) + } + } +} \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsExpressionTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsExpressionTransformer.kt index 086c7027483..617a818e914 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsExpressionTransformer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsExpressionTransformer.kt @@ -221,41 +221,8 @@ class IrElementToJsExpressionTransformer : BaseIrElementToJsNodeTransformer { - } - is JsExpressionStatement -> { - newStatements[statements.lastIndex] = JsReturn(lastStatement.expression) - } - // TODO: report warning or even error - else -> newStatements += JsReturn(JsPrefixOperation(JsUnaryOperator.VOID, JsIntLiteral(3))) - } - - val syntheticFunction = JsFunction(emptyScope, JsBlock(newStatements), "") - return JsInvocation(syntheticFunction).withSource(expression, context) - + if (context.checkIfJsCode(expression.symbol) || context.checkIfAnnotatedWithJsFunc(expression.symbol)) { + return JsCallTransformer(expression, context).generateExpression() } return translateCall(expression, context, this).withSource(expression, context) .also { diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsStatementTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsStatementTransformer.kt index 1ea96b78956..d4aa56c0dd9 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsStatementTransformer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrElementToJsStatementTransformer.kt @@ -135,24 +135,8 @@ class IrElementToJsStatementTransformer : BaseIrElementToJsNodeTransformer JsEmpty - 1 -> statements.single().withSource(expression, data) - // TODO: use transparent block (e.g. JsCompositeBlock) - else -> JsBlock(statements) - } + if (data.checkIfJsCode(expression.symbol) || data.checkIfAnnotatedWithJsFunc(expression.symbol)) { + return JsCallTransformer(expression, data).generateStatement() } return translateCall(expression, data, IrElementToJsExpressionTransformer()).withSource(expression, data).makeStmt() .also { data.staticContext.polyfills.visitDeclaration(expression.symbol.owner) } diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsCallTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsCallTransformer.kt new file mode 100644 index 00000000000..920ff1da4a4 --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsCallTransformer.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2010-2022 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.transformers.irToJs + +import org.jetbrains.kotlin.backend.common.compilationException +import org.jetbrains.kotlin.ir.backend.js.utils.JsGenerationContext +import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.js.backend.ast.* + +class JsCallTransformer(private val jsOrJsFuncCall: IrCall, private val context: JsGenerationContext) { + private val statements = getJsStatements() + + fun generateStatement(): JsStatement { + if (statements.isEmpty()) return JsEmpty + + val newStatements = statements.toMutableList().apply { + val expression = (last() as? JsReturn)?.expression ?: return@apply + + if (expression is JsPrefixOperation && expression.operator == JsUnaryOperator.VOID) { + removeLastOrNull() + } else { + set(lastIndex, expression.makeStmt()) + } + } + + return when (newStatements.size) { + 0 -> JsEmpty + 1 -> newStatements.single().withSource(jsOrJsFuncCall, context) + // TODO: use transparent block (e.g. JsCompositeBlock) + else -> JsBlock(newStatements) + } + } + + fun generateExpression(): JsExpression { + if (statements.isEmpty()) return JsPrefixOperation(JsUnaryOperator.VOID, JsIntLiteral(3)) // TODO: report warning or even error + + val lastStatement = statements.last() + val lastExpression = when (lastStatement) { + is JsReturn -> lastStatement.expression + is JsExpressionStatement -> lastStatement.expression + else -> null + } + if (statements.size == 1 && lastExpression != null) { + return lastExpression.withSource(jsOrJsFuncCall, context) + } + + val newStatements = statements.toMutableList() + + when (lastStatement) { + is JsReturn -> { + } + is JsExpressionStatement -> { + newStatements[statements.lastIndex] = JsReturn(lastStatement.expression) + } + // TODO: report warning or even error + else -> newStatements += JsReturn(JsPrefixOperation(JsUnaryOperator.VOID, JsIntLiteral(3))) + } + + val syntheticFunction = JsFunction(emptyScope, JsBlock(newStatements), "") + return JsInvocation(syntheticFunction).withSource(jsOrJsFuncCall, context) + } + + private fun getJsStatements(): List { + return when { + context.checkIfJsCode(jsOrJsFuncCall.symbol) -> { + translateJsCodeIntoStatementList( + jsOrJsFuncCall.getValueArgument(0) ?: compilationException("JsCode is expected", jsOrJsFuncCall), + context.staticContext.backendContext + ) + ?: compilationException("Cannot compute js code", jsOrJsFuncCall) + } + + context.checkIfAnnotatedWithJsFunc(jsOrJsFuncCall.symbol) -> + FunctionWithJsFuncAnnotationInliner(jsOrJsFuncCall, context).generateResultStatement() + + else -> compilationException("`js` function call or function with @JsFunc annotation expected", jsOrJsFuncCall) + } + } +} \ No newline at end of file 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 ef87cc54670..bd316cacdc9 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 @@ -61,6 +61,8 @@ fun IrAnnotationContainer.isJsNativeSetter(): Boolean = hasAnnotation(JsAnnotati fun IrAnnotationContainer.isJsNativeInvoke(): Boolean = hasAnnotation(JsAnnotations.jsNativeInvoke) +fun IrAnnotationContainer.isAnnotatedWithJsFun(): Boolean = hasAnnotation(JsAnnotations.jsFunFqn) + fun IrDeclarationWithName.getJsNameForOverriddenDeclaration(): String? { val jsName = getJsName() diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/JsGenerationContext.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/JsGenerationContext.kt index bc4e1bd407f..6661edfec5b 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/JsGenerationContext.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/utils/JsGenerationContext.kt @@ -77,4 +77,6 @@ class JsGenerationContext( } fun checkIfJsCode(symbol: IrFunctionSymbol): Boolean = symbol == staticContext.backendContext.intrinsics.jsCode + + fun checkIfAnnotatedWithJsFunc(symbol: IrFunctionSymbol): Boolean = symbol.owner.isAnnotatedWithJsFun() } diff --git a/js/js.translator/testData/box/jsCode/break.kt b/js/js.translator/testData/box/jsCode/break.kt index 701e8689e31..a2e99e47b86 100644 --- a/js/js.translator/testData/box/jsCode/break.kt +++ b/js/js.translator/testData/box/jsCode/break.kt @@ -1,4 +1,3 @@ -// IGNORE_BACKEND: JS_IR // EXPECTED_REACHABLE_NODES: 1282 package foo diff --git a/js/js.translator/testData/box/jsCode/codeFromVariable.kt b/js/js.translator/testData/box/jsCode/codeFromVariable.kt index 583ca567bb0..db2c2ecf68e 100644 --- a/js/js.translator/testData/box/jsCode/codeFromVariable.kt +++ b/js/js.translator/testData/box/jsCode/codeFromVariable.kt @@ -1,5 +1,3 @@ -// IGNORE_BACKEND: JS_IR -// IGNORE_BACKEND: JS_IR_ES6 // 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 bef5cdb7aec..9613a3b282d 100644 --- a/js/js.translator/testData/box/jsCode/continue.kt +++ b/js/js.translator/testData/box/jsCode/continue.kt @@ -1,5 +1,4 @@ // 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 f2cfcb6d4a8..e93df0cf9f6 100644 --- a/js/js.translator/testData/box/jsCode/label.kt +++ b/js/js.translator/testData/box/jsCode/label.kt @@ -1,4 +1,3 @@ -// 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 d271ac2f6f0..4602c4de8b1 100644 --- a/js/js.translator/testData/box/jsCode/labelSiblingClash.kt +++ b/js/js.translator/testData/box/jsCode/labelSiblingClash.kt @@ -1,5 +1,4 @@ // 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/referenceToKotlin.kt b/js/js.translator/testData/box/jsCode/referenceToKotlin.kt index 8d9f06d04ce..c4de98fc3c1 100644 --- a/js/js.translator/testData/box/jsCode/referenceToKotlin.kt +++ b/js/js.translator/testData/box/jsCode/referenceToKotlin.kt @@ -1,5 +1,3 @@ -// IGNORE_BACKEND: JS_IR -// IGNORE_BACKEND: JS_IR_ES6 // KJS_WITH_FULL_RUNTIME // EXPECTED_REACHABLE_NODES: 1687 external fun p(m: String): String @@ -26,9 +24,15 @@ fun test4(): String { return js("p('test4')") } +fun f() = js("p('test5')") + fun test5(): String { val p = "wrong5" - fun f() = js("p('test5')") + // The behavoiur of the classical backend is weird and buggy + // From the user side, the local variable `p` is captured + // but we have different behaviour because the renaming phase in classical backend + // will be invoked after the lambda will be moved up + // fun f() = js("p('test5')") return f() }