How Poko works

December 1, 2021

The Kotlin compiler’s job is to convert source code into byte code. Compiler plugins like Poko are able to intercept a representation of the source code during compilation and affect the produced byte code. To this end, Poko’s purpose is fairly straightforward: for any source class with the @Poko annotation, produce byte code that uses that class’s primary constructor properties in three overridden functions.

To complicate things, Kotlin added a new compiler backend in 1.4, and made that new backend the default in 1.5. So depending on the mode in which the compiler runs, the byte code output is completely different: either classic JVM byte code or IR byte code. Poko supports both, effectively meaning this plugin’s core functions must be written twice.

When the Kotlin compiler starts executing, it loads ComponentRegistrar services, which is how plugins can register themselves. PokoComponentRegistrar registers both the non-IR PokoCodegenExtension and the IR PokoIrGenerationExtension here. Depending on the consuming project’s compiler settings, only one of these will actually be used by the compiler.

IR function generation

PokoIrGenerationExtension executes for every Poko consumer who compiles with useIR = true, which is possible from Kotlin 1.4 and is the default in Kotlin 1.5. Poko first added support for this in 0.5.0, which supports Kotlin 1.4.20.

For every function that gets compiled, the extension checks if its owner class has the @Poko annotation and meets the requirements for Poko classes (has a primary constructor with at least one member property, etc.). If so, and if the function is one of the standard toString, equals, and hashCode functions, the plugin determines the class’s primary constructor properties and uses those properties to override that function.

This approach fundamentally depends on every class already having these three functions by default—it only intercepts the byte code generation after the compiler is already creating each function. This is possible because all JVM classes include each of these three functions, implicitly inherited from the Object base class. Code inside PokoMembersTransformer , like generateToStringMethodBody, is used to generate each function body by calling Kotlin compiler APIs that represent IR byte code constructs.

Non-IR function generation

PokoCodegenExtension executes for every Poko consumer who compiles with useIR = false, which is the default for Kotlin 1.4 and can still be set in Kotlin 1.5 and 1.6.

For every class that gets compiled, the extension checks if that class has the @Poko annotation and meets the requirements for Poko classes. If so, it determines the class’s primary constructor properties and uses those properties to override the toString, equals, and hashCode functions in that class.

Since every Java class has a default implementation for each of these functions, Poko finds the existing function declaration and mutates it with the new byte code. Internal FunctionGenerator classes, such as ToStringGenerator , are used to generate each function body by calling Kotlin compiler APIs that represent Java byte code constructs.


If you want to learn more about how Poko works, your best bet is to browse the source code and the tests . If you have questions, comments, or ideas, feel free to bring them to the discussion board !

Poko goes multiplatform

Flexible configuration with Deferred Resources