Compose Compiler Stability Inference System

skydovesJaewoong Eum (skydoves)||44 min read

Compose Compiler Stability Inference System

A comprehensive study of how the Compose compiler determines type stability for recomposition optimization.

Table of Contents

Chapter 1: Foundations

1.1 Introduction

The Compose compiler implements a stability inference system to enable recomposition optimization. This system analyzes types at compile time to determine whether their values can be safely compared for equality during recomposition.

Source File: compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt

The inference process involves analyzing type declarations, examining field properties, and tracking stability through generic type parameters. The results inform the runtime whether to skip recomposition when parameter values remain unchanged.

1.2 Core Concepts

Stability Definition

A type is considered stable when it satisfies three conditions:

  1. Immutability: The observable state of an instance does not change after construction
  2. Equality semantics: Two instances with equal observable state are equal via equals()
  3. Change notification: If the type contains observable mutable state, all state changes trigger composition invalidation

These properties allow the runtime to make optimization decisions based on value comparison.

Recomposition Mechanics

When a composable function receives parameters, the runtime determines whether to execute the function body:

@Composable
fun UserProfile(user: User) {
    // Function body
}

The decision process:

  1. Compare the new user value with the previous value
  2. If equal and the type is stable, skip recomposition
  3. If different or unstable, execute the function body

Without stability information, the runtime must conservatively recompose on every invocation, regardless of whether parameters changed.

1.3 The Role of Stability

Performance Impact

Stability inference affects recomposition in three ways:

Smart Skipping: Composable functions with stable parameters can be skipped when parameter values remain unchanged. This reduces the number of function executions during recomposition.

Comparison Propagation: The compiler passes stability information to child composable calls, enabling nested optimizations throughout the composition tree.

Comparison Strategy: The runtime selects between structural equality (equals()) for stable types and referential equality (===) for unstable types, affecting change detection behavior.

Consider this example:

// Unstable parameter type - interface with unknown stability
@Composable
fun ExpensiveList(items: List<String>) {
    // List is an interface - has Unknown stability
    // Falls back to instance comparison
}

// Stable parameter type - using immutable collection
@Composable
fun ExpensiveList(items: ImmutableList<String>) {
    // ImmutableList is in KnownStableConstructs
    // Can skip recomposition when unchanged
}

// Alternative: Using listOf() result
@Composable
fun ExpensiveList(items: List<String>) {
    // If items comes from listOf(), the expression is stable
    // But the List type itself is still an interface with Unknown stability
}

The key insight: List and MutableList are both interfaces with Unknown stability. To achieve stable parameters, use:

  1. ImmutableList from kotlinx.collections.immutable (in KnownStableConstructs)
  2. Add kotlin.collections.List to your stability configuration file
  3. Use @Stable annotation on your data classes containing List

Chapter 2: Stability Type System

2.1 Type Hierarchy

The compiler represents stability through a sealed class hierarchy defined in :

sealed class Stability {
    class Certain(val stable: Boolean) : Stability()
    class Runtime(val declaration: IrClass) : Stability()
    class Unknown(val declaration: IrClass) : Stability()
    class Parameter(val parameter: IrTypeParameter) : Stability()
    class Combined(val elements: List<Stability>) : Stability()
}

Each subtype represents a different category of stability information available to the compiler.

2.2 Compile-Time Stability

Stability.Certain

This type represents stability that can be determined completely at compile time.

Structure:

class Certain(val stable: Boolean) : Stability()

The stable field indicates whether the type is definitely stable (true) or definitely unstable (false).

Examples:

// Certain(stable = true)
class Point(val x: Int, val y: Int)

// Certain(stable = false)
class Counter(var count: Int)

Usage Conditions:

  • Primitive types (Int, Long, Boolean, etc.)
  • String and Unit
  • Function types (FunctionN, KFunctionN)
  • Classes with only stable val properties
  • Classes with any var property (immediately unstable)
  • Classes marked with @Stable or @Immutable annotations

Implementation: See for the knownStable() extension function.

2.3 Runtime Stability

Stability.Runtime

This type indicates that stability must be checked at runtime by reading a generated $stable field.

Structure:

class Runtime(val declaration: IrClass) : Stability()

The declaration references the class whose stability requires runtime determination.

Generated Code Example:

// Source code
class Box<T>(val value: T)

// Compiler-generated code
@StabilityInferred(parameters = 0b1)
class Box<T>(val value: T) {
    companion object {
        @JvmField
        val $stable: Int = /* computed based on type parameters */
    }
}

When Applied:

  • Classes from external modules (separately compiled)
  • Generic classes where type parameters affect stability
  • Classes with @StabilityInferred annotation

Runtime Behavior:

At instantiation sites, the runtime computes the $stable field value:

Box<Int>         // $stable = STABLE (0b000)
Box<MutableList> // $stable = UNSTABLE (0b100)

Implementation: See and .

2.4 Uncertain Stability

Stability.Unknown

This type represents cases where the compiler cannot determine stability.

Structure:

class Unknown(val declaration: IrClass) : Stability()

Examples:

interface Repository {
    fun getData(): String
}

