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

More precise inference for overloaded methods when arguments types align #7631

Merged
merged 2 commits into from
Feb 6, 2019

Conversation

adriaanm
Copy link
Contributor

@adriaanm adriaanm commented Jan 11, 2019

Normally, an argument supplied to an application of an overloaded method
is typed without an expected type. We can constrain type inference more
when we have only one candidate expected type between all alternatives.

So, this adds an exception when all alternatives explicitly specify the
same parameter type for an argument (a missing parameter type, due to e.g.
arity differences, is taken as NoType, thus resorting to no expected type).

Also improve a few corner cases with implicits (views), call-by-name arguments, named arguments.

Note that this also aligns us a bit better with dotty, where constraints are solved lazily,
while being pushed through overload resolution, so that this more precise kind of type inference
falls out "naturally" :-)

Fix scala/bug#11015

@SethTisue let's see how much of the community build I broke :-)

@adriaanm adriaanm added the release-notes worth highlighting in next release notes label Jan 11, 2019
@scala-jenkins scala-jenkins added this to the 2.13.0-RC1 milestone Jan 11, 2019
@adriaanm
Copy link
Contributor Author

problematic case, not sure why yet:

  def assertSameElements[A, B](expected: Iterable[A], actual: Iterable[B]): Unit = ???
  def assertSameElements[A, B](expected: Iterable[A], actual: IterableOnce[B]): Unit = ???
  assertSameElements("abcd".split('d'), Array("abc"))

@adriaanm adriaanm force-pushed the t11015 branch 2 times, most recently from ff18722 to 9094297 Compare January 14, 2019 13:59
@SethTisue
Copy link
Member

community build run queued:
https://scala-ci.typesafe.com/job/scala-2.13.x-integrate-community-build/1692/ (404 til Jenkins wakes up)

@retronym
Copy link
Member

retronym commented Jan 16, 2019

I was hoping this case would be fixed by this PR:

$ (jabba use openjdk@1.10.0-2; scala -e 'println(java.util.List.of(""))')
/var/folders/b7/xcc2k0ln6ldcv247ffpy2d1w0000gp/T/scalacmd6660322910276770357.scala:1: error: ambiguous reference to overloaded definition,
both method of in object List of type [E](x$1: E*)java.util.List[E]
and  method of in object List of type [E](x$1: E)java.util.List[E]
match argument types (String) and expected result type Any
println(java.util.List.of(""))
                       ^

But it still fails with this PR:

$ (jabba use openjdk@1.10.0-2; scala-pr 7631 -e 'println(java.util.List.of(""))')
/var/folders/b7/xcc2k0ln6ldcv247ffpy2d1w0000gp/T/scalacmd6997157620920767218.scala:1: error: ambiguous reference to overloaded definition,
both method of in class List of type [E](x$1: E*)java.util.List[E]
and  method of in class List of type [E](x$1: E)java.util.List[E]
match argument types (String) and expected result type Any
println(java.util.List.of(""))
                       ^

@adriaanm
Copy link
Contributor Author

I think that would need a change to specificity rules, off the top of my head.

@adriaanm
Copy link
Contributor Author

Community build fail to investigate:

src/test/scala/spray/json/JsonParserSpec.scala:181:35: type mismatch;
[spray-json] [error]  found   : String("{\"key\":1}{\"key\":2}")
[spray-json] [error]  required: spray.json.ParserInput
[spray-json] [error]       val parser = new JsonParser("""{"key":1}{"key":2}""")
[spray-json] [error]                                   ^
[spray-json] [error] one error found

Normally, an argument supplied to an application of an overloaded method
is typed without an expected type. We can constrain type inference more
when we have only one candidate expected type between all alternatives.

So, this adds an exception when all alternatives explicitly specify the
same parameter type for an argument (a missing parameter type, due to e.g.
arity differences, is taken as `NoType`, thus resorting to no expected type).

Also improve a few corner cases with call-by-name arguments, named arguments.
Avoid having to deal with funky prototypes during implicit search.
@adriaanm
Copy link
Contributor Author

Addressed the failure in spray-json with 68f3c9f

@adriaanm

This comment has been minimized.

@szeiger
Copy link
Member

szeiger commented Jan 23, 2019

Why would the java.util.List case be different than scala/bug#11015? In both cases you have a parameter of type A vs A*. Actually this prompted me to look at the spec for overload resolution again. Previously I fixating on the inferred type parameter Nothing which should be improved, but the spec doesn't even mention repeated parameters. I don't see anything in there that makes either A or A* more specific. So while type inference is not good enough for x.append(Foo(2)), shouldn't it be ambiguous either way? In which case the actual error would be that x.append(Foo[String](2)) compiles.

@adriaanm
Copy link
Contributor Author

adriaanm commented Jan 24, 2019

Good point. I'll investigate why that even compiles. I wonder if we can incorporate repeated params into the notion of being as-specific. You could think of being applicable to a repeated param involving an implicit conversion from T* to T, ..., T, which would result in a different score than direct applicability. See isStrictlyMoreSpecific, and this case inisAsSpecific:

case mt @ MethodType(_, _) if bothAreVarargs => checkIsApplicable(mt.paramTypes mapConserve repeatedToSingle)

this seems to explain the behavior, but i haven't verified yet

@adriaanm
Copy link
Contributor Author

adriaanm commented Jan 24, 2019

The original case for both methods being varargs was added in 1b80725 without much explanation.

@adriaanm
Copy link
Contributor Author

adriaanm commented Jan 24, 2019

Looks like I found the spot. I'll open a separate PR: #7680

