JS_IR: DCE

Could be enabled by toggling `-Xir-dce`

Box test output in js/js.translator/testData/out-min
This commit is contained in:
Anton Bannykh
2019-11-19 15:36:27 +03:00
parent faf4c05fc8
commit 81699299f5
23 changed files with 553 additions and 173 deletions
@@ -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)
@@ -205,10 +205,13 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
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"))
@@ -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<IrDeclaration> {
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<IrDeclaration>) {
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<IrDeclaration>.withNested(): Iterable<IrDeclaration> {
val result = mutableListOf<IrDeclaration>()
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<IrDeclaration>, context: JsIrBackendContext): Set<IrDeclaration> {
val queue = ArrayDeque<IrDeclaration>()
val result = hashSetOf<IrDeclaration>()
val constructedClasses = hashSetOf<IrClass>()
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<IrFunction>().single { it.name.asString() == "toString" }
val equalsMethod =
context.irBuiltIns.anyClass.owner.declarations.filterIsInstance<IrFunction>().single { it.name.asString() == "equals" }
val hashCodeMethod =
context.irBuiltIns.anyClass.owner.declarations.filterIsInstance<IrFunction>().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<IrConstructor>().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
}
@@ -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<IrModuleFragment>): Collection<IrM
}
class CompilerResult(
val jsCode: String,
val jsCode: String?,
val dceJsCode: String?,
val tsDefinitions: String? = null
)
@@ -44,7 +52,9 @@ fun compile(
allDependencies: KotlinLibraryResolveResult,
friendDependencies: List<KotlinLibrary>,
mainArguments: List<String>?,
exportedDeclarations: Set<FqName> = emptySet()
exportedDeclarations: Set<FqName> = 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!!
}
@@ -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",
@@ -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) {
@@ -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<IrSimpleFunction>()
.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 {
@@ -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<JsStatement> {
val statements = mutableListOf<JsStatement>().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<JsStatement> {
val statements = mutableListOf<JsStatement>().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<JsExpression> {
@@ -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 }
+1
View File
@@ -4,6 +4,7 @@ where advanced options include:
-Xfriend-modules=<path> 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.
@@ -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<String>,
allDependencies: List<String>,
friends: List<String>,
@@ -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<TranslationUnit>,
outputFile: File,
dceOutputFile: File,
config: JsConfig,
outputPrefixFile: File?,
outputPostfixFile: File?,
@@ -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<TranslationUnit>,
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"))
+1 -1
View File
@@ -3,5 +3,5 @@ package kotlin
// see StdLibTestBase.removeAdHocAssertions
fun fail(message: String? = null): Nothing {
throw Exception(message)
throw Throwable(message)
}
@@ -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")
@@ -4,6 +4,7 @@ interface I {
val a: Char
}
@JsExport
object X : I {
override var a = '#'
}
@@ -1,5 +1,7 @@
// EXPECTED_REACHABLE_NODES: 1289
// IGNORE_BACKEND: JS_IR
@JsExport
open class A {
val foo: Char
get() = 'X'
@@ -3,6 +3,7 @@ package foo
import kotlin.reflect.KProperty
@JsExport
class A {
@JsName("xx") val x: Int by B(23)
@@ -1,5 +1,6 @@
// EXPECTED_REACHABLE_NODES: 1290
@JsExport
object A {
@JsName("js_method") fun f() = "method"
+1
View File
@@ -1,5 +1,6 @@
// EXPECTED_REACHABLE_NODES: 1291
@JsExport
object A {
@JsName("js_f") fun f(x: Int) = "f($x)"
+1 -1
View File
@@ -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)
@@ -7,6 +7,7 @@ fun deinitialize(foo: dynamic) {
}
class Foo {
@JsName("bar")
lateinit var bar: String
fun test(): String {
@@ -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
}
+1 -1
View File
@@ -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()