Skip to content

Better resolution of implicits in patterns #13807

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1344,7 +1344,31 @@ trait Applications extends Compatibility {
unapplyArgType
}
val dummyArg = dummyTreeOfType(ownType)
val unapplyApp = typedExpr(untpd.TypedSplice(Apply(unapplyFn, dummyArg :: Nil)))
var unapplyApp: tpd.Tree = null
val unapplyAppTpe = {
val explicitCall = Apply(unapplyFn, dummyArg :: Nil)
tryEither {
unapplyApp = typedExpr(untpd.TypedSplice(explicitCall))
unapplyApp.tpe
} { (_, _) =>
// Typing explicitCall may fail if there are implicits that can only
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the core of the change.

// be resolved with tighter bounds on the output types
// (e.g. def unapply[T](x: String)(using Foo[T]): Option[T])
// If the call above fails to type, we try again here but with wildcards
Copy link
Member

Choose a reason for hiding this comment

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

Having read through https://contributors.scala-lang.org/t/better-implicit-resolution-in-pattern-matches/5308 I sort of understand the usecase, but I'm not sure it's worth it given this kind of weird business with wildcards needed in the implementation. It seems that pattern typing is fundamentally top-down and this code attempts to work around that in some specific cases, but I can't really tell what the implications of that change is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for follow up. I agree that this implementation is not ideal, but I think I would disagree that pattern typing ought to be fundamentally top-down. I think it is right now, but I don't see any good reason for it. I almost filed the feature request as a bug report because it's not really clear to me whether the current behavior is the spec.

If pattern matching were specified as syntactic sugar over a series of called to unapply/flatMap, etc., I think we might find that the behavior specified in the "feature request" would fall out naturally, and this PR would be more of a (possibly hacky) bug fix. I would like to fix the possibly hacky part, but I'll need a little consultation on what would seem less hacky and I suppose agreement on whether the syntactic sugar view of pattern matching is correct. Maybe it's worth starting a separate thread on what that view might be? Or is there already a spec for the bespoke type inference currently implemented beyond the implementation?

Copy link
Member

Choose a reason for hiding this comment

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

Or is there already a spec for the bespoke type inference currently implemented beyond the implementation?

The Scala 2 spec contains this section: https://www.scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html#pattern-matching-expressions but I honestly have no idea how closely it matches either the Scala 2 or 3 implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That spec doesn't say anything about how to type a pattern, just how to type a pattern in the context of type variables (IIUC). And if anything, it sounds a lot like the implementation in this PR, except with undefined for type parameters instead of wildcards for implicits?

First, it is attempted to type [pattern] p with T as its expected type. If this fails, p is instead typed with a modified expected type T' which results from T by replacing every occurrence of a type parameter a_i by undefined.

That said, any view of pattern matching as syntactic sugar for a series of calls to flatMap/orElse/isInstanceOf that I could think of would not lead to the behavior that I implement in this PR, so I can't really argue this is a bug. I will close the PR for lack of interest in this new feature. Thanks for your time!

// filled in for any implicits. Once we have recursively typed unapplyPatterns below,
// type variables may have tighter bounds and we can try again.
unapplyApp = null
var currType = mt.resultType
var callWithImplicits = explicitCall
while (currType.isImplicitMethod) {
val currMtType = currType.asInstanceOf[MethodType]
val wildcards = List.fill(currMtType.paramInfos.length)(dummyTreeOfType(WildcardType))
callWithImplicits = Apply(callWithImplicits, wildcards)
currType = currMtType.resultType
}
typedExpr(untpd.TypedSplice(callWithImplicits)).tpe
}
}
def unapplyImplicits(unapp: Tree): List[Tree] = {
val res = List.newBuilder[Tree]
def loop(unapp: Tree): Unit = unapp match {
Expand All @@ -1359,8 +1383,8 @@ trait Applications extends Compatibility {
res.result()
}

var argTypes = unapplyArgs(unapplyApp.tpe, unapplyFn, args, tree.srcPos)
for (argType <- argTypes) assert(!isBounds(argType), unapplyApp.tpe.show)
var argTypes = unapplyArgs(unapplyAppTpe, unapplyFn, args, tree.srcPos)
for (argType <- argTypes) assert(!isBounds(argType), unapplyAppTpe.show)
val bunchedArgs = argTypes match {
case argType :: Nil =>
if (args.lengthCompare(1) > 0 && Feature.autoTuplingEnabled && defn.isTupleNType(argType)) untpd.Tuple(args) :: Nil
Expand All @@ -1373,6 +1397,7 @@ trait Applications extends Compatibility {
List.fill(argTypes.length - args.length)(WildcardType)
}
val unapplyPatterns = bunchedArgs.lazyZip(argTypes) map (typed(_, _))
if (unapplyApp == null) unapplyApp = typedExpr(untpd.TypedSplice(Apply(unapplyFn, dummyArg :: Nil)))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re-infer types after type variables might have been further constrained.

val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns), ownType)
unapp.println(s"unapply patterns = $unapplyPatterns")
if ((ownType eq selType) || ownType.isError) result
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Inferencing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ trait Inferencing { this: Typer =>
end constrainIfDependentParamRef
}

/** An enumeration controlling the degree of forcing in "is-dully-defined" checks. */
/** An enumeration controlling the degree of forcing in "is-fully-defined" checks. */
@sharable object ForceDegree {
class Value(val appliesTo: TypeVar => Boolean, val ifBottom: IfBottom)
val none: Value = new Value(_ => false, IfBottom.ok)
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ object ProtoTypes {
* [](args): resultType
*
* @param args The untyped arguments to which the function is applied
* @param resType The expeected result type
* @param resType The expected result type
* @param typer The typer to use for typing the arguments
* @param applyKind The kind of application (regular/using/tupled infix operand)
* @param state The state object to use for tracking the changes to this prototype
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,9 @@ class Typer extends Namer
val expr1 =
if isWildcard then tree.expr.withType(underlyingTreeTpe.tpe)
else typed(tree.expr, underlyingTreeTpe.tpe.widenSkolem)
if (ctx.mode.is(Mode.Pattern)) {
Copy link
Contributor Author

@adampauls adampauls Oct 24, 2021

Choose a reason for hiding this comment

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

There must be something smarter here. Currently, pt seems to be nearly unused here, which seems wrong. This check has the side effect of e.g. constraining a type variable T in def unapply[T](...): Option[T] to be a subtype of a type ascription (for example, `case SomeUnapply(x: Foo)'). Needed to pass f242-3.scala. This is the thing I would like to do in a better way.

pt <:< underlyingTreeTpe.tpe
}
assignType(cpy.Typed(tree)(expr1, tpt), underlyingTreeTpe)
.withNotNullInfo(expr1.notNullInfo)
}
Expand Down Expand Up @@ -2019,7 +2022,7 @@ class Typer extends Namer
}

def typedBind(tree: untpd.Bind, pt: Type)(using Context): Tree = {
if !isFullyDefined(pt, ForceDegree.all) then
if !(ctx.mode.is(Mode.Pattern) || isFullyDefined(pt, ForceDegree.all)) then
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Needed to pass f242-2.scala

return errorTree(tree, i"expected type of $tree is not fully defined")
val body1 = typed(tree.body, pt)
body1 match {
Expand Down
6 changes: 0 additions & 6 deletions tests/neg-macros/i6436.check
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,3 @@
5 | case '{ StringContext(${Varargs(parts)}*) } => // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| no implicit argument of type scala.quoted.Quotes was found
-- [E006] Not Found Error: tests/neg-macros/i6436.scala:6:34 -----------------------------------------------------------
6 | val ps: Seq[Expr[String]] = parts // error
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is an improvement?

| ^^^^^
| Not found: parts

longer explanation available when compiling with `-explain`
4 changes: 2 additions & 2 deletions tests/neg-macros/i6436.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import scala.quoted.*
def f(sc: quoted.Expr[StringContext]): Unit = {
sc match {
case '{ StringContext(${Varargs(parts)}*) } => // error
val ps: Seq[Expr[String]] = parts // error
val ps: Seq[Expr[String]] = parts
}
}
}
6 changes: 6 additions & 0 deletions tests/neg/f242.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Error: tests/neg/f242.scala:23:17 -----------------------------------------------------------------------------------
23 | case Foo.Type(NotAnInstance(_)) => ??? // error: no implicit argument of type Test.Foo[T] was found
| ^
| no implicit argument of type Test.Foo[T] was found for an implicit parameter of method unapply in object Type
|
| where: T is a type variable with constraint <: Test.NotAnInstance
25 changes: 25 additions & 0 deletions tests/neg/f242.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
object Test {
trait Foo[Self]
object Foo {
class Type private(t: Any)

object Type {
def apply[T: Foo](t: T) = new Type(t)

def unapply[T: Foo](t: Type): Option[T] = ???
}
}

case class Instance1(a: String)
given Foo[Instance1]()

case class Instance2(b: Int)
given Foo[Instance2]()

case class NotAnInstance(b: Double)

Foo.Type(Instance1("5")) match {
case Foo.Type(Instance1(_: String)) => ???
case Foo.Type(NotAnInstance(_)) => ??? // error: no implicit argument of type Test.Foo[T] was found
}
}
12 changes: 12 additions & 0 deletions tests/pos/f242-2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
object Test {
object Unapply {
def unapply[T1, T2, T3](x: Int)(using T1)(using T2, T3): Option[(T1, T2, T3)] = ???
}
given Int = 6
given String = "s"
given Boolean = false

5 match {
case Unapply(f: Int, _: String, _: Boolean) if f == 5 => ???
}
}
14 changes: 14 additions & 0 deletions tests/pos/f242-3.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
object Test {
object Unapply {
def unapply[T](x: Int)(using T): Option[T] = ???
}
trait Foo
object Impl extends Foo

implicit val x: Foo = Impl
implicit val distractor: Int = 5

5 match {
case Unapply(Impl: Foo) => ???
}
}
9 changes: 9 additions & 0 deletions tests/pos/f242-4.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
object Test {
object Unapply {
def unapply(x: Int): Option[Any] = ???
}

5 match {
case Unapply(f: Int) if f == 5 => ???
}
}
25 changes: 25 additions & 0 deletions tests/pos/f242.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
object Test {
trait Foo[Self]
object Foo {
class Type private(t: Any)

object Type {
def apply[T: Foo](t: T) = new Type(t)

def unapply[T: Foo](t: Type): Option[T] = ???
}
}

case class Instance1(a: String)
given Foo[Instance1]()

case class Instance2(b: Int)
given Foo[Instance2]()

Foo.Type(Instance1("5")) match {
case Foo.Type(Instance1(_: String)) => ???
case Foo.Type(_: Instance1) => ???
case Foo.Type(Instance2(_: Int)) => ???
case Foo.Type(_: Instance2) => ???
}
}