Skip to content

Support override return type generic #102

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

Draft
wants to merge 10 commits into
base: dev
Choose a base branch
from

Conversation

ForteScarlet
Copy link
Owner

@ForteScarlet ForteScarlet commented Jul 8, 2025

  /**
    * Indicates whether there is a generic type used to override the function return type
    * on the current mark annotation.
    *
    * If `true`, when determining the return type of the generated function,
    * the content of this generic type will be directly regarded as the return type of the origin function,
    * rather than the actual type of the origin function.
    *
    * For example, Normally, the return type of the generated function
    * depends on the return type of the origin function:
    *
    * ```Kotlin
    * @JvmBlocking
    * suspend fun run(): Result<String>
    *
    * // Generated
    *
    * @Api4J
    * fun runBlocking(): Result<String>
    * ```
    *
    * However, if the annotation has a generic type and `hasReturnTypeOverrideGeneric` is true:
    *
    * ```Kotlin
    * @JvmBlockingWithType<String?>
    * suspend fun run(): Result<String>
    *
    * // Generated
    *
    * @Api4J
    * fun runBlocking(): String?
    * ```
    *
    * As can be seen, the generated function ignores the actual return type of the origin function
    * and instead treats the generic type specified in the annotation as the return type
    * of the origin function.
    *
    * Note: If you want to determine the return type through type overloading,
    * you must ensure that the transformer function you use correctly matches the input and output parameter types.
    * Otherwise, it may result in compilation errors or runtime exceptions.
    *
    * @since 0.14.0
    */
   @ExperimentalReturnTypeOverrideGenericApi
   val hasReturnTypeOverrideGeneric: Property<Boolean>

See also: #99

…rt for generic-based return type overrides, and update related configurations and annotations.
@ForteScarlet ForteScarlet self-assigned this Jul 8, 2025
@ForteScarlet ForteScarlet added the enhancement New feature or request label Jul 8, 2025
@ForteScarlet ForteScarlet changed the title try-support-annotation-generics Support override return type generic Jul 8, 2025
@ForteScarlet ForteScarlet added this to the 0.14.0 milestone Jul 8, 2025
…urations, and introduce `test-runner` module with generic return type support.
…t usages across the plugin, and improve transformer initialization with type parameter caching.
…etter organization and maintainability within the FIR transformer module.
But then a second problem arose: it seemed that there was a problem with using inline value classes when using generics.
@ForteScarlet ForteScarlet marked this pull request as ready for review July 14, 2025 09:46
@ForteScarlet ForteScarlet requested a review from Copilot July 14, 2025 10:02
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for overriding the generated function's return type via a generic parameter on the mark annotation. Key changes include

  • Introducing hasReturnTypeOverrideGeneric throughout the plugin configuration, DSL, and codegen
  • Enhancing the FIR transformer and runtime helpers to respect the annotation’s generic when determining return types
  • Adding a Res<T> wrapper, test-runner helpers, and extensive JVM/JS tests to validate the new behavior

Reviewed Changes

Copilot reviewed 49 out of 51 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
plugins/suspend-transform-plugin-gradle/.../MarkAnnotationSpec.kt Added hasReturnTypeOverrideGeneric property to the annotation DSL
compiler/.../SuspendTransformFirTransformer.kt Updated FIR transformer to apply generic return-type overrides
runtime/suspend-transform-runtime/src/jvmMain/.../RunInSuspendJvm.kt Clarified scope comments in the async bridge runtime
tests/test-runner/src/commonMain/kotlin/.../Res.kt Introduced Res<T> data class for testing overridden return types
tests/test-runner/src/jvmMain/kotlin/.../JvmRunner.kt Added helper functions including jvmResultToAsync and jvmResToBlock
tests/test-jvm/src/.../ReturnTypeOverrideTests.kt Extended JVM tests for return override scenarios
tests/test-js/src/.../JsReturnTypeOverrideTests.kt Extended JS tests for return override scenarios
.run/PublishAllToLocal.run.xml Updated IDE run config key to IS_LOCAL
Comments suppressed due to low confidence (2)

