From 6230405edb0d356469bd3d36560431c9e3d8f72a Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 4 Mar 2024 13:06:07 +0000 Subject: [PATCH 1/3] Rename to typedSelectWithAdapt --- .../src/dotty/tools/dotc/typer/Typer.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 1c779ac050fa..99fcc8cb3d81 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -724,7 +724,7 @@ 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] @@ -753,7 +753,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // 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) + return typedSelectWithAdapt(tree, pt, qual) // Otherwise, try to expand a named tuple selection val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes @@ -769,7 +769,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // 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))) + return typedSelectWithAdapt(tree, pt, qual.cast(defn.tupleType(elems))) // Otherwise try an extension or conversion if selName.isTermName then @@ -796,7 +796,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if qual1.tpe.isSmallGenericTuple then gadts.println(i"Tuple member selection healed by GADT approximation") - return typedSelect(tree, pt, qual1) + return typedSelectWithAdapt(tree, pt, qual1) val tree2 = tryExtensionOrConversion(tree1, pt, IgnoredProto(pt), qual1, ctx.typerState.ownedVars, this, inSelect = true) if !tree2.isEmpty then @@ -805,7 +805,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // 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) + return typedSelectWithAdapt(tree, pt, qual) def dynamicSelect(pt: Type) = val tree2 = cpy.Select(tree0)(untpd.TypedSplice(qual), selName) @@ -854,7 +854,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer inaccessibleErrorType(rawType, superAccess, tree.srcPos) case _ => notAMemberErrorType(tree, qual, pt)) - end typedSelect + end typedSelectWithAdapt /** Expand a selection A.m on a context bound companion A with type * `[ref_1 | ... | ref_N]` as described by @@ -906,7 +906,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 @@ -938,7 +938,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) @@ -3879,7 +3879,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 From c477ceab6b79c9021fa9f5bad4d9910db51c0cc1 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 4 Mar 2024 13:06:07 +0000 Subject: [PATCH 2/3] Reorganise typedSelectWithAdapt Prior to the next commit, I broke up the logic into internal methods, so some can be reused, consuming then in a big Tree#orElse chain. I also took the opportunity to rename the method, to more easily distinguish it from the other typedSelect. --- .../src/dotty/tools/dotc/typer/Typer.scala | 204 ++++++++++-------- 1 file changed, 114 insertions(+), 90 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 99fcc8cb3d81..5892baf5a2db 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -729,131 +729,155 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer 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 typedSelectWithAdapt(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, 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 typedSelectWithAdapt(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 typedSelectWithAdapt(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 typedSelectWithAdapt(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)) + 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(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 From 4443395a1de7317e5b3b0349f96d5c0102d5941f Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 19 Feb 2024 11:01:41 +0000 Subject: [PATCH 3/3] Heal member-select on opaque reference When the prefix of an opaque isn't the .this reference of the module class, then its RHS isn't visible. TypeComparer uses ctx.owner to "heal" or "lift" this type such that it is. We reuse that logic for member selection. --- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 13 ++++++++++ tests/pos/i19609.orig.scala | 12 ++++++++++ tests/pos/i19609.scala | 24 +++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/pos/i19609.orig.scala create mode 100644 tests/pos/i19609.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6e360faa322d..fd1deebbf8c2 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -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 diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5892baf5a2db..dbc9818abf23 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -758,6 +758,18 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer 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 def tryNamedTupleSelection() = val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes @@ -869,6 +881,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer tryType(tree, qual, rawType) .orElse(trySimplifyApply()) .orElse(tryInstantiateTypeVar()) + .orElse(tryLiftToThis()) .orElse(tryNamedTupleSelection()) .orElse(trySmallGenericTuple(qual, withCast = true)) .orElse(tryExt(tree, qual)) diff --git a/tests/pos/i19609.orig.scala b/tests/pos/i19609.orig.scala new file mode 100644 index 000000000000..62622075dbed --- /dev/null +++ b/tests/pos/i19609.orig.scala @@ -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 +} diff --git a/tests/pos/i19609.scala b/tests/pos/i19609.scala new file mode 100644 index 000000000000..0879fa16c7cf --- /dev/null +++ b/tests/pos/i19609.scala @@ -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 +}