In a compiler plugin, if I want to add an addition...
# compiler
d
In a compiler plugin, if I want to add an additional value parameter to certain annotated functions in the FIR stage, how would I best go about this? None of the FIR extensions seem to support this.
y
Hmmm, it depends a lot on how you expect it to work. The simplest (but hacky) way would be to do it in
FirStatusTransformerExtension
.
Is the value parameter for callers of the function, or for the function body itself? If it's for callers, you can do the normal generation extension stuff
d
It’s not for callers, and I can’t really use the generation extension because I don’t want to create a new function. I intend to supply the argument value in the IR stage at all call sites.
I suppose I could use
FirStatusTransformerExtension
indeed, now that I look at it. I just wonder how safe that is.
y
You could (again in
FirStatusTransformerExtension
) add a variable at the beginning of the function. Yeah its hacky. There's some vanilla Kotlin options you could consider though. Maybe context arguments might suit you better? You could also just have a "magical" function that the user calls:
Copy code
@SomeAnnotation
fun myFun() {
  val magic = magicFunCall()
}
and check that the call happens in the right place in a Fir checker, and replace the call in Ir to the parameter
d
I’m writing a plugin that allows you to get the fully qualified name of a type at compile time (this helps with Kotlin/JS where this name is normally not available, and in obfuscated JVM targets). I have already created a magic inline/reified function that gets replaced with the type’s name at compile time and that works great. But not if that function is called from within another inline function, so I want to transform those functions to inject the type’s name at the call site. The concept is already working within a single module/library, but to make it work across module boundaries I need to make sure the additional function argument is visible in FIR. I can’t use context arguments because they’re not available in multiplatform.
I’ll try
FirStatusTransformerExtension
and see if any errors pop up 🙂
Hang on, it’s context receivers that were only available in JVM. Are context parameters available in multiplatform now?
👌 1
y
I don't think you should do this in FIR at all then. Instead, detect the annotated inline functions, and make copies of them that take the extra argument. Then, replace every call to such a method with a call to the copy instead.
d
I still don’t think that would solve my issue though (context parameters)
I did look at that (making copies), but then I also have to copy over the function body, replacing all references to the original function parameters to those of the function copy. I’m sure this is possible, but seems rather cumbersome.
y
As in:
Copy code
inline fun <reified T> callsMagicFunOnTInternally() {
  ...
  magic<T>()
}
gets transformed in Ir into:
Copy code
inline fun <reified T> callsMagicFunOnTInternally(typeName: String) {
  ...
  typeName
}
and then a call like:
Copy code
callsMagicFunOnTInternally()
gets replaced in Ir with:
Copy code
callsMagicFunOnTInternally("some.fq.name")
you don't even need to copy the function in fact. Simply add the new parameter.
d
Also, that would still require a FIR stage, otherwise symbol resolution fails for calls made from a different klib
y
Copying functions is easy enough in IR btw, look at
deepCopyWithSymbols
. Symbol resolution won't fail if the extra parameter is only added in Ir I believe. You might wanna take inspiration from the Compose plugin, which adds an extra parameter to every
Composable
, and I believe that's done in IR in their case
d
Thanks, I’ll check that out
I couldn’t get it to work before by just adding the param in IR, but maybe I overlooked something
If I take this approach I get the following error:
Copy code
java.lang.IllegalStateException: couldn't find inline method Ldev/tebi/shared/logging/LoggerKt;.logger2(Lkotlin/reflect/KClass;)Ldev/tebi/shared/logging/Logger
In this case the
logger2(..)
function is one defined in another module, which was transformed by the plugin. My plugin also transforms the call-site of this function in the original module. But stuff goes wrong during inlining.
In the error the ‘magic’ argument is missing
The stacktrace mentions a bunch of inlining stuff
y
Do you transform the call to
logger2
in the current module?
d
Again, all this works just fine if it’s constrained to a single module
Yep I do
Copy code
org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: Couldn't inline method call: CALL 'public final fun logger2 <T> (<this>: kotlin.reflect.KClass<T of dev.tebi.shared.logging.LoggerKt.logger2>): dev.tebi.shared.logging.Logger [inline] declared in dev.tebi.shared.logging.LoggerKt' type=dev.tebi.shared.logging.Logger origin=null
Method: null
File is unknown
The root cause java.lang.IllegalStateException was thrown at: org.jetbrains.kotlin.codegen.inline.SourceCompilerForInlineKt.getMethodNode(SourceCompilerForInline.kt:129)
	at org.jetbrains.kotlin.backend.jvm.codegen.IrInlineCodegen.genInlineCall(IrInlineCodegen.kt:82)
	at org.jetbrains.kotlin.backend.jvm.codegen.IrInlineCallGenerator.genCall(IrInlineCallGenerator.kt:36)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:589)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitCall(ExpressionCodegen.kt:134)
	at org.jetbrains.kotlin.ir.expressions.IrCall.accept(IrCall.kt:24)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitVariable(ExpressionCodegen.kt:729)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitVariable(ExpressionCodegen.kt:134)
	at org.jetbrains.kotlin.ir.declarations.IrVariable.accept(IrVariable.kt:36)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitStatementContainer(ExpressionCodegen.kt:530)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:535)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.visitBlockBody(ExpressionCodegen.kt:134)
	at org.jetbrains.kotlin.ir.expressions.IrBlockBody.accept(IrBlockBody.kt:20)
	at org.jetbrains.kotlin.backend.jvm.codegen.ExpressionCodegen.generate(ExpressionCodegen.kt:232)
	at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.doGenerate(FunctionCodegen.kt:129)
	at org.jetbrains.kotlin.backend.jvm.codegen.FunctionCodegen.generate(FunctionCodegen.kt:50)
	... 44 more
