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

Two fixes to handling of abstract types with cap bounds #22838

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
79 changes: 55 additions & 24 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,7 @@ class CheckCaptures extends Recheck, SymTransformer:
else if !owner.exists then false
else isPure(owner.info) && isPureContext(owner.owner, limit)

// Augment expeced capture set `erefs` by all references in actual capture
// Augment expected capture set `erefs` by all references in actual capture
// set `arefs` that are outside some `C.this.type` reference in `erefs` for an enclosing
// class `C`. If an added reference is not a ThisType itself, add it to the capture set
// (i.e. use set) of the `C`. This makes sure that any outer reference implicitly subsumed
Expand Down Expand Up @@ -1574,33 +1574,62 @@ class CheckCaptures extends Recheck, SymTransformer:
*/
def checkOverrides = new TreeTraverser:
class OverridingPairsCheckerCC(clazz: ClassSymbol, self: Type, tree: Tree)(using Context) extends OverridingPairsChecker(clazz, self):
/** Check subtype with box adaptation.
* This function is passed to RefChecks to check the compatibility of overriding pairs.
* @param sym symbol of the field definition that is being checked
*/
override def checkSubType(actual: Type, expected: Type)(using Context): Boolean =
val expected1 = alignDependentFunction(addOuterRefs(expected, actual, tree.srcPos), actual.stripCapturing)
val actual1 =
val saved = curEnv
try
curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv)
val adapted =
adaptBoxed(actual, expected1, tree, covariant = true, alwaysConst = true)
actual match
case _: MethodType =>
// We remove the capture set resulted from box adaptation for method types,
// since class methods are always treated as pure, and their captured variables
// are charged to the capture set of the class (which is already done during
// box adaptation).
adapted.stripCapturing
case _ => adapted
finally curEnv = saved
actual1 frozen_<:< expected1

/** Omit the check if one of {overriding,overridden} was nnot capture checked */
override def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean =
!setup.isPreCC(overriding) && !setup.isPreCC(overridden)

/** Perform box adaptation for override checking */
override def adaptOverridePair(member: Symbol, memberTp: Type, otherTp: Type)(using Context): Option[(Type, Type)] =
if member.isType then
memberTp match
case TypeAlias(_) =>
otherTp match
case otherTp: RealTypeBounds =>
if otherTp.hi.isBoxedCapturing || otherTp.lo.isBoxedCapturing then
Some((memberTp, otherTp.unboxed))
else otherTp.hi match
case hi @ CapturingType(parent: TypeRef, refs)
if parent.symbol == defn.Caps_CapSet && refs.isUniversal =>
Some((
memberTp,
otherTp.derivedTypeBounds(
otherTp.lo,
hi.derivedCapturingType(parent, root.Fresh().singletonCaptureSet))))
case _ => None
case _ => None
case _ => None
else memberTp match
case memberTp @ ExprType(memberRes) =>
adaptOverridePair(member, memberRes, otherTp) match
case Some((mres, otp)) => Some((memberTp.derivedExprType(mres), otp))
case None => None
case _ => otherTp match
case otherTp @ ExprType(otherRes) =>
adaptOverridePair(member, memberTp, otherRes) match
case Some((mtp, ores)) => Some((mtp, otherTp.derivedExprType(ores)))
case None => None
case _ =>
val expected1 = alignDependentFunction(addOuterRefs(otherTp, memberTp, tree.srcPos), memberTp.stripCapturing)
val actual1 =
val saved = curEnv
try
curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv)
val adapted =
adaptBoxed(memberTp, expected1, tree, covariant = true, alwaysConst = true)
memberTp match
case _: MethodType =>
// We remove the capture set resulted from box adaptation for method types,
// since class methods are always treated as pure, and their captured variables
// are charged to the capture set of the class (which is already done during
// box adaptation).
adapted.stripCapturing
case _ => adapted
finally curEnv = saved
if (actual1 eq memberTp) && (expected1 eq otherTp) then None
else Some((actual1, expected1))
end adaptOverridePair

override def checkInheritedTraitParameters: Boolean = false

/** Check that overrides don't change the @use or @consume status of their parameters */
Expand Down Expand Up @@ -1875,7 +1904,9 @@ class CheckCaptures extends Recheck, SymTransformer:
def traverse(t: Tree)(using Context) = t match
case tree: InferredTypeTree =>
case tree: New =>
case tree: TypeTree => checkAppliedTypesIn(tree.withType(tree.nuType))
case tree: TypeTree =>
CCState.withCapAsRoot:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should leave a comment to make sure we don't introduce unsoundness