class Screen(val source: Repository)
// Repository has Unknown stability

Usage Conditions:

  • Interface types (unknown implementations)
  • Abstract classes without concrete analysis
  • Types in incremental compilation scenarios

Runtime Behavior:

When encountering Unknown stability, the runtime falls back to instance comparison (===) for change detection. This conservative approach ensures correctness but prevents skipping optimizations.

Implementation: See .

2.5 Parametric Stability

Stability.Parameter

This type represents stability that depends on a generic type parameter.

Structure:

class Parameter(val parameter: IrTypeParameter) : Stability()

Example:

class Wrapper<T>(val value: T)
//              ^^^^^^^^^^^^
// Stability depends on T

// Instantiation examples:
Wrapper<Int>       // Stable (Int is stable)
Wrapper<Counter>   // Unstable (Counter from 2.2 is unstable)

Resolution Process:

When analyzing Wrapper<Int>:

  1. Identify value: T has Stability.Parameter(T)
  2. Substitute T with Int
  3. Evaluate stabilityOf(Int) = Stable
  4. Result: Wrapper<Int> is stable

Implementation: See for type parameter handling.

2.6 Combined Stability

Stability.Combined

This type aggregates multiple stability factors from different sources.

Structure:

class Combined(val elements: List<Stability>) : Stability()

Examples:

class Complex<T, U>(
    val primitive: Int,     // Certain(stable = true)
    val param1: T,          // Parameter(T)
    val param2: U           // Parameter(U)
)
// Combined([Certain(true), Parameter(T), Parameter(U)])

Combination Rules:

The compiler combines stabilities using the plus operator ():

Stable     + Stable     = Stable
Stable     + Unstable   = Unstable
Unstable   + Stable     = Unstable
Stable     + Parameter  = Combined([Parameter])
Parameter  + Parameter  = Combined([Parameter, Parameter])
Runtime    + Parameter  = Combined([Runtime, Parameter])

Key Property: Unstable stability dominates all combinations. A single unstable component makes the entire result unstable.

2.7 Stability Decision Tree

The Compose compiler follows a systematic decision tree when determining stability. This tree represents the actual logic flow implemented in the compiler.

Complete Decision Tree

┌─────────────────────────────────┐
│     Start: Analyze Type/Class   │
└────────────┬────────────────────┘
             │
             ▼
┌─────────────────────────────────┐
│  Is it a primitive type?        │───Yes──→ [STABLE]
│  (Int, Boolean, Float, etc.)    │
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Is it String or Unit?          │───Yes──→ [STABLE]
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Is it a function type?         │───Yes──→ [STABLE]
│  (Function<*>, KFunction<*>)    │
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Has @Stable or @Immutable?     │───Yes──→ [STABLE]
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Has @StableMarker descendant?  │───Yes──→ [STABLE]
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Is it an Enum class/entry?     │───Yes──→ [STABLE]
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Is it a Protobuf type?         │───Yes──→ [STABLE]
│  (GeneratedMessage/Lite)        │
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Is it in KnownStableConstructs?│───Yes──→ [STABLE/RUNTIME]
│  (Pair, Triple, etc.)           │          (check type params)
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Matches external config?       │───Yes──→ [STABLE/RUNTIME]
│  (stability-config.conf)        │          (check type params)
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Is it an interface?            │───Yes──→ [UNKNOWN]
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Is it from external Java?      │───Yes──→ [UNSTABLE]
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Has @StabilityInferred?        │───Yes──→ [RUNTIME]
│  (from separate compilation)    │          (use bitmask)
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  Analyze class members:         │
│  - Check all properties         │
│  - Check backing fields         │
│  - Check superclass             │
└────────────┬────────────────────┘
             │
             ▼
┌─────────────────────────────────┐
│  Any var (mutable) property?    │───Yes──→ [UNSTABLE]
└────────────┬────────────────────┘
             │ No
             ▼
┌─────────────────────────────────┐
│  All members stable?            │───Yes──→ [STABLE]
└────────────┬────────────────────┘
             │ No
             ▼
        [UNSTABLE/COMBINED]

Decision Tree for Generic Types

When analyzing generic types, the compiler follows an additional decision path:

┌─────────────────────────────────┐
│   Generic Type: Class<T1, T2>   │
└────────────┬────────────────────┘
             │
             ▼
┌─────────────────────────────────┐
│  Base class stable?             │───No───→ [UNSTABLE]
└────────────┬────────────────────┘
             │ Yes
             ▼
┌─────────────────────────────────┐
│  Has stability bitmask?         │───No───→ Analyze each
│  (from KnownStableConstructs    │         type parameter
│   or external config)           │         individually
└────────────┬────────────────────┘
             │ Yes
             ▼
┌─────────────────────────────────┐
│  For each type parameter Ti:    │
│  Is bit i set in bitmask?       │───No───→ Ti doesn't affect
└────────────┬────────────────────┘         stability
             │ Yes
             ▼
┌─────────────────────────────────┐
│  Check stability of actual      │
│  type argument for Ti           │───→ Combine all results
└─────────────────────────────────┘

This article continues for subscribers

Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.

Become a Sponsor