I'm hitting a dead-end trying to build out (what I...
# compiler
i
I'm hitting a dead-end trying to build out (what I thought would be simple) code generation in a compiler plugin. Essentially, I want to call a function with a
@Composable () -> Unit
lambda parameter: like this:
Copy code
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!
f
I'm pretty new to the compiler plugin APIs, but I think about FIR as the API for other kotlin code and IR as the implementation. Or FIR is generating Stubs which are filled out in the IR phase. Not sure if this is helpful at all haha
c
You can also generate code in FIR, and it has a nice set of builder methods for common constructs. @Isaac Udy What are the error messages you're getting?
i
You 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:
Copy code
@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:
Copy code
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)
Using an IrTransformer and doing a
dumpKotlinLike
on the generated function produces something like this:
Copy code
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 exist
I was able to fix this problem by making the following change to the anonymous function's body:
Copy code
// 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
}
which results in the
dumpKotlinLike
for the function looking like this:
Copy code
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()
    }
  }
)))
}
And then everything works correctly, as the function is actually inlined in the IR there, rather than referring to some external function that's not making it into the compiled code.
If anyone knows how to make the function inlined like that, without needing to add the additional property access parameter, I'd be very interested to know!