[K/JS] Add VOID optimization on object properties + align with the optimization the JS Plain Object plugin

This commit is contained in:
Artem Kobzar
2024-01-19 13:03:06 +00:00
committed by Space Team
parent b2c30921e4
commit 7568ee5a79
14 changed files with 153 additions and 46 deletions
@@ -11,6 +11,8 @@ import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel
import org.jetbrains.kotlin.ir.backend.js.dce.DceDumpNameCache
import org.jetbrains.kotlin.ir.backend.js.dce.eliminateDeadDeclarations
import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.JsIrProgramFragment
import org.jetbrains.kotlin.ir.backend.js.utils.JsStaticContext
import org.jetbrains.kotlin.ir.backend.js.utils.getVoid
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.js.backend.ast.JsClass
import org.jetbrains.kotlin.js.backend.ast.JsFunction
@@ -35,11 +37,13 @@ fun optimizeProgramByIr(
}
}
fun optimizeFragmentByJsAst(fragment: JsIrProgramFragment) {
fun optimizeFragmentByJsAst(fragment: JsIrProgramFragment, context: JsStaticContext) {
val voidName = context.backendContext.intrinsics.void.owner.backingField?.let(context::getNameForField)
val optimizer = object : RecursiveJsVisitor() {
override fun visitFunction(x: JsFunction) {
super.visitFunction(x)
FunctionPostProcessor(x).apply()
FunctionPostProcessor(x, voidName).apply()
}
override fun visitClass(x: JsClass) {
@@ -10,6 +10,7 @@ import org.jetbrains.kotlin.ir.backend.js.utils.JsGenerationContext
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.backend.ast.metadata.constant
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
class IrDeclarationToJsTransformer : BaseIrElementToJsNodeTransformer<JsStatement, JsGenerationContext> {
@@ -471,7 +471,7 @@ class IrModuleToJsTransformer(
result.computeAndSaveDefinitions(definitionSet, fileExports)
if (optimizeGeneratedJs) {
optimizeFragmentByJsAst(result)
optimizeFragmentByJsAst(result, staticContext)
}
return JsIrProgramFragments(result, exportFragment)
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.types.IrSimpleType
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.backend.ast.metadata.constant
import org.jetbrains.kotlin.utils.DFS
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
@@ -50,7 +51,11 @@ class JsNameLinkingNamer(
}
}
return declaration.getName()
return declaration.getName().also {
if (declaration == context.intrinsics.void.owner.backingField) {
it.constant = true
}
}
}
override fun getNameForMemberFunction(function: IrSimpleFunction): JsName {
@@ -459,6 +459,7 @@ private class JsIrAstDeserializer(private val source: ByteArray) {
} ?: JsDynamicScope.declareName(identifier)
ifTrue { name.localAlias = readLocalAlias() }
ifTrue { name.imported = true }
ifTrue { name.constant = true }
ifTrue { name.specialFunction = specialFunctionValues[readInt()] }
return name
}
@@ -605,6 +605,7 @@ private class JsIrAstSerializer {
writeBoolean(name.isTemporary)
ifNotNull(name.localAlias) { writeLocalAlias(it) }
writeBoolean(name.imported && name !in importedNames)
writeBoolean(name.constant)
ifNotNull(name.specialFunction) { writeByte(it.ordinal) }
}
nameMap.size
@@ -77,6 +77,7 @@ var JsFunction.functionDescriptor: FunctionDescriptor? by MetadataProperty(defau
*/
var JsReturn.returnTarget: FunctionDescriptor? by MetadataProperty(default = null)
var HasMetadata.constant: Boolean by MetadataProperty(default = false)
var HasMetadata.synthetic: Boolean by MetadataProperty(default = false)
var HasMetadata.isInlineClassBoxing: Boolean by MetadataProperty(default = false)
@@ -17,8 +17,9 @@
package org.jetbrains.kotlin.js.inline.clean
import org.jetbrains.kotlin.js.backend.ast.JsFunction
import org.jetbrains.kotlin.js.backend.ast.JsName
class FunctionPostProcessor(val root: JsFunction) {
class FunctionPostProcessor(val root: JsFunction, private val voidName: JsName? = null) {
val optimizations = listOf(
{ RedundantLabelRemoval(root.body).apply() },
{ EmptyStatementElimination(root.body).apply() },
@@ -32,7 +33,8 @@ class FunctionPostProcessor(val root: JsFunction) {
{ RedundantStatementElimination(root).apply() },
{ CoroutineStateElimination(root.body).apply() },
{ BoxingUnboxingElimination(root.body).apply() },
{ MoveTemporaryVariableDeclarationToAssignment(root.body).apply() }
{ MoveTemporaryVariableDeclarationToAssignment(root.body).apply() },
{ voidName?.let { VoidPropertiesElimination(root.body, voidName).apply() } ?: false }
)
// TODO: reduce to A || B, A && B if possible
@@ -17,10 +17,7 @@
package org.jetbrains.kotlin.js.inline.clean
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.backend.ast.metadata.SideEffectKind
import org.jetbrains.kotlin.js.backend.ast.metadata.imported
import org.jetbrains.kotlin.js.backend.ast.metadata.sideEffects
import org.jetbrains.kotlin.js.backend.ast.metadata.synthetic
import org.jetbrains.kotlin.js.backend.ast.metadata.*
import org.jetbrains.kotlin.js.inline.util.collectFreeVariables
import org.jetbrains.kotlin.js.inline.util.collectLocalVariables
import org.jetbrains.kotlin.js.translate.context.Namer
@@ -481,8 +478,8 @@ internal class TemporaryVariableElimination(private val function: JsFunction) {
override fun visitNameRef(nameRef: JsNameRef) {
val name = nameRef.name
if (name != null && name in localVariables) {
if (name !in namesToSubstitute && shouldConsiderTemporary(name)) {
if (name != null && (name in localVariables || name.constant)) {
if (name.constant || (name !in namesToSubstitute && shouldConsiderTemporary(name))) {
if (!sideEffectOccurred) {
substitutableVariableReferences += name
}
@@ -637,10 +634,11 @@ internal class TemporaryVariableElimination(private val function: JsFunction) {
private fun isTrivial(expr: JsExpression): Boolean = when (expr) {
is JsNameRef -> {
val qualifier = expr.qualifier
if (expr.sideEffects == SideEffectKind.PURE && (qualifier == null || isTrivial(qualifier))) {
when {
expr.name?.constant == true -> true
expr.sideEffects == SideEffectKind.PURE && (qualifier == null || isTrivial(qualifier)) ->
expr.name !in temporary
}
else {
else -> {
val name = expr.name
name in localVariables && when (definitions[name]) {
// Local variables with zero definitions are function parameters. We can relocate and copy them.
@@ -650,6 +648,7 @@ internal class TemporaryVariableElimination(private val function: JsFunction) {
}
}
}
}
is JsLiteral.JsValueLiteral -> expr.toString().length < 10
is JsInvocation -> expr.sideEffects == SideEffectKind.PURE && isTrivial(expr.qualifier) && expr.arguments.all { isTrivial(it) }
is JsArrayAccess -> isTrivial(expr.arrayExpression) && isTrivial(expr.indexExpression) && expr.sideEffects == SideEffectKind.PURE
@@ -0,0 +1,42 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.js.inline.clean
import org.jetbrains.kotlin.js.backend.ast.*
import org.jetbrains.kotlin.js.backend.ast.metadata.isInlineClassBoxing
import org.jetbrains.kotlin.js.backend.ast.metadata.isInlineClassUnboxing
// Replaces { a: 2, b: VOID, c: VOID } with { a: 2 }
class VoidPropertiesElimination(private val root: JsBlock, private val voidName: JsName) {
private var changed = false
fun apply(): Boolean {
val visitor = object : JsVisitorWithContextImpl() {
override fun endVisit(x: JsPropertyInitializer, ctx: JsContext<JsNode>) {
super.endVisit(x, ctx)
if ((x.valueExpr as? JsNameRef)?.name === voidName) {
ctx.removeMe()
changed = true
}
}
}
visitor.accept(root)
return changed
}
}
@@ -10,10 +10,13 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.lower
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.declarations.impl.IrVariableImpl
import org.jetbrains.kotlin.ir.expressions.IrBlockBody
import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
import org.jetbrains.kotlin.ir.symbols.impl.IrVariableSymbolImpl
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.util.OperatorNameConventions
import org.jetbrains.kotlinx.jspo.compiler.resolve.JsPlainObjectsPluginKey
import org.jetbrains.kotlinx.jspo.compiler.resolve.StandardIds
@@ -32,7 +35,8 @@ private class MoveExternalInlineFunctionsWithBodiesOutsideLowering(private val c
file.declarations.add(declaration)
declaration.body = when (declaration.name) {
StandardNames.DATA_CLASS_COPY, OperatorNameConventions.INVOKE -> declaration.generateBodyForFactoryAndCopyFunction()
StandardNames.DATA_CLASS_COPY -> declaration.generateBodyForCopyFunction()
OperatorNameConventions.INVOKE -> declaration.generateBodyForFactoryFunction()
else -> error("Unexpected function with name `${declaration.name.identifier}`")
}
@@ -42,7 +46,7 @@ private class MoveExternalInlineFunctionsWithBodiesOutsideLowering(private val c
return emptyList()
}
private fun IrSimpleFunction.generateBodyForFactoryAndCopyFunction(): IrBlockBody {
private fun IrSimpleFunction.generateBodyForFactoryFunction(): IrBlockBody {
val declaration = this
return context.irFactory.createBlockBody(startOffset, declaration.endOffset).apply {
statements += IrReturnImpl(
@@ -64,6 +68,49 @@ private class MoveExternalInlineFunctionsWithBodiesOutsideLowering(private val c
)
}
}
private fun IrSimpleFunction.generateBodyForCopyFunction(): IrBlockBody {
val declaration = this
return context.irFactory.createBlockBody(startOffset, declaration.endOffset).apply {
val selfName = Name.identifier("${"$$"}tmp_self${"$$"}")
statements += IrVariableImpl(
declaration.startOffset,
declaration.endOffset,
IrDeclarationOrigin.IR_TEMPORARY_VARIABLE,
IrVariableSymbolImpl(),
selfName,
context.irBuiltIns.nothingType,
isVar = false,
isConst = false,
isLateinit = false
).apply {
parent = declaration
initializer = IrGetValueImpl(
declaration.startOffset,
declaration.endOffset,
declaration.dispatchReceiverParameter!!.symbol
)
}
statements += IrReturnImpl(
declaration.startOffset,
declaration.endOffset,
declaration.returnType,
declaration.symbol,
IrCallImpl(
declaration.startOffset,
declaration.endOffset,
declaration.returnType,
jsFunction,
0,
1,
).apply {
val jsObject = "{ ${declaration.valueParameters.joinToString(", ") { "${it.name.identifier}:${it.name.identifier}" }} }"
val objectAssignCall = "Object.assign({}, ${selfName.identifier}, $jsObject)"
putValueArgument(0, objectAssignCall.toIrConst(context.irBuiltIns.stringType))
}
)
}
}
}
open class JsPlainObjectsLoweringExtension : IrGenerationExtension {
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.name.Name
object StandardIds {
val KOTLIN_JS_FQN = FqName("kotlin.js")
val JS_FUNCTION_ID = CallableId(KOTLIN_JS_FQN, Name.identifier("js"))
val VOID_PROPERTY_NAME = Name.identifier("VOID")
}
object JsPlainObjectsAnnotations {
@@ -28,9 +28,9 @@ import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext
import org.jetbrains.kotlin.fir.extensions.NestedClassGenerationContext
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.moduleData
import org.jetbrains.kotlin.fir.references.builder.buildImplicitThisReference
import org.jetbrains.kotlin.fir.references.builder.buildResolvedNamedReference
import org.jetbrains.kotlin.fir.resolve.defaultType
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.fir.scopes.kotlinScopeProvider
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
import org.jetbrains.kotlin.fir.symbols.impl.*
@@ -40,11 +40,11 @@ import org.jetbrains.kotlin.fir.types.toFirResolvedTypeRef
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.types.ConstantValueKind
import org.jetbrains.kotlin.util.OperatorNameConventions
import org.jetbrains.kotlin.utils.addToStdlib.runIf
import org.jetbrains.kotlinx.jspo.compiler.fir.services.jsPlainObjectPropertiesProvider
import org.jetbrains.kotlinx.jspo.compiler.resolve.JsPlainObjectsPluginKey
import org.jetbrains.kotlinx.jspo.compiler.resolve.StandardIds
/**
* The extension generate a synthetic factory and copy-method for an `external interface` annotated with @JsPlainObjects
@@ -56,6 +56,7 @@ import org.jetbrains.kotlinx.jspo.compiler.resolve.JsPlainObjectsPluginKey
* @JsPlainObjects
* external interface Admin {
* val chat: Chat
* val email: String?
* }
* ```
*
@@ -64,21 +65,26 @@ import org.jetbrains.kotlinx.jspo.compiler.resolve.JsPlainObjectsPluginKey
* external interface Admin {
* val chat: Chat
*
* inline fun copy(chat: Chat = this.chat, name: String = this.name): Admin =
* inline fun copy(chat: Chat = this.chat, email: String = this.email): Admin =
* Admin.Companion.invoke(chat, name)
*
* companion object {
* inline operator fun invoke(chat: Chat, name: String): Admin =
* inline operator fun invoke(chat: Chat, email: String? = VOID): Admin =
* js("{ chat: chat, name: name }")
* }
* }
* ```
*/
class JsPlainObjectsFunctionsGenerator(session: FirSession) : FirDeclarationGenerationExtension(session) {
private val predicateBasedProvider = session.predicateBasedProvider
private val voidPropertySymbol by lazy {
session.symbolProvider
.getTopLevelPropertySymbols(StandardIds.KOTLIN_JS_FQN, StandardIds.VOID_PROPERTY_NAME)
.single()
}
private val matchedInterfaces by lazy {
predicateBasedProvider.getSymbolsByPredicate(JsPlainObjectsPredicates.AnnotatedWithJsPlainObject.LOOKUP)
session.predicateBasedProvider
.getSymbolsByPredicate(JsPlainObjectsPredicates.AnnotatedWithJsPlainObject.LOOKUP)
.filterIsInstance<FirRegularClassSymbol>()
.toSet()
}
@@ -102,7 +108,7 @@ class JsPlainObjectsFunctionsGenerator(session: FirSession) : FirDeclarationGene
return if (
owner is FirRegularClassSymbol &&
owner.isJsPlainObject &&
name == org.jetbrains.kotlin.name.SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT
name == SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT
) generateCompanionDeclaration(owner)
else null
}
@@ -167,12 +173,13 @@ class JsPlainObjectsFunctionsGenerator(session: FirSession) : FirDeclarationGene
): FirSimpleFunction {
return createJsPlainObjectsFunction(callableId, parent, jsPlainObjectInterface) {
runIf(resolvedReturnTypeRef.type.isNullable) {
buildConstExpression(
source = null,
value = null,
kind = ConstantValueKind.Null,
setType = true
)
buildPropertyAccessExpression {
calleeReference = buildResolvedNamedReference {
name = StandardIds.VOID_PROPERTY_NAME
resolvedSymbol = voidPropertySymbol
}
coneTypeOrNull = voidPropertySymbol.resolvedReturnType
}
}
}
}
@@ -185,15 +192,11 @@ class JsPlainObjectsFunctionsGenerator(session: FirSession) : FirDeclarationGene
val interfaceType = jsPlainObjectInterface.defaultType()
return createJsPlainObjectsFunction(callableId, parent, jsPlainObjectInterface) {
buildPropertyAccessExpression {
dispatchReceiver = buildThisReceiverExpression {
calleeReference = buildImplicitThisReference { boundSymbol = jsPlainObjectInterface }
coneTypeOrNull = interfaceType
}
calleeReference = buildResolvedNamedReference {
name = this@createJsPlainObjectsFunction.name
resolvedSymbol = this@createJsPlainObjectsFunction
name = StandardIds.VOID_PROPERTY_NAME
resolvedSymbol = voidPropertySymbol
}
coneTypeOrNull = resolvedReturnType
coneTypeOrNull = voidPropertySymbol.resolvedReturnType
}
}
}
@@ -16,7 +16,7 @@ fun box(): String {
if (user.age != 10) return "Fail: problem with `age` property"
val json = js("JSON.stringify(user)")
if (json != "{\"email\":null,\"age\":10,\"name\":\"Name\"}") return "Fail: got the next json: $json"
if (json != "{\"age\":10,\"name\":\"Name\"}") return "Fail: got the next json: $json"
val withEmail = User(name = "Name", age = 10, email = "test@test")