checkAppliedTypesIn(tree.withType(tree.nuType))
case _ => traverseChildren(t)
checkApplied.traverse(unit)
end postCheck
Expand Down
31 changes: 19 additions & 12 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,20 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
def fail(msg: Message) =
if !tptToCheck.isEmpty then report.error(msg, tptToCheck.srcPos)

/** If C derives from Capability and we have a C^cs in source, we leave it as is
* instead of expanding it to C^{cap.rd}^cs. We do this by stripping capability-generated
* universal capture sets from the parent of a CapturingType.
*/
def stripImpliedCaptureSet(tp: Type): Type = tp match
case tp @ CapturingType(parent, refs)
if (refs eq CaptureSet.universalImpliedByCapability) && !tp.isBoxedCapturing =>
parent
case tp: AliasingBounds =>
tp.derivedAlias(stripImpliedCaptureSet(tp.alias))
case tp: RealTypeBounds =>
tp.derivedTypeBounds(stripImpliedCaptureSet(tp.lo), stripImpliedCaptureSet(tp.hi))
case _ => tp

object toCapturing extends DeepTypeMap, SetupTypeMap:
override def toString = "transformExplicitType"

Expand Down Expand Up @@ -367,16 +381,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
CapturingType(fntpe, cs, boxed = false)
else fntpe

/** If C derives from Capability and we have a C^cs in source, we leave it as is
* instead of expanding it to C^{cap.rd}^cs. We do this by stripping capability-generated
* universal capture sets from the parent of a CapturingType.
*/
def stripImpliedCaptureSet(tp: Type): Type = tp match
case tp @ CapturingType(parent, refs)
if (refs eq CaptureSet.universalImpliedByCapability) && !tp.isBoxedCapturing =>
parent
case _ => tp

/** Check that types extending SharedCapability don't have a `cap` in their capture set.
* TODO This is not enough.
* We need to also track that we cannot get exclusive capabilities in paths
Expand Down Expand Up @@ -456,8 +460,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
toCapturing.keepFunAliases = false
transform(tp1)
else tp1
if freshen then root.capToFresh(tp2).tap(addOwnerAsHidden(_, sym))
else tp2
val tp3 =
if sym.isType then stripImpliedCaptureSet(tp2)
else tp2
if freshen then root.capToFresh(tp3).tap(addOwnerAsHidden(_, sym))
else tp3
end transformExplicitType

/** Substitute parameter symbols in `from` to paramRefs in corresponding
Expand Down
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/root.scala
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ object root:
case _ if root.isCap => Some(Kind.Global)
case _ => None

/** Map each occurrence of cap to a different Sep.Cap instance */
/** Map each occurrence of cap to a different Fresh instance
* Exception: CapSet^ stays as it is.
*/
class CapToFresh(owner: Symbol)(using Context) extends BiTypeMap, FollowAliasesMap:
thisMap =>

Expand All @@ -171,6 +173,8 @@ object root:
else t match
case t: CaptureRef if t.isCap =>
Fresh.withOwner(owner)
case t @ CapturingType(parent: TypeRef, _) if parent.symbol == defn.Caps_CapSet =>
t
case t @ CapturingType(_, _) =>
mapOver(t)
case t @ AnnotatedType(parent, ann) =>
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
if (tp1.prefix.isStable) return tryLiftedToThis1
case _ =>
if isCaptureVarComparison then
return subCaptures(tp1.captureSet, tp2.captureSet).isOK
return CCState.withCapAsRoot:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should leave a comment to make sure we don't introduce unsoundness

subCaptures(tp1.captureSet, tp2.captureSet).isOK
if (tp1 eq NothingType) || isBottom(tp1) then
return true
}
Expand Down Expand Up @@ -575,7 +576,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling
&& (isBottom(tp1) || GADTusage(tp2.symbol))

if isCaptureVarComparison then
return subCaptures(tp1.captureSet, tp2.captureSet).isOK
return CCState.withCapAsRoot:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should leave a comment to make sure we don't introduce unsoundness

subCaptures(tp1.captureSet, tp2.captureSet).isOK

isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi))
|| compareGADT
Expand Down
5 changes: 2 additions & 3 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1167,10 +1167,9 @@ object Types extends TypeUtils {
*
* @param isSubType a function used for checking subtype relationships.
*/
final def overrides(that: Type, matchLoosely: => Boolean, checkClassInfo: Boolean = true,
isSubType: (Type, Type) => Context ?=> Boolean = (tp1, tp2) => tp1 frozen_<:< tp2)(using Context): Boolean = {
final def overrides(that: Type, matchLoosely: => Boolean, checkClassInfo: Boolean = true)(using Context): Boolean = {
!checkClassInfo && this.isInstanceOf[ClassInfo]
|| isSubType(this.widenExpr, that.widenExpr)
|| (this.widenExpr frozen_<:< that.widenExpr)
|| matchLoosely && {
val this1 = this.widenNullaryMethod
val that1 = that.widenNullaryMethod
Expand Down
7 changes: 3 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import NameKinds.DefaultGetterName
import NullOpsDecorator.*
import collection.immutable.BitSet
import scala.annotation.tailrec
import cc.isCaptureChecking
import cc.{isCaptureChecking, CCState}

import scala.compiletime.uninitialized

Expand Down Expand Up @@ -210,8 +210,7 @@ object OverridingPairs:
* @param isSubType A function to be used for checking subtype relationships
* between term fields.
*/
def isOverridingPair(member: Symbol, memberTp: Type, other: Symbol, otherTp: Type, fallBack: => Boolean = false,
isSubType: (Type, Type) => Context ?=> Boolean = (tp1, tp2) => tp1 frozen_<:< tp2)(using Context): Boolean =
def isOverridingPair(member: Symbol, memberTp: Type, other: Symbol, otherTp: Type, fallBack: => Boolean = false)(using Context): Boolean =
if member.isType then // intersection of bounds to refined types must be nonempty
memberTp.bounds.hi.hasSameKindAs(otherTp.bounds.hi)
&& (
Expand All @@ -226,6 +225,6 @@ object OverridingPairs:
)
else
member.name.is(DefaultGetterName) // default getters are not checked for compatibility
|| memberTp.overrides(otherTp, member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack, isSubType = isSubType)
|| memberTp.overrides(otherTp, member.matchNullaryLoosely || other.matchNullaryLoosely || fallBack)

end OverridingPairs
62 changes: 39 additions & 23 deletions compiler/src/dotty/tools/dotc/typer/RefChecks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -242,25 +242,24 @@ object RefChecks {
&& (inLinearizationOrder(sym1, sym2, parent) || parent.is(JavaDefined))
&& !sym2.is(AbsOverride)

/** Checks the subtype relationship tp1 <:< tp2.
* It is passed to the `checkOverride` operation in `checkAll`, to be used for
* compatibility checking.
*/
def checkSubType(tp1: Type, tp2: Type)(using Context): Boolean = tp1 frozen_<:< tp2

/** A hook that allows to omit override checks between `overriding` and `overridden`.
* Overridden in capture checking to handle non-capture checked classes leniently.
*/
def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean = true

protected def additionalChecks(overriding: Symbol, overridden: Symbol)(using Context): Unit = ()
/** Adapt member type and other type so that they can be compared with `frozen_<:<`.
* @return optionally, if adaptation is necessary, the pair of adapted types (memberTp', otherTp')
* Note: we return an Option result to avoid a tuple allocation in the normal case
* where no adaptation is necessary.
*/
def adaptOverridePair(member: Symbol, memberTp: Type, otherTp: Type)(using Context): Option[(Type, Type)] = None

private val subtypeChecker: (Type, Type) => Context ?=> Boolean = this.checkSubType
protected def additionalChecks(overriding: Symbol, overridden: Symbol)(using Context): Unit = ()

def checkAll(checkOverride: ((Type, Type) => Context ?=> Boolean, Symbol, Symbol) => Unit) =
def checkAll(checkOverride: (Symbol, Symbol) => Unit) =
while hasNext do
if needsCheck(overriding, overridden) then
checkOverride(subtypeChecker, overriding, overridden)
checkOverride(overriding, overridden)
additionalChecks(overriding, overridden)
next()

Expand All @@ -275,7 +274,7 @@ object RefChecks {
if dcl.is(Deferred) then
for other <- dcl.allOverriddenSymbols do
if !other.is(Deferred) then
checkOverride(subtypeChecker, dcl, other)
checkOverride(dcl, other)
end checkAll

// Disabled for capture checking since traits can get different parameter refinements
Expand Down Expand Up @@ -428,28 +427,36 @@ object RefChecks {
/* Check that all conditions for overriding `other` by `member`
* of class `clazz` are met.
*/
def checkOverride(checkSubType: (Type, Type) => Context ?=> Boolean, member: Symbol, other: Symbol): Unit =
def memberTp(self: Type) =
def checkOverride(member: Symbol, other: Symbol): Unit =
def memberType(self: Type) =
if (member.isClass) TypeAlias(member.typeRef.etaExpand)
else self.memberInfo(member)
def otherTp(self: Type) =
self.memberInfo(other)
def otherType(self: Type) =
self.memberInfo(other)

var memberTp = memberType(self)
var otherTp = otherType(self)
checker.adaptOverridePair(member, memberTp, otherTp) match
case Some((mtp, otp)) =>
memberTp = mtp
otherTp = otp
case None =>

refcheck.println(i"check override ${infoString(member)} overriding ${infoString(other)}")

def noErrorType = !memberTp(self).isErroneous && !otherTp(self).isErroneous
def noErrorType = !memberTp.isErroneous && !otherTp.isErroneous

def overrideErrorMsg(core: Context ?=> String, compareTypes: Boolean = false): Message =
val (mtp, otp) = if compareTypes then (memberTp(self), otherTp(self)) else (NoType, NoType)
val (mtp, otp) = if compareTypes then (memberTp, otherTp) else (NoType, NoType)
OverrideError(core, self, member, other, mtp, otp)

def compatTypes(memberTp: Type, otherTp: Type): Boolean =
try
isOverridingPair(member, memberTp, other, otherTp,
fallBack = warnOnMigration(
overrideErrorMsg("no longer has compatible type"),
(if (member.owner == clazz) member else clazz).srcPos, version = `3.0`),
isSubType = checkSubType)
(if member.owner == clazz then member else clazz).srcPos,
version = `3.0`))
catch case ex: MissingType =>
// can happen when called with upwardsSelf as qualifier of memberTp and otherTp,
// because in that case we might access types that are not members of the qualifier.
Expand All @@ -469,7 +476,7 @@ object RefChecks {
// with box adaptation, we simply ignore capture annotations here.
// This should be safe since the compatibility under box adaptation is already
// checked.
memberTp(self).matches(otherTp(self))
memberTp.matches(otherTp)
}

def emitOverrideError(fullmsg: Message) =
Expand Down Expand Up @@ -624,20 +631,29 @@ object RefChecks {
overrideError("is not inline, cannot implement an inline method")
else if (other.isScala2Macro && !member.isScala2Macro) // (1.11)
overrideError("cannot be used here - only Scala-2 macros can override Scala-2 macros")
else if !compatTypes(memberTp(self), otherTp(self))
&& !compatTypes(memberTp(upwardsSelf), otherTp(upwardsSelf))
else if !compatTypes(memberTp, otherTp)
&& !member.is(Tracked)
// Tracked members need to be excluded since they are abstract type members with
// singleton types. Concrete overrides usually have a wider type.
// TODO: Should we exclude all refinements inherited from parents?
&& {
var memberTpUp = memberType(upwardsSelf)
var otherTpUp = otherType(upwardsSelf)
checker.adaptOverridePair(member, memberTpUp, otherTpUp) match
case Some((mtp, otp)) =>
memberTpUp = mtp
otherTpUp = otp
case _ =>
!compatTypes(memberTpUp, otherTpUp)
}
then
overrideError("has incompatible type", compareTypes = true)
else if (member.targetName != other.targetName)
if (other.targetName != other.name)
overrideError(i"needs to be declared with @targetName(${"\""}${other.targetName}${"\""}) so that external names match")
else
overrideError("cannot have a @targetName annotation since external names would be different")
else if intoOccurrences(memberTp(self)) != intoOccurrences(otherTp(self)) then
else if intoOccurrences(memberTp) != intoOccurrences(otherTp) then
overrideError("has different occurrences of `into` modifiers", compareTypes = true)
else if other.is(ParamAccessor) && !isInheritedAccessor(member, other)
&& !member.is(Tracked) // see remark on tracked members above
Expand Down
16 changes: 16 additions & 0 deletions tests/pos-custom-args/captures/check-override-typebounds.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import language.experimental.captureChecking

trait A:
type T <: caps.Capability

class B extends A:
type T = C

class C extends caps.Capability


trait A2:
type T[Cap^]

def takesCap[Cap^](t: T[Cap]): Unit

Loading