Skip to content

Commit

Permalink
Heal member-select on opaque reference (#19730)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand authored Jun 12, 2024
2 parents e2dfea3 + 4443395 commit 82ac0dd
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 96 deletions.
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1596,7 +1596,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
* Note: It would be legal to do the lifting also if M does not contain opaque types,
* but in this case the retries in tryLiftedToThis would be redundant.
*/
private def liftToThis(tp: Type): Type = {
def liftToThis(tp: Type): Type = {

def findEnclosingThis(moduleClass: Symbol, from: Symbol): Type =
if ((from.owner eq moduleClass) && from.isPackageObject && from.is(Opaque)) from.thisType
Expand Down
227 changes: 132 additions & 95 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -724,137 +724,174 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
then
report.error(StableIdentPattern(tree, pt), tree.srcPos)

def typedSelect(tree0: untpd.Select, pt: Type, qual: Tree)(using Context): Tree =
def typedSelectWithAdapt(tree0: untpd.Select, pt: Type, qual: Tree)(using Context): Tree =
val selName = tree0.name
val tree = cpy.Select(tree0)(qual, selName)
val superAccess = qual.isInstanceOf[Super]
val rawType = selectionType(tree, qual)
val checkedType = accessibleType(rawType, superAccess)

def finish(tree: untpd.Select, qual: Tree, checkedType: Type): Tree =
val select = toNotNullTermRef(assignType(tree, checkedType), pt)
if selName.isTypeName then checkStable(qual.tpe, qual.srcPos, "type prefix")
checkLegalValue(select, pt)
ConstFold(select)

// If regular selection is typeable, we are done
if checkedType.exists then
return finish(tree, qual, checkedType)
def tryType(tree: untpd.Select, qual: Tree, rawType: Type) =
val checkedType = accessibleType(rawType, superAccess)
// If regular selection is typeable, we are done
if checkedType.exists then
val select = toNotNullTermRef(assignType(tree, checkedType), pt)
if selName.isTypeName then checkStable(qual.tpe, qual.srcPos, "type prefix")
checkLegalValue(select, pt)
ConstFold(select)
else EmptyTree

// Otherwise, simplify `m.apply(...)` to `m(...)`
if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then
return qual
def trySimplifyApply() =
if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then
qual
else EmptyTree

// Otherwise, if there's a simply visible type variable in the result, try again
// with a more defined qualifier type. There's a second trial where we try to instantiate
// all type variables in `qual.tpe.widen`, but that is done only after we search for
// extension methods or conversions.
if couldInstantiateTypeVar(qual.tpe.widen) then
// there's a simply visible type variable in the result; try again with a more defined qualifier type
// There's a second trial where we try to instantiate all type variables in `qual.tpe.widen`,
// but that is done only after we search for extension methods or conversions.
return typedSelect(tree, pt, qual)
def tryInstantiateTypeVar() =
if couldInstantiateTypeVar(qual.tpe.widen) then
// there's a simply visible type variable in the result; try again with a more defined qualifier type
// There's a second trial where we try to instantiate all type variables in `qual.tpe.widen`,
// but that is done only after we search for extension methods or conversions.
typedSelectWithAdapt(tree, pt, qual)
else EmptyTree

// Otherwise, heal member selection on an opaque reference,
// reusing the logic in TypeComparer.
def tryLiftToThis() =
val wtp = qual.tpe.widen
val liftedTp = comparing(_.liftToThis(wtp))
if liftedTp ne wtp then
val qual1 = qual.cast(liftedTp)
val tree1 = cpy.Select(tree0)(qual1, selName)
val rawType1 = selectionType(tree1, qual1)
tryType(tree1, qual1, rawType1)
else EmptyTree

// Otherwise, try to expand a named tuple selection
val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes
val nameIdx = namedTupleElems.indexWhere(_._1 == selName)
if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then
return typed(
untpd.Apply(
untpd.Select(untpd.TypedSplice(qual), nme.apply),
untpd.Literal(Constant(nameIdx))),
pt)
def tryNamedTupleSelection() =
val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes
val nameIdx = namedTupleElems.indexWhere(_._1 == selName)
if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then
typed(
untpd.Apply(
untpd.Select(untpd.TypedSplice(qual), nme.apply),
untpd.Literal(Constant(nameIdx))),
pt)
else EmptyTree

// Otherwise, map combinations of A *: B *: .... EmptyTuple with nesting levels <= 22
// to the Tuple class of the right arity and select from that one
if qual.tpe.isSmallGenericTuple then
val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil)
return typedSelect(tree, pt, qual.cast(defn.tupleType(elems)))
def trySmallGenericTuple(qual: Tree, withCast: Boolean) =
if qual.tpe.isSmallGenericTuple then
if withCast then
val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil)
typedSelectWithAdapt(tree, pt, qual.cast(defn.tupleType(elems)))
else
typedSelectWithAdapt(tree, pt, qual)
else EmptyTree

// Otherwise try an extension or conversion
if selName.isTermName then
val tree1 = tryExtensionOrConversion(
tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true)
if !tree1.isEmpty then
return tree1
def tryExt(tree: untpd.Select, qual: Tree) =
if selName.isTermName then
tryExtensionOrConversion(
tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true)
else EmptyTree

// Otherwise, try a GADT approximation if we're trying to select a member
// Member lookup cannot take GADTs into account b/c of cache, so we
// approximate types based on GADT constraints instead. For an example,
// see MemberHealing in gadt-approximation-interaction.scala.
if ctx.gadt.isNarrowing then
val wtp = qual.tpe.widen
gadts.println(i"Trying to heal member selection by GADT-approximating $wtp")
val gadtApprox = Inferencing.approximateGADT(wtp)
gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox")
val qual1 = qual.cast(gadtApprox)
val tree1 = cpy.Select(tree0)(qual1, selName)
val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false)
if checkedType1.exists then
gadts.println(i"Member selection healed by GADT approximation")
return finish(tree1, qual1, checkedType1)

