diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt index e431bc0ed20..e0c464b55f3 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt @@ -122,6 +122,9 @@ class K2JSCompilerArguments : CommonCompilerArguments() { @Argument(value = "-Xir-produce-js", description = "Generates JS file using IR backend. Also disables pre-IR backend") var irProduceJs: Boolean by FreezableVar(false) + @Argument(value = "-Xir-dce", description = "Perform experimental dead code elimination") + var irDce: Boolean by FreezableVar(false) + @Argument(value = "-Xir-only", description = "Disables pre-IR backend") var irOnly: Boolean by FreezableVar(false) diff --git a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt index 61b45c2e2b0..466576b5ae4 100644 --- a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt +++ b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt @@ -205,10 +205,13 @@ class K2JsIrCompiler : CLICompiler() { phaseConfig, allDependencies = resolvedLibraries, friendDependencies = friendDependencies, - mainArguments = mainCallArguments + mainArguments = mainCallArguments, + generateFullJs = !arguments.irDce, + generateDceJs = arguments.irDce ) - outputFile.writeText(compiledModule.jsCode) + val jsCode = if (arguments.irDce) compiledModule.dceJsCode!! else compiledModule.jsCode!! + outputFile.writeText(jsCode) if (arguments.generateDts) { val dtsFile = outputFile.withReplacedExtensionOrNull(outputFile.extension, "d.ts")!! dtsFile.writeText(compiledModule.tsDefinitions ?: error("No ts definitions")) diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/Dce.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/Dce.kt new file mode 100644 index 00000000000..dbacb5ea384 --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/Dce.kt @@ -0,0 +1,287 @@ +/* + * Copyright 2010-2019 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 + +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.backend.js.export.isExported +import org.jetbrains.kotlin.ir.backend.js.utils.getJsName +import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName +import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.impl.IrBlockBodyImpl +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.types.classOrNull +import org.jetbrains.kotlin.ir.types.classifierOrFail +import org.jetbrains.kotlin.ir.types.classifierOrNull +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.ir.visitors.* +import java.util.* + +fun eliminateDeadDeclarations( + module: IrModuleFragment, + context: JsIrBackendContext, + mainFunction: IrSimpleFunction? +) { + + val allRoots = buildRoots(module, context, mainFunction) + + val usefulDeclarations = usefulDeclarations(allRoots, context) + + removeUselessDeclarations(module, usefulDeclarations) +} + +private fun buildRoots(module: IrModuleFragment, context: JsIrBackendContext, mainFunction: IrSimpleFunction?): Iterable { + val rootDeclarations = + (module.files + context.packageLevelJsModules + context.externalPackageFragment.values).flatMapTo(mutableListOf()) { file -> + file.declarations.filter { + it is IrField && it.initializer != null && it.fqNameWhenAvailable?.asString()?.startsWith("kotlin") != true + || it.isExported(context) + || it.isEffectivelyExternal() + || it is IrField && it.correspondingPropertySymbol?.owner?.isExported(context) == true + || it is IrSimpleFunction && it.correspondingPropertySymbol?.owner?.isExported(context) == true + } + } + + if (context.hasTests) rootDeclarations += context.testContainer + + if (mainFunction != null) { + rootDeclarations += mainFunction + if (mainFunction.isSuspend) { + rootDeclarations += context.coroutineEmptyContinuation.owner + } + } + + return rootDeclarations +} + +private fun removeUselessDeclarations(module: IrModuleFragment, usefulDeclarations: Set) { + module.files.forEach { + it.acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitFile(declaration: IrFile) { + process(declaration) + } + + override fun visitClass(declaration: IrClass) { + process(declaration) + } + + override fun visitConstructor(declaration: IrConstructor) { + if (declaration !in usefulDeclarations) { + // Keep the constructor declaration without body in order to declare the JS constructor function + declaration.body = IrBlockBodyImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, emptyList()) + } + } + + private fun process(container: IrDeclarationContainer) { + container.declarations.transformFlat { member -> + if (member !in usefulDeclarations && member !is IrConstructor) { + emptyList() + } else { + member.acceptVoid(this) + null + } + } + } + }) + } +} + +private fun Iterable.withNested(): Iterable { + val result = mutableListOf() + + this.forEach { + it.acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitBody(body: IrBody) { + // Skip + } + + override fun visitDeclaration(declaration: IrDeclaration) { + super.visitDeclaration(declaration) + result += declaration + } + }) + } + + return result +} + +fun usefulDeclarations(roots: Iterable, context: JsIrBackendContext): Set { + val queue = ArrayDeque() + val result = hashSetOf() + val constructedClasses = hashSetOf() + + fun IrDeclaration.enqueue() { + if (this !in result) { + result.add(this) + queue.addLast(this) + } + } + + // Add roots, including nested declarations + roots.withNested().forEach { it.enqueue() } + + val toStringMethod = + context.irBuiltIns.anyClass.owner.declarations.filterIsInstance().single { it.name.asString() == "toString" } + val equalsMethod = + context.irBuiltIns.anyClass.owner.declarations.filterIsInstance().single { it.name.asString() == "equals" } + val hashCodeMethod = + context.irBuiltIns.anyClass.owner.declarations.filterIsInstance().single { it.name.asString() == "hashCode" } + + while (queue.isNotEmpty()) { + while (queue.isNotEmpty()) { + val declaration = queue.pollFirst() + + if (declaration is IrClass) { + declaration.superTypes.forEach { + (it.classifierOrNull as? IrClassSymbol)?.owner?.enqueue() + } + + // Special hack for `IntrinsicsJs.kt` support + if (declaration.superTypes.any { it.isSuspendFunctionTypeOrSubtype() }) { + declaration.declarations.forEach { + if (it is IrSimpleFunction && it.name.asString().startsWith("invoke")) { + it.enqueue() + } + } + } + + // TODO find out how `doResume` gets removed + if (declaration.symbol == context.ir.symbols.coroutineImpl) { + declaration.declarations.forEach { + if (it is IrSimpleFunction && it.name.asString() == "doResume") { + it.enqueue() + } + } + } + } + + if (declaration is IrSimpleFunction) { + declaration.resolveFakeOverride()?.enqueue() + } + + // Collect instantiated classes. + if (declaration is IrConstructor) { + declaration.constructedClass.let { + it.enqueue() + constructedClasses += it + } + } + + val body = when (declaration) { + is IrFunction -> declaration.body + is IrField -> declaration.initializer + is IrVariable -> declaration.initializer + else -> null + } + + body?.acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitFunctionAccess(expression: IrFunctionAccessExpression) { + super.visitFunctionAccess(expression) + + expression.symbol.owner.enqueue() + } + + override fun visitVariableAccess(expression: IrValueAccessExpression) { + super.visitVariableAccess(expression) + + expression.symbol.owner.enqueue() + } + + override fun visitFieldAccess(expression: IrFieldAccessExpression) { + super.visitFieldAccess(expression) + + expression.symbol.owner.enqueue() + } + + override fun visitCall(expression: IrCall) { + super.visitCall(expression) + + when (expression.symbol) { + context.intrinsics.jsBoxIntrinsic -> { + val inlineClass = expression.getTypeArgument(0)!!.getInlinedClass()!! + val constructor = inlineClass.declarations.filterIsInstance().single { it.isPrimary } + constructor.enqueue() + } + context.intrinsics.jsClass -> { + (expression.getTypeArgument(0)!!.classifierOrFail.owner as IrDeclaration).enqueue() + } + context.intrinsics.jsObjectCreate.symbol -> { + val classToCreate = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrClass + classToCreate.enqueue() + constructedClasses += classToCreate + } + context.intrinsics.jsEquals -> { + equalsMethod.enqueue() + } + context.intrinsics.jsToString -> { + toStringMethod.enqueue() + } + context.intrinsics.jsHashCode -> { + hashCodeMethod.enqueue() + } + context.intrinsics.jsPlus -> { + if (expression.getValueArgument(0)?.type?.classOrNull == context.irBuiltIns.stringClass) { + toStringMethod.enqueue() + } + } + } + } + + override fun visitStringConcatenation(expression: IrStringConcatenation) { + super.visitStringConcatenation(expression) + + toStringMethod.enqueue() + } + }) + } + + fun IrOverridableDeclaration<*>.overridesUsefulFunction(): Boolean { + return this.overriddenSymbols.any { + (it.owner as? IrOverridableDeclaration<*>)?.let { + it in result || it.overridesUsefulFunction() + } ?: false + } + } + + for (klass in constructedClasses) { + for (declaration in klass.declarations) { + if (declaration in result) continue + + if (declaration is IrOverridableDeclaration<*> && declaration.overridesUsefulFunction()) { + declaration.enqueue() + } + + if (declaration is IrSimpleFunction && declaration.getJsNameOrKotlinName().asString() == "valueOf") { + declaration.enqueue() + } + + // A hack to support `toJson` and other js-specific members + if (declaration.getJsName() != null || + declaration is IrField && declaration.correspondingPropertySymbol?.owner?.getJsName() != null || + declaration is IrSimpleFunction && declaration.correspondingPropertySymbol?.owner?.getJsName() != null + ) { + declaration.enqueue() + } + } + } + } + + return result +} \ No newline at end of file diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt index 2fa3737b55b..c7d58f1ba5a 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt @@ -9,12 +9,19 @@ import com.intellij.openapi.project.Project import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.ir.backend.js.export.isExported import org.jetbrains.kotlin.ir.backend.js.lower.moveBodilessDeclarationsToSeparatePlace import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer import org.jetbrains.kotlin.ir.backend.js.utils.JsMainFunctionDetector import org.jetbrains.kotlin.ir.backend.js.utils.NameTables +import org.jetbrains.kotlin.ir.backend.js.utils.isJsExport +import org.jetbrains.kotlin.ir.declarations.IrDeclarationWithName +import org.jetbrains.kotlin.ir.declarations.IrField import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.util.ExternalDependenciesGenerator +import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable +import org.jetbrains.kotlin.ir.util.isEffectivelyExternal import org.jetbrains.kotlin.ir.util.patchDeclarationParents import org.jetbrains.kotlin.library.KotlinLibrary import org.jetbrains.kotlin.library.resolver.KotlinLibraryResolveResult @@ -32,7 +39,8 @@ fun sortDependencies(dependencies: Collection): Collection, mainArguments: List?, - exportedDeclarations: Set = emptySet() + exportedDeclarations: Set = emptySet(), + generateFullJs: Boolean = true, + generateDceJs: Boolean = false ): CompilerResult { val (moduleFragment, dependencyModules, irBuiltIns, symbolTable, deserializer) = loadIr(project, files, configuration, allDependencies, friendDependencies) @@ -74,7 +84,7 @@ fun compile( jsPhases.invokeToplevel(phaseConfig, context, moduleFragment) val transformer = IrModuleToJsTransformer(context, mainFunction, mainArguments) - return transformer.generateModule(moduleFragment) + return transformer.generateModule(moduleFragment, generateFullJs, generateDceJs) } fun generateJsCode( @@ -86,5 +96,5 @@ fun generateJsCode( jsPhases.invokeToplevel(PhaseConfig(jsPhases), context, moduleFragment) val transformer = IrModuleToJsTransformer(context, null, null, true, nameTables) - return transformer.generateModule(moduleFragment).jsCode + return transformer.generateModule(moduleFragment).jsCode!! } diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt index 11235b061f5..7f3d8a9e262 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt @@ -45,35 +45,9 @@ class ExportModelGenerator(val context: JsIrBackendContext) { } ) - private fun getExportCandidate(declaration: IrDeclaration): IrDeclarationWithName? { - // Only actual public declarations with name can be exported - if (declaration !is IrDeclarationWithVisibility || - declaration !is IrDeclarationWithName || - declaration.visibility != Visibilities.PUBLIC || - declaration.isExpect - ) { - return null - } - - // Workaround to get property declarations instead of its lowered accessors. - if (declaration is IrSimpleFunction) { - val property = declaration.correspondingPropertySymbol?.owner - if (property != null) { - // Return property for getter accessors only to prevent - // returning it twice (for getter and setter) in the same scope - return if (property.getter == declaration) - property - else - null - } - } - - return declaration - } - private fun exportDeclaration(declaration: IrDeclaration): ExportedDeclaration? { val candidate = getExportCandidate(declaration) ?: return null - if (!shouldDeclarationBeExported(candidate)) return null + if (!shouldDeclarationBeExported(candidate, context)) return null return when (candidate) { is IrSimpleFunction -> exportFunction(candidate) @@ -173,7 +147,7 @@ class ExportModelGenerator(val context: JsIrBackendContext) { for (declaration in klass.declarations) { val candidate = getExportCandidate(declaration) ?: continue - if (!shouldDeclarationBeExported(candidate)) continue + if (!shouldDeclarationBeExported(candidate, context)) continue when (candidate) { is IrSimpleFunction -> @@ -302,20 +276,6 @@ class ExportModelGenerator(val context: JsIrBackendContext) { else identifier } - private fun shouldDeclarationBeExported(declaration: IrDeclarationWithName): Boolean { - if (declaration.fqNameWhenAvailable in context.additionalExportedDeclarations) - return true - - if (declaration.isJsExport()) - return true - - return when (val parent = declaration.parent) { - is IrDeclarationWithName -> shouldDeclarationBeExported(parent) - is IrAnnotationContainer -> parent.isJsExport() - else -> false - } - } - private fun functionExportability(function: IrSimpleFunction): Exportability { if (function.isInline && function.typeParameters.any { it.isReified }) return Exportability.Prohibited("Inline reified function") @@ -353,6 +313,51 @@ sealed class Exportability { private val IrClassifierSymbol.isInterface get() = (owner as? IrClass)?.isInterface == true +private fun getExportCandidate(declaration: IrDeclaration): IrDeclarationWithName? { + // Only actual public declarations with name can be exported + if (declaration !is IrDeclarationWithVisibility || + declaration !is IrDeclarationWithName || + declaration.visibility != Visibilities.PUBLIC || + declaration.isExpect + ) { + return null + } + + // Workaround to get property declarations instead of its lowered accessors. + if (declaration is IrSimpleFunction) { + val property = declaration.correspondingPropertySymbol?.owner + if (property != null) { + // Return property for getter accessors only to prevent + // returning it twice (for getter and setter) in the same scope + return if (property.getter == declaration) + property + else + null + } + } + + return declaration +} + +private fun shouldDeclarationBeExported(declaration: IrDeclarationWithName, context: JsIrBackendContext): Boolean { + if (declaration.fqNameWhenAvailable in context.additionalExportedDeclarations) + return true + + if (declaration.isJsExport()) + return true + + return when (val parent = declaration.parent) { + is IrDeclarationWithName -> shouldDeclarationBeExported(parent, context) + is IrAnnotationContainer -> parent.isJsExport() + else -> false + } +} + +fun IrDeclaration.isExported(context: JsIrBackendContext): Boolean { + val candidate = getExportCandidate(this) ?: return false + return shouldDeclarationBeExported(candidate, context) +} + private val reservedWords = setOf( "break", "case", diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/BridgesConstruction.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/BridgesConstruction.kt index e891afc213c..8f1a69c4c49 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/BridgesConstruction.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/BridgesConstruction.kt @@ -138,6 +138,7 @@ class BridgesConstruction(val context: CommonBackendContext) : ClassLoweringPass valueParameters += bridge.valueParameters.map { p -> p.copyTo(this) } annotations += bridge.annotations overriddenSymbols.addAll(delegateTo.overriddenSymbols) + overriddenSymbols.add(bridge.symbol) } context.createIrBuilder(irFunction.symbol).irBlockBody(irFunction) { diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/calls/NumberOperatorCallsTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/calls/NumberOperatorCallsTransformer.kt index 2690a9c7699..59f616fe464 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/calls/NumberOperatorCallsTransformer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/calls/NumberOperatorCallsTransformer.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.ir.backend.js.lower.calls import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder import org.jetbrains.kotlin.ir.backend.js.utils.OperatorNames +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl @@ -172,57 +173,70 @@ class NumberOperatorCallsTransformer(context: JsIrBackendContext) : CallsTransfo else -> e } - private fun withLongCoercion(default: (IrFunctionAccessExpression) -> IrExpression): (IrFunctionAccessExpression) -> IrExpression = { call -> - assert(call.valueArgumentsCount == 1) - val arg = call.getValueArgument(0)!! + private fun withLongCoercion(default: (IrFunctionAccessExpression) -> IrExpression): (IrFunctionAccessExpression) -> IrExpression = + { call -> + assert(call.valueArgumentsCount == 1) + val arg = call.getValueArgument(0)!! - if (arg.type.isLong()) { - val receiverType = call.dispatchReceiver!!.type + var actualCall = call - when { - // Double OP Long => Double OP Long.toDouble() - receiverType.isDouble() -> { - call.putValueArgument(0, IrCallImpl( - call.startOffset, - call.endOffset, - intrinsics.longToDouble.owner.returnType, - intrinsics.longToDouble - ).apply { - dispatchReceiver = arg - }) - } - // Float OP Long => Float OP Long.toFloat() - receiverType.isFloat() -> { - call.putValueArgument(0, IrCallImpl( - call.startOffset, - call.endOffset, - intrinsics.longToFloat.owner.returnType, - intrinsics.longToFloat - ).apply { - dispatchReceiver = arg - }) - } - // {Byte, Short, Int} OP Long => {Byte, Sort, Int}.toLong() OP Long - !receiverType.isLong() -> { - call.dispatchReceiver = IrCallImpl( - call.startOffset, - call.endOffset, - intrinsics.jsNumberToLong.owner.returnType, - intrinsics.jsNumberToLong - ).apply { - putValueArgument(0, call.dispatchReceiver) + if (arg.type.isLong()) { + val receiverType = call.dispatchReceiver!!.type + + when { + // Double OP Long => Double OP Long.toDouble() + receiverType.isDouble() -> { + call.putValueArgument(0, IrCallImpl( + call.startOffset, + call.endOffset, + intrinsics.longToDouble.owner.returnType, + intrinsics.longToDouble + ).apply { + dispatchReceiver = arg + }) + } + // Float OP Long => Float OP Long.toFloat() + receiverType.isFloat() -> { + call.putValueArgument(0, IrCallImpl( + call.startOffset, + call.endOffset, + intrinsics.longToFloat.owner.returnType, + intrinsics.longToFloat + ).apply { + dispatchReceiver = arg + }) + } + // {Byte, Short, Int} OP Long => {Byte, Sort, Int}.toLong() OP Long + !receiverType.isLong() -> { + call.dispatchReceiver = IrCallImpl( + call.startOffset, + call.endOffset, + intrinsics.jsNumberToLong.owner.returnType, + intrinsics.jsNumberToLong + ).apply { + putValueArgument(0, call.dispatchReceiver) + } + + // Replace {Byte, Short, Int}.OP with corresponding Long.OP + val declaration = call.symbol.owner as IrSimpleFunction + val replacement = intrinsics.longClassSymbol.owner.declarations.filterIsInstance() + .single { member -> + member.name.asString() == declaration.name.asString() && + member.valueParameters.size == declaration.valueParameters.size && + member.valueParameters.zip(declaration.valueParameters).all { (a, b) -> a.type == b.type } + }.symbol + + actualCall = irCall(call, replacement) } } } - } - if (call.dispatchReceiver!!.type.isLong()) { - // LHS is Long => use as is - call - } else { - default(call) + if (actualCall.dispatchReceiver!!.type.isLong()) { + actualCall + } else { + default(actualCall) + } } - } fun IrFunctionSymbol.call(vararg arguments: IrExpression) = JsIrBuilder.buildCall(this, owner.returnType).apply { diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt index 093de6d6712..5fb7dc2b113 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt @@ -8,8 +8,10 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs import org.jetbrains.kotlin.config.CommonConfigurationKeys import org.jetbrains.kotlin.ir.backend.js.CompilerResult import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext +import org.jetbrains.kotlin.ir.backend.js.eliminateDeadDeclarations import org.jetbrains.kotlin.ir.backend.js.export.ExportModelGenerator import org.jetbrains.kotlin.ir.backend.js.export.ExportModelToJsStatements +import org.jetbrains.kotlin.ir.backend.js.export.ExportedModule import org.jetbrains.kotlin.ir.backend.js.export.toTypeScript import org.jetbrains.kotlin.ir.backend.js.lower.StaticMembersLowering import org.jetbrains.kotlin.ir.backend.js.utils.* @@ -33,63 +35,7 @@ class IrModuleToJsTransformer( private val moduleKind = backendContext.configuration[JSConfigurationKeys.MODULE_KIND]!! private val generateRegionComments = backendContext.configuration.getBoolean(JSConfigurationKeys.GENERATE_REGION_COMMENTS) - private fun generateModuleBody(module: IrModuleFragment, context: JsGenerationContext): List { - val statements = mutableListOf().also { - if (!generateScriptModule) it += JsStringLiteral("use strict").makeStmt() - } - - val preDeclarationBlock = JsGlobalBlock() - val postDeclarationBlock = JsGlobalBlock() - - statements.addWithComment("block: pre-declaration", preDeclarationBlock) - - val generateFilePaths = backendContext.configuration.getBoolean(JSConfigurationKeys.GENERATE_COMMENTS_WITH_FILE_PATH) - val pathPrefixMap = backendContext.configuration.getMap(JSConfigurationKeys.FILE_PATHS_PREFIX_MAP) - - module.files.forEach { - val fileStatements = it.accept(IrFileToJsTransformer(), context).statements - if (fileStatements.isNotEmpty()) { - var startComment = "" - - if (generateRegionComments) { - startComment = "region " - } - - if (generateRegionComments || generateFilePaths) { - val originalPath = it.path - val path = pathPrefixMap.entries - .find { (k, _) -> originalPath.startsWith(k) } - ?.let { (k, v) -> v + originalPath.substring(k.length) } - ?: originalPath - - startComment += "file: $path" - } - - if (startComment.isNotEmpty()) { - statements.add(JsSingleLineComment(startComment)) - } - - statements.addAll(fileStatements) - statements.endRegion() - } - } - - // sort member forwarding code - processClassModels(context.staticContext.classModels, preDeclarationBlock, postDeclarationBlock) - - statements.addWithComment("block: post-declaration", postDeclarationBlock.statements) - statements.addWithComment("block: init", context.staticContext.initializerBlock.statements) - - if (backendContext.hasTests) { - statements.startRegion("block: tests") - statements += JsInvocation(context.getNameForStaticFunction(backendContext.testContainer).makeRef()).makeStmt() - statements.endRegion() - } - - return statements - } - - fun generateModule(module: IrModuleFragment): CompilerResult { + fun generateModule(module: IrModuleFragment, fullJs: Boolean = true, dceJs: Boolean = false): CompilerResult { val additionalPackages = with(backendContext) { externalPackageFragment.values + listOf( bodilessBuiltInsPackageFragment, @@ -104,6 +50,17 @@ class IrModuleToJsTransformer( namer.merge(module.files, additionalPackages) + val jsCode = if (fullJs) generateWrappedModuleBody(module, exportedModule) else null + + val dceJsCode = if (dceJs) { + eliminateDeadDeclarations(module, backendContext, mainFunction) + generateWrappedModuleBody(module, exportedModule) + } else null + + return CompilerResult(jsCode, dceJsCode, dts) + } + + private fun generateWrappedModuleBody(module: IrModuleFragment, exportedModule: ExportedModule): String { val program = JsProgram() val nameGenerator = IrNamerImpl( @@ -159,7 +116,63 @@ class IrModuleToJsTransformer( ) } - return CompilerResult(program.toString(), dts) + return program.toString() + } + + private fun generateModuleBody(module: IrModuleFragment, context: JsGenerationContext): List { + val statements = mutableListOf().also { + if (!generateScriptModule) it += JsStringLiteral("use strict").makeStmt() + } + + val preDeclarationBlock = JsGlobalBlock() + val postDeclarationBlock = JsGlobalBlock() + + statements.addWithComment("block: pre-declaration", preDeclarationBlock) + + val generateFilePaths = backendContext.configuration.getBoolean(JSConfigurationKeys.GENERATE_COMMENTS_WITH_FILE_PATH) + val pathPrefixMap = backendContext.configuration.getMap(JSConfigurationKeys.FILE_PATHS_PREFIX_MAP) + + module.files.forEach { + val fileStatements = it.accept(IrFileToJsTransformer(), context).statements + if (fileStatements.isNotEmpty()) { + var startComment = "" + + if (generateRegionComments) { + startComment = "region " + } + + if (generateRegionComments || generateFilePaths) { + val originalPath = it.path + val path = pathPrefixMap.entries + .find { (k, _) -> originalPath.startsWith(k) } + ?.let { (k, v) -> v + originalPath.substring(k.length) } + ?: originalPath + + startComment += "file: $path" + } + + if (startComment.isNotEmpty()) { + statements.add(JsSingleLineComment(startComment)) + } + + statements.addAll(fileStatements) + statements.endRegion() + } + } + + // sort member forwarding code + processClassModels(context.staticContext.classModels, preDeclarationBlock, postDeclarationBlock) + + statements.addWithComment("block: post-declaration", postDeclarationBlock.statements) + statements.addWithComment("block: init", context.staticContext.initializerBlock.statements) + + if (backendContext.hasTests) { + statements.startRegion("block: tests") + statements += JsInvocation(context.getNameForStaticFunction(backendContext.testContainer).makeRef()).makeStmt() + statements.endRegion() + } + + return statements } private fun generateMainArguments(mainFunction: IrSimpleFunction, rootContext: JsGenerationContext): List { diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt index e5e1fbfb234..5b0b6fe357d 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/JsClassGenerator.kt @@ -6,10 +6,8 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs import org.jetbrains.kotlin.descriptors.Visibilities -import org.jetbrains.kotlin.ir.backend.js.utils.JsGenerationContext -import org.jetbrains.kotlin.ir.backend.js.utils.Namer -import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope -import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget +import org.jetbrains.kotlin.ir.backend.js.export.isExported +import org.jetbrains.kotlin.ir.backend.js.utils.* import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol @@ -84,22 +82,33 @@ class JsClassGenerator(private val irClass: IrClass, val context: JsGenerationCo ) } - val getterRef = property.getter?.accessorRef() - val setterRef = property.setter?.accessorRef() - classBlock.statements += JsExpressionStatement( - defineProperty( - classPrototypeRef, - context.getNameForProperty(property).ident, - getter = getterRef, - setter = setterRef + if (irClass.isExported(context.staticContext.backendContext) || + property.getter?.overridesExternal() == true || + property.getJsName() != null + ) { + val getterRef = property.getter?.accessorRef() + val setterRef = property.setter?.accessorRef() + classBlock.statements += JsExpressionStatement( + defineProperty( + classPrototypeRef, + context.getNameForProperty(property).ident, + getter = getterRef, + setter = setterRef + ) ) - ) + } } } context.staticContext.classModels[irClass.symbol] = classModel return classBlock } + private fun IrSimpleFunction.overridesExternal(): Boolean { + if (this.isEffectivelyExternal()) return true + + return this.overriddenSymbols.any { it.owner.overridesExternal() } + } + private fun generateMemberFunction(declaration: IrSimpleFunction): JsStatement? { val translatedFunction = declaration.run { if (isReal) accept(IrFunctionToJsTransformer(), context) else null } diff --git a/compiler/testData/cli/js/jsExtraHelp.out b/compiler/testData/cli/js/jsExtraHelp.out index de7c31c77ec..4051b0dc2b1 100644 --- a/compiler/testData/cli/js/jsExtraHelp.out +++ b/compiler/testData/cli/js/jsExtraHelp.out @@ -4,6 +4,7 @@ where advanced options include: -Xfriend-modules= Paths to friend modules -Xfriend-modules-disabled Disable internal declaration export -Xgenerate-dts Generate TypeScript declarations .d.ts file alongside JS file. Available in IR backend only. + -Xir-dce Perform experimental dead code elimination -Xir-only Disables pre-IR backend -Xir-produce-js Generates JS file using IR backend. Also disables pre-IR backend -Xir-produce-klib-dir Generate unpacked KLIB into parent directory of output JS file. diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt index efab0de0fc2..ba0e6ca2619 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt @@ -87,6 +87,7 @@ abstract class BasicBoxTest( protected open val runMinifierByDefault: Boolean = false protected open val skipMinification = getBoolean("kotlin.js.skipMinificationTest") + protected open val runIrDce: Boolean = false protected open val incrementalCompilationChecksEnabled = true @@ -103,6 +104,7 @@ abstract class BasicBoxTest( open fun doTest(filePath: String, expectedResult: String, mainCallParameters: MainCallParameters, coroutinesPackage: String = "") { val file = File(filePath) val outputDir = getOutputDir(file) + val dceOutputDir = getOutputDir(file, testGroupOutputDirForMinification) var fileContent = KotlinTestUtils.doLoadFile(file) if (coroutinesPackage.isNotEmpty()) { fileContent = fileContent.replace("COROUTINES_PACKAGE", coroutinesPackage) @@ -154,10 +156,11 @@ abstract class BasicBoxTest( val friends = module.friends.map { modules[it]?.outputFileName(outputDir) + ".meta.js" } val outputFileName = module.outputFileName(outputDir) + ".js" + val dceOutputFileName = module.outputFileName(dceOutputDir) + ".js" val isMainModule = mainModuleName == module.name generateJavaScriptFile( testFactory.tmpDir, - file.parent, module, outputFileName, dependencies, allDependencies, friends, modules.size > 1, + file.parent, module, outputFileName, dceOutputFileName, dependencies, allDependencies, friends, modules.size > 1, !SKIP_SOURCEMAP_REMAPPING.matcher(fileContent).find(), outputPrefixFile, outputPostfixFile, actualMainCallParameters, testPackage, testFunction, needsFullIrRuntime, isMainModule ) @@ -212,6 +215,9 @@ abstract class BasicBoxTest( val allJsFiles = additionalFiles + inputJsFiles + generatedJsFiles.map { it.first } + globalCommonFiles + localCommonFiles + additionalCommonFiles + additionalMainFiles + val dceAllJsFiles = additionalFiles + inputJsFiles + generatedJsFiles.map { it.first.replace(outputDir.absolutePath, dceOutputDir.absolutePath) } + + globalCommonFiles + localCommonFiles + additionalCommonFiles + additionalMainFiles + val dontRunGeneratedCode = InTextDirectivesUtils.dontRunGeneratedCode(targetBackend, file) if (!dontRunGeneratedCode && generateNodeJsRunner && !SKIP_NODE_JS.matcher(fileContent).find()) { @@ -223,6 +229,10 @@ abstract class BasicBoxTest( if (!dontRunGeneratedCode) { runGeneratedCode(allJsFiles, testModuleName, testPackage, testFunction, expectedResult, withModuleSystem) + + if (runIrDce) { + runGeneratedCode(dceAllJsFiles, testModuleName, testPackage, testFunction, expectedResult, withModuleSystem) + } } performAdditionalChecks(generatedJsFiles.map { it.first }, outputPrefixFile, outputPostfixFile) @@ -340,6 +350,7 @@ abstract class BasicBoxTest( directory: String, module: TestModule, outputFileName: String, + dceOutputFileName: String, dependencies: List, allDependencies: List, friends: List, @@ -369,10 +380,11 @@ abstract class BasicBoxTest( val sourceDirs = (testFiles + additionalFiles).map { File(it).parent }.distinct() val config = createConfig(sourceDirs, module, dependencies, allDependencies, friends, multiModule, tmpDir, incrementalData = null) val outputFile = File(outputFileName) + val dceOutputFile = File(dceOutputFileName) val incrementalData = IncrementalData() translateFiles( - psiFiles.map(TranslationUnit::SourceFile), outputFile, config, outputPrefixFile, outputPostfixFile, + psiFiles.map(TranslationUnit::SourceFile), outputFile, dceOutputFile, config, outputPrefixFile, outputPostfixFile, mainCallParameters, incrementalData, remap, testPackage, testFunction, needsFullIrRuntime, isMainModule ) @@ -423,7 +435,7 @@ abstract class BasicBoxTest( val recompiledOutputFile = File(outputFile.parentFile, outputFile.nameWithoutExtension + "-recompiled.js") translateFiles( - translationUnits, recompiledOutputFile, recompiledConfig, outputPrefixFile, outputPostfixFile, + translationUnits, recompiledOutputFile, recompiledOutputFile, recompiledConfig, outputPrefixFile, outputPostfixFile, mainCallParameters, incrementalData, remap, testPackage, testFunction, needsFullIrRuntime, false ) @@ -485,6 +497,7 @@ abstract class BasicBoxTest( protected open fun translateFiles( units: List, outputFile: File, + dceOutputFile: File, config: JsConfig, outputPrefixFile: File?, outputPostfixFile: File?, diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt index ee9df3145c2..f5daa58e0f7 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt @@ -46,6 +46,8 @@ abstract class BasicIrBoxTest( override val skipMinification = true + override val runIrDce: Boolean = true + // TODO Design incremental compilation for IR and add test support override val incrementalCompilationChecksEnabled = false @@ -62,6 +64,7 @@ abstract class BasicIrBoxTest( override fun translateFiles( units: List, outputFile: File, + dceOutputFile: File, config: JsConfig, outputPrefixFile: File?, outputPostfixFile: File?, @@ -118,12 +121,19 @@ abstract class BasicIrBoxTest( allDependencies = resolvedLibraries, friendDependencies = emptyList(), mainArguments = mainCallParameters.run { if (shouldBeGenerated()) arguments() else null }, - exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, testFunction))) + exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, testFunction))), + generateFullJs = true, + generateDceJs = runIrDce ) - val wrappedCode = wrapWithModuleEmulationMarkers(compiledModule.jsCode, moduleId = config.moduleId, moduleKind = config.moduleKind) + val wrappedCode = wrapWithModuleEmulationMarkers(compiledModule.jsCode!!, moduleId = config.moduleId, moduleKind = config.moduleKind) outputFile.write(wrappedCode) + compiledModule.dceJsCode?.let { dceJsCode -> + val dceWrappedCode = wrapWithModuleEmulationMarkers(dceJsCode, moduleId = config.moduleId, moduleKind = config.moduleKind) + dceOutputFile.write(dceWrappedCode) + } + if (generateDts) { val dtsFile = outputFile.withReplacedExtensionOrNull("_v5.js", ".d.ts") dtsFile?.write(compiledModule.tsDefinitions ?: error("No ts definitions")) diff --git a/js/js.translator/testData/_commonFiles/fail.kt b/js/js.translator/testData/_commonFiles/fail.kt index ac818be21fb..481c56d3c24 100644 --- a/js/js.translator/testData/_commonFiles/fail.kt +++ b/js/js.translator/testData/_commonFiles/fail.kt @@ -3,5 +3,5 @@ package kotlin // see StdLibTestBase.removeAdHocAssertions fun fail(message: String? = null): Nothing { - throw Exception(message) + throw Throwable(message) } \ No newline at end of file diff --git a/js/js.translator/testData/box/coercion/charValParameter.kt b/js/js.translator/testData/box/coercion/charValParameter.kt index 97ef1bdee0f..f77e02aca9b 100644 --- a/js/js.translator/testData/box/coercion/charValParameter.kt +++ b/js/js.translator/testData/box/coercion/charValParameter.kt @@ -1,5 +1,7 @@ // EXPECTED_REACHABLE_NODES: 1276 // IGNORE_BACKEND: JS_IR + +@JsExport class A(val x: Char) fun typeOf(x: dynamic): String = js("typeof x") diff --git a/js/js.translator/testData/box/coercion/defaultAccessors.kt b/js/js.translator/testData/box/coercion/defaultAccessors.kt index d470bde747d..72850576db0 100644 --- a/js/js.translator/testData/box/coercion/defaultAccessors.kt +++ b/js/js.translator/testData/box/coercion/defaultAccessors.kt @@ -4,6 +4,7 @@ interface I { val a: Char } +@JsExport object X : I { override var a = '#' } diff --git a/js/js.translator/testData/box/coercion/propertyBridgeChar.kt b/js/js.translator/testData/box/coercion/propertyBridgeChar.kt index aaec1ac98a6..7e80d964c7b 100644 --- a/js/js.translator/testData/box/coercion/propertyBridgeChar.kt +++ b/js/js.translator/testData/box/coercion/propertyBridgeChar.kt @@ -1,5 +1,7 @@ // EXPECTED_REACHABLE_NODES: 1289 // IGNORE_BACKEND: JS_IR + +@JsExport open class A { val foo: Char get() = 'X' diff --git a/js/js.translator/testData/box/delegation/jsNamePropertyDelegation.kt b/js/js.translator/testData/box/delegation/jsNamePropertyDelegation.kt index a944aea4591..f316ad8e49a 100644 --- a/js/js.translator/testData/box/delegation/jsNamePropertyDelegation.kt +++ b/js/js.translator/testData/box/delegation/jsNamePropertyDelegation.kt @@ -3,6 +3,7 @@ package foo import kotlin.reflect.KProperty +@JsExport class A { @JsName("xx") val x: Int by B(23) diff --git a/js/js.translator/testData/box/jsName/avoidNameClash.kt b/js/js.translator/testData/box/jsName/avoidNameClash.kt index 899c2311570..fd6498c8ee8 100644 --- a/js/js.translator/testData/box/jsName/avoidNameClash.kt +++ b/js/js.translator/testData/box/jsName/avoidNameClash.kt @@ -1,5 +1,6 @@ // EXPECTED_REACHABLE_NODES: 1290 +@JsExport object A { @JsName("js_method") fun f() = "method" diff --git a/js/js.translator/testData/box/jsName/simpleJsName.kt b/js/js.translator/testData/box/jsName/simpleJsName.kt index 245e7a687d2..52081f11ba0 100644 --- a/js/js.translator/testData/box/jsName/simpleJsName.kt +++ b/js/js.translator/testData/box/jsName/simpleJsName.kt @@ -1,5 +1,6 @@ // EXPECTED_REACHABLE_NODES: 1291 +@JsExport object A { @JsName("js_f") fun f(x: Int) = "f($x)" diff --git a/js/js.translator/testData/box/native/undefined.kt b/js/js.translator/testData/box/native/undefined.kt index 4e205d9b8ea..7a3e609bd71 100644 --- a/js/js.translator/testData/box/native/undefined.kt +++ b/js/js.translator/testData/box/native/undefined.kt @@ -13,7 +13,7 @@ fun box(): String { assertEquals(a.foo, undefined) assertNotEquals(a.toString, undefined) - val b: dynamic = object {val bar = ""} + val b: dynamic = object {@JsName("bar") val bar = ""} assertEquals(b.foo, undefined) assertNotEquals(b.bar, undefined) diff --git a/js/js.translator/testData/box/propertyAccess/simpleLateInitIsInitialized.kt b/js/js.translator/testData/box/propertyAccess/simpleLateInitIsInitialized.kt index 9619e1eeb64..61bfb22e207 100644 --- a/js/js.translator/testData/box/propertyAccess/simpleLateInitIsInitialized.kt +++ b/js/js.translator/testData/box/propertyAccess/simpleLateInitIsInitialized.kt @@ -7,6 +7,7 @@ fun deinitialize(foo: dynamic) { } class Foo { + @JsName("bar") lateinit var bar: String fun test(): String { diff --git a/libraries/stdlib/js-ir/runtime/coreRuntime.kt b/libraries/stdlib/js-ir/runtime/coreRuntime.kt index 8558ed109da..6aea2b16a27 100644 --- a/libraries/stdlib/js-ir/runtime/coreRuntime.kt +++ b/libraries/stdlib/js-ir/runtime/coreRuntime.kt @@ -55,7 +55,7 @@ fun hashCode(obj: dynamic): Int { } } -private var POW_2_32 = 4294967296 +private var POW_2_32 = 4294967296.0 private var OBJECT_HASH_CODE_PROPERTY_NAME = "kotlinHashCodeValue$" fun getObjectHashCode(obj: dynamic): Int { @@ -83,7 +83,9 @@ fun identityHashCode(obj: Any?): Int = getObjectHashCode(obj) internal fun captureStack(instance: Throwable) { if (js("Error").captureStackTrace != null) { - js("Error").captureStackTrace(instance, instance::class.js) + // TODO Why we generated get kclass for throwable in original code? + js("Error").captureStackTrace(instance, instance.asDynamic().constructor) +// js("Error").captureStackTrace(instance, instance::class.js) } else { instance.asDynamic().stack = js("new Error()").stack } diff --git a/libraries/stdlib/js/src/kotlin/console.kt b/libraries/stdlib/js/src/kotlin/console.kt index 220e332ce29..f8c641620f6 100644 --- a/libraries/stdlib/js/src/kotlin/console.kt +++ b/libraries/stdlib/js/src/kotlin/console.kt @@ -65,7 +65,7 @@ internal open class BufferedOutput : BaseOutput() { internal class BufferedOutputToConsoleLog : BufferedOutput() { override fun print(message: Any?) { var s = String(message) - val i = s.lastIndexOf('\n') + val i = s.nativeLastIndexOf("\n", 0) if (i >= 0) { buffer += s.substring(0, i) flush()