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

Heal member-select on opaque reference #19730

Merged
merged 3 commits into from
Jun 12, 2024
Merged
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
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
sjrd marked this conversation as resolved.
Show resolved Hide resolved
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
}
Loading