tests/test-runner/src/jvmMain/kotlin/love/forte/suspendtrans/test/runner/JvmRunner.kt:23

  • [nitpick] The function name jvmResToBlock abbreviates “Result” inconsistently compared to jvmResultToAsync and jvmResultToBlock. Consider renaming to jvmResultToBlock for consistency.
fun <T> jvmResToBlock(block: suspend () -> Res<T>): T {

.run/PublishAllToLocal.run.xml:6

  • [nitpick] Committing IDE-specific run configurations can clutter the repository. Consider adding the .run/ directory to .gitignore and removing this file from version control.
          <entry key="IS_LOCAL" value="true" />

Comment on lines +1 to +6
// import org.gradle.api.Action
// import org.gradle.api.Project
// import org.gradle.plugins.signing.SigningExtension
//
// internal fun Project.`signing`(configure: Action<SigningExtension>): Unit =
// (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("signing", configure)
Copy link
Preview

Copilot AI Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This file contains large sections of commented-out code. Consider removing unused comments or refactoring to keep the codebase clean and maintainable.

Suggested change
// import org.gradle.api.Action
// import org.gradle.api.Project
// import org.gradle.plugins.signing.SigningExtension
//
// internal fun Project.`signing`(configure: Action<SigningExtension>): Unit =
// (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("signing", configure)

Copilot uses AI. Check for mistakes.

@ForteScarlet ForteScarlet marked this pull request as draft July 15, 2025 11:43
@ForteScarlet
Copy link
Owner Author

I encountered an issue. Previously, I found that when overriding override fun FirDeclarationPredicateRegistrar.registerPredicates() in SuspendTransformFirTransformer to provide a Predicate based on transformer.markAnnotation, if the annotation involves a generic type T, like this:

// The annotation:
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Retention(AnnotationRetention.BINARY)
expect annotation class JvmResultBlock<T>(
    val baseName: String = "",
    val suffix: String = "Blocking",
    val asProperty: Boolean = false
)

// The code:
interface Foo {
    @JvmResultBlock<T>
    suspend fun <T> foo(value: T): Result<T>
}

The compilation fails with this error:

e: file:///suspend-transform-kotlin-compile-plugin/tests/test-js/src/commonMain/kotlin/returntypeoverride/ReturnTypeOverrides.kt:21:22 Unresolved reference 'T'.
e: file:///suspend-transform-kotlin-compile-plugin/tests/test-js/src/commonMain/kotlin/returntypeoverride/ReturnTypeOverrides.kt:25:22 Unresolved reference 'T'.
Finished executing kotlin compiler using DAEMON strategy

Specifically: Unresolved reference 'T'.

This issue only occurs with real generics. If an explicit type is used instead, there’s no problem:

// The code:
interface Foo {
    @JvmResultBlock<String>
    suspend fun <T> foo(value: T): Result<T>
}

However, if I remove override fun FirDeclarationPredicateRegistrar.registerPredicates() or make it register predicates that exclude generic annotations, compilation succeeds and achieves the desired behavior in this project’s test module.

The problem lies in "this project’s test module". When I test with an external project, the code generation fails. For example, given this source:

@ST
public interface SendSupport {
    public suspend fun send(text: String): MessageReceipt
    public suspend fun send(message: Message): MessageReceipt
    public suspend fun send(messageContent: MessageContent): MessageReceipt
}

Viewing the generated SendSupport.class directly in the IDE shows:

@love.forte.simbot.suspendrunner.SuspendTrans public interface SendSupport {
    public abstract suspend fun send(text: kotlin.String): love.forte.simbot.message.MessageReceipt

    public abstract suspend fun send(message: love.forte.simbot.message.Message): love.forte.simbot.message.MessageReceipt

    public abstract suspend fun send(messageContent: love.forte.simbot.message.MessageContent): love.forte.simbot.message.MessageReceipt

    public open fun sendAsync(message: love.forte.simbot.message.Message): java.util.concurrent.CompletableFuture<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendAsync(messageContent: love.forte.simbot.message.MessageContent): java.util.concurrent.CompletableFuture<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendAsync(text: kotlin.String): java.util.concurrent.CompletableFuture<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendBlocking(message: love.forte.simbot.message.Message): love.forte.simbot.message.MessageReceipt { /* compiled code */ }

    public open fun sendBlocking(messageContent: love.forte.simbot.message.MessageContent): love.forte.simbot.message.MessageReceipt { /* compiled code */ }

    public open fun sendBlocking(text: kotlin.String): love.forte.simbot.message.MessageReceipt { /* compiled code */ }

    public open fun sendReserve(message: love.forte.simbot.message.Message): love.forte.simbot.suspendrunner.reserve.SuspendReserve<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendReserve(messageContent: love.forte.simbot.message.MessageContent): love.forte.simbot.suspendrunner.reserve.SuspendReserve<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }

    public open fun sendReserve(text: kotlin.String): love.forte.simbot.suspendrunner.reserve.SuspendReserve<out love.forte.simbot.message.MessageReceipt> { /* compiled code */ }
}