val result = inferImplicitView(fromNoAnnot, to, tree, context, reportAmbiguous, saveErrors) match {
case fail if fail.isFailure => inferImplicitView(byNameType(fromNoAnnot), to, tree, context, reportAmbiguous, saveErrors)
val toNorm = normalizeProtoForView(to)
val result = inferImplicitView(fromNoAnnot, toNorm, tree, context, reportAmbiguous, saveErrors) match {
Copy link
Member

@retronym retronym Jan 29, 2019

Choose a reason for hiding this comment

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

Is there a sensible / fast path through inferImplicitView when pt = WildCardType, i.e. the overloaded signatures don't agree on an effective single formal param type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I don't follow. If the overloaded signatures don't agree, proto.underlying will be WildcardType, so that will be normalizeProtoForView's result, but a wildcard for the expected type of a view makes little sense. Are you saying we should add another check to avoid calling inferImplicitView for such a pt? I suggest that would be a separate issue, since this PR doesn't really add new ways we could get here with a wildcard expected type (the path from adapt/isCoercible would not get to their callsite of inferImplicitView with such a pt.

@retronym
Copy link
Member

retronym commented Jan 29, 2019

When an OverloadedArgProto is used as the pt to Typer.typed, I've got a few questions (!!! comments inline below).

        val ptPlugins = pluginsPt(pt, this, tree, mode) // !!! Analyzer plugins (continuations? any others we know of?) will be unready for `OverloadedArgProto`
        def retypingOk = (
          context.retyping
            && (tree.tpe ne null)
            && (tree.tpe.isErroneous || !(tree.tpe <:< ptPlugins))
          )
        if (retypingOk) {
          tree.setType(null)
          if (tree.hasSymbolField) tree.symbol = NoSymbol
        }
        val alreadyTyped = tree.tpe ne null
        val shouldPrint = !alreadyTyped && !phase.erasedTypes
        val ptWild = if (mode.inPatternMode)
          ptPlugins // scala/bug#5022 don't widen pt for patterns as types flow from it to the case body.
        else
          dropExistential(ptPlugins) // FIXME: document why this is done. // !!! Do we need to make `dropExistential` deal with `OverloadedArgProto`?

From over in your other PR, we also check repeatedness of pt in https://github.com/scala/scala/pull/7680/files?utf8=%E2%9C%93&diff=split&w=1#diff-4eab1aad4533a31c10565971e90f73eaR1151. That might fall out because of the way things are implemented (OverloadedArgProto.typeSymbol unwraps, I suppose).

@adriaanm adriaanm self-assigned this Feb 4, 2019
@adriaanm
Copy link
Contributor Author

adriaanm commented Feb 4, 2019

!!! Analyzer plugins

yes, they would have to be updated

dropExistential

I can't think of what this does -- my best guess is that it's for performance / workaround for issues doing subtyping/lub of existentials?

OverloadedArgProto.typeSymbol unwraps, I suppose

indeed

@retronym
Copy link
Member

retronym commented Feb 5, 2019

I commented out the dropExistential part of typed to jog our collective memory 😄

# Failed test paths (this command will update checkfiles)
partest --update-check \
  /Users/jz/code/scala/test/files/pos/patmat.scala \
  /Users/jz/code/scala/test/files/pos/sammy_exist.scala \
  /Users/jz/code/scala/test/files/pos/sammy_overload.scala \
  /Users/jz/code/scala/test/files/neg/logImplicits.scala \
  /Users/jz/code/scala/test/files/neg/sammy_error_exist_no_crash.scala \
  /Users/jz/code/scala/test/files/neg/t6258.scala \
  /Users/jz/code/scala/test/files/neg/t9273.scala

Extending the test to inject an OverlaodedArgProto wrapping an existential at pt:

trait Consumer[T] {
  def consume(x: T): Unit
}

object Test {
  def foo(x: String): Unit = ???
  def foo(): Unit = ???
  val f: Consumer[_ >: String] = foo

  def ol(a: Consumer[_ >: String], b: String) = ()
  def ol(a: Consumer[_ >: String], b: Int) = ()
  ol(foo, "") // fails with "error: type mismatch; found: Unit, required: Consumer[_ >: String]
}

Playing around a bit more reveals a latent bug (I think)

scala> def foo(a: Function[String, _ <: String]): a.type = a
foo: (a: Function[String, _ <: String])a.type

scala> foo(x => x)
<console>:13: error: missing parameter type
       foo(x => x)
           ^

This code doesn't account for the fact that ptNorm baseType FunctionSymbol can be an ExistentialType:

ptNorm baseType FunctionSymbol match {
case TypeRef(_, _, args :+ res) => (args, res) // if it's a TypeRef, we know its symbol will be FunctionSymbol
case _ => {
val dummyPt = if (pt == ErrorType) ErrorType else NoType
(List.fill(numVparams)(dummyPt), WildcardType) // dummyPt is in CBN position
}
}

@retronym
Copy link
Member

retronym commented Feb 5, 2019

If we fix that code (naively, for now, by just calling .underlying before pattern matching), we can compile that:

$ qscala -e 'def foo(a: Function[String, _ <: String]): a.type = a; foo(x => x)'

Then, we can remove the dropExistential again and see the failure:

$ qscala -nc -e 'def foo(a: Function[String, _ <: String]): a.type = a; foo(x => x)'
/var/folders/tz/p8vd07wn7wxck3b9v54grlzw0000gp/T/scalacmd7366029521523825229.scala:1: error: type mismatch;
 found   : x.type (with underlying type String)
 required: _$1
def foo(a: Function[String, _ <: String]): a.type = a; foo(x => x)
                                                               ^

@retronym
Copy link
Member

retronym commented Feb 5, 2019

Here's where my experiments led: https://github.com/retronym/scala/tree/topic/existential-pt

We really need a canonical way to extract the param/result types from a PF or F type. There is some duplication and divergence between, e.g., partialFunctionArgResTypeFromProto, functionOrPfOrSamArgTypes. Contrast with dropByName, repeatedToSingle etc which share the underlying code to deal with alias and wrapper types.

This isn't strictly related to this PR, so we can address separately. Let's discuss today.

@adriaanm
Copy link
Contributor Author

adriaanm commented Feb 6, 2019

Ok, as discussed, let's merge this and leave the existential worries for later :-)

@adriaanm adriaanm merged commit b8101d5 into scala:2.13.x Feb 6, 2019
@retronym
Copy link
Member

retronym commented Feb 7, 2019

Followup: #7726

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
5 participants