if qual1.tpe.isSmallGenericTuple then
gadts.println(i"Tuple member selection healed by GADT approximation")
return typedSelect(tree, pt, qual1)

val tree2 = tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true)
if !tree2.isEmpty then
return tree2
def tryGadt() =
if ctx.gadt.isNarrowing then
// Member lookup cannot take GADTs into account b/c of cache, so we
// approximate types based on GADT constraints instead. For an example,
// see MemberHealing in gadt-approximation-interaction.scala.
val wtp = qual.tpe.widen
gadts.println(i"Trying to heal member selection by GADT-approximating $wtp")
val gadtApprox = Inferencing.approximateGADT(wtp)
gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox")
val qual1 = qual.cast(gadtApprox)
val tree1 = cpy.Select(tree0)(qual1, selName)
tryType(tree1, qual1, selectionType(tree1, qual1))
.orElse(trySmallGenericTuple(qual1, withCast = false))
.orElse(tryExt(tree1, qual1))
else EmptyTree

// Otherwise, if there are uninstantiated type variables in the qualifier type,
// instantiate them and try again
if canDefineFurther(qual.tpe.widen) then
return typedSelect(tree, pt, qual)
def tryDefineFurther() =
if canDefineFurther(qual.tpe.widen) then
typedSelectWithAdapt(tree, pt, qual)
else EmptyTree

def dynamicSelect(pt: Type) =
val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName)
if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then
assignType(tree2, TryDynamicCallType)
else
typedDynamicSelect(tree2, Nil, pt)
val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName)
if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then
assignType(tree2, TryDynamicCallType)
else
typedDynamicSelect(tree2, Nil, pt)

// Otherwise, if the qualifier derives from class Dynamic, expand to a
// dynamic dispatch using selectDynamic or applyDynamic
if qual.tpe.derivesFrom(defn.DynamicClass) && selName.isTermName && !isDynamicExpansion(tree) then
return dynamicSelect(pt)
def tryDynamic() =
if qual.tpe.derivesFrom(defn.DynamicClass) && selName.isTermName && !isDynamicExpansion(tree) then
dynamicSelect(pt)
else EmptyTree

// Otherwise, if the qualifier derives from class Selectable,
// and the selector name matches one of the element of the `Fields` type member,
// and the selector is not assigned to,
// expand to a typed dynamic dispatch using selectDynamic wrapped in a cast
if qual.tpe.derivesFrom(defn.SelectableClass) && !isDynamicExpansion(tree)
&& pt != LhsProto
then
val pre = if !TypeOps.isLegalPrefix(qual.tpe) then SkolemType(qual.tpe) else qual.tpe
val fieldsType = pre.select(tpnme.Fields).dealias.simplified
val fields = fieldsType.namedTupleElementTypes
typr.println(i"try dyn select $qual, $selName, $fields")
fields.find(_._1 == selName) match
case Some((_, fieldType)) =>
val dynSelected = dynamicSelect(fieldType)
dynSelected match
case Apply(sel: Select, _) if !sel.denot.symbol.exists =>
// Reject corner case where selectDynamic needs annother selectDynamic to be called. E.g. as in neg/unselectable-fields.scala.
report.error(i"Cannot use selectDynamic here since it needs another selectDynamic to be invoked", tree.srcPos)
case _ =>
return dynSelected.ensureConforms(fieldType)
case _ =>
def trySelectable() =
if qual.tpe.derivesFrom(defn.SelectableClass) && !isDynamicExpansion(tree)
&& pt != LhsProto
then
val pre = if !TypeOps.isLegalPrefix(qual.tpe) then SkolemType(qual.tpe) else qual.tpe
val fieldsType = pre.select(tpnme.Fields).dealias.simplified
val fields = fieldsType.namedTupleElementTypes
typr.println(i"try dyn select $qual, $selName, $fields")
fields.find(_._1 == selName) match
case Some((_, fieldType)) =>
val dynSelected = dynamicSelect(fieldType)
dynSelected match
case Apply(sel: Select, _) if !sel.denot.symbol.exists =>
// Reject corner case where selectDynamic needs annother selectDynamic to be called. E.g. as in neg/unselectable-fields.scala.
report.error(i"Cannot use selectDynamic here since it needs another selectDynamic to be invoked", tree.srcPos)
case _ =>
dynSelected.ensureConforms(fieldType)
case _ => EmptyTree
else EmptyTree

// Otherwise, if the qualifier is a context bound companion, handle
// by selecting a witness in typedCBSelect
if qual.tpe.typeSymbol == defn.CBCompanion then
val witnessSelection = typedCBSelect(tree0, pt, qual)
if !witnessSelection.isEmpty then return witnessSelection
def tryCBCompanion() =
if qual.tpe.typeSymbol == defn.CBCompanion then
typedCBSelect(tree0, pt, qual)
else EmptyTree

// Otherwise, report an error
assignType(tree,
rawType match
case rawType: NamedType =>
inaccessibleErrorType(rawType, superAccess, tree.srcPos)
case _ =>
notAMemberErrorType(tree, qual, pt))
end typedSelect
def reportAnError() =
assignType(tree,
rawType match
case rawType: NamedType =>
inaccessibleErrorType(rawType, superAccess, tree.srcPos)
case _ =>
notAMemberErrorType(tree, qual, pt))

tryType(tree, qual, rawType)
.orElse(trySimplifyApply())
.orElse(tryInstantiateTypeVar())
.orElse(tryLiftToThis())
.orElse(tryNamedTupleSelection())
.orElse(trySmallGenericTuple(qual, withCast = true))
.orElse(tryExt(tree, qual))
.orElse(tryGadt())
.orElse(tryDefineFurther())
.orElse(tryDynamic())
.orElse(trySelectable())
.orElse(tryCBCompanion())
.orElse(reportAnError())
end typedSelectWithAdapt

/** Expand a selection A.m on a context bound companion A with type
* `<context-bound-companion>[ref_1 | ... | ref_N]` as described by
Expand Down Expand Up @@ -906,7 +943,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
case witness: TermRef =>
val altQual = tpd.ref(witness).withSpan(qual.span)
val altCtx = ctx.fresh.setNewTyperState()
val alt = typedSelect(tree, pt, altQual)(using altCtx)
val alt = typedSelectWithAdapt(tree, pt, altQual)(using altCtx)
def current = (alt, altCtx.typerState, witness)
if altCtx.reporter.hasErrors then prevs
else
Expand Down Expand Up @@ -938,7 +975,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
if ctx.isJava then
javaSelection(qual)
else
typedSelect(tree, pt, qual).withSpan(tree.span).computeNullable()
typedSelectWithAdapt(tree, pt, qual).withSpan(tree.span).computeNullable()

def javaSelection(qual: Tree)(using Context) =
val tree1 = assignType(cpy.Select(tree)(qual, tree.name), qual)
Expand Down Expand Up @@ -3879,7 +3916,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
if isExtension then return found
else
checkImplicitConversionUseOK(found, selProto)
return withoutMode(Mode.ImplicitsEnabled)(typedSelect(tree, pt, found))
return withoutMode(Mode.ImplicitsEnabled)(typedSelectWithAdapt(tree, pt, found))
case failure: SearchFailure =>
if failure.isAmbiguous then
return
Expand Down
12 changes: 12 additions & 0 deletions tests/pos/i19609.orig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
object o {
opaque type T = String

summon[o.T =:= T] // OK
summon[o.T =:= String] // OK

def test1(t: T): Int =
t.length // OK

def test2(t: o.T): Int =
t.length // Error: value length is not a member of Playground.o.T
}
24 changes: 24 additions & 0 deletions tests/pos/i19609.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
object o { u =>
opaque type T = String

def st = summon[String =:= T]
def su = summon[String =:= u.T]
def so = summon[String =:= o.T]

def ts = summon[T =:= String]
def tu = summon[T =:= u.T]
def to = summon[T =:= o.T]

def us = summon[u.T =:= String]
def ut = summon[u.T =:= T]
def uo = summon[u.T =:= o.T]

def os = summon[o.T =:= String]
def ot = summon[o.T =:= T]
def ou = summon[o.T =:= u.T]

def ms(x: String): Int = x.length // ok
def mt(x: T): Int = x.length // ok
def mu(x: u.T): Int = x.length // ok
def mo(x: o.T): Int = x.length // was: error: value length is not a member of o.T
}

0 comments on commit 82ac0dd

Please sign in to comment.