Everything appears normal. But decompiling it via IDEA reveals the problem:

@Metadata(
   mv = {2, 2, 0},
   k = 1,
   xi = 48,
   d1 = {"\u00006\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\bg\u0018\u00002\u00020\u0001J\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H¦@¢\u0006\u0002\u0010\u0006J\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0007\u001a\u00020\bH¦@¢\u0006\u0002\u0010\tJ\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\n\u001a\u00020\u000bH¦@¢\u0006\u0002\u0010\cJ\u0018\u0010\r\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u000e2\u0006\u0010\u0007\u001a\u00020\bH\u0017J\u0018\u0010\r\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u000e2\u0006\u0010\n\u001a\u00020\u000bH\u0017J\u0018\u0010\r\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u000e2\u0006\u0010\u0004\u001a\u00020\u0005H\u0017J\u0010\u0010\u000f\u001a\u00020\u00032\u0006\u0010\u0007\u001a\u00020\bH\u0017J\u0010\u0010\u000f\u001a\u00020\u00032\u0006\u0010\n\u001a\u00020\u000bH\u0017J\u0010\u0010\u000f\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H\u0017J\u0018\u0010\u0010\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u00112\u0006\u0010\u0007\u001a\u00020\bH\u0017J\u0018\u0010\u0010\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u00112\u0006\u0010\n\u001a\u00020\u000bH\u0017J\u0018\u0010\u0010\u001a\n\u0012\u0006\b\u0001\u0012\u00020\u00030\u00112\u0006\u0010\u0004\u001a\u00020\u0005H\u0017ø\u0001\u0000\u0082\u0002\u0006\n\u0004\b!0\u0001¨\u0006\u0012À\u0006\u0001"},
   d2 = {"Llove/forte/simbot/ability/SendSupport;", "", "send", "Llove/forte/simbot/message/MessageReceipt;", "text", "", "(Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "message", "Llove/forte/simbot/message/Message;", "(Llove/forte/simbot/message/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "messageContent", "Llove/forte/simbot/message/MessageContent;", "(Llove/forte/simbot/message/MessageContent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "sendAsync", "Ljava/util/concurrent/CompletableFuture;", "sendBlocking", "sendReserve", "Llove/forte/simbot/suspendrunner/reserve/SuspendReserve;", "simbot-api"}
)
@SuspendTrans
public interface SendSupport {
   // $FF: synthetic method
   Object send(String var1, Continuation var2);

   // $FF: synthetic method
   Object send(Message var1, Continuation var2);

   // $FF: synthetic method
   Object send(MessageContent var1, Continuation var2);
}

As seen, no expected synthetic functions are actually generated in the class—they only appear in @Metadata (visible via IDE). Restoring override fun FirDeclarationPredicateRegistrar.registerPredicates() fixes this external project issue, but reintroduces the Unresolved reference 'T' error.

@ForteScarlet
Copy link
Owner Author

see: KT-79267

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant