diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 126e54a7b49e..4db5f96c3d18 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -4,7 +4,6 @@ package dotc import core._ import Contexts._ import typer.{TyperPhase, RefChecks} -import cc.CheckCaptures import parsing.Parser import Phases.Phase import transform._ @@ -84,8 +83,9 @@ class Compiler { new PatternMatcher) :: // Compile pattern matches List(new TestRecheck.Pre) :: // Test only: run rechecker, enabled under -Yrecheck-test List(new TestRecheck) :: // Test only: run rechecker, enabled under -Yrecheck-test - List(new CheckCaptures.Pre) :: // Preparations for check captures phase, enabled under captureChecking - List(new CheckCaptures) :: // Check captures, enabled under captureChecking + List(new cc.AddTryOwners) :: // Add symbols as owners of try blocks, enabled under captureChecking + List(new cc.Setup) :: // Preparations for check captures phase, enabled under captureChecking + List(new cc.CheckCaptures) :: // Check captures, enabled under captureChecking List(new ElimOpaque, // Turn opaque into normal aliases new sjs.ExplicitJSClasses, // Make all JS classes explicit (Scala.js only) new ExplicitOuter, // Add accessors to outer classes from nested ones. diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 973af0e8781d..eefb45c7d602 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -1284,6 +1284,21 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { !(sym.is(Method) && sym.info.isInstanceOf[MethodOrPoly]) // if is a method it is parameterless } + /** A tree traverser that generates the the same import contexts as original typer for statements. + * TODO: Should we align TreeMapWithPreciseStatContexts and also keep track of exprOwners? + */ + abstract class TreeTraverserWithPreciseImportContexts extends TreeTraverser: + override def apply(x: Unit, trees: List[Tree])(using Context): Unit = + def recur(trees: List[Tree]): Unit = trees match + case (imp: Import) :: rest => + traverse(rest)(using ctx.importContext(imp, imp.symbol)) + case tree :: rest => + traverse(tree) + traverse(rest) + case Nil => + recur(trees) + end TreeTraverserWithPreciseImportContexts + extension (xs: List[tpd.Tree]) def tpes: List[Type] = xs match { case x :: xs1 => x.tpe :: xs1.tpes diff --git a/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala b/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala new file mode 100644 index 000000000000..b79bd2a9ccaa --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala @@ -0,0 +1,46 @@ +package dotty.tools.dotc +package cc + +import core.* +import DenotTransformers.IdentityDenotTransformer +import Phases.Phase +import Contexts.*, Symbols.*, Flags.*, Types.* +import config.Feature +import ast.tpd +import StdNames.nme +import Decorators.i + +object AddTryOwners: + val name: String = "addTryOwners" + val description: String = "add symbols for try blocks in preparation of capture checking" + +class AddTryOwners extends Phase, IdentityDenotTransformer: + thisPhase => + + import tpd.* + + override def phaseName: String = AddTryOwners.name + override def description: String = AddTryOwners.description + + override def isRunnable(using Context) = + super.isRunnable && Feature.ccEnabledSomewhere + + override def isCheckable = false + + def run(using Context): Unit = + val addTryOwners = new TreeTraverserWithPreciseImportContexts: + def traverse(tree: Tree)(using Context): Unit = tree match + case tree @ Try(expr, cases, finalizer) if Feature.enabled(Feature.saferExceptions) => + val tryOwner = newSymbol(ctx.owner, nme.TRY_BLOCK, SyntheticMethod, MethodType(Nil, defn.UnitType)) + ccState.tryBlockOwner(tree) = tryOwner + ccState.isLevelOwner(tryOwner) = true + expr.changeOwnerAfter(ctx.owner, tryOwner, thisPhase) + inContext(ctx.withOwner(tryOwner)): + traverse(expr) + traverse(cases) + traverse(finalizer) + case _ => + traverseChildren(tree) + addTryOwners.traverse(ctx.compilationUnit.tpdTree) +end AddTryOwners + diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index fd89159e2076..206734bccc18 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -24,8 +24,17 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte import CaptureAnnotation.* import tpd.* - /** A cache for boxed version of a capturing type with this annotation */ - val boxedType = BoxedTypeCache() + /** A cache for the version of this annotation which differs in its boxed status. */ + var boxDual: CaptureAnnotation | Null = null + + /** A boxed annotation which is either the same annotation or its boxDual */ + def boxedAnnot(using Context): CaptureAnnotation = + if boxed then this + else if boxDual != null then boxDual.nn + else + val dual = CaptureAnnotation(refs, boxed = true)(cls) + dual.boxDual = this + dual /** Reconstitute annotation tree from capture set */ override def tree(using Context) = diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index dfaa3c701576..5fcd6d243076 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -7,7 +7,7 @@ import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* import ast.{tpd, untpd} import Decorators.*, NameOps.* import config.SourceVersion -import config.Printers.capt +import config.Printers.{capt, ccSetup} import util.Property.Key import tpd.* import StdNames.nme @@ -17,37 +17,52 @@ import collection.mutable private val Captures: Key[CaptureSet] = Key() private val BoxedType: Key[BoxedTypeCache] = Key() -/** Switch whether unpickled function types and byname types should be mapped to - * impure types. With the new gradual typing using Fluid capture sets, this should - * be no longer needed. Also, it has bad interactions with pickling tests. - */ -private val adaptUnpickledFunctionTypes = false +object ccConfig: -/** Switch whether we constrain a root var that includes the source of a - * root map to be an alias of that source (so that it can be mapped) - */ -private val constrainRootsWhenMapping = true + /** Switch whether unpickled function types and byname types should be mapped to + * impure types. With the new gradual typing using Fluid capture sets, this should + * be no longer needed. Also, it has bad interactions with pickling tests. + */ + private[cc] val adaptUnpickledFunctionTypes = false -/** The arguments of a @retains or @retainsByName annotation */ -private[cc] def retainedElems(tree: Tree)(using Context): List[Tree] = tree match - case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems - case _ => Nil + /** Switch whether we constrain a root var that includes the source of a + * root map to be an alias of that source (so that it can be mapped) + */ + private[cc] val constrainRootsWhenMapping = true + + /** Use old scheme for refining vars, which should be no longer necessary + */ + val oldRefiningVars = false +end ccConfig def allowUniversalInBoxed(using Context) = Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) -/** An exception thrown if a @retains argument is not syntactically a CaptureRef */ -class IllegalCaptureRef(tpe: Type) extends Exception +/** Are we at checkCaptures phase? */ +def isCaptureChecking(using Context): Boolean = + ctx.phaseId == Phases.checkCapturesPhase.id -/** Capture checking state, which is stored in a context property */ -class CCState: +/** Are we at checkCaptures or Setup phase? */ +def isCaptureCheckingOrSetup(using Context): Boolean = + val ccId = Phases.checkCapturesPhase.id + val ctxId = ctx.phaseId + ctxId == ccId || ctxId == ccId - 1 - val rhsClosure: mutable.HashSet[Symbol] = new mutable.HashSet +/** A dependent function type with given arguments and result type + * TODO Move somewhere else where we treat all function type related ops together. + */ +def depFun(args: List[Type], resultType: Type, isContextual: Boolean)(using Context): Type = + MethodType.companion(isContextual = isContextual)(args, resultType) + .toFunctionType(alwaysDependent = true) - val levelOwners: mutable.HashSet[Symbol] = new mutable.HashSet +/** An exception thrown if a @retains argument is not syntactically a CaptureRef */ +class IllegalCaptureRef(tpe: Type) extends Exception(tpe.toString) - /** Associates certain symbols (the nesting level owners) with their ccNestingLevel */ - val nestingLevels: mutable.HashMap[Symbol, Int] = new mutable.HashMap +/** Capture checking state, which is known to other capture checking components */ +class CCState: + + /** Cache for level ownership */ + val isLevelOwner: mutable.HashMap[Symbol, Boolean] = new mutable.HashMap /** Associates nesting level owners with the local roots valid in their scopes. */ val localRoots: mutable.HashMap[Symbol, Symbol] = new mutable.HashMap @@ -55,19 +70,18 @@ class CCState: /** The last pair of capture reference and capture set where * the reference could not be added to the set due to a level conflict. */ - var levelError: Option[(CaptureRef, CaptureSet)] = None + var levelError: Option[CaptureSet.CompareResult.LevelError] = None /** Under saferExceptions: The symbol generated for a try. * Installed by Setup, removed by CheckCaptures. */ val tryBlockOwner: mutable.HashMap[Try, Symbol] = new mutable.HashMap -end CCState -/** Property key for capture checking state */ -val ccStateKey: Key[CCState] = Key() +end CCState /** The currently valid CCState */ -def ccState(using Context) = ctx.property(ccStateKey).get +def ccState(using Context) = + Phases.checkCapturesPhase.asInstanceOf[CheckCaptures].ccState trait FollowAliases extends TypeMap: def mapOverFollowingAliases(t: Type): Type = t match @@ -81,23 +95,18 @@ trait FollowAliases extends TypeMap: if t2 ne t1 then return t2 mapOver(t) -class mapRoots(from: CaptureRoot, to: CaptureRoot)(using Context) extends BiTypeMap, FollowAliases: - thisMap => +class mapRoots(from0: CaptureRoot, to: CaptureRoot)(using Context) extends DeepTypeMap, BiTypeMap, FollowAliases: + val from = from0.followAlias + + //override val toString = i"mapRoots($from, $to)" def apply(t: Type): Type = if t eq from then to else t match - case t: CaptureRoot.Var => - val ta = t.followAlias - if ta ne t then apply(ta) - else from match - case from: TermRef - if t.upperLevel >= from.symbol.ccNestingLevel - && constrainRootsWhenMapping // next two lines do the constraining - && CaptureRoot.isEnclosingRoot(from, t) - && CaptureRoot.isEnclosingRoot(t, from) => to - case from: CaptureRoot.Var if from.followAlias eq t => to - case _ => t + case t: CaptureRoot.Var if ccConfig.constrainRootsWhenMapping && t.unifiesWith(from) => + to + case t @ Setup.Box(t1) => + t.derivedBox(this(t1)) case _ => mapOverFollowingAliases(t) @@ -123,16 +132,21 @@ extension (tree: Tree) tree.getAttachment(Captures) match case Some(refs) => refs case None => - val refs = CaptureSet(retainedElems(tree).map(_.toCaptureRef)*) + val refs = CaptureSet(tree.retainedElems.map(_.toCaptureRef)*) .showing(i"toCaptureSet $tree --> $result", capt) tree.putAttachment(Captures, refs) refs + /** The arguments of a @retains or @retainsByName annotation */ + def retainedElems(using Context): List[Tree] = tree match + case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems + case _ => Nil + /** Under pureFunctions, add a @retainsByName(*)` annotation to the argument of * a by name parameter type, turning the latter into an impure by name parameter type. */ def adaptByNameArgUnderPureFuns(using Context): Tree = - if adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then + if ccConfig.adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then val rbn = defn.RetainsByNameAnnot Annotated(tree, New(rbn.typeRef).select(rbn.primaryConstructor).appliedTo( @@ -152,6 +166,7 @@ extension (tp: Type) if (parent eq p) && (refs eq r) then tp else CapturingType(parent, refs, tp.isBoxed) + // TODO Move boxed/unboxed to CaapturingType? /** If this is a unboxed capturing type with nonempty capture set, its boxed version. * Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed. * The identity for all other types. @@ -160,9 +175,9 @@ extension (tp: Type) case tp @ CapturingType(parent, refs) if !tp.isBoxed && !refs.isAlwaysEmpty => tp.annot match case ann: CaptureAnnotation => - ann.boxedType(tp) + AnnotatedType(parent, ann.boxedAnnot) case ann => - ann.tree.getAttachment(BoxedType) match + ann.tree.getAttachment(BoxedType) match // TODO drop case None => ann.tree.putAttachment(BoxedType, BoxedTypeCache()) case _ => ann.tree.attachment(BoxedType)(tp) @@ -171,15 +186,17 @@ extension (tp: Type) case _ => tp - /** If `sym` is a type parameter, the boxed version of `tp`, otherwise `tp` */ - def boxedIfTypeParam(sym: Symbol)(using Context) = - if sym.is(TypeParam) then tp.boxed else tp - - /** The boxed version of `tp`, unless `tycon` is a function symbol */ - def boxedUnlessFun(tycon: Type)(using Context) = - if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionSymbol(tycon.typeSymbol) - then tp - else tp.boxed + /** If this is a unboxed capturing type with nonempty capture set, its boxed version. + * Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed. + * The identity for all other types. + */ + def unboxed(using Context): Type = tp.dealias match + case tp @ CapturingType(parent, refs) if tp.isBoxed && !refs.isAlwaysEmpty => + CapturingType(parent, refs) + case tp: RealTypeBounds => + tp.derivedTypeBounds(tp.lo.unboxed, tp.hi.unboxed) + case _ => + tp /** The capture set consisting of all top-level captures of `tp` that appear under a box. * Unlike for `boxed` this also considers parents of capture types, unions and @@ -228,7 +245,7 @@ extension (tp: Type) */ def adaptFunctionTypeUnderPureFuns(using Context): Type = tp match case AppliedType(fn, args) - if adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere && defn.isFunctionClass(fn.typeSymbol) => + if ccConfig.adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere && defn.isFunctionClass(fn.typeSymbol) => val fname = fn.typeSymbol.name defn.FunctionType( fname.functionArity, @@ -241,22 +258,12 @@ extension (tp: Type) * a by name parameter type, turning the latter into an impure by name parameter type. */ def adaptByNameArgUnderPureFuns(using Context): Type = - if adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then + if ccConfig.adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then AnnotatedType(tp, CaptureAnnotation(CaptureSet.universal, boxed = false)(defn.RetainsByNameAnnot)) else tp - def isCapturingType(using Context): Boolean = - tp match - case CapturingType(_, _) => true - case _ => false - - def isEventuallyCapturingType(using Context): Boolean = - tp match - case EventuallyCapturingType(_, _) => true - case _ => false - /** Is type known to be always pure by its class structure, * so that adding a capture set to it would not make sense? */ @@ -276,15 +283,56 @@ extension (tp: Type) case _ => false -extension (cls: ClassSymbol) + def isCapabilityClassRef(using Context) = tp match + case _: TypeRef | _: AppliedType => tp.typeSymbol.hasAnnotation(defn.CapabilityAnnot) + case _ => false + + /** Drop @retains annotations everywhere */ + def dropAllRetains(using Context): Type = // TODO we should drop retains from inferred types before unpickling + val tm = new TypeMap: + def apply(t: Type) = t match + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + apply(parent) + case _ => + mapOver(t) + tm(tp) + + def hasUniversalRootOf(sym: Symbol)(using Context): Boolean = + + def isOwnRoot(tp: Type)(using Context): Boolean = + tp.isCapabilityClassRef + || tp.dealias.match + case tp: TermRef => + tp.isGenericRootCapability || tp.localRootOwner == sym + case _ => + false + + tp.dealiasKeepAnnots match + case tp @ AnnotatedType(parent, annot) => + val found = annot match + case CaptureAnnotation(refs, _) => refs.elems.exists(isOwnRoot(_)) + case _ => annot.tree.retainedElems.exists(tree => isOwnRoot(tree.tpe)) + found || parent.hasUniversalRootOf(sym) + case tp: TypeRef => + tp.isRef(defn.Caps_Cap) || tp.isCapabilityClassRef + case tp: LazyRef => + tp.ref.hasUniversalRootOf(sym) + case tp: TypeVar => + tp.underlying.hasUniversalRootOf(sym) + case _ => + tp.isCapabilityClassRef + end hasUniversalRootOf + +extension (cls: Symbol) def pureBaseClass(using Context): Option[Symbol] = - cls.baseClasses.find(bc => + if cls.isClass then cls.asClass.baseClasses.find: bc => defn.pureBaseClasses.contains(bc) - || { - val selfType = bc.givenSelfType - selfType.exists && selfType.captureSet.isAlwaysEmpty - }) + || bc.givenSelfType.dealiasKeepAnnots.match + case CapturingType(_, refs) => refs.isAlwaysEmpty + case RetainingType(_, refs) => refs.isEmpty // TODO: Better: test at phase cc instead? + case selfType => selfType.exists && selfType.captureSet.isAlwaysEmpty + else None extension (sym: Symbol) @@ -330,7 +378,46 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox - def isLevelOwner(using Context): Boolean = ccState.levelOwners.contains(sym) + def takesCappedParamIn(info: Type)(using Context): Boolean = + info.dealias.stripPoly match + case mt: MethodType => + (mt.paramInfos.exists(_.hasUniversalRootOf(sym)) || takesCappedParamIn(mt.resType)) + //.showing(i"takes capped param1 $sym: $mt = $result") + case AppliedType(fn, args) if defn.isFunctionClass(fn.typeSymbol) => + args.init.exists(_.hasUniversalRootOf(sym)) || takesCappedParamIn(args.last) + case defn.RefinedFunctionOf(rinfo) => + takesCappedParamIn(rinfo) + //.showing(i"takes capped param2 $sym: $rinfo = $result") + case _ => + false + + // TODO Also include vals (right now they are manually entered in levelOwners by Setup) + def isLevelOwner(using Context): Boolean = + val symd = sym.denot + def isCaseClassSynthetic = // TODO drop + symd.maybeOwner.isClass && symd.owner.is(Case) && symd.is(Synthetic) && symd.info.firstParamNames.isEmpty + def classQualifies = + if sym.isEffectivelyFinal then + takesCappedParamIn(symd.primaryConstructor.info) + || symd.asClass.givenSelfType.hasUniversalRootOf(sym) + else + !sym.isPureClass + def compute = + if symd.isClass then + symd.is(CaptureChecked) && classQualifies || symd.isRoot + else + (symd.is(Method, butNot = Accessor) + || symd.isTerm && !symd.isOneOf(TermParamOrAccessor | Mutable)) + && (!symd.owner.isClass + || symd.owner.is(CaptureChecked) + || Synthetics.needsTransform(symd) + ) + && (!symd.isAnonymousFunction || sym.definedLocalRoot.exists) + && takesCappedParamIn(symd.info) + && { ccSetup.println(i"Level owner $sym"); true } + + ccState.isLevelOwner.getOrElseUpdate(sym, compute) + end isLevelOwner /** The owner of the current level. Qualifying owners are * - methods other than constructors and anonymous functions @@ -340,25 +427,25 @@ extension (sym: Symbol) * - _root_ */ def levelOwner(using Context): Symbol = - if !sym.exists || sym.isRoot || sym.isStaticOwner then defn.RootClass - else if sym.isLevelOwner then sym - else sym.owner.levelOwner + def recur(sym: Symbol): Symbol = + if !sym.exists || sym.isRoot || sym.isStaticOwner then defn.RootClass + else if sym.isLevelOwner then sym + else recur(sym.owner) + recur(sym) - /** The nesting level of `sym` for the purposes of `cc`, - * -1 for NoSymbol - */ - def ccNestingLevel(using Context): Int = - if sym.exists then - val lowner = sym.levelOwner - ccState.nestingLevels.getOrElseUpdate(lowner, - if lowner.isRoot then 0 else lowner.owner.ccNestingLevel + 1) - else -1 - - /** Optionally, the nesting level of `sym` for the purposes of `cc`, provided - * a capture checker is running. + /** The level owner enclosing `sym` which has the given name, or NoSymbol if none exists. + * If name refers to a val that has a closure as rhs, we return the closure as level + * owner. */ - def ccNestingLevelOpt(using Context): Option[Int] = - if ctx.property(ccStateKey).isDefined then Some(ccNestingLevel) else None + def levelOwnerNamed(name: String)(using Context): Symbol = + def recur(sym: Symbol): Symbol = + if sym.name.toString == name then + if sym.isLevelOwner then sym + else NoSymbol + else if sym == defn.RootClass then NoSymbol + else recur(sym.owner) + recur(sym) + .showing(i"find outer $sym [ $name ] = $result", capt) /** The parameter with type caps.Cap in the leading term parameter section, * or NoSymbol, if none exists. @@ -373,60 +460,24 @@ extension (sym: Symbol) val owner = sym.levelOwner assert(owner.exists) def newRoot = newSymbol(if owner.isClass then newLocalDummy(owner) else owner, - nme.LOCAL_CAPTURE_ROOT, Synthetic, defn.Caps_Cap.typeRef, nestingLevel = owner.ccNestingLevel) + nme.LOCAL_CAPTURE_ROOT, Synthetic, defn.Caps_Cap.typeRef) def lclRoot = if owner.isTerm then owner.definedLocalRoot.orElse(newRoot) else newRoot ccState.localRoots.getOrElseUpdate(owner, lclRoot) - /** The level owner enclosing `sym` which has the given name, or NoSymbol if none exists. - * If name refers to a val that has a closure as rhs, we return the closure as level - * owner. - */ - def levelOwnerNamed(name: String)(using Context): Symbol = - def recur(owner: Symbol, prev: Symbol): Symbol = - if owner.name.toString == name then - if owner.isLevelOwner then owner - else if owner.isTerm && !owner.isOneOf(Method | Module) && prev.exists then prev - else NoSymbol - else if owner == defn.RootClass then - NoSymbol - else - val prev1 = if owner.isAnonymousFunction && owner.isLevelOwner then owner else NoSymbol - recur(owner.owner, prev1) - recur(sym, NoSymbol) - .showing(i"find outer $sym [ $name ] = $result", capt) - - def maxNested(other: Symbol)(using Context): Symbol = - if sym.ccNestingLevel < other.ccNestingLevel then other else sym - /* does not work yet, we do mix sets with different levels, for instance in cc-this.scala. - else if sym.ccNestingLevel > other.ccNestingLevel then sym - else - assert(sym == other, i"conflicting symbols at same nesting level: $sym, $other") - sym - */ + def maxNested(other: Symbol, onConflict: (Symbol, Symbol) => Context ?=> Symbol)(using Context): Symbol = + if !sym.exists || other.isContainedIn(sym) then other + else if !other.exists || sym.isContainedIn(other) then sym + else onConflict(sym, other) def minNested(other: Symbol)(using Context): Symbol = - if sym.ccNestingLevel > other.ccNestingLevel then other else sym - -extension (tp: TermRef | ThisType) - /** The nesting level of this reference as defined by capture checking */ - def ccNestingLevel(using Context): Int = tp match - case tp: TermRef => tp.symbol.ccNestingLevel - case tp: ThisType => tp.cls.ccNestingLevel + if !other.exists || other.isContainedIn(sym) then sym + else if !sym.exists || sym.isContainedIn(other) then other + else sym.owner.minNested(other.owner) extension (tp: AnnotatedType) /** Is this a boxed capturing type? */ def isBoxed(using Context): Boolean = tp.annot match case ann: CaptureAnnotation => ann.boxed case _ => false - -extension (ts: List[Type]) - /** Equivalent to ts.mapconserve(_.boxedUnlessFun(tycon)) but more efficient where - * it is the identity. - */ - def boxedUnlessFun(tycon: Type)(using Context) = - if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionClass(tycon.typeSymbol) - then ts - else ts.mapconserve(_.boxed) - diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 4e1241518963..68323120c75b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -4,108 +4,138 @@ package cc import core.* import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import config.Printers.capt import Hashable.Binders import printing.Showable import util.SimpleIdentitySet -import Decorators.i +import Decorators.* +import StdNames.nme import scala.annotation.constructorOnly +import scala.annotation.internal.sharable type CaptureRoot = TermRef | CaptureRoot.Var object CaptureRoot: - case class Var(owner: Symbol, source: Symbol = NoSymbol)(using @constructorOnly ictx: Context) extends CaptureRef, Showable: + @sharable private var nextId = 0 - var upperBound: Symbol = owner - var lowerBound: Symbol = NoSymbol - var upperLevel: Int = owner.ccNestingLevel - var lowerLevel: Int = Int.MinValue - private[CaptureRoot] var lowerRoots: SimpleIdentitySet[Var] = SimpleIdentitySet.empty - private[CaptureRoot] var upperRoots: SimpleIdentitySet[Var] = SimpleIdentitySet.empty - private[CaptureRoot] var alias: CaptureRoot = this + case class Var(owner: Symbol, source: Symbol)(using @constructorOnly ictx: Context) extends CaptureRef, Showable: + + val id = + nextId += 1 + nextId + + var innerLimit: Symbol = owner.levelOwner + var outerLimit: Symbol = defn.RootClass + var outerRoots: SimpleIdentitySet[Var] = SimpleIdentitySet.empty - override def localRootOwner(using Context) = owner override def isTrackableRef(using Context): Boolean = true override def captureSetOfInfo(using Context) = CaptureSet.universal - def setAlias(target: CaptureRoot) = - alias = target + private var myAlias: CaptureRoot = this + def alias = myAlias + def alias_=(r: CaptureRoot)(using Context) = + //assert(id != 2, i"$this := $r") + alias match + case alias: TermRef => + val owner = alias.localRootOwner + assert( + owner.isContainedIn(outerLimit) && innerLimit.isContainedIn(owner), + i"illegal alias $owner for $this") + case _ => + myAlias = r - def followAlias: CaptureRoot = alias match - case alias: Var if alias ne this => alias.followAlias - case _ => this + /** A fresh var with the same limits and outerRoots as this one */ + def fresh(using Context): Var = + val r = Var(owner, NoSymbol) + r.innerLimit = innerLimit + r.outerLimit = outerLimit + r.outerRoots = outerRoots + r - def locallyConsistent = - lowerLevel <= upperLevel - && lowerRoots.forall(_.upperLevel <= upperLevel) - && upperRoots.forall(_.lowerLevel >= lowerLevel) + /** A fresh var that is enclosed by all roots in `rs`. + * @throws A NoCommonRoot exception if this is not possible + * since root scopes dont' overlap. + */ + def freshEnclosedBy(rs: CaptureRoot*)(using Context): CaptureRoot = + val r = fresh + if rs.forall(_.encloses(r)) then r else throw NoCommonRoot(rs*) def computeHash(bs: Binders): Int = hash def hash: Int = System.identityHashCode(this) def underlying(using Context): Type = defn.Caps_Cap.typeRef end Var - def isEnclosingRoot(c1: CaptureRoot, c2: CaptureRoot)(using Context): Boolean = - if c1 eq c2 then return true - c1 match - case c1: Var if c1.alias ne c1 => return isEnclosingRoot(c1.alias, c2) - case _ => - c2 match - case c2: Var if c2.alias ne c2 => return isEnclosingRoot(c1, c2.alias) - case _ => - (c1, c2) match - case (c1: TermRef, c2: TermRef) => - c1.ccNestingLevel <= c2.ccNestingLevel - case (c1: TermRef, c2: Var) => - val level1 = c1.ccNestingLevel - if level1 <= c2.lowerLevel then - true // no change - else if level1 <= c2.upperLevel && c2.upperRoots.forall(isEnclosingRoot(c1, _)) then - if level1 == c2.upperLevel then - c2.alias = c1 + class NoCommonRoot(rs: CaptureRoot*)(using Context) extends Exception( + i"No common capture root nested in ${rs.mkString(" and ")}" + ) + + extension (r: CaptureRoot) + + def followAlias(using Context): CaptureRoot = r match + case r: Var if r.alias ne r => r.alias.followAlias + case _ => r + + def unifiesWith(other: CaptureRoot)(using Context): Boolean = + r.encloses(other) && other.encloses(r) + + def encloses(other: CaptureRoot)(using Context): Boolean = + val (r1, r2) = (followAlias, other.followAlias) + (r1 eq r2) || (r1, r2).match + case (r1: TermRef, r2: TermRef) => + r2.localRootOwner.isContainedIn(r1.localRootOwner) + case (r1: TermRef, r2: Var) => + val r1Owner = r1.localRootOwner + if r2.outerLimit.isContainedIn(r1Owner) then true + else if !r2.innerLimit.isContainedIn(r1Owner) then false else - c2.lowerBound = c1.symbol - c2.lowerLevel = level1 - true - else false - case (c1: Var, c2: TermRef) => - val level2 = c2.ccNestingLevel - if c1.upperLevel <= level2 then - true // no change - else if c1.lowerLevel <= level2 && c1.lowerRoots.forall(isEnclosingRoot(_, c2)) then - if level2 == c1.lowerLevel then - c1.alias = c2 + if r2.innerLimit == r1Owner then r2.alias = r1 + else r2.outerLimit = r1Owner + true + case (r1: Var, r2: TermRef) => + val r2Owner = r2.localRootOwner + if r2Owner.isContainedIn(r1.innerLimit) then true + else if !r2Owner.isContainedIn(r1.outerLimit) then false else - c1.upperBound = c2.symbol - c1.upperLevel = level2 - true - else false - case (c1: Var, c2: Var) => - if c1.upperRoots.contains(c2) then - true // no change - else if c1.lowerLevel > c2.upperLevel then - false // local inconsistency - else - c1.upperRoots += c2 // set early to prevent infinite looping - if c1.lowerRoots.forall(isEnclosingRoot(_, c2)) - && c2.upperRoots.forall(isEnclosingRoot(c1, _)) - then - if c1.lowerRoots.contains(c2) then - val c2a = c2.followAlias - if c2a ne c1 then c1.alias = c2a - else - if c1.upperLevel > c2.upperLevel then - c1.upperBound = c2.upperBound - c1.upperLevel = c2.upperLevel - if c2.lowerLevel < c1.lowerLevel then - c2.lowerBound = c1.lowerBound - c2.lowerLevel = c1.lowerLevel - c2.lowerRoots += c1 + if r1.outerLimit == r2Owner then r1.alias = r2 + else r1.innerLimit = r2Owner + true + case (r1: Var, r2: Var) => + if r2.outerRoots.contains(r1) then true // no change + else if !r2.innerLimit.isContainedIn(r1.outerLimit) then false // no overlap + else if r1.outerRoots.contains(r2) then // unify + r1.alias = r2 + r2.outerLimit = + r1.outerLimit.maxNested(r2.outerLimit, + onConflict = (_, _) => throw NoCommonRoot(r1, r2)) + r2.innerLimit = r1.innerLimit.minNested(r2.innerLimit) true else - c1.upperRoots -= c2 - false - end isEnclosingRoot + r2.outerRoots += r1 // set early to prevent infinite looping + if r1.outerRoots.forall(_.encloses(r2)) then true + else + r2.outerRoots -= r2 + false + end encloses + end extension + + /** The capture root enclosed by `root1` and `root2`. + * If one of these is a Var, create a fresh Var with the appropriate constraints. + * If the scopes of `root1` and `root2` don't overlap, thow a `NoCommonRoot` exception. + */ + def lub(root1: CaptureRoot, root2: CaptureRoot)(using Context): CaptureRoot = + val (r1, r2) = (root1.followAlias, root2.followAlias) + if r1 eq r2 then r1 + else (r1, r2) match + case (r1: TermRef, r2: TermRef) => + r1.localRootOwner.maxNested(r2.localRootOwner, + onConflict = (_, _) => throw NoCommonRoot(r1, r2) + ).termRef + case (r1: TermRef, r2: Var) => + r2.freshEnclosedBy(r1, r2) + case (r1: Var, r2) => + r1.freshEnclosedBy(r1, r2) + end CaptureRoot diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 5fdbea2a1bc6..748fd36f6be7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -4,7 +4,7 @@ package cc import core.* import Types.*, Symbols.*, Flags.*, Contexts.*, Decorators.* -import config.Printers.capt +import config.Printers.{capt, ccSetup} import Annotations.Annotation import annotation.threadUnsafe import annotation.constructorOnly @@ -56,10 +56,14 @@ sealed abstract class CaptureSet extends Showable: */ def isAlwaysEmpty: Boolean - /** The level owner in which the set is defined. Sets can only take - * elements with nesting level up to the cc-nestinglevel of owner. + /** An optinal level limit, or NoSymbol if none exists. All elements of the set + * must be in scopes visible from the level limit. */ - def owner: Symbol + def levelLimit: Symbol + + def rootSet(using Context): CaptureSet = + assert(levelLimit.exists, this) + levelLimit.localRoot.termRef.singletonCaptureSet /** Is this capture set definitely non-empty? */ final def isNotEmpty: Boolean = !elems.isEmpty @@ -83,6 +87,12 @@ sealed abstract class CaptureSet extends Showable: case _ => false } + /** Does this capture set contain the root reference `cap` as element? */ + final def containsRoot(using Context) = + elems.exists: + case ref: TermRef => ref.isRootCapability + case _ => false + /** Add new elements to this capture set if allowed. * @pre `newElems` is not empty and does not overlap with `this.elems`. * Constant capture sets never allow to add new elements. @@ -92,7 +102,12 @@ sealed abstract class CaptureSet extends Showable: * @return CompareResult.OK if elements were added, or a conflicting * capture set that prevents addition otherwise. */ - protected def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult + protected def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + (CompareResult.OK /: newElems): (r, elem) => + r.andAlso(addNewElem(elem, origin)) + + /** Add a single element, with the same meaning as in `addNewElems` */ + protected def addNewElem(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult /** If this is a variable, add `cs` as a dependent set */ protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult @@ -129,15 +144,15 @@ sealed abstract class CaptureSet extends Showable: private def subsumes(y: CaptureRef) = (x eq y) || y.match - case y: TermRef => (y.prefix eq x) || x.isRootIncluding(y) - case y: CaptureRoot.Var => x.isRootIncluding(y) + case y: TermRef => (y.prefix eq x) || y.isLocalRootCapability && x.isSuperRootOf(y) + case y: CaptureRoot.Var => x.isSuperRootOf(y) case _ => false - || (x.isGenericRootCapability || y.isGenericRootCapability && x.isRootCapability) + || (x.isGenericRootCapability || y.isRootCapability && x.isRootCapability) && ctx.property(LooseRootChecking).isDefined - private def isRootIncluding(y: CaptureRoot) = - x.isLocalRootCapability && y.isLocalRootCapability - && CaptureRoot.isEnclosingRoot(y, x.asInstanceOf[CaptureRoot]) + private def isSuperRootOf(y: CaptureRoot) = x match + case x: CaptureRoot if x.isLocalRootCapability => y.encloses(x) + case _ => false end extension /** {x} <:< this where <:< is subcapturing, but treating all variables @@ -191,7 +206,8 @@ sealed abstract class CaptureSet extends Showable: case elem :: elems1 => var result = that.tryInclude(elem, this) if !result.isOK && !elem.isRootCapability && summon[VarState] != FrozenState then - result = elem.captureSetOfInfo.subCaptures(that) + ccState.levelError = ccState.levelError.orElse(result.levelError) + result = result.orElse(elem.captureSetOfInfo.subCaptures(that)) if result.isOK then recur(elems1) else @@ -200,7 +216,7 @@ sealed abstract class CaptureSet extends Showable: case Nil => addDependent(that) recur(elems.toList) - .showing(i"subcaptures $this <:< $that = ${result.show}", capt) + //.showing(i"subcaptures $this <:< $that = ${result.show}", capt) /** Two capture sets are considered =:= equal if they mutually subcapture each other * in a frozen state. @@ -216,7 +232,9 @@ sealed abstract class CaptureSet extends Showable: if this.subCaptures(that, frozen = true).isOK then that else if that.subCaptures(this, frozen = true).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) - else Var(this.owner.maxNested(that.owner), this.elems ++ that.elems) + else Var( + this.levelLimit.maxNested(that.levelLimit, onConflict = (sym1, sym2) => sym1), + this.elems ++ that.elems) .addAsDependentTo(this).addAsDependentTo(that) /** The smallest superset (via <:<) of this capture set that also contains `ref`. @@ -256,6 +274,11 @@ sealed abstract class CaptureSet extends Showable: else Const(elems.filter(p)) else Filtered(asVar, p) + /** This set with a new owner and therefore also a new levelLimit */ + def changeOwner(newOwner: Symbol)(using Context): CaptureSet = + if this.isConst then this + else ChangedOwner(asVar, newOwner) + /** Capture set obtained by applying `tm` to all elements of the current capture set * and joining the results. If the current capture set is a variable, the same * transformation is applied to all future additions of new elements. @@ -297,6 +320,22 @@ sealed abstract class CaptureSet extends Showable: def substParams(tl: BindingType, to: List[Type])(using Context) = map(Substituters.SubstParamsMap(tl, to)) + /** The capture root that corresponds to this capture set. This is: + * - if the capture set is a Var with a defined level limit, the + * associated capture root, + * - otherwise, if the set is nonempty, the innermost root such + * that some element of the set subcaptures this root, + * - otherwise, if the set is empty, `default`. + */ + def impliedRoot(default: CaptureRoot)(using Context): CaptureRoot = + if levelLimit.exists then levelLimit.localRoot.termRef + else if elems.isEmpty then default + else elems.toList + .map: + case elem: CaptureRoot if elem.isLocalRootCapability => elem + case elem => elem.captureSetOfInfo.impliedRoot(default) + .reduce((x: CaptureRoot, y: CaptureRoot) => CaptureRoot.lub(x, y)) + /** Invoke handler if this set has (or later aquires) the root capability `cap` */ def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = if isUniversal then handler() @@ -305,8 +344,8 @@ sealed abstract class CaptureSet extends Showable: /** Invoke handler on the elements to ensure wellformedness of the capture set. * The handler might add additional elements to the capture set. */ - def ensureWellformed(handler: List[CaptureRef] => Context ?=> Unit)(using Context): this.type = - handler(elems.toList) + def ensureWellformed(handler: CaptureRef => Context ?=> Unit)(using Context): this.type = + elems.foreach(handler(_)) this /** An upper approximation of this capture set, i.e. a constant set that is @@ -382,8 +421,8 @@ object CaptureSet: def isConst = true def isAlwaysEmpty = elems.isEmpty - def addNewElems(elems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - CompareResult.fail(this) + def addNewElem(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = + CompareResult.Fail(this :: Nil) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -391,7 +430,7 @@ object CaptureSet: def withDescription(description: String): Const = Const(elems, description) - def owner = NoSymbol + def levelLimit = NoSymbol override def toString = elems.toString end Const @@ -404,7 +443,7 @@ object CaptureSet: */ object Fluid extends Const(emptySet): override def isAlwaysEmpty = false - override def addNewElems(elems: Refs, origin: CaptureSet)(using Context, VarState) = CompareResult.OK + override def addNewElem(elem: CaptureRef, origin: CaptureSet)(using Context, VarState) = CompareResult.OK override def accountsFor(x: CaptureRef)(using Context): Boolean = true override def mightAccountFor(x: CaptureRef)(using Context): Boolean = true override def toString = "" @@ -418,16 +457,14 @@ object CaptureSet: varId += 1 varId - override val owner = directOwner.levelOwner + //assert(id != 95) + + override val levelLimit = + if directOwner.exists then directOwner.levelOwner else NoSymbol /** A variable is solved if it is aproximated to a from-then-on constant set. */ private var isSolved: Boolean = false - private var ownLevelCache = -1 - private def ownLevel(using Context) = - if ownLevelCache == -1 then ownLevelCache = owner.ccNestingLevel - ownLevelCache - /** The elements currently known to be in the set */ var elems: Refs = initialElems @@ -443,12 +480,10 @@ object CaptureSet: var rootAddedHandler: () => Context ?=> Unit = () => () /** A handler to be invoked when new elems are added to this set */ - var newElemAddedHandler: List[CaptureRef] => Context ?=> Unit = _ => () + var newElemAddedHandler: CaptureRef => Context ?=> Unit = _ => () var description: String = "" - private var triedElem: Option[CaptureRef] = None - /** Record current elements in given VarState provided it does not yet * contain an entry for this variable. */ @@ -473,50 +508,35 @@ object CaptureSet: def resetDeps()(using state: VarState): Unit = deps = state.deps(this) - def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + final def addNewElem(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = if isConst || !recordElemsState() then - CompareResult.fail(this) // fail if variable is solved or given VarState is frozen - else if newElems.exists(!levelOK(_)) then - val res = widenCaptures(newElems) match - case Some(newElems1) => tryInclude(newElems1, origin) - case None => CompareResult.fail(this) - if !res.isOK then recordLevelError() - res + CompareResult.Fail(this :: Nil) // fail if variable is solved or given VarState is frozen + else if !levelOK(elem) then + val res = CompareResult.LevelError(this, elem) + if elem.isRootCapability then res + else res.orElse(addNewElems(elem.captureSetOfInfo.elems, origin)) else - //assert(id != 2, newElems) - elems ++= newElems - if isUniversal then rootAddedHandler() - newElemAddedHandler(newElems.toList) + //assert(id != 19 || !elem.isLocalRootCapability, elem.asInstanceOf[TermRef].localRootOwner) + elems += elem + if elem.isGenericRootCapability then rootAddedHandler() + newElemAddedHandler(elem) // assert(id != 5 || elems.size != 3, this) (CompareResult.OK /: deps) { (r, dep) => - r.andAlso(dep.tryInclude(newElems, this)) - } - - private def recordLevelError()(using Context): Unit = - for elem <- triedElem do - ccState.levelError = Some((elem, this)) - - private def levelOK(elem: CaptureRef)(using Context): Boolean = elem match - case elem: (TermRef | ThisType) => elem.ccNestingLevel <= ownLevel - case elem: CaptureRoot.Var => CaptureRoot.isEnclosingRoot(elem, owner.localRoot.termRef) - case _ => true - - private def widenCaptures(elems: Refs)(using Context): Option[Refs] = - val res = optional: - (SimpleIdentitySet[CaptureRef]() /: elems): (acc, elem) => - if levelOK(elem) then acc + elem - else - val saved = triedElem - triedElem = triedElem.orElse(Some(elem)) - if elem.isRootCapability then break() - val res = acc ++ widenCaptures(elem.captureSetOfInfo.elems).? - triedElem = saved // reset only in case of success, leave as is on error - res - def resStr = res match - case Some(refs) => i"${refs.toList}" - case None => "FAIL" - capt.println(i"widen captures ${elems.toList} for $this at $owner = $resStr") - res + r.andAlso(dep.tryInclude(elem, this)) + }.addToTrace(this) + + private def levelOK(elem: CaptureRef)(using Context): Boolean = + !levelLimit.exists + || elem.match + case elem: TermRef => + var sym = elem.symbol + if sym.isLevelOwner then sym = sym.owner + levelLimit.isContainedIn(sym.levelOwner) + case elem: ThisType => + levelLimit.isContainedIn(elem.cls.levelOwner) + case elem: CaptureRoot.Var => + elem.encloses(levelLimit.localRoot.termRef) + case _ => true def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult = if (cs eq this) || cs.isUniversal || isConst then @@ -525,13 +545,13 @@ object CaptureSet: deps += cs CompareResult.OK else - CompareResult.fail(this) + CompareResult.Fail(this :: Nil) override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = rootAddedHandler = handler super.disallowRootCapability(handler) - override def ensureWellformed(handler: List[CaptureRef] => (Context) ?=> Unit)(using Context): this.type = + override def ensureWellformed(handler: CaptureRef => (Context) ?=> Unit)(using Context): this.type = newElemAddedHandler = handler super.ensureWellformed(handler) @@ -539,13 +559,14 @@ object CaptureSet: /** Roughly: the intersection of all constant known supersets of this set. * The aim is to find an as-good-as-possible constant set that is a superset - * of this set. The universal set {cap} is a sound fallback. + * of this set. The associated root set {cap[owner]} is a sound fallback. */ final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = if isConst then this else if elems.exists(_.isRootCapability) then CaptureSet(elems.filter(_.isRootCapability).toList*) - else if computingApprox then universal + else if computingApprox then + rootSet else computingApprox = true try computeApprox(origin).ensuring(_.isConst) @@ -553,7 +574,7 @@ object CaptureSet: /** The intersection of all upper approximations of dependent sets */ protected def computeApprox(origin: CaptureSet)(using Context): CaptureSet = - (universal /: deps) { (acc, sup) => acc ** sup.upperApprox(this) } + (rootSet /: deps) { (acc, sup) => acc ** sup.upperApprox(this) } /** Widen the variable's elements to its upper approximation and * mark it as constant from now on. This is used for contra-variant type variables @@ -586,11 +607,11 @@ object CaptureSet: for vars <- ctx.property(ShownVars) do vars += this val debugInfo = if !isConst && ctx.settings.YccDebug.value then ids else "" - val nestingInfo = - if ctx.settings.YprintLevel.value - then s"" + val limitInfo = + if ctx.settings.YprintLevel.value && levelLimit.exists + then i"" else "" - debugInfo ++ nestingInfo + debugInfo ++ limitInfo /** Used for diagnostics and debugging: A string that traces the creation * history of a variable by following source links. Each variable on the @@ -602,8 +623,8 @@ object CaptureSet: val trail = this.match case dv: DerivedVar => dv.source.ids case _ => "" - s"$id${getClass.getSimpleName.nn.take(1)}$trail" - + val descr = getClass.getSimpleName.nn.take(1) + s"$id$descr$trail" override def toString = s"Var$id$elems" end Var @@ -643,7 +664,7 @@ object CaptureSet: */ class Mapped private[CaptureSet] (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) - extends DerivedVar(source.owner, initial.elems): + extends DerivedVar(source.levelLimit, initial.elems): addAsDependentTo(initial) // initial mappings could change by propagation private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap] @@ -668,7 +689,7 @@ object CaptureSet: .andAlso { if added.isConst then CompareResult.OK else if added.asVar.recordDepsState() then { addAsDependentTo(added); CompareResult.OK } - else CompareResult.fail(this) + else CompareResult.Fail(this :: Nil) } .andAlso { if (origin ne source) && (origin ne initial) && mapIsIdempotent then @@ -685,7 +706,7 @@ object CaptureSet: // we approximate types resulting from such maps by returning a possible super type // from the actual type. But this is neither sound nor complete. report.warning(em"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") - CompareResult.fail(this) + CompareResult.Fail(this :: Nil) else CompareResult.OK } @@ -694,7 +715,7 @@ object CaptureSet: if source eq origin then // it's a mapping of origin, so not a superset of `origin`, // therefore don't contribute to the intersection. - universal + rootSet else source.upperApprox(this).map(tm) @@ -709,7 +730,7 @@ object CaptureSet: */ final class BiMapped private[CaptureSet] (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) - extends DerivedVar(source.owner, initialElems): + extends DerivedVar(source.levelLimit, initialElems): override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = if origin eq source then @@ -736,10 +757,14 @@ object CaptureSet: override def toString = s"BiMapped$id($source, elems = $elems)" end BiMapped + /** Same as `source` but with a new directOwner */ + class ChangedOwner private[CaptureSet](val source: Var, newOwner: Symbol)(using @constructorOnly ctx: Context) + extends DerivedVar(newOwner, source.elems) + /** A variable with elements given at any time as { x <- source.elems | p(x) } */ class Filtered private[CaptureSet] (val source: Var, p: Context ?=> CaptureRef => Boolean)(using @constructorOnly ctx: Context) - extends DerivedVar(source.owner, source.elems.filter(p)): + extends DerivedVar(source.levelLimit, source.elems.filter(p)): override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = val filtered = newElems.filter(p) @@ -751,14 +776,14 @@ object CaptureSet: super.addNewElems(newElems, origin) .andAlso { if filtered.size == newElems.size then source.tryInclude(newElems, this) - else CompareResult.fail(this) + else CompareResult.Fail(this :: Nil) } override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = if source eq origin then // it's a filter of origin, so not a superset of `origin`, // therefore don't contribute to the intersection. - universal + rootSet else source.upperApprox(this).filter(p) @@ -770,7 +795,7 @@ object CaptureSet: extends Filtered(source, !other.accountsFor(_)) class Intersected(cs1: CaptureSet, cs2: CaptureSet)(using Context) - extends Var(cs1.owner.minNested(cs2.owner), elemIntersection(cs1, cs2)): + extends Var(cs1.levelLimit.minNested(cs2.levelLimit), elemIntersection(cs1, cs2)): addAsDependentTo(cs1) addAsDependentTo(cs2) deps += cs1 @@ -789,7 +814,7 @@ object CaptureSet: if (origin eq cs1) || (origin eq cs2) then // it's a combination of origin with some other set, so not a superset of `origin`, // therefore don't contribute to the intersection. - universal + rootSet else CaptureSet(elemIntersection(cs1.upperApprox(this), cs2.upperApprox(this))) @@ -848,25 +873,47 @@ object CaptureSet: /** A TypeMap that is the identity on capture references */ trait IdentityCaptRefMap extends TypeMap - type CompareResult = CompareResult.TYPE + enum CompareResult extends Showable: + case OK + case Fail(trace: List[CaptureSet]) + case LevelError(cs: CaptureSet, elem: CaptureRef) + + override def toText(printer: Printer): Text = + inContext(printer.printerContext): + this match + case OK => Str("OK") + case Fail(trace) => + if ctx.settings.YccDebug.value then printer.toText(trace, ", ") + else blocking.show + case LevelError(cs: CaptureSet, elem: CaptureRef) => + Str(i"($elem at wrong level for $cs in ${cs.levelLimit})") + + /** The result is OK */ + def isOK: Boolean = this == OK + + /** If not isOK, the blocking capture set */ + def blocking: CaptureSet = (this: @unchecked) match + case Fail(cs) => cs.last + case LevelError(cs, _) => cs + + /** Optionally, this result if it is a level error */ + def levelError: Option[LevelError] = this match + case result: LevelError => Some(result) + case _ => None + + inline def andAlso(op: Context ?=> CompareResult)(using Context): CompareResult = + if isOK then op else this + + inline def orElse(op: Context ?=> CompareResult)(using Context): CompareResult = + if isOK then this + else + val alt = op + if alt.isOK then alt + else this - /** The result of subcapturing comparisons is an opaque type CompareResult.TYPE. - * This is either OK, indicating success, or - * another capture set, indicating failure. The failure capture set - * is the one that did not allow propagaton of elements into it. - */ - object CompareResult: - opaque type TYPE = CaptureSet - val OK: TYPE = Const(emptySet) - def fail(cs: CaptureSet): TYPE = cs - - extension (result: TYPE) - /** The result is OK */ - def isOK: Boolean = result eq OK - /** If not isOK, the blocking capture set */ - def blocking: CaptureSet = result - inline def andAlso(op: Context ?=> TYPE)(using Context): TYPE = if result.isOK then op else result - def show(using Context): String = if result.isOK then "OK" else i"$result" + inline def addToTrace(cs: CaptureSet) = this match + case Fail(trace) => Fail(cs :: trace) + case _ => this end CompareResult /** A VarState serves as a snapshot mechanism that can undo @@ -954,10 +1001,16 @@ object CaptureSet: css.foldLeft(empty)(_ ++ _) */ - /** The capture set of the type underlying a CaptureRef */ + /** The capture set of the type underlying CaptureRef */ def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match - case ref: TermRef if ref.isRootCapability => ref.singletonCaptureSet - case _ => ofType(ref.underlying, followResult = true) + case ref: TermRef if ref.isRootCapability => + ref.singletonCaptureSet + case ref: TermRef if ref.symbol.isLevelOwner => + ofType(ref.underlying, followResult = true).filter( + ref.symbol.localRoot.termRef != _) + // TODO: Can replace filter with - ref.symbol.localRoot.termRef when we drop level nesting + case _ => + ofType(ref.underlying, followResult = true) /** Capture set of a type */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = @@ -1035,16 +1088,14 @@ object CaptureSet: def levelErrors: Addenda = new Addenda: override def toAdd(using Context) = - for - state <- ctx.property(ccStateKey).toList - (ref, cs) <- state.levelError - yield + for CompareResult.LevelError(cs, ref) <- ccState.levelError.toList yield + ccState.levelError = None val levelStr = ref match - case ref: (TermRef | ThisType) => i", defined at level ${ref.ccNestingLevel}" + case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}" case _ => "" i""" | |Note that reference ${ref}$levelStr - |cannot be included in outer capture set $cs, defined at level ${cs.owner.nestingLevel} in ${cs.owner}""" + |cannot be included in outer capture set $cs which is associated with ${cs.levelLimit}""" end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index a7c283f4cc3b..294414a6c37b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -4,6 +4,7 @@ package cc import core.* import Types.*, Symbols.*, Contexts.* +import Decorators.i /** A (possibly boxed) capturing type. This is internally represented as an annotated type with a @retains * or @retainsByName annotation, but the extractor will succeed only at phase CheckCaptures. @@ -25,58 +26,62 @@ import Types.*, Symbols.*, Contexts.* */ object CapturingType: - /** Smart constructor that drops empty capture sets and fuses compatible capturiong types. + /** Smart constructor that + * - drops empty capture sets + * - drops capability class ecpansion if they are refined with another capturing type + * - fuses compatible capturiong types. * An outer type capturing type A can be fused with an inner capturing type B if their * boxing status is the same or if A is boxed. */ def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type = if refs.isAlwaysEmpty then parent else parent match + case parent @ CapturingType(parent1, refs1) if refs1 eq defn.expandedUniversalSet => + apply(parent1, refs, boxed) case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed => apply(parent1, refs ++ refs1, boxed) case _ => AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) - /** An extractor that succeeds only during CheckCapturingPhase. Boxing statis is - * returned separately by CaptureOps.isBoxed. + /** An extractor for CapturingTypes. Capturing types are recognized if + * - the annotation is a CaptureAnnotation and we are not past CheckCapturingPhase, or + * - the annotation is a @retains and we are in CheckCapturingPhase, + * but not if the IgnoreCaptures mode is set. + * Boxing status is returned separately by CaptureOps.isBoxed. */ def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = - if ctx.phase == Phases.checkCapturesPhase - && tp.annot.symbol == defn.RetainsAnnot - && !ctx.mode.is(Mode.IgnoreCaptures) - then - EventuallyCapturingType.unapply(tp) - else None + if ctx.mode.is(Mode.IgnoreCaptures) then None + else decomposeCapturingType(tp) + + /** Decompose `tp` as a capturing type without taking IgnoreCaptures into account */ + def decomposeCapturingType(tp: Type)(using Context): Option[(Type, CaptureSet)] = tp match + case AnnotatedType(parent, ann: CaptureAnnotation) + if isCaptureCheckingOrSetup => + Some((parent, ann.refs)) + case AnnotatedType(parent, ann) + if ann.symbol == defn.RetainsAnnot && isCaptureChecking => + // There are some circumstances where we cannot map annotated types + // with retains annotations to capturing types, so this second recognizer + // path still has to exist. One example is when checking capture sets + // of dependent function type results for well-formedness. E.g. in + // `(x: C^{f}) -> () ->{x} Unit` we need to check that the capture set of + // `x` is not empty. We use the original, untransformed type for that + // since the transformed type already normalizes capture sets which would + // drop subsumed references. But the original type refers to the untransfomed + // type `C^{f}` which does not have a capture annotation yet. The transformed + // type would be in a copy of the dependent function type, but it is useless + // since we need to check the original reference. + try Some((parent, ann.tree.toCaptureSet)) + catch case ex: IllegalCaptureRef => None + case _ => + None /** Check whether a type is uncachable when computing `baseType`. - * - Avoid caching all the types during the setup phase, since at that point - * the capture set variables are not fully installed yet. - * - Avoid caching capturing types when IgnoreCaptures mode is set, since the - * capture sets may be thrown away in the computed base type. - */ + * We avoid caching capturing types when IgnoreCaptures mode is set, since the + */ def isUncachable(tp: Type)(using Context): Boolean = - ctx.phase == Phases.checkCapturesPhase && - (Setup.isDuringSetup || ctx.mode.is(Mode.IgnoreCaptures) && tp.isEventuallyCapturingType) + ctx.mode.is(Mode.IgnoreCaptures) && decomposeCapturingType(tp).isDefined end CapturingType -/** An extractor for types that will be capturing types at phase CheckCaptures. Also - * included are types that indicate captures on enclosing call-by-name parameters - * before phase ElimByName. - */ -object EventuallyCapturingType: - - def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = - val sym = tp.annot.symbol - if sym == defn.RetainsAnnot || sym == defn.RetainsByNameAnnot then - tp.annot match - case ann: CaptureAnnotation => - Some((tp.parent, ann.refs)) - case ann => - try Some((tp.parent, ann.tree.toCaptureSet)) - catch case ex: IllegalCaptureRef => None - else None - -end EventuallyCapturingType - diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b9904db4ea41..c09b8cb9da45 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -6,13 +6,14 @@ import core.* import Phases.*, DenotTransformers.*, SymDenotations.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* import Types.*, StdNames.*, Denotations.* -import config.Printers.{capt, recheckr} +import config.Printers.{capt, recheckr, ccSetup} import config.{Config, Feature} import ast.{tpd, untpd, Trees} import Trees.* import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker} import typer.Checking.{checkBounds, checkAppliedTypesIn} -import typer.ErrorReporting.Addenda +import typer.ErrorReporting.{Addenda, err} +import typer.ProtoTypes.AnySelectionProto import util.{SimpleIdentitySet, EqHashMap, SrcPos, Property} import transform.SymUtils.* import transform.{Recheck, PreRecheck} @@ -27,26 +28,6 @@ import reporting.trace object CheckCaptures: import ast.tpd.* - class Pre extends PreRecheck, SymTransformer: - - override def isRunnable(using Context) = super.isRunnable && Feature.ccEnabledSomewhere - - /** - Reset `private` flags of parameter accessors so that we can refine them - * in Setup if they have non-empty capture sets. - * - Special handling of some symbols defined for case classes. - * Enabled only until recheck is finished, and provided some compilation unit - * is CC-enabled. - */ - def transformSym(sym: SymDenotation)(using Context): SymDenotation = - if !pastRecheck && Feature.ccEnabledSomewhere then - if sym.isAllOf(PrivateParamAccessor) && !sym.hasAnnotation(defn.ConstructorOnlyAnnot) then - sym.copySymDenotation(initFlags = sym.flags &~ Private | Recheck.ResetPrivate) - else if Synthetics.needsTransform(sym) then - Synthetics.transform(sym) - else sym - else sym - end Pre - enum EnvKind: case Regular // normal case case NestedInOwner // environment is a temporary one nested in the owner's environment, @@ -73,6 +54,14 @@ object CheckCaptures: /** If an environment is open it tracks free references */ def isOpen = !captured.isAlwaysEmpty && kind != EnvKind.Boxed + + def outersIterator: Iterator[Env] = new: + private var cur = Env.this + def hasNext = !cur.isOutermost + def next(): Env = + val res = cur + cur = cur.outer + res end Env /** Similar normal substParams, but this is an approximating type map that @@ -140,7 +129,7 @@ object CheckCaptures: case _: SingletonType => report.error(em"Singleton type $parent cannot have capture set", parent.srcPos) case _ => - for elem <- retainedElems(ann) do + for elem <- ann.retainedElems do elem match case QualifiedRoot(outer) => // Will be checked by Setup's checkOuterRoots @@ -151,38 +140,6 @@ object CheckCaptures: case tpe => report.error(em"$elem: $tpe is not a legal element of a capture set", elem.srcPos) - /** If `tp` is a capturing type, check that all references it mentions have non-empty - * capture sets. Also: warn about redundant capture annotations. - * This check is performed after capture sets are computed in phase cc. - */ - def checkWellformedPost(tp: Type, pos: SrcPos)(using Context): Unit = tp match - case CapturingType(parent, refs) => - for ref <- refs.elems do - if ref.captureSetOfInfo.elems.isEmpty then - report.error(em"$ref cannot be tracked since its capture set is empty", pos) - else if parent.captureSet.accountsFor(ref) then - report.warning(em"redundant capture: $parent already accounts for $ref in $tp", pos) - case _ => - - /** Warn if `ann`, which is the tree of a @retains annotation, defines some elements that - * are already accounted for by other elements of the same annotation. - * Note: We need to perform the check on the original annotation rather than its - * capture set since the conversion to a capture set already eliminates redundant elements. - */ - def warnIfRedundantCaptureSet(ann: Tree, tpt: Tree)(using Context): Unit = - var retained = retainedElems(ann).toArray - for i <- 0 until retained.length do - val refTree = retained(i) - val ref = refTree.toCaptureRef - val others = for j <- 0 until retained.length if j != i yield retained(j).toCaptureRef - val remaining = CaptureSet(others*) - if remaining.accountsFor(ref) then - val srcTree = - if refTree.span.exists then refTree - else if ann.span.exists then ann - else tpt - report.warning(em"redundant capture: $remaining already accounts for $ref", srcTree.srcPos) - /** Attachment key for bodies of closures, provided they are values */ val ClosureBodyValue = Property.Key[Unit] @@ -195,19 +152,17 @@ class CheckCaptures extends Recheck, SymTransformer: def phaseName: String = "cc" override def isRunnable(using Context) = super.isRunnable && Feature.ccEnabledSomewhere + override def firstPrepPhase = preRecheckPhase.prev.asInstanceOf[AddTryOwners] def newRechecker()(using Context) = CaptureChecker(ctx) - private val state = new CCState - override def run(using Context): Unit = if Feature.ccEnabled then super.run - - override def printingContext(ctx: Context) = ctx.withProperty(ccStateKey, Some(new CCState)) + + val ccState = new CCState class CaptureChecker(ictx: Context) extends Rechecker(ictx): - import ast.tpd.* override def keepType(tree: Tree) = super.keepType(tree) @@ -218,30 +173,24 @@ class CheckCaptures extends Recheck, SymTransformer: */ private def interpolator(startingVariance: Int = 1)(using Context) = new TypeTraverser: variance = startingVariance - override def traverse(t: Type) = - t match - case CapturingType(parent, refs: CaptureSet.Var) => - if variance < 0 then - capt.println(i"solving $t") - refs.solve() - if ctx.owner.isLevelOwner then - // instantiate root vars with upper bound ctx.owner to its local root - for ref <- refs.elems do ref match - case ref: CaptureRoot.Var => ref.followAlias match - case rv: CaptureRoot.Var if rv.upperBound == ctx.owner => - val inst = ctx.owner.localRoot.termRef - capt.println(i"instantiate $rv to $inst") - rv.setAlias(inst) + override def traverse(t: Type) = t match + case t @ CapturingType(parent, refs) => + refs match + case refs: CaptureSet.Var if variance < 0 => refs.solve() + case _ => + for ref <- refs.elems do + ref match + case ref: CaptureRoot.Var => + ref.followAlias match + case rv: CaptureRoot.Var if rv.innerLimit == ctx.owner.levelOwner => + rv.alias = ctx.owner.localRoot.termRef case _ => - case _ => - traverse(parent) - case t @ defn.RefinedFunctionOf(rinfo) => - traverse(rinfo) - case tp: TypeVar => - case tp: TypeRef => - traverse(tp.prefix) - case _ => - traverseChildren(t) + case _ => + traverse(parent) + case t @ defn.RefinedFunctionOf(rinfo) => + traverse(rinfo) + case _ => + traverseChildren(t) /** If `tpt` is an inferred type, interpolate capture set variables appearing contra- * variantly in it. @@ -279,21 +228,28 @@ class CheckCaptures extends Recheck, SymTransformer: pos, provenance) /** The current environment */ - private var curEnv: Env = inContext(ictx): + private val rootEnv: Env = inContext(ictx): Env(defn.RootClass, EnvKind.Regular, CaptureSet.empty, null) + private var curEnv = rootEnv /** Currently checked closures and their expected types, used for error reporting */ private var openClosures: List[(Symbol, Type)] = Nil private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap() + /** A list of actions to perform at postCheck. The reason to defer these actions + * is that it is sometimes better for type inference to not constrain too early + * with a checkConformsExpr. + */ + private var todoAtPostCheck = new mutable.ListBuffer[() => Unit] + /** If `sym` is a class or method nested inside a term, a capture set variable representing * the captured variables of the environment associated with `sym`. */ def capturedVars(sym: Symbol)(using Context) = myCapturedVars.getOrElseUpdate(sym, - if sym.ownersIterator.exists(_.isTerm) then - CaptureSet.Var(if sym.isConstructor then sym.owner.owner else sym.owner) + if sym.ownersIterator.exists(_.isTerm) + then CaptureSet.Var(sym.owner) else CaptureSet.empty) /** For all nested environments up to `limit` or a closed environment perform `op`, @@ -356,46 +312,15 @@ class CheckCaptures extends Recheck, SymTransformer: def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) - private def handleBackwardsCompat(tp: Type, sym: Symbol, initialVariance: Int = 1)(using Context): Type = - val fluidify = new TypeMap with IdempotentCaptRefMap: - variance = initialVariance - def apply(t: Type): Type = t match - case t: MethodType => - mapOver(t) - case t: TypeLambda => - t.derivedLambdaType(resType = this(t.resType)) - case CapturingType(_, _) => - t - case _ => - val t1 = t match - case t @ defn.RefinedFunctionOf(rinfo: MethodType) => - t.derivedRefinedType(refinedInfo = this(rinfo)) - case _ => - mapOver(t) - if variance > 0 then t1 - else setup.decorate(t1, mapRoots = false, addedSet = Function.const(CaptureSet.Fluid)) - - def isPreCC(sym: Symbol): Boolean = - sym.isTerm && sym.maybeOwner.isClass - && !sym.owner.is(CaptureChecked) - && !defn.isFunctionSymbol(sym.owner) - - if isPreCC(sym) then - val tpw = tp.widen - val fluidTp = fluidify(tpw) - if fluidTp eq tpw then tp - else fluidTp.showing(i"fluid for ${sym.showLocated}, ${sym.is(JavaDefined)}: $tp --> $result", capt) - else tp - end handleBackwardsCompat - - override def recheckIdent(tree: Ident)(using Context): Type = + override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = if tree.symbol.is(Method) then if tree.symbol.info.isParameterless then // there won't be an apply; need to include call captures now includeCallCaptures(tree.symbol, tree.srcPos) else markFree(tree.symbol, tree.srcPos) - handleBackwardsCompat(super.recheckIdent(tree), tree.symbol) + instantiateLocalRoots(tree.symbol, NoPrefix, pt, tree.srcPos): + super.recheckIdent(tree, pt) /** A specialized implementation of the selection rule. * @@ -424,30 +349,52 @@ class CheckCaptures extends Recheck, SymTransformer: val selType = recheckSelection(tree, qualType, name, disambiguate) val selCs = selType.widen.captureSet - if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then - handleBackwardsCompat(selType, tree.symbol) - else - val qualCs = qualType.captureSet - capt.println(i"pick one of $qualType, ${selType.widen}, $qualCs, $selCs in $tree") - 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 + instantiateLocalRoots(tree.symbol, qualType, pt, tree.srcPos): + if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then selType + else + val qualCs = qualType.captureSet + capt.println(i"pick one of $qualType, ${selType.widen}, $qualCs, $selCs in $tree") + 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") - override def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = - val srcRoot = - if meth.isConstructor && meth.owner.source == ctx.compilationUnit.source - then meth.owner.localRoot - else defn.captureRoot - val mapr = mapRoots(srcRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner, meth)) - funtpe.derivedLambdaType( - paramInfos = funtpe.paramInfos.mapConserve(mapr), - resType = mapr(funtpe.resType)).asInstanceOf[MethodType] + /** Instantiate local roots of `sym` in type `tp` to root variables, provided + * - `sym` is a level owner, and + * - `tp` is the type of a function that gets applied, either as a method + * or as a function value that gets applied. + */ + def instantiateLocalRoots(sym: Symbol, pre: Type, pt: Type, pos: SrcPos)(tp: Type)(using Context): Type = + def canInstantiate = + sym.is(Method, butNot = Accessor) + || sym.isTerm && defn.isFunctionType(sym.info) && pt == AnySelectionProto + if canInstantiate then + val tpw = tp.widen + var tp1 = tpw + val rootVar = CaptureRoot.Var(ctx.owner, sym) + if sym.isLevelOwner then + val outerOwner = sym.skipConstructor.owner.levelOwner + if outerOwner.isClass then + val outerRoot = outerOwner.localRoot.termRef + outerRoot.asSeenFrom(pre, sym.owner) match + case outerLimit: CaptureRoot if outerLimit ne outerRoot => + capt.println(i"constraining $rootVar of $sym by $outerLimit") + if !outerLimit.encloses(rootVar) then + // Should this be an assertion failure instead? + report.error(em"outer instance $outerLimit does not enclose local root $rootVar", pos) + case _ => + tp1 = mapRoots(sym.localRoot.termRef, rootVar)(tp1) + if tp1 ne tpw then + ccSetup.println(i"INST local $sym: $tp, ${sym.localRoot} = $tp1") + if tpw eq tp1 then tp else tp1 + else + tp /** A specialized implementation of the apply rule. * @@ -473,11 +420,11 @@ class CheckCaptures extends Recheck, SymTransformer: if meth == defn.Caps_unsafeAssumePure then val arg :: Nil = tree.args: @unchecked - val argType0 = recheck(arg, pt.capturing(CaptureSet(CaptureRoot.Var(ctx.owner)))) + val argType0 = recheck(arg, pt.capturing(CaptureSet(CaptureRoot.Var(ctx.owner, meth)))) val argType = if argType0.captureSet.isAlwaysEmpty then argType0 else argType0.widen.stripCapturing - capt.println(i"rechecking $arg with ${pt.capturing(CaptureSet.universal)}: $argType") + capt.println(i"rechecking $arg with $pt: $argType") super.recheckFinish(argType, tree, pt) else if meth == defn.Caps_unsafeBox then mapArgUsing(_.forceBoxStatus(true)) @@ -572,7 +519,7 @@ class CheckCaptures extends Recheck, SymTransformer: augmentConstructorType(parent, initCs ++ refs) case _ => val (refined, cs) = addParamArgRefinements(core, initCs) - refined.capturing(cs) + refined.capturing(cs.changeOwner(ctx.owner)) augmentConstructorType(ownType, capturedVars(cls) ++ capturedVars(sym)) .showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt) @@ -616,10 +563,10 @@ class CheckCaptures extends Recheck, SymTransformer: openClosures = openClosures.tail end recheckClosureBlock - override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = + override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = try - if !sym.is(Module) then // Modules are checked by checking the module class - super.recheckValDef(tree, sym) + if sym.is(Module) then sym.info // Modules are checked by checking the module class + else checkInferredResult(super.recheckValDef(tree, sym), tree) finally if !sym.is(Param) then // Parameters with inferred types belong to anonymous methods. We need to wait @@ -628,17 +575,56 @@ class CheckCaptures extends Recheck, SymTransformer: // function is compiled since we do not propagate expected types into blocks. interpolateVarsIn(tree.tpt) - override def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = - if !Synthetics.isExcluded(sym) then + override def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = + if Synthetics.isExcluded(sym) then sym.info + else val saved = curEnv val localSet = capturedVars(sym) if !localSet.isAlwaysEmpty then curEnv = Env(sym, EnvKind.Regular, localSet, curEnv) - try super.recheckDefDef(tree, sym) + try checkInferredResult(super.recheckDefDef(tree, sym), tree) finally - interpolateVarsIn(tree.tpt) + if !sym.isAnonymousFunction then + // Anonymous functions propagate their type to the enclosing environment + // so it is not in general sound to interpolate their types. + interpolateVarsIn(tree.tpt) curEnv = saved + def checkInferredResult(tp: Type, tree: ValOrDefDef)(using Context): Type = + val sym = tree.symbol + + def isLocal = + sym.owner.ownersIterator.exists(_.isTerm) + || sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass) + + def canUseInferred = // If canUseInferred is false, all capturing types in the type of `sym` need to be given explicitly + sym.is(Private) // private symbols can always have inferred types + || sym.name.is(DefaultGetterName) // default getters are exempted since otherwise it would be + // too annoying. This is a hole since a defualt getter's result type + // might leak into a type variable. + || // non-local symbols cannot have inferred types since external capture types are not inferred + isLocal // local symbols still need explicit types if + && !sym.owner.is(Trait) // they are defined in a trait, since we do OverridingPairs checking before capture inference + + def addenda(expected: Type) = new Addenda: + override def toAdd(using Context) = + def result = if tree.isInstanceOf[ValDef] then"" else " result" + i""" + | + |Note that the expected type $expected + |is the previously inferred$result type of $sym + |which is also the type seen in separately compiled sources. + |The new inferred type $tp + |must conform to this type.""" :: Nil + + tree.tpt match + case tpt: InferredTypeTree if !canUseInferred => + val expected = tpt.tpe.dropAllRetains + todoAtPostCheck += (() => checkConformsExpr(tp, expected, tree.rhs, addenda(expected))) + case _ => + tp + end checkInferredResult + /** Class-specific capture set relations: * 1. The capture set of a class includes the capture sets of its parents. * 2. The capture set of the self type of a class includes the capture set of the class. @@ -652,7 +638,8 @@ class CheckCaptures extends Recheck, SymTransformer: for parent <- impl.parents do // (1) checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos, i"\nof the references allowed to be captured by $cls") - if !localSet.isAlwaysEmpty then curEnv = Env(cls, EnvKind.Regular, localSet, curEnv) + if !localSet.isAlwaysEmpty then + curEnv = Env(cls, EnvKind.Regular, localSet, curEnv) try val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") checkSubset(localSet, thisSet, tree.srcPos) // (2) @@ -722,6 +709,9 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => val res = try super.recheck(tree, pt) + catch case ex: CaptureRoot.NoCommonRoot => + report.error(ex.getMessage.nn) + tree.tpe finally curEnv = saved if tree.isTerm && !pt.isBoxedCapturing then markFree(res.boxedCaptureSet, tree.srcPos) @@ -765,32 +755,33 @@ class CheckCaptures extends Recheck, SymTransformer: // - Adapt box status and environment capture sets by simulating box/unbox operations. // - Instantiate `cap` in actual as needed to a local root. - override def isCompatible(actual: Type, expected: Type)(using Context): Boolean = - super.isCompatible(actual, expected) - || { - // When testing whether `A <: B`, it could be that `B` uses a local capture root, - // but a uses `cap`, i.e. is capture polymorphic. In this case, adaptation is allowed - // to instantiate `A` to match the root in `B`. - val actual1 = mapRoots(defn.captureRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner))(actual) - (actual1 ne actual) && { - val res = super.isCompatible(actual1, expected) - if !res && ctx.settings.YccDebug.value then - println(i"Failure under mapped roots:") - println(i"${TypeComparer.explained(_.isSubType(actual, expected))}") - res - } - } - - /** Massage `actual` and `expected` types using the methods below before checking conformance */ + /** Massage `actual` and `expected` types before checking conformance. + * Massaging is done by the methods following this one: + * - align dependent function types and add outer references in the expected type + * - adapt boxing in the actual type + * If the resulting types are not compatible, try again with an actual type + * where local capture roots are instantiated to root variables. + */ override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Unit = val expected1 = alignDependentFunction(addOuterRefs(expected, actual), actual.stripCapturing) - val actual1 = adaptBoxed(actual, expected1, tree.srcPos) - //println(i"check conforms $actual1 <<< $expected1") - super.checkConformsExpr(actual1, expected1, tree, addenda ++ CaptureSet.levelErrors) - - private def toDepFun(args: List[Type], resultType: Type, isContextual: Boolean)(using Context): Type = - MethodType.companion(isContextual = isContextual)(args, resultType) - .toFunctionType(alwaysDependent = true) + val actualBoxed = adaptBoxed(actual, expected1, tree.srcPos) + //println(i"check conforms $actualBoxed <<< $expected1") + var ok = isCompatible(actualBoxed, expected1) + if !ok then stripTyped(tree) match + case tree: RefTree if !ctx.owner.isContainedIn(tree.symbol.levelOwner) => + // When testing whether `A <: B`, it could be that `B` uses a local capture root, + // but a uses `cap`, i.e. is capture polymorphic. In this case, adaptation is allowed + // to instantiate `A` to match the root in `B`. + val actualWide = actual.widen + val actualInst = mapRoots(tree.symbol.localRoot.termRef, CaptureRoot.Var(ctx.owner, tree.symbol))(actualWide) + capt.println(i"fallBack from $actualWide to $actualInst to match $expected1") + ok = (actualInst ne actualWide) + && isCompatible(adaptBoxed(actualInst, expected1, tree.srcPos), expected1) + case _ => + if !ok then + capt.println(i"conforms failed for ${tree}: $actual vs $expected") + err.typeMismatch(tree.withType(actualBoxed), expected1, addenda ++ CaptureSet.levelErrors) + end checkConformsExpr /** Turn `expected` into a dependent function when `actual` is dependent. */ private def alignDependentFunction(expected: Type, actual: Type)(using Context): Type = @@ -801,7 +792,7 @@ class CheckCaptures extends Recheck, SymTransformer: else CapturingType(eparent1, refs, boxed = expected0.isBoxed) case expected @ defn.FunctionOf(args, resultType, isContextual) if defn.isNonRefinedFunction(expected) && defn.isFunctionNType(actual) && !defn.isNonRefinedFunction(actual) => - val expected1 = toDepFun(args, resultType, isContextual) + val expected1 = depFun(args, resultType, isContextual) expected1 case _ => expected @@ -1038,32 +1029,57 @@ class CheckCaptures extends Recheck, SymTransformer: finally curEnv = saved actual1 frozen_<:< expected1 - override def adjustInfo(tp: Type, member: Symbol)(using Context): Type = - handleBackwardsCompat(tp, member, initialVariance = 0) - //.showing(i"adjust $other: $tp --> $result") + override def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean = + !setup.isPreCC(overriding) && !setup.isPreCC(overridden) end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = t match case t: Template => - checkAllOverrides(ctx.owner.asClass, OverridingPairsCheckerCC(_, _, t)) + checkAllOverrides(ctx.owner.asClass, OverridingPairsCheckerCC(_, _, t))( + using ctx.withProperty(LooseRootChecking, Some(()))) case _ => traverseChildren(t) - private var setup: Setup = compiletime.uninitialized + /** Check a ValDef or DefDef as an action performed in a completer. + */ + def completeDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type = + val saved = curEnv + try + // Setup environment to reflect the new owner. + val envForOwner = curEnv.outersIterator + .takeWhile(e => !capturedVars(e.owner).isAlwaysEmpty) + .map(e => (e.owner, e)) + .toMap + def restoreEnvFor(sym: Symbol): Env = + val localSet = capturedVars(sym) + if localSet.isAlwaysEmpty then rootEnv + else envForOwner.get(sym) match + case Some(e) => e + case None => Env(sym, EnvKind.Regular, localSet, restoreEnvFor(sym.owner)) + curEnv = restoreEnvFor(sym.owner) + capt.println(i"Complete $sym in ${curEnv.outersIterator.toList.map(_.owner)}") + recheckDef(tree, sym) + finally + curEnv = saved + + private val setup: SetupAPI = thisPhase.prev.asInstanceOf[Setup] override def checkUnit(unit: CompilationUnit)(using Context): Unit = - setup = Setup(preRecheckPhase, thisPhase, recheckDef) - inContext(ctx.withProperty(ccStateKey, Some(state))): - setup(ctx.compilationUnit.tpdTree) - //println(i"SETUP:\n${Recheck.addRecheckedTypes.transform(ctx.compilationUnit.tpdTree)}") - withCaptureSetsExplained: - super.checkUnit(unit) - checkOverrides.traverse(unit.tpdTree) - checkSelfTypes(unit.tpdTree) - postCheck(unit.tpdTree) - if ctx.settings.YccDebug.value then - show(unit.tpdTree) // this does not print tree, but makes its variables visible for dependency printing + setup.setupUnit(ctx.compilationUnit.tpdTree, completeDef) + + if ctx.settings.YccPrintSetup.value then + val echoHeader = "[[syntax tree at end of cc setup]]" + val treeString = show(ctx.compilationUnit.tpdTree) + report.echo(s"$echoHeader\n$treeString\n") + + withCaptureSetsExplained: + super.checkUnit(unit) + checkOverrides.traverse(unit.tpdTree) + checkSelfTypes(unit.tpdTree) + postCheck(unit.tpdTree) + if ctx.settings.YccDebug.value then + show(unit.tpdTree) // this does not print tree, but makes its variables visible for dependency printing /** Check that self types of subclasses conform to self types of super classes. * (See comment below how this is achieved). The check assumes that classes @@ -1094,12 +1110,7 @@ class CheckCaptures extends Recheck, SymTransformer: } assert(roots.nonEmpty) for case root: ClassSymbol <- roots do - inContext(ctx.fresh.setOwner(root).withProperty(LooseRootChecking, Some(()))): - // Without LooseRootChecking, we get problems with F-bounded parent types. - // These can make `cap` "pop out" in ways that are hard to prevent. I believe - // to prevent it we'd have to map `cap` in a whole class graph with all parent - // classes, which would be very expensive. So for now we approximate by assuming - // different roots are compatible for self type conformance checking. + inContext(ctx.fresh.setOwner(root)): checkSelfAgainstParents(root, root.baseClasses) val selfType = root.asClass.classInfo.selfType interpolator(startingVariance = -1).traverse(selfType) @@ -1151,22 +1162,23 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => true private def healCaptureSet(cs: CaptureSet): Unit = - cs.ensureWellformed: elems => + cs.ensureWellformed: elem => ctx ?=> var seen = new util.HashSet[CaptureRef] - def recur(elems: List[CaptureRef]): Unit = - for case ref: TermParamRef <- elems do - if !allowed.contains(ref) && !seen.contains(ref) then - seen += ref - if ref.underlying.isRef(defn.Caps_Cap) then - report.error(i"escaping local reference $ref", tree.srcPos) - else - val widened = ref.captureSetOfInfo - val added = widened.filter(isAllowed(_)) - capt.println(i"heal $ref in $cs by widening to $added") - checkSubset(added, cs, tree.srcPos) - recur(widened.elems.toList) - recur(elems) + def recur(ref: CaptureRef): Unit = ref match + case ref: TermParamRef + if !allowed.contains(ref) && !seen.contains(ref) => + seen += ref + if ref.underlying.isRef(defn.Caps_Cap) then + report.error(i"escaping local reference $ref", tree.srcPos) + else + val widened = ref.captureSetOfInfo + val added = widened.filter(isAllowed(_)) + capt.println(i"heal $ref in $cs by widening to $added") + checkSubset(added, cs, tree.srcPos) + widened.elems.foreach(recur) + case _ => + recur(elem) def traverse(tp: Type) = tp match @@ -1190,9 +1202,6 @@ class CheckCaptures extends Recheck, SymTransformer: /** Perform the following kinds of checks * - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`. - * - Check that externally visible `val`s or `def`s have empty capture sets. If not, - * suggest an explicit type. This is so that separate compilation (where external - * symbols have empty capture sets) gives the same results as joint compilation. * - Check that arguments of TypeApplys and AppliedTypes conform to their bounds. * - Heal ill-formed capture sets of type parameters. See `healTypeParam`. */ @@ -1205,50 +1214,6 @@ class CheckCaptures extends Recheck, SymTransformer: traverseChildren(tree)(using lctx) check(tree) def check(tree: Tree)(using Context) = tree match - case _: InferredTypeTree => - case tree: TypeTree if !tree.span.isZeroExtent => - tree.knownType.foreachPart { tp => - checkWellformedPost(tp, tree.srcPos) - tp match - case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => - warnIfRedundantCaptureSet(annot.tree, tree) - case _ => - } - case t: ValOrDefDef - if t.tpt.isInstanceOf[InferredTypeTree] && !Synthetics.isExcluded(t.symbol) => - val sym = t.symbol - val isLocal = - sym.owner.ownersIterator.exists(_.isTerm) - || sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass) - def canUseInferred = // If canUseInferred is false, all capturing types in the type of `sym` need to be given explicitly - sym.is(Private) // Private symbols can always have inferred types - || sym.name.is(DefaultGetterName) // Default getters are exempted since otherwise it would be - // too annoying. This is a hole since a defualt getter's result type - // might leak into a type variable. - || // non-local symbols cannot have inferred types since external capture types are not inferred - isLocal // local symbols still need explicit types if - && !sym.owner.is(Trait) // they are defined in a trait, since we do OverridingPairs checking before capture inference - || // If there are overridden symbols, their types form an upper bound - sym.allOverriddenSymbols.nonEmpty // for the inferred type. In this case, separate compilation would - // not be a soundness issue. - def isNotPureThis(ref: CaptureRef) = ref match { - case ref: ThisType => !ref.cls.isPureClass - case _ => true - } - if !canUseInferred then - val inferred = t.tpt.knownType - def checkPure(tp: Type) = tp match - case CapturingType(_, refs) - if !refs.elems.filter(isNotPureThis).isEmpty => - val resultStr = if t.isInstanceOf[DefDef] then " result" else "" - report.error( - em"""Non-local $sym cannot have an inferred$resultStr type - |$inferred - |with non-empty capture set $refs. - |The type needs to be declared explicitly.""".withoutDisambiguation(), - t.srcPos) - case _ => - inferred.foreachPart(checkPure, StopAt.Static) case t @ TypeApply(fun, args) => fun.knownType.widen match case tl: PolyType => @@ -1264,17 +1229,19 @@ class CheckCaptures extends Recheck, SymTransformer: end check end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) + for chk <- todoAtPostCheck do chk() + setup.postCheck() + if !ctx.reporter.errorsReported then - //inContext(ctx.withProperty(LooseRootChecking, Some(()))): - // We dont report errors here if previous errors were reported, because other - // errors often result in bad applied types, but flagging these bad types gives - // often worse error messages than the original errors. - val checkApplied = new TreeTraverser: - def traverse(t: Tree)(using Context) = t match - case tree: InferredTypeTree => - case tree: New => - case tree: TypeTree => checkAppliedTypesIn(tree.withKnownType) - case _ => traverseChildren(t) - checkApplied.traverse(unit) + // We dont report errors here if previous errors were reported, because other + // errors often result in bad applied types, but flagging these bad types gives + // often worse error messages than the original errors. + val checkApplied = new TreeTraverser: + def traverse(t: Tree)(using Context) = t match + case tree: InferredTypeTree => + case tree: New => + case tree: TypeTree => checkAppliedTypesIn(tree.withKnownType) + case _ => traverseChildren(t) + checkApplied.traverse(unit) end CaptureChecker end CheckCaptures diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala new file mode 100644 index 000000000000..7902b03445fb --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala @@ -0,0 +1,35 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.* +import ast.tpd.* +import Annotations.Annotation +import Decorators.i + +/** A builder and extractor for annotated types with @retains or @retainsByName annotations. + */ +object RetainingType: + + def apply(tp: Type, refs: List[Tree], byName: Boolean = false)(using Context): Type = + val annotCls = if byName then defn.RetainsByNameAnnot else defn.RetainsAnnot + val annotTree = + New(annotCls.typeRef, + Typed( + SeqLiteral(refs, TypeTree(defn.AnyType)), + TypeTree(defn.RepeatedParamClass.typeRef.appliedTo(defn.AnyType))) :: Nil) + AnnotatedType(tp, Annotation(annotTree)) + + def unapply(tp: AnnotatedType)(using Context): Option[(Type, List[Tree])] = + val sym = tp.annot.symbol + if sym == defn.RetainsAnnot || sym == defn.RetainsByNameAnnot then + tp.annot match + case _: CaptureAnnotation => + assert(ctx.mode.is(Mode.IgnoreCaptures), s"bad retains $tp at ${ctx.phase}") + None + case ann => + Some((tp.parent, ann.tree.retainedElems)) + else + None +end RetainingType diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index e18c5e559aba..7dacbb3e5e05 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -8,12 +8,53 @@ import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* import Types.*, StdNames.* import Annotations.Annotation import config.Feature -import config.Printers.capt -import ast.tpd -import transform.Recheck.* -import CaptureSet.IdentityCaptRefMap +import config.Printers.{capt, ccSetup} +import ast.tpd, tpd.* +import transform.{PreRecheck, Recheck}, Recheck.* +import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded import util.Property +import printing.{Printer, Texts}, Texts.{Text, Str} +import collection.mutable + +/** Operations accessed from CheckCaptures */ +trait SetupAPI: + type DefRecheck = (tpd.ValOrDefDef, Symbol) => Context ?=> Type + def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit + def isPreCC(sym: Symbol)(using Context): Boolean + def postCheck()(using Context): Unit + +object Setup: + def newScheme(using Context) = ctx.settings.YccNew.value // if new impl is conditional + + private val IsDuringSetupKey = new Property.Key[Unit] + + def isDuringSetup(using Context): Boolean = ctx.property(IsDuringSetupKey).isDefined + + case class Box(t: Type) extends UncachedGroundType, TermType: + override def fallbackToText(printer: Printer): Text = + Str("Box(") ~ printer.toText(t) ~ ")" + def derivedBox(t1: Type): Type = + if t1 eq t then this else Box(t1) + + trait DepFunSpecializedTypeMap(using Context) extends TypeMap: + override def mapOver(tp: Type) = tp match + case defn.RefinedFunctionOf(rinfo: MethodOrPoly) => + val rinfo1 = this(rinfo) + if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) + else tp + case _ => + super.mapOver(tp) + + /** Recognizer for `res $throws exc`, returning `(res, exc)` in case of success */ + object throwsAlias: + def unapply(tp: Type)(using Context): Option[(Type, Type)] = tp match + case AppliedType(tycon, res :: exc :: Nil) if tycon.typeSymbol == defn.throwsAlias => + Some((res, exc)) + case _ => + None +end Setup +import Setup.* /** A tree traverser that prepares a compilation unit to be capture checked. * It does the following: @@ -27,21 +68,72 @@ import util.Property * are boxed on access). * - Link the external types of val and def symbols with the inferred types based on their parameter symbols. */ -class Setup( - preRecheckPhase: DenotTransformer, - thisPhase: DenotTransformer, - recheckDef: (tpd.ValOrDefDef, Symbol) => Context ?=> Unit) -extends tpd.TreeTraverser: - import tpd.* - - /** Create dependent function with underlying function class `tycon` and given - * arguments `argTypes` and result `resType`. +class Setup extends PreRecheck, SymTransformer, SetupAPI: + thisPhase => + + override def isRunnable(using Context) = + super.isRunnable && Feature.ccEnabledSomewhere + + private val toBeUpdated = new mutable.HashSet[Symbol] + + private def newFlagsFor(symd: SymDenotation)(using Context): FlagSet = + if symd.isAllOf(PrivateParamAccessor) && symd.owner.is(CaptureChecked) && !symd.hasAnnotation(defn.ConstructorOnlyAnnot) + then symd.flags &~ Private | Recheck.ResetPrivate + else symd.flags + + def isPreCC(sym: Symbol)(using Context): Boolean = + sym.isTerm && sym.maybeOwner.isClass + && !sym.is(Module) + && !sym.owner.is(CaptureChecked) + && !defn.isFunctionSymbol(sym.owner) + + private def fluidify(using Context) = new TypeMap with IdempotentCaptRefMap: + def apply(t: Type): Type = t match + case t: MethodType => + mapOver(t) + case t: TypeLambda => + t.derivedLambdaType(resType = this(t.resType)) + case CapturingType(_, _) => + t + case _ => + val t1 = t match + case t @ defn.RefinedFunctionOf(rinfo: MethodType) => + t.derivedRefinedType(t.parent, t.refinedName, this(rinfo)) + case _ => + mapOver(t) + if variance > 0 then t1 + else decorate(t1, addedSet = Function.const(CaptureSet.Fluid)) + + /** - Reset `private` flags of parameter accessors so that we can refine them + * in Setup if they have non-empty capture sets. + * - Special handling of some symbols defined for case classes. + * Enabled only until recheck is finished, and provided some compilation unit + * is CC-enabled. */ - private def depFun(tycon: Type, argTypes: List[Type], resType: Type)(using Context): Type = - MethodType.companion( - isContextual = defn.isContextFunctionClass(tycon.classSymbol), - )(argTypes, resType) - .toFunctionType(alwaysDependent = true) + def transformSym(symd: SymDenotation)(using Context): SymDenotation = + if !pastRecheck && Feature.ccEnabledSomewhere then + val sym = symd.symbol + def rootTarget = + if sym.isAliasType && sym.isStatic then NoSymbol else sym + def mappedInfo = + if toBeUpdated.contains(sym) then symd.info + else transformExplicitType(symd.info, rootTarget) + // TODO if sym is class && level owner: add a capture root + if Synthetics.needsTransform(symd) then + Synthetics.transform(symd, mappedInfo) + else if isPreCC(sym) then + symd.copySymDenotation(info = fluidify(sym.info)) + else if symd.owner.isTerm || symd.is(CaptureChecked) || symd.owner.is(CaptureChecked) then + val newFlags = newFlagsFor(symd) + val newInfo = mappedInfo + if sym.isClass then + sym.thisType.asInstanceOf[ThisType].invalidateCaches() + if newFlags != symd.flags || (newInfo ne sym.info) + then symd.copySymDenotation(initFlags = newFlags, info = newInfo) + else symd + else symd + else symd + end transformSym /** If `tp` is an unboxed capturing type or a function returning an unboxed capturing type, * convert it to be boxed. @@ -78,39 +170,30 @@ extends tpd.TreeTraverser: * pos.../lists.scala and pos/...curried-shorthands.scala fail. * Need to figure out why. * 3. Refine other class types C by adding capture set variables to their parameter getters - * (see addCaptureRefinements) + * (see addCaptureRefinements), provided `refine` is true. * 4. Add capture set variables to all types that can be tracked * * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ - private def mapInferred(mapRoots: Boolean)(using Context) = new TypeMap: + private def mapInferred(refine: Boolean)(using Context): TypeMap = new TypeMap: override def toString = "map inferred" - /** Drop @retains annotations everywhere */ - object cleanup extends TypeMap: - def apply(t: Type) = t match - case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => - apply(parent) - case _ => - mapOver(t) - /** Refine a possibly applied class type C where the class has tracked parameters * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } * where CV_1, ..., CV_n are fresh capture sets. */ def addCaptureRefinements(tp: Type): Type = tp match - case _: TypeRef | _: AppliedType if tp.typeParams.isEmpty => + case _: TypeRef | _: AppliedType if refine && tp.typeParams.isEmpty => tp.typeSymbol match case cls: ClassSymbol - if !defn.isFunctionClass(cls) && !cls.is(JavaDefined) => - // We assume that Java classes can refer to capturing Scala types only indirectly, - // using type parameters. Hence, no need to refine them. + if !defn.isFunctionClass(cls) && cls.is(CaptureChecked) => cls.paramGetters.foldLeft(tp) { (core, getter) => - if getter.termRef.isTracked then - val getterType = tp.memberInfo(getter).strippedDealias + if atPhase(thisPhase.next)(getter.termRef.isTracked) then + val getterType = + mapInferred(refine = false)(tp.memberInfo(getter)).strippedDealias RefinedType(core, getter.name, - CapturingType(getterType, CaptureSet.RefiningVar(ctx.owner, getter))) - .showing(i"add capture refinement $tp --> $result", capt) + CapturingType(getterType, CaptureSet.RefiningVar(NoSymbol, getter))) + .showing(i"add capture refinement $tp --> $result", ccSetup) else core } @@ -122,7 +205,8 @@ extends tpd.TreeTraverser: private def mapNested(ts: List[Type]): List[Type] = val saved = isTopLevel isTopLevel = false - try ts.mapConserve(this) finally isTopLevel = saved + try ts.mapConserve(this) + finally isTopLevel = saved def apply(tp: Type) = val tp1 = tp match @@ -143,14 +227,15 @@ extends tpd.TreeTraverser: val args1 = mapNested(args0) val res1 = this(res0) if isTopLevel then - depFun(tycon1, args1, res1) - .showing(i"add function refinement $tp ($tycon1, $args1, $res1) (${tp.dealias}) --> $result", capt) + depFun(args1, res1, + isContextual = defn.isContextFunctionClass(tycon1.classSymbol)) + .showing(i"add function refinement $tp ($tycon1, $args1, $res1) (${tp.dealias}) --> $result", ccSetup) else if (tycon1 eq tycon) && (args1 eq args0) && (res1 eq res0) then tp else tp.derivedAppliedType(tycon1, args1 :+ res1) else - tp.derivedAppliedType(tycon1, args.mapConserve(arg => this(arg))) + tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) case defn.RefinedFunctionOf(rinfo: MethodType) => val rinfo1 = apply(rinfo) if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) @@ -161,103 +246,139 @@ extends tpd.TreeTraverser: resType = this(tp.resType)) case tp: TypeLambda => // Don't recurse into parameter bounds, just cleanup any stray retains annotations + // !!! TODO we should also map roots to rootvars here tp.derivedLambdaType( - paramInfos = tp.paramInfos.mapConserve(cleanup(_).bounds), + paramInfos = tp.paramInfos.mapConserve(_.dropAllRetains.bounds), resType = this(tp.resType)) + case Box(tp1) => + box(this(tp1)) case _ => mapOver(tp) - addVar(addCaptureRefinements(tp1), ctx.owner, mapRoots) + addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) end apply end mapInferred - private def transformInferredType(tp: Type, boxed: Boolean, mapRoots: Boolean)(using Context): Type = - val tp1 = mapInferred(mapRoots)(tp) - if boxed then box(tp1) else tp1 - - /** Recognizer for `res $throws exc`, returning `(res, exc)` in case of success */ - object throwsAlias: - def unapply(tp: Type)(using Context): Option[(Type, Type)] = tp match - case AppliedType(tycon, res :: exc :: Nil) if tycon.typeSymbol == defn.throwsAlias => - Some((res, exc)) - case _ => - None + private def transformInferredType(tp: Type)(using Context): Type = + mapInferred(refine = true)(tp) + + private def transformExplicitType(tp: Type, rootTarget: Symbol, tptToCheck: Option[Tree] = None)(using Context): Type = + val expandAliases = new DeepTypeMap: + override def toString = "expand aliases" + + /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib + * are defined with `?=>` rather than `?->`. + * We also have to add a capture set to the last expanded throws alias. I.e. + * T $throws E1 $throws E2 + * expands to + * (erased x$0: CanThrow[E1]) ?-> (erased x$1: CanThrow[E1]) ?->{x$0} T + */ + private def expandThrowsAlias(tp: Type, encl: List[MethodType] = Nil): Type = tp match + case throwsAlias(res, exc) => + val paramType = AnnotatedType( + defn.CanThrowClass.typeRef.appliedTo(exc), + Annotation(defn.ErasedParamAnnot, defn.CanThrowClass.span)) + val isLast = throwsAlias.unapply(res).isEmpty + val paramName = nme.syntheticParamName(encl.length) + val mt = ContextualMethodType(paramName :: Nil)( + _ => paramType :: Nil, + mt => if isLast then res else expandThrowsAlias(res, mt :: encl)) + val fntpe = defn.PolyFunctionOf(mt) + if !encl.isEmpty && isLast then + val cs = CaptureSet(encl.map(_.paramRefs.head)*) + CapturingType(fntpe, cs, boxed = false) + else fntpe + case _ => tp + + /** Map references to capability classes C to C^ */ + private def expandCapabilityClass(tp: Type): Type = + if tp.isCapabilityClassRef + then CapturingType(tp, defn.expandedUniversalSet, boxed = false) + else tp + + private def checkQualifiedRoots(tree: Tree): Unit = + for case elem @ QualifiedRoot(outer) <- tree.retainedElems do + if !ctx.owner.levelOwnerNamed(outer).exists then + report.error(em"`$outer` does not name an outer definition that represents a capture level", elem.srcPos) + + private def recur(t: Type): Type = normalizeCaptures(mapOver(t)) + + def apply(t: Type) = + val t1 = expandThrowsAlias(t) + if t1 ne t then return this(t1) + val t2 = expandCapabilityClass(t) + if t2 ne t then return t2 + t match + case t: TypeRef => + val sym = t.symbol + if sym.isClass then t + else t.info match + case TypeAlias(alias) => + val transformed = + if sym.isStatic then this(alias) + else transformExplicitType(alias, + if rootTarget.exists then sym else NoSymbol)( + using ctx.withOwner(sym.owner)) + if transformed ne alias then transformed else t + //.showing(i"EXPAND $t with ${t.info} to $result in ${t.symbol.owner}/${ctx.owner}") + case _ => + recur(t) + case t @ AppliedType(tycon: TypeProxy, args) if true => + tycon.underlying match + case TypeAlias(aliasTycon) => + val args1 = + if defn.isFunctionClass(t.dealias.typeSymbol) then args + else args.map(Box(_)) + val alias = aliasTycon.applyIfParameterized(args1) + val transformed = this(alias) + if transformed ne alias then + //println(i"APP ALIAS $t / $alias / $transformed") + transformed + else + //println(i"NO ALIAS $t / $alias / $transformed") + recur(t) + case _ => + recur(t) + case t @ CapturingType(parent, refs) => + checkQualifiedRoots(t.annot.tree) // TODO: NEEDED? + t.derivedCapturingType(this(parent), refs) + case t @ AnnotatedType(parent, ann) => + if ann.symbol == defn.RetainsAnnot then + val parent1 = this(parent) + for tpt <- tptToCheck do + checkQualifiedRoots(ann.tree) + checkWellformedLater(parent1, ann.tree, tpt) + CapturingType(parent1, ann.tree.toCaptureSet) + else + t.derivedAnnotatedType(this(parent), ann) + case Box(t1) => + box(this(t1)) + case t: LazyRef => + val t1 = this(t.ref) + if t1 ne t.ref then t1 else t + case t: TypeVar => + this(t.underlying) + case _ => + recur(t) + end expandAliases - /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib - * are defined with `?=>` rather than `?->`. - * We also have to add a capture set to the last expanded throws alias. I.e. - * T $throws E1 $throws E2 - * expands to - * (erased x$0: CanThrow[E1]) ?-> (erased x$1: CanThrow[E1]) ?->{x$0} T - */ - private def expandThrowsAlias(tp: Type, encl: List[MethodType] = Nil)(using Context): Type = tp match - case throwsAlias(res, exc) => - val paramType = AnnotatedType( - defn.CanThrowClass.typeRef.appliedTo(exc), - Annotation(defn.ErasedParamAnnot, defn.CanThrowClass.span)) - val isLast = throwsAlias.unapply(res).isEmpty - val paramName = nme.syntheticParamName(encl.length) - val mt = ContextualMethodType(paramName :: Nil)( - _ => paramType :: Nil, - mt => if isLast then res else expandThrowsAlias(res, mt :: encl)) - val fntpe = defn.PolyFunctionOf(mt) - if !encl.isEmpty && isLast then - val cs = CaptureSet(encl.map(_.paramRefs.head)*) - CapturingType(fntpe, cs, boxed = false) - else fntpe - case _ => tp - - extension (tp: Type) def isCapabilityClassRef(using Context) = tp match - case _: TypeRef | _: AppliedType => tp.typeSymbol.hasAnnotation(defn.CapabilityAnnot) - case _ => false - - /** Map references to capability classes C to C^ */ - private def expandCapabilityClass(tp: Type)(using Context): Type = - if tp.isCapabilityClassRef - then CapturingType(tp, CaptureSet.universal, boxed = false) - else tp - - private def checkQualifiedRoots(tree: Tree)(using Context): Unit = - for case elem @ QualifiedRoot(outer) <- retainedElems(tree) do - if !ctx.owner.levelOwnerNamed(outer).exists then - report.error(em"`$outer` does not name an outer definition that represents a capture level", elem.srcPos) - - private def expandAliases(using Context) = new TypeMap with FollowAliases: - override def toString = "expand aliases" - def apply(t: Type) = - val t1 = expandThrowsAlias(t) - if t1 ne t then return this(t1) - val t2 = expandCapabilityClass(t) - if t2 ne t then return t2 - t match - case t @ AnnotatedType(t1, ann) => - checkQualifiedRoots(ann.tree) - val t3 = - if ann.symbol == defn.RetainsAnnot && t1.isCapabilityClassRef then t1 - else this(t1) - // Don't map capture sets, since that would implicitly normalize sets that - // are not well-formed. - t.derivedAnnotatedType(t3, ann) - case t => - normalizeCaptures(mapOverFollowingAliases(t)) - - private def transformExplicitType(tp: Type, boxed: Boolean, mapRoots: Boolean)(using Context): Type = val tp1 = expandAliases(tp) val tp2 = - if mapRoots - then cc.mapRoots(defn.captureRoot.termRef, ctx.owner.localRoot.termRef)(tp1) + if rootTarget.exists + then mapRoots(defn.captureRoot.termRef, rootTarget.localRoot.termRef)(tp1) .showing(i"map roots $tp1, ${tp1.getClass} == $result", capt) else tp1 - val tp3 = if boxed then box(tp2) else tp2 - if tp3 ne tp then capt.println(i"expanded: $tp --> $tp3") - tp3 + if tp2 ne tp then ccSetup.println(i"expanded in ${ctx.owner}: $tp --> $tp1 --> $tp2") + tp2 + end transformExplicitType /** Transform type of type tree, and remember the transformed type as the type the tree */ - private def transformTT(tree: TypeTree, boxed: Boolean, exact: Boolean, mapRoots: Boolean)(using Context): Unit = + private def transformTT(tree: TypeTree, boxed: Boolean, exact: Boolean, rootTarget: Symbol)(using Context): Unit = if !tree.hasRememberedType then + val tp = if boxed then Box(tree.tpe) else tree.tpe tree.rememberType( if tree.isInstanceOf[InferredTypeTree] && !exact - then transformInferredType(tree.tpe, boxed, mapRoots) - else transformExplicitType(tree.tpe, boxed, mapRoots)) + then transformInferredType(tp) + else transformExplicitType(tp, rootTarget, tptToCheck = Some(tree))) /** Substitute parameter symbols in `from` to paramRefs in corresponding * method or poly types `to`. We use a single BiTypeMap to do everything. @@ -296,275 +417,213 @@ extends tpd.TreeTraverser: def inverse = thisMap end SubstParams - /** If the outer context directly enclosing the definition of `sym` - * has a owner, that owner, otherwise null. - */ - def newOwnerFor(sym: Symbol)(using Context): Symbol | Null = - var octx = ctx - while octx.owner == sym do octx = octx.outer - if octx.owner.name == nme.TRY_BLOCK then octx.owner else null - /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = - sym.updateInfo(preRecheckPhase, info, newOwnerFor(sym)) + toBeUpdated += sym + sym.updateInfo(thisPhase, info, newFlagsFor(sym)) + toBeUpdated -= sym sym.namedType match - case ref: CaptureRef => ref.invalidateCaches() + case ref: CaptureRef => ref.invalidateCaches() // TODO: needed? case _ => - /** Update only the owner part fo info if necessary. A symbol should be updated - * only once by either updateInfo or updateOwner. - */ - private def updateOwner(sym: Symbol)(using Context) = - if newOwnerFor(sym) != null then updateInfo(sym, sym.info) - - extension (sym: Symbol) def takesCappedParam(using Context): Boolean = - def search = new TypeAccumulator[Boolean]: - def apply(x: Boolean, t: Type): Boolean = //reporting.trace.force(s"hasCapAt $v, $t"): - if x then true - else t match - case t @ AnnotatedType(t1, annot) - if annot.symbol == defn.RetainsAnnot || annot.symbol == defn.RetainsByNameAnnot => - val elems = annot match - case CaptureAnnotation(refs, _) => refs.elems.toList - case _ => retainedElems(annot.tree).map(_.tpe) - if elems.exists(_.widen.isRef(defn.Caps_Cap)) then true - else !t1.isCapabilityClassRef && this(x, t1) - case t: PolyType => - apply(x, t.resType) - case t: MethodType => - t.paramInfos.exists(apply(false, _)) - case _ => - if t.isRef(defn.Caps_Cap) || t.isCapabilityClassRef then true - else - val t1 = t.dealiasKeepAnnots - if t1 ne t then this(x, t1) - else foldOver(x, t) - true || sym.info.stripPoly.match - case mt: MethodType => - mt.paramInfos.exists(search(false, _)) - case _ => - false - end extension - - def traverse(tree: Tree)(using Context): Unit = - tree match - case tree @ DefDef(_, paramss, tpt: TypeTree, _) => - val meth = tree.symbol - if isExcluded(meth) then - return - - def isCaseClassSynthetic = // TODO drop - meth.owner.isClass && meth.owner.is(Case) && meth.is(Synthetic) && meth.info.firstParamNames.isEmpty - - inContext(ctx.withOwner(meth)): - val canHaveLocalRoot = - if meth.isAnonymousFunction then - ccState.rhsClosure.remove(meth) - || meth.definedLocalRoot.exists // TODO drop - else !meth.isConstructor && !isCaseClassSynthetic - if canHaveLocalRoot && meth.takesCappedParam then - //println(i"level owner: $meth") - ccState.levelOwners += meth - paramss.foreach(traverse) - transformTT(tpt, boxed = false, - exact = tree.symbol.allOverriddenSymbols.hasNext, - mapRoots = true) - traverse(tree.rhs) - //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") - - case tree @ ValDef(_, tpt: TypeTree, _) => - def containsCap(tp: Type) = tp.existsPart: - case CapturingType(_, refs) => refs.isUniversal - case _ => false - def mentionsCap(tree: Tree): Boolean = tree match - case Apply(fn, _) => mentionsCap(fn) - case TypeApply(fn, args) => args.exists(mentionsCap) - case _: InferredTypeTree => false - case _: TypeTree => containsCap(expandAliases(tree.tpe)) - case _ => false + extension (sym: Symbol) def nextInfo(using Context): Type = + atPhase(thisPhase.next)(sym.info) + + def setupTraverser(recheckDef: DefRecheck) = new TreeTraverserWithPreciseImportContexts: + + def transformResultType(tpt: TypeTree, sym: Symbol)(using Context) = + transformTT(tpt, + boxed = sym.is(Mutable, butNot = Method), // types of mutable variables are boxed + exact = sym.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set TODO drop + rootTarget = ctx.owner) + + def traverse(tree: Tree)(using Context): Unit = + tree match + case tree @ DefDef(_, paramss, tpt: TypeTree, _) => + val meth = tree.symbol + if isExcluded(meth) then + return + + inContext(ctx.withOwner(meth)): + paramss.foreach(traverse) + transformResultType(tpt, meth) + traverse(tree.rhs) + //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") + + case tree @ ValDef(_, tpt: TypeTree, _) => + def containsCap(tp: Type) = tp.existsPart: + case CapturingType(_, refs) => refs.isUniversal + case _ => false + def mentionsCap(tree: Tree): Boolean = tree match + case Apply(fn, _) => mentionsCap(fn) + case TypeApply(fn, args) => args.exists(mentionsCap) + case _: InferredTypeTree => false + case _: TypeTree => containsCap(transformExplicitType(tree.tpe, rootTarget = NoSymbol)) + case _ => false + + val sym = tree.symbol + val defCtx = if sym.isOneOf(TermParamOrAccessor) then ctx else ctx.withOwner(sym) + inContext(defCtx): + tree.rhs match + case possiblyTypedClosureDef(ddef) + if !mentionsCap(rhsOfEtaExpansion(ddef)) + && !sym.is(Mutable) + && ddef.symbol.takesCappedParamIn(ddef.symbol.info) => + ccSetup.println(i"Level owner at setup $sym / ${ddef.symbol.info}") + ccState.isLevelOwner(sym) = true + case _ => + transformResultType(tpt, sym) + ccSetup.println(i"mapped $tree = ${tpt.knownType}") + traverse(tree.rhs) + case tree @ TypeApply(fn, args) => + traverse(fn) + for case arg: TypeTree <- args do + transformTT(arg, boxed = true, exact = false, rootTarget = ctx.owner) // type arguments in type applications are boxed + + case tree: TypeDef if tree.symbol.isClass => + inContext(ctx.withOwner(tree.symbol)): + traverseChildren(tree) + + case tree @ SeqLiteral(elems, tpt: TypeTree) => + traverse(elems) + transformTT(tpt, boxed = true, exact = false, rootTarget = ctx.owner) + + case _ => + traverseChildren(tree) + postProcess(tree) + end traverse + + def postProcess(tree: Tree)(using Context): Unit = tree match + case tree: TypeTree => + val lowner = + transformTT(tree, boxed = false, exact = false, + rootTarget = if ctx.owner.isStaticOwner then NoSymbol else ctx.owner + // roots of other types in static locations are not mapped + ) + case tree: ValOrDefDef => val sym = tree.symbol - val mapRoots = tree.rhs match - case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => - //ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - ccState.rhsClosure += ddef.symbol - // Toplevel closures bound to vals count as level owners - // unless the closure is an implicit eta expansion over a type application - // that mentions `cap`. In that case we prefer not to silently rebind - // the `cap` to a local root of an invisible closure. See - // pos-custom-args/captures/eta-expansions.scala for examples of both cases. - !tpt.isInstanceOf[InferredTypeTree] - // in this case roots in inferred val type count as polymorphic + + /** The return type of a constructor instantiated with local type and value + * parameters. Constructors have `unit` result type, that's why we can't + * get this type by reading the result type tree, and have to construct it + * explicitly. + */ + def constrReturnType(info: Type, psymss: List[List[Symbol]]): Type = info match + case info: MethodOrPoly => + constrReturnType(info.instantiate(psymss.head.map(_.namedType)), psymss.tail) case _ => - true - transformTT(tpt, - boxed = sym.is(Mutable), // types of mutable variables are boxed - exact = sym.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set - mapRoots - ) - capt.println(i"mapped $tree = ${tpt.knownType}") - traverse(tree.rhs) - - case tree @ TypeApply(fn, args) => - traverse(fn) - for case arg: TypeTree <- args do - transformTT(arg, boxed = true, exact = false, mapRoots = true) // type arguments in type applications are boxed - - case tree: Template => - val cls = tree.symbol.owner - inContext(ctx.withOwner(cls)): - if cls.primaryConstructor.takesCappedParam then - //println(i"level owner $cls") - ccState.levelOwners += cls - traverseChildren(tree) - case tree: Try if Feature.enabled(Feature.saferExceptions) => - val tryOwner = newSymbol(ctx.owner, nme.TRY_BLOCK, SyntheticMethod, MethodType(Nil, defn.UnitType)) - ccState.levelOwners += tryOwner - ccState.tryBlockOwner(tree) = tryOwner - inContext(ctx.withOwner(tryOwner)): - traverseChildren(tree) - case _ => - traverseChildren(tree) - postProcess(tree) - end traverse - - override def apply(x: Unit, trees: List[Tree])(using Context): Unit = trees match - case (imp: Import) :: rest => - traverse(rest)(using ctx.importContext(imp, imp.symbol)) - case tree :: rest => - traverse(tree) - traverse(rest) - case Nil => - - def postProcess(tree: Tree)(using Context): Unit = tree match - case tree: TypeTree => - transformTT(tree, boxed = false, exact = false, - mapRoots = !ctx.owner.levelOwner.isStaticOwner // other types in static locations are not boxed - ) - case tree: ValOrDefDef => - val sym = tree.symbol - - /** The return type of a constructor instantiated with local type and value - * parameters. Constructors have `unit` result type, that's why we can't - * get this type by reading the result type tree, and have to construct it - * explicitly. - */ - def constrReturnType(info: Type, psymss: List[List[Symbol]]): Type = info match - case info: MethodOrPoly => - constrReturnType(info.instantiate(psymss.head.map(_.namedType)), psymss.tail) - case _ => - info - - /** The local result type, which is the known type of the result type tree, - * with special treatment for constructors. - */ - def localReturnType = - if sym.isConstructor then constrReturnType(sym.info, sym.paramSymss) - else tree.tpt.knownType - - // Replace an existing symbol info with inferred types where capture sets of - // TypeParamRefs and TermParamRefs put in correspondence by BiTypeMaps with the - // capture sets of the types of the method's parameter symbols and result type. - def integrateRT( - info: Type, // symbol info to replace - psymss: List[List[Symbol]], // the local (type and term) parameter symbols corresponding to `info` - prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order - prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order - ): Type = - info match - case mt: MethodOrPoly => - val psyms = psymss.head - val mapr = - if sym.isLevelOwner then mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) - else identity[Type] - mt.companion(mt.paramNames)( - mt1 => - if !psyms.exists(_.isUpdatedAfter(preRecheckPhase)) && !mt.isParamDependent && prevLambdas.isEmpty then - mt.paramInfos - else - val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => mapr(subst(psym.info)).asInstanceOf[mt.PInfo]), - mt1 => - mapr(integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas)) - ) - case info: ExprType => - info.derivedExprType(resType = - integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) - case info => - if prevLambdas.isEmpty then localReturnType - else SubstParams(prevPsymss, prevLambdas)(localReturnType) - - def signatureChanges = - tree.tpt.hasRememberedType && !sym.isConstructor - || tree.match - case tree: DefDef => tree.termParamss.nestedExists(_.tpt.hasRememberedType) + info + + /** The local result type, which is the known type of the result type tree, + * with special treatment for constructors. + */ + def localReturnType = + if sym.isConstructor then constrReturnType(sym.info, sym.paramSymss) + else tree.tpt.knownType + + def paramSignatureChanges = tree.match + case tree: DefDef => tree.paramss.nestedExists: + case param: ValDef => param.tpt.hasRememberedType + case param: TypeDef => param.rhs.hasRememberedType case _ => false - if sym.exists && signatureChanges then - val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) - .showing(i"update info $sym: ${sym.info} = $result", capt) - if newInfo ne sym.info then - updateInfo(sym, - if sym.isAnonymousFunction then - // closures are handled specially; the newInfo is constrained from - // the expected type and only afterwards we recheck the definition - newInfo - else new LazyType: - def complete(denot: SymDenotation)(using Context) = - // infos of other methods are determined from their definitions which - // are checked on demand - denot.info = newInfo - recheckDef(tree, sym)) - else updateOwner(sym) - else if !sym.is(Module) then updateOwner(sym) // Modules are updated with their module classes - - case tree: Bind => - val sym = tree.symbol - updateInfo(sym, transformInferredType(sym.info, boxed = false, mapRoots = true)) - case tree: TypeDef => - tree.symbol match - case cls: ClassSymbol => - val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - val newSelfType = + def signatureChanges = + tree.tpt.hasRememberedType && !sym.isConstructor || paramSignatureChanges + + // Replace an existing symbol info with inferred types where capture sets of + // TypeParamRefs and TermParamRefs put in correspondence by BiTypeMaps with the + // capture sets of the types of the method's parameter symbols and result type. + def integrateRT( + info: Type, // symbol info to replace + psymss: List[List[Symbol]], // the local (type and term) parameter symbols corresponding to `info` + resType: Type, // the locally computed return type + prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order + prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order + ): Type = + info match + case mt: MethodOrPoly => + val psyms = psymss.head + mt.companion(mt.paramNames)( + mt1 => + if !paramSignatureChanges && !mt.isParamDependent && prevLambdas.isEmpty then + mt.paramInfos + else + val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) + psyms.map(psym => subst(psym.nextInfo).asInstanceOf[mt.PInfo]), + mt1 => + integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) + ) + case info: ExprType => + info.derivedExprType(resType = + integrateRT(info.resType, psymss, resType, prevPsymss, prevLambdas)) + case info => + if prevLambdas.isEmpty then resType + else SubstParams(prevPsymss, prevLambdas)(resType) + + if sym.exists && signatureChanges then + def absInfo(resType: Type): Type = + integrateRT(sym.info, sym.paramSymss, resType, Nil, Nil) + .showing(i"update info $sym: ${sym.info} = $result", ccSetup) + val newInfo = absInfo(localReturnType) + if newInfo ne sym.info then + val updatedInfo = + if sym.isAnonymousFunction + || sym.is(Param) + || sym.is(ParamAccessor) + || sym.isPrimaryConstructor + then + // closures are handled specially; the newInfo is constrained from + // the expected type and only afterwards we recheck the definition + newInfo + else new LazyType: + def complete(denot: SymDenotation)(using Context) = + // infos of other methods are determined from their definitions which + // are checked on demand + assert(!isDuringSetup, i"$sym") + assert(ctx.phase == thisPhase.next, i"$sym") + ccSetup.println(i"forcing $sym, printing = ${ctx.mode.is(Mode.Printing)}") + //if ctx.mode.is(Mode.Printing) then new Error().printStackTrace() + denot.info = newInfo + recheckDef(tree, sym) + updateInfo(sym, updatedInfo) + + case tree: Bind => + val sym = tree.symbol + updateInfo(sym, transformInferredType(sym.info)) + case tree: TypeDef => + tree.symbol match + case cls: ClassSymbol => + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then // add capture set to self type of nested classes if no self type is given explicitly. // It's unclear what the right level owner should be. A self type should // be able to mention class parameters, which are owned by the class; that's // why the class was picked as level owner. But self types should not be able // to mention other fields. - CapturingType(cinfo.selfType, CaptureSet.Var(cls)) - else selfInfo match - case selfInfo: Type => - inContext(ctx.withOwner(cls)): - transformExplicitType(selfInfo, boxed = false, mapRoots = true) - case _ => - NoType - if newSelfType.exists then - capt.println(i"mapped self type for $cls: $newSelfType, was $selfInfo") - val newInfo = ClassInfo(prefix, cls, ps, decls, newSelfType) - updateInfo(cls, newInfo) - cls.thisType.asInstanceOf[ThisType].invalidateCaches() - if cls.is(ModuleClass) then - // if it's a module, the capture set of the module reference is the capture set of the self type - val modul = cls.sourceModule - updateInfo(modul, CapturingType(modul.info, newSelfType.captureSet)) - modul.termRef.invalidateCaches() - else - updateOwner(cls) - if cls.is(ModuleClass) then updateOwner(cls.sourceModule) - case _ => - val info = atPhase(preRecheckPhase)(tree.symbol.info) - val newInfo = transformExplicitType(info, boxed = false, mapRoots = !ctx.owner.isStaticOwner) - updateInfo(tree.symbol, newInfo) - if newInfo ne info then - capt.println(i"update info of ${tree.symbol} from $info to $newInfo") - case _ => - end postProcess + val newSelfType = CapturingType(cinfo.selfType, CaptureSet.Var(cls)) + val ps1 = inContext(ctx.withOwner(cls)): + ps.mapConserve(transformExplicitType(_, rootTarget = cls)) + val newInfo = ClassInfo(prefix, cls, ps1, decls, newSelfType) + updateInfo(cls, newInfo) + ccSetup.println(i"update class info of $cls with parents $ps selfinfo $selfInfo to $newInfo") + cls.thisType.asInstanceOf[ThisType].invalidateCaches() + if cls.is(ModuleClass) then + // if it's a module, the capture set of the module reference is the capture set of the self type + val modul = cls.sourceModule + updateInfo(modul, CapturingType(modul.info, newSelfType.captureSet)) + modul.termRef.invalidateCaches() + case _ => + case _ => + end postProcess + end setupTraverser private def superTypeIsImpure(tp: Type)(using Context): Boolean = { - tp.dealias match + tp.dealiasKeepAnnots match case CapturingType(_, refs) => !refs.isAlwaysEmpty + case RetainingType(parent, refs) => + !refs.isEmpty case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol if sym.isClass then @@ -581,7 +640,7 @@ extends tpd.TreeTraverser: superTypeIsImpure(tp.tp1) && superTypeIsImpure(tp.tp2) case _ => false - }.showing(i"super type is impure $tp = $result", capt) + }.showing(i"super type is impure $tp = $result", ccSetup) /** Should a capture set variable be added on type `tp`? */ def needsVariable(tp: Type)(using Context): Boolean = { @@ -591,7 +650,7 @@ extends tpd.TreeTraverser: if sym.isClass then !sym.isPureClass && sym != defn.AnyClass else - val tp1 = tp.dealias + val tp1 = tp.dealiasKeepAnnots if tp1 ne tp then needsVariable(tp1) else superTypeIsImpure(tp1) case tp: (RefinedOrRecType | MatchType) => @@ -602,8 +661,8 @@ extends tpd.TreeTraverser: needsVariable(tp.tp1) || needsVariable(tp.tp2) case CapturingType(parent, refs) => needsVariable(parent) - && refs.isConst // if refs is a variable, no need to add another - && !refs.isUniversal // if refs is {cap}, an added variable would not change anything + && refs.isConst // if refs is a variable, no need to add another + && !refs.containsRoot // if refs is {cap}, an added variable would not change anything case AnnotatedType(parent, _) => needsVariable(parent) case _ => @@ -632,6 +691,10 @@ extends tpd.TreeTraverser: CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) + case tp @ AppliedType(tycon, args) if !defn.isFunctionClass(tp.dealias.typeSymbol) => + tp.derivedAppliedType(tycon, args.mapConserve(box)) + case tp: RealTypeBounds => + tp.derivedTypeBounds(tp.lo, box(tp.hi)) case tp: LazyRef => normalizeCaptures(tp.ref) case _ => @@ -640,7 +703,7 @@ extends tpd.TreeTraverser: /** Add a capture set variable to `tp` if necessary, or maybe pull out * an embedded capture set variable from a part of `tp`. */ - def decorate(tp: Type, mapRoots: Boolean, addedSet: Type => CaptureSet)(using Context): Type = + def decorate(tp: Type, addedSet: Type => CaptureSet)(using Context): Type = if tp.typeSymbol == defn.FromJavaObjectSymbol then // For capture checking, we assume Object from Java is the same as Any tp @@ -648,28 +711,77 @@ extends tpd.TreeTraverser: def maybeAdd(target: Type, fallback: Type) = if needsVariable(target) then CapturingType(target, addedSet(target)) else fallback - val tp0 = normalizeCaptures(tp) - val tp1 = tp0.dealiasKeepAnnots - if tp1 ne tp0 then - val tp2 = transformExplicitType(tp1, boxed = false, mapRoots) - maybeAdd(tp2, if tp2 ne tp1 then tp2 else tp0) - else maybeAdd(tp0, tp0) + val dealiased = tp.dealiasKeepAnnots + if dealiased ne tp then + val transformed = transformInferredType(dealiased) + maybeAdd(transformed, if transformed ne dealiased then transformed else tp) + else maybeAdd(tp, tp) /** Add a capture set variable to `tp` if necessary, or maybe pull out * an embedded capture set variable from a part of `tp`. */ - def addVar(tp: Type, owner: Symbol, mapRoots: Boolean)(using Context): Type = - decorate(tp, mapRoots, + def addVar(tp: Type, owner: Symbol)(using Context): Type = + decorate(tp, addedSet = _.dealias.match case CapturingType(_, refs) => CaptureSet.Var(owner, refs.elems) case _ => CaptureSet.Var(owner)) - def apply(tree: Tree)(using Context): Unit = - traverse(tree)(using ctx.withProperty(Setup.IsDuringSetupKey, Some(()))) + def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit = + setupTraverser(recheckDef).traverse(tree)( + using ctx.withPhase(thisPhase).withProperty(IsDuringSetupKey, Some(()))) -object Setup: - val IsDuringSetupKey = new Property.Key[Unit] + // ------ Checks to run after main capture checking -------------------------- + + /** A list of actions to perform at postCheck */ + private val todoAtPostCheck = new mutable.ListBuffer[Context => Unit] - def isDuringSetup(using Context): Boolean = - ctx.property(IsDuringSetupKey).isDefined + /** If `tp` is a capturing type, check that all references it mentions have non-empty + * capture sets. + * Also: warn about redundant capture annotations. + * This check is performed after capture sets are computed in phase cc. + * Note: We need to perform the check on the original annotation rather than its + * capture set since the conversion to a capture set already eliminates redundant elements. + */ + private def checkWellformedPost(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = + capt.println(i"checkWF post $parent ${ann.retainedElems} in $tpt") + val normCap = new TypeMap: + def apply(t: Type): Type = t match + case t: TermRef if t.isGenericRootCapability => ctx.owner.localRoot.termRef + case _ => t + + var retained = ann.retainedElems.toArray + for i <- 0 until retained.length do + val refTree = retained(i) + val ref = refTree.toCaptureRef + + def pos = + if refTree.span.exists then refTree.srcPos + else if ann.span.exists then ann.srcPos + else tpt.srcPos + + def check(others: CaptureSet, dom: Type | CaptureSet): Unit = + val remaining = others.map(normCap) + if remaining.accountsFor(ref) then + report.warning(em"redundant capture: $dom already accounts for $ref", pos) + + if ref.captureSetOfInfo.elems.isEmpty then + report.error(em"$ref cannot be tracked since its capture set is empty", pos) + if parent.captureSet ne defn.expandedUniversalSet then + check(parent.captureSet, parent) + + val others = + for j <- 0 until retained.length if j != i yield retained(j).toCaptureRef + val remaining = CaptureSet(others*) + check(remaining, remaining) + end for + end checkWellformedPost + + /** Check well formed at post check time */ + private def checkWellformedLater(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = + if !tpt.span.isZeroExtent then + todoAtPostCheck += (ctx1 => + checkWellformedPost(parent, ann, tpt)(using ctx1.withOwner(ctx.owner))) + + def postCheck()(using Context): Unit = + for chk <- todoAtPostCheck do chk(ctx) end Setup \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 1509fd838265..c84467160b50 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -7,7 +7,6 @@ import Symbols.*, SymDenotations.*, Contexts.*, Flags.*, Types.*, Decorators.* import StdNames.nme import Names.Name import NameKinds.DefaultGetterName -import Phases.checkCapturesPhase import config.Printers.capt /** Classification and transformation methods for function methods and @@ -59,9 +58,11 @@ object Synthetics: /** Transform the type of a method either to its type under capture checking * or back to its previous type. * @param sym The method to transform @pre needsTransform(sym) must hold. - * @param toCC Whether to transform the type to capture checking or back. + * @param info The possibly already mapped info of sym */ - def transform(sym: SymDenotation)(using Context): SymDenotation = + def transform(symd: SymDenotation, info: Type)(using Context): SymDenotation = + + def localRootSet = symd.symbol.localRoot.termRef.singletonCaptureSet /** Add capture dependencies to the type of the `apply` or `copy` method of a case class. * An apply method in a case class like this: @@ -73,7 +74,7 @@ object Synthetics: */ def addCaptureDeps(info: Type): Type = info match case info: MethodType => - val trackedParams = info.paramRefs.filter(atPhase(checkCapturesPhase)(_.isTracked)) + val trackedParams = info.paramRefs.filter(atPhase(Phases.checkCapturesPhase)(_.isTracked)) def augmentResult(tp: Type): Type = tp match case tp: MethodOrPoly => tp.derivedLambdaType(resType = augmentResult(tp.resType)) @@ -99,7 +100,7 @@ object Synthetics: info.derivedLambdaType(resType = transformDefaultGetterCaptures(info.resType, owner, idx)) case info: ExprType => info.derivedExprType(transformDefaultGetterCaptures(info.resType, owner, idx)) - case EventuallyCapturingType(parent, _) => + case CapturingType(parent, _) => transformDefaultGetterCaptures(parent, owner, idx) case info @ AnnotatedType(parent, annot) => info.derivedAnnotatedType(transformDefaultGetterCaptures(parent, owner, idx), annot) @@ -117,7 +118,7 @@ object Synthetics: def transformUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => val paramInfo :: Nil = info.paramInfos: @unchecked - val newParamInfo = CapturingType(paramInfo, CaptureSet.universal) + val newParamInfo = CapturingType(paramInfo, localRootSet) val trackedParam = info.paramRefs.head def newResult(tp: Type): Type = tp match case tp: MethodOrPoly => @@ -129,17 +130,17 @@ object Synthetics: case info: PolyType => info.derivedLambdaType(resType = transformUnapplyCaptures(info.resType)) - def transformComposeCaptures(symd: SymDenotation) = - val (pt: PolyType) = symd.info: @unchecked + def transformComposeCaptures(info: Type, owner: Symbol) = + val (pt: PolyType) = info: @unchecked val (mt: MethodType) = pt.resType: @unchecked - val (enclThis: ThisType) = symd.owner.thisType: @unchecked + val (enclThis: ThisType) = owner.thisType: @unchecked pt.derivedLambdaType(resType = MethodType(mt.paramNames)( - mt1 => mt.paramInfos.map(_.capturing(CaptureSet.universal)), + mt1 => mt.paramInfos.map(_.capturing(localRootSet)), mt1 => CapturingType(mt.resType, CaptureSet(enclThis, mt1.paramRefs.head)))) - def transformCurriedTupledCaptures(symd: SymDenotation) = - val (et: ExprType) = symd.info: @unchecked - val (enclThis: ThisType) = symd.owner.thisType: @unchecked + def transformCurriedTupledCaptures(info: Type, owner: Symbol) = + val (et: ExprType) = info: @unchecked + val (enclThis: ThisType) = owner.thisType: @unchecked def mapFinalResult(tp: Type, f: Type => Type): Type = val defn.FunctionOf(args, res, isContextual) = tp: @unchecked if defn.isFunctionNType(res) then @@ -149,19 +150,21 @@ object Synthetics: ExprType(mapFinalResult(et.resType, CapturingType(_, CaptureSet(enclThis)))) def transformCompareCaptures = - MethodType(defn.ObjectType.capturing(CaptureSet.universal) :: Nil, defn.BooleanType) + MethodType(defn.ObjectType.capturing(localRootSet) :: Nil, defn.BooleanType) - sym.copySymDenotation(info = sym.name match + symd.copySymDenotation(info = symd.name match case DefaultGetterName(nme.copy, n) => - transformDefaultGetterCaptures(sym.info, sym.owner, n) + transformDefaultGetterCaptures(info, symd.owner, n) case nme.unapply => - transformUnapplyCaptures(sym.info) + ccState.isLevelOwner(symd.symbol) = true + transformUnapplyCaptures(info) case nme.apply | nme.copy => - addCaptureDeps(sym.info) + addCaptureDeps(info) case nme.andThen | nme.compose => - transformComposeCaptures(sym) + ccState.isLevelOwner(symd.symbol) = true + transformComposeCaptures(info, symd.owner) case nme.curried | nme.tupled => - transformCurriedTupledCaptures(sym) + transformCurriedTupledCaptures(info, symd.owner) case n if n == nme.eq || n == nme.ne => transformCompareCaptures) end transform diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 63d616e1ce3d..526c98eb20b8 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -1,4 +1,6 @@ -package dotty.tools.dotc.config +package dotty.tools.dotc +package config +import core.Contexts.{Context, ctx} object Printers { @@ -13,6 +15,9 @@ object Printers { val default = new Printer val capt = noPrinter + val ccSetupOn = new Printer + def ccSetup(using Context): Printer = if ctx.settings.YccTest.value then ccSetupOn else noPrinter + val constr = noPrinter val core = noPrinter val checks = noPrinter diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 43c2cd5a2ff9..0ecdaf84b499 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -409,6 +409,9 @@ private sealed trait YSettings: val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation.") val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only).") val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Used in conjunction with captureChecking language import, debug info for captured references.") + val YccNew: Setting[Boolean] = BooleanSetting("-Ycc-new", "Used in conjunction with captureChecking language import, debug info for captured references") + val YccTest: Setting[Boolean] = BooleanSetting("-Ycc-test", "Used in conjunction with captureChecking language import, debug info for captured references") + val YccPrintSetup: Setting[Boolean] = BooleanSetting("-Ycc-print-setup", "") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 51182f52a382..570ca6a3afec 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -16,7 +16,8 @@ import Comments.Comment import util.Spans.NoSpan import config.Feature import Symbols.requiredModuleRef -import cc.{CapturingType, CaptureSet, EventuallyCapturingType} +import cc.{CaptureSet, RetainingType} +import ast.tpd.ref import scala.annotation.tailrec @@ -121,8 +122,8 @@ class Definitions { denot.info = TypeAlias( HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)( tl => List.fill(arity + 1)(TypeBounds.empty), - tl => CapturingType(underlyingClass.typeRef.appliedTo(tl.paramRefs), - CaptureSet.universal) + tl => RetainingType(underlyingClass.typeRef.appliedTo(tl.paramRefs), + ref(captureRoot.termRef) :: Nil) )) else val cls = denot.asClass.classSymbol @@ -982,6 +983,7 @@ class Definitions { @tu lazy val Caps_unsafeBox: Symbol = CapsUnsafeModule.requiredMethod("unsafeBox") @tu lazy val Caps_unsafeUnbox: Symbol = CapsUnsafeModule.requiredMethod("unsafeUnbox") @tu lazy val Caps_unsafeBoxFunArg: Symbol = CapsUnsafeModule.requiredMethod("unsafeBoxFunArg") + @tu lazy val expandedUniversalSet: CaptureSet = CaptureSet(captureRoot.termRef) @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") @@ -1247,8 +1249,8 @@ class Definitions { */ object ByNameFunction: def apply(tp: Type)(using Context): Type = tp match - case tp @ EventuallyCapturingType(tp1, refs) if tp.annot.symbol == RetainsByNameAnnot => - CapturingType(apply(tp1), refs) + case tp @ RetainingType(tp1, refs) if tp.annot.symbol == RetainsByNameAnnot => + RetainingType(apply(tp1), refs) case _ => defn.ContextFunction0.typeRef.appliedTo(tp :: Nil) def unapply(tp: Type)(using Context): Option[Type] = tp match diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 4b06140f75c2..fa57c503d61b 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -405,7 +405,7 @@ object Flags { val (_, _, ChildrenQueried @ _) = newFlags(56, "") /** A module variable (Scala 2.x only) / a capture-checked class - * (re-used as a flag for private parameter accessors in Recheck) + * (Scala2ModuleVar is re-used as a flag for private parameter accessors in Recheck) */ val (_, Scala2ModuleVar @ _, CaptureChecked @ _) = newFlags(57, "/") diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 3dfa5225df5b..709b788034f8 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -24,7 +24,7 @@ import config.Config import reporting._ import collection.mutable import transform.TypeUtils._ -import cc.{CapturingType, derivedCapturingType, Setup, EventuallyCapturingType, isEventuallyCapturingType} +import cc.{CapturingType, derivedCapturingType} import scala.annotation.internal.sharable diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index 9155af668c7a..1cd1a3ad4d39 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -12,7 +12,6 @@ import Names._ import StdNames.nme import Flags.{Module, Provisional} import dotty.tools.dotc.config.Config -import cc.boxedUnlessFun import dotty.tools.dotc.transform.TypeUtils.isErasedValueType object TypeApplications { @@ -354,7 +353,7 @@ class TypeApplications(val self: Type) extends AnyVal { } if ((dealiased eq stripped) || followAlias) try - val instantiated = dealiased.instantiate(args.mapConserve(_.boxedUnlessFun(self))) + val instantiated = dealiased.instantiate(args) if (followAlias) instantiated.normalized else instantiated catch case ex: IndexOutOfBoundsException => @@ -502,7 +501,7 @@ class TypeApplications(val self: Type) extends AnyVal { * Existential types in arguments are returned as TypeBounds instances. */ final def argInfos(using Context): List[Type] = self.stripped match - case AppliedType(tycon, args) => args.boxedUnlessFun(tycon) + case AppliedType(tycon, args) => args case _ => Nil /** If this is an encoding of a function type, return its arguments, otherwise return Nil. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8df809dc9ee6..aa8933e19c39 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,7 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly -import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam, isAlwaysPure, mapRoots, localRoot} +import cc.* import NameKinds.WildcardParamName /** Provides methods to compare types. @@ -68,6 +68,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private var myInstance: TypeComparer = this def currentInstance: TypeComparer = myInstance + /** All capturing types in the original `tp1` enclosing the currently + * compared type. + */ + private var enclosingCapturing1: List[AnnotatedType] = Nil + /** Is a subtype check in progress? In that case we may not * permanently instantiate type variables, because the corresponding * constraint might still be retracted and the instantiation should @@ -256,7 +261,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling report.log(explained(_.isSubType(tp1, tp2, approx))) } // Eliminate LazyRefs before checking whether we have seen a type before - val normalize = new TypeMap { + val normalize = new TypeMap with CaptureSet.IdempotentCaptRefMap { val DerefLimit = 10 var derefCount = 0 def apply(t: Type) = t match { @@ -538,12 +543,23 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling res - case CapturingType(parent1, refs1) => - if tp2.isAny then true - else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) - || !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure - then recur(parent1, tp2) - else thirdTry + case tp1 @ CapturingType(parent1, refs1) => + def compareCapturing = + if tp2.isAny then true + else if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) + || !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure + then + val tp2a = + if tp1.isBoxedCapturing && !parent1.isBoxedCapturing + then tp2.unboxed + else tp2 + try + enclosingCapturing1 = tp1 :: enclosingCapturing1 + recur(parent1, tp2a) + finally + enclosingCapturing1 = enclosingCapturing1.tail + else thirdTry + compareCapturing case tp1: AnnotatedType if !tp1.isRefining => recur(tp1.parent, tp2) case tp1: MatchType => @@ -574,7 +590,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling || narrowGADTBounds(tp2, tp1, approx, isUpper = false)) && (isBottom(tp1) || GADTusage(tp2.symbol)) - isSubApproxHi(tp1, info2.lo.boxedIfTypeParam(tp2.symbol)) && (trustBounds || isSubApproxHi(tp1, info2.hi)) + isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi)) || compareGADT || tryLiftedToThis2 || fourthTry @@ -641,7 +657,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def compareRefined: Boolean = val tp1w = tp1.widen - if ctx.phase == Phases.checkCapturesPhase then + if isCaptureCheckingOrSetup then // A relaxed version of subtyping for dependent functions where method types // are treated as contravariant. @@ -655,10 +671,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case (info1: MethodType, info2: MethodType) => matchingMethodParams(info1, info2, precise = false) && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) - case (info1 @ CapturingType(parent1, refs1), info2: Type) => + case (info1 @ CapturingType(parent1, refs1), info2: Type) + if info2.stripCapturing.isInstanceOf[MethodOrPoly] => subCaptures(refs1, info2.captureSet, frozenConstraint).isOK && sameBoxed(info1, info2, refs1) && isSubInfo(parent1, info2) - case (info1: Type, CapturingType(parent2, refs2)) => + case (info1: Type, CapturingType(parent2, refs2)) + if info1.stripCapturing.isInstanceOf[MethodOrPoly] => val refs1 = info1.captureSet (refs1.isAlwaysEmpty || subCaptures(refs1, refs2, frozenConstraint).isOK) && sameBoxed(info1, info2, refs1) && isSubInfo(info1, parent2) @@ -667,7 +685,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if defn.isFunctionType(tp2) then if tp2.derivesFrom(defn.PolyFunctionClass) then - return isSubInfo(tp1.member(nme.apply).info, tp2.refinedInfo) + return isSubInfo(tp1.ccMember(nme.apply).info, tp2.refinedInfo) else tp1w.widenDealias match case tp1: RefinedType => @@ -908,7 +926,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling canWidenAbstract && acc(true, tp) def tryBaseType(cls2: Symbol) = - val base = nonExprBaseType(tp1, cls2).boxedIfTypeParam(tp1.typeSymbol) + val base = nonExprBaseType(tp1, cls2) if base.exists && (base ne tp1) && (!caseLambda.exists || widenAbstractOKFor(tp2) @@ -942,7 +960,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && (tp2.isAny || GADTusage(tp1.symbol)) (!caseLambda.exists || widenAbstractOKFor(tp2)) - && isSubType(hi1.boxedIfTypeParam(tp1.symbol), tp2, approx.addLow) && (trustBounds || isSubType(lo1, tp2, approx.addLow)) + && isSubType(hi1, tp2, approx.addLow) && (trustBounds || isSubType(lo1, tp2, approx.addLow)) || compareGADT || tryLiftedToThis1 case _ => @@ -995,7 +1013,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def tp1widened = val tp1w = tp1.underlying.widenExpr tp1 match - case tp1: CaptureRef if tp1.isTracked => + case tp1: CaptureRef if isCaptureCheckingOrSetup && tp1.isTracked => CapturingType(tp1w.stripCapturing, tp1.singletonCaptureSet) case _ => tp1w @@ -1792,7 +1810,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling else if v > 0 then isSubType(arg1, arg2) else isSameType(arg2, arg1) - isSubArg(args1.head.boxedUnlessFun(tp1), args2.head.boxedUnlessFun(tp1)) + isSubArg(args1.head, args2.head) } && recurArgs(args1.tail, args2.tail, tparams2.tail) recurArgs(args1, args2, tparams2) @@ -2020,7 +2038,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * rebase both itself and the member info of `tp` on a freshly created skolem type. */ def hasMatchingMember(name: Name, tp1: Type, tp2: RefinedType): Boolean = - trace(i"hasMatchingMember($tp1 . $name :? ${tp2.refinedInfo}), mbr: ${tp1.member(name).info}", subtyping) { + trace(i"hasMatchingMember($tp1 . $name :? ${tp2.refinedInfo}), mbr: ${tp1.ccMember(name).info}", subtyping) { // If the member is an abstract type and the prefix is a path, compare the member itself // instead of its bounds. This case is needed situations like: @@ -2096,13 +2114,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling ExprType(info1.resType) case info1 => info1 - if ctx.phase == Phases.checkCapturesPhase then + if ccConfig.oldRefiningVars && isCaptureCheckingOrSetup then // When comparing against a RefiningVar refinement, map the // localRoot of the corresponding class in `tp1` to the owner of the // refining capture set. tp2.refinedInfo match case rinfo2 @ CapturingType(_, refs: CaptureSet.RefiningVar) => - info1 = mapRoots(refs.getter.owner.localRoot.termRef, refs.owner.localRoot.termRef)(info1) + info1 = mapRoots(refs.getter.owner.localRoot.termRef, refs.levelLimit.localRoot.termRef)(info1) case _ => isSubInfo(info1, info2, m.symbol.info.orElse(info1)) @@ -2110,9 +2128,30 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling || (tp1.isStable && m.symbol.isStableMember && isSubType(TermRef(tp1, m.symbol), tp2.refinedInfo)) end qualifies - tp1.member(name).hasAltWithInline(qualifies) + tp1.ccMember(name).hasAltWithInline(qualifies) } + extension (qual: Type) + /** Add all directly enclosing capture sets to `qual` and select `name` on the + * resulting type. A capture set is directly enclosing if there is an enclosing + * capturing type with the set and all types between `qual` and that type + * are RefinedTypes or CapturingTypes. + */ + def ccMember(name: Name): Denotation = + def isEnclosing(tp: Type): Boolean = tp match + case RefinedType(parent, _, _) => isEnclosing(parent) + case CapturingType(parent, _) => isEnclosing(parent) + case _ => tp eq qual + + def addCaptures(tp: Type, encls: List[AnnotatedType]): Type = encls match + case (ct @ CapturingType(parent, refs)) :: encls1 if isEnclosing(parent) => + addCaptures(CapturingType(tp, refs, ct.isBoxedCapturing), encls1) + case _ => + tp + + addCaptures(qual, enclosingCapturing1).member(name) + end ccMember + final def ensureStableSingleton(tp: Type): SingletonType = tp.stripTypeVar match { case tp: SingletonType if tp.isStable => tp case tp: ValueType => SkolemType(tp) @@ -2220,7 +2259,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val paramsMatch = if precise then isSameTypeWhenFrozen(formal1, formal2a) - else if ctx.phase == Phases.checkCapturesPhase then + else if isCaptureCheckingOrSetup then // allow to constrain capture set variables isSubType(formal2a, formal1) else @@ -2372,7 +2411,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** The least upper bound of two types * @param canConstrain If true, new constraints might be added to simplify the lub. * @param isSoft If the lub is a union, this determines whether it's a soft union. - * @note We do not admit singleton types in or-types as lubs. */ def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true): Type = /*>|>*/ trace(s"lub(${tp1.show}, ${tp2.show}, canConstrain=$canConstrain, isSoft=$isSoft)", subtyping, show = true) /*<|<*/ { if (tp1 eq tp2) tp1 @@ -3389,7 +3427,7 @@ class ExplainingTypeComparer(initctx: Context) extends TypeComparer(initctx) { } override def hasMatchingMember(name: Name, tp1: Type, tp2: RefinedType): Boolean = - traceIndented(s"hasMatchingMember(${show(tp1)} . $name, ${show(tp2.refinedInfo)}), member = ${show(tp1.member(name).info)}") { + traceIndented(s"hasMatchingMember(${show(tp1)} . $name, ${show(tp2.refinedInfo)}), member = ${show(tp1.ccMember(name).info)}") { super.hasMatchingMember(name, tp1, tp2) } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 2b4dc05c1a16..9eb18629b730 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -11,7 +11,7 @@ import StdNames._ import collection.mutable import ast.tpd._ import reporting.trace -import config.Printers.typr +import config.Printers.{typr, ccSetup} import config.Feature import transform.SymUtils.* import typer.ProtoTypes._ @@ -19,7 +19,7 @@ import typer.ForceDegree import typer.Inferencing._ import typer.IfBottom import reporting.TestingReporter -import cc.{CapturingType, derivedCapturingType, CaptureSet, isBoxed, isBoxedCapturing} +import cc.{CapturingType, derivedCapturingType, CaptureSet, isBoxed, isBoxedCapturing, isLevelOwner, localRoot} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -93,13 +93,26 @@ object TypeOps: } } + def mapLocalRoot(tp: TermRef): Type = + if tp.symbol.owner.isLocalDummy then + val pre1 = toPrefix(pre, cls, tp.localRootOwner.asClass) + if pre1 ne tp then pre1 match + case pre1: ThisType if pre1.cls.isLevelOwner => pre1.cls.localRoot.termRef + case _ => pre1.captureSet.impliedRoot(tp) + else tp + else tp + trace.conditionally(track, s"asSeen ${tp.show} from (${pre.show}, ${cls.show})", show = true) { // !!! DEBUG // All cases except for ThisType are the same as in Map. Inlined for performance // TODO: generalize the inlining trick? tp match { case tp: NamedType => val sym = tp.symbol - if (sym.isStatic && !sym.maybeOwner.seesOpaques || (tp.prefix `eq` NoPrefix)) tp + if sym.isStatic && !sym.maybeOwner.seesOpaques then tp + else if tp.prefix `eq` NoPrefix then + if tp.name == nme.LOCAL_CAPTURE_ROOT + then mapLocalRoot(tp.asInstanceOf[TermRef]) + else tp else derivedSelect(tp, atVariance(variance max 0)(this(tp.prefix))) case tp: LambdaType => mapOverLambda(tp) // special cased common case diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 8a725d8a0754..7a4a19ea9d6e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,7 +36,7 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized -import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun, ccNestingLevel} +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, RetainingType, CaptureRoot, isCaptureChecking} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -1450,12 +1450,12 @@ object Types { if (tp1.exists) tp1.dealias1(keep, keepOpaques) else tp case tp: AnnotatedType => val parent1 = tp.parent.dealias1(keep, keepOpaques) - tp match + if keep(tp) then tp.derivedAnnotatedType(parent1, tp.annot) + else tp match case tp @ CapturingType(parent, refs) => tp.derivedCapturingType(parent1, refs) case _ => - if keep(tp) then tp.derivedAnnotatedType(parent1, tp.annot) - else parent1 + parent1 case tp: LazyRef => tp.ref.dealias1(keep, keepOpaques) case _ => this @@ -2180,12 +2180,10 @@ object Types { /** Is this reference a local root capability `{}` * for some level owner? */ - def isLocalRootCapability(using Context): Boolean = - localRootOwner.exists - - /** If this is a local root capability, its owner, otherwise NoSymbol. - */ - def localRootOwner(using Context): Symbol = NoSymbol + def isLocalRootCapability(using Context): Boolean = this match + case tp: TermRef => tp.localRootOwner.exists + case tp: CaptureRoot.Var => true + case _ => false /** Is this reference the a (local or generic) root capability? */ def isRootCapability(using Context): Boolean = @@ -2207,7 +2205,7 @@ object Types { else myCaptureSet = CaptureSet.Pending val computed = CaptureSet.ofInfo(this) - if ctx.phase != Phases.checkCapturesPhase || underlying.isProvisional then + if !isCaptureChecking || underlying.isProvisional then myCaptureSet = null else myCaptureSet = computed @@ -2708,7 +2706,7 @@ object Types { if (tparams.head.eq(tparam)) return args.head match { case _: TypeBounds if !widenAbstract => TypeRef(pre, tparam) - case arg => arg.boxedUnlessFun(tycon) + case arg => arg } tparams = tparams.tail args = args.tail @@ -2926,13 +2924,11 @@ object Types { override def isGenericRootCapability(using Context): Boolean = name == nme.CAPTURE_ROOT && symbol == defn.captureRoot - override def localRootOwner(using Context): Symbol = - if name == nme.LOCAL_CAPTURE_ROOT then - if symbol.owner.isLocalDummy then symbol.owner.owner - else symbol.owner - else if info.isRef(defn.Caps_Cap) then - val owner = symbol.maybeOwner - if owner.isTerm then owner else NoSymbol + def localRootOwner(using Context): Symbol = + val owner = symbol.maybeOwner + def normOwner = if owner.isLocalDummy then owner.owner else owner + if name == nme.LOCAL_CAPTURE_ROOT then normOwner + else if info.isRef(defn.Caps_Cap) && owner.isTerm then normOwner else NoSymbol override def normalizedRef(using Context): CaptureRef = @@ -5118,8 +5114,8 @@ object Types { else if (clsd.is(Module)) givenSelf else if (ctx.erasedTypes) appliedRef else givenSelf.dealiasKeepAnnots match - case givenSelf1 @ EventuallyCapturingType(tp, _) => - givenSelf1.derivedAnnotatedType(tp & appliedRef, givenSelf1.annot) + case givenSelf1 @ AnnotatedType(tp, ann) if ann.symbol == defn.RetainsAnnot => + givenSelf1.derivedAnnotatedType(tp & appliedRef, ann) case _ => AndType(givenSelf, appliedRef) } @@ -5960,17 +5956,16 @@ object Types { } /** A type map that maps also parents and self type of a ClassInfo */ - abstract class DeepTypeMap(using Context) extends TypeMap { - override def mapClassInfo(tp: ClassInfo): ClassInfo = { + abstract class DeepTypeMap(using Context) extends TypeMap: + override def mapClassInfo(tp: ClassInfo): ClassInfo = val prefix1 = this(tp.prefix) - val parents1 = tp.declaredParents mapConserve this - val selfInfo1: TypeOrSymbol = tp.selfInfo match { - case selfInfo: Type => this(selfInfo) - case selfInfo => selfInfo - } - tp.derivedClassInfo(prefix1, parents1, tp.decls, selfInfo1) - } - } + inContext(ctx.withOwner(tp.cls)): + val parents1 = tp.declaredParents.mapConserve(this) + val selfInfo1: TypeOrSymbol = tp.selfInfo match + case selfInfo: Type => inContext(ctx.withOwner(tp.cls))(this(selfInfo)) + case selfInfo => selfInfo + tp.derivedClassInfo(prefix1, parents1, tp.decls, selfInfo1) + end DeepTypeMap @sharable object IdentityTypeMap extends TypeMap()(NoContext) { def apply(tp: Type): Type = tp diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 7fa335afbf44..7c0f1ac5518b 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -654,6 +654,7 @@ class TreeUnpickler(reader: TastyReader, if (isClass) { if sym.owner.is(Package) then if annots.exists(_.hasSymbol(defn.CaptureCheckedAnnot)) then + sym.setFlag(CaptureChecked) withCaptureChecks = true withPureFuns = true else if annots.exists(_.hasSymbol(defn.WithPureFunsAnnot)) then diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 83ff03e05592..174454d24751 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -15,7 +15,7 @@ import util.SourcePosition import scala.util.control.NonFatal import scala.annotation.switch import config.{Config, Feature} -import cc.{CapturingType, EventuallyCapturingType, CaptureSet, CaptureRoot, isBoxed, ccNestingLevel, levelOwner} +import cc.{CapturingType, RetainingType, CaptureSet, CaptureRoot, isBoxed, levelOwner, retainedElems} class PlainPrinter(_ctx: Context) extends Printer { @@ -49,9 +49,10 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def debugPos: Boolean = ctx.settings.YdebugPos.value /** If true, shorten local roots of current owner tp `cap`, - * TODO: we should drop this switch once we implemented disambiguation of capture roots. + * For now it is better to turn this off, so that we get the best info for diagnosis. + * TODO: we should consider dropping this switch once we implemented disambiguation of capture roots. */ - private val shortenCap = true + private val shortenCap = false def homogenize(tp: Type): Type = if (homogenizedView) @@ -161,8 +162,20 @@ class PlainPrinter(_ctx: Context) extends Printer { val core: Text = if !cs.isConst && cs.elems.isEmpty then "?" else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + // ~ Str("?").provided(!cs.isConst) core ~ cs.optionalInfo + private def toTextRetainedElem[T <: Untyped](ref: Tree[T]): Text = ref match + case ref: RefTree[_] if ref.typeOpt.exists => + toTextCaptureRef(ref.typeOpt) + case Apply(fn, Literal(str) :: Nil) if fn.symbol == defn.Caps_capIn => + s"cap[${str.stringValue}]" + case _ => + toText(ref) + + private def toTextRetainedElems[T <: Untyped](refs: List[Tree[T]]): Text = + "{" ~ Text(refs.map(ref => toTextRetainedElem(ref))) ~ "}" + /** Print capturing type, overridden in RefinedPrinter to account for * capturing function types. */ @@ -173,6 +186,11 @@ class PlainPrinter(_ctx: Context) extends Printer { final protected def rootSetText = Str("{cap}") // TODO Use disambiguation + // Lazy version of isRootCapability; used to not force completers when printing + private def isRootCap(tp: CaptureRef): Boolean = tp match + case tp: TermRef => tp.symbol.isCompleted && tp.isRootCapability + case _ => tp.isRootCapability + def toText(tp: Type): Text = controlled { homogenize(tp) match { case tp: TypeType => @@ -180,7 +198,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: TermRef if !tp.denotationIsCurrent && !homogenizedView // always print underlying when testing picklers - && !tp.isRootCapability + && !isRootCap(tp) || tp.symbol.is(Module) || tp.symbol.name == nme.IMPORT => toTextRef(tp) ~ ".type" @@ -230,9 +248,9 @@ class PlainPrinter(_ctx: Context) extends Printer { keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~ (" <: " ~ toText(bound) provided !bound.isAny) }.close - case tp @ EventuallyCapturingType(parent, refs) => + case tp @ CapturingType(parent, refs) => val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value - val rootsInRefs = refs.elems.filter(_.isRootCapability).toList + val rootsInRefs = refs.elems.filter(isRootCap(_)).toList val showAsCap = rootsInRefs match case (tp: TermRef) :: Nil => if tp.symbol == defn.captureRoot then @@ -249,6 +267,11 @@ class PlainPrinter(_ctx: Context) extends Printer { false val refsText = if showAsCap then rootSetText else toTextCaptureSet(refs) toTextCapturing(parent, refsText, boxText) + case tp @ RetainingType(parent, refs) => + val refsText = refs match + case ref :: Nil if ref.symbol == defn.captureRoot => rootSetText + case _ => toTextRetainedElems(refs) + toTextCapturing(parent, refsText, "") ~ Str("R").provided(printDebug) case tp: PreviousErrorType if ctx.settings.XprintTypes.value => "" // do not print previously reported error message because they may try to print this error type again recuresevely case tp: ErrorType => @@ -271,8 +294,10 @@ class PlainPrinter(_ctx: Context) extends Printer { } case ExprType(restp) => def arrowText: Text = restp match - case ct @ EventuallyCapturingType(parent, refs) if ct.annot.symbol == defn.RetainsByNameAnnot => - if refs.isUniversal then Str("=>") else Str("->") ~ toTextCaptureSet(refs) + case AnnotatedType(parent, ann) if ann.symbol == defn.RetainsByNameAnnot => + val refs = ann.tree.retainedElems + if refs.exists(_.symbol == defn.captureRoot) then Str("=>") + else Str("->") ~ toTextRetainedElems(refs) case _ => if Feature.pureFunsEnabled then "->" else "=>" changePrec(GlobalPrec)(arrowText ~ " " ~ toText(restp)) @@ -349,10 +374,7 @@ class PlainPrinter(_ctx: Context) extends Printer { */ protected def idString(sym: Symbol): String = (if (showUniqueIds || Printer.debugPrintUnique) "#" + sym.id else "") + - (if showNestingLevel then - if ctx.phase == Phases.checkCapturesPhase then "%" + sym.ccNestingLevel - else "%" + sym.nestingLevel - else "") + (if showNestingLevel then "%" + sym.nestingLevel else "") def nameString(sym: Symbol): String = simpleNameString(sym) + idString(sym) // + "<" + (if (sym.exists) sym.owner else "") + ">" @@ -383,11 +405,9 @@ class PlainPrinter(_ctx: Context) extends Printer { tp match { case tp: TermRef => if tp.symbol.name == nme.LOCAL_CAPTURE_ROOT then // TODO: Move to toTextCaptureRef - if ctx.owner.levelOwner == tp.localRootOwner && !printDebug && shortenCap then - Str("cap") - else - Str(s"cap[${tp.localRootOwner.name}]") ~ - Str(s"%${tp.symbol.ccNestingLevel}").provided(showNestingLevel) + if ctx.owner.levelOwner == tp.localRootOwner && !printDebug && shortenCap + then Str("cap") + else Str(s"cap[${nameString(tp.localRootOwner)}]") else toTextPrefixOf(tp) ~ selectionString(tp) case tp: ThisType => nameString(tp.cls) + ".this" @@ -411,10 +431,12 @@ class PlainPrinter(_ctx: Context) extends Printer { if tp.followAlias ne tp then toTextRef(tp.followAlias) else def boundText(sym: Symbol): Text = - (toTextRef(sym.termRef) - ~ Str(s"/${sym.ccNestingLevel}").provided(showNestingLevel) - ).provided(sym.exists) - "'cap[" ~ boundText(tp.lowerBound) ~ ".." ~ boundText(tp.upperBound) ~ "]" + toTextRef(sym.termRef).provided(sym.exists) + "'cap[" + ~ toTextRef(tp.outerLimit.termRef).provided(!tp.outerLimit.isRoot) + ~ ".." + ~ toTextRef(tp.innerLimit.termRef) + ~ "]" ~ ("(from instantiating " ~ nameString(tp.source) ~ ")").provided(tp.source.exists) } } diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 114037fd0bd0..d096a3383cb3 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -29,7 +29,7 @@ import config.{Config, Feature} import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} -import cc.{CaptureSet, CapturingType, toCaptureSet, IllegalCaptureRef, ccNestingLevelOpt} +import cc.{CaptureSet, CapturingType, toCaptureSet, IllegalCaptureRef} class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { @@ -868,13 +868,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { protected def optAscription[T <: Untyped](tpt: Tree[T]): Text = optText(tpt)(": " ~ _) - private def nestingLevel(sym: Symbol): Int = - sym.ccNestingLevelOpt.getOrElse(sym.nestingLevel) - private def idText(tree: untpd.Tree): Text = (if showUniqueIds && tree.hasType && tree.symbol.exists then s"#${tree.symbol.id}" else "") ~ (if showNestingLevel then tree.typeOpt match - case tp: NamedType if !tp.symbol.isStatic => s"%${nestingLevel(tp.symbol)}" + case tp: NamedType if !tp.symbol.isStatic => s"%${tp.symbol.nestingLevel}" case tp: TypeVar => s"%${tp.nestingLevel}" case tp: TypeParamRef => ctx.typerState.constraint.typeVarOfParam(tp) match case tvar: TypeVar => s"%${tvar.nestingLevel}" diff --git a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala index 48dc7c818360..0e38e9c074cd 100644 --- a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala +++ b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala @@ -8,6 +8,7 @@ import NameKinds.DefaultGetterName import NullOpsDecorator._ import collection.immutable.BitSet import scala.annotation.tailrec +import cc.isCaptureChecking /** A module that can produce a kind of iterator (`Cursor`), * which yields all pairs of overriding/overridden symbols @@ -31,7 +32,7 @@ object OverridingPairs: */ protected def exclude(sym: Symbol): Boolean = !sym.memberCanMatchInheritedSymbols - || ctx.phase == Phases.checkCapturesPhase && sym.is(Recheck.ResetPrivate) + || isCaptureChecking && sym.is(Recheck.ResetPrivate) /** The parents of base that are checked when deciding whether an overriding * pair has already been treated in a parent class. diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 306ca2b0eb9c..8fbfddf3581c 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -17,7 +17,7 @@ import typer.TypeAssigner.seqLitType import typer.ConstFold import typer.ErrorReporting.{Addenda, NothingToAdd} import NamerOps.methodType -import config.Printers.recheckr +import config.Printers.{recheckr, ccSetup, noPrinter} import util.Property import StdNames.nme import reporting.trace @@ -46,27 +46,23 @@ object Recheck: case Some(tpe) => tree1.withType(tpe) case None => tree1 - extension (sym: Symbol) + extension (sym: Symbol)(using Context) /** Update symbol's info to newInfo after `prevPhase`. * Also update owner to newOwnerOrNull if it is not null. * The update is valid until after Recheck. After that the symbol's denotation * is reset to what it was before PreRecheck. */ - def updateInfo(prevPhase: DenotTransformer, newInfo: Type, newOwnerOrNull: Symbol | Null = null)(using Context): Unit = - val newOwner = if newOwnerOrNull == null then sym.owner else newOwnerOrNull - if (sym.info ne newInfo) || (sym.owner ne newOwner) then - val flags = sym.flags - sym.copySymDenotation( - owner = newOwner, - info = newInfo, - initFlags = if newInfo.isInstanceOf[LazyType] then flags &~ Touched else flags - ).installAfter(prevPhase) + def updateInfo(prevPhase: DenotTransformer, newInfo: Type, newFlags: FlagSet = sym.flags, newOwner: Symbol = sym.owner): Unit = + if (sym.info ne newInfo) || sym.flags != newFlags || (sym.maybeOwner ne newOwner) then + val flags = if newInfo.isInstanceOf[LazyType] then newFlags &~ Touched else newFlags + sym.copySymDenotation(owner = newOwner, info = newInfo, initFlags = flags) + .installAfter(prevPhase) /** Does symbol have a new denotation valid from phase.next that is different * from the denotation it had before? */ - def isUpdatedAfter(phase: Phase)(using Context) = + def isUpdatedAfter(phase: Phase) = val symd = sym.denot symd.validFor.firstPhaseId == phase.id + 1 && (sym.originDenotation ne symd) @@ -142,19 +138,28 @@ abstract class Recheck extends Phase, SymTransformer: import ast.tpd.* import Recheck.* + /** The phase before rechecking, used to setup symbol infos. */ def preRecheckPhase = this.prev.asInstanceOf[PreRecheck] + /** The first phase that pepares for rechecking. This is usually preRecheckPhase + * but could also be before. Updated symbols will snap back to their + * denotations at firestPrepPhase after rechecking. + */ + def firstPrepPhase: Phase = preRecheckPhase + override def changesBaseTypes: Boolean = true override def isCheckable = false // TODO: investigate what goes wrong we Ycheck directly after rechecking. // One failing test is pos/i583a.scala - /** Change any `ResetPrivate` flags back to `Private` */ + /** Change denotation back to what it was before (pre-)rechecking` */ def transformSym(symd: SymDenotation)(using Context): SymDenotation = val sym = symd.symbol - if sym.isUpdatedAfter(preRecheckPhase) - then atPhase(preRecheckPhase)(sym.denot.copySymDenotation()) + def updatedAfter(p: Phase): Boolean = + sym.isUpdatedAfter(p) || p != preRecheckPhase && updatedAfter(p.next) + if updatedAfter(firstPrepPhase) + then atPhase(firstPrepPhase)(sym.denot.copySymDenotation()) else symd def run(using Context): Unit = @@ -202,7 +207,7 @@ abstract class Recheck extends Phase, SymTransformer: val tree2 = ConstFold(tree1) if tree2 ne tree1 then tree2.tpe else tp - def recheckIdent(tree: Ident)(using Context): Type = + def recheckIdent(tree: Ident, pt: Type)(using Context): Type = tree.tpe def recheckSelect(tree: Select, pt: Type)(using Context): Type = @@ -250,13 +255,17 @@ abstract class Recheck extends Phase, SymTransformer: val exprType = recheck(expr, defn.UnitType) bindType.symbol.info - def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = - if !tree.rhs.isEmpty then recheck(tree.rhs, sym.info) + def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = + val resType = recheck(tree.tpt) + if tree.rhs.isEmpty then resType + else recheck(tree.rhs, resType) - def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = - val rhsCtx = linkConstructorParams(sym).withOwner(sym) - if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then - inContext(rhsCtx) { recheck(tree.rhs, recheck(tree.tpt)) } + def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = + inContext(linkConstructorParams(sym).withOwner(sym)): + val resType = recheck(tree.tpt) + if tree.rhs.isEmpty || sym.isInlineMethod || sym.isEffectivelyErased + then resType + else recheck(tree.rhs, resType) def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type = recheck(tree.rhs) @@ -286,6 +295,7 @@ abstract class Recheck extends Phase, SymTransformer: protected def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = mt.instantiate(argTypes) + /** A hook to massage the type of an applied method; currently not overridden */ protected def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = funtpe def recheckApply(tree: Apply, pt: Type)(using Context): Type = @@ -425,7 +435,7 @@ abstract class Recheck extends Phase, SymTransformer: seqLitType(tree, TypeComparer.lub(declaredElemType :: elemTypes)) def recheckTypeTree(tree: TypeTree)(using Context): Type = - knownType(tree) // allows to install new types at Setup + tree.knownType // allows to install new types at Setup def recheckAnnotated(tree: Annotated)(using Context): Type = tree.tpe match @@ -451,7 +461,7 @@ abstract class Recheck extends Phase, SymTransformer: case _ => traverse(stats) - def recheckDef(tree: ValOrDefDef, sym: Symbol)(using Context): Unit = + def recheckDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type = inContext(ctx.localContext(tree, sym)) { tree match case tree: ValDef => recheckValDef(tree, sym) @@ -467,7 +477,7 @@ abstract class Recheck extends Phase, SymTransformer: def recheckNamed(tree: NameTree, pt: Type)(using Context): Type = val sym = tree.symbol tree match - case tree: Ident => recheckIdent(tree) + case tree: Ident => recheckIdent(tree, pt) case tree: Select => recheckSelect(tree, pt) case tree: Bind => recheckBind(tree, pt) case tree: ValOrDefDef => @@ -526,12 +536,14 @@ abstract class Recheck extends Phase, SymTransformer: tpe def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = - trace(i"rechecking $tree with pt = $pt", recheckr, show = true) { + def op = try recheckFinish(recheckStart(tree, pt), tree, pt) catch case ex: Exception => println(i"error while rechecking $tree") throw ex - } + if ccSetup eq noPrinter then op + else trace.force(i"rechecking $tree with pt = $pt", recheckr, show = true): + op /** Typing and previous transforms sometimes leaves skolem types in prefixes of * NamedTypes in `expected` that do not match the `actual` Type. -Ycheck does @@ -582,7 +594,7 @@ abstract class Recheck extends Phase, SymTransformer: err.typeMismatch(tree.withType(actual), expected, addenda) else if debugSuccesses then tree match - case _: Ident => + case closureDef(_) => println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") case _ => end checkConformsExpr diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 51cf019a2f85..992438b3dbae 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -39,6 +39,7 @@ import config.Feature.sourceVersion import config.SourceVersion._ import printing.Formatting.hlAsKeyword import transform.TypeUtils.* +import cc.isCaptureChecking import collection.mutable import reporting._ @@ -67,7 +68,7 @@ object Checking { */ def checkBounds(args: List[tpd.Tree], boundss: List[TypeBounds], instantiate: (Type, List[Type]) => Type, app: Type = NoType, tpt: Tree = EmptyTree)(using Context): Unit = - if ctx.phase != Phases.checkCapturesPhase then + if !isCaptureChecking then args.lazyZip(boundss).foreach { (arg, bound) => if !bound.isLambdaSub && !arg.tpe.hasSimpleKind then errorTree(arg, @@ -152,7 +153,7 @@ object Checking { // if we attempt to check bounds of F-bounded mutually recursive Java interfaces. // Do check all bounds in Scala units and those bounds in Java units that // occur in applications of Scala type constructors. - && !(ctx.phase == Phases.checkCapturesPhase && !tycon.typeSymbol.is(CaptureChecked)) + && !isCaptureChecking || tycon.typeSymbol.is(CaptureChecked) // Don't check bounds when capture checking type constructors that were not // themselves capture checked. Since the type constructor could not foresee // possible capture sets, it's better to be lenient for backwards compatibility. diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index a21a94aab271..779485936b3b 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -14,6 +14,7 @@ import Implicits.{hasExtMethod, Candidate} import java.util.{Timer, TimerTask} import collection.mutable import scala.util.control.NonFatal +import cc.isCaptureChecking /** This trait defines the method `importSuggestionAddendum` that adds an addendum * to error messages suggesting additional imports. @@ -319,7 +320,7 @@ trait ImportSuggestions: * If there's nothing to suggest, an empty string is returned. */ override def importSuggestionAddendum(pt: Type)(using Context): String = - if ctx.phase == Phases.checkCapturesPhase then + if isCaptureChecking then return "" // it's too late then to look for implicits val (fullMatches, headMatches) = importSuggestions(pt)(using ctx.fresh.setExploreTyperState()) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 061d759e9ca4..4b418f779f95 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -20,7 +20,7 @@ import config.SourceVersion.{`3.0`, `future`} import config.Printers.refcheck import reporting._ import Constants.Constant -import cc.{mapRoots, localRoot} +import cc.{mapRoots, localRoot, isCaptureChecking, isLevelOwner} object RefChecks { import tpd._ @@ -105,7 +105,7 @@ object RefChecks { def checkSelfConforms(other: ClassSymbol) = var otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) - if ctx.phase == Phases.checkCapturesPhase then + if isCaptureChecking then otherSelf = mapRoots(other.localRoot.termRef, cls.localRoot.termRef)(otherSelf) .showing(i"map self $otherSelf = $result", capt) if otherSelf.exists then @@ -223,36 +223,39 @@ object RefChecks { false precedesIn(parent.asClass.baseClasses) - // We can exclude pairs safely from checking only under three additional conditions - // - their signatures also match in the parent class. - // See neg/i12828.scala for an example where this matters. - // - They overriding/overridden appear in linearization order. - // See neg/i5094.scala for an example where this matters. - // - The overridden symbol is not `abstract override`. For such symbols - // we need a more extensive test since the virtual super chain depends - // on the precise linearization order, which might be different for the - // subclass. See neg/i14415.scala. + /** We can exclude pairs safely from checking only under three additional conditions + * - their signatures also match in the parent class. + * See neg/i12828.scala for an example where this matters. + * - They overriding/overridden appear in linearization order. + * See neg/i5094.scala for an example where this matters. + * - The overridden symbol is not `abstract override`. For such symbols + * we need a more extensive test since the virtual super chain depends + * on the precise linearization order, which might be different for the + * subclass. See neg/i14415.scala. + */ override def canBeHandledByParent(sym1: Symbol, sym2: Symbol, parent: Symbol): Boolean = isOverridingPair(sym1, sym2, parent.thisType) .showing(i"already handled ${sym1.showLocated}: ${sym1.asSeenFrom(parent.thisType).signature}, ${sym2.showLocated}: ${sym2.asSeenFrom(parent.thisType).signature} = $result", refcheck) && inLinearizationOrder(sym1, sym2, parent) && !sym2.is(AbsOverride) - // Checks the subtype relationship tp1 <:< tp2. - // It is passed to the `checkOverride` operation in `checkAll`, to be used for - // compatibility checking. + /** 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 adjust the type of `member` and `other` before checking conformance. + /** A hook that allows to omit override checks between `overriding` and `overridden`. * Overridden in capture checking to handle non-capture checked classes leniently. */ - def adjustInfo(tp: Type, member: Symbol)(using Context): Type = tp + def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean = true private val subtypeChecker: (Type, Type) => Context ?=> Boolean = this.checkSubType def checkAll(checkOverride: ((Type, Type) => Context ?=> Boolean, Symbol, Symbol) => Unit) = while hasNext do - checkOverride(subtypeChecker, overriding, overridden) + if needsCheck(overriding, overridden) then + checkOverride(subtypeChecker, overriding, overridden) next() // The OverridingPairs cursor does assume that concrete overrides abstract @@ -266,7 +269,7 @@ object RefChecks { if dcl.is(Deferred) then for other <- dcl.allOverriddenSymbols do if !other.is(Deferred) then - checkOverride(checkSubType, dcl, other) + checkOverride(subtypeChecker, dcl, other) end checkAll end OverridingPairsChecker @@ -371,8 +374,12 @@ object RefChecks { def checkOverride(checkSubType: (Type, Type) => Context ?=> Boolean, member: Symbol, other: Symbol): Unit = def memberTp(self: Type) = if (member.isClass) TypeAlias(member.typeRef.EtaExpand(member.typeParams)) - else checker.adjustInfo(self.memberInfo(member), member) - def otherTp(self: Type) = checker.adjustInfo(self.memberInfo(other), other) + else self.memberInfo(member) + def otherTp(self: Type) = + val info = self.memberInfo(other) + if isCaptureChecking && member.isLevelOwner && other.isLevelOwner + then info.substSym(other.localRoot :: Nil, member.localRoot :: Nil) + else info refcheck.println(i"check override ${infoString(member)} overriding ${infoString(other)}") diff --git a/tests/neg-custom-args/captures/box-adapt-boxing.scala b/tests/neg-custom-args/captures/box-adapt-boxing.scala index 4b631472bad4..b6eb59aef593 100644 --- a/tests/neg-custom-args/captures/box-adapt-boxing.scala +++ b/tests/neg-custom-args/captures/box-adapt-boxing.scala @@ -17,7 +17,7 @@ def main(io: Cap^, fs: Cap^): Unit = { type Op0[X] = Box[X] -> Unit type Op1[X] = Unit -> Box[X] val f: Unit -> (Cap^{io}) -> Unit = ??? - val test: Op1[Op0[Cap^{io}]^{io}]^{} = f + val test: Op1[Op0[Cap^{io}]^{io}]^{} = f // error ??? not sure this is correct // expected: {} Unit -> box {io} (box {io} Cap) -> Unit // actual: Unit -> ({io} Cap) -> Unit // @@ -33,6 +33,6 @@ def main(io: Cap^, fs: Cap^): Unit = { type Op[X] = Unit -> Box[X] val f: Unit -> (Cap^{io}) -> Unit = ??? val g: Op[Id[Cap^{io}]^{fs}] = f // error - val h: Op[Id[Cap^{io}]^{io}] = f + val h: Op[Id[Cap^{io}]^{io}] = f // error ??? not sure this is correct } } diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 61b83fc24688..781b26b6944d 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -8,5 +8,5 @@ -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- 19 | h(g()) // error | ^^^ - | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap^{cap[test2]}) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ?->{cap1} I diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 124bd61a0109..8e56f9515dbf 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,12 +1,12 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:4:11 -------------------------------------------------------------- 4 | () => if x == null then y else y // error | ^ - | (x : C^) cannot be referenced here; it is not included in the allowed capture set {} + | (x : C^{cap[f]}) cannot be referenced here; it is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> C -- Error: tests/neg-custom-args/captures/capt1.scala:7:11 -------------------------------------------------------------- 7 | () => if x == null then y else y // error | ^ - | (x : C^) cannot be referenced here; it is not included in the allowed capture set {} + | (x : C^{cap[g]}) cannot be referenced here; it is not included in the allowed capture set {} | of an enclosing function literal with expected type Matchable -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:14:2 ----------------------------------------- 14 | def f(y: Int) = if x == null then y else y // error @@ -36,5 +36,5 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:32:30 ------------------------------------------------------------- 32 | val z2 = h[() -> Cap](() => x) // error | ^ - | (x : C^) cannot be referenced here; it is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> box C^ + | (x : C^{cap[]}) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> box C^{cap[]} diff --git a/tests/neg-custom-args/captures/cc-this.check b/tests/neg-custom-args/captures/cc-this.check index 335302c5c259..798dd81bf6e1 100644 --- a/tests/neg-custom-args/captures/cc-this.check +++ b/tests/neg-custom-args/captures/cc-this.check @@ -8,8 +8,8 @@ -- Error: tests/neg-custom-args/captures/cc-this.scala:10:15 ----------------------------------------------------------- 10 | class C2(val x: () => Int): // error | ^ - | reference (C2.this.x : () => Int) is not included in the allowed capture set {} of the self type of class C2 + |reference (C2.this.x : () ->{cap[C2]} Int) is not included in the allowed capture set {} of the self type of class C2 -- Error: tests/neg-custom-args/captures/cc-this.scala:17:8 ------------------------------------------------------------ 17 | class C4(val f: () => Int) extends C3 // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | reference (C4.this.f : () => Int) is not included in the allowed capture set {} of pure base class class C3 + |reference (C4.this.f : () ->{cap[C4]} Int) is not included in the allowed capture set {} of pure base class class C3 diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check index fbf7d5f4d831..14c4c76370a6 100644 --- a/tests/neg-custom-args/captures/cc-this2.check +++ b/tests/neg-custom-args/captures/cc-this2.check @@ -2,5 +2,5 @@ -- Error: tests/neg-custom-args/captures/cc-this2/D_2.scala:2:6 -------------------------------------------------------- 2 |class D extends C: // error |^ - |reference (cap : caps.Cap) is not included in the allowed capture set {} of pure base class class C + |reference (cap[D] : caps.Cap) is not included in the allowed capture set {} of pure base class class C 3 | this: D^ => diff --git a/tests/neg-custom-args/captures/cc-this3.check b/tests/neg-custom-args/captures/cc-this3.check index d57471c6872e..2ab46bb677a0 100644 --- a/tests/neg-custom-args/captures/cc-this3.check +++ b/tests/neg-custom-args/captures/cc-this3.check @@ -1,7 +1,7 @@ -- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this3.scala:8:6 --------------------------------------- 8 |class B extends A: // error | ^ - | illegal inheritance: self type B^ of class B does not conform to self type A^{} + | illegal inheritance: self type B^{cap[B]} of class B does not conform to self type A^{} | of parent class A | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check index 382ee177aa91..b7103511cb3a 100644 --- a/tests/neg-custom-args/captures/eta.check +++ b/tests/neg-custom-args/captures/eta.check @@ -8,5 +8,5 @@ -- Error: tests/neg-custom-args/captures/eta.scala:6:20 ---------------------------------------------------------------- 6 | bar( () => f ) // error | ^ - | (f : () => Unit) cannot be referenced here; it is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> box () ->? Unit + | (f : () ->{cap[foo]} Unit) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> box () ->? Unit diff --git a/tests/neg-custom-args/captures/exception-definitions.check b/tests/neg-custom-args/captures/exception-definitions.check index 4533cf2c083d..6b676bc59539 100644 --- a/tests/neg-custom-args/captures/exception-definitions.check +++ b/tests/neg-custom-args/captures/exception-definitions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/exception-definitions.scala:2:6 ----------------------------------------------- 2 |class Err extends Exception: // error |^ - |reference (cap : caps.Cap) is not included in the allowed capture set {} of pure base class class Throwable + |reference (cap[Err] : caps.Cap) is not included in the allowed capture set {} of pure base class class Throwable 3 | self: Err^ => -- Error: tests/neg-custom-args/captures/exception-definitions.scala:7:12 ---------------------------------------------- 7 | val x = c // error @@ -10,4 +10,4 @@ -- Error: tests/neg-custom-args/captures/exception-definitions.scala:8:8 ----------------------------------------------- 8 | class Err3(c: Any^) extends Exception // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | reference (Err3.this.c : Any^) is not included in the allowed capture set {} of pure base class class Throwable + |reference (Err3.this.c : Any^{cap[Err3]}) is not included in the allowed capture set {} of pure base class class Throwable diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index f72325a0be8a..d362f9681d6e 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -11,7 +11,7 @@ def main(io: Cap^{cap}, net: Cap^{cap}): Unit = { } val test2: (c: Cap^{cap}) -> () ->{cap} Unit = - localCap { c => // error, was: should work + localCap { c => // should work (was error) (c1: Cap^{cap}) => () => { c1.use() } } diff --git a/tests/neg-custom-args/captures/i15116.check b/tests/neg-custom-args/captures/i15116.check index 765477df7466..3a41fdae73ec 100644 --- a/tests/neg-custom-args/captures/i15116.check +++ b/tests/neg-custom-args/captures/i15116.check @@ -1,28 +1,60 @@ --- Error: tests/neg-custom-args/captures/i15116.scala:3:6 -------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:3:13 ---------------------------------------- 3 | val x = Foo(m) // error - | ^^^^^^^^^^^^^^ - | Non-local value x cannot have an inferred type - | Foo{val m: String^{Bar.this.m}}^{Bar.this.m} - | with non-empty capture set {Bar.this.m}. - | The type needs to be declared explicitly. --- Error: tests/neg-custom-args/captures/i15116.scala:5:6 -------------------------------------------------------------- + | ^^^^^^ + | Found: Foo{val m²: (Bar.this.m : String^{cap[Bar]})}^{Bar.this.m} + | Required: Foo + | + | where: m is a value in class Bar + | m² is a value in class Foo + | + | + | Note that the expected type Foo + | is the previously inferred type of value x + | which is also the type seen in separately compiled sources. + | The new inferred type Foo{val m: (Bar.this.m : String^{cap[Bar]})}^{Bar.this.m} + | must conform to this type. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:5:13 ---------------------------------------- 5 | val x = Foo(m) // error - | ^^^^^^^^^^^^^^ - | Non-local value x cannot have an inferred type - | Foo{val m: String^{Baz.this}}^{Baz.this} - | with non-empty capture set {Baz.this}. - | The type needs to be declared explicitly. --- Error: tests/neg-custom-args/captures/i15116.scala:7:6 -------------------------------------------------------------- + | ^^^^^^ + | Found: Foo{val m: String^{Baz.this}}^{Baz.this} + | Required: Foo + | + | Note that the expected type Foo + | is the previously inferred type of value x + | which is also the type seen in separately compiled sources. + | The new inferred type Foo{val m: String^{Baz.this}}^{Baz.this} + | must conform to this type. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:7:13 ---------------------------------------- 7 | val x = Foo(m) // error - | ^^^^^^^^^^^^^^ - | Non-local value x cannot have an inferred type - | Foo{val m: String^{Bar1.this.m}}^{Bar1.this.m} - | with non-empty capture set {Bar1.this.m}. - | The type needs to be declared explicitly. --- Error: tests/neg-custom-args/captures/i15116.scala:9:6 -------------------------------------------------------------- + | ^^^^^^ + | Found: Foo{val m²: (Bar1.this.m : String^{cap[Bar1]})}^{Bar1.this.m} + | Required: Foo + | + | where: m is a value in class Bar1 + | m² is a value in class Foo + | + | + | Note that the expected type Foo + | is the previously inferred type of value x + | which is also the type seen in separately compiled sources. + | The new inferred type Foo{val m: (Bar1.this.m : String^{cap[Bar1]})}^{Bar1.this.m} + | must conform to this type. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15116.scala:9:13 ---------------------------------------- 9 | val x = Foo(m) // error - | ^^^^^^^^^^^^^^ - | Non-local value x cannot have an inferred type - | Foo{val m: String^{Baz2.this}}^{Baz2.this} - | with non-empty capture set {Baz2.this}. - | The type needs to be declared explicitly. + | ^^^^^^ + | Found: Foo{val m: String^{Baz2.this}}^{Baz2.this} + | Required: Foo + | + | Note that the expected type Foo + | is the previously inferred type of value x + | which is also the type seen in separately compiled sources. + | The new inferred type Foo{val m: String^{Baz2.this}}^{Baz2.this} + | must conform to this type. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index cb6b40361add..e6c2dff87dc7 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -1,45 +1,32 @@ -- Error: tests/neg-custom-args/captures/i15772.scala:19:26 ------------------------------------------------------------ 19 | val c : C^{x} = new C(x) // error | ^ - | (x : C^) cannot be referenced here; it is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> Int + | (x : C^{cap[main1]}) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> Int -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:20:46 --------------------------------------- 20 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error | ^^^^^^^ - | Found: (C{val arg: C^{cap[C]}}^{c} ->{'cap[..main1](from instantiating box1), c} Unit) ->{c} Unit - | Required: (C^ => Unit) -> Unit + | Found: (C{val arg: C^{cap[main1]}}^{c} ->{'cap[main1..boxed1](from instantiating box1), c} Unit) ->{c} Unit + | Required: (C^{cap[boxed1]} ->{cap[boxed1]} Unit) -> Unit | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/i15772.scala:26:26 ------------------------------------------------------------ 26 | val c : C^{x} = new C(x) // error | ^ - | (x : C^) cannot be referenced here; it is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> Int + | (x : C^{cap[main2]}) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> Int -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:27:35 --------------------------------------- 27 | val boxed2 : Observe[C^] = box2(c) // error | ^^^^^^^ - | Found: (C{val arg: C^{cap[C]}}^{c} ->{'cap[..main2](from instantiating box2), c} Unit) ->{c} Unit - | Required: (C^ => Unit) -> Unit + | Found: (C{val arg: C^{cap[main2]}}^{c} ->{'cap[main2..boxed2](from instantiating box2), c} Unit) ->{c} Unit + | Required: (C^{cap[boxed2]} ->{cap[boxed2]} Unit) -> Unit | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:34 --------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:33 --------------------------------------- 33 | val boxed2 : Observe[C]^ = box2(c) // error - | ^ - | Found: box C^{cap[c]} - | Required: box C{val arg: C^?}^? - | - | Note that reference (cap[c] : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method main3 - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:34:29 --------------------------------------- -34 | boxed2((cap: C^) => unsafe(c)) // error - | ^ - | Found: C^{cap[c]} - | Required: C^{'cap[..main3](from instantiating unsafe)} - | - | Note that reference (cap[c] : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method main3 + | ^^^^^^^ + | Found: (C{val arg: C^{cap[main3]}}^{cap[main3]} ->{cap[main3]} Unit) ->{cap[main3]} Unit + | Required: (C ->{cap[boxed2]} Unit) ->{cap[boxed2]} Unit | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- @@ -48,7 +35,4 @@ | Found: () ->{x} Unit | Required: () -> Unit | - | Note that reference (cap[c] : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method main3 - | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index 7e62fdeffb24..a054eac835c1 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -31,7 +31,7 @@ def main2(x: C^) : () -> Int = def main3(x: C^) = def c : C^ = new C(x) val boxed2 : Observe[C]^ = box2(c) // error - boxed2((cap: C^) => unsafe(c)) // error + boxed2((cap: C^) => unsafe(c)) 0 trait File: diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index aeb410f07d65..29a667c038c5 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -8,8 +8,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:35:29 ------------------------------------- 35 | val ref1c: LazyList[Int] = ref1 // error | ^^^^ - | Found: (ref1 : lazylists.LazyCons[Int]{val xs: () ->{cap1} lazylists.LazyList[Int]^{cap[LazyCons]}}^{cap1}) - | Required: lazylists.LazyList[Int] + | Found: lazylists.LazyCons[Int]{val xs: () ->{cap1} lazylists.LazyList[Int]^?}^{ref1} + | Required: lazylists.LazyList[Int] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:37:36 ------------------------------------- @@ -37,6 +37,6 @@ 22 | def tail: LazyList[Nothing]^ = ??? // error overriding | ^ | error overriding method tail in class LazyList of type -> lazylists.LazyList[Nothing]; - | method tail of type -> lazylists.LazyList[Nothing]^{cap[tail]} has incompatible type + | method tail of type -> lazylists.LazyList[Nothing]^{cap[]} has incompatible type | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 74318b6bb254..702f8e6260b9 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,11 +1,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:37:4 -------------------------- 37 | tabulate(10) { i => // error | ^ - | Found: LazyList[Int]^ + | Found: LazyList[Int]^{cap[]} | Required: LazyList[Int]^? - | - | Note that reference (cap : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method problem 38 | if i > 9 then throw Ex1() 39 | i * i 40 | } diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 5038ab1bea93..70a94edcd31a 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ 18 | final class Mapped extends LazyList[B]: // error | ^ - | Found: LazyList[B^?]^{f, xs} + | Found: LazyList[box B^?]^{f, xs} | Required: LazyList[B]^{f} 19 | this: (Mapped^{xs, f}) => 20 | def isEmpty = false @@ -13,7 +13,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------ 27 | final class Mapped extends LazyList[B]: // error | ^ - | Found: LazyList[B^?]^{f, xs} + | Found: LazyList[box B^?]^{f, xs} | Required: LazyList[B]^{xs} 28 | this: Mapped^{xs, f} => 29 | def isEmpty = false @@ -33,7 +33,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:45:4 ------------------------------------ 45 | final class Mapped extends LazyList[B]: // error | ^ - | Found: LazyList[B^?]^{f, xs} + | Found: LazyList[box B^?]^{f, xs} | Required: LazyList[B]^{xs} 46 | this: (Mapped^{xs, f}) => 47 | def isEmpty = false diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 8c91ec13b5d8..c1ecdb141bb3 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -1,28 +1,28 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:19:28 -------------------------------------- 19 | val ref1c: LazyRef[Int] = ref1 // error | ^^^^ - | Found: (ref1 : LazyRef[Int]{val elem: () ->{cap1} Int}^{cap1}) + | Found: LazyRef[Int]{val elem: () ->{cap1} Int}^{ref1} | Required: LazyRef[Int] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- 21 | val ref2c: LazyRef[Int]^{cap2} = ref2 // error | ^^^^ - | Found: (ref2 : LazyRef[Int]{val elem: () => Int}^{cap2, ref1}) + | Found: LazyRef[Int]{val elem: () ->{cap[test]} Int}^{ref2} | Required: LazyRef[Int]^{cap2} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- 23 | val ref3c: LazyRef[Int]^{ref1} = ref3 // error | ^^^^ - | Found: (ref3 : LazyRef[Int]{val elem: () => Int}^{cap2, ref1}) + | Found: LazyRef[Int]{val elem: () ->{cap[test]} Int}^{ref3} | Required: LazyRef[Int]^{ref1} | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- 25 | val ref4c: LazyRef[Int]^{cap1} = ref4 // error | ^^^^ - | Found: (ref4 : LazyRef[Int]{val elem: () => Int}^{cap2, cap1}) + | Found: LazyRef[Int]{val elem: () ->{cap[test]} Int}^{ref4} | Required: LazyRef[Int]^{cap1} | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/leaked-curried.check b/tests/neg-custom-args/captures/leaked-curried.check index 509069797def..a13c15c7c7fd 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -1,11 +1,4 @@ -- Error: tests/neg-custom-args/captures/leaked-curried.scala:13:20 ---------------------------------------------------- 13 | () => () => io // error | ^^ - |(io : Cap^{cap[main]}) cannot be referenced here; it is not included in the allowed capture set {} of pure base class trait Pure --- [E164] Declaration Error: tests/neg-custom-args/captures/leaked-curried.scala:12:10 --------------------------------- -12 | val get: () ->{} () ->{io} Cap^ = // error - | ^ - | error overriding value get in trait Box of type () -> () ->{cap[Box]} Cap^{cap[Box]}; - | value get of type () -> () ->{io} Cap^ has incompatible type - | - | longer explanation available when compiling with `-explain` + |(io : Cap^{cap[]}) cannot be referenced here; it is not included in the allowed capture set {} of pure base class trait Pure diff --git a/tests/neg-custom-args/captures/leaked-curried.scala b/tests/neg-custom-args/captures/leaked-curried.scala index a566999d7c39..30ec4f7dbab5 100644 --- a/tests/neg-custom-args/captures/leaked-curried.scala +++ b/tests/neg-custom-args/captures/leaked-curried.scala @@ -9,7 +9,7 @@ trait Box: def main(): Unit = val leaked = withCap: (io: Cap^) => class Foo extends Box, Pure: - val get: () ->{} () ->{io} Cap^ = // error + val get: () ->{} () ->{io} Cap^ = () => () => io // error new Foo val bad = leaked.get()().use() // using a leaked capability diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index 8d11c196b10f..6ace4e09dfcc 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -1,10 +1,10 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/levels.scala:15:11 --------------------------------------- -15 | r.setV(g) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/levels.scala:14:11 --------------------------------------- +14 | r.setV(g) // error | ^ | Found: box (x: String) ->{cap3} String | Required: box (x$0: String) ->? String | - | Note that reference (cap3 : CC^), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method test + | Note that reference (cap3 : CC^{cap[scope]}), defined in method scope + | cannot be included in outer capture set ? which is associated with method test | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/levels.scala b/tests/neg-custom-args/captures/levels.scala index 35fb2d490398..47ad8e3c50db 100644 --- a/tests/neg-custom-args/captures/levels.scala +++ b/tests/neg-custom-args/captures/levels.scala @@ -9,8 +9,7 @@ def test(cap1: CC^) = val r = Ref((x: String) => x) - def scope = - val cap3: CC^ = ??? + def scope(cap3: CC^) = def g(x: String): String = if cap3 == cap3 then "" else "a" r.setV(g) // error () diff --git a/tests/neg-custom-args/captures/localcaps.scala b/tests/neg-custom-args/captures/localcaps.scala index 50cbe8e0f8f9..5e69a8a48f1f 100644 --- a/tests/neg-custom-args/captures/localcaps.scala +++ b/tests/neg-custom-args/captures/localcaps.scala @@ -1,7 +1,9 @@ class C: + this: C^ => + def x: C^{cap[d]} = ??? // error def y: C^{cap[C]} = ??? // ok - private val z = (x: Int) => (c: C^{cap[z]}) => x // ok + private val z = (c0: caps.Cap) => (x: Int) => (c: C^{cap[z]}) => x // ok private val z2 = identity((x: Int) => (c: C^{cap[z2]}) => x) // error diff --git a/tests/neg-custom-args/captures/nesting-inversion.scala b/tests/neg-custom-args/captures/nesting-inversion.scala new file mode 100644 index 000000000000..0460f1243cca --- /dev/null +++ b/tests/neg-custom-args/captures/nesting-inversion.scala @@ -0,0 +1,43 @@ +def f(x: (() => Unit)): (() => Unit) => (() => Unit) = + def g(y: (() => Unit)): (() => Unit) = x + g + +def test1(x: (() => Unit)): Unit = + def test2(y: (() => Unit)) = + val a: (() => Unit) => (() => Unit) = f(y) + a(x) // OK, but should be error + test2(() => ()) + +def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = + class C1(x1: (() => Unit), xx2: (() => Unit) => Unit): + def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^{cap} = C2(y1, y2) + class C2(y1: (() => Unit), y2: (() => Unit) => Unit): + val a: (() => Unit) => (() => Unit) = f(y1) + a(x1) //OK, but should be error + C2(() => (), x => ()) + + def test3(y1: (() => Unit), y2: (() => Unit) => Unit) = + val cc1 = C1(y1, y2) + val cc2 = cc1.c2(x1, x2) + val cc3: cc1.C2^{cap[test2]} = cc2 // error + +def test4(x1: () => Unit) = + class C1: + this: C1^ => + class C2(z: () => Unit): + this: C2^ => + val foo: () => Unit = ??? + + def test5(x2: () => Unit) = + val xx1: C1^{cap[test5]} = C1() + val y1 = + val xx2 = xx1.C2(x1) + val xx3: xx1.C2^{cap[test4]} = xx2 // ok, but dubious + // actual capture set is in test4 + // but level constraints would determine that the root should be in test5 + // only, there is no root in the set to be mapped + xx2 + val f1 = y1.foo + val xx4 = xx1.C2(x2) + val xx5: xx1.C2^{cap[test4]} = xx4 // error + diff --git a/tests/neg-custom-args/captures/pairs.check b/tests/neg-custom-args/captures/pairs.check new file mode 100644 index 000000000000..6054d94c278e --- /dev/null +++ b/tests/neg-custom-args/captures/pairs.check @@ -0,0 +1,28 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/pairs.scala:15:31 ---------------------------------------- +15 | val x1c: Cap^ ->{c} Unit = x1 // error + | ^^ + | Found: (x$0: Cap^{cap[test]}) ->{x1} Unit + | Required: Cap^{cap[x1c]} ->{c} Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/pairs.scala:17:30 ---------------------------------------- +17 | val y1c: Cap ->{d} Unit = y1 // error + | ^^ + | Found: (x$0: Cap^{cap[test]}) ->{y1} Unit + | Required: Cap^{cap[y1c]} ->{d} Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/pairs.scala:34:30 ---------------------------------------- +34 | val x1c: Cap ->{c} Unit = x1 // error + | ^^ + | Found: (x$0: Cap^{cap[test]}) ->{x1} Unit + | Required: Cap^{cap[x1c]} ->{c} Unit + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/pairs.scala:36:30 ---------------------------------------- +36 | val y1c: Cap ->{d} Unit = y1 // error + | ^^ + | Found: (x$0: Cap^{cap[test]}) ->{y1} Unit + | Required: Cap^{cap[y1c]} ->{d} Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/pairs.scala b/tests/neg-custom-args/captures/pairs.scala new file mode 100644 index 000000000000..c78772eb7438 --- /dev/null +++ b/tests/neg-custom-args/captures/pairs.scala @@ -0,0 +1,37 @@ +@annotation.capability class Cap + +object Monomorphic: + + class Pair(x: Cap => Unit, y: Cap => Unit): + type PCap = Cap + def fst: PCap ->{x} Unit = x + def snd: PCap ->{y} Unit = y + + def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val p = Pair(f, g) + val x1 = p.fst + val x1c: Cap^ ->{c} Unit = x1 // error + val y1 = p.snd + val y1c: Cap ->{d} Unit = y1 // error + +object Monomorphic2: + + class Pair(x: Cap => Unit, y: Cap => Unit): + def fst: Cap^{cap[Pair]} ->{x} Unit = x + def snd: Cap^{cap[Pair]} ->{y} Unit = y + + class Pair2(x: Cap => Unit, y: Cap => Unit): + def fst: Cap^{cap[Pair2]} => Unit = x + def snd: Cap^{cap[Pair2]} => Unit = y + + def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val p = Pair(f, g) + val x1 = p.fst + val x1c: Cap ->{c} Unit = x1 // error + val y1 = p.snd + val y1c: Cap ->{d} Unit = y1 // error + diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index f57aef60745b..12a4cbd62c63 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -7,30 +7,22 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/real-try.scala:19:4 -------------------------------------- 19 | () => foo(1) // error | ^^^^^^^^^^^^ - | Found: () => Unit + | Found: () ->{cap[]} Unit | Required: () ->? Unit | - | Note that reference (cap : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method test - | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/real-try.scala:25:4 -------------------------------------- 25 | () => Cell(foo(1)) // error | ^^^^^^^^^^^^^^^^^^ - | Found: () => Cell[Unit]^? + | Found: () ->{cap[]} Cell[Unit]^? | Required: () ->? Cell[Unit]^? | - | Note that reference (cap : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method test - | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/real-try.scala:31:4 -------------------------------------- +-- Error: tests/neg-custom-args/captures/real-try.scala:31:21 ---------------------------------------------------------- 31 | Cell(() => foo(1))// // error - | ^^^^^^^^^^^^^^^^^^ - | Found: Cell[box () => Unit]^? - | Required: Cell[() ->? Unit]^? + | ^ + |(canThrow$4 : CanThrow[Ex1 | Ex2]^{cap[]}) cannot be referenced here; it is not included in the allowed capture set ? + |of an enclosing function literal with expected type box () ->? Unit | - | Note that reference (cap : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method test - | - | longer explanation available when compiling with `-explain` + |Note that reference (canThrow$4 : CanThrow[Ex1 | Ex2]^{cap[]}), defined in method + |cannot be included in outer capture set ? which is associated with package diff --git a/tests/neg-custom-args/captures/refs.scala b/tests/neg-custom-args/captures/refs.scala index b64b27ef4af0..b20e8029b65e 100644 --- a/tests/neg-custom-args/captures/refs.scala +++ b/tests/neg-custom-args/captures/refs.scala @@ -18,9 +18,10 @@ def usingLogFile[T](op: (local: caps.Cap) ?-> FileOutputStream^{local} => T): T result def test1 = - usingLogFile[Proc]: (local: caps.Cap) ?=> // error (but with a hard to parse error message) + usingLogFile[Proc]: (local: caps.Cap) ?=> (f: FileOutputStream^{local}) => - () => f.write(1) // this line has type () ->{local} Unit, but usingLogFile + () => f.write(1) // error (but with a hard to parse error message) + // this line has type () ->{local} Unit, but usingLogFile // requires Proc, which expands to () -> 'cap[..test1](from instantiating usingLogFile) def test2 = diff --git a/tests/neg-custom-args/captures/simple-escapes.check b/tests/neg-custom-args/captures/simple-escapes.check index 611d4f0f0dc3..3171f6f72612 100644 --- a/tests/neg-custom-args/captures/simple-escapes.check +++ b/tests/neg-custom-args/captures/simple-escapes.check @@ -2,7 +2,7 @@ 16 | foo = f // error | ^ | Found: box FileOutputStream^{f} - | Required: box FileOutputStream^{cap[Test1]} + | Required: box FileOutputStream^{cap[]} | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/simple-escapes.scala:19:15 ---------------------------------------------------- diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 5994e3901179..9bea04c527ed 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,10 +1,8 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:23:49 ------------------------------------------ -23 | val a = handle[Exception, CanThrow[Exception]] { // error - | ^ - |Found: (lcap: caps.Cap) ?->? (x$0: CT[Exception]^{lcap}) ->? box CT[Exception]^{lcap} - |Required: (lcap: caps.Cap) ?-> CT[Exception]^{lcap} ->{'cap[..test](from instantiating handle)} box CT[Exception]^ -24 | (x: CanThrow[Exception]) => x -25 | }{ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:24:32 ------------------------------------------ +24 | (x: CanThrow[Exception]) => x // error + | ^ + | Found: (x : CT[Exception]^{lcap}) + | Required: CT[Exception]^? | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- @@ -18,7 +16,7 @@ 49 | () => 50 | raise(new Exception)(using x) 51 | 22 -52 |} { // error +52 |} { // error | ^ | Found: () ->{x$0, lcap} Int | Required: () -> Int diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index fe58145bca54..71db5d8ed7b1 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -20,8 +20,8 @@ def handle[E <: Exception, R <: Top](op: (lcap: caps.Cap) ?-> CT[E]^{lcap} => R) catch case ex: E => handler(ex) def test = - val a = handle[Exception, CanThrow[Exception]] { // error - (x: CanThrow[Exception]) => x + val a = handle[Exception, CanThrow[Exception]] { + (x: CanThrow[Exception]) => x // error }{ (ex: Exception) => ??? } @@ -49,6 +49,6 @@ val global: () -> Int = handle { () => raise(new Exception)(using x) 22 -} { // error +} { // error (ex: Exception) => () => 22 } diff --git a/tests/neg-custom-args/captures/usingLogFile-alt.check b/tests/neg-custom-args/captures/usingLogFile-alt.check index 93fc3ca5edb5..35276914f7b2 100644 --- a/tests/neg-custom-args/captures/usingLogFile-alt.check +++ b/tests/neg-custom-args/captures/usingLogFile-alt.check @@ -3,8 +3,8 @@ | ^^^^^^^^^ | reference (file : java.io.OutputStream^{lcap}) is not included in the allowed capture set {x$0, x$0²} | - | Note that reference (file : java.io.OutputStream^{lcap}), defined at level 1 - | cannot be included in outer capture set {x$0, x$0}, defined at level 0 in package + | Note that reference (file : java.io.OutputStream^{lcap}), defined in method $anonfun + | cannot be included in outer capture set {x$0, x$0} which is associated with package | | where: x$0 is a reference to a value parameter | x$0² is a reference to a value parameter diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index 9550b5864586..433172663640 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -7,7 +7,7 @@ 36 | usingLogFile { f => later4 = Cell(() => f.write(0)) } // error | ^^^^^^^^^^^^^^^^^^^^^^ | Found: Test2.Cell[box () ->{f} Unit]^? - | Required: Test2.Cell[() ->{cap[]} Unit] + | Required: Test2.Cell[box () ->{cap[]} Unit] | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ @@ -26,3 +26,10 @@ 62 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error | ^^^^^^^^^ | escaping local reference local.type +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:70:16 ------------------------------------------------------ +70 | val later = usingFile("logfile", // error !!! but should be ok, since we can widen `l` to `file` instead of to `cap` + | ^^^^^^^^^ + | reference (_$1 : java.io.OutputStream^{local}) is not included in the allowed capture set {x$0, local} + | + | Note that reference (_$1 : java.io.OutputStream^{local}), defined in method $anonfun + | cannot be included in outer capture set {x$0, local} which is associated with package diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index b87b81d0eda8..a983231ff51a 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -67,6 +67,6 @@ object Test4: op(logger) def test = - val later = usingFile("logfile", - usingLogger(_, l => () => l.log("test"))) // ok, since we can widen `l` to `file` instead of to `cap` + val later = usingFile("logfile", // error !!! but should be ok, since we can widen `l` to `file` instead of to `cap` + usingLogger(_, l => () => l.log("test"))) later() diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index f8abe22e9d53..7447d65ef026 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -5,46 +5,26 @@ | Required: () -> Unit | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/vars.scala:23:14 -------------------------------------------------------------- -23 | a = x => g(x) // error +-- Error: tests/neg-custom-args/captures/vars.scala:22:14 -------------------------------------------------------------- +22 | a = x => g(x) // error | ^^^^ - | reference (cap3 : CC^) is not included in the allowed capture set {cap[test]} + | reference (cap3 : CC^{cap[scope]}) is not included in the allowed capture set {cap[test]} | of an enclosing function literal with expected type box String ->{cap[test]} String --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:24:8 ------------------------------------------ -24 | a = g // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:23:8 ------------------------------------------ +23 | a = g // error | ^ | Found: box (x: String) ->{cap3} String | Required: box (x$0: String) ->{cap[test]} String | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:26:12 ----------------------------------------- -26 | b = List(g) // error - | ^^^^^^^ - | Found: List[box (x$0: String) ->{cap3} String] - | Required: List[String ->{cap[test]} String] +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:13 ----------------------------------------- +25 | b = List(g) // error + | ^ + | Found: Seq[(x: String) ->{cap3} String] + | Required: Seq[box (x$0: String) ->{cap3} String] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:30:10 ----------------------------------------- -30 | val s = scope // error (but should be OK, we need to allow poly-captures) - | ^^^^^ - | Found: (x$0: String) ->{cap[scope]} String - | Required: (x$0: String) ->? String - | - | Note that reference (cap[scope] : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method test - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:31:29 ----------------------------------------- -31 | val sc: String => String = scope // error (but should also be OK) - | ^^^^^ - | Found: (x$0: String) ->{cap[scope]} String - | Required: String => String - | - | Note that reference (cap[scope] : caps.Cap), defined at level 2 - | cannot be included in outer capture set ?, defined at level 1 in method test - | - | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/vars.scala:35:2 --------------------------------------------------------------- -35 | local { root => cap3 => // error +-- Error: tests/neg-custom-args/captures/vars.scala:34:2 --------------------------------------------------------------- +34 | local { root => cap3 => // error | ^^^^^ | escaping local reference root.type diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index 9b280a42a2f2..8819a73d5bc8 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -16,8 +16,7 @@ def test(cap1: Cap, cap2: Cap) = a("") // was error, now ok b.head // was error, now ok - def scope = - val cap3: Cap = CC() + def scope(cap3: Cap) = def g(x: String): String = if cap3 == cap3 then "" else "a" def h(): String = "" a = x => g(x) // error @@ -27,8 +26,8 @@ def test(cap1: Cap, cap2: Cap) = val gc = g g - val s = scope // error (but should be OK, we need to allow poly-captures) - val sc: String => String = scope // error (but should also be OK) + val s = scope(new CC) + val sc: String => String = scope(new CC) def local[T](op: (local: caps.Cap) -> CC^{local} -> T): T = op(caps.cap)(CC()) diff --git a/tests/pos-custom-args/captures/colltest.scala b/tests/pos-custom-args/captures/colltest.scala index af4ae6f03c56..ab3ac437db9f 100644 --- a/tests/pos-custom-args/captures/colltest.scala +++ b/tests/pos-custom-args/captures/colltest.scala @@ -31,5 +31,4 @@ object CollectionStrawMan5 { val left: Partitioned^{self} = Partitioned(true) val right: Partitioned^{self} = Partitioned(false) } - } \ No newline at end of file diff --git a/tests/pos-custom-args/captures/concat-iterator.scala b/tests/pos-custom-args/captures/concat-iterator.scala new file mode 100644 index 000000000000..828b8b3b2657 --- /dev/null +++ b/tests/pos-custom-args/captures/concat-iterator.scala @@ -0,0 +1,17 @@ +package test + +trait IOnce[A]: + self: I[A]^ => + +trait I[+A]: + self: I[A]^ => + + def concat[B >: A](xs: => IOnce[B]^): I[B]^{this, xs} = new I.ConcatI[B](self).concat(xs) + +object I: + private final class ConcatI[+A](val from: I[A]^) extends I[A]: + override def concat[B >: A](that: => IOnce[B]^): I[B]^{this, that} = ??? + + + + diff --git a/tests/pos-custom-args/captures/function-combinators.scala b/tests/pos-custom-args/captures/function-combinators.scala index 4354af4c7636..7369f8399f2f 100644 --- a/tests/pos-custom-args/captures/function-combinators.scala +++ b/tests/pos-custom-args/captures/function-combinators.scala @@ -2,7 +2,7 @@ class ContextClass type Context = ContextClass^ def Test(using ctx1: Context, ctx2: Context) = - val f: Int => Int = identity + val f: Int ->{cap[Test]} Int = identity val g1: Int ->{ctx1} Int = identity val g2: Int ->{ctx2} Int = identity val h: Int -> Int = identity @@ -19,7 +19,7 @@ def Test(using ctx1: Context, ctx2: Context) = val c3 = h.andThen(g2); val _: Int ->{g2} Int = c3 val c4 = h.andThen(h); val _: Int -> Int = c4 - val f2: (Int, Int) => Int = _ + _ + val f2: (Int, Int) ->{cap[Test]} Int = _ + _ val f2c = f2.curried; val _: Int -> Int ->{f2} Int = f2c val f2t = f2.tupled; val _: ((Int, Int)) ->{f2} Int = f2t diff --git a/tests/pos-custom-args/captures/lists.scala b/tests/pos-custom-args/captures/lists.scala index 56473e68d49f..c3a604128942 100644 --- a/tests/pos-custom-args/captures/lists.scala +++ b/tests/pos-custom-args/captures/lists.scala @@ -30,7 +30,7 @@ def test(c: Cap, d: Cap, e: Cap) = CONS(z, ys) val zsc: LIST[Cap ->{d, y} Unit] = zs val z1 = zs.head - val z1c: Cap ->{y, d} Unit = z1 + val z1c: Cap^{cap[test]} ->{y, d} Unit = z1 val ys1 = zs.tail val y1 = ys1.head diff --git a/tests/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala new file mode 100644 index 000000000000..e9e74dedb7c1 --- /dev/null +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -0,0 +1,24 @@ + +def f(x: (() => Unit)): (() => Unit) => (() => Unit) = + def g(y: (() => Unit)): (() => Unit) = x + g + +def test1(x: (() => Unit)): Unit = + def test2(y: (() => Unit)) = + val a: (() => Unit) => (() => Unit) = f(y) + a(x) // OK, but should be error + test2(() => ()) + +def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = + class C1(x1: (() => Unit), xx2: (() => Unit) => Unit): + def c2(y1: (() => Unit), y2: (() => Unit) => Unit): C2^{cap} = C2(y1, y2) + class C2(y1: (() => Unit), y2: (() => Unit) => Unit): + val a: (() => Unit) => (() => Unit) = f(y1) + a(x1) //OK, but should be error + C2(() => (), x => ()) + + def test3(y1: (() => Unit), y2: (() => Unit) => Unit) = + val cc1/*: C1^{cap[test3]}*/ = C1(y1, y2) // error (but should be OK) + val cc2 = cc1.c2(x1, x2) // error (but should be OK) + //val cc3: cc1.C2^{cap[test2]} = cc2 + diff --git a/tests/pos-custom-args/captures/outer-roots.scala b/tests/pos-custom-args/captures/outer-roots.scala new file mode 100644 index 000000000000..75b27e89667c --- /dev/null +++ b/tests/pos-custom-args/captures/outer-roots.scala @@ -0,0 +1,26 @@ +class C +type Cap = C^ + +class A +class B + +class Foo(x: Cap): + + def foo: A ->{cap[Foo]} Unit = ??? + + class Bar(y: Cap): + + def bar: B ->{cap[Bar]} Unit = ??? + + def f(a: A ->{cap[Foo]} Unit, b: B ->{cap[Bar]} Unit) + : (A ->{a} Unit, B ->{b} Unit) + = (a, b) + +def test(c1: Cap, c2: Cap) = + val x = Foo(c1) + val y = x.Bar(c2) + val xfoo = x.foo + val ybar = y.bar + val z1 = y.f(xfoo, ybar) + val z2 = y.f(x.foo, y.bar) + () diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index 78991e4377c0..c8c169a1041c 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -1,4 +1,5 @@ - +//class CC +//type Cap = CC^ @annotation.capability class Cap object Generic: @@ -12,9 +13,9 @@ object Generic: def g(x: Cap): Unit = if d == x then () val p = Pair(f, g) val x1 = p.fst - val x1c: Cap ->{c} Unit = x1 + val x1c: Cap^{cap[test]} ->{c} Unit = x1 val y1 = p.snd - val y1c: Cap ->{d} Unit = y1 + val y1c: Cap^{cap[test]} ->{d} Unit = y1 object Monomorphic: @@ -28,9 +29,9 @@ object Monomorphic: def g(x: Cap): Unit = if d == x then () val p = Pair(f, g) val x1 = p.fst - val x1c: Cap ->{c} Unit = x1 + val x1c: Cap^{cap[test]} ->{c} Unit = x1 val y1 = p.snd - val y1c: Cap ->{d} Unit = y1 + val y1c: Cap^{cap[test]} ->{d} Unit = y1 object Monomorphic2: @@ -47,7 +48,6 @@ object Monomorphic2: def g(x: Cap): Unit = if d == x then () val p = Pair(f, g) val x1 = p.fst - val x1c: Cap ->{c} Unit = x1 + val x1c: Cap^{cap[test]} ->{c} Unit = x1 val y1 = p.snd - val y1c: Cap ->{d} Unit = y1 - + val y1c: Cap^{cap[test]} ->{d} Unit = y1 diff --git a/tests/pos-custom-args/captures/refs.scala b/tests/pos-custom-args/captures/refs.scala new file mode 100644 index 000000000000..372b93421c82 --- /dev/null +++ b/tests/pos-custom-args/captures/refs.scala @@ -0,0 +1,29 @@ +type Proc = () => Unit + +class MonoRef(init: Proc): + type MonoProc = Proc + var x: MonoProc = init + def getX: MonoProc = x + def setX(x: MonoProc): Unit = this.x = x + +def test(p: Proc) = + val x = MonoRef(p) + x.setX(p) + val y = x.getX + val yc1: Proc = y + val yc2: () ->{x} Unit = y + val yc3: () ->{cap[test]} Unit = y + +class MonoRef2(init: () => Unit): + var x: () ->{cap[MonoRef2]} Unit = init + def getX: () ->{cap[MonoRef2]} Unit = x + def setX(x: () ->{cap[MonoRef2]} Unit): Unit = this.x = x + +def test2(p: Proc) = + val x = MonoRef2(p) + x.setX(p) + val y = x.getX + val yc1: Proc = y + val yc2: () ->{x} Unit = y + val yc3: () ->{cap[test2]} Unit = y + diff --git a/tests/pos-custom-args/captures/test.scala b/tests/pos-custom-args/captures/test.scala index cf532bbdf34a..974e7673aa86 100644 --- a/tests/pos-custom-args/captures/test.scala +++ b/tests/pos-custom-args/captures/test.scala @@ -1,9 +1,16 @@ class C type Cap = C^ -class Foo(x: Cap): - this: Foo^{x} => +type Proc = () => Unit + +class Ref(p: () => Unit): + private var x: () => Unit = p + def set(x: () ->{cap[Ref]} Unit): Unit = this.x = x + def get: () => Unit = x + +def test(c: () => Unit) = + val p: () => Unit = ??? + val r = Ref(p) + val x = r.get + r.set(x) -def test(c: Cap) = - val x = Foo(c) - () diff --git a/tests/pos-custom-args/captures/vars.scala b/tests/pos-custom-args/captures/vars.scala index ccf2cd587eb1..a335be96fed1 100644 --- a/tests/pos-custom-args/captures/vars.scala +++ b/tests/pos-custom-args/captures/vars.scala @@ -14,4 +14,4 @@ def test(cap1: Cap, cap2: Cap) = val r = Ref() r.elem = f - val fc: String ->{cap1} String = r.elem + val fc: String ->{cap1} String = r.elem \ No newline at end of file diff --git a/tests/pos-special/stdlib/Test1.scala b/tests/pos-special/stdlib/Test1.scala index 786e3aaa2bf1..d5f26060651c 100644 --- a/tests/pos-special/stdlib/Test1.scala +++ b/tests/pos-special/stdlib/Test1.scala @@ -20,7 +20,7 @@ object Test0: object Test1: def test(it: Iterator[Int]^, v: View[Int]^) = - val isEven: Int => Boolean = _ % 2 == 0 + val isEven: Int ->{cap[test]} Boolean = _ % 2 == 0 val it2 = it.filter(isEven) val _: Iterator[Int]^{it, isEven} = it2 val it2c: Iterator[Int]^{it2} = it2 diff --git a/tests/pos-special/stdlib/Test2.scala b/tests/pos-special/stdlib/Test2.scala index cab9440c17db..a59da522b183 100644 --- a/tests/pos-special/stdlib/Test2.scala +++ b/tests/pos-special/stdlib/Test2.scala @@ -64,7 +64,7 @@ object Test { def iterOps(xs: => Iterator[Int]^) = val strPlusInt: (String, Int) => String = _ + _ val intPlusStr: (Int, String) => String = _ + _ - val isEven: Int => Boolean = _ % 2 == 0 + val isEven: Int ->{cap[iterOps]} Boolean = _ % 2 == 0 val isNonNeg: Int => Boolean = _ > 0 val flips: Int => List[Int] = x => x :: -x :: Nil val x1 = xs.foldLeft("")(strPlusInt) @@ -116,7 +116,7 @@ object Test { def viewOps(xs: View[Int]^) = { val strPlusInt: (String, Int) => String = _ + _ val intPlusStr: (Int, String) => String = _ + _ - val isEven: Int => Boolean = _ % 2 == 0 + val isEven: Int ->{cap[viewOps]} Boolean = _ % 2 == 0 val isNonNeg: Int => Boolean = _ > 0 val flips: Int => List[Int] = x => x :: -x :: Nil val x1 = xs.foldLeft("")(strPlusInt) diff --git a/tests/pos/functorial-functors.scala b/tests/pos/functorial-functors.scala new file mode 100644 index 000000000000..5e810fa989a6 --- /dev/null +++ b/tests/pos/functorial-functors.scala @@ -0,0 +1,14 @@ +class Common: + + trait Functor: + type F[X] + extension [A](x: F[A]) def map[B](f: A => B): F[B] + + trait Monad extends Functor: + extension [A](x: F[A]) + def flatMap[B](f: A => F[B]): F[B] + def map[B](f: A => B) = x.flatMap(f `andThen` pure) + + def pure[A](x: A): F[A] +end Common + diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index fbb22039c327..5ea259b965ed 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -64,7 +64,7 @@ object Test { def viewOps(xs: View[Int]^{cap}) = { val strPlusInt: (String, Int) => String = _ + _ val intPlusStr: (Int, String) => String = _ + _ - val isEven: Int => Boolean = _ % 2 == 0 + val isEven: Int ->{cap[viewOps]} Boolean = _ % 2 == 0 val isNonNeg: Int => Boolean = _ > 0 val flips: Int => List[Int] = x => Cons(x, Cons(-x, Nil)) val x1 = xs.foldLeft("")(strPlusInt)