Caused by: java.lang.IllegalStateException: couldn't find inline method Ldev/tebi/shared/logging/LoggerKt;.logger2(Lkotlin/reflect/KClass;)Ldev/tebi/shared/logging/Logger;
Here’s more of the stacktrace. It happens in the JVM backend, so (presumably) after IR lowering?
y
Since your plugin is for JS, have you tried testing it on JS? JVM inlining is different. You could simply not do the transformations on JVM. Either way, I'm very sure this is possible since inline composables exist
d
The plugin is not just for JS, it’s also for (obfuscated) JVM/Android.
And no, I haven’t tried it for JS yet
y
What does the IR dump look like after your plugin runs? I'm suspecting that maybe the call isn't transformed right
d
You’re right, the call is missing the additional parameter. I’m trying to figure out where it’s going wrong
y
Are you detecting the functions by using FIR predicate stuff? It can only detect annotated declarations in the current module. Instead, in IR, just go thru every call and find if it's annotated
d
No I’m not using FIR at all in this case, I use
IrElementTransformerVoid
y
Make sure your annotation is retained in the binary too.
d
It seems I shot myself in the foot with a premature optimization. I figured it would be faster to first check if the number of arguments in the function declaration is different from the number of arguments in the call. If they’re the same I do a quick exit. But the declaration actually does not contain the additional argument (at least not at this point), so this check always fails. I’ll just have to rely on checking the annotations.
👍🏼 1
Thanks for thinking along and making me retry an old solution 🙂 going to work on fixing this now
❤️ 1
Finally found the solution here: https://dev.to/shikasd/kotlin-compiler-plugins-and-binaries-on-multiplatform-139e Specifically this part “As the stubs are generated from metadata, IR functions provided from dependencies don’t have Compose-specific synthetic parameters in them, but their bytecode does! That’s why Compose compiler plugin alters stubs for such functions to make sure these parameters are also present in the calls to them.” I had to alter the function stubs as well to make it work cross-module.
🎉 1
y
Hopefully you saw this in the article too:
Just adding parameters for dependencies on the IR stage won't cut it anymore! The compilation fails during the step of creating IR in the first place while trying to deserialize a non-existent function with the original signature.
To mitigate this, Compose copies functions in Kotlin/JS instead of replacing them
So copying will be necessary for JS, exactly the opposite of what I expected
d
Sorry, I didn’t see your reply until now. I did see that, but I couldn’t find that logic anywhere in the compose compiler source code. Also I tried my implementation for Kotlin/JS and it just works. I think that this detail has changed. The post is over 4 years old and a lot has changed for Kotlin/JS. So thankfully I don’t have to do that.
K 1