Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ abstract class AbstractLiveLiteralTransformTests(
pluginContext: IrPluginContext,
) {
val keyVisitor = DurableKeyVisitor(builtKeys)
val stabilityInferencer = StabilityInferencer(
pluginContext.moduleDescriptor,
emptySet()
)
val stabilityInferencer = StabilityInferencer(emptySet())
val featureFlags = FeatureFlags()
val transformer = object : LiveLiteralTransformer(
liveLiteralsEnabled || liveLiteralsV2Enabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,6 @@ class ClassStabilityTransformTests(useFir: Boolean) : AbstractIrTransformTest(us
"Uncertain(Foo)"
)

@Test
fun testInterfacesAreUncertainOnIncrementalCompilation() {
assertStability(
classDefSrc = """
interface Foo
""",
transform = {
it.origin = IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB
},
stability = "Uncertain(Foo)"
)
}

@Test
fun testInterfaceWithStableValAreUncertain() = assertStability(
"interface Foo { val value: Int }",
Expand All @@ -223,6 +210,40 @@ class ClassStabilityTransformTests(useFir: Boolean) : AbstractIrTransformTest(us
"Runtime(A),Runtime(B),Runtime(C)"
)

// In the past, if an `internal` type `T` was inferred to be stable when it was compiled in a
// compilation unit `A`, we would infer it to be stable when compiling a compilation unit `B`
// that uses the type. This caused failures in situations in which `T` was later made unstable
// by an edit and `A` was incrementally recompiled, but `B` was not. This test serves as a
// regression test against such failures, e.g. https://issuetracker.google.com/issues/427530633.
@Test
fun testInferredStabilityOfInternalTypesFromOtherCompilationUnits() {
assertStability(
classDefSrc = """
import androidx.compose.runtime.internal.StabilityInferred

@StabilityInferred(parameters = 1)
internal class Foo
""",
transform = {
it.origin = IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB
},
stability = "Runtime(Foo)"
)

assertStability(
classDefSrc = """
import androidx.compose.runtime.internal.StabilityInferred

@StabilityInferred(parameters = 1)
internal interface Bar
""",
transform = {
it.origin = IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB
},
stability = "Runtime(Bar)"
)
}

@Test
fun testStable17() = assertStability(
"""
Expand Down Expand Up @@ -1720,7 +1741,7 @@ class ClassStabilityTransformTests(useFir: Boolean) : AbstractIrTransformTest(us
})
val irClass = (irModule.files.last().declarations.first() as IrClass).apply(transform)
val externalTypeMatchers = externalTypes.map { FqNameMatcher(it) }.toSet()
val stabilityInferencer = StabilityInferencer(irModule.descriptor, externalTypeMatchers)
val stabilityInferencer = StabilityInferencer(externalTypeMatchers)
val classStability = stabilityInferencer.stabilityOf(irClass.defaultType as IrType)

assertEquals(
Expand Down Expand Up @@ -1749,8 +1770,7 @@ class ClassStabilityTransformTests(useFir: Boolean) : AbstractIrTransformTest(us
val irClass = irModule.files.last().declarations.first() as IrClass
val externalTypeMatchers = externalTypes.map { FqNameMatcher(it) }.toSet()
val classStability =
StabilityInferencer(irModule.descriptor, externalTypeMatchers)
.stabilityOf(irClass.defaultType as IrType)
StabilityInferencer(externalTypeMatchers).stabilityOf(irClass.defaultType as IrType)

assertEquals(
stability,
Expand Down Expand Up @@ -1893,7 +1913,7 @@ class ClassStabilityTransformTests(useFir: Boolean) : AbstractIrTransformTest(us
}
val externalTypeMatchers = externalTypes.map { FqNameMatcher(it) }.toSet()
val exprStability =
StabilityInferencer(irModule.descriptor, externalTypeMatchers).stabilityOf(irExpr)
StabilityInferencer(externalTypeMatchers).stabilityOf(irExpr)

assertEquals(
stability,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,23 @@ class FunctionBodySkippingTransformTests(
"""
)

@Test
fun testInternalStableUnstableParams(): Unit = comparisonPropagation(
"""
internal class Foo(var value: Int = 0)
""",
"""
@Composable
internal fun CanSkip(foo: Foo = Foo()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this test makes a lot of sense as is.

You want to check for codegen of parameters with the types of internal class FooStable(val value: Int) and internal class FooUnstable(var value: Int) instead of unstable class only.

The other thing I noticed is that unstable class produced changedInstance rather than changed call, which tells me that changedInstance is always generated for Runtime stability. Please file a bug for this.

Also no need to check default arguments, that does not affect stability here.

used(foo)
}
@Composable
internal fun CannotSkip(foo: Foo) {
used(foo)
}
"""
)

@Test
fun testOptionalUnstableWithStableExtensionReceiver(): Unit = comparisonPropagation(
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// Source
// ------------------------------------------

import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable


@Composable
internal fun CanSkip(foo: Foo = Foo()) {
used(foo)
}
@Composable
internal fun CannotSkip(foo: Foo) {
used(foo)
}

//
// Transformed IR
// ------------------------------------------

@Composable
internal fun CanSkip(foo: Foo?, %composer: Composer?, %changed: Int, %default: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(CanSkip):Test.kt")
val %dirty = %changed
if (%changed and 0b0110 == 0) {
%dirty = %dirty or if (%default and 0b0001 == 0 && %composer.changedInstance(foo)) 0b0100 else 0b0010
}
if (%composer.shouldExecute(%dirty and 0b0011 != 0b0010, %dirty and 0b0001)) {
%composer.startDefaults()
if (%changed and 0b0001 == 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 != 0) {
foo = Foo()
%dirty = %dirty and 0b1110.inv()
}
} else {
%composer.skipToGroupEnd()
if (%default and 0b0001 != 0) {
%dirty = %dirty and 0b1110.inv()
}
}
%composer.endDefaults()
if (isTraceInProgress()) {
traceEventStart(<>, %dirty, -1, <>)
}
used(foo as Foo)
if (isTraceInProgress()) {
traceEventEnd()
}
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
CanSkip(foo, %composer, updateChangedFlags(%changed or 0b0001), %default)
}
}
@Composable
internal fun CannotSkip(foo: Foo, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(CannotSkip):Test.kt")
val %dirty = %changed
if (%changed and 0b0110 == 0) {
%dirty = %dirty or if (%composer.changedInstance(foo)) 0b0100 else 0b0010
}
if (%composer.shouldExecute(%dirty and 0b0011 != 0b0010, %dirty and 0b0001)) {
if (isTraceInProgress()) {
traceEventStart(<>, %dirty, -1, <>)
}
used(foo)
if (isTraceInProgress()) {
traceEventEnd()
}
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
CannotSkip(foo, %composer, updateChangedFlags(%changed or 0b0001))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// Source
// ------------------------------------------

import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable


@Composable
internal fun CanSkip(foo: Foo = Foo()) {
used(foo)
}
@Composable
internal fun CannotSkip(foo: Foo) {
used(foo)
}

//
// Transformed IR
// ------------------------------------------

@Composable
internal fun CanSkip(foo: Foo?, %composer: Composer?, %changed: Int, %default: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(CanSkip)N(foo):Test.kt")
val %dirty = %changed
if (%changed and 0b0110 == 0) {
%dirty = %dirty or if (%default and 0b0001 == 0 && %composer.changedInstance(foo)) 0b0100 else 0b0010
}
if (%composer.shouldExecute(%dirty and 0b0011 != 0b0010, %dirty and 0b0001)) {
%composer.startDefaults()
if (%changed and 0b0001 == 0 || %composer.defaultsInvalid) {
if (%default and 0b0001 != 0) {
foo = Foo()
%dirty = %dirty and 0b1110.inv()
}
} else {
%composer.skipToGroupEnd()
if (%default and 0b0001 != 0) {
%dirty = %dirty and 0b1110.inv()
}
}
%composer.endDefaults()
if (isTraceInProgress()) {
traceEventStart(<>, %dirty, -1, <>)
}
used(foo as Foo)
if (isTraceInProgress()) {
traceEventEnd()
}
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
CanSkip(foo, %composer, updateChangedFlags(%changed or 0b0001), %default)
}
}
@Composable
internal fun CannotSkip(foo: Foo, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(CannotSkip)N(foo):Test.kt")
val %dirty = %changed
if (%changed and 0b0110 == 0) {
%dirty = %dirty or if (%composer.changedInstance(foo)) 0b0100 else 0b0010
}
if (%composer.shouldExecute(%dirty and 0b0011 != 0b0010, %dirty and 0b0001)) {
if (isTraceInProgress()) {
traceEventStart(<>, %dirty, -1, <>)
}
used(foo)
if (isTraceInProgress()) {
traceEventEnd()
}
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
CannotSkip(foo, %composer, updateChangedFlags(%changed or 0b0001))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ class ComposeIrGenerationExtension(
return
}

val stabilityInferencer = StabilityInferencer(
pluginContext.moduleDescriptor,
stableTypeMatchers,
)
val stabilityInferencer = StabilityInferencer(stableTypeMatchers)

if (useK2) {
moduleFragment.acceptVoid(ComposableLambdaAnnotator(pluginContext))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
import androidx.compose.compiler.plugins.kotlin.lower.annotationClass
import androidx.compose.compiler.plugins.kotlin.lower.isSyntheticComposableFunction
import org.jetbrains.kotlin.backend.jvm.ir.isInlineClassType
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol
Expand Down Expand Up @@ -197,7 +195,6 @@ private data class SymbolForAnalysis(
)

class StabilityInferencer(
private val currentModule: ModuleDescriptor,
externalStableTypeMatchers: Set<FqNameMatcher>,
) {
private val externalTypeMatcherCollection = FqNameMatcherCollection(externalStableTypeMatchers)
Expand Down Expand Up @@ -238,25 +235,14 @@ class StabilityInferencer(
mask = externalTypeMatcherCollection
.maskForName(declaration.fqNameWhenAvailable) ?: 0
stability = Stability.Stable
} else if (declaration.isInterface && declaration.isInCurrentModule()) {
// trying to avoid extracting stability bitmask for interfaces in current module
// to support incremental compilation
return Stability.Unknown(declaration)
} else {
val bitmask = declaration.stabilityParamBitmask() ?: return Stability.Unstable

val knownStableMask =
if (typeParameters.size < 32) 0b1 shl typeParameters.size else 0
val isKnownStable = bitmask and knownStableMask != 0
mask = bitmask and knownStableMask.inv()

// supporting incremental compilation, where declaration stubs can be
// in the same module, so we need to use already inferred values
stability = if (isKnownStable && declaration.isInCurrentModule()) {
Stability.Stable
} else {
Stability.Runtime(declaration)
}
stability = Stability.Runtime(declaration)
}
return when {
mask == 0 || typeParameters.isEmpty() -> stability
Expand Down Expand Up @@ -305,10 +291,6 @@ class StabilityInferencer(
return stability
}

@OptIn(ObsoleteDescriptorBasedAPI::class)
private fun IrDeclaration.isInCurrentModule() =
module == currentModule

private fun IrClass.isProtobufType(): Boolean {
// Quick exit as all protos are final
if (!isFinalClass) return false
Expand Down