Skip to content

Conversation

@smowton
Copy link
Contributor

@smowton smowton commented Jul 21, 2022

Kotlin's implementation of defaults depends on the -Xjvm-default setting (or the @JvmDefault deprecated annotation, not implemented here): by default, actual interface class files don't use default methods, and any class that would inherit one instead implements the interface calling a static method defined on TheInterface$DefaultImpls. With
-Xjvm-default=all or =all-compatibility, real interface default methods are emitted, with the latter retaining the DefaultImpls methods so that other Kotlin can use it.

Here I adopt a hybrid solution: create a real default method implementation, but also emit a forwarding method like @override int f(int x) { return super.TheInterface.f(x); }, because the Java extractor will see MyClass.f in the emitted class file and try to dispatch directly to it. The only downside is that we emit a default interface
method body for a prototype that will appear to be abstract to the Java extractor and which it will extract as such. I work around this by tolerating the combination default abstract in QL. The alternative would be to fully mimic the DefaultImpls approach, giving 100% fidelity to kotlinc's strategy and therefore no clash with the Java
extractor's view of the world.

@smowton smowton requested review from a team as code owners July 21, 2022 08:21
@smowton smowton force-pushed the smowton/feature/interface-forwarding branch from 735b421 to b157920 Compare July 21, 2022 08:22
@smowton smowton added the no-change-note-required This PR does not need a change note label Jul 26, 2022
@smowton smowton force-pushed the smowton/feature/interface-forwarding branch 3 times, most recently from 9ae872d to 80079ac Compare October 10, 2022 15:09
@smowton
Copy link
Contributor Author

smowton commented Oct 10, 2022

@igfoo @tamasvajk this is now fixed up and ready for review

@smowton smowton force-pushed the smowton/feature/interface-forwarding branch from 80079ac to 8f58b11 Compare October 10, 2022 17:59
@igfoo
Copy link
Member

igfoo commented Oct 11, 2022

Generally looks plausible to me, but the tests are failing.

}

private fun isKotlinDefinedInterface(cls: IrClass?) =
cls != null && cls.isInterface && cls.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to cover annotation classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, because they cannot have non-Object supertypes or members, so the circumstance should never be able to arise.

Kotlin's implementation of defaults depends on the -Xjvm-default setting (or the @JvmDefault deprecated annotation, not implemented here): by default, actual interface class files don't use default method, and any class that would inherit one instead implements the interface calling a static method defined on TheInterface$DefaultImpls. With
-Xjvm-default=all or =all-compatibility, real interface default methods are emitted, with the latter retaining the DefaultImpls methods so that other Kotlin can use it.

Here I adopt a hybrid solution: create a real default method implementation, but also emit a forwarding method like `@override int f(int x) { return super.TheInterface.f(x); }`, because the Java extractor will see `MyClass.f` in the emitted class file and try to dispatch directly to it. The only downside is that we emit a default interface
method body for a prototype that will appear to be `abstract` to the Java extractor and which it will extract as such. I work around this by tolerating the combination `default abstract` in QL. The alternative would be to fully mimic the DefaultImpls approach, giving 100% fidelity to kotlinc's strategy and therefore no clash with the Java
extractor's view of the world.
…ing class and the interface are Kotlin-defined.

If the interface is Java-defined and it provides a default interface implementation then real class-file default methods are being used and kotlinc won't synthesise anything. If the loaded .class file wasn't made by Kotlin, then we see all the real methods and there is no need to synthesise anything either.
@smowton smowton force-pushed the smowton/feature/interface-forwarding branch from 68218ee to 50f99d8 Compare October 18, 2022 09:31
@smowton
Copy link
Contributor Author

smowton commented Oct 18, 2022

This should now be in a mergeable state.

| file://<external>/KTypeProjection.class:0:0:0:0 | copy$default | Forwarder for Kotlin calls that need default arguments filling in |
| file://<external>/KTypeProjection.class:0:0:0:0 | covariant | Proxy static method for a @JvmStatic-annotated function or property |
| file://<external>/KTypeProjection.class:0:0:0:0 | invariant | Proxy static method for a @JvmStatic-annotated function or property |
| file://<external>/List.class:0:0:0:0 | forEach | Forwarder for a Kotlin class inheriting an interface default method |
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this correct? List.forEach is not inherited from a default interface method. It's the Iterable<T>.forEach extension function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this is the Kotlin Iterable equivalent of https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html#forEach-java.util.function.Consumer- -- there's also the extension function defined in CollectionsKt, which takes a Function1<T, Unit> instead of a Consumer<T>. Not sure why they bothered to do that instead of just SAM converting the function to Consumer.


}

class Real : Test { }
Copy link
Contributor

Choose a reason for hiding this comment

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

What should happen if Real was an interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point -- just pushed a commit that avoids synthesising forwarders in abstract types, and makes sure the super qualifier is a direct supertype like it's supposed to be.

…per accesses correctly

Intermediate interfaces don't need interface forwarders, since the Kotlin compiler won't try to make them non-abstract by synthesising methods.

Super references should always target an immediate superclass, not the ancestor containing the intended implementation.
@smowton smowton force-pushed the smowton/feature/interface-forwarding branch from b8bafe0 to 14b8892 Compare October 19, 2022 14:37
@smowton smowton merged commit e868cdf into github:main Oct 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Java Kotlin no-change-note-required This PR does not need a change note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants