Isaac Udy
11/29/2025, 3:18 AM@Composable () -> Unit lambda parameter: like this:
fun myGeneratedFunction() {
someOtherExternalFunction( parameter = { MyExternalComposableFunction() } )
}
But I can't figure out how to do this correctly. Attempting the generation in the IR phase leaves me with compiler errors related to unhandled intrinsics, and attempting the generation in the FIR phase is giving me grief about constructing the lambda/anonymous function call - it compiles fine, but then crashes at runtime because the anonymous function apparently isn't making it into the byte code.
Would love some help/tips if anyone has experience generating code that calls Composables in either IR or FIR in a compiler plugin. Thanks!Fergus Hewson
11/29/2025, 6:18 PMCaleb B
11/29/2025, 8:01 PMIsaac Udy
11/29/2025, 10:18 PMYou can also generate code in FIR, and it has a nice set of builder methods for common constructs.Yeah, I was trying this but that's what was giving me a runtime crash. I've managed to solve this now, I'll dump some examples here: I've had to snip out a bunch of the code, otherwise it's very long, but:
@OptIn(SymbolInternals::class)
private fun buildScopeDestinationCall(
scopeParameter: FirValueParameterSymbol,
navigationKeyClassId: ClassId,
composableFunctionSymbol: FirNamedFunctionSymbol,
): List<FirStatement> {
val builderDestinationCallableId = ...
val builderSymbol = ...
val builderDestinationParameter = ...
val builderDestinationReference = buildResolvedNamedReference { ... }
val builderDestinationSymbol = ...
// Find the navigationDestination function
val navigationDestinationCallableId = ...
val navigationDestinationSymbol = ...
val navigationDestinationReference = buildResolvedNamedReference { ... }
val contentParameterSymbol = ...
// Create the type argument for NavigationKey
val navigationKeyType = navigationKeyClassId.constructClassLikeType()
val lambdaSymbol = FirAnonymousFunctionSymbol()
val anonymousFunction = buildAnonymousFunction {
...
// Set the body
body = buildBlock {
statements += buildFunctionCall { ... }
}
}
val lambdaExpression = buildAnonymousFunctionExpression {
...
this.anonymousFunction = anonymousFunction
}
// Build the navigationDestination<KeyType> { ... } call
val navigationDestinationCall = buildFunctionCall {
...
argumentList = buildResolvedArgumentList(
original = null,
mapping = linkedMapOf(
lambdaExpression to contentParameterSymbol.fir,
),
)
}
// Build the scope parameter access: `scope`
val scopeAccess = buildPropertyAccessExpression { ... }
return listOf(
buildFunctionCall {
source = null
calleeReference = builderDestinationReference
coneTypeOrNull = builderDestinationSymbol.resolvedReturnType
dispatchReceiver = scopeAccess
argumentList = buildResolvedArgumentList(
original = null,
mapping = linkedMapOf(
navigationDestinationCall to builderDestinationParameter.fir,
)
)
typeArguments += org.jetbrains.kotlin.fir.types.builder.buildTypeProjectionWithVariance {
this.typeRef = buildResolvedTypeRef { coneType = navigationKeyType }
this.variance = org.jetbrains.kotlin.types.Variance.INVARIANT
}
}
)
}
The FIR generated here basically appears to work, it compiles fine, but then at runtime I would get this error:
java.lang.NoSuchMethodError: No virtual method getLambda$31314613$application_debug()Lkotlin/jvm/functions/Function3; in class Lenro_generated_bindings/ComposableSingletons$__GENERATED_DECLARATIONS__Kt; or its super classes (declaration of 'enro_generated_bindings.ComposableSingletons$__GENERATED_DECLARATIONS__Kt' appears in /data/data/dev.enro.tests.application/code_cache/.overlay/base.apk/classes12.dex)
at enro_generated_bindings._dev_enro_tests_application_NestedContainerExampleScreenBinding.bind(Unknown Source:9)Isaac Udy
11/29/2025, 10:22 PMdumpKotlinLike on the generated function produces something like this:
fun bind(scope: BuilderScope) {
scope.destination<ModuleOneDestination>(destination = navigationDestination<ModuleOneDestination>(content = ComposableSingletons$__GENERATED_DECLARATIONS__Kt.<get-lambda$-2118814483>()))
}
and looking through the generated class files, it's true that get-lambda... doesn't existIsaac Udy
11/29/2025, 10:23 PM// Set the body
body = buildBlock {
// This property access expression is important to bind the anonymous function
// to the outer function scope, otherwise the function anonymous function
// gets added to the ComposableSingletons and doesn't appear to end up in the
// *actual* compiled files, and will throw runtime errors because it can't be found
statements += buildPropertyAccessExpression {
source = null
calleeReference = buildResolvedNamedReference {
source = null
name = scopeParameter.name
resolvedSymbol = scopeParameter
}
coneTypeOrNull = scopeParameter.resolvedReturnType
}
statements += composableFunctionCall
}Isaac Udy
11/29/2025, 10:24 PMdumpKotlinLike for the function looking like this:
fun bind(scope: BuilderScope) {
scope.destination<TestModuleEditableDestination>(destination = navigationDestination<TestModuleEditableDestination>(content = composableLambdaInstance(key = -1188984987, tracked = true, block = @Composable
@ComposableTarget(applier = "androidx.compose.ui.UiComposable")
local fun NavigationDestinationScope<TestModuleEditableDestination>.<anonymous>(/* var */ $composer: Composer?, $changed: Int) {
sourceInformation(composer = $composer, sourceInformation = "C:__GENERATED DECLARATIONS__.kt#u18akp")
when {
$composer.shouldExecute(parametersChanged = EQEQ(arg0 = $changed.and(other = 17), arg1 = 16).not(), flags = $changed.and(other = 1)) -> { // BLOCK
when {
isTraceInProgress() -> traceEventStart(key = -1188984987, dirty1 = $changed, dirty2 = -1, info = "enro_generated_bindings._dev_enro_tests_module_TestModuleEditableScreenBinding.bind.<anonymous> (__GENERATED DECLARATIONS__.kt:-1)")
}
scope /*~> Unit */
TestModuleEditableScreen($composer = $composer, $changed = 0)
when {
isTraceInProgress() -> traceEventEnd()
}
}
else -> $composer.skipToGroupEnd()
}
}
)))
}Isaac Udy
11/29/2025, 10:25 PMIsaac Udy
11/29/2025, 10:26 PM