Skip to content

Commit 82ac0dd

Browse files
authored
Heal member-select on opaque reference (#19730)
2 parents e2dfea3 + 4443395 commit 82ac0dd

File tree

4 files changed

+169
-96
lines changed

4 files changed

+169
-96
lines changed

Diff for: compiler/src/dotty/tools/dotc/core/TypeComparer.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1596,7 +1596,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
15961596
* Note: It would be legal to do the lifting also if M does not contain opaque types,
15971597
* but in this case the retries in tryLiftedToThis would be redundant.
15981598
*/
1599-
private def liftToThis(tp: Type): Type = {
1599+
def liftToThis(tp: Type): Type = {
16001600

16011601
def findEnclosingThis(moduleClass: Symbol, from: Symbol): Type =
16021602
if ((from.owner eq moduleClass) && from.isPackageObject && from.is(Opaque)) from.thisType

Diff for: compiler/src/dotty/tools/dotc/typer/Typer.scala

+132-95
Original file line numberDiff line numberDiff line change
@@ -724,137 +724,174 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
724724
then
725725
report.error(StableIdentPattern(tree, pt), tree.srcPos)
726726

727-
def typedSelect(tree0: untpd.Select, pt: Type, qual: Tree)(using Context): Tree =
727+
def typedSelectWithAdapt(tree0: untpd.Select, pt: Type, qual: Tree)(using Context): Tree =
728728
val selName = tree0.name
729729
val tree = cpy.Select(tree0)(qual, selName)
730730
val superAccess = qual.isInstanceOf[Super]
731731
val rawType = selectionType(tree, qual)
732-
val checkedType = accessibleType(rawType, superAccess)
733732

734-
def finish(tree: untpd.Select, qual: Tree, checkedType: Type): Tree =
735-
val select = toNotNullTermRef(assignType(tree, checkedType), pt)
736-
if selName.isTypeName then checkStable(qual.tpe, qual.srcPos, "type prefix")
737-
checkLegalValue(select, pt)
738-
ConstFold(select)
739-
740-
// If regular selection is typeable, we are done
741-
if checkedType.exists then
742-
return finish(tree, qual, checkedType)
733+
def tryType(tree: untpd.Select, qual: Tree, rawType: Type) =
734+
val checkedType = accessibleType(rawType, superAccess)
735+
// If regular selection is typeable, we are done
736+
if checkedType.exists then
737+
val select = toNotNullTermRef(assignType(tree, checkedType), pt)
738+
if selName.isTypeName then checkStable(qual.tpe, qual.srcPos, "type prefix")
739+
checkLegalValue(select, pt)
740+
ConstFold(select)
741+
else EmptyTree
743742

744743
// Otherwise, simplify `m.apply(...)` to `m(...)`
745-
if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then
746-
return qual
744+
def trySimplifyApply() =
745+
if selName == nme.apply && qual.tpe.widen.isInstanceOf[MethodType] then
746+
qual
747+
else EmptyTree
747748

748749
// Otherwise, if there's a simply visible type variable in the result, try again
749750
// with a more defined qualifier type. There's a second trial where we try to instantiate
750751
// all type variables in `qual.tpe.widen`, but that is done only after we search for
751752
// extension methods or conversions.
752-
if couldInstantiateTypeVar(qual.tpe.widen) then
753-
// there's a simply visible type variable in the result; try again with a more defined qualifier type
754-
// There's a second trial where we try to instantiate all type variables in `qual.tpe.widen`,
755-
// but that is done only after we search for extension methods or conversions.
756-
return typedSelect(tree, pt, qual)
753+
def tryInstantiateTypeVar() =
754+
if couldInstantiateTypeVar(qual.tpe.widen) then
755+
// there's a simply visible type variable in the result; try again with a more defined qualifier type
756+
// There's a second trial where we try to instantiate all type variables in `qual.tpe.widen`,
757+
// but that is done only after we search for extension methods or conversions.
758+
typedSelectWithAdapt(tree, pt, qual)
759+
else EmptyTree
760+
761+
// Otherwise, heal member selection on an opaque reference,
762+
// reusing the logic in TypeComparer.
763+
def tryLiftToThis() =
764+
val wtp = qual.tpe.widen
765+
val liftedTp = comparing(_.liftToThis(wtp))
766+
if liftedTp ne wtp then
767+
val qual1 = qual.cast(liftedTp)
768+
val tree1 = cpy.Select(tree0)(qual1, selName)
769+
val rawType1 = selectionType(tree1, qual1)
770+
tryType(tree1, qual1, rawType1)
771+
else EmptyTree
757772

758773
// Otherwise, try to expand a named tuple selection
759-
val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes
760-
val nameIdx = namedTupleElems.indexWhere(_._1 == selName)
761-
if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then
762-
return typed(
763-
untpd.Apply(
764-
untpd.Select(untpd.TypedSplice(qual), nme.apply),
765-
untpd.Literal(Constant(nameIdx))),
766-
pt)
774+
def tryNamedTupleSelection() =
775+
val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes
776+
val nameIdx = namedTupleElems.indexWhere(_._1 == selName)
777+
if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then
778+
typed(
779+
untpd.Apply(
780+
untpd.Select(untpd.TypedSplice(qual), nme.apply),
781+
untpd.Literal(Constant(nameIdx))),
782+
pt)
783+
else EmptyTree
767784

768785
// Otherwise, map combinations of A *: B *: .... EmptyTuple with nesting levels <= 22
769786
// to the Tuple class of the right arity and select from that one
770-
if qual.tpe.isSmallGenericTuple then
771-
val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil)
772-
return typedSelect(tree, pt, qual.cast(defn.tupleType(elems)))
787+
def trySmallGenericTuple(qual: Tree, withCast: Boolean) =
788+
if qual.tpe.isSmallGenericTuple then
789+
if withCast then
790+
val elems = qual.tpe.widenTermRefExpr.tupleElementTypes.getOrElse(Nil)
791+
typedSelectWithAdapt(tree, pt, qual.cast(defn.tupleType(elems)))
792+
else
793+
typedSelectWithAdapt(tree, pt, qual)
794+
else EmptyTree
773795

774796
// Otherwise try an extension or conversion
775-
if selName.isTermName then
776-
val tree1 = tryExtensionOrConversion(
777-
tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true)
778-
if !tree1.isEmpty then
779-
return tree1
797+
def tryExt(tree: untpd.Select, qual: Tree) =
798+
if selName.isTermName then
799+
tryExtensionOrConversion(
800+
tree, pt, IgnoredProto(pt), qual, ctx.typerState.ownedVars, this, inSelect = true)
801+
else EmptyTree
780802

781803
// Otherwise, try a GADT approximation if we're trying to select a member
782-
// Member lookup cannot take GADTs into account b/c of cache, so we
783-
// approximate types based on GADT constraints instead. For an example,
784-
// see MemberHealing in gadt-approximation-interaction.scala.
785-
if ctx.gadt.isNarrowing then
786-
val wtp = qual.tpe.widen
787-
gadts.println(i"Trying to heal member selection by GADT-approximating $wtp")
788-
val gadtApprox = Inferencing.approximateGADT(wtp)
789-
gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox")
790-
val qual1 = qual.cast(gadtApprox)
791-
val tree1 = cpy.Select(tree0)(qual1, selName)
792-
val checkedType1 = accessibleType(selectionType(tree1, qual1), superAccess = false)
793-
if checkedType1.exists then
794-
gadts.println(i"Member selection healed by GADT approximation")
795-
return finish(tree1, qual1, checkedType1)
796-
797-
if qual1.tpe.isSmallGenericTuple then
798-
gadts.println(i"Tuple member selection healed by GADT approximation")
799-
return typedSelect(tree, pt, qual1)
800-
801-
val tree2 = tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true)
802-
if !tree2.isEmpty then
803-
return tree2
804+
def tryGadt() =
805+
if ctx.gadt.isNarrowing then
806+
// Member lookup cannot take GADTs into account b/c of cache, so we
807+
// approximate types based on GADT constraints instead. For an example,
808+
// see MemberHealing in gadt-approximation-interaction.scala.
809+
val wtp = qual.tpe.widen
810+
gadts.println(i"Trying to heal member selection by GADT-approximating $wtp")
811+
val gadtApprox = Inferencing.approximateGADT(wtp)
812+
gadts.println(i"GADT-approximated $wtp ~~ $gadtApprox")
813+
val qual1 = qual.cast(gadtApprox)
814+
val tree1 = cpy.Select(tree0)(qual1, selName)
815+
tryType(tree1, qual1, selectionType(tree1, qual1))
816+
.orElse(trySmallGenericTuple(qual1, withCast = false))
817+
.orElse(tryExt(tree1, qual1))
818+
else EmptyTree
804819

805820
// Otherwise, if there are uninstantiated type variables in the qualifier type,
806821
// instantiate them and try again
807-
if canDefineFurther(qual.tpe.widen) then
808-
return typedSelect(tree, pt, qual)
822+
def tryDefineFurther() =
823+
if canDefineFurther(qual.tpe.widen) then
824+
typedSelectWithAdapt(tree, pt, qual)
825+
else EmptyTree
809826

810827
def dynamicSelect(pt: Type) =
811-
val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName)
812-
if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then
813-
assignType(tree2, TryDynamicCallType)
814-
else
815-
typedDynamicSelect(tree2, Nil, pt)
828+
val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName)
829+
if pt.isInstanceOf[FunOrPolyProto] || pt == LhsProto then
830+
assignType(tree2, TryDynamicCallType)
831+
else
832+
typedDynamicSelect(tree2, Nil, pt)
816833

