Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring K2 code generation logic #80

Merged
merged 13 commits into from
Feb 14, 2025
Merged

Refactoring K2 code generation logic #80

merged 13 commits into from
Feb 14, 2025

Conversation

ForteScarlet
Copy link
Owner

@ForteScarlet ForteScarlet commented Feb 13, 2025

This PR introduces significant changes to the code generation logic under K2.

Caution

The refactoring in this PR involves significant modifications to the code generation logic under K2, and its full reliability and compatibility cannot be guaranteed at this time.
If you encounter any issues, please actively report them.

Before the Changes

Previously, the code generation flow in K2 was structured as follows:

flowchart LR
    SOURCE --> FIR["`FIR
(generate symbol)
(without body)`"]
    FIR --> IR["`IR
(generate body)`"]
Loading

Take the following code as an example:

interface Cat {
    @JvmBlocking
    @JvmAsync
    suspend fun meow()
}

The plugin would first resolve meow during the FIR phase, then generate two synthetic functions (meowBlocking and meowAsync) without bodies, which would later proceed to the IR phase.

interface Cat {
    @JvmBlocking
    @JvmAsync
    suspend fun meow()
    
    // Synthetic functions
    fun meowBlocking()   // no body
    fun meowAsync(): CompletableFuture<Unit>    // no body
}

At this stage, the plugin also needed to preserve and pass the mappings between meow -> meowBlocking and meow -> meowAsync, as the IR phase required generating body for synthetic functions (which involve invoking the original function).

However, since most FIR and IR types are incompatible, this approach caused complications and led to issues like #72 and #67.

How did we preserve these mappings before? Simply by converting function names and parameter types (as strings) into neutral types stored in PluginKey. However, this method proved unreliable in cases involving typealias and was inefficient due to redundant function traversals during the IR phase.

Intermediate Phase

To address issues with typealias, generics, and other challenges, #73 introduced a new marker annotation @TargetMarker. The idea (detailed in #73 and #75) was to establish a correspondence between synthetic and original functions using a unique ID in the annotation.

While #73 resolved #72, some unreproducible edge cases remained. As a workaround, #76 reverted to the original logic when the new approach failed.

After the Changes

While reviewing code (unsure of the source), I discovered that FIR could also generate body, which inspired #75. The initial refactor involved generating internal bridge functions during the FIR phase:

interface Cat {
    @JvmBlocking
    @JvmAsync
    suspend fun meow()

    // Bridge function
    private fun __meowBlocking_0()

    // Synthetic function
    fun meowBlocking() {
        __meowBlocking_0()
    }

    // Bridge function
    private fun __meowAsync_0(): CompletableFuture<Unit>
    
    // Synthetic function
    fun meowAsync(): CompletableFuture<Unit> {
        return __meowAsync_0()
    }
}

By combining TargetMarker with a unique ID, the IR phase could directly generate bodies for these bridge functions, eliminating unreliable mappings and redundant traversals. Later I realized FIR could generate more complex bodies (e.g., referencing external functions/types), allowing full synthetic function generation without bridge functions.

This PR now directly generates complete synthetic functions (meowBlocking and meowAsync) during FIR, removing the need for IR-phase adjustments.

interface Cat {
    @JvmBlocking
    @JvmAsync
    suspend fun meow()
    
    // Synthetic functions
    fun meowBlocking() {
        runInBlocking({ meow() })
    }
    fun meowAsync(): CompletableFuture<Unit> {
        return runInAsync({ meow() }, this as? CoroutineScope)
    }
}

Testing

Given the extensive code changes, full reliability and compatibility cannot yet be guaranteed.
In the test files, both FIR and IR outputs show significant differences, but the final ASM files remain unchanged. This suggests binary compatibility with previous outputs.

A downstream project using this plugin also shows no compatibility issues (verified via binary-compatibility-validator).

Local testing for the JS platform revealed no issues so far.

Conclusion

Theoretically, these changes should improve efficiency and reduce instability caused by unreliable type mapping mechanisms.

close #75

@ForteScarlet ForteScarlet self-assigned this Feb 13, 2025
@ForteScarlet ForteScarlet marked this pull request as ready for review February 13, 2025 17:43
@ForteScarlet ForteScarlet merged commit 9aba9bc into dev Feb 14, 2025
1 check passed
@ForteScarlet ForteScarlet deleted the optimze-k2 branch February 14, 2025 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improvement of generation logic in K2 Error generating JVM code for function with typealias argument
1 participant