Skip to content

Commit f3811be

Browse files
authored
Merge pull request #12214 from dotty-staging/fix-12211
Fix comparison of dependent function types
2 parents bb0a9a7 + cd77720 commit f3811be

File tree

7 files changed

+144
-45
lines changed

7 files changed

+144
-45
lines changed

compiler/src/dotty/tools/dotc/core/TypeComparer.scala

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,40 +1698,70 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
16981698
*/
16991699
protected def hasMatchingMember(name: Name, tp1: Type, tp2: RefinedType): Boolean =
17001700
trace(i"hasMatchingMember($tp1 . $name :? ${tp2.refinedInfo}), mbr: ${tp1.member(name).info}", subtyping) {
1701-
val rinfo2 = tp2.refinedInfo
1702-
1703-
// If the member is an abstract type and the prefix is a path, compare the member itself
1704-
// instead of its bounds. This case is needed situations like:
1705-
//
1706-
// class C { type T }
1707-
// val foo: C
1708-
// foo.type <: C { type T {= , <: , >:} foo.T }
1709-
//
1710-
// or like:
1711-
//
1712-
// class C[T]
1713-
// C[?] <: C[TV]
1714-
//
1715-
// where TV is a type variable. See i2397.scala for an example of the latter.
1716-
def matchAbstractTypeMember(info1: Type) = info1 match {
1717-
case TypeBounds(lo, hi) if lo ne hi =>
1718-
tp2.refinedInfo match {
1719-
case rinfo2: TypeBounds if tp1.isStable =>
1720-
val ref1 = tp1.widenExpr.select(name)
1721-
isSubType(rinfo2.lo, ref1) && isSubType(ref1, rinfo2.hi)
1722-
case _ =>
1723-
false
1724-
}
1725-
case _ => false
1726-
}
17271701

1728-
def qualifies(m: SingleDenotation) =
1729-
isSubType(m.info.widenExpr, rinfo2.widenExpr) || matchAbstractTypeMember(m.info)
1702+
def qualifies(m: SingleDenotation): Boolean =
1703+
// If the member is an abstract type and the prefix is a path, compare the member itself
1704+
// instead of its bounds. This case is needed situations like:
1705+
//
1706+
// class C { type T }
1707+
// val foo: C
1708+
// foo.type <: C { type T {= , <: , >:} foo.T }
1709+
//
1710+
// or like:
1711+
//
1712+
// class C[T]
1713+
// C[?] <: C[TV]
1714+
//
1715+
// where TV is a type variable. See i2397.scala for an example of the latter.
1716+
def matchAbstractTypeMember(info1: Type): Boolean = info1 match {
1717+
case TypeBounds(lo, hi) if lo ne hi =>
1718+
tp2.refinedInfo match {
1719+
case rinfo2: TypeBounds if tp1.isStable =>
1720+
val ref1 = tp1.widenExpr.select(name)
1721+
isSubType(rinfo2.lo, ref1) && isSubType(ref1, rinfo2.hi)
1722+
case _ =>
1723+
false
1724+
}
1725+
case _ => false
1726+
}
17301727

1731-
tp1.member(name) match { // inlined hasAltWith for performance
1728+
// An additional check for type member matching: If the refinement of the
1729+
// supertype `tp2` does not refer to a member symbol defined in the parent of `tp2`.
1730+
// then the symbol referred to in the subtype must have a signature that coincides
1731+
// in its parameters with the refinement's signature. The reason for the check
1732+
// is that if the refinement does not refer to a member symbol, we will have to
1733+
// resort to reflection to invoke the member. And reflection needs to know exact
1734+
// erased parameter types. See neg/i12211.scala.
1735+
def sigsOK(symInfo: Type, info2: Type) =
1736+
tp2.underlyingClassRef(refinementOK = true).member(name).exists
1737+
|| symInfo.isInstanceOf[MethodType]
1738+
&& symInfo.signature.consistentParams(info2.signature)
1739+
1740+
// A relaxed version of isSubType, which compares method types
1741+
// under the standard arrow rule which is contravarient in the parameter types,
1742+
// but under the condition that signatures might have to match (see sigsOK)
1743+
// This relaxed version is needed to correctly compare dependent function types.
1744+
// See pos/i12211.scala.
1745+
def isSubInfo(info1: Type, info2: Type, symInfo: Type): Boolean =
1746+
info2 match
1747+
case info2: MethodType =>
1748+
info1 match
1749+
case info1: MethodType =>
1750+
val symInfo1 = symInfo.stripPoly
1751+
matchingMethodParams(info1, info2, precise = false)
1752+
&& isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType)
1753+
&& sigsOK(symInfo1, info2)
1754+
case _ => isSubType(info1, info2)
1755+
case _ => isSubType(info1, info2)
1756+
1757+
val info1 = m.info.widenExpr
1758+
isSubInfo(info1, tp2.refinedInfo.widenExpr, m.symbol.info.orElse(info1))
1759+
|| matchAbstractTypeMember(m.info)
1760+
end qualifies
1761+
1762+
tp1.member(name) match // inlined hasAltWith for performance
17321763
case mbr: SingleDenotation => qualifies(mbr)
17331764
case mbr => mbr hasAltWith qualifies
1734-
}
17351765
}
17361766

