Skip to content

Various fixes to capture checking #16199

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

Merged
merged 15 commits into from
Oct 24, 2022
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
3 changes: 1 addition & 2 deletions compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte

override def symbol(using Context) = cls

override def derivedAnnotation(tree: Tree)(using Context): Annotation =
unsupported(i"derivedAnnotation(Tree), $tree, $refs")
override def derivedAnnotation(tree: Tree)(using Context): Annotation = this

def derivedAnnotation(refs: CaptureSet, boxed: Boolean)(using Context): Annotation =
if (this.refs eq refs) && (this.boxed == boxed) then this
Expand Down
15 changes: 15 additions & 0 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ extension (tp: Type)
/** Is the boxedCaptureSet of this type nonempty? */
def isBoxedCapturing(using Context) = !tp.boxedCaptureSet.isAlwaysEmpty

/** If this type is a capturing type, the version with boxed statues as given by `boxed`.
* If it is a TermRef of a capturing type, and the box status flips, widen to a capturing
* type that captures the TermRef.
*/
def forceBoxStatus(boxed: Boolean)(using Context): Type = tp.widenDealias match
case tp @ CapturingType(parent, refs) if tp.isBoxed != boxed =>
val refs1 = tp match
case ref: CaptureRef if ref.isTracked => ref.singletonCaptureSet
case _ => refs
CapturingType(parent, refs1, boxed)
case _ =>
tp

/** Map capturing type to their parents. Capturing types accessible
* via dealising are also stripped.
*/
Expand Down Expand Up @@ -155,6 +168,8 @@ extension (sym: Symbol)
case _ => false
containsEnclTypeParam(sym.info.finalResultType)
&& !sym.allowsRootCapture
&& sym != defn.Caps_unsafeBox
&& sym != defn.Caps_unsafeUnbox

extension (tp: AnnotatedType)
/** Is this a boxed capturing type? */
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/cc/CapturingType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ object CapturingType:
* returned separately by CaptureOps.isBoxed.
*/
def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] =
if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then
if ctx.phase == Phases.checkCapturesPhase
&& tp.annot.symbol == defn.RetainsAnnot
&& !ctx.mode.is(Mode.IgnoreCaptures)
then
EventuallyCapturingType.unapply(tp)
else None

Expand Down
96 changes: 68 additions & 28 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package cc
import core.*
import Phases.*, DenotTransformers.*, SymDenotations.*
import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.*
import Types.*, StdNames.*
import Types.*, StdNames.*, Denotations.*
import config.Printers.{capt, recheckr}
import config.Config
import ast.{tpd, untpd, Trees}
Expand Down Expand Up @@ -89,7 +89,7 @@ object CheckCaptures:
elem.tpe match
case ref: CaptureRef =>
if !ref.canBeTracked then
report.error(em"$elem cannot be tracked since it is not a parameter or a local variable", elem.srcPos)
report.error(em"$elem cannot be tracked since it is not a parameter or local value", elem.srcPos)
case tpe =>
report.error(em"$elem: $tpe is not a legal element of a capture set", elem.srcPos)

Expand Down Expand Up @@ -288,16 +288,34 @@ class CheckCaptures extends Recheck, SymTransformer:
* outcome of a `mightSubcapture` test. It picks `{f}` if this might subcapture Cr
* and Cr otherwise.
*/
override def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = {
val selType = super.recheckSelection(tree, qualType, name)
override def recheckSelection(tree: Select, qualType: Type, name: Name, pt: Type)(using Context) = {
def disambiguate(denot: Denotation): Denotation = denot match
case MultiDenotation(denot1, denot2) =>
// This case can arise when we try to merge multiple types that have different
// capture sets on some part. For instance an asSeenFrom might produce
// a bi-mapped capture set arising from a substition. Applying the same substitution
// to the same type twice will nevertheless produce different capture setsw which can
// lead to a failure in disambiguation since neither alternative is better than the
// other in a frozen constraint. An example test case is disambiguate-select.scala.
// We address the problem by disambiguating while ignoring all capture sets as a fallback.
withMode(Mode.IgnoreCaptures) {
disambiguate(denot1).meet(disambiguate(denot2), qualType)
}
case _ => denot

val selType = recheckSelection(tree, qualType, name, disambiguate)
val selCs = selType.widen.captureSet
if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then
selType
else
val qualCs = qualType.captureSet
capt.println(i"intersect $qualType, ${selType.widen}, $qualCs, $selCs in $tree")
if qualCs.mightSubcapture(selCs) then
if qualCs.mightSubcapture(selCs)
&& !selCs.mightSubcapture(qualCs)
&& !pt.stripCapturing.isInstanceOf[SingletonType]
then
selType.widen.stripCapturing.capturing(qualCs)
.showing(i"alternate type for select $tree: $selType --> $result, $qualCs / $selCs", capt)
else
selType
}//.showing(i"recheck sel $tree, $qualType = $result")
Expand All @@ -314,23 +332,32 @@ class CheckCaptures extends Recheck, SymTransformer:
* and Cr otherwise.
*/
override def recheckApply(tree: Apply, pt: Type)(using Context): Type =
includeCallCaptures(tree.symbol, tree.srcPos)
super.recheckApply(tree, pt) match
case appType @ CapturingType(appType1, refs) =>
tree.fun match
case Select(qual, _)
if !tree.fun.symbol.isConstructor
&& !qual.tpe.isBoxedCapturing
&& !tree.args.exists(_.tpe.isBoxedCapturing)
&& qual.tpe.captureSet.mightSubcapture(refs)
&& tree.args.forall(_.tpe.captureSet.mightSubcapture(refs))
=>
val callCaptures = tree.args.foldLeft(qual.tpe.captureSet)((cs, arg) =>
cs ++ arg.tpe.captureSet)
appType.derivedCapturingType(appType1, callCaptures)
.showing(i"narrow $tree: $appType, refs = $refs, qual = ${qual.tpe.captureSet} --> $result", capt)
case _ => appType
case appType => appType
val meth = tree.fun.symbol
includeCallCaptures(meth, tree.srcPos)
if meth == defn.Caps_unsafeBox || meth == defn.Caps_unsafeUnbox then
val arg :: Nil = tree.args: @unchecked
val argType0 = recheckStart(arg, pt)
.forceBoxStatus(boxed = meth == defn.Caps_unsafeBox)
val argType = super.recheckFinish(argType0, arg, pt)
super.recheckFinish(argType, tree, pt)
else
super.recheckApply(tree, pt) match
case appType @ CapturingType(appType1, refs) =>
tree.fun match
case Select(qual, _)
if !tree.fun.symbol.isConstructor
&& !qual.tpe.isBoxedCapturing
&& !tree.args.exists(_.tpe.isBoxedCapturing)
&& qual.tpe.captureSet.mightSubcapture(refs)
&& tree.args.forall(_.tpe.captureSet.mightSubcapture(refs))
=>
val callCaptures = tree.args.foldLeft(qual.tpe.captureSet)((cs, arg) =>
cs ++ arg.tpe.captureSet)
appType.derivedCapturingType(appType1, callCaptures)
.showing(i"narrow $tree: $appType, refs = $refs, qual = ${qual.tpe.captureSet} --> $result", capt)
case _ => appType
case appType => appType
end recheckApply

/** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`.
* This means:
Expand Down Expand Up @@ -435,10 +462,25 @@ class CheckCaptures extends Recheck, SymTransformer:
case _ =>
super.recheckBlock(block, pt)

/** If `rhsProto` has `*` as its capture set, wrap `rhs` in a `unsafeBox`.
* Used to infer `unsafeBox` for expressions that get assigned to variables
* that have universal capture set.
*/
def maybeBox(rhs: Tree, rhsProto: Type)(using Context): Tree =
if rhsProto.captureSet.isUniversal then
ref(defn.Caps_unsafeBox).appliedToType(rhsProto).appliedTo(rhs)
else rhs

override def recheckAssign(tree: Assign)(using Context): Type =
val rhsProto = recheck(tree.lhs).widen
recheck(maybeBox(tree.rhs, rhsProto), rhsProto)
defn.UnitType

override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit =
try
if !sym.is(Module) then // Modules are checked by checking the module class
super.recheckValDef(tree, sym)
if sym.is(Mutable) then recheck(maybeBox(tree.rhs, sym.info), sym.info)
else super.recheckValDef(tree, sym)
finally
if !sym.is(Param) then
// Parameters with inferred types belong to anonymous methods. We need to wait
Expand Down Expand Up @@ -535,8 +577,6 @@ class CheckCaptures extends Recheck, SymTransformer:
tpe
case _: Try =>
tpe
case _: ValDef if tree.symbol.is(Mutable) =>
tree.symbol.info
case _ =>
NoType
def checkNotUniversal(tp: Type): Unit = tp.widenDealias match
Expand Down Expand Up @@ -665,7 +705,7 @@ class CheckCaptures extends Recheck, SymTransformer:

/** Destruct a capturing type `tp` to a tuple (cs, tp0, boxed),
* where `tp0` is not a capturing type.
*
*
* If `tp` is a nested capturing type, the return tuple always represents
* the innermost capturing type. The outer capture annotations can be
* reconstructed with the returned function.
Expand Down Expand Up @@ -721,7 +761,7 @@ class CheckCaptures extends Recheck, SymTransformer:
val criticalSet = // the set which is not allowed to have `*`
if covariant then cs1 // can't box with `*`
else expected.captureSet // can't unbox with `*`
if criticalSet.isUniversal then
if criticalSet.isUniversal && expected.isValueType then
// We can't box/unbox the universal capability. Leave `actual` as it is
// so we get an error in checkConforms. This tends to give better error
// messages than disallowing the root capability in `criticalSet`.
Expand All @@ -742,7 +782,6 @@ class CheckCaptures extends Recheck, SymTransformer:
recon(CapturingType(parent1, cs1, actualIsBoxed))
}


var actualw = actual.widenDealias
actual match
case ref: CaptureRef if ref.isTracked =>
Expand All @@ -762,6 +801,7 @@ class CheckCaptures extends Recheck, SymTransformer:
override def checkUnit(unit: CompilationUnit)(using Context): Unit =
Setup(preRecheckPhase, thisPhase, recheckDef)
.traverse(ctx.compilationUnit.tpdTree)
//println(i"SETUP:\n${Recheck.addRecheckedTypes.transform(ctx.compilationUnit.tpdTree)}")
withCaptureSetsExplained {
super.checkUnit(unit)
checkSelfTypes(unit.tpdTree)
Expand Down
39 changes: 27 additions & 12 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,12 @@ extends tpd.TreeTraverser:
else expandAbbreviations(tp1)

/** Transform type of type tree, and remember the transformed type as the type the tree */
private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit =
tree.rememberType(
if tree.isInstanceOf[InferredTypeTree]
then transformInferredType(tree.tpe, boxed)
else transformExplicitType(tree.tpe, boxed))
private def transformTT(tree: TypeTree, boxed: Boolean, exact: Boolean)(using Context): Unit =
if !tree.hasRememberedType then
tree.rememberType(
if tree.isInstanceOf[InferredTypeTree] && !exact
then transformInferredType(tree.tpe, boxed)
else transformExplicitType(tree.tpe, boxed))

/** Substitute parameter symbols in `from` to paramRefs in corresponding
* method or poly types `to`. We use a single BiTypeMap to do everything.
Expand Down Expand Up @@ -376,20 +377,34 @@ extends tpd.TreeTraverser:

def traverse(tree: Tree)(using Context): Unit =
tree match
case tree: DefDef if isExcluded(tree.symbol) =>
return
case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) =>
transformTT(tpt, boxed = true) // types of mutable variables are boxed
traverse(tree.rhs)
case tree: DefDef =>
if isExcluded(tree.symbol) then
return
tree.tpt match
case tpt: TypeTree if tree.symbol.allOverriddenSymbols.hasNext =>
transformTT(tpt, boxed = false, exact = true)
//println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}")
case _ =>
traverseChildren(tree)
case tree @ ValDef(_, tpt: TypeTree, _) =>
val isVar = tree.symbol.is(Mutable)
val overrides = tree.symbol.allOverriddenSymbols.hasNext
//if overrides then println(i"transforming overriding ${tree.symbol}")
if isVar || overrides then
transformTT(tpt,
boxed = isVar, // types of mutable variables are boxed
exact = overrides // types of symbols that override a parent don't get a capture set
)
traverseChildren(tree)
case tree @ TypeApply(fn, args) =>
traverse(fn)
for case arg: TypeTree <- args do
transformTT(arg, boxed = true) // type arguments in type applications are boxed
transformTT(arg, boxed = true, exact = false) // type arguments in type applications are boxed
case _ =>
traverseChildren(tree)
tree match
case tree: TypeTree =>
transformTT(tree, boxed = false) // other types are not boxed
transformTT(tree, boxed = false, exact = false) // other types are not boxed
case tree: ValOrDefDef =>
val sym = tree.symbol

Expand Down
29 changes: 17 additions & 12 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -943,23 +943,27 @@ class Definitions {

@tu lazy val RuntimeTuplesModule: Symbol = requiredModule("scala.runtime.Tuples")
@tu lazy val RuntimeTuplesModuleClass: Symbol = RuntimeTuplesModule.moduleClass
lazy val RuntimeTuples_consIterator: Symbol = RuntimeTuplesModule.requiredMethod("consIterator")
lazy val RuntimeTuples_concatIterator: Symbol = RuntimeTuplesModule.requiredMethod("concatIterator")
lazy val RuntimeTuples_apply: Symbol = RuntimeTuplesModule.requiredMethod("apply")
lazy val RuntimeTuples_cons: Symbol = RuntimeTuplesModule.requiredMethod("cons")
lazy val RuntimeTuples_size: Symbol = RuntimeTuplesModule.requiredMethod("size")
lazy val RuntimeTuples_tail: Symbol = RuntimeTuplesModule.requiredMethod("tail")
lazy val RuntimeTuples_concat: Symbol = RuntimeTuplesModule.requiredMethod("concat")
lazy val RuntimeTuples_toArray: Symbol = RuntimeTuplesModule.requiredMethod("toArray")
lazy val RuntimeTuples_productToArray: Symbol = RuntimeTuplesModule.requiredMethod("productToArray")
lazy val RuntimeTuples_isInstanceOfTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfTuple")
lazy val RuntimeTuples_isInstanceOfEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfEmptyTuple")
lazy val RuntimeTuples_isInstanceOfNonEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfNonEmptyTuple")
@tu lazy val RuntimeTuples_consIterator: Symbol = RuntimeTuplesModule.requiredMethod("consIterator")
@tu lazy val RuntimeTuples_concatIterator: Symbol = RuntimeTuplesModule.requiredMethod("concatIterator")
@tu lazy val RuntimeTuples_apply: Symbol = RuntimeTuplesModule.requiredMethod("apply")
@tu lazy val RuntimeTuples_cons: Symbol = RuntimeTuplesModule.requiredMethod("cons")
@tu lazy val RuntimeTuples_size: Symbol = RuntimeTuplesModule.requiredMethod("size")
@tu lazy val RuntimeTuples_tail: Symbol = RuntimeTuplesModule.requiredMethod("tail")
@tu lazy val RuntimeTuples_concat: Symbol = RuntimeTuplesModule.requiredMethod("concat")
@tu lazy val RuntimeTuples_toArray: Symbol = RuntimeTuplesModule.requiredMethod("toArray")
@tu lazy val RuntimeTuples_productToArray: Symbol = RuntimeTuplesModule.requiredMethod("productToArray")
@tu lazy val RuntimeTuples_isInstanceOfTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfTuple")
@tu lazy val RuntimeTuples_isInstanceOfEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfEmptyTuple")
@tu lazy val RuntimeTuples_isInstanceOfNonEmptyTuple: Symbol = RuntimeTuplesModule.requiredMethod("isInstanceOfNonEmptyTuple")

@tu lazy val TupledFunctionTypeRef: TypeRef = requiredClassRef("scala.util.TupledFunction")
def TupledFunctionClass(using Context): ClassSymbol = TupledFunctionTypeRef.symbol.asClass
def RuntimeTupleFunctionsModule(using Context): Symbol = requiredModule("scala.runtime.TupledFunctions")

@tu lazy val CapsModule: Symbol = requiredModule("scala.caps")
@tu lazy val Caps_unsafeBox: Symbol = CapsModule.requiredMethod("unsafeBox")
@tu lazy val Caps_unsafeUnbox: Symbol = CapsModule.requiredMethod("unsafeUnbox")

// Annotation base classes
@tu lazy val AnnotationClass: ClassSymbol = requiredClass("scala.annotation.Annotation")
@tu lazy val ClassfileAnnotationClass: ClassSymbol = requiredClass("scala.annotation.ClassfileAnnotation")
Expand Down Expand Up @@ -1020,6 +1024,7 @@ class Definitions {
@tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability")
@tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains")
@tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName")
@tu lazy val RetainsUniversalAnnot: ClassSymbol = requiredClass("scala.annotation.retainsUniversal")

@tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable")

Expand Down
8 changes: 8 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Mode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ object Mode {
/** Use Scala2 scheme for overloading and implicit resolution */
val OldOverloadingResolution: Mode = newMode(15, "OldOverloadingResolution")

/** Treat CapturingTypes as plain AnnotatedTypes even in phase =Ycc.
* Reuses the value of OldOverloadingResolution to save Mode bits.
* This is OK since OldOverloadingResolution only affects implicit search, which
* is done during phases Typer and Inlinig, and IgnoreCaptures only has an
* effect during phase CheckCaptures.
*/
val IgnoreCaptures = OldOverloadingResolution

/** Allow hk applications of type lambdas to wildcard arguments;
* used for checking that such applications do not normally arise
*/
Expand Down
7 changes: 5 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1872,7 +1872,10 @@ object Types {
def dropRepeatedAnnot(using Context): Type = dropAnnot(defn.RepeatedAnnot)

def annotatedToRepeated(using Context): Type = this match {
case tp @ ExprType(tp1) => tp.derivedExprType(tp1.annotatedToRepeated)
case tp @ ExprType(tp1) =>
tp.derivedExprType(tp1.annotatedToRepeated)
case self @ AnnotatedType(tp, annot) if annot matches defn.RetainsByNameAnnot =>
self.derivedAnnotatedType(tp.annotatedToRepeated, annot)
case AnnotatedType(tp, annot) if annot matches defn.RepeatedAnnot =>
val typeSym = tp.typeSymbol.asClass
assert(typeSym == defn.SeqClass || typeSym == defn.ArrayClass)
Expand Down Expand Up @@ -2787,7 +2790,7 @@ object Types {
((prefix eq NoPrefix)
|| symbol.is(ParamAccessor) && (prefix eq symbol.owner.thisType)
|| isRootCapability
) && !symbol.is(Method)
) && !symbol.isOneOf(UnstableValueFlags)

override def isRootCapability(using Context): Boolean =
name == nme.CAPTURE_ROOT && symbol == defn.captureRoot
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
case tp: LazyRef if !printDebug =>
try toText(tp.ref)
catch case ex: Throwable => "..."
case AnySelectionProto =>
"a type that can be selected or applied"
case tp: SelectionProto =>
"?{ " ~ toText(tp.name) ~
(Str(" ") provided !tp.name.toSimpleName.last.isLetterOrDigit) ~
Expand Down
Loading