```annotation class Id fun <T> id(arg: T): T...
# compiler
m
Copy code
annotation 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
Copy code
f(42, id { x, y -> })
To this at compile time? In other words, I want to wrap every annotated lambda in a call to
id
The end goal is to pass a
(T) -> U
lambda when a function requires a
T.() -> U
argument.
Copy code
fun func(f: String.() -> Unit) {}

func @Id { arg -> /* arg: String */}
So any alternatives to
@Id
that lets me achieve this would also be fine
y
I believe so. With an IR plugin, you could likely find every annotated call and add a call to
id
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 it
m
@Youssef Shoaib [MOD] I’ve never developed a plugin, could you please tell me what language features I should learn to implement this? Some keywords I could look up would be appreciated. Also why are you not sure this is realistic?
Copy code
fun 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
Copy code
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
Copy code
class MyClass {
    fun f(arg: MyArg) {
        arg.build {
            f()
            g()
        }
    }
}
y
Ahh, I see! The compiler is split into 2 phases: frontend and backend. The frontend is where all compiler errors get detected, and it's where call resolution happens and stuff. You interact with it in plugins thru FIR (frontend intermediate representation). Then there's the backend. It takes what the frontend produced, and turns it into IR (intermediate representation), runs some transformations on it, then eventually turns it into platform specific code. So the problem is that FIR plugins are very limited in what they can do. In IR, you can do any transformation that you want! There are, however, hacks you can employ. If you look at the call refinement extension, it lets you intercept all function calls, so while not officially supported, you can technically edit the function call to add the
id
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 template
m
@Youssef Shoaib [MOD] Thank you very much for the detailed response and the pointer! I’ll make sure to check it out.
@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.
Copy code
@ThisParam import kotlinx.coroutines.*

runBlocking { scope -> /* no more `this: CoroutineScope` */ }
Either something like the above
Copy code
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.
Copy code
inline fun <T> runBlocking(context: CoroutineContext, block: suspend (CoroutineScope) -> T): T {
    return runBlocking(context, block)
}
The generated overload could look like this for example.
y
Sadly, I don't think that works... I would probably go for the hacky FIR solution then. It does exactly what you suggested at first (adding an
id
call)
m
@Youssef Shoaib [MOD] Oh, no. Exactly what part of this makes it impossible?
Copy code
@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
Copy code
inline fun item(x: X, y: Y, f: (T) -> U) {
    return path.to.item(x, y, f)
}
Such as this would have been neat.
y
Sadly, you can't annotate imports. You can annotate individual calls, but I don't think FIR plugins can collect that ahead of time and generate the right overloads
m
@Youssef Shoaib [MOD] Noted, thanks for letting me know. What about this design?
Copy code
@ThisParam fun path.to.item
To be replaced with
Copy code
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?
y
does all Kotlin code be syntactically valid before and after applying a plugin?
Yes, so that wouldn't work...
m
That’s deeply unfortunate. I guess
@Id
is the only option I have. Thank you for your advice and pointer 😞
y
Something to think about is whether there are normal Kotlin solutions to your problem. My understanding is that you don't like the ambiguity of calling functions on an implicit receiver. You can always make it explicit:
Copy code
class 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:
Copy code
class MyClass {
    fun f(arg: MyArg) {
        arg.build builder@{
            this@MyClass.f()
            this@builder.g()
        }
    }
}
You can also get receivers by their type:
Copy code
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:
Copy code
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):
Copy code
class MyClass {
    fun f(arg: MyArg) {
        val self = this
        arg.build {
            val builder = this
            self.f()
            builder.g()
        }
    }
}
m
@Youssef Shoaib [MOD] I really appreciate the excellent writeup. Is there something as concise as
Copy code
arg.build(fun(builder) {
    builder.g()
})
That I can still pass as a trailing argument outside the parentheses?
Copy code
arg.build { @This builder, x, y ->
    builder.g()
}
I wonder whether this design is possible where the lambda argument above is transformed to
Copy code
{ 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.
y
Btw,
with(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.
m
@Youssef Shoaib [MOD] Really? This doesn’t work, so I assumed it would, but on second thought after giving your alternative proposals another read, I suppose
this@build
would sidestep the shadowing. There’s no deadline, so please take your time! I’d really appreciate any ideas you share with me.
y
Oh apologies, yes it does replace what
this
refers to, but it leaves the implicit receiver, so you can still do:
Copy code
class MyClass {
    fun f() {}
    fun g() {
        return with(Unit) {
            f() // uses this@MyClass implicitly
        }
    }
}
Another breadcrumb: maybe
FirSamConversionTransformerExtension
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 types
FWIW, the call refinement extension I talked about before (that can let you do the
id
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.
Apologies that this code is a mess, but here's a somewhat similar plugin I wrote a while back. The important part is that it finds all the lambdas (but in your case, you can just focus on the last one), and adds code inside their body. For your case, you can just add the
id({ ... })
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:
Copy code
arg.build { @This builder, x, y ->
    builder.g()
}
into:
Copy code
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
m
@Youssef Shoaib [MOD] Apologies for the late reply. I’m on my phone at the moment, but some of these sound promising, and I’ll check out everything you’ve linked to. I really appreciate you going to this length to provide me with pointers, it’s exceptionally helpful and people like you make this community great!
❤️ 1