17371767
final def ensureStableSingleton(tp: Type): SingletonType = tp.stripTypeVar match {
@@ -1841,15 +1871,20 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
18411871
}
18421872

18431873
/** Do the parameter types of `tp1` and `tp2` match in a way that allows `tp1`
1844-
* to override `tp2` ? This is the case if they're pairwise `=:=`.
1874+
* to override `tp2` ? Two modes: precise or not.
1875+
* If `precise` is set (which is the default) this is the case if they're pairwise `=:=`.
1876+
* Otherwise parameters in `tp2` must be subtypes of corresponding parameters in `tp1`.
18451877
*/
1846-
def matchingMethodParams(tp1: MethodType, tp2: MethodType): Boolean = {
1878+
def matchingMethodParams(tp1: MethodType, tp2: MethodType, precise: Boolean = true): Boolean = {
18471879
def loop(formals1: List[Type], formals2: List[Type]): Boolean = formals1 match {
18481880
case formal1 :: rest1 =>
18491881
formals2 match {
18501882
case formal2 :: rest2 =>
18511883
val formal2a = if (tp2.isParamDependent) formal2.subst(tp2, tp1) else formal2
1852-
isSameTypeWhenFrozen(formal1, formal2a) && loop(rest1, rest2)
1884+
val paramsMatch =
1885+
if precise then isSameTypeWhenFrozen(formal1, formal2a)
1886+
else isSubTypeWhenFrozen(formal2a, formal1)
1887+
paramsMatch && loop(rest1, rest2)
18531888
case nil =>
18541889
false
18551890
}

docs/docs/reference/changed-features/structural-types-spec.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ RefineStatSeq ::= RefineStat {semi RefineStat}
1212
RefineStat ::= ‘val’ VarDcl | ‘def’ DefDcl | ‘type’ {nl} TypeDcl
1313
```
1414

15-
## Implementation of structural types
15+
## Implementation of Structural Types
1616

1717
The standard library defines a universal marker trait
1818
[`scala.Selectable`](https://github.com/lampepfl/dotty/blob/master/library/src/scala/Selectable.scala):
@@ -82,21 +82,49 @@ Note that `v`'s static type does not necessarily have to conform to `Selectable`
8282
conversion that can turn `v` into a `Selectable`, and the selection methods could also be available as
8383
[extension methods](../contextual/extension-methods.md).
8484

85-
## Limitations of structural types
85+
## Limitations of Structural Types
8686

8787
- Dependent methods cannot be called via structural call.
88-
- Overloaded methods cannot be called via structural call.
89-
- Refinements do not handle polymorphic methods.
9088

91-
## Differences with Scala 2 structural types
89+
- Refinements may not introduce overloads: If a refinement specifies the signature
90+
of a method `m`, and `m` is also defined in the parent type of the refinement, then
91+
the new signature must properly override the existing one.
92+
93+
- Subtyping of structural refinements must preserve erased parameter types: Assume
94+
we want to prove `S <: T { def m(x: A): B }`. Then, as usual, `S` must have a member method `m` that can take an argument of type `A`. Furthermore, if `m` is not a member of `T` (i.e. the refinement is structural), an additional condition applies. In this case, the member _definition_ `m` of `S` will have a parameter
95+
with type `A'` say. The additional condition is that the erasure of `A'` and `A` is the same. Here is an example:
96+
97+
```scala
98+
class Sink[A] { def put(x: A): Unit = {} }
99+
val a = Sink[String]()
100+
val b: { def put(x: String): Unit } = a // error
101+
b.put("abc") // looks for a method with a `String` parameter
102+
```
103+
The second to last line is not well-typed, since the erasure of the parameter type of `put` in class `Sink` is `Object`, but the erasure of `put`'s parameter in the type of `b` is `String`. This additional condition is necessary, since we will have to resort to reflection to call a structural member like `put` in the type of `b` above. The condition ensures that the statically known parameter types of the refinement correspond up to erasure to the parameter types of the selected call target at runtime.
104+
105+
The usual reflection dispatch algorithms need to know exact erased parameter types. For instance, if the example above would typecheck, the call
106+
`b.put("abc")` on the last line would look for a method `put` in the runtime type of `b` that takes a `String` parameter. But the `put` method is the one from class `Sink`, which takes an `Object` parameter. Hence the call would fail at runtime with a `NoSuchMethodException`.
107+
108+
One might hope for a "more intelligent" reflexive dispatch algorithm that does not require exact parameter type matching. Unfortunately, this can always run into ambiguities. For instance, continuing the example above, we might introduce a new subclass `Sink1` of `Sink` and change the definition of `a` as follows:
109+
110+
```scala
111+
class Sink1[A] extends Sink[A] { def put(x: "123") = ??? }
112+
val a: Sink[String] = Sink1[String]()
113+
```
114+
115+
Now there are two `put` methods in the runtime type of `b` with erased parameter
116+
types `Object` and `String`, respectively. Yet dynamic dispatch still needs to go
117+
to the first `put` method, even though the second looks like a better match.
118+
119+
## Differences with Scala 2 Structural Types
92120

93121
- Scala 2 supports structural types by means of Java reflection. Unlike
94122
Scala 3, structural calls do not rely on a mechanism such as
95123
`Selectable`, and reflection cannot be avoided.
96-
- In Scala 2, structural calls to overloaded methods are possible.
124+
- In Scala 2, refinements can introduce overloads.
97125
- In Scala 2, mutable `var`s are allowed in refinements. In Scala 3,
98126
they are no longer allowed.
99-
127+
- Scala 2 does not impose the "same-erasure" restriction on subtyping of structural types. It allows some calls to fail at runtime instead.
100128

101129
## Context
102130

tasty/test/dotty/tools/tasty/TastyHeaderUnpicklerTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ object TastyHeaderUnpicklerTest {
5757
buf.writeNat(exp)
5858
buf.writeNat(compilerBytes.length)
5959
buf.writeBytes(compilerBytes, compilerBytes.length)
60-
buf.writeUncompressedLong(237478l)
61-
buf.writeUncompressedLong(324789l)
60+
buf.writeUncompressedLong(237478L)
61+
buf.writeUncompressedLong(324789L)
6262
buf
6363
}
6464

tests/neg/i12211.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
import reflect.Selectable.*
3+
4+
val x: { def f(x: Any): String } = new { def f(x: Any) = x.toString }
5+
val y: { def f(x: String): String } = x // error: type mismatch (different signatures)
6+
7+
class Sink[A] { def put(x: A): Unit = {} }
8+
class Sink1[A] extends Sink[A] { def put(x: "123") = ??? }
9+
10+
@main def Test =
11+
println(y.f("abc"))
12+
val a = new Sink[String]
13+
val b: { def put(x: String): Unit } = a // error: type mismatch (different signatures)
14+
b.put("") // gave a NoSuchMethodException: Sink.put(java.lang.String)
15+
val c: Sink[String] = Sink1[String]()

tests/run/structuralNoSuchMethod.scala renamed to tests/neg/structuralNoSuchMethod.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ object Test {
1111
def f(x: X, y: String): String = "f1"
1212
}
1313

14-
val x: T = new C[String]
14+
val x: T = new C[String] // error
1515

1616
def main(args: Array[String]) =
17-
try println(x.f("", "")) // throws NoSuchMethodException
17+
try println(x.f("", "")) // used to throw NoSuchMethodException
1818
catch {
1919
case ex: NoSuchMethodException =>
2020
println("no such method")

tests/pos/i12211.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
def fst0[A, B[_]](a: A)(b: B[a.type]): a.type = a
3+
4+
def fst[A, B[_]]: (a: A) => (b: B[a.type]) => a.type =
5+
(a: A) => (b: B[a.type]) => a
6+
7+
def snd[A, B[_]]: (a: A) => () => (b: B[a.type]) => b.type =
8+
(a: A) => () => (b: B[a.type]) => b
9+
10+
def fst1[A, B[_]]: (a: A) => (b: B[a.type]) => a.type = fst0
11+
12+
def test1[A, B[_]]: (a: A) => () => (b: B[a.type]) => Any =
13+
snd[A, B]
14+
15+
def test2[A, B[_]]: (a: A) => (b: B[a.type]) => A = fst[A, B]
16+
17+
class AA
18+
class BB[T]
19+
20+
def test3: (a: AA) => (b: BB[a.type]) => BB[?] =
21+
(a: AA) => (b: BB[a.type]) => b

tests/run/enum-values.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ enum ClassOnly: // this should still generate the `ordinal` and `fromOrdinal` co
5050
s"$c does not `eq` companion.fromOrdinal(${c.ordinal}), got ${companion.fromOrdinal(c.ordinal)}")
5151

5252
def notFromOrdinal[T <: AnyRef & reflect.Enum](companion: FromOrdinal[T], compare: T): Unit =
53-
cantFind(companion, compare.ordinal)
53+
cantFind(companion.asInstanceOf[FromOrdinal[Any]], compare.ordinal)
5454

5555
def cantFind[T](companion: FromOrdinal[T], ordinal: Int): Unit =
5656
try

0 commit comments

Comments
 (0)