Kotlin: Implement JvmOverloads annotation#9811
Conversation
| maybeParentId, | ||
| getFunctionShortName(f).nameInDB, | ||
| f.valueParameters, | ||
| maybeParameterList ?: f.valueParameters, |
There was a problem hiding this comment.
Can you add an explanation of when/why maybeParameterList is passed in to the function's comment please?
ef18a20 to
ecb473b
Compare
tamasvajk
left a comment
There was a problem hiding this comment.
I added some minor comments. The PR looks generally okay, but some tests are still failing.
Also, I think we'll have problems with introducing the new parameters with type IrValueParameterWithOverriddenIndex. They have incorrect parents, so we might generate incorrect label for them. I haven't checked it, but in case a parameter is accessed in a default argument value, extractExpressionExpr doesn't have the overloadId:
fun fn(s0: String = "", s1: String = fn(s0, ""), s2: String = "") = ""
Maybe storing the overloadId in IrValueParameterWithOverriddenIndex would help.
| import java.util.* | ||
| import kotlin.collections.ArrayList | ||
|
|
||
| fun emptyTypeParameterMap() = HashMap<IrDeclarationParent, Label<out DbClassorinterfaceorcallable>>() |
There was a problem hiding this comment.
Nitpicking: HashMap<IrDeclarationParent, Label<out DbClassorinterfaceorcallable>>() is not an emptyTypeParameterMap, but rather an emptyTypeParameterParentMap, isn't it?
d635254 to
e4f73bf
Compare
|
@tamasvajk now that I've had to use a piece of background state to cope with type variable references, I've changed the strategy for handling value parameters to use the same mechanism instead of creating a proxy class that wraps |
tamasvajk
left a comment
There was a problem hiding this comment.
Added some questions for discussion.
| } | ||
|
|
||
| private inner class DeclarationStackAdjuster(declaration: IrDeclaration): Closeable { | ||
| private inner class DeclarationStackAdjuster(val declaration: IrDeclaration, val overriddenAttributes: OverriddenFunctionAttributes? = null): Closeable { |
There was a problem hiding this comment.
@igfoo You wanted to get rid of the declaration stack completely. If that's still the plan, then we might need to look for another solution here.
There was a problem hiding this comment.
Don't worry about that for now. We can revisit this if/when that happens.
| } | ||
|
|
||
| private inner class DeclarationStackAdjuster(declaration: IrDeclaration): Closeable { | ||
| private inner class DeclarationStackAdjuster(val declaration: IrDeclaration, val overriddenAttributes: OverriddenFunctionAttributes? = null): Closeable { |
There was a problem hiding this comment.
It would probably be cleaner to have another constructor, which takes an IrFunction and an OverriddenFunctionAttributes.
| val externalClassExtractor: ExternalDeclExtractor, | ||
| val primitiveTypeMapping: PrimitiveTypeMapping, | ||
| val pluginContext: IrPluginContext, | ||
| val overriddenFunctionAttributes : HashMap<IrDeclaration, OverriddenFunctionAttributes>, |
There was a problem hiding this comment.
Couldn't this be an HashMap<IrFunction, ... ?
| (it as? IrDeclaration)?.let { decl -> | ||
| overriddenFunctionAttributes[decl]?.id | ||
| } ?: |
There was a problem hiding this comment.
Doesn't this and all other overriddenFunctionAttributes uses in KotlinUsesExtractor assume that when the access is made, we're extracting the given function with overrides? So in this case, are we sure that getTypeParameterParentLabel is only called when overriddenFunctionAttributes[decl] is being extracted?
I think, you might be right that getTypeParameterParentLabel and getValueParameterLabel are only called within forceExtractFunction and extractGeneratedOverload, but it feels somewhat fragile. Isn't this the same as (this as KotlinFileExtractor).declarationStack.peek().overriddenFunctionAttributes, which doesn't make sense at the moment, because the attributes are not on the stack, but if they were, we would consider this dangerous because of the cast.
There was a problem hiding this comment.
For value parameters: I'd expect this could be seen from inside capturing local declarations and classes too. Something like
fun f(x: Int, y: Int = x.let {
fun captures() = it
captures()
})
For type parameters: these definitely can be seen from elsewhere, principally when extracting instantiated generic types that occur in the signature of a method with overloads. So when we extract fun <T> f(x: List<T>, y: T? = null) then we need to generate List<T> with T = f/1::T as well as the case with T = f/2::T that gets created for the "normal" version of f. That's why the overridden functions map gets passed to child instances of KotlinUses/FileExtractor.
| } | ||
|
|
||
| private inner class DeclarationStackAdjuster(declaration: IrDeclaration): Closeable { | ||
| private inner class DeclarationStackAdjuster(val declaration: IrDeclaration, val overriddenAttributes: OverriddenFunctionAttributes? = null): Closeable { |
There was a problem hiding this comment.
Wouldn't it make more sense to push (declaration, overriddenAttributes) onto the stack? That way we wouldn't have to maintain two sets of states (overriddenFunctionAttributes and declarationStack)?
There was a problem hiding this comment.
I separated them because I needed the function override state to be propagated when extracting generic type instantiations as mentioned above and didn't want to get into the decl stack being non-empty on completing extraction of that type, but I can keep them together if that's preferred at the slight cost of searching the stack.
| val overriddenParentAttributes = (declarationParent as? IrDeclaration)?.let { | ||
| overriddenFunctionAttributes[it] | ||
| } | ||
| val parentId = parent ?: overriddenParentAttributes?.id ?: useDeclarationParent(declarationParent, false) |
There was a problem hiding this comment.
Would this work with recursive calls too? overriddenFunctionAttributes only has a reference to one overload at a time. Could we need multiple at any time? For example when generating the overload f(String,String) would we need access to both f(String,String) and f(String)?
object X {
@JvmStatic
@JvmOverloads
fun f(s0: String = f("", ""), s1: String = f(""), s2: String = "") = ""
}
There was a problem hiding this comment.
Answering this to myself: I think this question doesn't make sense. We're accessing parameters here, I don't think it's possible to access parameters of multiple overloads, we can at most access parameters that are declared earlier in the parameter list, and those will come from the current declaration.
There was a problem hiding this comment.
I think these are ok because the overridden attributes only affect value and type parameter refs, whereas these two are referring to the function as a callee
|
@tamasvajk I've adopted your suggestion to store the overridden attributes in the declaration stack |
tamasvajk
left a comment
There was a problem hiding this comment.
I've added some refactoring ideas, feel free to ignore them.
I think the PR is in a mergeable state, we know there are some missing cases, which will be covered in upcoming PRs. We could also apply the refactorings later.
| val globalExtensionState: KotlinExtractorGlobalState | ||
| ) { | ||
|
|
||
| class DeclarationStack { |
There was a problem hiding this comment.
I find it somewhat odd that the declaration stack is in the UsesExtractor. In my mind it belongs to the file extractor because that's where we extract the declarations.
| fun getValueParameterLabel(vp: IrValueParameter, parent: Label<out DbCallable>?): String { | ||
| val declarationParent = vp.parent | ||
| val parentId = parent ?: useDeclarationParent(declarationParent, false) | ||
| val overriddenParentAttributes = (declarationParent as? IrFunction)?.let { |
There was a problem hiding this comment.
If we kept the declaration stack in the file extractor, we could use the below:
val overriddenParentAttributes = if (this is KotlinFileExtractor && this.filePath == vp.file.path && declarationParent is IrFunction) {
this.declarationStack.findOverriddenAttributes(declarationParent)
} else {
null
}
I find this more explicit in what our expectations are: the overridden attributes should only be used if we're inside the extraction of the corresponding function declaration.
There was a problem hiding this comment.
Let me know if this is a wrong assumption.
There was a problem hiding this comment.
vp.file.path can throw, so the correct check would be vp.fileOrNull?.path. or declarationParent .fileOrNull?.path.
I think adding the check would make it explicit that we only expect to find anything by findOverriddenAttributes if the files match. It would only make the intent more explicit, but it wouldn't change the behaviour. Also, performance wise having the check or not probably doesn't matter, because both fileOrNull and findOverriddenAttributes walks a parent-child declaration list linearly.
There was a problem hiding this comment.
The same should be done in getTypeParameterParentLabel just to keep the symmetry between the two cases.
There was a problem hiding this comment.
Not done, because this is the case where we do need to look for type-parameter substitution happening up the stack, even in a different file -- for example, due to generic type specialisations transitively required from a synthetic function's parameter or return types.
There was a problem hiding this comment.
Makes sense. I always forget about this special case.
cb1289d to
5e2c607
Compare
This generates functions that omit parameters with default values, rightmost first, such that Java can achieve a similar experience to Kotlin (which represents calls internally as if the default was supplied explicitly, and/or uses a $default method that supplies the needed arguments). A complication: combining JvmOverloads with JvmStatic means that both the companion object and the surrounding class get overloads.
tamasvajk
left a comment
There was a problem hiding this comment.
Added some minor comments, otherwise it looks good to me.
| if (this is KotlinFileExtractor) | ||
| this.declarationStack | ||
| else |
There was a problem hiding this comment.
As discussed on Slack, I don't think we should reuse the declaration stack if the filePath doesn't match. I think we won't have any trouble by reusing it, at least not currently, but this means that we can end up with declaration stacks that contain declarations from multiple files, and therefore be in a state where the declarations in the stack do not contain each other in reality.
As an example: withFileOfClass is called from addClassLabel, which calls extractNonPrivateMemberPrototypes on the returned KotlinFileExtractor, and it internally calls extractFunction which does modify the declaration stack.
There was a problem hiding this comment.
Actually, "be in a state where the declarations in the stack do not contain each other in reality" is probably also true if the fileClass check holds. But in this case, at least the declarations in the stack would be from the same file.
There was a problem hiding this comment.
This actually does need to be the same stack for the case of type variables -- for example, when extracting the List<T> (and its downstream types, Iterator<T> etc) in the context of synthetic fun <A> f(a: List<A>) related to the real function fun <B> f(a: List<B>, b: Int = 0), we need to substitute B -> A everywhere, and the stack of outstanding function extractions is used to find the enclosing extraction of the relevant function declaring the type variable.
There was a problem hiding this comment.
I see, thanks for the explanation.
| fun getValueParameterLabel(vp: IrValueParameter, parent: Label<out DbCallable>?): String { | ||
| val declarationParent = vp.parent | ||
| val parentId = parent ?: useDeclarationParent(declarationParent, false) | ||
| val overriddenParentAttributes = (declarationParent as? IrFunction)?.let { |
There was a problem hiding this comment.
vp.file.path can throw, so the correct check would be vp.fileOrNull?.path. or declarationParent .fileOrNull?.path.
I think adding the check would make it explicit that we only expect to find anything by findOverriddenAttributes if the files match. It would only make the intent more explicit, but it wouldn't change the behaviour. Also, performance wise having the check or not probably doesn't matter, because both fileOrNull and findOverriddenAttributes walks a parent-child declaration list linearly.
This generates functions that omit parameters with default values, rightmost first, such that Java can achieve a similar experience to Kotlin (which represents calls internally as if the default was supplied explicitly, and/or uses a $default method that supplies the needed arguments).
A complication: combining JvmOverloads with JvmStatic means that both the companion object and the surrounding class get overloads.