817834
// Otherwise, if the qualifier derives from class Dynamic, expand to a
818835
// dynamic dispatch using selectDynamic or applyDynamic
819-
if qual.tpe.derivesFrom(defn.DynamicClass) && selName.isTermName && !isDynamicExpansion(tree) then
820-
return dynamicSelect(pt)
836+
def tryDynamic() =
837+
if qual.tpe.derivesFrom(defn.DynamicClass) && selName.isTermName && !isDynamicExpansion(tree) then
838+
dynamicSelect(pt)
839+
else EmptyTree
821840

822841
// Otherwise, if the qualifier derives from class Selectable,
823842
// and the selector name matches one of the element of the `Fields` type member,
824843
// and the selector is not assigned to,
825844
// expand to a typed dynamic dispatch using selectDynamic wrapped in a cast
826-
if qual.tpe.derivesFrom(defn.SelectableClass) && !isDynamicExpansion(tree)
827-
&& pt != LhsProto
828-
then
829-
val pre = if !TypeOps.isLegalPrefix(qual.tpe) then SkolemType(qual.tpe) else qual.tpe
830-
val fieldsType = pre.select(tpnme.Fields).dealias.simplified
831-
val fields = fieldsType.namedTupleElementTypes
832-
typr.println(i"try dyn select $qual, $selName, $fields")
833-
fields.find(_._1 == selName) match
834-
case Some((_, fieldType)) =>
835-
val dynSelected = dynamicSelect(fieldType)
836-
dynSelected match
837-
case Apply(sel: Select, _) if !sel.denot.symbol.exists =>
838-
// Reject corner case where selectDynamic needs annother selectDynamic to be called. E.g. as in neg/unselectable-fields.scala.
839-
report.error(i"Cannot use selectDynamic here since it needs another selectDynamic to be invoked", tree.srcPos)
840-
case _ =>
841-
return dynSelected.ensureConforms(fieldType)
842-
case _ =>
845+
def trySelectable() =
846+
if qual.tpe.derivesFrom(defn.SelectableClass) && !isDynamicExpansion(tree)
847+
&& pt != LhsProto
848+
then
849+
val pre = if !TypeOps.isLegalPrefix(qual.tpe) then SkolemType(qual.tpe) else qual.tpe
850+
val fieldsType = pre.select(tpnme.Fields).dealias.simplified
851+
val fields = fieldsType.namedTupleElementTypes
852+
typr.println(i"try dyn select $qual, $selName, $fields")
853+
fields.find(_._1 == selName) match
854+
case Some((_, fieldType)) =>
855+
val dynSelected = dynamicSelect(fieldType)
856+
dynSelected match
857+
case Apply(sel: Select, _) if !sel.denot.symbol.exists =>
858+
// Reject corner case where selectDynamic needs annother selectDynamic to be called. E.g. as in neg/unselectable-fields.scala.
859+
report.error(i"Cannot use selectDynamic here since it needs another selectDynamic to be invoked", tree.srcPos)
860+
case _ =>
861+
dynSelected.ensureConforms(fieldType)
862+
case _ => EmptyTree
863+
else EmptyTree
843864

844865
// Otherwise, if the qualifier is a context bound companion, handle
845866
// by selecting a witness in typedCBSelect
846-
if qual.tpe.typeSymbol == defn.CBCompanion then
847-
val witnessSelection = typedCBSelect(tree0, pt, qual)
848-
if !witnessSelection.isEmpty then return witnessSelection
867+
def tryCBCompanion() =
868+
if qual.tpe.typeSymbol == defn.CBCompanion then
869+
typedCBSelect(tree0, pt, qual)
870+
else EmptyTree
849871

850872
// Otherwise, report an error
851-
assignType(tree,
852-
rawType match
853-
case rawType: NamedType =>
854-
inaccessibleErrorType(rawType, superAccess, tree.srcPos)
855-
case _ =>
856-
notAMemberErrorType(tree, qual, pt))
857-
end typedSelect
873+
def reportAnError() =
874+
assignType(tree,
875+
rawType match
876+
case rawType: NamedType =>
877+
inaccessibleErrorType(rawType, superAccess, tree.srcPos)
878+
case _ =>
879+
notAMemberErrorType(tree, qual, pt))
880+
881+
tryType(tree, qual, rawType)
882+
.orElse(trySimplifyApply())
883+
.orElse(tryInstantiateTypeVar())
884+
.orElse(tryLiftToThis())
885+
.orElse(tryNamedTupleSelection())
886+
.orElse(trySmallGenericTuple(qual, withCast = true))
887+
.orElse(tryExt(tree, qual))
888+
.orElse(tryGadt())
889+
.orElse(tryDefineFurther())
890+
.orElse(tryDynamic())
891+
.orElse(trySelectable())
892+
.orElse(tryCBCompanion())
893+
.orElse(reportAnError())
894+
end typedSelectWithAdapt
858895

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

