-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Two fixes for overload resolution for PartialFunctions #5975
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
Conversation
The extension of shape-based overload resolutions for pattern-matching anonymous functions in scala#5698 did not work correctly in all cases: - Type unification for shapes needs to treat `PartialFunction` types the same as `FunctionN` and SAM types. They were considered for unification but the parameter type was not extracted correctly. - When the matched argument types are constructed after a successful shape match, a pattern-matching anonymous function has to be typed as `PartialFunction` instead of `FunctionN`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
if (argPtAlts.nonEmpty && treeInfo.isFunctionMissingParamType(tree)) functionProto(argPtAlts) | ||
if (argPtAlts.isEmpty) WildcardType | ||
else if (treeInfo.isFunctionMissingParamType(tree)) functionProto(argPtAlts) | ||
else if (treeInfo.isPartialFunctionMissingParamType(tree)) partialFunctionProto(argPtAlts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A difference is that now the compiler will generate a PartialFunction even if the selected alternative has a Function parameter type
object T {
def m(f: Object => Int, s: String): Int = 1
def m(f: String => Int, s: Object): Int = 1
}
T.m({ case x => 2 }, null)
generates
T.m(({
@SerialVersionUID(value = 0) final <synthetic> class $anonfun extends scala.runtime.AbstractPartialFunction[Object,Int] with Serializable {
def <init>(): <$anon: Object => Int> = {
$anonfun.super.<init>();
()
};
final override def applyOrElse[A1 <: Object, B1 >: Int](x1: A1, default: A1 => B1): B1 = ((x1.asInstanceOf[Object]: Object): Object @unchecked) match {
case (x @ _) => 2
case (defaultCase$ @ _) => default.apply(x1)
};
final def isDefinedAt(x1: Object): Boolean = ((x1.asInstanceOf[Object]: Object): Object @unchecked) match {
case (x @ _) => true
case (defaultCase$ @ _) => false
}
};
new $anonfun()
}: PartialFunction[Object,Int]), null);
In 2.13.0-M1
T.m(((x0$1: Object) => x0$1 match {
case (x @ _) => 2
}), null);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first impulse was to write "this works as intended and specified in #5698" but the spec change doesn't actually mandate it and it would be more efficient to create a Function1
in cases where that's sufficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a non-trivial problem. Perhaps extract the argument types from fun
after inferMethodAlternative
(is there a simple way to do that?) and then rebuild args1
with Function1
types in place of PartialFunction
where it is sufficient?
def collectF[K2 : Ordering, V2](pf: Function1[(String, Int), (K2, V2)]): Int = 0 | ||
def foo(xs: SortedMap) = xs.collect { case (k, v) => 1 } | ||
def fooF(xs: SortedMap) = xs.collectF { case (k, v) => 1 } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a (few) test(s) where the overloads mix Function and PartialFunction parameter types (and also assert the expected method is selected).
I've added the changes necessary to type Match blocks as Function1 again after overload resolution unless the inferred alternative actually requires a PartialFunction. |
Hmm, that last commit adds quite a bit of complexity. How about using a PF expected type if some of the overloads takes a PF? Then we could still end up with a PF passed as F, but only when overloads mix F and PF parameter types. What do you think? |
Most of the complexity comes from retyping in the case of mixed overloads. This is the whole
This is already done (in the lines following https://github.com/scala/scala/pull/5975/files#diff-4eab1aad4533a31c10565971e90f73eaR3382) to avoid copying the tree in the non-mixed cases. If we remove the |
How do we move this forward? Should I cut it back to a simpler impl that doesn't optimize the mixed overload case? I'd like to get this into M2 because we're running into these bugs in collections. |
case TypeRef(_, _, tpes) => tpes.init | ||
case _ => Nil | ||
} | ||
val selectedParams = paramTypes(normalize(fun.tpe)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's surprising me a bit that fun.tpe
can be ExistentialType
or TypeRef
, what are examples? How about using fun.symbol.tpe
instead, would that always be a MethodType
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expected MethodType
but TypeRef
was the most common case. MethodType
and ExistentialType
came up in some partest cases but I didn't check for more details. I'll give fun.symbol.tpe
a try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClassArgsTypeRef
shows up in the last two test cases (passing a case literal to a method with mixed overloads) in run/InferOverloadedPartialFunction.scala
, both for fun.tpe
and fun.symbol.tpe
:
assert(ys.collect { case (k, v) => 1 } == 0)
assert(ys.collect { case (k, v) => (1, 1) } == 2)
I wasn't able to reproduce the ExistentialType
anymore with partest pos neg run
. Maybe it would show up in a full test run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the source of the non-method types is the call to normalize
- why is it there? Otherwise you should get a PolyType
or MethodType
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, normalize
converts the method type to a function type. Just cargo-cult programming (copied from some place that did something similar). I've pushed a change that doesn't normalize anymore, now I get the expected MethodType
or PolyType
.
} | ||
map3(args, altArgPts, keepTypes) { | ||
case (_, _, Some(keep)) => keep | ||
case (arg, argPtAlts, None) => typedSingleArg(arg, argPtAlts, amode, matchAsPartial = false)._1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@retronym wdyt about throwing away a type checked tree and typing it again? Are you aware of other places where we do this? Any potential problems that come to mind with side-effects of type checking, for examples related to definitions within that tree? I cannot think of any from the top of my head.
The alternative would be to generate a PartialFunction instance in case an overloaded method has alternatives with Function1
and PF
parameter types, and the Function1
alternative is chosen.
} | ||
map3(args, altArgPts, keepTypes) { | ||
case (_, _, Some(keep)) => keep | ||
case (arg, argPtAlts, None) => typedSingleArg(arg, argPtAlts, amode, matchAsPartial = false)._1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could probably use the selected function's parameter type here as expected type to type check the argument, so you wouldn't need to go through typedSingleArg
/ functionProto
again. Just a thought, maybe it's better to keep the two type checking invocations as similar as possible.
This is the simpler version that doesn't retype arguments: 2.13.x...szeiger:fix/pf-overloads2 |
case MethodType(syms, _) => syms.map(_.tpe) | ||
case TypeRef(_, _, tpes) => tpes.init | ||
case _ => Nil | ||
case ErrorType => Nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd go for the more cautious case _
ddb549a
to
bb91a8e
Compare
Let's merge your "simpler version that doesn't retype arguments" (2.13.x...szeiger:fix/pf-overloads2) for M2. We can re-visit the other version for M3, but I think it's not worth the additional complexity. |
@szeiger should this remain in the PR queue...? |
The mixed overloads case is probably not important enough to warrant further optimization. |
The extension of shape-based overload resolutions for pattern-matching
anonymous functions in #5698 did not
work correctly in all cases:
Type unification for shapes needs to treat
PartialFunction
typesthe same as
FunctionN
and SAM types. They were considered forunification but the parameter type was not extracted correctly.
When the matched argument types are constructed after a successful
shape match, a pattern-matching anonymous function has to be typed as
PartialFunction
instead ofFunctionN
.