Lesson 3
Transformed calls and composable function references
Follow ordinary-looking calls and callable references through compiler adaptation and compatibility checks.
Status: A call and a function reference are different compiler problems even when both eventually invoke composable code. That distinction is Durable . Adapter shapes, memoization, and feature detection are Version-specific .
Outcome
You should be able to answer this question:
Why can
Child(value)look like an ordinary call while::Childneeds an adapter?
A direct call is rewritten in the caller’s current composer context. A reference must be represented as a value with a callable type, exposing composer and change information while hiding synthetic default-mask details.
Direct calls carry the current protocol
Start with source:
@Composable
fun Parent() {
Child(42)
}
@Composable
fun Child(value: Int) { /* ... */ }
The compiler transforms Child and rewrites the call to pass the current composer plus changed information. Runtime reflection is not involved.
A canonical golden shows this pattern. In the pinned TraceInformationTest/testBasicComposableFunctions.txt, the captured call is:
fun C(%composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
if (%composer.shouldExecute(%changed != 0, %changed and 0b0001)) {
A().B(1337, %composer, 0b0110)
} else {
%composer.skipToGroupEnd()
}
}
The excerpt is captured output with unrelated source-information and trace lines omitted. As before, <> is the golden renderer’s placeholder for a generated key. The call’s 0b0110 is an example of changed information, not a recipe to copy into every function.
References need a shape change
Now make the child a value:
@Composable
fun Host(content: @Composable (Int) -> Unit) {
content(42)
}
@Composable
fun Child(value: Int) { /* ... */ }
val reference: @Composable (Int) -> Unit = ::Child
At the lowered level, a composable function with one value parameter has a function type that also carries a composer and an integer. The pinned compiler golden FunctionReferenceTransformTests/reference_adapted.txt captures an adapter for a reference with a mismatched source-level shape:
val tmp0_group = %composer.cache(false) {
@Composable
@FunctionKeyMeta(key = -1453109843, startOffset = 150, endOffset = 154)
fun Fn(p0: Int, %composer: Composer?, %changed: Int) {
Fn(p0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %composer, 0b1110 and %changed, 0b001111111110)
}
::Fn
}
This is a shortened excerpt from the pinned golden, not pseudocode. The adapter supplies omitted values and forwards the composer. The $default mask stays hidden from the reference-facing type.
The current ComposerParamTransformer.kt adapts non-restartable references and references to functions with defaults. It does not pretend either original shape matches the reference type.
Compatibility is a protocol feature
The compiler and runtime are not independent black boxes. RuntimeFeatures.kt currently models target runtime versions 1.8 and 1.9, with source information targeted at 1.9. VersionChecker.kt reads ComposeVersion and fails for an old runtime. The Compose Compiler 1.5.11 notes show the same shipped principle: use startReplaceGroup only when the target runtime contains it (release notes).
Experiment: direct call versus reference
Use FunctionReferenceTransformTests as a control set. Compare reference.txt, reference_adapted.txt, and reference_default.txt. Record the source-level type, adapter presence, cache block, and default argument location.
Expected: direct calls pass the composer; references may add an adapter and memoize it. Limit: cache in one golden is not a universal allocation or performance claim.
Misconceptions
- “A composable function reference is just a normal Kotlin reference.” Its lowered callable shape carries Compose protocol parameters.
- “Adapters mean the runtime uses reflection.” The compiler emits ordinary functions and calls.
- “A compiler/runtime mismatch merely disables skipping.” The version checker exists because protocol methods and data can be incompatible.
Check yourself
Why must a reference to a composable function with default parameters hide a default mask from the reference-facing type? What does the adapter provide instead?
Evidence notes
| Claim | Direct evidence | Status |
|---|---|---|
| Direct composable calls are rewritten with composer and changed arguments | ComposerParamTransformer.kt and pinned compiler goldens | Version-specific |
| Composable references can require adapters for non-restartable functions or defaults | ComposerParamTransformer.kt, visitFunctionReference | Version-specific |
| Compiler/runtime features are version-checked rather than assumed | RuntimeFeatures.kt, VersionChecker.kt, and release notes | Version-specific |
Freshness
Refresh this lesson when function-reference lowering, RuntimeFeatures, VersionChecker, or the compiler’s golden tests move. Recheck release notes before mapping a source feature to a released compiler/runtime pair.
Finished this lesson?
Your progress stays only in this browser.