[JS IR] Add to IR keep possibility similar to legacy-dce one

It helps to:

- keep declarations even if they are not reachable and not exported
- not minify names of not exported declarations

Compiler argument: -Xir-keep=A,B

Can be used for top-level declarations or for member

^KT-54118 fixed
This commit is contained in:
Ilya Goncharov
2022-09-20 16:06:17 +00:00
committed by Space
parent 4863e5d47b
commit d203dc35bf
28 changed files with 460 additions and 13 deletions
@@ -100,6 +100,13 @@ class K2JSCompilerArguments : CommonCompilerArguments() {
@Argument(value = "-target", valueDescription = "{ v5 }", description = "Generate JS files for specific ECMA version")
var target: String? by NullableStringFreezableVar(null)
@Argument(
value = "-Xir-keep",
description = "Comma-separated list of fully-qualified names to not be eliminated by DCE (if it can be reached), " +
"and for which to keep non-minified names."
)
var irKeep: String? by NullableStringFreezableVar(null)
@GradleOption(
value = DefaultValues.JsModuleKinds::class,
gradleInputType = GradleInputTypes.INPUT
@@ -100,6 +100,10 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
module,
phaseConfig,
irFactory,
keep = arguments.irKeep?.split(",")
?.filterNot { it.isEmpty() }
?.toSet()
?: emptySet(),
dceRuntimeDiagnostic = RuntimeDiagnostic.resolve(
arguments.irDceRuntimeDiagnostic,
messageCollector
@@ -66,6 +66,7 @@ class JsIrBackendContext(
val symbolTable: SymbolTable,
irModuleFragment: IrModuleFragment,
val additionalExportedDeclarationNames: Set<FqName>,
keep: Set<String>,
override val configuration: CompilerConfiguration, // TODO: remove configuration from backend context
override val scriptMode: Boolean = false,
override val es6mode: Boolean = false,
@@ -86,6 +87,9 @@ class JsIrBackendContext(
val minimizedNameGenerator: MinimizedNameGenerator =
MinimizedNameGenerator()
val keeper: Keeper =
Keeper(keep)
val fieldDataCache = mutableMapOf<IrClass, Map<IrField, String>>()
override val builtIns = module.builtIns
@@ -51,6 +51,7 @@ fun compile(
phaseConfig: PhaseConfig,
irFactory: IrFactory,
exportedDeclarations: Set<FqName> = emptySet(),
keep: Set<String> = emptySet(),
dceRuntimeDiagnostic: RuntimeDiagnostic? = null,
es6mode: Boolean = false,
verifySignatures: Boolean = true,
@@ -76,6 +77,7 @@ fun compile(
deserializer,
phaseConfig,
exportedDeclarations,
keep,
dceRuntimeDiagnostic,
es6mode,
baseClassIntoMetadata,
@@ -97,6 +99,7 @@ fun compileIr(
irLinker: JsIrLinker,
phaseConfig: PhaseConfig,
exportedDeclarations: Set<FqName>,
keep: Set<String>,
dceRuntimeDiagnostic: RuntimeDiagnostic?,
es6mode: Boolean,
baseClassIntoMetadata: Boolean,
@@ -119,6 +122,7 @@ fun compileIr(
symbolTable,
allModules.first(),
exportedDeclarations,
keep,
configuration,
es6mode = es6mode,
dceRuntimeDiagnostic = dceRuntimeDiagnostic,
@@ -126,7 +130,7 @@ fun compileIr(
safeExternalBoolean = safeExternalBoolean,
safeExternalBooleanDiagnostic = safeExternalBooleanDiagnostic,
granularity = granularity,
icCompatibleIr2Js = if (icCompatibleIr2Js) IcCompatibleIr2Js.COMPATIBLE else IcCompatibleIr2Js.DISABLED
icCompatibleIr2Js = if (icCompatibleIr2Js) IcCompatibleIr2Js.COMPATIBLE else IcCompatibleIr2Js.DISABLED,
)
// Load declarations referenced during `context` initialization
@@ -32,6 +32,7 @@ fun compileWithIC(
filesToLower: Collection<IrFile>,
mainArguments: List<String>? = null,
exportedDeclarations: Set<FqName> = emptySet(),
keep: Set<String> = emptySet(),
generateFullJs: Boolean = true,
generateDceJs: Boolean = false,
dceDriven: Boolean = false,
@@ -42,7 +43,7 @@ fun compileWithIC(
baseClassIntoMetadata: Boolean = false,
lowerPerModule: Boolean = false,
safeExternalBoolean: Boolean = false,
safeExternalBooleanDiagnostic: RuntimeDiagnostic? = null
safeExternalBooleanDiagnostic: RuntimeDiagnostic? = null,
): List<JsIrFragmentAndBinaryAst> {
val irBuiltIns = mainModule.irBuiltins
val symbolTable = (irBuiltIns as IrBuiltInsOverDescriptors).symbolTable
@@ -53,13 +54,14 @@ fun compileWithIC(
symbolTable,
mainModule,
exportedDeclarations,
keep,
configuration,
es6mode = es6mode,
dceRuntimeDiagnostic = dceRuntimeDiagnostic,
baseClassIntoMetadata = baseClassIntoMetadata,
safeExternalBoolean = safeExternalBoolean,
safeExternalBooleanDiagnostic = safeExternalBooleanDiagnostic,
icCompatibleIr2Js = IcCompatibleIr2Js.IC_MODE
icCompatibleIr2Js = IcCompatibleIr2Js.IC_MODE,
)
// Load declarations referenced during `context` initialization
@@ -57,6 +57,7 @@ private fun IrDeclaration.addRootsTo(
getter?.addRootsTo(nestedVisitor, context)
setter?.addRootsTo(nestedVisitor, context)
}
isEffectivelyExternal() -> {
val correspondingProperty = when (this) {
is IrField -> correspondingPropertySymbol?.owner
@@ -68,15 +69,18 @@ private fun IrDeclaration.addRootsTo(
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 -> {
val correspondingProperty = correspondingPropertySymbol?.owner ?: return
if (correspondingProperty.isExported(context)) {
@@ -43,6 +43,7 @@ internal class JsUsefulDeclarationProcessor(
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")
@@ -63,34 +64,42 @@ internal class JsUsefulDeclarationProcessor(
}
}
}
context.reflectionSymbols.getKClassFromExpression -> {
val ref = expression.getTypeArgument(0)?.classOrNull ?: context.irBuiltIns.anyClass
referencedJsClassesFromExpressions += ref.owner
}
context.intrinsics.jsObjectCreateSymbol -> {
val classToCreate = expression.getTypeArgument(0)!!.classifierOrFail.owner as IrClass
classToCreate.enqueue(data, "intrinsic: jsObjectCreateSymbol")
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
@@ -104,6 +113,7 @@ internal class JsUsefulDeclarationProcessor(
constructedClasses.add(klass)
}
}
context.intrinsics.jsInvokeSuspendSuperType,
context.intrinsics.jsInvokeSuspendSuperTypeWithReceiver,
context.intrinsics.jsInvokeSuspendSuperTypeWithReceiverAndParam -> {
@@ -126,6 +136,14 @@ internal class JsUsefulDeclarationProcessor(
override fun processClass(irClass: IrClass) {
super.processClass(irClass)
if (context.keeper.shouldKeep(irClass)) {
irClass.declarations
.filter { context.keeper.shouldKeep(it) }
.forEach { declaration ->
declaration.enqueue(irClass, "kept declaration")
}
}
if (irClass.containsMetadata()) {
when {
irClass.isObject -> context.intrinsics.metadataObjectConstructorSymbol.owner.enqueue(irClass, "object metadata")
@@ -114,6 +114,18 @@ class IrModuleToJsTransformerTmp(
}
private fun doStaticMembersLowering(modules: Iterable<IrModuleFragment>) {
modules.forEach { module ->
module.files.forEach {
it.accept(
backendContext.keeper,
Keeper.KeepData(
classInKeep = false,
classShouldBeKept = false
)
)
}
}
modules.forEach { module ->
module.files.forEach { StaticMembersLowering(backendContext).lower(it) }
}
@@ -86,10 +86,15 @@ class JsNameLinkingNamer(private val context: JsIrBackendContext, private val mi
override fun getNameForMemberFunction(function: IrSimpleFunction): JsName {
require(function.dispatchReceiverParameter != null)
val signature = jsFunctionSignature(function, context)
if (context.keeper.shouldKeep(function)) {
context.minimizedNameGenerator.keepName(signature)
}
val result = if (minimizedMemberNames && !function.hasStableJsName(context)) {
function.parentAsClass.fieldData()
context.minimizedNameGenerator.nameBySignature(signature)
} else signature
} else {
signature
}
return result.toJsName()
}
@@ -125,6 +130,7 @@ class JsNameLinkingNamer(private val context: JsIrBackendContext, private val mi
context.minimizedNameGenerator.reserveName(signature)
}
}
declaration is IrProperty -> {
if (declaration.isExported(context)) {
context.minimizedNameGenerator.reserveName(declaration.getJsNameOrKotlinName().identifier)
@@ -146,7 +152,7 @@ class JsNameLinkingNamer(private val context: JsIrBackendContext, private val mi
correspondingProperty.isSimpleProperty
val safeName = when {
hasStableName -> (correspondingProperty ?: it).getJsNameOrKotlinName().identifier
minimizedMemberNames -> context.minimizedNameGenerator.generateNextName()
minimizedMemberNames && !context.keeper.shouldKeep(it) -> context.minimizedNameGenerator.generateNextName()
else -> it.safeName()
}
val resultName = if (!hasStableName) {
@@ -156,6 +162,7 @@ class JsNameLinkingNamer(private val context: JsIrBackendContext, private val mi
} else safeName
result[it] = resultName
}
it is IrFunction && it.dispatchReceiverParameter != null -> {
nameCnt[jsFunctionSignature(it, context)] = 1 // avoid clashes with member functions
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.ir.backend.js.utils
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable
import org.jetbrains.kotlin.ir.visitors.IrElementVisitor
class Keeper(private val keep: Set<String>) : IrElementVisitor<Unit, Keeper.KeepData> {
private val keptDeclarations: MutableSet<IrDeclaration> = mutableSetOf()
fun shouldKeep(declaration: IrDeclaration): Boolean {
return declaration in keptDeclarations
}
override fun visitElement(element: IrElement, data: KeepData) {
element.acceptChildren(this, data)
}
/** Keep declarations can work both ways
* if member of a class is in keep, the class should be also kept
* if a class is kept, members of the class should be also kept
* but there can be nested classes, and for nested classes we need only to propagate "keep" from top-level to nested (not vice versa)
* because we have 2 directions, we need 2 boolean flags
* [KeepData.classInKeep] responsible to propagate "keep" from class level to members direction
* [KeepData.classShouldBeKept] responsible to bubble "keep" from members to class level direction
*/
override fun visitClass(declaration: IrClass, data: KeepData) {
val prevShouldBeKept = data.classShouldBeKept
val prevClassInKeep = data.classInKeep
data.classShouldBeKept = false
val keptClass = data.classInKeep || isInKeep(declaration)
if (keptClass) {
keptDeclarations.add(declaration)
}
data.classInKeep = keptClass
super.visitClass(declaration, data)
if (data.classShouldBeKept) {
keptDeclarations.add(declaration)
}
data.classShouldBeKept = prevShouldBeKept
data.classInKeep = prevClassInKeep
}
override fun visitDeclaration(declaration: IrDeclarationBase, data: KeepData) {
super.visitDeclaration(declaration, data)
if (declaration in keptDeclarations) {
return
}
if (declaration is IrDeclarationWithName && isInKeep(declaration) || data.classInKeep) {
keptDeclarations.add(declaration)
data.classShouldBeKept = true
return
}
}
private fun isInKeep(declaration: IrDeclarationWithName): Boolean {
return declaration.fqNameWhenAvailable?.asString() in keep
}
class KeepData(var classInKeep: Boolean, var classShouldBeKept: Boolean)
}
@@ -9,6 +9,7 @@ class MinimizedNameGenerator {
private var index = 0
private val functionSignatureToName = mutableMapOf<String, String>()
private val reservedNames = mutableSetOf<String>()
private val keptNames = mutableSetOf<String>()
fun generateNextName(): String {
var candidate = index++.toJsIdentifier()
@@ -19,11 +20,16 @@ class MinimizedNameGenerator {
}
fun nameBySignature(signature: String): String {
if (signature in keptNames) return signature
return functionSignatureToName.getOrPut(signature) {
generateNextName()
}
}
fun keepName(signature: String): Boolean {
return keptNames.add(signature)
}
fun reserveName(signature: String) {
reservedNames.add(signature)
}
+1
View File
@@ -19,6 +19,7 @@ where advanced options include:
Enable runtime diagnostics when performing DCE instead of removing declarations
-Xir-generate-inline-anonymous-functions
Lambda expressions that capture values are translated into in-line anonymous JavaScript functions
-Xir-keep Comma-separated list of fully-qualified names to not be eliminated by DCE (if it can be reached), and for which to keep non-minified names.
-Xir-minimized-member-names Perform minimization for names of members
-Xir-module-name=<name> Specify a compilation module name for IR backend
-Xir-new-ir2js New fragment-based ir2js
@@ -210,6 +210,11 @@ object JsEnvironmentConfigurationDirectives : SimpleDirectivesContainer() {
applicability = DirectiveApplicability.Global
)
val ONLY_IR_DCE by directive(
description = "Disable non DCE build",
applicability = DirectiveApplicability.Global
)
val RUN_IC by directive(
description = "",
applicability = DirectiveApplicability.Global
@@ -251,4 +256,9 @@ object JsEnvironmentConfigurationDirectives : SimpleDirectivesContainer() {
""".trimIndent(),
applicability = DirectiveApplicability.Global,
)
val KEEP by stringDirective(
description = "Keep declarations",
applicability = DirectiveApplicability.Global
)
}