Skip to content

Commit 502c7c9

Browse files
committed
Fix #6199: Use a skolemized prefix in asSeenFrom when needed
Prior to 91ee02f, unstable prefixes appearing in selection types were handled in a two-pass approach which can be summarized as: 1. Type the selection with the unstable prefix, if this prefix appears in a position where it cannot be widened away, mark it with a special annotation. 2. If the result of the selection contains this annotation, retype it after having wrapped its prefix in a skolem. This got replaced by the use of an `ApproximatingTypeMap` which can always construct a valid (but potentially approximated) type for the selection. Most of the time this is all we need but there are some cases where skolemizing the prefix is still useful as witnessed by the tests added in this commit. They are now handled by unconditionally skolemizing unstable prefixes. To avoid any potential performance impact from this, we teach `asSeenFrom` to always widen these prefixes first and only make use of the skolem to avoid returning an approximation. Since this almost never occurs (it happens only once when compiling Dotty) this means we incur almost no extra cost (the skolem itself still needs to be allocated unconditionally in advance, this cannot be delegated to `asSeenFrom` because it's supposed to be an idempotent operation and each skolem is unique).
1 parent 61ea245 commit 502c7c9

File tree

7 files changed

+116
-18
lines changed

7 files changed

+116
-18
lines changed

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

+44-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package core
55
import Contexts._, Types._, Symbols._, Names._, Flags._
66
import SymDenotations._
77
import util.Spans._
8+
import util.Stats
89
import util.SourcePosition
910
import NameKinds.DepParamName
1011
import Decorators._
@@ -26,11 +27,34 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
2627
/** The type `tp` as seen from prefix `pre` and owner `cls`. See the spec
2728
* for what this means.
2829
*/
29-
final def asSeenFrom(tp: Type, pre: Type, cls: Symbol): Type =
30+
final def asSeenFrom(tp: Type, pre: Type, cls: Symbol): Type = {
31+
pre match {
32+
case pre: QualSkolemType =>
33+
// When a selection has an unstable qualifier, the qualifier type gets
34+
// wrapped in a `QualSkolemType` so that it may appear soundly as the
35+
// prefix of a path in the selection type.
36+
// However, we'd like to avoid referring to skolems when possible since
37+
// they're an extra level of indirection we usually don't need, so we
38+
// compute the type as seen from the widened prefix, and in the rare
39+
// cases where this leads to an approximated type we recompute it with
40+
// the skolemized prefix. See the i6199* tests for usecases.
41+
val widenedAsf = new AsSeenFromMap(pre.info, cls)
42+
val ret = widenedAsf.apply(tp)
43+
44+
if (!widenedAsf.approximated)
45+
return ret
46+
47+
Stats.record("asSeenFrom skolem prefix required")
48+
case _ =>
49+
}
50+
3051
new AsSeenFromMap(pre, cls).apply(tp)
52+
}
3153

3254
/** The TypeMap handling the asSeenFrom */
3355
class AsSeenFromMap(pre: Type, cls: Symbol) extends ApproximatingTypeMap {
56+
/** Set to true when the result of `apply` was approximated to avoid an unstable prefix. */
57+
var approximated: Boolean = false
3458

3559
def apply(tp: Type): Type = {
3660

@@ -44,7 +68,19 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
4468
case pre: SuperType => toPrefix(pre.thistpe, cls, thiscls)
4569
case _ =>
4670
if (thiscls.derivesFrom(cls) && pre.baseType(thiscls).exists)
47-
if (variance <= 0 && !isLegalPrefix(pre)) range(defn.NothingType, pre)
71+
if (variance <= 0 && !isLegalPrefix(pre)) {
72+
if (variance < 0) {
73+
approximated = true
74+
defn.NothingType
75+
}
76+
else
77+
// Don't set the `approximated` flag yet: if this is a prefix
78+
// of a path, we might be able to dealias the path instead
79+
// (this is handled in `ApproximatingTypeMap`). If dealiasing
80+
// is not possible, then `expandBounds` will end up being
81+
// called which we override to set the `approximated` flag.
82+
range(defn.NothingType, pre)
83+
}
4884
else pre
4985
else if ((pre.termSymbol is Package) && !(thiscls is Package))
5086
toPrefix(pre.select(nme.PACKAGE), cls, thiscls)
@@ -74,9 +110,14 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
74110
override def reapply(tp: Type): Type =
75111
// derived infos have already been subjected to asSeenFrom, hence to need to apply the map again.
76112
tp
113+
114+
override protected def expandBounds(tp: TypeBounds): Type = {
115+
approximated = true
116+
super.expandBounds(tp)
117+
}
77118
}
78119

79-
private def isLegalPrefix(pre: Type)(implicit ctx: Context) =
120+
def isLegalPrefix(pre: Type)(implicit ctx: Context): Boolean =
80121
pre.isStable || !ctx.phase.isTyper
81122

82123
/** Implementation of Types#simplified */

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

+21-5
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ object Types {
549549
* flags and no `excluded` flag and produce a denotation that contains
550550
* the type of the member as seen from given prefix `pre`.
551551
*/
552-
final def findMember(name: Name, pre: Type, required: FlagConjunction, excluded: FlagSet)(implicit ctx: Context): Denotation = {
552+
final def findMember(name: Name, pre: Type, required: FlagConjunction = EmptyFlagConjunction, excluded: FlagSet = EmptyFlags)(implicit ctx: Context): Denotation = {
553553
@tailrec def go(tp: Type): Denotation = tp match {
554554
case tp: TermRef =>
555555
go (tp.underlying match {
@@ -3723,6 +3723,21 @@ object Types {
37233723
override def toString: String = s"Skolem($hashCode)"
37243724
}
37253725

3726+
/** A skolem type used to wrap the type of the qualifier of a selection.
3727+
*
3728+
* When typing a selection `e.f`, if `e` is unstable then we unconditionally
3729+
* skolemize it. We use a subclass of `SkolemType` for this so that
3730+
* [[TypeOps#asSeenFrom]] may treat it specially for optimization purposes,
3731+
* see its implementation for more details.
3732+
*/
3733+
class QualSkolemType(info: Type) extends SkolemType(info) {
3734+
override def derivedSkolemType(info: Type)(implicit ctx: Context): SkolemType =
3735+
if (info eq this.info) this else QualSkolemType(info)
3736+
}
3737+
object QualSkolemType {
3738+
def apply(info: Type): QualSkolemType = new QualSkolemType(info)
3739+
}
3740+
37263741
// ------------ Type variables ----------------------------------------
37273742

37283743
/** In a TypeApply tree, a TypeVar is created for each argument type to be inferred.
@@ -4646,6 +4661,9 @@ object Types {
46464661
case _ => tp
46474662
}
46484663

4664+
protected def expandBounds(tp: TypeBounds): Type =
4665+
range(atVariance(-variance)(reapply(tp.lo)), reapply(tp.hi))
4666+
46494667
/** Try to widen a named type to its info relative to given prefix `pre`, where possible.
46504668
* The possible cases are listed inline in the code.
46514669
*/
@@ -4657,10 +4675,10 @@ object Types {
46574675
// if H#T = U, then for any x in L..H, x.T =:= U,
46584676
// hence we can replace with U under all variances
46594677
reapply(alias.rewrapAnnots(tp1))
4660-
case TypeBounds(lo, hi) =>
4678+
case tp: TypeBounds =>
46614679
// If H#T = _ >: S <: U, then for any x in L..H, S <: x.T <: U,
46624680
// hence we can replace with S..U under all variances
4663-
range(atVariance(-variance)(reapply(lo)), reapply(hi))
4681+
expandBounds(tp)
46644682
case info: SingletonType =>
46654683
// if H#x: y.type, then for any x in L..H, x.type =:= y.type,
46664684
// hence we can replace with y.type under all variances
@@ -4676,8 +4694,6 @@ object Types {
46764694
* underlying bounds to a range, otherwise return the expansion.
46774695
*/
46784696
def expandParam(tp: NamedType, pre: Type): Type = {
4679-
def expandBounds(tp: TypeBounds) =
4680-
range(atVariance(-variance)(reapply(tp.lo)), reapply(tp.hi))
46814697
tp.argForParam(pre) match {
46824698
case arg @ TypeRef(pre, _) if pre.isArgPrefixOf(arg.symbol) =>
46834699
arg.info match {

compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala

+10-9
Original file line numberDiff line numberDiff line change
@@ -1016,14 +1016,14 @@ class TreeUnpickler(reader: TastyReader,
10161016
val localCtx =
10171017
if (name == nme.CONSTRUCTOR) ctx.addMode(Mode.InSuperCall) else ctx
10181018
val qual = readTerm()(localCtx)
1019-
var pre = qual.tpe.widenIfUnstable
1020-
val denot = accessibleDenot(pre, name, sig)
1019+
var qualType = qual.tpe.widenIfUnstable
1020+
val denot = accessibleDenot(qualType, name, sig)
10211021
val owner = denot.symbol.maybeOwner
1022-
if (owner.isPackageObject && pre.termSymbol.is(Package))
1023-
pre = pre.select(owner.sourceModule)
1022+
if (owner.isPackageObject && qualType.termSymbol.is(Package))
1023+
qualType = qualType.select(owner.sourceModule)
10241024
val tpe = name match {
1025-
case name: TypeName => TypeRef(pre, name, denot)
1026-
case name: TermName => TermRef(pre, name, denot)
1025+
case name: TypeName => TypeRef(qualType, name, denot)
1026+
case name: TermName => TermRef(qualType, name, denot)
10271027
}
10281028
ConstFold(untpd.Select(qual, name).withType(tpe))
10291029
}
@@ -1033,10 +1033,11 @@ class TreeUnpickler(reader: TastyReader,
10331033
(untpd.Ident(qual.name).withSpan(qual.span), qual.tpe.asInstanceOf[TypeRef])
10341034
}
10351035

1036-
def accessibleDenot(pre: Type, name: Name, sig: Signature) = {
1037-
val d = pre.member(name).atSignature(sig)
1036+
def accessibleDenot(qualType: Type, name: Name, sig: Signature) = {
1037+
val pre = ctx.typeAssigner.maybeSkolemizePrefix(qualType, name)
1038+
val d = qualType.findMember(name, pre).atSignature(sig)
10381039
if (!d.symbol.exists || d.symbol.isAccessibleFrom(pre)) d
1039-
else pre.nonPrivateMember(name).atSignature(sig)
1040+
else qualType.findMember(name, pre, excluded = Private).atSignature(sig)
10401041
}
10411042

10421043
def readSimpleTerm(): Tree = tag match {

compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala

+14-1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,17 @@ trait TypeAssigner {
226226
test(tpe, true)
227227
}
228228

229+
/** Return a potentially skolemized version of `qualTpe` to be used
230+
* as a prefix when selecting `name`.
231+
*
232+
* @see QualSkolemType, TypeOps#asSeenFrom
233+
*/
234+
def maybeSkolemizePrefix(qualType: Type, name: Name)(implicit ctx: Context): Type =
235+
if (name.isTermName && !ctx.isLegalPrefix(qualType))
236+
QualSkolemType(qualType)
237+
else
238+
qualType
239+
229240
/** The type of the selection `tree`, where `qual1` is the typed qualifier part. */
230241
def selectionType(tree: untpd.RefTree, qual1: Tree)(implicit ctx: Context): Type = {
231242
var qualType = qual1.tpe.widenIfUnstable
@@ -234,8 +245,10 @@ trait TypeAssigner {
234245
qualType = errorType(em"$qualType takes type parameters", qual1.sourcePos)
235246
else if (!qualType.isInstanceOf[TermType])
236247
qualType = errorType(em"$qualType is illegal as a selection prefix", qual1.sourcePos)
248+
237249
val name = tree.name
238-
val mbr = qualType.member(name)
250+
val pre = maybeSkolemizePrefix(qualType, name)
251+
val mbr = qualType.findMember(name, pre)
239252
if (reallyExists(mbr))
240253
qualType.select(name, mbr)
241254
else if (qualType.derivesFrom(defn.DynamicClass) && name.isTermName && !Dynamic.isDynamicMethod(name))

tests/pos/i6199a.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class Encoder[T] { def encode(v: T): String = v.toString }
2+
case class ValueWithEncoder[T](value: T, encoder: Encoder[T])
3+
4+
object Test {
5+
val a: Seq[ValueWithEncoder[_]] = Seq.empty
6+
val b = a.map((value, encoder) => encoder.encode(value))
7+
val c: Seq[String] = b
8+
}

tests/pos/i6199b.scala

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
trait Foo {
2+
type State
3+
def bar(f: Bar[State] => Bar[State]): Foo = this
4+
}
5+
object Foo {
6+
def unit: Foo = new Foo {
7+
type State = Any
8+
}
9+
def doBar: Foo = unit.bar(bar => bar)
10+
}
11+
class Bar[+A]

tests/pos/i6199c.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
trait Foo[State] {
2+
def bar(f: Bar[State] => Bar[State]): Foo[_] = this
3+
}
4+
object Foo {
5+
def unit: Foo[_] = new Foo[Any] {}
6+
def doBar: Foo[_] = unit.bar(bar => bar)
7+
}
8+
class Bar[+A]

0 commit comments

Comments
 (0)