Lesson 3

Stability becomes comparison metadata

Read compiler stability inference, stable versus immutable promises, changed comparisons, and dirty masks without treating bit layouts as API.

3 min readUpdated Jul 12, 2026

Status: Stability is compiler/runtime protocol input, not reflection. Its promise model is Durable ; inferred categories, fields, comparisons, and mask positions are Version-specific .

Outcome

Explain why a parameter gets equality or identity comparison, and describe dirty masks as per-parameter metadata rather than memorize their packing.

Inference is more than “stable or unstable”

The pinned compiler represents stability as Certain, Runtime, Unknown, Parameter, or Combined (Stability.kt). Primitives, strings, functions, enums, and objects can be known stable; mutable backing properties can make a class unstable; generic and external types can remain runtime or uncertain. This analyzes declarations and expressions; it does not mean fields are frozen.

ClassStabilityTransformer emits @StabilityInferred(parameters = ...) and, for public or internal classes, a %stable field (source). Separately compiled code can then recover stability metadata.

Stable is a behavioral promise; immutable is stronger

@Stable promises consistent equality, composition notification for public property changes, and stable public property types (Stable.kt). It can therefore hide private mutable state or use a notifying state-backed property.

@Immutable promises that public properties and fields never change after construction; the annotation is not validated and is stronger than val (Immutable.kt). Both are stability markers, not warnings to silence: breaking their assumptions makes optimization behavior undefined.

What the comparisons mean

The runtime documents Composer.changed(value) as value equality and changedInstance(value) as === (Composer.kt). The stable Int golden uses changed; strong-skipping output for an unstable parameter uses changedInstance. An unchanged instance can therefore skip despite a mutated field.

A dirty mask carries per-parameter states such as same, different, static, uncertain, and stability information between transformed calls. At this pinned compiler revision, ParamState and ComposableFunctionBodyTransformer.kt explicitly use three conceptual bits per slot: the field for parameter slot i occupies bits i*3 + 1 through i*3 + 3, while bit 0 is reserved to force execution (ParamState and bitsForSlot; packing notes). This is compiler-internal encoding, not a public mask layout. Ask “what does the callee know?” rather than copying a mask recipe.

Captured output: compare two inputs

These are exact excerpts from pinned compiler goldens; unrelated lines are omitted, and <> remains the golden renderer’s generated-key placeholder.

Stable Int parameter, testSingleStableParam.txt:

if (%changed and 0b0110 == 0) {
  %dirty = %dirty or if (%composer.changed(x)) 0b0100 else 0b0010
}

Unstable Foo parameter, testSingleUnstableParam.txt:

@StabilityInferred(parameters = 0)
class Foo(var value: Int = 0) {
  val %stable: Int = 8
    get() {
      return field
    }
}
...
%dirty = %dirty or if (%composer.changedInstance(x)) 0b0100 else 0b0010

The ellipsis simplifies captured lines; it is not generated output.

Reproduce the comparison

This is a pinned compiler-test procedure, not an experiment run for this lesson. From a clean Kotlin checkout at a1ab72da08c7cc2acca226db6193ea38d988e253, run the focused compiler tests:

./gradlew :plugins:compose-compiler-plugin:compiler-hosted:integration-tests:test \
  --tests 'androidx.compose.compiler.plugins.kotlin.FunctionBodySkippingTransformTests.testSingleStableParam' \
  --tests 'androidx.compose.compiler.plugins.kotlin.FunctionBodySkippingTransformTests.testSingleUnstableParam'

Control: compare the two goldens. Expected: both contain restart/skip protocol, but metadata differs. Limit: a golden proves only its fixture and flags.

Check yourself

Why is @Immutable not equivalent to “all properties are Kotlin val”? Why can changedInstance miss a mutation that a stable equals comparison could report?

Evidence ledger

ClaimDirect evidenceStatus
Compiler stability has certain, runtime, uncertain, parameter, and combined formsPinned Stability.ktVersion-specific
Stable and immutable annotations express different guaranteesPinned Stable.kt and Immutable.ktDurable
changed and changedInstance encode equality versus identity checksPinned Composer.ktVersion-specific
Stability metadata and comparison calls appear in captured outputPinned testSingleStableParam.txt and testSingleUnstableParam.txtVersion-specific

Freshness

Refresh when StabilityInferencer, stable-marker annotations, StabilityBits, or the compiler golden renderer changes. Treat exact bit positions, generated field values, and class eligibility as stale until the pinned implementation and goldens are re-read.

Finished this lesson?

Your progress stays only in this browser.