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

Implement sealed inline classes feature #4903

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

ilmirus
Copy link
Member

@ilmirus ilmirus commented Jul 20, 2022

@ilmirus ilmirus requested a review from udalov July 20, 2022 11:29
@ilmirus ilmirus force-pushed the rra/ilmir/sealed-inline-classes branch from 91bc4fb to 4a87c9a Compare July 22, 2022 10:41
Copy link
Contributor

@sfs sfs left a comment

Choose a reason for hiding this comment

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

I'm still going through the commits, but here's a first batch of comments.

+irDelegatingConstructorCall(context.irBuiltIns.anyClass.owner.constructors.single())
+irDelegatingConstructorCall(valueClass.superTypes.single {
it.classOrNull?.owner?.kind == ClassKind.CLASS
}.classOrNull!!.constructors.single().owner)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm confused. This code (and the example in the test) seems to imply that we generate wrapper classes for inline subclasses of sealed inline classes, while the specification explicitly states that these classes are not generated. Why are they needed and when are they used?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. Sorry. The problem here is that design has changed middle-way and first commits became obsolete. To avoid the confusion, I should have squashed the commits. I did it now and split it into several relevant parts.

val sealedKeyword = declaration.modifierList?.getModifier(KtTokens.SEALED_KEYWORD) ?: declaration
trace.report(
Errors.INLINE_CLASS_UNDERLYING_VALUE_IS_SUBCLASS_OF_ANOTHER_UNDERLYING_VALUE.on(sealedKeyword)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

At least with the design in the KEEP this check seems superfluous, since intermediate sealed classes would never actually be materialized.

Copy link
Member Author

Choose a reason for hiding this comment

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

Plans here are restrict the check even further and disallow non-final types as underlying types of inline subclasses of sealed inline classes, due to Kotlin/KEEP#318 (comment)

@sfs
Copy link
Contributor

sfs commented Aug 9, 2022

There's two more comments that I couldn't add because they were from earlier commits:


I don't think the code for type parameters in coerceInlineClasses is entirely correct. The unsafeCoerce intrinsic looks like this:

fun <A, B> unsafeCoerce(x: A): B

while the generated call is

unsafeCoerce<From, UnderlyingType>(argument as UnderlyingType)

I think it's probably for the best to avoid folding any additional casts into this intrinsic in any case, since it really should only be used for boxing/unboxing inline classes. Casts from/to upper bounds can always be added around it.


The workaround for nullable receivers in JvmInlineClassLowering.visitGetField won't work for nullable type parameters, e.g., if we have

value class A(val x: String)

and now, e.g., the debugger is asked to evaluate an expression a?.x_field in a context where a : T and T : A? then the code will fail to produce a non-nullable receiver, since makeNotNull doesn't handle type parameters. It's probably best to add an explicit cast to the inline class type instead.

In fact, this should probably be done when we generate code for checked calls in general, since an IrGetField (IrCall) needs a non-nullable (dispatch) receiver.

Copy link
Member

@udalov udalov left a comment

Choose a reason for hiding this comment

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

I've only reviewed ~2/3 of the changes. Will continue next week.

*
* First, we check for noinline children, then for inline children and finally, just run the top's method body.
*/
private fun rewriteOpenAndAbstractMethodsForSealed(info: SealedInlineClassInfo) {
Copy link
Member

Choose a reason for hiding this comment

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

It would be amazing to split this function into several logically independent functions to simplify reading/navigation

Copy link
Member Author

Choose a reason for hiding this comment

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

Split it a bit.

get() = this.isValue && valueClassRepresentation is InlineClassRepresentation

val IrClass.isInline: Boolean
get() = (isSingleFieldValueClass && annotations.hasAnnotation(JVM_INLINE_ANNOTATION_FQ_NAME)) || (isValue && modality == Modality.SEALED)
Copy link
Member

Choose a reason for hiding this comment

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

It almost seems like it would be a good idea to add another subclass of ValueClassRepresentation, SealedInlineClassRepresentation, for example to keep the invariant isValue == (valueClassRepresentation != null)

Copy link
Member Author

Choose a reason for hiding this comment

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

Introduced SealedInlineClassRepresentation, and used it everywhere.

+--------------------------+-------------+-------------+-------------+-------------+-------------+
|inline class |nope |nope |nope |nope |nope |
+--------------------------+-------------+-------------+-------------+-------------+-------------+
|sealed interface |yep |OOS |yep |yep |nope |
Copy link
Member

Choose a reason for hiding this comment

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

Not really clear what "OOS" means :)

Copy link
Member

Choose a reason for hiding this comment

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

Probably "out of scope"

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep

generateAdditionalMembersForMultiFieldValueClasses(irClass, ktClassOrObject)
}

if (DescriptorUtils.isEnumClass(classDescriptor)) {
generateAdditionalMembersForEnumClass(irClass)
}

if (classDescriptor.isInline && !classDescriptor.annotations.hasAnnotation(JVM_INLINE_ANNOTATION_FQ_NAME)) {
Copy link
Member

Choose a reason for hiding this comment

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

It feels weird that we add an annotation which is not there on the declaration site, to supposedly fix the lack of information of whether the class was value/inline in IR... or something? And because of that, a lot of tests depend on stdlib now. Can we invent some other approach, like adding some new information to IR nodes? Maybe you can explain the problem?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, it is by design - the annotation is used by libraries, which use reflection, like mockito, to determine, whether a class is inline of not and to pass underlying value instead of boxed one. So, we have to generate the annotation.

@ilmirus ilmirus force-pushed the rra/ilmir/sealed-inline-classes branch from 4a87c9a to 122be41 Compare October 20, 2022 01:53
@ilmirus
Copy link
Member Author

ilmirus commented Oct 20, 2022

@sfs, @udalov Thanks a lot for your comments. I reorganized the code, so it is hopefully easier to review, since there were a lot of obsolete code, due to design change midway. Sorry I kept you waiting - I was waiting for value classes, which required some refactoring, so, I would have to adopt the changes anyway.

They are always mapped to `Any?`, so, there is no need to serialize
inline class representation for sealed inline classes.
 #KT-27576
`inline` modifier during PSI2IR instead of during lowering.
This is to distinguish inline classes and value classes with one
property, but which are not inline.
 #KT-27576
TODO: generate method for is checks.
 #KT-27576
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants