Kotlin suspend modifier changes function signature, but compiler reports overload error
Asked Answered
G

1

7

Introduction

Given two functions, foo() and foo(), the first one is standard and the second is suspendible

fun foo(x: Int): Int {
    return 2*x
}

suspend fun foo(x: Int): Int {
    return 4*x
}

The following code does not compile, because two functions with the same signature are conflicting.

conflicting overloads: public fun foo(x: Int): Int defined in file t.kt, public suspend fun foo(x: Int): Int defined in file t.kt

Suspending Functions

If my understanding of suspending function is correct, then:

  1. A Continuation parameter is added to a suspending function, used by the state machine to stop and start the suspending code
  2. The return type used under the hood for suspend-marked functions is Any (Thus Object for java)

Those two side effects in theory should be enough to alter the second foo() function signature, hence to view the suspend-marked functions differently from the first one.

Analysis

At first, I have supposed that the function signature check may be performed before actually compiling the code into bytecode. However, having the two presented functions turned into actual bytecode actually results in 2 methods having 2 different signatures.

Java: t.kt -> t.decompiled.java

@Metadata(
    mv = {1, 1, 16},
    bv = {1, 0, 3},
    k = 2,
    d1 = {"\u0000\n\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\u001a\u000e\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0001\u001a\u0019\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0001H\u0086@ø\u0001\u0000¢\u0006\u0002\u0010\u0003\u0082\u0002\u0004\n\u0002\b\u0019¨\u0006\u0004"},
    d2 = {"foo", "", "x", "(ILkotlin/coroutines/Continuation;)Ljava/lang/Object;", "app"}
)

public final class TKt {
    public static final int foo(int x) {
        return 2 * x;
    }

    @Nullable
    public static final Object foo(int x, @NotNull Continuation $completion) {
       return Boxing.boxInt(4 * x);
    }
}

KB: fun foo(x: Int): Int

// access flags 0x19
public final static foo(I)I
   // annotable parameter count: 1 (visible)
   // annotable parameter count: 1 (invisible)
   L0
    LINENUMBER 4 L0
    ICONST_2
    ILOAD 0
    IMUL
    IRETURN
   L1
    LOCALVARIABLE x I L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

KB: suspend fun foo(x: Int): Int

  // access flags 0x19
  // signature (ILkotlin/coroutines/Continuation<-Ljava/lang/Integer;>;)Ljava/lang/Object;
  // declaration:  foo(int, kotlin.coroutines.Continuation<? super java.lang.Integer>)
  public final static foo(ILkotlin/coroutines/Continuation;)Ljava/lang/Object; @Lorg/jetbrains/annotations/Nullable;() // invisible
   // annotable parameter count: 2 (visible)
   // annotable parameter count: 2 (invisible)
   @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1
   L0
    LINENUMBER 8 L0
    ICONST_4
    ILOAD 0
    IMUL
    INVOKESTATIC kotlin/coroutines/jvm/internal/Boxing.boxInt (I)Ljava/lang/Integer;
    ARETURN
   L1
    LOCALVARIABLE x I L0 L1 0
    LOCALVARIABLE $completion Lkotlin/coroutines/Continuation; L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2

Scoped Calls

At this point I thought that it is not always possible for kotlin to decide which function to call. Sure, these two functions are completely different and separate, their signature has not even a partial match (Different return type and arguments)

The point is that in the kotlin word, the suspending function can only be called from inside a coroutine scope, but the normal function can be called from both places. The following table can serve as a great example to graphically analyse the situation.

+---------------+---------------+-----------------+
|               | Default Scope | Coroutine Scope |
+---------------+---------------+-----------------+
| foo()         | ✓             | ✓               |
+---------------+---------------+-----------------+
| suspend foo() | ✘             | ✓               |
+---------------+---------------+-----------------+

The only scenario that may involve a definition collision between these two entities is the following.

fun foo(x: Int): Int {
    return 2*x
}

suspend fun foo(x: Int): Int {
    return 4*x
}

GlobalScope.launch {
    println(foo(7))
}

In this case, without an hypothetical (a.k.a. Existing only in my head) operator letting Kotlin know which function to invoke, if the suspendible one or the standard one, you can't be sure about which function you are invoking.

Is this analysis correct or am i missing something in between?

Conclusion

This question will be linked in a YouTrack issue with a similar content, and this may be the starting point for a compiler improvement (Maybe differentiating overload errors from suspendible clashing with standard function error), or for a new Kotlin feature, expanding the suspendible functions interoperability with normal functions (I'm imagining a sort of spread-like operator which is prefixed to the function call, and the presence of the operator differentiates one call from another).

Galvanotropism answered 3/1, 2020 at 13:9 Comment(1)
Kotlin is not Java, it just happens to be compiled a certain way for the JVM. It's an implementation detail and not language semantics.Sarge
M
8

You are right in regards of bytecode - the signatures are different.

However it is unable to determine function unambiguously from Kotlin language side. For example, what method should be called below?

fun main() {
    runBlocking {
        println(foo(1)) // which one should be called here?
    }
}

fun foo(x: Int): Int {
    return 2*x
}

suspend fun foo(x: Int): Int {
    return 4*x
}

The same behavior is with Java Synthetic methods. Please check this answer - you can define two methods in bytecode, however it isn't allowed in Java Language syntax.

Mabel answered 3/1, 2020 at 13:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.