943980
def javaSelection(qual: Tree)(using Context) =
944981
val tree1 = assignType(cpy.Select(tree)(qual, tree.name), qual)
@@ -3879,7 +3916,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
38793916
if isExtension then return found
38803917
else
38813918
checkImplicitConversionUseOK(found, selProto)
3882-
return withoutMode(Mode.ImplicitsEnabled)(typedSelect(tree, pt, found))
3919+
return withoutMode(Mode.ImplicitsEnabled)(typedSelectWithAdapt(tree, pt, found))
38833920
case failure: SearchFailure =>
38843921
if failure.isAmbiguous then
38853922
return

Diff for: tests/pos/i19609.orig.scala

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
object o {
2+
opaque type T = String
3+
4+
summon[o.T =:= T] // OK
5+
summon[o.T =:= String] // OK
6+
7+
def test1(t: T): Int =
8+
t.length // OK
9+
10+
def test2(t: o.T): Int =
11+
t.length // Error: value length is not a member of Playground.o.T
12+
}

Diff for: tests/pos/i19609.scala

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
object o { u =>
2+
opaque type T = String
3+
4+
def st = summon[String =:= T]
5+
def su = summon[String =:= u.T]
6+
def so = summon[String =:= o.T]
7+
8+
def ts = summon[T =:= String]
9+
def tu = summon[T =:= u.T]
10+
def to = summon[T =:= o.T]
11+
12+
def us = summon[u.T =:= String]
13+
def ut = summon[u.T =:= T]
14+
def uo = summon[u.T =:= o.T]
15+
16+
def os = summon[o.T =:= String]
17+
def ot = summon[o.T =:= T]
18+
def ou = summon[o.T =:= u.T]
19+
20+
def ms(x: String): Int = x.length // ok
21+
def mt(x: T): Int = x.length // ok
22+
def mu(x: u.T): Int = x.length // ok
23+
def mo(x: o.T): Int = x.length // was: error: value length is not a member of o.T
24+
}

0 commit comments

Comments
 (0)