min
05/24/2026, 10:41 AMannotation class Id
fun <T> id(arg: T): T { return arg }
f(42) @Id { x, y -> }
Is it possible to write a plugin that transforms the above
f(42, id { x, y -> })
To this at compile time? In other words, I want to wrap every annotated lambda in a call to idmin
05/24/2026, 10:44 AM(T) -> U lambda when a function requires a T.() -> U argument.
fun func(f: String.() -> Unit) {}
func @Id { arg -> /* arg: String */}
So any alternatives to @Id that lets me achieve this would also be fineYoussef 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 itmin
05/24/2026, 10:51 AMfun hof(f: String.() -> Unit) {}
fun <T> id(arg: T): T { return arg }
hof { arg -> }
hof(id { arg -> })
In version 2.4, the first call to hof is a compiler error but the second works, which is why I want to wrap every trailing lambda argument in a call to id. Do you have a better idea? Now that I’ve thought about it, I’d actually prefer the following
annotation class This
class MyClass {
fun f(@This self: MyClass, arg: MyArg) {
arg.build { @This builder ->
self.f()
builder.g()
}
}
}
The above is much better than
class MyClass {
fun f(arg: MyArg) {
arg.build {
f()
g()
}
}
}Youssef 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 templatemin
05/24/2026, 11:10 AM@GenerateArgumentOverload seems much simpler to implement, but before I commit to going down that path, I’d like to be sure that it’ll be possible to apply that to third party declarations also, just so I don’t end up wasting months on something that’ll turn out to be a dead end.
@ThisParam import kotlinx.coroutines.*
runBlocking { scope -> /* no more `this: CoroutineScope` */ }
Either something like the above
import kotlinx.coroutines.*
@ThisParam runBlocking { scope -> }
Or the above should be possible to generate an overload in the current file/package/module on the fly.
inline fun <T> runBlocking(context: CoroutineContext, block: suspend (CoroutineScope) -> T): T {
return runBlocking(context, block)
}
The generated overload could look like this for example.Youssef Shoaib [MOD]
05/24/2026, 11:12 AMid call)min
05/24/2026, 11:19 AM@ThisParam import path.to.item // : (X, Y, T.() -> U) -> Unit
An annotation to apply to an import statement that
1. Replaces it with a new local declaration that has the same signature,
2. And changes the function argument types to remove receiver types
inline fun item(x: X, y: Y, f: (T) -> U) {
return path.to.item(x, y, f)
}
Such as this would have been neat.Youssef Shoaib [MOD]
05/24/2026, 11:21 AMmin
05/24/2026, 11:25 AM@ThisParam fun path.to.item
To be replaced with
inline fun item(x: X, y: Y, f: (T) -> U) {
return path.to.item(x, y, f)
}
Sorry for another followup, I want to know if there are any alternatives to the @Id design before I commit to learning the necessary knowledge to implement that. More generally, does all Kotlin code have to be syntactically valid before and after applying a plugin? Or does anything go for as long as I have something valid after running the plugin?Youssef 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...
min
05/24/2026, 11:28 AM@Id is the only option I have. Thank you for your advice and pointer 😞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()
}
}
}min
05/24/2026, 11:38 AMarg.build(fun(builder) {
builder.g()
})
That I can still pass as a trailing argument outside the parentheses?
arg.build { @This builder, x, y ->
builder.g()
}
I wonder whether this design is possible where the lambda argument above is transformed to
{ x, y ->
val builder = this
with(Unit) {
builder.g()
}
}
Or, as you’ve pointed out, there’s an existing Kotlin feature that obviates the need for this plugin. I find that
• The @ labels are rather verbose,
• The default names and contexts don’t make good contextual semantic names, so to speak,
• And declaring a local variable to alias this is what I’ve been doing, but it’s rather tedious.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.min
05/24/2026, 11:44 AMthis@build would sidestep the shadowing. There’s no deadline, so please take your time! I’d really appreciate any ideas you share with me.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 wantmin
05/24/2026, 12:18 PM