blob: cfcf5d1cc30415afbd8933d54246aff24d607cda [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* 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 androidx.compose.compiler.plugins.kotlin.lower
import androidx.compose.compiler.plugins.kotlin.ComposeCallableIds
import androidx.compose.compiler.plugins.kotlin.ComposeClassIds
import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
import androidx.compose.compiler.plugins.kotlin.FunctionMetrics
import androidx.compose.compiler.plugins.kotlin.KtxNameConventions
import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
import androidx.compose.compiler.plugins.kotlin.analysis.ComposeWritableSlices
import androidx.compose.compiler.plugins.kotlin.analysis.KnownStableConstructs
import androidx.compose.compiler.plugins.kotlin.analysis.Stability
import androidx.compose.compiler.plugins.kotlin.analysis.StabilityInferencer
import androidx.compose.compiler.plugins.kotlin.analysis.isUncertain
import androidx.compose.compiler.plugins.kotlin.analysis.knownStable
import androidx.compose.compiler.plugins.kotlin.analysis.knownUnstable
import androidx.compose.compiler.plugins.kotlin.irTrace
import com.intellij.openapi.progress.ProcessCanceledException
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.jvm.ir.isInlineClassType
import org.jetbrains.kotlin.builtins.PrimitiveType
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.builders.declarations.addTypeParameter
import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter
import org.jetbrains.kotlin.ir.builders.declarations.buildField
import org.jetbrains.kotlin.ir.builders.declarations.buildFun
import org.jetbrains.kotlin.ir.builders.declarations.buildProperty
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
import org.jetbrains.kotlin.ir.declarations.IrAttributeContainer
import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrField
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.IrTypeParameter
import org.jetbrains.kotlin.ir.declarations.IrValueDeclaration
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.declarations.IrVariable
import org.jetbrains.kotlin.ir.declarations.impl.IrExternalPackageFragmentImpl
import org.jetbrains.kotlin.ir.declarations.impl.IrVariableImpl
import org.jetbrains.kotlin.ir.declarations.inlineClassRepresentation
import org.jetbrains.kotlin.ir.declarations.name
import org.jetbrains.kotlin.ir.expressions.IrBlock
import org.jetbrains.kotlin.ir.expressions.IrBranch
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrConstKind
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrContainerExpression
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression
import org.jetbrains.kotlin.ir.expressions.IrGetEnumValue
import org.jetbrains.kotlin.ir.expressions.IrGetField
import org.jetbrains.kotlin.ir.expressions.IrGetObjectValue
import org.jetbrains.kotlin.ir.expressions.IrGetValue
import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression
import org.jetbrains.kotlin.ir.expressions.IrReturn
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall
import org.jetbrains.kotlin.ir.expressions.IrVararg
import org.jetbrains.kotlin.ir.expressions.impl.IrBlockImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrBranchImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrCompositeImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrElseBranchImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionExpressionImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrGetFieldImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrIfThenElseImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrReturnImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrSetValueImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrWhenImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrWhileLoopImpl
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.symbols.IrReturnTargetSymbol
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
import org.jetbrains.kotlin.ir.symbols.IrValueSymbol
import org.jetbrains.kotlin.ir.symbols.impl.IrVariableSymbolImpl
import org.jetbrains.kotlin.ir.types.IrSimpleType
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.classifierOrFail
import org.jetbrains.kotlin.ir.types.defaultType
import org.jetbrains.kotlin.ir.types.impl.IrSimpleTypeImpl
import org.jetbrains.kotlin.ir.types.impl.IrStarProjectionImpl
import org.jetbrains.kotlin.ir.types.isBoolean
import org.jetbrains.kotlin.ir.types.isByte
import org.jetbrains.kotlin.ir.types.isChar
import org.jetbrains.kotlin.ir.types.isDouble
import org.jetbrains.kotlin.ir.types.isFloat
import org.jetbrains.kotlin.ir.types.isInt
import org.jetbrains.kotlin.ir.types.isLong
import org.jetbrains.kotlin.ir.types.isMarkedNullable
import org.jetbrains.kotlin.ir.types.isNullable
import org.jetbrains.kotlin.ir.types.isNullableAny
import org.jetbrains.kotlin.ir.types.isPrimitiveType
import org.jetbrains.kotlin.ir.types.isShort
import org.jetbrains.kotlin.ir.types.makeNullable
import org.jetbrains.kotlin.ir.types.typeWith
import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.getArgumentsWithIr
import org.jetbrains.kotlin.ir.util.getPropertyGetter
import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.util.isFunction
import org.jetbrains.kotlin.ir.util.kotlinFqName
import org.jetbrains.kotlin.ir.util.parentAsClass
import org.jetbrains.kotlin.ir.util.parentClassOrNull
import org.jetbrains.kotlin.ir.util.primaryConstructor
import org.jetbrains.kotlin.ir.util.properties
import org.jetbrains.kotlin.ir.util.statements
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.load.kotlin.computeJvmDescriptor
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.platform.jvm.isJvm
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.util.OperatorNameConventions
import org.jetbrains.kotlin.utils.DFS
abstract class AbstractComposeLowering(
val context: IrPluginContext,
val symbolRemapper: DeepCopySymbolRemapper,
val metrics: ModuleMetrics,
val stabilityInferencer: StabilityInferencer
) : IrElementTransformerVoid(), ModuleLoweringPass {
protected val builtIns = context.irBuiltIns
private val _composerIrClass =
context.referenceClass(ComposeClassIds.Composer)?.owner
?: error("Cannot find the Composer class in the classpath")
private val _composableIrClass =
context.referenceClass(ComposeClassIds.Composable)?.owner
?: error("Cannot find the Composable annotation class in the classpath")
// this ensures that composer always references up-to-date composer class symbol
// otherwise, after remapping of symbols in DeepCopyTransformer, it results in duplicated
// references
protected val composerIrClass: IrClass
get() = symbolRemapper.getReferencedClass(_composerIrClass.symbol).owner
protected val composableIrClass: IrClass
get() = symbolRemapper.getReferencedClass(_composableIrClass.symbol).owner
fun referenceFunction(symbol: IrFunctionSymbol): IrFunctionSymbol {
return symbolRemapper.getReferencedFunction(symbol)
}
fun referenceSimpleFunction(symbol: IrSimpleFunctionSymbol): IrSimpleFunctionSymbol {
return symbolRemapper.getReferencedSimpleFunction(symbol)
}
fun referenceConstructor(symbol: IrConstructorSymbol): IrConstructorSymbol {
return symbolRemapper.getReferencedConstructor(symbol)
}
fun getTopLevelClass(classId: ClassId): IrClassSymbol {
return getTopLevelClassOrNull(classId)
?: error("Class not found in the classpath: ${classId.asSingleFqName()}")
}
fun getTopLevelClassOrNull(classId: ClassId): IrClassSymbol? {
return context.referenceClass(classId)
}
fun getTopLevelFunction(callableId: CallableId): IrSimpleFunctionSymbol {
return getTopLevelFunctionOrNull(callableId)
?: error("Function not found in the classpath: ${callableId.asSingleFqName()}")
}
fun getTopLevelFunctionOrNull(callableId: CallableId): IrSimpleFunctionSymbol? {
return context.referenceFunctions(callableId).firstOrNull()
}
fun getTopLevelFunctions(callableId: CallableId): List<IrSimpleFunctionSymbol> {
return context.referenceFunctions(callableId).toList()
}
fun getTopLevelPropertyGetter(callableId: CallableId): IrFunctionSymbol {
val propertySymbol = context.referenceProperties(callableId).firstOrNull()
?: error("Property was not found ${callableId.asSingleFqName()}")
return symbolRemapper.getReferencedFunction(
propertySymbol.owner.getter!!.symbol
)
}
fun metricsFor(function: IrFunction): FunctionMetrics =
(function as? IrAttributeContainer)?.let {
context.irTrace[ComposeWritableSlices.FUNCTION_METRICS, it] ?: run {
val metrics = metrics.makeFunctionMetrics(function)
context.irTrace.record(ComposeWritableSlices.FUNCTION_METRICS, it, metrics)
metrics
}
} ?: metrics.makeFunctionMetrics(function)
fun IrType.unboxInlineClass() = unboxType() ?: this
fun IrType.replaceArgumentsWithStarProjections(): IrType =
when (this) {
is IrSimpleType -> IrSimpleTypeImpl(
classifier,
isMarkedNullable(),
List(arguments.size) { IrStarProjectionImpl },
annotations,
abbreviation
)
else -> this
}
// IR external stubs don't have their value parameters' parent properly mapped to the
// function itself. This normally isn't a problem because nothing in the IR lowerings ask for
// the parent of the parameters, but we do. I believe this should be considered a bug in
// kotlin proper, but this works around it.
fun IrValueParameter.hasDefaultValueSafe(): Boolean = DFS.ifAny(
listOf(this),
{ current ->
(current.parent as? IrSimpleFunction)?.overriddenSymbols?.map { fn ->
fn.owner.valueParameters[current.index].also { p ->
p.parent = fn.owner
}
} ?: listOf()
},
{ current -> current.defaultValue != null }
)
// NOTE(lmr): This implementation mimics the kotlin-provided unboxInlineClass method, except
// this one makes sure to bind the symbol if it is unbound, so is a bit safer to use.
fun IrType.unboxType(): IrType? {
val klass = classOrNull?.owner ?: return null
val representation = klass.inlineClassRepresentation ?: return null
if (!isInlineClassType()) return null
// TODO: Apply type substitutions
val underlyingType = representation.underlyingType.unboxInlineClass()
if (!isNullable()) return underlyingType
if (underlyingType.isNullable() || underlyingType.isPrimitiveType())
return null
return underlyingType.makeNullable()
}
protected fun IrExpression.unboxValueIfInline(): IrExpression {
if (type.isNullable()) return this
val classSymbol = type.classOrNull ?: return this
val klass = classSymbol.owner
if (type.isInlineClassType()) {
if (context.platform.isJvm()) {
return coerceInlineClasses(
this,
type,
type.unboxInlineClass()
).unboxValueIfInline()
} else {
val primaryValueParameter = klass.primaryConstructor?.valueParameters?.get(0)
val cantUnbox = primaryValueParameter == null || klass.properties.none {
it.name == primaryValueParameter.name && it.getter != null
}
if (cantUnbox) {
// LazyIr (external module) doesn't show a getter of a private property.
// So we can't unbox the value
return this
}
val fieldGetter = klass.getPropertyGetter(primaryValueParameter!!.name.identifier)
?: error("Expected a getter")
return irCall(
symbol = fieldGetter,
dispatchReceiver = this
).unboxValueIfInline()
}
}
return this
}
fun IrAnnotationContainer.hasComposableAnnotation(): Boolean {
return hasAnnotation(ComposeFqNames.Composable)
}
fun IrCall.isInvoke(): Boolean {
if (origin == IrStatementOrigin.INVOKE)
return true
val function = symbol.owner
return function.name == OperatorNameConventions.INVOKE &&
function.parentClassOrNull?.defaultType?.let {
it.isFunction() || it.isSyntheticComposableFunction()
} ?: false
}
fun IrCall.isComposableCall(): Boolean {
return symbol.owner.hasComposableAnnotation() || isComposableLambdaInvoke()
}
fun IrCall.isSyntheticComposableCall(): Boolean {
return context.irTrace[ComposeWritableSlices.IS_SYNTHETIC_COMPOSABLE_CALL, this] == true
}
fun IrCall.isComposableLambdaInvoke(): Boolean {
if (!isInvoke()) return false
// [ComposerParamTransformer] replaces composable function types of the form
// `@Composable Function1<T1, T2>` with ordinary functions with extra parameters, e.g.,
// `Function3<T1, Composer, Int, T2>`. After this lowering runs we have to check the
// `attributeOwnerId` to recover the original type.
val receiver = dispatchReceiver?.let { it.attributeOwnerId as? IrExpression ?: it }
return receiver?.type?.let {
it.hasComposableAnnotation() || it.isSyntheticComposableFunction()
} ?: false
}
fun IrCall.isComposableSingletonGetter(): Boolean {
return context.irTrace[ComposeWritableSlices.IS_COMPOSABLE_SINGLETON, this] == true
}
fun IrClass.isComposableSingletonClass(): Boolean {
return context.irTrace[ComposeWritableSlices.IS_COMPOSABLE_SINGLETON_CLASS, this] == true
}
fun Stability.irStableExpression(
resolve: (IrTypeParameter) -> IrExpression? = { null }
): IrExpression? = when (this) {
is Stability.Combined -> {
val exprs = elements.mapNotNull { it.irStableExpression(resolve) }
when {
exprs.size != elements.size -> null
exprs.isEmpty() -> irConst(StabilityBits.STABLE.bitsForSlot(0))
exprs.size == 1 -> exprs.first()
else -> exprs.reduce { a, b ->
irOr(a, b)
}
}
}
is Stability.Certain ->
if (stable)
irConst(StabilityBits.STABLE.bitsForSlot(0))
else
null
is Stability.Parameter -> resolve(parameter)
is Stability.Runtime -> {
val stableField = makeStabilityField().also { it.parent = declaration }
IrGetFieldImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
stableField.symbol,
stableField.type
)
}
is Stability.Unknown -> null
}
// set the bit at a certain index
private fun Int.withBit(index: Int, value: Boolean): Int {
return if (value) {
this or (1 shl index)
} else {
this and (1 shl index).inv()
}
}
protected operator fun Int.get(index: Int): Boolean {
return this and (1 shl index) != 0
}
// create a bitmask with the following bits
protected fun bitMask(vararg values: Boolean): Int = values.foldIndexed(0) { i, mask, bit ->
mask.withBit(i, bit)
}
protected fun irGetBit(param: IrDefaultBitMaskValue, index: Int): IrExpression {
// value and (1 shl index) != 0
return irNotEqual(
param.irIsolateBitAtIndex(index),
irConst(0)
)
}
protected fun irSet(variable: IrValueDeclaration, value: IrExpression): IrExpression {
return IrSetValueImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
context.irBuiltIns.unitType,
variable.symbol,
value = value,
origin = null
)
}
protected fun irCall(
symbol: IrFunctionSymbol,
origin: IrStatementOrigin? = null,
dispatchReceiver: IrExpression? = null,
extensionReceiver: IrExpression? = null,
vararg args: IrExpression
): IrCallImpl {
return IrCallImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
symbol.owner.returnType,
symbol as IrSimpleFunctionSymbol,
symbol.owner.typeParameters.size,
symbol.owner.valueParameters.size,
origin
).also {
if (dispatchReceiver != null) it.dispatchReceiver = dispatchReceiver
if (extensionReceiver != null) it.extensionReceiver = extensionReceiver
args.forEachIndexed { index, arg ->
it.putValueArgument(index, arg)
}
}
}
protected fun IrType.binaryOperator(name: Name, paramType: IrType): IrFunctionSymbol =
context.symbols.getBinaryOperator(name, this, paramType)
protected fun irAnd(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
return irCall(
lhs.type.binaryOperator(OperatorNameConventions.AND, rhs.type),
null,
lhs,
null,
rhs
)
}
protected fun irOr(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
val int = context.irBuiltIns.intType
return irCall(
int.binaryOperator(OperatorNameConventions.OR, int),
null,
lhs,
null,
rhs
)
}
protected fun irBooleanOr(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
val boolean = context.irBuiltIns.booleanType
return irCall(
boolean.binaryOperator(OperatorNameConventions.OR, boolean),
null,
lhs,
null,
rhs
)
}
protected fun irOrOr(lhs: IrExpression, rhs: IrExpression): IrExpression {
return IrWhenImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
origin = IrStatementOrigin.OROR,
type = context.irBuiltIns.booleanType,
branches = listOf(
IrBranchImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
condition = lhs,
result = irConst(true)
),
IrElseBranchImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
condition = irConst(true),
result = rhs
)
)
)
}
protected fun irAndAnd(lhs: IrExpression, rhs: IrExpression): IrExpression {
return IrWhenImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
origin = IrStatementOrigin.ANDAND,
type = context.irBuiltIns.booleanType,
branches = listOf(
IrBranchImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
condition = lhs,
result = rhs
),
IrElseBranchImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
condition = irConst(true),
result = irConst(false)
)
)
)
}
protected fun irXor(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
val int = context.irBuiltIns.intType
return irCall(
int.binaryOperator(OperatorNameConventions.XOR, int),
null,
lhs,
null,
rhs
)
}
protected fun irGreater(lhs: IrExpression, rhs: IrExpression): IrCallImpl {
val int = context.irBuiltIns.intType
val gt = context.irBuiltIns.greaterFunByOperandType[int.classifierOrFail]
return irCall(
gt!!,
IrStatementOrigin.GT,
null,
null,
lhs,
rhs
)
}
protected fun irReturn(
target: IrReturnTargetSymbol,
value: IrExpression,
type: IrType = value.type
): IrExpression {
return IrReturnImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
type,
target,
value
)
}
protected fun irReturnVar(
target: IrReturnTargetSymbol,
value: IrVariable,
): IrExpression {
return IrReturnImpl(
value.initializer?.startOffset ?: UNDEFINED_OFFSET,
value.initializer?.endOffset ?: UNDEFINED_OFFSET,
value.type,
target,
irGet(value)
)
}
/** Compare [lhs] and [rhs] using structural equality (`==`). */
protected fun irEqual(lhs: IrExpression, rhs: IrExpression): IrExpression {
return irCall(
context.irBuiltIns.eqeqSymbol,
null,
null,
null,
lhs,
rhs
)
}
protected fun irNot(value: IrExpression): IrExpression {
return irCall(
context.irBuiltIns.booleanNotSymbol,
dispatchReceiver = value
)
}
/** Compare [lhs] and [rhs] using structural inequality (`!=`). */
protected fun irNotEqual(lhs: IrExpression, rhs: IrExpression): IrExpression {
return irNot(irEqual(lhs, rhs))
}
// context.irIntrinsics.symbols.intAnd
// context.irIntrinsics.symbols.getBinaryOperator(name, lhs, rhs)
// context.irBuiltIns.booleanNotSymbol
// context.irBuiltIns.eqeqeqSymbol
// context.irBuiltIns.eqeqSymbol
// context.irBuiltIns.greaterFunByOperandType
protected fun irConst(value: Int): IrConst<Int> = IrConstImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
context.irBuiltIns.intType,
IrConstKind.Int,
value
)
protected fun irConst(value: Long): IrConst<Long> = IrConstImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
context.irBuiltIns.longType,
IrConstKind.Long,
value
)
protected fun irConst(value: String): IrConst<String> = IrConstImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
context.irBuiltIns.stringType,
IrConstKind.String,
value
)
protected fun irConst(value: Boolean) = IrConstImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
context.irBuiltIns.booleanType,
IrConstKind.Boolean,
value
)
protected fun irNull() = IrConstImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
context.irBuiltIns.anyNType,
IrConstKind.Null,
null
)
protected fun irForLoop(
elementType: IrType,
subject: IrExpression,
loopBody: (IrValueDeclaration) -> IrExpression
): IrStatement {
val getIteratorFunction = subject.type.classOrNull!!.owner.functions
.single { it.name.asString() == "iterator" }
val iteratorSymbol = getIteratorFunction.returnType.classOrNull!!
val iteratorType = if (iteratorSymbol.owner.typeParameters.isNotEmpty()) {
iteratorSymbol.typeWith(elementType)
} else {
iteratorSymbol.defaultType
}
val nextSymbol = iteratorSymbol.owner.functions
.single { it.name.asString() == "next" }
val hasNextSymbol = iteratorSymbol.owner.functions
.single { it.name.asString() == "hasNext" }
val call = IrCallImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
iteratorType,
getIteratorFunction.symbol,
getIteratorFunction.symbol.owner.typeParameters.size,
getIteratorFunction.symbol.owner.valueParameters.size,
IrStatementOrigin.FOR_LOOP_ITERATOR
).also {
it.dispatchReceiver = subject
}
val iteratorVar = irTemporary(
value = call,
isVar = false,
name = "tmp0_iterator",
irType = iteratorType,
origin = IrDeclarationOrigin.FOR_LOOP_ITERATOR
)
return irBlock(
type = builtIns.unitType,
origin = IrStatementOrigin.FOR_LOOP,
statements = listOf(
iteratorVar,
IrWhileLoopImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
builtIns.unitType,
IrStatementOrigin.FOR_LOOP_INNER_WHILE
).apply {
val loopVar = irTemporary(
value = IrCallImpl(
symbol = nextSymbol.symbol,
origin = IrStatementOrigin.FOR_LOOP_NEXT,
startOffset = UNDEFINED_OFFSET,
endOffset = UNDEFINED_OFFSET,
typeArgumentsCount = nextSymbol.symbol.owner.typeParameters.size,
valueArgumentsCount = nextSymbol.symbol.owner.valueParameters.size,
type = elementType
).also {
it.dispatchReceiver = irGet(iteratorVar)
},
origin = IrDeclarationOrigin.FOR_LOOP_VARIABLE,
isVar = false,
name = "value",
irType = elementType
)
condition = irCall(
symbol = hasNextSymbol.symbol,
origin = IrStatementOrigin.FOR_LOOP_HAS_NEXT,
dispatchReceiver = irGet(iteratorVar)
)
body = irBlock(
type = builtIns.unitType,
origin = IrStatementOrigin.FOR_LOOP_INNER_WHILE,
statements = listOf(
loopVar,
loopBody(loopVar)
)
)
}
)
)
}
protected fun irTemporary(
value: IrExpression,
name: String,
irType: IrType = value.type,
isVar: Boolean = false,
origin: IrDeclarationOrigin = IrDeclarationOrigin.IR_TEMPORARY_VARIABLE
): IrVariableImpl {
return IrVariableImpl(
value.startOffset,
value.endOffset,
origin,
IrVariableSymbolImpl(),
Name.identifier(name),
irType,
isVar,
isConst = false,
isLateinit = false
).apply {
initializer = value
}
}
protected fun irGet(type: IrType, symbol: IrValueSymbol): IrExpression {
return IrGetValueImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
type,
symbol
)
}
protected fun irGet(variable: IrValueDeclaration): IrExpression {
return irGet(variable.type, variable.symbol)
}
protected fun irIf(condition: IrExpression, body: IrExpression): IrExpression {
return IrIfThenElseImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
context.irBuiltIns.unitType,
origin = IrStatementOrigin.IF
).also {
it.branches.add(
IrBranchImpl(condition, body)
)
}
}
protected fun irIfThenElse(
type: IrType = context.irBuiltIns.unitType,
condition: IrExpression,
thenPart: IrExpression,
elsePart: IrExpression,
startOffset: Int = UNDEFINED_OFFSET,
endOffset: Int = UNDEFINED_OFFSET
) =
IrIfThenElseImpl(startOffset, endOffset, type, IrStatementOrigin.IF).apply {
branches.add(
IrBranchImpl(
startOffset,
endOffset,
condition,
thenPart
)
)
branches.add(irElseBranch(elsePart, startOffset, endOffset))
}
protected fun irWhen(
type: IrType = context.irBuiltIns.unitType,
origin: IrStatementOrigin? = null,
branches: List<IrBranch>
) = IrWhenImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
type,
origin,
branches
)
protected fun irBranch(
condition: IrExpression,
result: IrExpression
): IrBranch {
return IrBranchImpl(condition, result)
}
protected fun irElseBranch(
expression: IrExpression,
startOffset: Int = UNDEFINED_OFFSET,
endOffset: Int = UNDEFINED_OFFSET
) = IrElseBranchImpl(startOffset, endOffset, irConst(true), expression)
protected fun irBlock(
type: IrType = context.irBuiltIns.unitType,
origin: IrStatementOrigin? = null,
statements: List<IrStatement>
): IrExpression {
return IrBlockImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
type,
origin,
statements
)
}
protected fun irComposite(
type: IrType = context.irBuiltIns.unitType,
origin: IrStatementOrigin? = null,
statements: List<IrStatement>
): IrExpression {
return IrCompositeImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
type,
origin,
statements
)
}
protected fun irLambdaExpression(
startOffset: Int,
endOffset: Int,
returnType: IrType,
body: (IrSimpleFunction) -> Unit
): IrExpression {
val function = context.irFactory.buildFun {
this.startOffset = SYNTHETIC_OFFSET
this.endOffset = SYNTHETIC_OFFSET
this.returnType = returnType
origin = IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA
name = SpecialNames.ANONYMOUS
visibility = DescriptorVisibilities.LOCAL
}.also(body)
return IrFunctionExpressionImpl(
startOffset = startOffset,
endOffset = endOffset,
type = context.function(function.valueParameters.size).typeWith(
function.valueParameters.map { it.type } + listOf(function.returnType)
),
origin = IrStatementOrigin.LAMBDA,
function = function
)
}
fun makeStabilityField(): IrField {
return context.irFactory.buildField {
startOffset = SYNTHETIC_OFFSET
endOffset = SYNTHETIC_OFFSET
name = KtxNameConventions.STABILITY_FLAG
isStatic = context.platform.isJvm()
isFinal = true
type = context.irBuiltIns.intType
visibility = DescriptorVisibilities.PUBLIC
}
}
protected fun makeStabilityProp(): IrProperty {
return context.irFactory.buildProperty {
startOffset = SYNTHETIC_OFFSET
endOffset = SYNTHETIC_OFFSET
name = KtxNameConventions.STABILITY_PROP_FLAG
visibility = DescriptorVisibilities.PRIVATE
}
}
fun IrExpression.isStatic(): Boolean {
return when (this) {
// A constant by definition is static
is IrConst<*> -> true
// We want to consider all enum values as static
is IrGetEnumValue -> true
// Getting a companion object or top level object can be considered static if the
// type of that object is Stable. (`Modifier` for instance is a common example)
is IrGetObjectValue -> {
if (symbol.owner.isCompanion) true
else stabilityInferencer.stabilityOf(type).knownStable()
}
is IrConstructorCall -> isStatic()
is IrCall -> isStatic()
is IrGetValue -> {
when (val owner = symbol.owner) {
is IrVariable -> {
// If we have an immutable variable whose initializer is also static,
// then we can determine that the variable reference is also static.
!owner.isVar && owner.initializer?.isStatic() == true
}
else -> false
}
}
is IrFunctionExpression,
is IrTypeOperatorCall ->
context.irTrace[ComposeWritableSlices.IS_STATIC_FUNCTION_EXPRESSION, this] ?: false
is IrGetField ->
// K2 sometimes produces `IrGetField` for reads from constant properties
symbol.owner.correspondingPropertySymbol?.owner?.isConst == true
is IrBlock -> {
// Check the slice in case the block was generated as expression
// (e.g. inlined intrinsic remember call)
context.irTrace[ComposeWritableSlices.IS_STATIC_EXPRESSION, this] ?: false
}
else -> false
}
}
private fun IrConstructorCall.isStatic(): Boolean {
// special case constructors of inline classes as static if their underlying
// value is static.
if (type.isInlineClassType()) {
return stabilityInferencer.stabilityOf(type.unboxInlineClass()).knownStable() &&
getValueArgument(0)?.isStatic() == true
}
// If a type is immutable, then calls to its constructor are static if all of
// the provided arguments are static.
if (symbol.owner.parentAsClass.hasAnnotationSafe(ComposeFqNames.Immutable)) {
return areAllArgumentsStatic()
}
return false
}
private fun IrCall.isStatic(): Boolean {
val function = symbol.owner
val fqName = function.kotlinFqName
return when (origin) {
is IrStatementOrigin.GET_PROPERTY -> {
// If we are in a GET_PROPERTY call, then this should usually resolve to
// non-null, but in case it doesn't, just return false
val prop = (function as? IrSimpleFunction)
?.correspondingPropertySymbol?.owner ?: return false
// if the property is a top level constant, then it is static.
if (prop.isConst) return true
val typeIsStable = stabilityInferencer.stabilityOf(type).knownStable()
val dispatchReceiverIsStatic = dispatchReceiver?.isStatic() != false
val extensionReceiverIsStatic = extensionReceiver?.isStatic() != false
// if we see that the property is read-only with a default getter and a
// stable return type , then reading the property can also be considered
// static if this is a top level property or the subject is also static.
if (!prop.isVar &&
prop.getter?.origin == IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR &&
typeIsStable &&
dispatchReceiverIsStatic && extensionReceiverIsStatic
) {
return true
}
val getterIsStable = prop.hasAnnotation(ComposeFqNames.Stable) ||
function.hasAnnotation(ComposeFqNames.Stable)
if (
getterIsStable &&
typeIsStable &&
dispatchReceiverIsStatic &&
extensionReceiverIsStatic
) {
return true
}
false
}
is IrStatementOrigin.PLUS,
is IrStatementOrigin.MUL,
is IrStatementOrigin.MINUS,
is IrStatementOrigin.ANDAND,
is IrStatementOrigin.OROR,
is IrStatementOrigin.DIV,
is IrStatementOrigin.EQ,
is IrStatementOrigin.EQEQ,
is IrStatementOrigin.EQEQEQ,
is IrStatementOrigin.GT,
is IrStatementOrigin.GTEQ,
is IrStatementOrigin.LT,
is IrStatementOrigin.LTEQ -> {
// special case mathematical operators that are in the stdlib. These are
// immutable operations so the overall result is static if the operands are
// also static
val isStableOperator = fqName.topLevelName() == "kotlin" ||
function.hasAnnotation(ComposeFqNames.Stable)
val typeIsStable = stabilityInferencer.stabilityOf(type).knownStable()
if (!typeIsStable) return false
if (!isStableOperator) {
return false
}
getArgumentsWithIr().all { it.second.isStatic() }
}
null -> {
if (fqName == ComposeFqNames.remember) {
// if it is a call to remember with 0 input arguments, then we can
// consider the value static if the result type of the lambda is stable
val syntheticRememberParams = 1 + // composer param
1 // changed param
val expectedArgumentsCount = 1 + syntheticRememberParams // 1 for lambda
if (
valueArgumentsCount == expectedArgumentsCount &&
stabilityInferencer.stabilityOf(type).knownStable()
) {
return true
}
} else if (fqName == ComposeFqNames.composableLambda) {
// calls to this function are generated by the compiler, and this
// function behaves similar to a remember call in that the result will
// _always_ be the same and the resulting type is _always_ stable, so
// thus it is static.
return true
}
if (context.irTrace[ComposeWritableSlices.IS_COMPOSABLE_SINGLETON, this] == true) {
return true
}
// normal function call. If the function is marked as Stable and the result
// is Stable, then the static-ness of it is the static-ness of its arguments
// For functions that we have an exception for, skip these checks. We've already
// assumed the stability here and can go straight to checking their arguments.
if (fqName.asString() !in KnownStableConstructs.stableFunctions) {
val isStable = symbol.owner.hasAnnotation(ComposeFqNames.Stable)
if (!isStable) return false
val typeIsStable = stabilityInferencer.stabilityOf(type).knownStable()
if (!typeIsStable) return false
}
areAllArgumentsStatic()
}
else -> false
}
}
private fun IrMemberAccessExpression<*>.areAllArgumentsStatic(): Boolean {
// getArguments includes the receivers!
return getArgumentsWithIr().all { (_, argExpression) ->
when (argExpression) {
// In a vacuum, we can't assume varargs are static because they're backed by
// arrays. Arrays aren't stable types due to their implicit mutability and
// lack of built-in equality checks. But in this context, because the static-ness of
// an argument is meaningless unless the function call that owns the argument is
// stable and capable of being static. So in this case, we're able to ignore the
// array implementation detail and check whether all of the parameters sent in the
// varargs are static on their own.
is IrVararg -> argExpression.elements.all { varargElement ->
(varargElement as? IrExpression)?.isStatic() ?: false
}
else -> argExpression.isStatic()
}
}
}
protected fun dexSafeName(name: Name): Name {
return if (
name.isSpecial || name.asString().contains(unsafeSymbolsRegex)
) {
val sanitized = name
.asString()
.replace(unsafeSymbolsRegex, "\\$")
Name.identifier(sanitized)
} else name
}
fun coerceInlineClasses(argument: IrExpression, from: IrType, to: IrType) =
IrCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
to,
unsafeCoerceIntrinsic!!
).apply {
putTypeArgument(0, from)
putTypeArgument(1, to)
putValueArgument(0, argument)
}
fun IrExpression.coerceToUnboxed() =
coerceInlineClasses(this, this.type, this.type.unboxInlineClass())
// Construct a reference to the JVM specific <unsafe-coerce> intrinsic.
// This code should be kept in sync with the declaration in JvmSymbols.kt.
@OptIn(ObsoleteDescriptorBasedAPI::class)
private val unsafeCoerceIntrinsic: IrSimpleFunctionSymbol? by lazy {
if (context.platform.isJvm()) {
context.irFactory.buildFun {
name = Name.special("<unsafe-coerce>")
origin = IrDeclarationOrigin.IR_BUILTINS_STUB
}.apply {
parent = IrExternalPackageFragmentImpl.createEmptyExternalPackageFragment(
context.moduleDescriptor,
FqName("kotlin.jvm.internal")
)
val src = addTypeParameter("T", context.irBuiltIns.anyNType)
val dst = addTypeParameter("R", context.irBuiltIns.anyNType)
addValueParameter("v", src.defaultType)
returnType = dst.defaultType
}.symbol
} else {
null
}
}
@OptIn(ObsoleteDescriptorBasedAPI::class)
fun IrSimpleFunction.sourceKey(): Int {
val info = context.irTrace[
ComposeWritableSlices.DURABLE_FUNCTION_KEY,
this
]
if (info != null) {
info.used = true
return info.key
}
val signature = symbol.descriptor.computeJvmDescriptor(withName = false)
val name = fqNameForIrSerialization
val stringKey = "$name$signature"
return stringKey.hashCode()
}
/*
* Delegated accessors are generated with IrReturn(IrCall(<delegated function>)) structure.
* To verify the delegated function is composable, this function is unpacking it and
* checks annotation on the symbol owner of the call.
*/
fun IrFunction.isComposableDelegatedAccessor(): Boolean =
origin == IrDeclarationOrigin.DELEGATED_PROPERTY_ACCESSOR &&
body?.let {
val returnStatement = it.statements.singleOrNull() as? IrReturn
val callStatement = returnStatement?.value as? IrCall
val target = callStatement?.symbol?.owner
target?.hasComposableAnnotation()
} == true
private val cacheFunction by guardedLazy {
getTopLevelFunctions(ComposeCallableIds.cache).first {
it.owner.valueParameters.size == 2 && it.owner.extensionReceiverParameter != null
}.owner
}
fun irCache(
currentComposer: IrExpression,
startOffset: Int,
endOffset: Int,
returnType: IrType,
invalid: IrExpression,
calculation: IrExpression
): IrCall {
val symbol = referenceFunction(cacheFunction.symbol)
return IrCallImpl(
startOffset,
endOffset,
returnType,
symbol as IrSimpleFunctionSymbol,
symbol.owner.typeParameters.size,
symbol.owner.valueParameters.size
).apply {
extensionReceiver = currentComposer
putValueArgument(0, invalid)
putValueArgument(1, calculation)
putTypeArgument(0, returnType)
}
}
fun irChanged(
currentComposer: IrExpression,
value: IrExpression,
inferredStable: Boolean,
compareInstanceForFunctionTypes: Boolean,
compareInstanceForUnstableValues: Boolean
): IrExpression {
// compose has a unique opportunity to avoid inline class boxing for changed calls, since
// we know that the only thing that we are detecting here is "changed or not", we can
// just as easily pass in the underlying value, which will avoid boxing to check for
// equality on recompositions. As a result here we want to pass in the underlying
// property value for inline classes, not the instance itself. The inline class lowering
// will turn this into just passing the wrapped value later on. If the type is already
// boxed, then we don't want to unnecessarily _unbox_ it. Note that if Kotlin allows for
// an overridden equals method of inline classes in the future, we may have to avoid the
// boxing in a different way.
val expr = value.unboxValueIfInline()
val type = expr.type
val stability = stabilityInferencer.stabilityOf(value)
val primitiveDescriptor = type.toPrimitiveType()
.let { changedPrimitiveFunctions[it] }
return if (!compareInstanceForUnstableValues) {
val descriptor = primitiveDescriptor
?: if (type.isFunction() && compareInstanceForFunctionTypes) {
changedInstanceFunction
} else {
changedFunction
}
irMethodCall(currentComposer, descriptor).also {
it.putValueArgument(0, expr)
}
} else {
val descriptor = when {
primitiveDescriptor != null -> primitiveDescriptor
compareInstanceForFunctionTypes && type.isFunction() -> changedInstanceFunction
stability.knownStable() -> changedFunction
inferredStable -> changedFunction
stability.knownUnstable() -> changedInstanceFunction
stability.isUncertain() -> changedInstanceFunction
else -> error("Cannot determine descriptor for irChanged")
}
irMethodCall(currentComposer, descriptor).also {
it.putValueArgument(0, expr)
}
}
}
fun irStartReplaceableGroup(
currentComposer: IrExpression,
key: IrExpression,
startOffset: Int = UNDEFINED_OFFSET,
endOffset: Int = UNDEFINED_OFFSET
): IrExpression {
return irMethodCall(
currentComposer,
startReplaceableFunction,
startOffset,
endOffset
).also {
it.putValueArgument(0, key)
}
}
fun irEndReplaceableGroup(
currentComposer: IrExpression,
startOffset: Int = UNDEFINED_OFFSET,
endOffset: Int = UNDEFINED_OFFSET,
): IrExpression {
return irMethodCall(
currentComposer,
endReplaceableFunction,
startOffset,
endOffset
)
}
fun IrStatement.wrap(
startOffset: Int = this.startOffset,
endOffset: Int = this.endOffset,
type: IrType,
before: List<IrStatement> = emptyList(),
after: List<IrStatement> = emptyList()
): IrContainerExpression {
return IrBlockImpl(
startOffset,
endOffset,
type,
null,
before + this + after
)
}
private val changedFunction = composerIrClass.functions
.first {
it.name.identifier == "changed" && it.valueParameters.first().type.isNullableAny()
}
private val changedInstanceFunction = composerIrClass.functions
.firstOrNull {
it.name.identifier == "changedInstance" &&
it.valueParameters.first().type.isNullableAny()
} ?: changedFunction
val startReplaceableFunction by guardedLazy {
composerIrClass.functions
.first {
it.name.identifier == "startReplaceableGroup" && it.valueParameters.size == 1
}
}
val endReplaceableFunction by guardedLazy {
composerIrClass.functions
.first {
it.name.identifier == "endReplaceableGroup" && it.valueParameters.size == 0
}
}
private fun IrType.toPrimitiveType(): PrimitiveType? = when {
isInt() -> PrimitiveType.INT
isBoolean() -> PrimitiveType.BOOLEAN
isFloat() -> PrimitiveType.FLOAT
isLong() -> PrimitiveType.LONG
isDouble() -> PrimitiveType.DOUBLE
isByte() -> PrimitiveType.BYTE
isChar() -> PrimitiveType.CHAR
isShort() -> PrimitiveType.SHORT
else -> null
}
private val changedPrimitiveFunctions by guardedLazy {
composerIrClass
.functions
.filter { it.name.identifier == "changed" }
.mapNotNull { f ->
f.valueParameters.first().type.toPrimitiveType()?.let { primitive ->
primitive to f
}
}
.toMap()
}
fun irMethodCall(
target: IrExpression,
function: IrFunction,
startOffset: Int = UNDEFINED_OFFSET,
endOffset: Int = UNDEFINED_OFFSET
): IrCall {
return irCall(function, startOffset, endOffset).apply {
dispatchReceiver = target
}
}
fun irCall(
function: IrFunction,
startOffset: Int = UNDEFINED_OFFSET,
endOffset: Int = UNDEFINED_OFFSET
): IrCall {
val type = function.returnType
val symbol = referenceFunction(function.symbol)
return IrCallImpl(
startOffset,
endOffset,
type,
symbol as IrSimpleFunctionSymbol,
symbol.owner.typeParameters.size,
symbol.owner.valueParameters.size
)
}
}
private val unsafeSymbolsRegex = "[ <>]".toRegex()
fun IrFunction.composerParam(): IrValueParameter? {
for (param in valueParameters.asReversed()) {
if (param.isComposerParam()) return param
if (!param.name.asString().startsWith('$')) return null
}
return null
}
fun IrValueParameter.isComposerParam(): Boolean =
name == KtxNameConventions.COMPOSER_PARAMETER && type.classFqName == ComposeFqNames.Composer
// FIXME: There is a `functionN` factory in `IrBuiltIns`, but it currently produces unbound symbols.
// We can switch to this and remove this function once KT-54230 is fixed.
fun IrPluginContext.function(arity: Int): IrClassSymbol =
referenceClass(ClassId(FqName("kotlin"), Name.identifier("Function$arity")))!!
@OptIn(ObsoleteDescriptorBasedAPI::class)
fun IrAnnotationContainer.hasAnnotationSafe(fqName: FqName): Boolean =
annotations.any {
// compiler helper getAnnotation fails during remapping in [ComposableTypeRemapper], so we
// use this impl
fqName == it.annotationClass?.descriptor?.fqNameSafe
}
// workaround for KT-45361
val IrConstructorCall.annotationClass
get() = type.classOrNull
inline fun <T> includeFileNameInExceptionTrace(file: IrFile, body: () -> T): T {
try {
return body()
} catch (e: ProcessCanceledException) {
throw e
} catch (e: Exception) {
throw Exception("IR lowering failed at: ${file.name}", e)
}
}
fun FqName.topLevelName() =
asString().substringBefore(".")