Compile-Time Reflection

What consideration is there to add some compile-time reflection to eliminate the overhead of reflection at runtime?

For example, currently, Kotlin 1.4 intends to optimize out any KProperty that would be generated for delegation methods like Lazy's getValue, which doesn’t use the KPropery parameter. We could go further and replace any use of property.name with the property’s name, which should be known at compilation time, to eliminate the need for a reflection call.

We could go further and inline the getters and setters on a KProperty as well. swap(::a, ::b) could be an inline method:

private inline fun <T> swap(a: KMutableProperty0<T>, b: KMutableProperty0<T>) {
        val temp = a.get()
        a.set(b.get())
        b.set(temp)
}

as if swap actually took as arguments the two gets and sets, and inlined them together.

Finally, we could inline iterating over properties, as would be common in most serialization frameworks:

inline fun <reified T : Any> T.toJson() : String {
    return T::class.memberProperties
        .filter { it.javaField != null }
        .joinToString(prefix = "{", postfix = "}", separator = ", ") { property ->
            '"' + property.name + "\": " + when (property.returnType) {
                String::class, Character::class -> "\"" + property.get(this) + "\""
                Number::class, Boolean::class -> property.get(this)
                else -> property.get(this).toJson()
            }
        }
}

This would require a few more concepts:

  • An inlined list, that supports a few methods at compilation time so that each element is inlined into each operator (like loop unrolling) so that they can be optimized (which would be a useful feature in its own right)
  • Optimizing a when statement when the class argument is known at compilation time
  • A convenient syntax for the above when statement to use Number::class to include all subtypes (as Int::class isn’t equal to Number::class, but it shouldn’t be necessary to list every subclass)
  • Support for recursive calls to inline methods. This would probably require that each method not be actually inlined, but instead just reified to the specific type. That way, if someone were to serialize a large data structure, it wouldn’t repeat the serialization code for each type it encounters everywhere it is encountered, and it would be possible for a data structure to potentially contain itself and still be serializable.

To my knowledge, the only current way to do something like the above is to write either a compiler plugin (which isn’t nearly as portable, and is complicated) or an annotation processor (which has a high engineering overhead, it would take an order of magnitude or two more code to write an annotation processor that generates a class that does the same thing as toJson for each annotated class, and then anyone using an IDE will have error messages until they fully compile their project).

3 Likes

There is an issue for that: https://youtrack.jetbrains.com/issue/KT-16304

Like it.

While I like most of the points I don’t see how any of those can be done without inlineing. I mean sure maybe something like C++ templates can be done using the new klib format, but I’m not sure what I think of that.
That said, at least the first 2 points (list unrolling and optimized when) is probably something kotlin should add at some point.

1 Like

What I mean for fixing the recursive call is that the method toJson, instead of being inlined for every caller, would be compiled once for every type called. For example, given some data classes Person(name: String, item: Item) and Item(name: String), you would have compiled methods toJson$Person (which internally calls toJson$Item) and toJson$Item). Perhaps they would need to be called reified fun instead of inline fun to distinguish this. As only the type is being “inlined” and not any lambdas, it should be possible.

I would love to have some type of compilation reflextion.
You already know I HATE @annotions for extending the language.

I love the build in automatisms for code generation (data classes)
and code reassembling (like coroutines).

The reflection could be used for generating functions by given “string” name.
so
@sql('customerid')
val custid: integer
could be avoided to map customerid to custid and vice versa

Some ideas:
var callMe="setName"
createFunc(callMe) (name:string) { ... }
will represent the function
func setName (name:string) { ... }

The opposite Function could be
func getSomeValue() ...
nameOf(getSomeValue) : string will result in the string "getSomeValue"

Both cases could be handled during compile time
(first case if the string parameter could the evaluated)

This programmable compile time function generation
could result in a much more powerful toolchain like
the #if #define #pragma definitions in C

for (i in 1..10)
{ createVar('adr'+i):string
createFunc('setAddress'+i) (adr:string)
{ varOf("adr"+i)=adr
}
}
would create 10 functions setAddress1() to setAddress10() and 10 variables adr1 to adr10

Some type of OR-Mappers could then be implemented to use as easy as data classes.

P.S. I did write func instread of fun ! This is done intentionally.

Creating new variables and functions is significantly different from what I’m proposing. Compile-time reflection would not add new ways to write code (except for the adding the concept of a reified method to prevent infinite recursion, but that may be a useful concept independent of this suggestion). Instead, it would apply knowledge available at compilation time to greatly optimize methods that would otherwise run just fine (albeit significantly slower and using more resources). Adding function and variable generation would be much more complicated for those reading the code, as suddenly the reader (and the IDE) would need to know that setAddress10 may not be declared anywhere specifically, but it will be generated when the program is compiled. (This is also a problem that occurs frequently when using annotation processors, as there will be many compilation errors in the IDE for names that don’t mean anything until the program is fully compiled for the first time since possibly the last branch switch.)

1 Like

You already know I HATE @annotions for extending the language.

For larger changes to a structure, it is recommended to realize this over annotation.

Personally, I prefer compile time reflection (CR) over runtime reflection (RR), as (CR) could be visualized better in an IDE and is done at compile time and not a runtime.
The main reason why Java is slow is RR.
Languages with CR can build VMs providing RR if needed.

The only requirement to CR is that it must be standardized in the standard library, i.e. each compiler must support this when implementing the language.

1 Like