Slackbot
05/24/2026, 10:41 AMYoussef Shoaib [MOD]
05/24/2026, 10:45 AMid
For your end goal, though, I'm not sure how realistic it is. You can try and do some hacky stuff in FIR, but I don't think there's a "proper" way to do itYoussef Shoaib [MOD]
05/24/2026, 11:05 AMid call.
A better idea would be to have some @GenerateArgumentOverload annotation that you can annotate hof with, and simply generate an overload that takes the other function type (you might also have to add some dummy arguments unit: Unit = Unit to stop it from complaining about ambiguity and platform declaration clashes)
To get started, you can use the compiler plugin templateYoussef Shoaib [MOD]
05/24/2026, 11:12 AMid call)Youssef Shoaib [MOD]
05/24/2026, 11:21 AMYoussef Shoaib [MOD]
05/24/2026, 11:26 AMdoes all Kotlin code be syntactically valid before and after applying a plugin?Yes, so that wouldn't work...
Youssef Shoaib [MOD]
05/24/2026, 11:31 AMclass MyClass {
fun f(arg: MyArg) {
arg.build {
this@MyClass.f()
this@build.g()
}
}
}
You can even add a label to lambdas, which effectively lets you name the receiver:
class MyClass {
fun f(arg: MyArg) {
arg.build builder@{
this@MyClass.f()
this@builder.g()
}
}
}
You can also get receivers by their type:
class MyClass {
fun f(arg: MyArg) {
arg.build {
contextOf<MyClass>().f()
contextOf<BuilderType>().g()
}
}
}
Finally, you can create "default names" for receiver types you care about by using extension properties:
val MyClass.myClass get() = this
val MyBuilder.builder get() = this
class MyClass {
fun f(arg: MyArg) {
arg.build {
myClass.f()
builder.g()
}
}
}
You can also just use local variables to give names to your receivers (but they'll still remain receivers):
class MyClass {
fun f(arg: MyArg) {
val self = this
arg.build {
val builder = this
self.f()
builder.g()
}
}
}Youssef Shoaib [MOD]
05/24/2026, 11:41 AMwith(Unit) wouldn't get rid of the receiver.
I gotta think on this for a bit. I do think there's hacky ways to do this. The fundamental issue, ultimately, is that arg.build { builder, x, y -> ... } will be rejected by the compiler. FIR plugins don't really have ways to prevent that, except with hacky workarounds that could eventually stop working.Youssef Shoaib [MOD]
05/24/2026, 11:46 AMthis refers to, but it leaves the implicit receiver, so you can still do:
class MyClass {
fun f() {}
fun g() {
return with(Unit) {
f() // uses this@MyClass implicitly
}
}
}Youssef Shoaib [MOD]
05/24/2026, 11:51 AMFirSamConversionTransformerExtension can help here? It's what's used in Gradle to let you call functions that take Action with an extension lambda instead, so maybe you could do the opposite.
It might be a deadend though. It seems it only works for SAM interfaces (i.e. `fun interface`s), so I don't think it'd let you intercept regular function typesYoussef Shoaib [MOD]
05/24/2026, 11:58 AMid trick) is called FirFunctionCallRefinementExtension. Inside its intercept method, you can call callInfo.argumentList.transformArguments to change the arguments given to the call, so you can use that to add the id to the last argument if it's a lambda.Youssef Shoaib [MOD]
05/24/2026, 12:05 PMid({ ... }) calls as previously discussed
But you can also take an approach similar to mine, and instead add the val builder = this line at the start of the lambda. Hopefully my code can serve as a good starting point!
This fits your thoughts from above for transforming:
arg.build { @This builder, x, y ->
builder.g()
}
into:
arg.build { x, y ->
val builder = this
with(Unit) { builder.g() }
}
Obviously, it doesn't prevent access to this@build, and it even doesn't prevent implicit access to just g(), but it still lets you be explicit when you want