[WASM] DCE implementation

This commit is contained in:
Igor Yakovlev
2021-12-29 19:28:22 +01:00
committed by TeamCityServer
parent 84caad7ba2
commit 2ec0411a7f
22 changed files with 1005 additions and 593 deletions
@@ -328,6 +328,7 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
exportedDeclarations = setOf(FqName("main")),
emitNameSection = arguments.wasmDebug,
propertyLazyInitialization = arguments.irPropertyLazyInitialization,
dceEnabled = arguments.irDce,
)
val outputWasmFile = outputFile.withReplacedExtensionOrNull(outputFile.extension, "wasm")!!
outputWasmFile.writeBytes(res.wasm)
@@ -1,565 +0,0 @@
/*
* Copyright 2010-2020 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.backend.common.ir.isMemberOfOpenClass
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.ir.JsIrBuilder
import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
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.types.getClass
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.config.RuntimeDiagnostic
import org.jetbrains.kotlin.utils.addIfNotNull
import java.util.*
fun eliminateDeadDeclarations(
modules: Iterable<IrModuleFragment>,
context: JsIrBackendContext,
removeUnusedAssociatedObjects: Boolean = true,
) {
val allRoots = buildRoots(modules, context)
val usefulDeclarations = usefulDeclarations(allRoots, context, removeUnusedAssociatedObjects)
processUselessDeclarations(modules, usefulDeclarations, context, removeUnusedAssociatedObjects)
}
private fun IrField.isConstant(): Boolean {
return correspondingPropertySymbol?.owner?.isConst ?: false
}
private fun IrDeclaration.addRootsTo(to: MutableCollection<IrDeclaration>, context: JsIrBackendContext) {
when {
this is IrProperty -> {
backingField?.addRootsTo(to, context)
getter?.addRootsTo(to, context)
setter?.addRootsTo(to, context)
}
isEffectivelyExternal() -> {
to += this
}
isExported(context) -> {
to += this
}
this is IrField -> {
// TODO: simplify
if ((initializer != null && !isKotlinPackage() || correspondingPropertySymbol?.owner?.isExported(context) == true) && !isConstant()) {
to += this
}
}
this is IrSimpleFunction -> {
if (correspondingPropertySymbol?.owner?.isExported(context) == true) {
to += this
}
}
}
}
private fun buildRoots(modules: Iterable<IrModuleFragment>, context: JsIrBackendContext): Iterable<IrDeclaration> {
val rootDeclarations = mutableListOf<IrDeclaration>()
val allFiles = (modules.flatMap { it.files } + context.packageLevelJsModules + context.externalPackageFragment.values)
allFiles.forEach {
it.declarations.forEach {
it.addRootsTo(rootDeclarations, context)
}
}
rootDeclarations += context.testFunsPerFile.values
val dceRuntimeDiagnostic = context.dceRuntimeDiagnostic
if (dceRuntimeDiagnostic != null) {
rootDeclarations += dceRuntimeDiagnostic.unreachableDeclarationMethod(context).owner
}
// TODO: Generate calls to main as IR->IR lowering and reference coroutineEmptyContinuation directly
JsMainFunctionDetector(context).getMainFunctionOrNull(modules.last())?.let { mainFunction ->
rootDeclarations += mainFunction
if (mainFunction.isLoweredSuspendFunction(context)) {
rootDeclarations += context.coroutineEmptyContinuation.owner
}
}
return rootDeclarations
}
private fun processUselessDeclarations(
modules: Iterable<IrModuleFragment>,
usefulDeclarations: Set<IrDeclaration>,
context: JsIrBackendContext,
removeUnusedAssociatedObjects: Boolean,
) {
modules.forEach { module ->
module.files.forEach {
it.acceptVoid(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitFile(declaration: IrFile) {
process(declaration)
}
private fun IrConstructorCall.shouldKeepAnnotation(): Boolean {
associatedObject()?.let { obj ->
if (obj !in usefulDeclarations) return false
}
return true
}
override fun visitClass(declaration: IrClass) {
process(declaration)
// Remove annotations for `findAssociatedObject` feature, which reference objects eliminated by the DCE.
// Otherwise `JsClassGenerator.generateAssociatedKeyProperties` will try to reference the object factory (which is removed).
// That will result in an error from the Namer. It cannot generate a name for an absent declaration.
if (removeUnusedAssociatedObjects && declaration.annotations.any { !it.shouldKeepAnnotation() }) {
declaration.annotations = declaration.annotations.filter { it.shouldKeepAnnotation() }
}
}
// TODO bring back the primary constructor fix
private fun process(container: IrDeclarationContainer) {
container.declarations.transformFlat { member ->
if (member !in usefulDeclarations) {
member.processUselessDeclaration(context)
} else {
member.acceptVoid(this)
null
}
}
}
})
}
}
}
private fun IrDeclaration.processUselessDeclaration(context: JsIrBackendContext): List<IrDeclaration>? {
return when {
context.dceRuntimeDiagnostic != null -> {
processWithDiagnostic(context)
return null
}
else -> emptyList()
}
}
private fun RuntimeDiagnostic.unreachableDeclarationMethod(context: JsIrBackendContext) =
when (this) {
RuntimeDiagnostic.LOG -> context.intrinsics.jsUnreachableDeclarationLog
RuntimeDiagnostic.EXCEPTION -> context.intrinsics.jsUnreachableDeclarationException
}
private fun RuntimeDiagnostic.removingBody(): Boolean {
return this != RuntimeDiagnostic.LOG
}
private fun IrDeclaration.processWithDiagnostic(context: JsIrBackendContext) {
when (this) {
is IrFunction -> processFunctionWithDiagnostic(context)
is IrField -> processFieldWithDiagnostic()
is IrDeclarationContainer -> declarations.forEach { it.processWithDiagnostic(context) }
}
}
private fun IrFunction.processFunctionWithDiagnostic(context: JsIrBackendContext) {
val dceRuntimeDiagnostic = context.dceRuntimeDiagnostic!!
val isRemovingBody = dceRuntimeDiagnostic.removingBody()
val targetMethod = dceRuntimeDiagnostic.unreachableDeclarationMethod(context)
val call = JsIrBuilder.buildCall(
target = targetMethod,
type = targetMethod.owner.returnType
)
if (isRemovingBody) {
body = context.irFactory.createBlockBody(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET
)
}
body?.prependFunctionCall(call)
}
private fun IrField.processFieldWithDiagnostic() {
if (initializer != null && isKotlinPackage()) {
initializer = null
}
}
private fun IrField.isKotlinPackage() =
fqNameWhenAvailable?.asString()?.startsWith("kotlin") == true
// TODO refactor it, the function became too big. Please contact me (Zalim) before doing it.
fun usefulDeclarations(
roots: Iterable<IrDeclaration>,
context: JsIrBackendContext,
removeUnusedAssociatedObjects: Boolean,
): Set<IrDeclaration> {
val printReachabilityInfo =
context.configuration.getBoolean(JSConfigurationKeys.PRINT_REACHABILITY_INFO) ||
java.lang.Boolean.getBoolean("kotlin.js.ir.dce.print.reachability.info")
val reachabilityInfo: MutableSet<String> = if (printReachabilityInfo) linkedSetOf() else Collections.emptySet()
val queue = ArrayDeque<IrDeclaration>()
val result = hashSetOf<IrDeclaration>()
// This collection contains declarations whose reachability should be propagated to overrides.
// Overriding uncontagious declaration will not lead to becoming a declaration reachable.
// By default, all declarations treated as contagious, it's not the most efficient, but it's safest.
// In case when we access a declaration through a fake-override declaration, the original (real) one will not be marked as contagious,
// so, later, other overrides will not be processed unconditionally only because it overrides a reachable declaration.
//
// The collection must be a subset of [result] set.
val contagiousReachableDeclarations = hashSetOf<IrOverridableDeclaration<*>>()
val constructedClasses = hashSetOf<IrClass>()
val classesWithObjectAssociations = hashSetOf<IrClass>()
val referencedJsClasses = hashSetOf<IrDeclaration>()
val referencedJsClassesFromExpressions = hashSetOf<IrClass>()
fun IrDeclaration.enqueue(
from: IrDeclaration?,
description: String?,
isContagious: Boolean = true,
altFromFqn: String? = null
) {
// Ignore non-external IrProperty because we don't want to generate code for them and codegen doesn't support it.
if (this is IrProperty && !this.isExternal) return
// TODO check that this is overridable
// it requires fixing how functions with default arguments is handled
val isContagiousOverridableDeclaration = isContagious && this is IrOverridableDeclaration<*> && this.isMemberOfOpenClass
if (printReachabilityInfo) {
val fromFqn = (from as? IrDeclarationWithName)?.fqNameWhenAvailable?.asString() ?: altFromFqn ?: "<unknown>"
val toFqn = (this as? IrDeclarationWithName)?.fqNameWhenAvailable?.asString() ?: "<unknown>"
val comment = (description ?: "") + (if (isContagiousOverridableDeclaration) "[CONTAGIOUS!]" else "")
val v = "\"$fromFqn\" -> \"$toFqn\"" + (if (comment.isBlank()) "" else " // $comment")
reachabilityInfo.add(v)
}
if (isContagiousOverridableDeclaration) {
contagiousReachableDeclarations.add(this as IrOverridableDeclaration<*>)
}
if (this !in result) {
result.add(this)
queue.addLast(this)
}
}
// use withInitialIr to avoid ConcurrentModificationException in dce-driven lowering when adding roots' nested declarations (members)
// Add roots
roots.forEach {
it.enqueue(null, null, altFromFqn = "<ROOT>")
}
// Add roots' nested declarations
roots.forEach {
it.acceptVoid(
object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitBody(body: IrBody) {
// Skip
}
override fun visitDeclaration(declaration: IrDeclarationBase) {
if (declaration !== it) declaration.enqueue(it, "roots' nested declaration")
super.visitDeclaration(declaration)
}
}
)
}
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()
fun IrDeclaration.enqueue(description: String, isContagious: Boolean = true) {
enqueue(declaration, description, isContagious)
}
if (declaration is IrClass) {
declaration.superTypes.forEach {
(it.classifierOrNull as? IrClassSymbol)?.owner?.enqueue("superTypes")
}
if (declaration.isObject && declaration.isExported(context)) {
context.mapping.objectToGetInstanceFunction[declaration]!!
.enqueue(declaration, "Exported object getInstance function")
}
declaration.annotations.forEach {
val annotationClass = it.symbol.owner.constructedClass
if (annotationClass.isAssociatedObjectAnnotatedAnnotation) {
classesWithObjectAssociations += declaration
annotationClass.enqueue("@AssociatedObject annotated annotation class")
}
}
}
if (declaration is IrSimpleFunction && declaration.isFakeOverride) {
declaration.resolveFakeOverride()?.enqueue("real overridden fun", isContagious = false)
}
// Collect instantiated classes.
if (declaration is IrConstructor) {
declaration.constructedClass.let {
it.enqueue("constructed class")
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("function access")
}
override fun visitRawFunctionReference(expression: IrRawFunctionReference) {
super.visitRawFunctionReference(expression)
expression.symbol.owner.enqueue("raw function access")
}
override fun visitVariableAccess(expression: IrValueAccessExpression) {
super.visitVariableAccess(expression)
expression.symbol.owner.enqueue("variable access")
}
override fun visitFieldAccess(expression: IrFieldAccessExpression) {
super.visitFieldAccess(expression)
expression.symbol.owner.enqueue("field access")
}
override fun visitCall(expression: IrCall) {
super.visitCall(expression)
when (expression.symbol) {
context.intrinsics.jsBoxIntrinsic -> {
val inlineClass = context.inlineClassesUtils.getInlinedClass(expression.getTypeArgument(0)!!)!!
val constructor = inlineClass.declarations.filterIsInstance<IrConstructor>().single { it.isPrimary }
constructor.enqueue("intrinsic: jsBoxIntrinsic")
}
context.intrinsics.jsClass -> {
val ref = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrDeclaration
ref.enqueue("intrinsic: jsClass")
referencedJsClasses += ref
// When class reference provided as parameter to external function
// It can be instantiated by external JS script
// Need to leave constructor for this
// https://youtrack.jetbrains.com/issue/KT-46672
// TODO: Possibly solution with origin is not so good
// There is option with applying this hack to jsGetKClass
if (expression.origin == JsStatementOrigins.CLASS_REFERENCE) {
// Maybe we need to filter primary constructor
// Although at this time, we should have only primary constructor
(ref as IrClass)
.constructors
.forEach {
it.enqueue("intrinsic: jsClass (constructor)")
}
}
}
context.reflectionSymbols.getKClassFromExpression -> {
val ref = expression.getTypeArgument(0)?.classOrNull ?: context.irBuiltIns.anyClass
referencedJsClassesFromExpressions += ref.owner
}
context.intrinsics.jsObjectCreate -> {
val classToCreate = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrClass
classToCreate.enqueue("intrinsic: jsObjectCreate")
constructedClasses += classToCreate
}
context.intrinsics.jsEquals -> {
equalsMethod.enqueue("intrinsic: jsEquals")
}
context.intrinsics.jsToString -> {
toStringMethod.enqueue("intrinsic: jsToString")
}
context.intrinsics.jsHashCode -> {
hashCodeMethod.enqueue("intrinsic: jsHashCode")
}
context.intrinsics.jsPlus -> {
if (expression.getValueArgument(0)?.type?.classOrNull == context.irBuiltIns.stringClass) {
toStringMethod.enqueue("intrinsic: jsPlus")
}
}
context.intrinsics.jsConstruct -> {
val callType = expression.getTypeArgument(0)!!
val constructor = callType.getClass()!!.primaryConstructor
constructor!!.enqueue("ctor call from jsConstruct-intrinsic")
}
context.intrinsics.es6DefaultType -> {
//same as jsClass
val ref = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrDeclaration
ref.enqueue("intrinsic: jsClass")
referencedJsClasses += ref
//Generate klass in `val currResultType = resultType || klass`
val arg = expression.getTypeArgument(0)!!
val klass = arg.getClass()
constructedClasses.addIfNotNull(klass)
}
context.intrinsics.jsInvokeSuspendSuperType,
context.intrinsics.jsInvokeSuspendSuperTypeWithReceiver,
context.intrinsics.jsInvokeSuspendSuperTypeWithReceiverAndParam -> {
invokeFunForLambda(expression)
.enqueue("intrinsic: suspendSuperType")
}
}
}
override fun visitStringConcatenation(expression: IrStringConcatenation) {
super.visitStringConcatenation(expression)
toStringMethod.enqueue("string concatenation")
}
})
}
fun IrOverridableDeclaration<*>.findOverriddenContagiousDeclaration(): IrOverridableDeclaration<*>? {
for (overriddenSymbol in this.overriddenSymbols) {
val overriddenDeclaration = overriddenSymbol.owner as? IrOverridableDeclaration<*> ?: continue
if (overriddenDeclaration in contagiousReachableDeclarations) return overriddenDeclaration
overriddenDeclaration.findOverriddenContagiousDeclaration()?.let {
return it
}
}
return null
}
// Handle objects, constructed via `findAssociatedObject` annotation
referencedJsClassesFromExpressions += constructedClasses.filterDescendantsOf(referencedJsClassesFromExpressions) // Grow the set of possible results of instance::class expression
for (klass in classesWithObjectAssociations) {
if (removeUnusedAssociatedObjects && klass !in referencedJsClasses && klass !in referencedJsClassesFromExpressions) continue
for (annotation in klass.annotations) {
val annotationClass = annotation.symbol.owner.constructedClass
if (removeUnusedAssociatedObjects && annotationClass !in referencedJsClasses) continue
annotation.associatedObject()?.let { obj ->
context.mapping.objectToGetInstanceFunction[obj]?.enqueue(klass, "associated object factory")
}
}
}
for (klass in constructedClasses) {
// TODO a better way to support inverse overrides.
for (declaration in ArrayList(klass.declarations)) {
if (declaration in result) continue
if (declaration is IrOverridableDeclaration<*>) {
declaration.findOverriddenContagiousDeclaration()?.let {
declaration.enqueue(it, "overrides useful declaration")
}
}
if (declaration is IrSimpleFunction && declaration.getJsNameOrKotlinName().asString() == "valueOf") {
declaration.enqueue(klass, "valueOf")
}
// 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(klass, "annotated by @JsName")
}
// A hack to enforce property lowering.
// Until a getter is accessed it doesn't get moved to the declaration list.
if (declaration is IrProperty) {
fun IrSimpleFunction.enqueue(description: String) {
findOverriddenContagiousDeclaration()?.let { enqueue(it, description) }
}
declaration.getter?.enqueue("(getter) overrides useful declaration")
declaration.setter?.enqueue("(setter) overrides useful declaration")
}
}
}
context.additionalExportedDeclarations
.forEach { it.enqueue(null, "from additionalExportedDeclarations", altFromFqn = "<ROOT>") }
}
if (printReachabilityInfo) {
reachabilityInfo.forEach(::println)
}
return result
}
private fun Collection<IrClass>.filterDescendantsOf(bases: Collection<IrClass>): Collection<IrClass> {
val visited = hashSetOf<IrClass>()
val baseDescendants = hashSetOf<IrClass>()
baseDescendants += bases
fun overridesAnyBase(klass: IrClass): Boolean {
if (klass in baseDescendants) return true
if (klass in visited) return false
visited += klass
klass.superTypes.forEach {
(it.classifierOrNull as? IrClassSymbol)?.owner?.let {
if (overridesAnyBase(it)) {
baseDescendants += klass
return true
}
}
}
return false
}
return this.filter { overridesAnyBase(it) }
}
@@ -0,0 +1,119 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.ir.backend.js.dce
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
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.expressions.IrBody
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
import org.jetbrains.kotlin.js.config.RuntimeDiagnostic
fun eliminateDeadDeclarations(
modules: Iterable<IrModuleFragment>,
context: JsIrBackendContext,
removeUnusedAssociatedObjects: Boolean = true,
) {
val allRoots = buildRoots(modules, context)
val printReachabilityInfo =
context.configuration.getBoolean(JSConfigurationKeys.PRINT_REACHABILITY_INFO) ||
java.lang.Boolean.getBoolean("kotlin.js.ir.dce.print.reachability.info")
val usefulDeclarations = JsUsefulDeclarationProcessor(context, printReachabilityInfo, removeUnusedAssociatedObjects)
.collectDeclarations(allRoots)
val uselessDeclarationsProcessor =
UselessDeclarationsRemover(removeUnusedAssociatedObjects, usefulDeclarations, context, context.dceRuntimeDiagnostic)
modules.forEach { module ->
module.files.forEach {
it.acceptVoid(uselessDeclarationsProcessor)
}
}
}
private fun IrField.isConstant(): Boolean =
correspondingPropertySymbol?.owner?.isConst ?: false
private fun IrDeclaration.addRootsTo(
nestedVisitor: IrElementVisitorVoid,
context: JsIrBackendContext
) {
when {
this is IrProperty -> {
backingField?.addRootsTo(nestedVisitor, context)
getter?.addRootsTo(nestedVisitor, context)
setter?.addRootsTo(nestedVisitor, context)
}
isEffectivelyExternal() -> {
acceptVoid(nestedVisitor)
}
isExported(context) -> {
acceptVoid(nestedVisitor)
}
this is IrField -> {
// TODO: simplify
if ((initializer != null && !isKotlinPackage() || correspondingPropertySymbol?.owner?.isExported(context) == true) && !isConstant()) {
acceptVoid(nestedVisitor)
}
}
this is IrSimpleFunction -> {
if (correspondingPropertySymbol?.owner?.isExported(context) == true) {
acceptVoid(nestedVisitor)
}
}
}
}
private fun buildRoots(modules: Iterable<IrModuleFragment>, context: JsIrBackendContext): List<IrDeclaration> = buildList {
val declarationsCollector = object : IrElementVisitorVoid {
override fun visitElement(element: IrElement): Unit = element.acceptChildrenVoid(this)
override fun visitBody(body: IrBody): Unit = Unit // Skip
override fun visitDeclaration(declaration: IrDeclarationBase) {
super.visitDeclaration(declaration)
add(declaration)
}
}
val allFiles = (modules.flatMap { it.files } + context.packageLevelJsModules + context.externalPackageFragment.values)
allFiles.forEach { file ->
file.declarations.forEach { declaration ->
declaration.addRootsTo(declarationsCollector, context)
}
}
val dceRuntimeDiagnostic = context.dceRuntimeDiagnostic
if (dceRuntimeDiagnostic != null) {
dceRuntimeDiagnostic.unreachableDeclarationMethod(context).owner.acceptVoid(declarationsCollector)
}
// TODO: Generate calls to main as IR->IR lowering and reference coroutineEmptyContinuation directly
JsMainFunctionDetector(context).getMainFunctionOrNull(modules.last())?.let { mainFunction ->
add(mainFunction)
if (mainFunction.isLoweredSuspendFunction(context)) {
context.coroutineEmptyContinuation.owner.acceptVoid(declarationsCollector)
}
}
addAll(context.testFunsPerFile.values)
addAll(context.additionalExportedDeclarations)
}
internal fun RuntimeDiagnostic.unreachableDeclarationMethod(context: JsIrBackendContext) =
when (this) {
RuntimeDiagnostic.LOG -> context.intrinsics.jsUnreachableDeclarationLog
RuntimeDiagnostic.EXCEPTION -> context.intrinsics.jsUnreachableDeclarationException
}
internal fun IrField.isKotlinPackage() =
fqNameWhenAvailable?.asString()?.startsWith("kotlin") == true
@@ -0,0 +1,181 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.ir.backend.js.dce
import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext
import org.jetbrains.kotlin.ir.backend.js.JsStatementOrigins
import org.jetbrains.kotlin.ir.backend.js.export.isExported
import org.jetbrains.kotlin.ir.backend.js.utils.associatedObject
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.invokeFunForLambda
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrCall
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.types.getClass
import org.jetbrains.kotlin.ir.util.constructedClass
import org.jetbrains.kotlin.ir.util.constructors
import org.jetbrains.kotlin.ir.util.primaryConstructor
internal class JsUsefulDeclarationProcessor(
override val context: JsIrBackendContext,
printReachabilityInfo: Boolean,
removeUnusedAssociatedObjects: Boolean
) : UsefulDeclarationProcessor(printReachabilityInfo, removeUnusedAssociatedObjects) {
private val equalsMethod = getMethodOfAny("equals")
private val hashCodeMethod = getMethodOfAny("hashCode")
override val bodyVisitor: BodyVisitorBase = object : BodyVisitorBase() {
override fun visitCall(expression: IrCall, data: IrDeclaration) {
super.visitCall(expression, data)
when (expression.symbol) {
context.intrinsics.jsBoxIntrinsic -> {
val inlineClass = context.inlineClassesUtils.getInlinedClass(expression.getTypeArgument(0)!!)!!
val constructor = inlineClass.declarations.filterIsInstance<IrConstructor>().single { it.isPrimary }
constructor.enqueue(data, "intrinsic: jsBoxIntrinsic")
}
context.intrinsics.jsClass -> {
val ref = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrDeclaration
ref.enqueue(data, "intrinsic: jsClass")
referencedJsClasses += ref
// When class reference provided as parameter to external function
// It can be instantiated by external JS script
// Need to leave constructor for this
// https://youtrack.jetbrains.com/issue/KT-46672
// TODO: Possibly solution with origin is not so good
// There is option with applying this hack to jsGetKClass
if (expression.origin == JsStatementOrigins.CLASS_REFERENCE) {
// Maybe we need to filter primary constructor
// Although at this time, we should have only primary constructor
(ref as IrClass)
.constructors
.forEach {
it.enqueue(data, "intrinsic: jsClass (constructor)")
}
}
}
context.reflectionSymbols.getKClassFromExpression -> {
val ref = expression.getTypeArgument(0)?.classOrNull ?: context.irBuiltIns.anyClass
referencedJsClassesFromExpressions += ref.owner
}
context.intrinsics.jsObjectCreate -> {
val classToCreate = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrClass
classToCreate.enqueue(data, "intrinsic: jsObjectCreate")
constructedClasses += classToCreate
}
context.intrinsics.jsEquals -> {
equalsMethod.enqueue(data, "intrinsic: jsEquals")
}
context.intrinsics.jsToString -> {
toStringMethod.enqueue(data, "intrinsic: jsToString")
}
context.intrinsics.jsHashCode -> {
hashCodeMethod.enqueue(data, "intrinsic: jsHashCode")
}
context.intrinsics.jsPlus -> {
if (expression.getValueArgument(0)?.type?.classOrNull == context.irBuiltIns.stringClass) {
toStringMethod.enqueue(data, "intrinsic: jsPlus")
}
}
context.intrinsics.jsConstruct -> {
val callType = expression.getTypeArgument(0)!!
val constructor = callType.getClass()!!.primaryConstructor
constructor!!.enqueue(data, "ctor call from jsConstruct-intrinsic")
}
context.intrinsics.es6DefaultType -> {
//same as jsClass
val ref = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrDeclaration
ref.enqueue(data, "intrinsic: jsClass")
referencedJsClasses += ref
//Generate klass in `val currResultType = resultType || klass`
val arg = expression.getTypeArgument(0)!!
val klass = arg.getClass()
if (klass != null) {
constructedClasses.add(klass)
}
}
context.intrinsics.jsInvokeSuspendSuperType,
context.intrinsics.jsInvokeSuspendSuperTypeWithReceiver,
context.intrinsics.jsInvokeSuspendSuperTypeWithReceiverAndParam -> {
invokeFunForLambda(expression)
.enqueue(data, "intrinsic: suspendSuperType")
}
}
}
}
override fun processConstructedClassDeclaration(declaration: IrDeclaration) {
if (declaration in result) return
super.processConstructedClassDeclaration(declaration)
if (declaration is IrSimpleFunction && declaration.getJsNameOrKotlinName().asString() == "valueOf") {
declaration.enqueue(declaration, "valueOf")
}
// 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(declaration, "annotated by @JsName")
}
}
private val referencedJsClasses = hashSetOf<IrDeclaration>()
private val referencedJsClassesFromExpressions = hashSetOf<IrClass>()
override fun handleAssociatedObjects() {
//Handle objects, constructed via `findAssociatedObject` annotation
referencedJsClassesFromExpressions += constructedClasses.filterDescendantsOf(referencedJsClassesFromExpressions) // Grow the set of possible results of instance::class expression
for (klass in classesWithObjectAssociations) {
if (removeUnusedAssociatedObjects && klass !in referencedJsClasses && klass !in referencedJsClassesFromExpressions) continue
for (annotation in klass.annotations) {
val annotationClass = annotation.symbol.owner.constructedClass
if (removeUnusedAssociatedObjects && annotationClass !in referencedJsClasses) continue
annotation.associatedObject()?.let { obj ->
context.mapping.objectToGetInstanceFunction[obj]?.enqueue(klass, "associated object factory")
}
}
}
}
override fun isExported(declaration: IrDeclaration): Boolean = declaration.isExported(context)
}
private fun Collection<IrClass>.filterDescendantsOf(bases: Collection<IrClass>): Collection<IrClass> {
val visited = hashSetOf<IrClass>()
val baseDescendants = hashSetOf<IrClass>()
baseDescendants += bases
fun overridesAnyBase(klass: IrClass): Boolean {
if (klass in baseDescendants) return true
if (klass in visited) return false
visited += klass
klass.superTypes.forEach {
(it.classifierOrNull as? IrClassSymbol)?.owner?.let { klass ->
if (overridesAnyBase(klass)) {
baseDescendants += klass
return true
}
}
}
return false
}
return this.filter { overridesAnyBase(it) }
}
@@ -0,0 +1,235 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.ir.backend.js.dce
import org.jetbrains.kotlin.backend.common.ir.isMemberOfOpenClass
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.backend.js.JsCommonBackendContext
import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.types.classifierOrNull
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.IrElementVisitor
import java.util.*
abstract class UsefulDeclarationProcessor(
private val printReachabilityInfo: Boolean,
protected val removeUnusedAssociatedObjects: Boolean
) {
abstract val context: JsCommonBackendContext
protected fun getMethodOfAny(name: String): IrDeclaration =
context.irBuiltIns.anyClass.owner.declarations.filterIsInstance<IrFunction>().single { it.name.asString() == name }
protected val toStringMethod: IrDeclaration by lazy { getMethodOfAny("toString") }
protected abstract fun isExported(declaration: IrDeclaration): Boolean
protected abstract val bodyVisitor: BodyVisitorBase
protected abstract inner class BodyVisitorBase : IrElementVisitor<Unit, IrDeclaration> {
override fun visitValueAccess(expression: IrValueAccessExpression, data: IrDeclaration) = visitVariableAccess(expression, data)
override fun visitGetValue(expression: IrGetValue, data: IrDeclaration) = visitVariableAccess(expression, data)
override fun visitSetValue(expression: IrSetValue, data: IrDeclaration) = visitVariableAccess(expression, data)
private fun visitVariableAccess(expression: IrValueAccessExpression, data: IrDeclaration) {
visitDeclarationReference(expression, data)
expression.symbol.owner.enqueue(data, "variable access")
}
override fun visitElement(element: IrElement, data: IrDeclaration) {
element.acceptChildren(this, data)
}
override fun visitFunctionAccess(expression: IrFunctionAccessExpression, data: IrDeclaration) {
super.visitFunctionAccess(expression, data)
expression.symbol.owner.enqueue(data, "function access")
}
override fun visitRawFunctionReference(expression: IrRawFunctionReference, data: IrDeclaration) {
super.visitRawFunctionReference(expression, data)
expression.symbol.owner.enqueue(data, "raw function access")
}
override fun visitFieldAccess(expression: IrFieldAccessExpression, data: IrDeclaration) {
super.visitFieldAccess(expression, data)
expression.symbol.owner.enqueue(data, "field access")
}
override fun visitStringConcatenation(expression: IrStringConcatenation, data: IrDeclaration) {
super.visitStringConcatenation(expression, data)
toStringMethod.enqueue(data, "string concatenation")
}
}
private fun addReachabilityInfoIfNeeded(
from: IrDeclaration,
to: IrDeclaration,
description: String?,
isContagiousOverridableDeclaration: Boolean,
) {
if (!printReachabilityInfo) return
val fromFqn = (from as? IrDeclarationWithName)?.fqNameWhenAvailable?.asString() ?: "<unknown>"
val toFqn = (to as? IrDeclarationWithName)?.fqNameWhenAvailable?.asString() ?: "<unknown>"
val comment = (description ?: "") + (if (isContagiousOverridableDeclaration) "[CONTAGIOUS!]" else "")
val info = "\"$fromFqn\" -> \"$toFqn\"" + (if (comment.isBlank()) "" else " // $comment")
reachabilityInfo.add(info)
}
protected fun IrDeclaration.enqueue(
from: IrDeclaration,
description: String?,
isContagious: Boolean = true,
) {
// Ignore non-external IrProperty because we don't want to generate code for them and codegen doesn't support it.
if (this is IrProperty && !this.isExternal) return
// TODO check that this is overridable
// it requires fixing how functions with default arguments is handled
val isContagiousOverridableDeclaration = isContagious && this is IrOverridableDeclaration<*> && this.isMemberOfOpenClass
addReachabilityInfoIfNeeded(from, this, description, isContagiousOverridableDeclaration)
if (isContagiousOverridableDeclaration) {
contagiousReachableDeclarations.add(this as IrOverridableDeclaration<*>)
}
if (this !in result) {
result.add(this)
queue.addLast(this)
}
}
// This collection contains declarations whose reachability should be propagated to overrides.
// Overriding uncontagious declaration will not lead to becoming a declaration reachable.
// By default, all declarations treated as contagious, it's not the most efficient, but it's safest.
// In case when we access a declaration through a fake-override declaration, the original (real) one will not be marked as contagious,
// so, later, other overrides will not be processed unconditionally only because it overrides a reachable declaration.
//
// The collection must be a subset of [result] set.
private val contagiousReachableDeclarations = hashSetOf<IrOverridableDeclaration<*>>()
protected val constructedClasses = hashSetOf<IrClass>()
private val reachabilityInfo: MutableSet<String> = if (printReachabilityInfo) linkedSetOf() else Collections.emptySet()
private val queue = ArrayDeque<IrDeclaration>()
protected val result = hashSetOf<IrDeclaration>()
protected val classesWithObjectAssociations = hashSetOf<IrClass>()
protected open fun processField(irField: IrField): Unit = Unit
protected open fun processClass(irClass: IrClass) {
irClass.superTypes.forEach {
(it.classifierOrNull as? IrClassSymbol)?.owner?.enqueue(irClass, "superTypes")
}
if (irClass.isObject && isExported(irClass)) {
context.mapping.objectToGetInstanceFunction[irClass]
?.enqueue(irClass, "Exported object getInstance function")
}
irClass.annotations.forEach {
val annotationClass = it.symbol.owner.constructedClass
if (annotationClass.isAssociatedObjectAnnotatedAnnotation) {
classesWithObjectAssociations += irClass
annotationClass.enqueue(irClass, "@AssociatedObject annotated annotation class")
}
}
}
protected open fun processSimpleFunction(irFunction: IrSimpleFunction) {
if (irFunction.isFakeOverride) {
irFunction.resolveFakeOverride()?.enqueue(irFunction, "real overridden fun", isContagious = false)
}
}
protected open fun processConstructor(irConstructor: IrConstructor) {
// Collect instantiated classes.
irConstructor.constructedClass.let {
it.enqueue(irConstructor, "constructed class")
constructedClasses += it
}
}
protected open fun processConstructedClassDeclaration(declaration: IrDeclaration) {
if (declaration in result) return
fun IrOverridableDeclaration<*>.findOverriddenContagiousDeclaration(): IrOverridableDeclaration<*>? {
for (overriddenSymbol in this.overriddenSymbols) {
val overriddenDeclaration = overriddenSymbol.owner as? IrOverridableDeclaration<*> ?: continue
if (overriddenDeclaration in contagiousReachableDeclarations) return overriddenDeclaration
overriddenDeclaration.findOverriddenContagiousDeclaration()?.let {
return it
}
}
return null
}
if (declaration is IrOverridableDeclaration<*>) {
declaration.findOverriddenContagiousDeclaration()?.let {
declaration.enqueue(it, "overrides useful declaration")
}
}
// A hack to enforce property lowering.
// Until a getter is accessed it doesn't get moved to the declaration list.
if (declaration is IrProperty) {
declaration.getter?.run {
findOverriddenContagiousDeclaration()?.let { enqueue(declaration, "(getter) overrides useful declaration") }
}
declaration.setter?.run {
findOverriddenContagiousDeclaration()?.let { enqueue(declaration, "(setter) overrides useful declaration") }
}
}
}
protected open fun handleAssociatedObjects(): Unit = Unit
fun collectDeclarations(rootDeclarations: Iterable<IrDeclaration>): Set<IrDeclaration> {
rootDeclarations.forEach {
it.enqueue(it, "<ROOT>")
}
while (queue.isNotEmpty()) {
while (queue.isNotEmpty()) {
val declaration = queue.pollFirst()
when (declaration) {
is IrClass -> processClass(declaration)
is IrSimpleFunction -> processSimpleFunction(declaration)
is IrConstructor -> processConstructor(declaration)
is IrField -> processField(declaration)
}
val body = when (declaration) {
is IrFunction -> declaration.body
is IrField -> declaration.initializer
is IrVariable -> declaration.initializer
else -> null
}
body?.accept(bodyVisitor, declaration)
}
handleAssociatedObjects()
for (klass in constructedClasses) {
// TODO a better way to support inverse overrides.
for (declaration in klass.declarations) {
processConstructedClassDeclaration(declaration)
}
}
}
if (printReachabilityInfo) {
reachabilityInfo.forEach(::println)
}
return result
}
}
@@ -0,0 +1,108 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.ir.backend.js.dce
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
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.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.js.config.RuntimeDiagnostic
class UselessDeclarationsRemover(
private val removeUnusedAssociatedObjects: Boolean,
private val usefulDeclarations: Set<IrDeclaration>,
private val context: JsIrBackendContext,
private val dceRuntimeDiagnostic: RuntimeDiagnostic?,
) : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitFile(declaration: IrFile) {
process(declaration)
}
private fun IrConstructorCall.shouldKeepAnnotation(): Boolean {
associatedObject()?.let { obj ->
if (obj !in usefulDeclarations) return false
}
return true
}
override fun visitClass(declaration: IrClass) {
process(declaration)
// Remove annotations for `findAssociatedObject` feature, which reference objects eliminated by the DCE.
// Otherwise `JsClassGenerator.generateAssociatedKeyProperties` will try to reference the object factory (which is removed).
// That will result in an error from the Namer. It cannot generate a name for an absent declaration.
if (removeUnusedAssociatedObjects && declaration.annotations.any { !it.shouldKeepAnnotation() }) {
declaration.annotations = declaration.annotations.filter { it.shouldKeepAnnotation() }
}
}
// TODO bring back the primary constructor fix
private fun process(container: IrDeclarationContainer) {
container.declarations.transformFlat { member ->
if (member !in usefulDeclarations) {
member.processUselessDeclaration()
} else {
member.acceptVoid(this)
null
}
}
}
private fun IrDeclaration.processUselessDeclaration(): List<IrDeclaration>? {
return when {
dceRuntimeDiagnostic != null -> {
processWithDiagnostic(dceRuntimeDiagnostic)
null
}
else -> emptyList()
}
}
private fun RuntimeDiagnostic.removingBody(): Boolean =
this != RuntimeDiagnostic.LOG
private fun IrDeclaration.processWithDiagnostic(dceRuntimeDiagnostic: RuntimeDiagnostic) {
when (this) {
is IrFunction -> processFunctionWithDiagnostic(dceRuntimeDiagnostic)
is IrField -> processFieldWithDiagnostic()
is IrDeclarationContainer -> declarations.forEach { it.processWithDiagnostic(dceRuntimeDiagnostic) }
}
}
private fun IrFunction.processFunctionWithDiagnostic(dceRuntimeDiagnostic: RuntimeDiagnostic) {
val isRemovingBody = dceRuntimeDiagnostic.removingBody()
val targetMethod = dceRuntimeDiagnostic.unreachableDeclarationMethod(context)
val call = JsIrBuilder.buildCall(
target = targetMethod,
type = targetMethod.owner.returnType
)
if (isRemovingBody) {
body = context.irFactory.createBlockBody(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET
)
}
body?.prependFunctionCall(call)
}
private fun IrField.processFieldWithDiagnostic() {
if (initializer != null && isKotlinPackage()) {
initializer = null
}
}
}
@@ -8,7 +8,7 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.ir.backend.js.CompilationOutputs
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.dce.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
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.ir.backend.js.transformers.irToJs
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.ir.backend.js.*
import org.jetbrains.kotlin.ir.backend.js.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.ir.backend.js.export.*
import org.jetbrains.kotlin.ir.backend.js.lower.StaticMembersLowering
import org.jetbrains.kotlin.ir.backend.js.utils.*
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.backend.wasm
import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel
import org.jetbrains.kotlin.backend.wasm.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmCompiledModuleFragment
import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmModuleFragmentGenerator
import org.jetbrains.kotlin.backend.wasm.lower.markExportedDeclarations
@@ -31,6 +32,7 @@ fun compileWasm(
exportedDeclarations: Set<FqName> = emptySet(),
propertyLazyInitialization: Boolean,
emitNameSection: Boolean = false,
dceEnabled: Boolean = false,
): WasmCompilerResult {
val mainModule = depsDescriptors.mainModule
val configuration = depsDescriptors.compilerConfiguration
@@ -69,8 +71,12 @@ fun compileWasm(
wasmPhases.invokeToplevel(phaseConfig, context, moduleFragment)
if (dceEnabled) {
eliminateDeadDeclarations(listOf(moduleFragment), context)
}
val compiledWasmModule = WasmCompiledModuleFragment(context.irBuiltIns)
val codeGenerator = WasmModuleFragmentGenerator(context, compiledWasmModule)
val codeGenerator = WasmModuleFragmentGenerator(context, compiledWasmModule, allowIncompleteImplementations = dceEnabled)
codeGenerator.generateModule(moduleFragment)
val linkedModule = compiledWasmModule.linkWasmCompiledFragments()
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.backend.wasm.dce
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.IrBody
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
internal fun eliminateDeadDeclarations(modules: List<IrModuleFragment>, context: WasmBackendContext) {
val printReachabilityInfo =
context.configuration.getBoolean(JSConfigurationKeys.PRINT_REACHABILITY_INFO) ||
java.lang.Boolean.getBoolean("kotlin.wasm.dce.print.reachability.info")
val usefulDeclarations = WasmUsefulDeclarationProcessor(
context = context,
printReachabilityInfo = printReachabilityInfo
).collectDeclarations(rootDeclarations = buildRoots(modules, context))
val remover = WasmUselessDeclarationsRemover(usefulDeclarations)
modules.onAllFiles {
acceptVoid(remover)
}
}
private fun buildRoots(modules: List<IrModuleFragment>, context: WasmBackendContext): List<IrDeclaration> = buildList {
val declarationsCollector = object : IrElementVisitorVoid {
override fun visitElement(element: IrElement): Unit = element.acceptChildrenVoid(this)
override fun visitBody(body: IrBody): Unit = Unit // Skip
override fun visitDeclaration(declaration: IrDeclarationBase) {
super.visitDeclaration(declaration)
add(declaration)
}
}
modules.onAllFiles {
declarations.forEach { declaration ->
if (declaration.isJsExport()) {
declaration.acceptVoid(declarationsCollector)
}
}
}
add(context.irBuiltIns.throwableClass.owner)
add(context.mainCallsWrapperFunction)
add(context.fieldInitFunction)
}
private inline fun List<IrModuleFragment>.onAllFiles(body: IrFile.() -> Unit) {
forEach { module ->
module.files.forEach { file ->
file.body()
}
}
}
@@ -0,0 +1,205 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.backend.wasm.dce
import org.jetbrains.kotlin.backend.common.ir.isOverridable
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.ir2wasm.*
import org.jetbrains.kotlin.backend.wasm.utils.*
import org.jetbrains.kotlin.ir.backend.js.dce.UsefulDeclarationProcessor
import org.jetbrains.kotlin.ir.backend.js.utils.*
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.wasm.ir.*
internal class WasmUsefulDeclarationProcessor(
override val context: WasmBackendContext,
printReachabilityInfo: Boolean
) : UsefulDeclarationProcessor(printReachabilityInfo, removeUnusedAssociatedObjects = false) {
private val unitGetInstance: IrSimpleFunction = context.findUnitGetInstanceFunction()
override val bodyVisitor: BodyVisitorBase = object : BodyVisitorBase() {
override fun visitConst(expression: IrConst<*>, data: IrDeclaration) = when (expression.kind) {
is IrConstKind.Null -> expression.type.enqueueType(data, "expression type")
is IrConstKind.String -> context.wasmSymbols.stringGetLiteral.owner
.enqueue(data, "String literal intrinsic getter stringGetLiteral")
else -> Unit
}
private fun tryToProcessIntrinsicCall(from: IrDeclaration, call: IrCall): Boolean = when (call.symbol) {
context.wasmSymbols.unboxIntrinsic -> {
val fromType = call.getTypeArgument(0)
if (fromType != null && !fromType.isNothing() && !fromType.isNullableNothing()) {
val backingField = call.getTypeArgument(1)
?.let { context.inlineClassesUtils.getInlinedClass(it) }
?.let { getInlineClassBackingField(it) }
backingField?.enqueue(from, "backing inline class field for unboxIntrinsic")
}
true
}
context.wasmSymbols.wasmClassId,
context.wasmSymbols.wasmInterfaceId,
context.wasmSymbols.wasmRefCast -> {
call.getTypeArgument(0)?.getClass()?.enqueue(from, "generic intrinsic ${call.symbol.owner.name}")
true
}
else -> false
}
private fun tryToProcessWasmOpIntrinsicCall(from: IrDeclaration, call: IrCall, function: IrFunction): Boolean {
if (function.hasWasmNoOpCastAnnotation()) {
return true
}
val opString = function.getWasmOpAnnotation()
if (opString != null) {
val op = WasmOp.valueOf(opString)
when (op.immediates.size) {
0 -> {
if (op == WasmOp.REF_TEST) {
call.getTypeArgument(0)?.enqueueRuntimeClassOrAny(from, "REF_TEST")
}
}
1 -> {
if (op.immediates.firstOrNull() == WasmImmediateKind.STRUCT_TYPE_IDX) {
function.dispatchReceiverParameter?.type?.classOrNull?.owner?.enqueue(from, "STRUCT_TYPE_IDX")
}
}
}
return true
}
return false
}
override fun visitCall(expression: IrCall, data: IrDeclaration) {
super.visitCall(expression, data)
if (expression.symbol == context.wasmSymbols.boxIntrinsic) {
expression.getTypeArgument(0)?.enqueueRuntimeClassOrAny(data, "boxIntrinsic")
return
}
val function: IrFunction = expression.symbol.owner.realOverrideTarget
if (function.returnType == context.irBuiltIns.unitType) {
unitGetInstance.enqueue(data, "function Unit return type")
}
if (tryToProcessIntrinsicCall(data, expression)) return
if (tryToProcessWasmOpIntrinsicCall(data, expression, function)) return
val isSuperCall = expression.superQualifierSymbol != null
if (function is IrSimpleFunction && function.isOverridable && !isSuperCall) {
val klass = function.parentAsClass
if (!klass.isInterface) {
context.wasmSymbols.getVirtualMethodId.owner.enqueue(data, "call on class receiver")
} else {
klass.enqueue(data, "receiver class")
context.wasmSymbols.getInterfaceImplId.owner.enqueue(data, "call on interface receiver")
}
function.enqueue(data, "method call")
}
}
}
private fun IrType.getInlinedValueTypeIfAny(): IrType? = when (this) {
context.irBuiltIns.booleanType,
context.irBuiltIns.byteType,
context.irBuiltIns.shortType,
context.irBuiltIns.charType,
context.irBuiltIns.booleanType,
context.irBuiltIns.byteType,
context.irBuiltIns.shortType,
context.irBuiltIns.intType,
context.irBuiltIns.charType,
context.irBuiltIns.longType,
context.irBuiltIns.floatType,
context.irBuiltIns.doubleType,
context.irBuiltIns.nothingType,
context.wasmSymbols.voidType -> null
else -> when {
isBuiltInWasmRefType(this) -> null
erasedUpperBound?.isExternal == true -> null
else -> when (val ic = context.inlineClassesUtils.getInlinedClass(this)) {
null -> this
else -> context.inlineClassesUtils.getInlineClassUnderlyingType(ic).getInlinedValueTypeIfAny()
}
}
}
private fun IrType.enqueueRuntimeClassOrAny(from: IrDeclaration, info: String): Unit =
(this.getRuntimeClass ?: context.wasmSymbols.any.owner).enqueue(from, info, isContagious = false)
private fun IrType.enqueueType(from: IrDeclaration, info: String) {
getInlinedValueTypeIfAny()
?.enqueueRuntimeClassOrAny(from, info)
}
private fun IrDeclaration.enqueueParentClass() {
parentClassOrNull?.enqueue(this, "parent class", isContagious = false)
}
override fun processField(irField: IrField) {
super.processField(irField)
irField.enqueueParentClass()
irField.type.enqueueType(irField, "field types")
}
override fun processClass(irClass: IrClass) {
super.processClass(irClass)
irClass.getWasmArrayAnnotation()?.type
?.enqueueType(irClass, "array type for wasm array annotated")
if (context.inlineClassesUtils.isClassInlineLike(irClass)) {
irClass.declarations
.firstIsInstanceOrNull<IrConstructor>()
?.takeIf { it.isPrimary }
?.enqueue(irClass, "inline class primary ctor")
}
}
private fun IrValueParameter.enqueueValueParameterType(from: IrDeclaration) {
if (context.inlineClassesUtils.shouldValueParameterBeBoxed(this)) {
type.enqueueRuntimeClassOrAny(from, "function ValueParameterType")
} else {
type.enqueueType(from, "function ValueParameterType")
}
}
private fun processIrFunction(irFunction: IrFunction) {
if (irFunction.isFakeOverride) return
val isIntrinsic = irFunction.hasWasmNoOpCastAnnotation() || irFunction.getWasmOpAnnotation() != null
if (isIntrinsic) return
irFunction.getEffectiveValueParameters().forEach { it.enqueueValueParameterType(irFunction) }
irFunction.returnType.enqueueType(irFunction, "function return type")
}
override fun processSimpleFunction(irFunction: IrSimpleFunction) {
super.processSimpleFunction(irFunction)
irFunction.enqueueParentClass()
if (irFunction.isFakeOverride) {
irFunction.overriddenSymbols.forEach { overridden ->
overridden.owner.enqueue(irFunction, "original for fake-override")
}
}
processIrFunction(irFunction)
}
override fun processConstructor(irConstructor: IrConstructor) {
super.processConstructor(irConstructor)
if (!context.inlineClassesUtils.isClassInlineLike(irConstructor.parentAsClass)) {
processIrFunction(irConstructor)
}
}
override fun isExported(declaration: IrDeclaration): Boolean = declaration.isJsExport()
}
@@ -0,0 +1,44 @@
/*
* 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.backend.wasm.dce
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrDeclarationContainer
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.util.transformFlat
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.acceptVoid
class WasmUselessDeclarationsRemover(
private val usefulDeclarations: Set<IrDeclaration>
) : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitFile(declaration: IrFile) {
process(declaration)
}
override fun visitClass(declaration: IrClass) {
process(declaration)
}
// TODO bring back the primary constructor fix
private fun process(container: IrDeclarationContainer) {
container.declarations.transformFlat { member ->
if (member !in usefulDeclarations) {
emptyList()
} else {
member.acceptVoid(this)
null
}
}
}
}
@@ -271,7 +271,6 @@ class BodyGenerator(val context: WasmFunctionCodegenContext) : IrElementVisitorV
}
}
if (tryToGenerateIntrinsicCall(call, function)) {
if (function.returnType == irBuiltIns.unitType)
body.buildGetUnit()
@@ -284,7 +283,7 @@ class BodyGenerator(val context: WasmFunctionCodegenContext) : IrElementVisitorV
val klass = function.parentAsClass
if (!klass.isInterface) {
val classMetadata = context.getClassMetadata(klass.symbol)
val vfSlot = classMetadata.virtualMethods.map { it.function }.indexOf(function)
val vfSlot = classMetadata.virtualMethods.indexOfFirst { it.function == function }
// Dispatch receiver should be simple and without side effects at this point
// TODO: Verify
generateExpression(call.dispatchReceiver!!)
@@ -30,7 +30,7 @@ import org.jetbrains.kotlin.ir.visitors.acceptVoid
import org.jetbrains.kotlin.name.parentOrNull
import org.jetbrains.kotlin.wasm.ir.*
class DeclarationGenerator(val context: WasmModuleCodegenContext) : IrElementVisitorVoid {
class DeclarationGenerator(val context: WasmModuleCodegenContext, private val allowIncompleteImplementations: Boolean) : IrElementVisitorVoid {
// Shortcuts
private val backendContext: WasmBackendContext = context.backendContext
@@ -238,7 +238,6 @@ class DeclarationGenerator(val context: WasmModuleCodegenContext) : IrElementVis
wasmExpressionGenerator.buildRttCanon(wasmGcType)
}
val rtt = WasmGlobal(
name = "rtt_of_$nameStr",
isMutable = false,
@@ -258,13 +257,16 @@ class DeclarationGenerator(val context: WasmModuleCodegenContext) : IrElementVis
// TODO: Cache it
val interfaceMetadata = InterfaceMetadata(i, irBuiltIns)
val table = interfaceMetadata.methods.associate { method ->
val classMethod: VirtualMethodMetadata =
metadata.virtualMethods
.find { it.signature == method.signature } // TODO: Use map
?: error("Cannot find class implementation of method ${method.signature} in class ${declaration.fqNameWhenAvailable}")
val classMethod: VirtualMethodMetadata? = metadata.virtualMethods
.find { it.signature == method.signature && it.function.modality != Modality.ABSTRACT } // TODO: Use map
method.function.symbol as IrFunctionSymbol to context.referenceFunction(classMethod.function.symbol)
if (classMethod == null && !allowIncompleteImplementations) {
error("Cannot find class implementation of method ${method.signature} in class ${declaration.fqNameWhenAvailable}")
}
val matchedMethod = classMethod?.let { context.referenceFunction(it.function.symbol) }
method.function.symbol as IrFunctionSymbol to matchedMethod
}
context.registerInterfaceImplementationMethod(
interfaceImplementation,
table
@@ -74,7 +74,7 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) {
ReferencableElements<InterfaceImplementation, Int>()
val interfaceImplementationsMethods =
LinkedHashMap<InterfaceImplementation, Map<IrFunctionSymbol, WasmSymbol<WasmFunction>>>()
LinkedHashMap<InterfaceImplementation, Map<IrFunctionSymbol, WasmSymbol<WasmFunction>?>>()
val exports = mutableListOf<WasmExport<*>>()
@@ -83,6 +83,7 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) {
val jsFuns = mutableListOf<JsCodeSnippet>()
class FunWithPriority(val function: WasmFunction, val priority: String)
val initFunctions = mutableListOf<FunWithPriority>()
val scratchMemAddr = WasmSymbol<Int>()
@@ -229,28 +230,34 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) {
)
val interfaceTableElementsLists = interfaceMethodTables.defined.keys.associateWith {
mutableMapOf<Int, WasmSymbol<WasmFunction>>()
mutableMapOf<Int, WasmSymbol<WasmFunction>?>()
}
interfaceImplementationIds.forEach { ii: InterfaceImplementation, implId: Int ->
for ((interfaceFunction: IrFunctionSymbol, wasmFunction: WasmSymbol<WasmFunction>) in interfaceImplementationsMethods[ii]!!) {
for ((ii: InterfaceImplementation, implId: Int) in interfaceImplementationIds) {
for ((interfaceFunction: IrFunctionSymbol, wasmFunction: WasmSymbol<WasmFunction>?) in interfaceImplementationsMethods[ii]!!) {
interfaceTableElementsLists[interfaceFunction]!![implId] = wasmFunction
}
}
val interfaceTableElements = interfaceTableElementsLists.map { (interfaceFunction, methods) ->
val type = interfaceMethodTables.defined[interfaceFunction]!!.elementType
val methodTable = interfaceMethodTables.defined[interfaceFunction]!!
val type = methodTable.elementType
val functions = MutableList(methods.size) { idx ->
val wasmFunc = methods[idx]!!
val wasmFunc = methods[idx]
val expression = buildWasmExpression {
buildInstr(WasmOp.REF_FUNC, WasmImmediate.FuncIdx(wasmFunc))
if (wasmFunc != null) {
buildInstr(WasmOp.REF_FUNC, WasmImmediate.FuncIdx(wasmFunc))
} else {
//DCE could remove implementation from class, so we should to put a stub into method implementations table
buildRefNull(type.getHeapType())
}
}
WasmTable.Value.Expression(expression)
}
WasmElement(
type,
values = functions,
WasmElement.Mode.Active(interfaceMethodTables.defined[interfaceFunction]!!, offsetExpr)
WasmElement.Mode.Active(methodTable, offsetExpr)
)
}
@@ -35,7 +35,7 @@ interface WasmModuleCodegenContext : WasmBaseCodegenContext {
fun registerInterfaceImplementationMethod(
interfaceImplementation: InterfaceImplementation,
table: Map<IrFunctionSymbol, WasmSymbol<WasmFunction>>,
table: Map<IrFunctionSymbol, WasmSymbol<WasmFunction>?>,
)
fun referenceInterfaceImplementationId(interfaceImplementation: InterfaceImplementation): WasmSymbol<Int>
@@ -126,7 +126,7 @@ class WasmModuleCodegenContextImpl(
override fun registerInterfaceImplementationMethod(
interfaceImplementation: InterfaceImplementation,
table: Map<IrFunctionSymbol, WasmSymbol<WasmFunction>>
table: Map<IrFunctionSymbol, WasmSymbol<WasmFunction>?>
) {
wasmFragment.interfaceImplementationsMethods[interfaceImplementation] = table
}
@@ -13,14 +13,16 @@ import org.jetbrains.kotlin.ir.visitors.acceptVoid
class WasmModuleFragmentGenerator(
backendContext: WasmBackendContext,
wasmModuleFragment: WasmCompiledModuleFragment
wasmModuleFragment: WasmCompiledModuleFragment,
allowIncompleteImplementations: Boolean,
) {
private val declarationGenerator =
DeclarationGenerator(
WasmModuleCodegenContextImpl(
backendContext,
wasmModuleFragment
)
wasmModuleFragment,
),
allowIncompleteImplementations
)
fun generateModule(irModuleFragment: IrModuleFragment) {
@@ -2,6 +2,8 @@
// FIR status: not supported in JVM
// IGNORE_BACKEND: JVM, JVM_IR
// IGNORE_BACKEND: NATIVE
// IGNORE_BACKEND: WASM
// WASM_MUTE_REASON: LAZY_INIT_PROPERTIES
// IGNORE_LIGHT_ANALYSIS
// MODULE: lib1
// FILE: lib1.kt
@@ -16,6 +16,7 @@ import org.jetbrains.kotlin.ir.backend.js.codegen.CompilerOutputSink
import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationGranularity
import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationOptions
import org.jetbrains.kotlin.ir.backend.js.codegen.generateEsModules
import org.jetbrains.kotlin.ir.backend.js.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsManglerDesc
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformerTmp
@@ -33,7 +33,6 @@ import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.resolve.CompilerEnvironment
import org.jetbrains.kotlin.test.*
import org.jetbrains.kotlin.test.directives.JsEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.util.KtTestUtil
import java.io.Closeable
import java.io.File
@@ -176,15 +175,14 @@ abstract class BasicWasmBoxTest(
AnalyzerWithCompilerReport(config.configuration)
)
val directives = KotlinTestUtils.parseDirectives(FileUtil.loadFile(testFile))
val compilerResult = compileWasm(
sourceModule,
phaseConfig = phaseConfig,
irFactory = IrFactoryImpl,
exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, testFunction))),
propertyLazyInitialization = directives.contains(JsEnvironmentConfigurationDirectives.PROPERTY_LAZY_INITIALIZATION.name),
propertyLazyInitialization = true,
emitNameSection = true,
dceEnabled = true,
)
outputWatFile.write(compilerResult.wat)
+2
View File
@@ -1,4 +1,6 @@
// EXPECTED_REACHABLE_NODES: 1281
// IGNORE_BACKEND: WASM
// WASM_MUTE_REASON: LAZY_INIT_PROPERTIES
package foo
external interface Chrome {