Does anybody know which JVM instance IntelliJ load...
# compiler
g
Does anybody know which JVM instance IntelliJ loads the K2 compiler in for IDE analysis? I need to load a JVM Agent for a compiler plugin
d
It is loaded into the process with idea itself
g
Is it given its own class loader or does it share one with idea?
And I know that share is relative, but do you know if there is strong overlap in loaded classes?
I have an agent that adds certain equality semantics to all Kotlin Lambdas. Unfortunately some common dependencies rely on this behavior and my compiler plugin relies on the common dependencies
I'm trying to limit unintended consequences changing equality on all kotlin lambdas and Idea is a large surface which I don't want to cause issues for
d
Why do you need to apply this logic the compiler itself?
g
Long story short, I have an immutable collections library with some non primitive collections such as DefaultMap and QualifiedSet which rely on provided functions to compute things such as a default value for a key or a key qualifier for a value. These collections need to have certain equality semantics for their intended use. Two instances should be considered the same collection if they have the same values and the same functions. "Same functions" is a relative statement as there is no real way to make such a statement outside of abstract mathematics, but for my our purposes same function is two instances that share a concrete lambda declaration and capture the same values (
==
for each captured
val
(and
var
, but that's iffy)). Since I am sharing logic between my core library and my compiler plugin, the plugin is dependent on these immutable collections and how they are used
d
Got it Seems reasonable (but overcomplicated IMO)
g
I was expecting that answer
d
Is it given its own class loader or does it share one with idea?
@Roman Golyshev could you help with it?
but do you know if there is strong overlap in loaded classes
What do you mean? Most of the code from the frontend is reused, but some service and core transfomers are replaced with IDE-specific implementations
g
All of this complication is for a "declarative" rules engine
Like how forked are the classes that are used in the Kotlin compiler? For example do UI elements that are written in kotlin for Idea share the same class instances of Kotlin stdlib that are used by the Kotlin compiler
d
As far as I know there is only one kotlin stdlib in the runtime classpath
But I'm not the IDE developer, so I may be wrong
g
Okay, thank you for taking the time to get back to me 🙂
r
Hey! It seems that currently we’re using the same classloader as in the whole Kotlin plugin So altering it in any way does not seem like a good idea 😅
(actually, sorry, it’s not completely correct - I’m currently checking it again)
It seems that we actually do have a special classloader for the compiler plugins registrars. This classloader has to have a parental classloader from the Kotlin plugin, because it has to have shared classes with the compiler (and the compiler is most probably loaded through the Kotlin plugin classloader). If this condition does not hold, then the loaded compiler plugin registrars are not going to be compatible with the rest of the compiler running in the IDE I am currently under impression that the Kotlin STDLIB classes are also loaded from the parental classloader However, the classloader is currently shared between all the plugins I am not too familiar with JVM agents, unfortunately; could you please tell what exactly your JVM agent does? It changes the `equals`/`hashCode` on some Kotlin STDLIB classes like
KFunction0
?
g
Copy code
new AgentBuilder.Default()
                .type(isSubTypeOf(Class.forName("kotlin.jvm.internal.Lambda")))
                .transform((builder, type, classLoader, module, protectionDomain) ->
                        builder
                                .method(named("equals")).intercept(to(new Object(){
                                    public static boolean impl(@This Object o1, @Argument(0) Object o2) throws IllegalAccessException {
                                        if(o1 == o2) return true;
                                        if(o2 == null) return false;
                                        if(o1.getClass() != o2.getClass()) return false;
                                        var fields = o1.getClass().getDeclaredFields();
                                        if(fields.length == 0) return true;
                                        AccessibleObject.setAccessible(fields, true);
                                        for (var field: fields) if(!Objects.equals(field.get(o1), field.get(o2))) return false;
                                        return true;
                                    }
                                }.getClass()))
                                .method(named("hashCode")).intercept(to(new Object(){
                                    public static int impl(@This Object o1) throws IllegalAccessException {
                                        int result = 31 + o1.hashCode();
                                        var fields = o1.getClass().getDeclaredFields();
                                        if(fields.length == 0) return result;
                                        AccessibleObject.setAccessible(fields, true);
                                        for (var field : fields) {
                                            var element = field.get(o1);
                                            result = 31 * result + (element == null ? 0 : element.hashCode());
                                        }
                                        return result;
                                    }
                                }.getClass()))

                ).installOn(instrumentation);
Thank you for looking into the class loaders
So currently it instruments all Kotlin Lambda's and defines their value as a product of their class and the variables they capture
Ideally this is not done reflectively, but this was quick to test and it works
Copy code
assert({ "hi" } != { "hi"})

fun makeFunc(v: String) = { v }

assert(makeFunc("hi") == makeFunc("hi"))

assert(makeFunc("hi") != makeFunc("bye"))
I can do things like
Copy code
fun doSomething(a: String, b: String, c: Int){
    return memoize{ (a + b).length + c } // Imagine that this is potentially some other expensive and pure operation
}
Where all memoize has to do is look up if the function in a map to see if it has already been computed, because it will result in the same value given it captured the same values
r
At which moment is your agent attached to the running JVM? When does this happen?
g
Right now it is a premain (So at JVM start), but it could theoretically be attached later
It might not work with different class loaders due to the way the way the class is matched though, but that is an easy fix
I have not yet tried to just attach this at Idea start