From 787e5bb37e6ea35786519b5a266a8a970eb30bf5 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 13 Sep 2023 09:14:04 +0200 Subject: [PATCH 01/58] Refine "cannot have inferred type" test --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b9904db4ea41..b0dcf4250a93 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1238,7 +1238,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !canUseInferred then val inferred = t.tpt.knownType def checkPure(tp: Type) = tp match - case CapturingType(_, refs) + case CapturingType(_, refs: CaptureSet.Var) if !refs.elems.filter(isNotPureThis).isEmpty => val resultStr = if t.isInstanceOf[DefDef] then " result" else "" report.error( From 1c0a50b39020ce6c0630862236673a97012ba8e0 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 18 Sep 2023 18:19:33 +0200 Subject: [PATCH 02/58] Run Setup code one phase before CheckCaptures This is needed if we want to do setup transforms as part of denotation transformers. --- compiler/src/dotty/tools/dotc/Compiler.scala | 5 +- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 158 ++-- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 7 +- .../dotty/tools/dotc/cc/CapturingType.scala | 58 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 118 ++- .../dotty/tools/dotc/cc/RetainingType.scala | 34 + compiler/src/dotty/tools/dotc/cc/Setup.scala | 778 ++++++++++-------- .../src/dotty/tools/dotc/cc/Synthetics.scala | 2 +- .../dotty/tools/dotc/config/Printers.scala | 7 +- .../tools/dotc/config/ScalaSettings.scala | 3 + .../dotty/tools/dotc/core/Definitions.scala | 12 +- .../src/dotty/tools/dotc/core/Flags.scala | 2 +- .../tools/dotc/core/SymDenotations.scala | 2 +- .../dotty/tools/dotc/core/TypeComparer.scala | 6 +- .../src/dotty/tools/dotc/core/Types.scala | 16 +- .../tools/dotc/printing/PlainPrinter.scala | 27 +- .../dotty/tools/dotc/transform/Recheck.scala | 28 +- .../captures/box-adapt-boxing.scala | 4 +- tests/neg-custom-args/captures/lazylist.check | 4 +- .../neg-custom-args/captures/lazylists2.check | 6 +- tests/neg-custom-args/captures/real-try.check | 2 +- .../captures/usingLogFile.check | 2 +- tests/neg-custom-args/captures/vars.check | 2 +- tests/pos-custom-args/captures/pairs.scala | 3 +- 24 files changed, 729 insertions(+), 557 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/RetainingType.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 126e54a7b49e..3a0980e24323 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,8 @@ 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.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/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index dfaa3c701576..b82bf795338e 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 @@ -28,23 +28,22 @@ private val adaptUnpickledFunctionTypes = false */ private val constrainRootsWhenMapping = true -/** 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 - 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 -/** Capture checking state, which is stored in a context property */ +/** Capture checking state, which is known to other capture checking components */ class CCState: + /** Temporary set indicating closures that are the rhs of a val or def. + * An entry gets removed when we check isLevelOwner on it. + */ val rhsClosure: mutable.HashSet[Symbol] = new mutable.HashSet - val levelOwners: mutable.HashSet[Symbol] = new mutable.HashSet + /** Cache for level ownership */ + val isLevelOwner: mutable.HashMap[Symbol, Boolean] = new mutable.HashMap /** Associates certain symbols (the nesting level owners) with their ccNestingLevel */ val nestingLevels: mutable.HashMap[Symbol, Int] = new mutable.HashMap @@ -61,13 +60,12 @@ class CCState: * 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 @@ -98,6 +96,8 @@ class mapRoots(from: CaptureRoot, to: CaptureRoot)(using Context) extends BiType && CaptureRoot.isEnclosingRoot(t, from) => to case from: CaptureRoot.Var if from.followAlias eq t => to case _ => t + case t @ Setup.Box(t1) => + t.derivedBox(this(t1)) case _ => mapOverFollowingAliases(t) @@ -123,11 +123,16 @@ 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. */ @@ -176,8 +181,10 @@ extension (tp: Type) 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) + def boxedUnlessFun(tycon: Type)(using Context) = // TODO: drop + if ctx.phase != Phases.checkCapturesPhase + && ctx.phase != Phases.checkCapturesPhase.prev + || defn.isFunctionSymbol(tycon.typeSymbol) then tp else tp.boxed @@ -247,16 +254,6 @@ extension (tp: Type) 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 +273,20 @@ extension (tp: Type) case _ => false -extension (cls: ClassSymbol) + def isCapabilityClassRef(using Context) = tp match + case _: TypeRef | _: AppliedType => tp.typeSymbol.hasAnnotation(defn.CapabilityAnnot) + case _ => false + +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 +332,53 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox - def isLevelOwner(using Context): Boolean = ccState.levelOwners.contains(sym) + // Not yet needed + 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 _ => annot.tree.retainedElems.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 + + 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 compute = + if symd.isClass then + symd.is(CaptureChecked) || symd.isRoot + else + symd.is(Method) + && (!symd.owner.isClass || symd.owner.is(CaptureChecked)) + && !Synthetics.isExcluded(sym) + && !isCaseClassSynthetic + && !symd.isConstructor + && (!symd.isAnonymousFunction + || ccState.rhsClosure.remove(sym) + || sym.definedLocalRoot.exists // TODO drop + ) + ccState.isLevelOwner.getOrElseUpdate(sym, compute) /** The owner of the current level. Qualifying owners are * - methods other than constructors and anonymous functions @@ -344,6 +392,24 @@ extension (sym: Symbol) else if sym.isLevelOwner then sym else sym.owner.levelOwner + /** 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) + /** The nesting level of `sym` for the purposes of `cc`, * -1 for NoSymbol */ @@ -358,7 +424,9 @@ extension (sym: Symbol) * a capture checker is running. */ def ccNestingLevelOpt(using Context): Option[Int] = - if ctx.property(ccStateKey).isDefined then Some(ccNestingLevel) else None + if ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev + then Some(ccNestingLevel) + else None /** The parameter with type caps.Cap in the leading term parameter section, * or NoSymbol, if none exists. @@ -379,24 +447,6 @@ extension (sym: Symbol) 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. @@ -425,8 +475,10 @@ 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) + def boxedUnlessFun(tycon: Type)(using Context) = // TODO drop + if ctx.phase != Phases.checkCapturesPhase + && ctx.phase != Phases.checkCapturesPhase.prev + || defn.isFunctionClass(tycon.typeSymbol) then ts else ts.mapconserve(_.boxed) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 5fdbea2a1bc6..026f02872086 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -200,7 +200,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. @@ -1035,10 +1035,7 @@ object CaptureSet: def levelErrors: Addenda = new Addenda: override def toAdd(using Context) = - for - state <- ctx.property(ccStateKey).toList - (ref, cs) <- state.levelError - yield + for (ref, cs) <- ccState.levelError.toList yield val levelStr = ref match case ref: (TermRef | ThisType) => i", defined at level ${ref.ccNestingLevel}" case _ => "" diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index a7c283f4cc3b..95ca10cf3674 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -25,28 +25,44 @@ 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 ctx.phaseId <= Phases.checkCapturesPhase.id => + Some((parent, ann.refs)) + case AnnotatedType(parent, ann) + if ann.symbol == defn.RetainsAnnot && ctx.phase == Phases.checkCapturesPhase => + 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 @@ -55,28 +71,10 @@ object CapturingType: * capture sets may be thrown away in the computed base type. */ def isUncachable(tp: Type)(using Context): Boolean = - ctx.phase == Phases.checkCapturesPhase && - (Setup.isDuringSetup || ctx.mode.is(Mode.IgnoreCaptures) && tp.isEventuallyCapturingType) + ctx.phase == Phases.checkCapturesPhase + && (Setup.isDuringSetup + || 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 b0dcf4250a93..4374cd1e3f27 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -27,26 +27,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, @@ -140,7 +120,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 @@ -152,36 +132,43 @@ object CheckCaptures: 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. + * 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 + def checkWellformedPost(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = + 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 - val others = for j <- 0 until retained.length if j != i yield retained(j).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) + check(parent.captureSet, parent) + + 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) + check(remaining, remaining) + end for + end checkWellformedPost /** Attachment key for bodies of closures, provided they are values */ val ClosureBodyValue = Property.Key[Unit] @@ -198,13 +185,11 @@ class CheckCaptures extends Recheck, SymTransformer: 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.* @@ -1050,20 +1035,24 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) - private var setup: Setup = compiletime.uninitialized + private var setup: SetupAPI = compiletime.uninitialized 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 = thisPhase.prev.asInstanceOf[Setup] + setup.setupUnit(ctx.compilationUnit.tpdTree, recheckDef) + + 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 @@ -1207,13 +1196,10 @@ class CheckCaptures extends Recheck, SymTransformer: 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 _ => - } + tree.tpe.foreachPart: + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + checkWellformedPost(parent, annot.tree, tree) + case _ => case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] && !Synthetics.isExcluded(t.symbol) => val sym = t.symbol 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..5af59b193aa3 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala @@ -0,0 +1,34 @@ +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 => + new Error(i"bad retains $tp").printStackTrace + assert(false) + 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..393616814cf8 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 config.Printers.{capt, ccSetup} +import ast.tpd, tpd.* +import transform.{PreRecheck, Recheck}, Recheck.* import CaptureSet.IdentityCaptRefMap import Synthetics.isExcluded import util.Property +import printing.{Printer, Texts}, Texts.{Text, Str} + +/** Operations accessed from CheckCaptures */ +trait SetupAPI: + type DefRecheck = (tpd.ValOrDefDef, Symbol) => Context ?=> Unit + def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit + def decorate(tp: Type, mapRoots: Boolean, addedSet: Type => CaptureSet)(using Context): Type + +object Setup: + def enabled(using Context) = true // 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 + //|| ctx.phase == Phases.checkCapturesPhase.prev + + 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,12 +68,49 @@ 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.* +class Setup extends PreRecheck, SymTransformer, SetupAPI: + thisPhase => + + override def isRunnable(using Context) = + super.isRunnable && Feature.ccEnabledSomewhere + + private def toCC(sym: Symbol)(using Context) = new FollowAliases: + def apply(t: Type): Type = + mapOverFollowingAliases(t) + + 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 + + /** - 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(symd: SymDenotation)(using Context): SymDenotation = + def needsTransform = symd.owner.isTerm || symd.owner.is(CaptureChecked) + def needsInfoTransform = symd.isGetter + if !pastRecheck && Feature.ccEnabledSomewhere then + val sym = symd.symbol + atPhase(thisPhase.next)(symd.maybeOwner.info) // ensure owner is completed + // if sym is class && level owner: add a capture root + // translate cap/capIn to local roots + // what are local roots? should we add them in CCState? + // create local roots if necessary + if Synthetics.needsTransform(symd) then + Synthetics.transform(symd) + else + val newFlags = newFlagsFor(symd) + val newInfo = + if needsInfoTransform then transformExplicitType(sym.info, mapRoots = false) + else /*toCC(sym)*/(sym.info) + + if newFlags != symd.flags || (newInfo ne sym.info) + then symd.copySymDenotation(initFlags = newFlags, info = newInfo) + else symd + else symd /** Create dependent function with underlying function class `tycon` and given * arguments `argTypes` and result `resType`. @@ -102,15 +180,13 @@ extends tpd.TreeTraverser: case _: TypeRef | _: AppliedType if 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 + if atPhase(thisPhase.next)(getter.termRef.isTracked) then val getterType = tp.memberInfo(getter).strippedDealias RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.RefiningVar(ctx.owner, getter))) - .showing(i"add capture refinement $tp --> $result", capt) + .showing(i"add capture refinement $tp --> $result", ccSetup) else core } @@ -122,7 +198,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 @@ -144,13 +221,13 @@ extends tpd.TreeTraverser: val res1 = this(res0) if isTopLevel then depFun(tycon1, args1, res1) - .showing(i"add function refinement $tp ($tycon1, $args1, $res1) (${tp.dealias}) --> $result", capt) + .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 +238,134 @@ 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), 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, mapRoots) 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, mapRoots: Boolean)(using Context): Type = + mapInferred(mapRoots)(tp) + + private def transformExplicitType(tp: Type, mapRoots: Boolean)(using Context): Type = + val expandAliases = new TypeMap: + 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 => + if t.symbol.isClass then t + else t.info match + case TypeAlias(alias) => + val transformed = + if t.symbol.isStatic then this(alias) + else transformExplicitType(alias, mapRoots)( + using ctx.withOwner(t.symbol.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 + checkQualifiedRoots(ann.tree) + CapturingType(this(parent), 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) .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: $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 = 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, mapRoots) + else transformExplicitType(tp, mapRoots)) /** Substitute parameter symbols in `from` to paramRefs in corresponding * method or poly types `to`. We use a single BiTypeMap to do everything. @@ -299,272 +407,244 @@ extends tpd.TreeTraverser: /** 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 = + def newOwnerFor(sym: Symbol)(using Context): Symbol = var octx = ctx while octx.owner == sym do octx = octx.outer - if octx.owner.name == nme.TRY_BLOCK then octx.owner else null + if octx.owner.name == nme.TRY_BLOCK then octx.owner else sym.maybeOwner /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = - sym.updateInfo(preRecheckPhase, info, newOwnerFor(sym)) + sym.updateInfo(thisPhase, info, newFlagsFor(sym), newOwnerFor(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) + updateInfo(sym, sym.info) + + extension (sym: Symbol) def nextInfo(using Context): Type = + atPhase(thisPhase.next)(sym.info) + + def setupTraverser(recheckDef: DefRecheck) = new TreeTraverser: + + 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) + 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(transformExplicitType(tree.tpe, mapRoots = false)) + case _ => false + + val sym = tree.symbol + val mapRoots = + // need to run before cc so that rhsClosure is defined without transforming + // symbols to cc, since rhsClosure is used in that transformation. + 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 + 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 + ) + ccSetup.println(i"mapped $tree = ${tpt.knownType}") 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 + 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 => + inContext(ctx.withOwner(tree.symbol.owner)): + traverseChildren(tree) + case tree: Try if Feature.enabled(Feature.saferExceptions) => + val tryOwner = newSymbol(ctx.owner, nme.TRY_BLOCK, SyntheticMethod, MethodType(Nil, defn.UnitType)) + ccState.isLevelOwner(tryOwner) = true + 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 - 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) + info - 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 + /** 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.termParamss.nestedExists(_.tpt.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 = - 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 + 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` + 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 !paramSignatureChanges && !mt.isParamDependent && prevLambdas.isEmpty then + mt.paramInfos + else + val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) + psyms.map(psym => mapr(subst(psym.nextInfo)).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) + + if sym.exists && signatureChanges then + val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) + .showing(i"update info $sym: ${sym.info} = $result", ccSetup) + if newInfo ne sym.info then + updateInfo(sym, + if sym.isAnonymousFunction || sym.is(Param) || sym.is(ParamAccessor) 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") + 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, mapRoots = true)) + case tree: TypeDef => + tree.symbol match + case cls: ClassSymbol => + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo + val newSelfType = + 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, mapRoots = true) + case _ => + NoType + if newSelfType.exists then + ccSetup.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 = tree.symbol.info + val newInfo = transformExplicitType(info, mapRoots = !ctx.owner.isStaticOwner) + updateInfo(tree.symbol, newInfo) + if newInfo ne info then + ccSetup.println(i"update info of ${tree.symbol} from $info to $newInfo") + 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 +661,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 +671,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) => @@ -632,6 +712,8 @@ 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: LazyRef => normalizeCaptures(tp.ref) case _ => @@ -648,12 +730,12 @@ 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 = transformExplicitType(dealiased, mapRoots) + // !!! TODO: We should map roots to root vars here + 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`. @@ -664,12 +746,8 @@ extends tpd.TreeTraverser: 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] - - def isDuringSetup(using Context): Boolean = - ctx.property(IsDuringSetupKey).isDefined 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..f59ac80ef1b5 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -99,7 +99,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 RetainingType(parent, _) => transformDefaultGetterCaptures(parent, owner, idx) case info @ AnnotatedType(parent, annot) => info.derivedAnnotatedType(transformDefaultGetterCaptures(parent, owner, idx), annot) 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/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8df809dc9ee6..97277eeb6600 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -641,7 +641,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def compareRefined: Boolean = val tp1w = tp1.widen - if ctx.phase == Phases.checkCapturesPhase then + if ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev then // A relaxed version of subtyping for dependent functions where method types // are treated as contravariant. @@ -2096,7 +2096,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling ExprType(info1.resType) case info1 => info1 - if ctx.phase == Phases.checkCapturesPhase then + if ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev then // When comparing against a RefiningVar refinement, map the // localRoot of the corresponding class in `tp1` to the owner of the // refining capture set. @@ -2220,7 +2220,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 ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev then // allow to constrain capture set variables isSubType(formal2a, formal1) else diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 8a725d8a0754..736c570c80d0 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, boxedUnlessFun, ccNestingLevel} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -2927,12 +2927,10 @@ object Types { 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 + 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,7 +5116,9 @@ object Types { else if (clsd.is(Module)) givenSelf else if (ctx.erasedTypes) appliedRef else givenSelf.dealiasKeepAnnots match - case givenSelf1 @ EventuallyCapturingType(tp, _) => + case givenSelf1 @ CapturingType(tp, _) => + givenSelf1.derivedAnnotatedType(tp & appliedRef, givenSelf1.annot) + case givenSelf1 @ RetainingType(tp, _) => givenSelf1.derivedAnnotatedType(tp & appliedRef, givenSelf1.annot) case _ => AndType(givenSelf, appliedRef) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 83ff03e05592..17bdee3dbc1a 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, ccNestingLevel, levelOwner, retainedElems} class PlainPrinter(_ctx: Context) extends Printer { @@ -161,8 +161,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. */ @@ -230,7 +242,7 @@ 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 showAsCap = rootsInRefs match @@ -249,6 +261,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 +288,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)) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 306ca2b0eb9c..47b277d06321 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) @@ -526,12 +522,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 +580,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/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/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index aeb410f07d65..ba5080d808f8 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: (ref1 : lazylists.LazyCons[Int]{val xs: () ->{cap1} lazylists.LazyList[Int]^}^{cap1}) + | Required: lazylists.LazyList[Int] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:37:36 ------------------------------------- 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/real-try.check b/tests/neg-custom-args/captures/real-try.check index f57aef60745b..ba58b000c8b8 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -28,7 +28,7 @@ 31 | Cell(() => foo(1))// // error | ^^^^^^^^^^^^^^^^^^ | Found: Cell[box () => Unit]^? - | Required: Cell[() ->? Unit]^? + | Required: Cell[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 diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index 9550b5864586..f4de75efc493 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 ------------------------------------------------------ diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index f8abe22e9d53..f589883f6aab 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -21,7 +21,7 @@ 26 | b = List(g) // error | ^^^^^^^ | Found: List[box (x$0: String) ->{cap3} String] - | Required: List[String ->{cap[test]} String] + | Required: List[box String ->{cap[test]} String] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:30:10 ----------------------------------------- diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index 78991e4377c0..e722e2fa74bd 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: From ffaf5636adf71b1a22d5f7296c3329e29b0648a5 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 18 Sep 2023 18:27:57 +0200 Subject: [PATCH 03/58] Don't shorten local roots when printing --- .../src/dotty/tools/dotc/printing/PlainPrinter.scala | 5 +++-- tests/neg-custom-args/captures/byname.check | 2 +- tests/neg-custom-args/captures/capt1.check | 8 ++++---- tests/neg-custom-args/captures/cc-this.check | 4 ++-- tests/neg-custom-args/captures/cc-this2.check | 2 +- tests/neg-custom-args/captures/cc-this3.check | 2 +- tests/neg-custom-args/captures/eta.check | 4 ++-- .../captures/exception-definitions.check | 4 ++-- tests/neg-custom-args/captures/i15772.check | 12 ++++++------ .../captures/lazylists-exceptions.check | 4 ++-- tests/neg-custom-args/captures/lazyref.check | 6 +++--- tests/neg-custom-args/captures/leaked-curried.check | 2 +- tests/neg-custom-args/captures/levels.check | 2 +- tests/neg-custom-args/captures/real-try.check | 12 ++++++------ tests/neg-custom-args/captures/try.check | 2 +- tests/neg-custom-args/captures/vars.check | 4 ++-- 16 files changed, 38 insertions(+), 37 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 17bdee3dbc1a..c2b9bb677a8c 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -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) 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..93b263aacba6 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[foo]}) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type () -> box C^{cap[foo]} 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/i15772.check b/tests/neg-custom-args/captures/i15772.check index cb6b40361add..3b1628b77af1 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -1,25 +1,25 @@ -- 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 + | Required: (C^{cap[main1]} ->{cap[main1]} 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 + | Required: (C^{cap[main2]} ->{cap[main2]} Unit) -> Unit | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:34 --------------------------------------- diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 74318b6bb254..57e5e65cd374 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,10 +1,10 @@ -- [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 + | 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 diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 8c91ec13b5d8..a3cf34067b30 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -8,21 +8,21 @@ -- [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: (ref2 : LazyRef[Int]{val elem: () ->{cap[test]} Int}^{cap2, ref1}) | 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: (ref3 : LazyRef[Int]{val elem: () ->{cap[test]} Int}^{cap2, ref1}) | 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: (ref4 : LazyRef[Int]{val elem: () ->{cap[test]} Int}^{cap2, cap1}) | 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..1918fecbc496 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -6,6 +6,6 @@ 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 + | value get of type () -> () ->{io} Cap^{cap[Foo]} has incompatible type | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index 8d11c196b10f..2a72dbd354d1 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -4,7 +4,7 @@ | Found: box (x: String) ->{cap3} String | Required: box (x$0: String) ->? String | - | Note that reference (cap3 : CC^), defined at level 2 + | Note that reference (cap3 : CC^{cap[scope]}), 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` diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index ba58b000c8b8..c81a30d82918 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -7,30 +7,30 @@ -- [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 + | 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 + | 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 -------------------------------------- 31 | Cell(() => foo(1))// // error | ^^^^^^^^^^^^^^^^^^ - | Found: Cell[box () => Unit]^? + | Found: Cell[box () ->{cap[]} Unit]^? | Required: Cell[box () ->? Unit]^? | - | Note that reference (cap : caps.Cap), defined at level 2 + | 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` diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 5994e3901179..feb16d8674ea 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -2,7 +2,7 @@ 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]^ + |Required: (lcap: caps.Cap) ?-> CT[Exception]^{lcap} ->{'cap[..test](from instantiating handle)} box CT[Exception]^{cap[test]} 24 | (x: CanThrow[Exception]) => x 25 | }{ | diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index f589883f6aab..fc01f5cce675 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -8,7 +8,7 @@ -- Error: tests/neg-custom-args/captures/vars.scala:23:14 -------------------------------------------------------------- 23 | 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 @@ -38,7 +38,7 @@ 31 | val sc: String => String = scope // error (but should also be OK) | ^^^^^ | Found: (x$0: String) ->{cap[scope]} String - | Required: String => String + | Required: String ->{cap[test]} 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 From 96b0030739170eba121b9857fc0f87757278c5bb Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 18 Sep 2023 20:34:51 +0200 Subject: [PATCH 04/58] Drop rhsClosure from CCState --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 12 ++---------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 3 ++- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index b82bf795338e..b872309810e6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -37,11 +37,6 @@ class IllegalCaptureRef(tpe: Type) extends Exception /** Capture checking state, which is known to other capture checking components */ class CCState: - /** Temporary set indicating closures that are the rhs of a val or def. - * An entry gets removed when we check isLevelOwner on it. - */ - val rhsClosure: mutable.HashSet[Symbol] = new mutable.HashSet - /** Cache for level ownership */ val isLevelOwner: mutable.HashMap[Symbol, Boolean] = new mutable.HashMap @@ -369,15 +364,12 @@ extension (sym: Symbol) if symd.isClass then symd.is(CaptureChecked) || symd.isRoot else - symd.is(Method) + symd.is(Method, butNot = Accessor) && (!symd.owner.isClass || symd.owner.is(CaptureChecked)) && !Synthetics.isExcluded(sym) && !isCaseClassSynthetic && !symd.isConstructor - && (!symd.isAnonymousFunction - || ccState.rhsClosure.remove(sym) - || sym.definedLocalRoot.exists // TODO drop - ) + && (!symd.isAnonymousFunction || sym.definedLocalRoot.exists) ccState.isLevelOwner.getOrElseUpdate(sym, compute) /** The owner of the current level. Qualifying owners are diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 393616814cf8..1e542c31c7d1 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -463,7 +463,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tree.rhs match case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => //ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - ccState.rhsClosure += ddef.symbol + //ccState.isLevelOwner(sym) = true + ccState.isLevelOwner(ddef.symbol) = true // 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 From 5992a3f7153727c53812e8cb4ac7a8eb5205db40 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 19 Sep 2023 08:53:22 +0200 Subject: [PATCH 05/58] Fix computation of level owners 1. Update symbols owner before recursing to children, so that new owners taking account of try block owners are installed. 2. Query owner chaines at phase checkCpatures when computing enclosing level owners. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 24 +++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 86 +++++++++++-------- tests/neg-custom-args/captures/i15772.check | 20 ++--- tests/neg-custom-args/captures/lazylist.check | 2 +- tests/neg-custom-args/captures/vars.check | 20 ----- tests/neg-custom-args/captures/vars.scala | 4 +- 6 files changed, 70 insertions(+), 86 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index b872309810e6..03ac6d5b330d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -380,26 +380,28 @@ 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)(using Context): Symbol = + if !sym.exists || sym.isRoot || sym.isStaticOwner then defn.RootClass + else if sym.isLevelOwner then sym + else recur(sym.owner) + recur(sym)(using ctx.withPhase(Phases.checkCapturesPhase)) /** 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 + def recur(sym: Symbol, prev: Symbol)(using Context): Symbol = + if sym.name.toString == name then + if sym.isLevelOwner then sym + else if sym.isTerm && !sym.isOneOf(Method | Module) && prev.exists then prev else NoSymbol - else if owner == defn.RootClass then + else if sym == defn.RootClass then NoSymbol else - val prev1 = if owner.isAnonymousFunction && owner.isLevelOwner then owner else NoSymbol - recur(owner.owner, prev1) - recur(sym, NoSymbol) + val prev1 = if sym.isAnonymousFunction && sym.isLevelOwner then sym else NoSymbol + recur(sym.owner, prev1) + recur(sym, NoSymbol)(using ctx.withPhase(Phases.checkCapturesPhase)) .showing(i"find outer $sym [ $name ] = $result", capt) /** The nesting level of `sym` for the purposes of `cc`, diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 1e542c31c7d1..12eac183befd 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -354,7 +354,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: then cc.mapRoots(defn.captureRoot.termRef, ctx.owner.localRoot.termRef)(tp1) .showing(i"map roots $tp1, ${tp1.getClass} == $result", capt) else tp1 - if tp2 ne tp then ccSetup.println(i"expanded: $tp --> $tp1 --> $tp2") + if tp2 ne tp then ccSetup.println(i"expanded in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 end transformExplicitType @@ -419,8 +419,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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. + /** Update the owner of `sym` to `newOwnerFor(sym)`. + * This can happen because of inserted try block owners. */ private def updateOwner(sym: Symbol)(using Context) = updateInfo(sym, sym.info) @@ -437,6 +437,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if isExcluded(meth) then return + updateOwner(meth) inContext(ctx.withOwner(meth)): paramss.foreach(traverse) transformTT(tpt, boxed = false, @@ -457,45 +458,56 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => false val sym = tree.symbol - val mapRoots = - // need to run before cc so that rhsClosure is defined without transforming - // symbols to cc, since rhsClosure is used in that transformation. - tree.rhs match - case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => - //ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - //ccState.isLevelOwner(sym) = true - ccState.isLevelOwner(ddef.symbol) = true - // 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 - 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 - ) - ccSetup.println(i"mapped $tree = ${tpt.knownType}") - traverse(tree.rhs) + val newOwner = + if sym.isOneOf(TermParamOrAccessor) then ctx.owner + else + updateOwner(sym) + sym + inContext(ctx.withOwner(newOwner)): + val mapRoots = + // need to run before cc so that rhsClosure is defined without transforming + // symbols to cc, since rhsClosure is used in that transformation. + tree.rhs match + case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => + //ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) + //ccState.isLevelOwner(sym) = true + ccState.isLevelOwner(ddef.symbol) = true + // 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. + true && !tpt.isInstanceOf[InferredTypeTree] + // in this case roots in inferred val type count as polymorphic + 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 + ) + 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, mapRoots = true) // type arguments in type applications are boxed - case tree: Template => - inContext(ctx.withOwner(tree.symbol.owner)): + case tree: TypeDef if tree.symbol.isClass => + val cls = tree.symbol + updateOwner(cls) + if cls.is(ModuleClass) then updateOwner(cls.sourceModule) + inContext(ctx.withOwner(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.isLevelOwner(tryOwner) = true ccState.tryBlockOwner(tree) = tryOwner inContext(ctx.withOwner(tryOwner)): traverseChildren(tree) + case _ => traverseChildren(tree) postProcess(tree) @@ -551,12 +563,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 = + val mapr = + if sym.isLevelOwner then mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) + else identity[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 !paramSignatureChanges && !mt.isParamDependent && prevLambdas.isEmpty then @@ -569,10 +581,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: ) case info: ExprType => info.derivedExprType(resType = - integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) + mapr(integrateRT(info.resType, psymss, prevPsymss, prevLambdas))) case info => - if prevLambdas.isEmpty then localReturnType - else SubstParams(prevPsymss, prevLambdas)(localReturnType) + mapr( + if prevLambdas.isEmpty then localReturnType + else SubstParams(prevPsymss, prevLambdas)(localReturnType)) if sym.exists && signatureChanges then val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) @@ -592,9 +605,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: ccSetup.println(i"FORCING $sym") 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 diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 3b1628b77af1..7aed49d4d574 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -22,25 +22,20 @@ | Required: (C^{cap[main2]} ->{cap[main2]} 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 + | ^^^^^^^ + |Found: (C{val arg: C^{cap[C]}}^{'cap[..main3](from instantiating box2)} ->{'cap[..main3](from instantiating box2)} Unit) ->? + | Unit + |Required: (C ->{cap[main3]} Unit) ->{cap[main3]} Unit | | 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]} + | Found: 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 - | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- 44 | x: (() -> Unit) // error @@ -48,7 +43,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/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index ba5080d808f8..4b7611fc3fb7 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -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]^ has incompatible type | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index fc01f5cce675..74000c79a212 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -24,26 +24,6 @@ | Required: List[box String ->{cap[test]} 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 ->{cap[test]} 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 | ^^^^^ diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index 9b280a42a2f2..81a68ce6a005 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -27,8 +27,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 + val sc: String => String = scope def local[T](op: (local: caps.Cap) -> CC^{local} -> T): T = op(caps.cap)(CC()) From f299a9b747130f62107e3325e49df6ce24bed656 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 19 Sep 2023 10:50:02 +0200 Subject: [PATCH 06/58] Setup newScheme mechanism --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 12eac183befd..f37324e0a02f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -23,7 +23,7 @@ trait SetupAPI: def decorate(tp: Type, mapRoots: Boolean, addedSet: Type => CaptureSet)(using Context): Type object Setup: - def enabled(using Context) = true // ctx.settings.YccNew.value // if new impl is conditional + def newScheme(using Context) = ctx.settings.YccNew.value // if new impl is conditional private val IsDuringSetupKey = new Property.Key[Unit] @@ -470,14 +470,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tree.rhs match case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => //ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - //ccState.isLevelOwner(sym) = true + if newScheme then ccState.isLevelOwner(sym) = true ccState.isLevelOwner(ddef.symbol) = true // 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. - true && !tpt.isInstanceOf[InferredTypeTree] + newScheme || !tpt.isInstanceOf[InferredTypeTree] // in this case roots in inferred val type count as polymorphic case _ => true From d1b2e340b6e74aa4e4dc954b163b491e88537847 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 19 Sep 2023 11:42:36 +0200 Subject: [PATCH 07/58] Introduce rootTarget for mapRoots customization --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 82 ++++++++++--------- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 4374cd1e3f27..fd886e5123c1 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -358,7 +358,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => mapOver(t) if variance > 0 then t1 - else setup.decorate(t1, mapRoots = false, addedSet = Function.const(CaptureSet.Fluid)) + else setup.decorate(t1, rootTarget = NoSymbol, addedSet = Function.const(CaptureSet.Fluid)) def isPreCC(sym: Symbol): Boolean = sym.isTerm && sym.maybeOwner.isClass diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index f37324e0a02f..17b2da3f3d2f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -20,7 +20,7 @@ import printing.{Printer, Texts}, Texts.{Text, Str} trait SetupAPI: type DefRecheck = (tpd.ValOrDefDef, Symbol) => Context ?=> Unit def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit - def decorate(tp: Type, mapRoots: Boolean, addedSet: Type => CaptureSet)(using Context): Type + def decorate(tp: Type, rootTarget: Symbol, addedSet: Type => CaptureSet)(using Context): Type object Setup: def newScheme(using Context) = ctx.settings.YccNew.value // if new impl is conditional @@ -74,10 +74,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: override def isRunnable(using Context) = super.isRunnable && Feature.ccEnabledSomewhere - private def toCC(sym: Symbol)(using Context) = new FollowAliases: - def apply(t: Type): Type = - mapOverFollowingAliases(t) - 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 @@ -90,26 +86,26 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * is CC-enabled. */ def transformSym(symd: SymDenotation)(using Context): SymDenotation = - def needsTransform = symd.owner.isTerm || symd.owner.is(CaptureChecked) - def needsInfoTransform = symd.isGetter + def needsInfoTransform = newScheme || symd.isGetter if !pastRecheck && Feature.ccEnabledSomewhere then val sym = symd.symbol atPhase(thisPhase.next)(symd.maybeOwner.info) // ensure owner is completed // if sym is class && level owner: add a capture root // translate cap/capIn to local roots - // what are local roots? should we add them in CCState? // create local roots if necessary if Synthetics.needsTransform(symd) then Synthetics.transform(symd) - else + else if symd.owner.isTerm || symd.is(CaptureChecked) || symd.owner.is(CaptureChecked) then val newFlags = newFlagsFor(symd) val newInfo = - if needsInfoTransform then transformExplicitType(sym.info, mapRoots = false) - else /*toCC(sym)*/(sym.info) + if needsInfoTransform + then transformExplicitType(sym.info, rootTarget = if newScheme then sym else NoSymbol) + else sym.info if newFlags != symd.flags || (newInfo ne sym.info) then symd.copySymDenotation(initFlags = newFlags, info = newInfo) else symd + else symd else symd /** Create dependent function with underlying function class `tycon` and given @@ -161,7 +157,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ - private def mapInferred(mapRoots: Boolean)(using Context) = new TypeMap: + private def mapInferred(rootTarget: Symbol)(using Context) = new TypeMap: override def toString = "map inferred" /** Drop @retains annotations everywhere */ @@ -246,14 +242,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: box(this(tp1)) case _ => mapOver(tp) - addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner, mapRoots) + addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner, rootTarget) end apply end mapInferred - private def transformInferredType(tp: Type, mapRoots: Boolean)(using Context): Type = - mapInferred(mapRoots)(tp) + private def transformInferredType(tp: Type, rootTarget: Symbol)(using Context): Type = + mapInferred(rootTarget)(tp) - private def transformExplicitType(tp: Type, mapRoots: Boolean)(using Context): Type = + private def transformExplicitType(tp: Type, rootTarget: Symbol)(using Context): Type = val expandAliases = new TypeMap: override def toString = "expand aliases" @@ -301,13 +297,15 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if t2 ne t then return t2 t match case t: TypeRef => - if t.symbol.isClass then t + val sym = t.symbol + if sym.isClass then t else t.info match case TypeAlias(alias) => val transformed = - if t.symbol.isStatic then this(alias) - else transformExplicitType(alias, mapRoots)( - using ctx.withOwner(t.symbol.owner)) + 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 _ => @@ -350,8 +348,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 if tp2 ne tp then ccSetup.println(i"expanded in ${ctx.owner}: $tp --> $tp1 --> $tp2") @@ -359,13 +357,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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(tp, mapRoots) - else transformExplicitType(tp, mapRoots)) + then transformInferredType(tp, rootTarget) + else transformExplicitType(tp, rootTarget)) /** Substitute parameter symbols in `from` to paramRefs in corresponding * method or poly types `to`. We use a single BiTypeMap to do everything. @@ -442,7 +440,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: paramss.foreach(traverse) transformTT(tpt, boxed = false, exact = tree.symbol.allOverriddenSymbols.hasNext, - mapRoots = true) + rootTarget = ctx.owner) traverse(tree.rhs) //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") @@ -454,7 +452,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case Apply(fn, _) => mentionsCap(fn) case TypeApply(fn, args) => args.exists(mentionsCap) case _: InferredTypeTree => false - case _: TypeTree => containsCap(transformExplicitType(tree.tpe, mapRoots = false)) + case _: TypeTree => containsCap(transformExplicitType(tree.tpe, rootTarget = NoSymbol)) case _ => false val sym = tree.symbol @@ -464,7 +462,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: updateOwner(sym) sym inContext(ctx.withOwner(newOwner)): - val mapRoots = + val rootsNeedMap = // need to run before cc so that rhsClosure is defined without transforming // symbols to cc, since rhsClosure is used in that transformation. tree.rhs match @@ -484,7 +482,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 + rootTarget = if rootsNeedMap then ctx.owner else NoSymbol ) ccSetup.println(i"mapped $tree = ${tpt.knownType}") traverse(tree.rhs) @@ -492,7 +490,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 + transformTT(arg, boxed = true, exact = false, rootTarget = ctx.owner) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => val cls = tree.symbol @@ -521,10 +519,15 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: traverse(rest) case Nil => + extension (sym: Symbol)(using Context) def unlessStatic = + val owner = sym.levelOwner + if sym.isStaticOwner then NoSymbol else owner + def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => + val lowner = transformTT(tree, boxed = false, exact = false, - mapRoots = !ctx.owner.levelOwner.isStaticOwner // other types in static locations are not boxed + rootTarget = ctx.owner.unlessStatic // other types in static locations are not boxed ) case tree: ValOrDefDef => val sym = tree.symbol @@ -564,7 +567,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order ): Type = val mapr = - if sym.isLevelOwner then mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) + if !newScheme && sym.isLevelOwner + then mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) else identity[Type] info match case mt: MethodOrPoly => @@ -608,7 +612,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree: Bind => val sym = tree.symbol - updateInfo(sym, transformInferredType(sym.info, mapRoots = true)) + updateInfo(sym, transformInferredType(sym.info, rootTarget = ctx.owner)) case tree: TypeDef => tree.symbol match case cls: ClassSymbol => @@ -624,7 +628,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else selfInfo match case selfInfo: Type => inContext(ctx.withOwner(cls)): - transformExplicitType(selfInfo, mapRoots = true) + transformExplicitType(selfInfo, rootTarget = ctx.owner) case _ => NoType if newSelfType.exists then @@ -642,7 +646,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if cls.is(ModuleClass) then updateOwner(cls.sourceModule) case _ => val info = tree.symbol.info - val newInfo = transformExplicitType(info, mapRoots = !ctx.owner.isStaticOwner) + val newInfo = transformExplicitType(info, rootTarget = ctx.owner.unlessStatic) updateInfo(tree.symbol, newInfo) if newInfo ne info then ccSetup.println(i"update info of ${tree.symbol} from $info to $newInfo") @@ -733,7 +737,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** 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, rootTarget: Symbol, 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 @@ -743,7 +747,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else fallback val dealiased = tp.dealiasKeepAnnots if dealiased ne tp then - val transformed = transformExplicitType(dealiased, mapRoots) + val transformed = transformExplicitType(dealiased, rootTarget) // !!! TODO: We should map roots to root vars here maybeAdd(transformed, if transformed ne dealiased then transformed else tp) else maybeAdd(tp, tp) @@ -751,8 +755,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** 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, rootTarget: Symbol)(using Context): Type = + decorate(tp, rootTarget, addedSet = _.dealias.match case CapturingType(_, refs) => CaptureSet.Var(owner, refs.elems) case _ => CaptureSet.Var(owner)) From f4687dfb6785651bf1aed815cfc76b8df67eeb35 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 19 Sep 2023 13:16:14 +0200 Subject: [PATCH 08/58] Change result type inference of vals and defs --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 17b2da3f3d2f..b999552939cd 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -563,6 +563,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 = @@ -581,18 +582,18 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) psyms.map(psym => mapr(subst(psym.nextInfo)).asInstanceOf[mt.PInfo]), mt1 => - mapr(integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas)) + mapr(integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas)) ) case info: ExprType => info.derivedExprType(resType = - mapr(integrateRT(info.resType, psymss, prevPsymss, prevLambdas))) + mapr(integrateRT(info.resType, psymss, resType, prevPsymss, prevLambdas))) case info => mapr( - if prevLambdas.isEmpty then localReturnType - else SubstParams(prevPsymss, prevLambdas)(localReturnType)) + if prevLambdas.isEmpty then resType + else SubstParams(prevPsymss, prevLambdas)(resType)) if sym.exists && signatureChanges then - val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) + val newInfo = integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil) .showing(i"update info $sym: ${sym.info} = $result", ccSetup) if newInfo ne sym.info then updateInfo(sym, From 3786e669c96a4e8b1fdd1ade5f2cca734fa0b36c Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 19 Sep 2023 14:20:07 +0200 Subject: [PATCH 09/58] Make RecheckDef return a type --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 11 ++++++----- compiler/src/dotty/tools/dotc/cc/Setup.scala | 6 ++++-- .../dotty/tools/dotc/transform/Recheck.scala | 19 +++++++++++-------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index fd886e5123c1..f3135a74b800 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -601,10 +601,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 super.recheckValDef(tree, sym) finally if !sym.is(Param) then // Parameters with inferred types belong to anonymous methods. We need to wait @@ -613,8 +613,9 @@ 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 diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index b999552939cd..1b0a63acf25f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -18,7 +18,7 @@ import printing.{Printer, Texts}, Texts.{Text, Str} /** Operations accessed from CheckCaptures */ trait SetupAPI: - type DefRecheck = (tpd.ValOrDefDef, Symbol) => Context ?=> Unit + type DefRecheck = (tpd.ValOrDefDef, Symbol) => Context ?=> Type def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit def decorate(tp: Type, rootTarget: Symbol, addedSet: Type => CaptureSet)(using Context): Type @@ -593,8 +593,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else SubstParams(prevPsymss, prevLambdas)(resType)) if sym.exists && signatureChanges then - val newInfo = integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil) + 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 updateInfo(sym, if sym.isAnonymousFunction || sym.is(Param) || sym.is(ParamAccessor) then diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 47b277d06321..c3ed979b9761 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -246,13 +246,16 @@ 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 = + if tree.rhs.isEmpty then sym.info + else recheck(tree.rhs, sym.info) - 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) @@ -421,7 +424,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 @@ -447,7 +450,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) From 23ab1bae5ef221c097ecba1fcc19626560bdb478 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 19 Sep 2023 22:49:46 +0200 Subject: [PATCH 10/58] Use a separate phase to add try owners I tried several other strategies, but nothing worked. - update after Setup: This means we cannot to levelOwner during setup. - running levelOwner after Setup causes CyclicErrors when transforming symbols in Setup - having a seperate notion of ccOwner maintained in a CCState table does not work since there are too many hidden uses of owner everywhere that would ignore that table. --- compiler/src/dotty/tools/dotc/Compiler.scala | 1 + compiler/src/dotty/tools/dotc/ast/tpd.scala | 15 ++++++ .../dotty/tools/dotc/cc/AddTryOwners.scala | 45 +++++++++++++++++ .../src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 1 + compiler/src/dotty/tools/dotc/cc/Setup.scala | 50 ++----------------- .../dotty/tools/dotc/transform/Recheck.scala | 14 ++++-- tests/neg-custom-args/captures/real-try.check | 14 +++--- 8 files changed, 88 insertions(+), 60 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 3a0980e24323..4db5f96c3d18 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -83,6 +83,7 @@ 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 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 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..2e9f7ecfccd2 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala @@ -0,0 +1,45 @@ +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 + 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/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 03ac6d5b330d..c308209300db 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -380,18 +380,18 @@ extension (sym: Symbol) * - _root_ */ def levelOwner(using Context): Symbol = - def recur(sym: Symbol)(using Context): Symbol = + 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)(using ctx.withPhase(Phases.checkCapturesPhase)) + recur(sym) /** 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(sym: Symbol, prev: Symbol)(using Context): Symbol = + def recur(sym: Symbol, prev: Symbol): Symbol = if sym.name.toString == name then if sym.isLevelOwner then sym else if sym.isTerm && !sym.isOneOf(Method | Module) && prev.exists then prev @@ -401,7 +401,7 @@ extension (sym: Symbol) else val prev1 = if sym.isAnonymousFunction && sym.isLevelOwner then sym else NoSymbol recur(sym.owner, prev1) - recur(sym, NoSymbol)(using ctx.withPhase(Phases.checkCapturesPhase)) + recur(sym, NoSymbol) .showing(i"find outer $sym [ $name ] = $result", capt) /** The nesting level of `sym` for the purposes of `cc`, diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f3135a74b800..c9553c10efe3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -182,6 +182,7 @@ 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) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 1b0a63acf25f..374615ea23e5 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -402,31 +402,17 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 = - var octx = ctx - while octx.owner == sym do octx = octx.outer - if octx.owner.name == nme.TRY_BLOCK then octx.owner else sym.maybeOwner - /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = - sym.updateInfo(thisPhase, info, newFlagsFor(sym), newOwnerFor(sym)) + sym.updateInfo(thisPhase, info, newFlagsFor(sym)) sym.namedType match case ref: CaptureRef => ref.invalidateCaches() // TODO: needed? case _ => - /** Update the owner of `sym` to `newOwnerFor(sym)`. - * This can happen because of inserted try block owners. - */ - private def updateOwner(sym: Symbol)(using Context) = - updateInfo(sym, sym.info) - extension (sym: Symbol) def nextInfo(using Context): Type = atPhase(thisPhase.next)(sym.info) - def setupTraverser(recheckDef: DefRecheck) = new TreeTraverser: + def setupTraverser(recheckDef: DefRecheck) = new TreeTraverserWithPreciseImportContexts: def traverse(tree: Tree)(using Context): Unit = tree match @@ -435,7 +421,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if isExcluded(meth) then return - updateOwner(meth) inContext(ctx.withOwner(meth)): paramss.foreach(traverse) transformTT(tpt, boxed = false, @@ -456,12 +441,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => false val sym = tree.symbol - val newOwner = - if sym.isOneOf(TermParamOrAccessor) then ctx.owner - else - updateOwner(sym) - sym - inContext(ctx.withOwner(newOwner)): + val defCtx = if sym.isOneOf(TermParamOrAccessor) then ctx else ctx.withOwner(sym) + inContext(defCtx): val rootsNeedMap = // need to run before cc so that rhsClosure is defined without transforming // symbols to cc, since rhsClosure is used in that transformation. @@ -493,17 +474,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: transformTT(arg, boxed = true, exact = false, rootTarget = ctx.owner) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => - val cls = tree.symbol - updateOwner(cls) - if cls.is(ModuleClass) then updateOwner(cls.sourceModule) - inContext(ctx.withOwner(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.isLevelOwner(tryOwner) = true - ccState.tryBlockOwner(tree) = tryOwner - inContext(ctx.withOwner(tryOwner)): + inContext(ctx.withOwner(tree.symbol)): traverseChildren(tree) case _ => @@ -511,14 +482,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 => - extension (sym: Symbol)(using Context) def unlessStatic = val owner = sym.levelOwner if sym.isStaticOwner then NoSymbol else owner @@ -644,9 +607,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 = tree.symbol.info val newInfo = transformExplicitType(info, rootTarget = ctx.owner.unlessStatic) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index c3ed979b9761..b0e226d60c09 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -138,19 +138,27 @@ 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 = diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index c81a30d82918..eee1b8c456cc 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -24,13 +24,11 @@ | 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 () ->{cap[]} Unit]^? - | Required: Cell[box () ->? 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 at level 2 + |cannot be included in outer capture set ?, defined at level 1 in method test From 8968e8ec1aad05546a225598bb0ff4703beb61c1 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 20 Sep 2023 12:27:29 +0200 Subject: [PATCH 11/58] Transform infos of all symbols defined in current run in SymTransformer (except if they are updated). This is the first step towards converting to capturing types in SymTransformer, where we have the context. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index c308209300db..3ac0c419e975 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -32,7 +32,7 @@ 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 +class IllegalCaptureRef(tpe: Type) extends Exception(tpe.toString) /** Capture checking state, which is known to other capture checking components */ class CCState: diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 374615ea23e5..255cf54fad6d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -15,6 +15,7 @@ import CaptureSet.IdentityCaptRefMap import Synthetics.isExcluded import util.Property import printing.{Printer, Texts}, Texts.{Text, Str} +import collection.mutable /** Operations accessed from CheckCaptures */ trait SetupAPI: @@ -74,7 +75,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: override def isRunnable(using Context) = super.isRunnable && Feature.ccEnabledSomewhere - def newFlagsFor(symd: SymDenotation)(using Context): FlagSet = + 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 @@ -89,7 +92,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def needsInfoTransform = newScheme || symd.isGetter if !pastRecheck && Feature.ccEnabledSomewhere then val sym = symd.symbol - atPhase(thisPhase.next)(symd.maybeOwner.info) // ensure owner is completed + val needsInfoTransform = sym.isDefinedInCurrentRun && !toBeUpdated.contains(sym) // if sym is class && level owner: add a capture root // translate cap/capIn to local roots // create local roots if necessary @@ -98,8 +101,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else if symd.owner.isTerm || symd.is(CaptureChecked) || symd.owner.is(CaptureChecked) then val newFlags = newFlagsFor(symd) val newInfo = - if needsInfoTransform - then transformExplicitType(sym.info, rootTarget = if newScheme then sym else NoSymbol) + if needsInfoTransform then + atPhase(thisPhase.next)(symd.maybeOwner.info) // ensure owner is completed + transformExplicitType(sym.info, rootTarget = if newScheme then sym else NoSymbol) else sym.info if newFlags != symd.flags || (newInfo ne sym.info) @@ -107,6 +111,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else symd else symd else symd + end transformSym /** Create dependent function with underlying function class `tycon` and given * arguments `argTypes` and result `resType`. @@ -404,7 +409,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = + toBeUpdated += sym sym.updateInfo(thisPhase, info, newFlagsFor(sym)) + toBeUpdated -= sym sym.namedType match case ref: CaptureRef => ref.invalidateCaches() // TODO: needed? case _ => @@ -514,7 +521,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else tree.tpt.knownType def paramSignatureChanges = tree.match - case tree: DefDef => tree.termParamss.nestedExists(_.tpt.hasRememberedType) + case tree: DefDef => tree.paramss.nestedExists: + case param: ValDef => param.tpt.hasRememberedType + case param: TypeDef => param.rhs.hasRememberedType case _ => false def signatureChanges = From 8528371443668726cb70a125a26f39610f9de7f7 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 20 Sep 2023 12:28:20 +0200 Subject: [PATCH 12/58] Fix bug when restoring denotations in Recheck. Need to copy the denotation, since denotations come with next pointers which would get scrambled otherwise. --- compiler/src/dotty/tools/dotc/transform/Recheck.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index b0e226d60c09..e21e66550cec 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -158,7 +158,8 @@ abstract class Recheck extends Phase, SymTransformer: val sym = symd.symbol def updatedAfter(p: Phase): Boolean = sym.isUpdatedAfter(p) || p != preRecheckPhase && updatedAfter(p.next) - if updatedAfter(firstPrepPhase) then atPhase(firstPrepPhase)(sym.denot.copySymDenotation()) + if updatedAfter(firstPrepPhase) + then atPhase(firstPrepPhase)(sym.denot.copySymDenotation()) else symd def run(using Context): Unit = From 9412db8b89af228149667402996fb90cf3be57af Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 20 Sep 2023 12:28:51 +0200 Subject: [PATCH 13/58] Do fluidify when transforming symbols instead of on access --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 41 ++----------------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 30 ++++++++++++-- .../dotty/tools/dotc/typer/RefChecks.scala | 37 +++++++++-------- 3 files changed, 51 insertions(+), 57 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c9553c10efe3..b3cddc514a31 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -342,38 +342,6 @@ 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, rootTarget = NoSymbol, 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 = if tree.symbol.is(Method) then if tree.symbol.info.isParameterless then @@ -381,7 +349,7 @@ class CheckCaptures extends Recheck, SymTransformer: includeCallCaptures(tree.symbol, tree.srcPos) else markFree(tree.symbol, tree.srcPos) - handleBackwardsCompat(super.recheckIdent(tree), tree.symbol) + super.recheckIdent(tree) /** A specialized implementation of the selection rule. * @@ -411,7 +379,7 @@ 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) + selType else val qualCs = qualType.captureSet capt.println(i"pick one of $qualType, ${selType.widen}, $qualCs, $selCs in $tree") @@ -1025,9 +993,8 @@ 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) = diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 255cf54fad6d..dfe60bb2fa1a 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -11,7 +11,7 @@ import config.Feature import config.Printers.{capt, ccSetup} import ast.tpd, tpd.* import transform.{PreRecheck, Recheck}, Recheck.* -import CaptureSet.IdentityCaptRefMap +import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded import util.Property import printing.{Printer, Texts}, Texts.{Text, Str} @@ -21,7 +21,7 @@ import collection.mutable trait SetupAPI: type DefRecheck = (tpd.ValOrDefDef, Symbol) => Context ?=> Type def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit - def decorate(tp: Type, rootTarget: Symbol, addedSet: Type => CaptureSet)(using Context): Type + def isPreCC(sym: Symbol)(using Context): Boolean object Setup: def newScheme(using Context) = ctx.settings.YccNew.value // if new impl is conditional @@ -82,6 +82,29 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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, rootTarget = NoSymbol, 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. @@ -89,7 +112,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * is CC-enabled. */ def transformSym(symd: SymDenotation)(using Context): SymDenotation = - def needsInfoTransform = newScheme || symd.isGetter if !pastRecheck && Feature.ccEnabledSomewhere then val sym = symd.symbol val needsInfoTransform = sym.isDefinedInCurrentRun && !toBeUpdated.contains(sym) @@ -98,6 +120,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // create local roots if necessary if Synthetics.needsTransform(symd) then Synthetics.transform(symd) + 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 = diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 061d759e9ca4..c13b3854a7dd 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -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 @@ -371,8 +374,8 @@ 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) = self.memberInfo(other) refcheck.println(i"check override ${infoString(member)} overriding ${infoString(other)}") From 5056a5f1643f237ce4ccd63b53bfb8d807ff9ada Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 20 Sep 2023 22:09:19 +0200 Subject: [PATCH 14/58] Align rechecking ValDefs and DefDefs Makes things more regular and allows for a non-identity mapping between declared types and infos of vals. --- compiler/src/dotty/tools/dotc/transform/Recheck.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index e21e66550cec..cef6ddabd468 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -256,8 +256,9 @@ abstract class Recheck extends Phase, SymTransformer: bindType.symbol.info def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = - if tree.rhs.isEmpty then sym.info - else recheck(tree.rhs, sym.info) + val resType = recheck(tree.tpt) + if tree.rhs.isEmpty then resType + else recheck(tree.rhs, resType) def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = inContext(linkConstructorParams(sym).withOwner(sym)): From a10057df9c77b60888dbe5ac63c6c90c7170b9b5 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 20 Sep 2023 22:15:11 +0200 Subject: [PATCH 15/58] Revise inferred type checking The new check is that a publicly visible inferred type after capture checking must conform to the type before capture checking (which is also the type seen by other separately compiled units). --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 10 +++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 77 +++++++++--------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 18 ++--- tests/neg-custom-args/captures/i15116.check | 80 +++++++++++++------ .../captures/usingLogFile.check | 34 ++++++-- 5 files changed, 134 insertions(+), 85 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 3ac0c419e975..abe4fa4bde77 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -272,6 +272,16 @@ extension (tp: Type) 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) + extension (cls: Symbol) def pureBaseClass(using Context): Option[Symbol] = diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b3cddc514a31..39b0510b8760 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -573,7 +573,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = try if sym.is(Module) then sym.info // Modules are checked by checking the module class - else super.recheckValDef(tree, sym) + 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 @@ -589,11 +589,46 @@ class CheckCaptures extends Recheck, SymTransformer: 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) 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 + 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. @@ -1148,9 +1183,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`. */ @@ -1169,41 +1201,6 @@ class CheckCaptures extends Recheck, SymTransformer: case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => checkWellformedPost(parent, 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: CaptureSet.Var) - 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 => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index dfe60bb2fa1a..979c93775ab1 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -127,7 +127,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val newInfo = if needsInfoTransform then atPhase(thisPhase.next)(symd.maybeOwner.info) // ensure owner is completed - transformExplicitType(sym.info, rootTarget = if newScheme then sym else NoSymbol) + transformExplicitType(sym.info, rootTarget = if newScheme && false then sym else NoSymbol) else sym.info if newFlags != symd.flags || (newInfo ne sym.info) @@ -189,14 +189,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: private def mapInferred(rootTarget: Symbol)(using Context) = 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. @@ -265,7 +257,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // 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)) @@ -480,14 +472,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tree.rhs match case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => //ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - if newScheme then ccState.isLevelOwner(sym) = true + if newScheme && false then ccState.isLevelOwner(sym) = true ccState.isLevelOwner(ddef.symbol) = true // 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. - newScheme || !tpt.isInstanceOf[InferredTypeTree] + (newScheme && false) || !tpt.isInstanceOf[InferredTypeTree] // in this case roots in inferred val type count as polymorphic case _ => true @@ -564,7 +556,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order ): Type = val mapr = - if !newScheme && sym.isLevelOwner + if !(newScheme && false) && sym.isLevelOwner then mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) else identity[Type] info match 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/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index f4de75efc493..5fd9b417e260 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -1,3 +1,16 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/usingLogFile.scala:23:27 --------------------------------- +23 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: () ->{x$0, local} Unit + | Required: () -> Unit + | + | Note that the expected type () -> Unit + | is the previously inferred type of value later + | which is also the type seen in separately compiled sources. + | The new inferred type box () ->{x$0, local} Unit + | must conform to this type. + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/usingLogFile.scala:32:37 ------------------------------------------------------ 32 | usingLogFile { f => later3 = () => f.write(0) } // error | ^ @@ -10,18 +23,23 @@ | Required: Test2.Cell[box () ->{cap[]} Unit] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ -23 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^ - | escaping local reference local.type +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/usingLogFile.scala:47:27 --------------------------------- +47 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: () ->{x$0, local} Unit + | Required: () -> Unit + | + | Note that the expected type () -> Unit + | is the previously inferred type of value later + | which is also the type seen in separately compiled sources. + | The new inferred type box (() ->{x$0, local} Unit)^? + | must conform to this type. + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/usingLogFile.scala:28:23 ------------------------------------------------------ 28 | private val later2 = usingLogFile { f => Cell(() => f.write(0)) } // error | ^^^^^^^^^^^^ | escaping local reference local.type --- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:14 ------------------------------------------------------ -47 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^ - | escaping local reference local.type -- Error: tests/neg-custom-args/captures/usingLogFile.scala:62:16 ------------------------------------------------------ 62 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error | ^^^^^^^^^ From 4a44d431a88aee981a6320296ba8cb6584332a47 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 21 Sep 2023 10:14:19 +0200 Subject: [PATCH 16/58] Take singleton types into account when instantiating local roots Widen singleton types when instantiating local roots in checkConforms --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 5 +++-- tests/neg-custom-args/captures/i15772.check | 7 ------- tests/neg-custom-args/captures/i15772.scala | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 39b0510b8760..c0089bd99f9d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -761,8 +761,9 @@ class CheckCaptures extends Recheck, SymTransformer: // 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 actualw = actual.widen + val actual1 = mapRoots(defn.captureRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner))(actualw) + (actual1 ne actualw) && { val res = super.isCompatible(actual1, expected) if !res && ctx.settings.YccDebug.value then println(i"Failure under mapped roots:") diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 7aed49d4d574..22c95f8a8ed5 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -30,13 +30,6 @@ |Required: (C ->{cap[main3]} Unit) ->{cap[main3]} Unit | | 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^ - | Required: C^{'cap[..main3](from instantiating unsafe)} - | - | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- 44 | x: (() -> Unit) // error | ^ 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: From 3160f1c64e5ce5d42c4ba0b91be005d60a450b9d Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 21 Sep 2023 16:56:20 +0200 Subject: [PATCH 17/58] Replace some uses of CaptureSet universal to account for local roots --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 23 ++++++++++++++----- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 ++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 026f02872086..3cde5cae981a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -61,6 +61,10 @@ sealed abstract class CaptureSet extends Showable: */ def owner: Symbol + def rootSet(using Context): CaptureSet = + if Setup.newScheme then owner.localRoot.termRef.singletonCaptureSet + else universal + /** 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. @@ -539,13 +549,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 +564,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 @@ -694,7 +705,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) @@ -758,7 +769,7 @@ object 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) @@ -789,7 +800,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))) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 979c93775ab1..5b8043fdf45d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -685,8 +685,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 _ => From c841cff3b74ca4a7e388a7a2d1ed10a125e55242 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 21 Sep 2023 16:59:41 +0200 Subject: [PATCH 18/58] Less forcing of info when printing Don't force info when checking whether something is a root capability while printing --- .../src/dotty/tools/dotc/printing/PlainPrinter.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index c2b9bb677a8c..1ed02e8b6e7d 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -186,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 => @@ -193,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" @@ -245,7 +250,7 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close 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 From cd8b337c9ae2a88eb448ec24220bd51218b013c9 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 23 Sep 2023 10:05:07 +0200 Subject: [PATCH 19/58] Also set CaptureChecked status for unpickled roots --- compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala | 1 + 1 file changed, 1 insertion(+) 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 From 505efbd105323c3e49de1a57b905826397c63eb3 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 23 Sep 2023 10:51:36 +0200 Subject: [PATCH 20/58] Let vals be lever owners Refactor everything so that now vals as well as defs can be level owners --- .../tools/dotc/cc/CaptureAnnotation.scala | 13 +- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 32 +++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 21 ++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 137 ++++++++++-------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 72 +++------ .../src/dotty/tools/dotc/cc/Synthetics.scala | 38 ++--- .../dotty/tools/dotc/core/TypeComparer.scala | 14 +- .../dotty/tools/dotc/transform/Recheck.scala | 5 +- tests/neg-custom-args/captures/byname.check | 2 +- .../captures/heal-tparam-cs.scala | 2 +- tests/neg-custom-args/captures/i15772.check | 3 +- tests/neg-custom-args/captures/lazylist.check | 2 +- .../captures/leaked-curried.check | 7 - .../captures/leaked-curried.scala | 2 +- .../captures/usingLogFile.check | 41 ++---- .../captures/usingLogFile.scala | 4 +- tests/neg-custom-args/captures/vars.check | 2 +- tests/pos-custom-args/captures/capt2.scala | 2 +- tests/pos-custom-args/captures/colltest.scala | 1 - .../captures/function-combinators.scala | 4 +- tests/pos-special/stdlib/Test1.scala | 2 +- tests/pos-special/stdlib/Test2.scala | 6 +- .../captures/colltest5/Test_2.scala | 4 +- 23 files changed, 220 insertions(+), 196 deletions(-) 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 abe4fa4bde77..452da11efa34 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -31,6 +31,13 @@ private val constrainRootsWhenMapping = true def allowUniversalInBoxed(using Context) = Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) +/** 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) + /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type) extends Exception(tpe.toString) @@ -152,6 +159,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 +168,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,6 +179,18 @@ extension (tp: Type) case _ => tp + /** 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 + /** 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 @@ -366,6 +386,7 @@ extension (sym: Symbol) 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 @@ -375,8 +396,11 @@ extension (sym: Symbol) symd.is(CaptureChecked) || symd.isRoot else symd.is(Method, butNot = Accessor) - && (!symd.owner.isClass || symd.owner.is(CaptureChecked)) - && !Synthetics.isExcluded(sym) + && (!symd.owner.isClass + || symd.owner.is(CaptureChecked) + || Synthetics.needsTransform(symd) + ) + //&& !Synthetics.isExcluded(sym) && !isCaseClassSynthetic && !symd.isConstructor && (!symd.isAnonymousFunction || sym.definedLocalRoot.exists) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 3cde5cae981a..e93521fe0072 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 @@ -62,8 +62,7 @@ sealed abstract class CaptureSet extends Showable: def owner: Symbol def rootSet(using Context): CaptureSet = - if Setup.newScheme then owner.localRoot.termRef.singletonCaptureSet - else universal + owner.localRoot.termRef.singletonCaptureSet /** Is this capture set definitely non-empty? */ final def isNotEmpty: Boolean = !elems.isEmpty @@ -142,7 +141,7 @@ sealed abstract class CaptureSet extends Showable: case y: TermRef => (y.prefix eq x) || x.isRootIncluding(y) case y: CaptureRoot.Var => x.isRootIncluding(y) case _ => false - || (x.isGenericRootCapability || y.isGenericRootCapability && x.isRootCapability) + || (x.isGenericRootCapability || y.isRootCapability && x.isRootCapability) && ctx.property(LooseRootChecking).isDefined private def isRootIncluding(y: CaptureRoot) = @@ -504,9 +503,11 @@ object CaptureSet: private def recordLevelError()(using Context): Unit = for elem <- triedElem do + capt.println(i"level error for $elem, $this") ccState.levelError = Some((elem, this)) private def levelOK(elem: CaptureRef)(using Context): Boolean = elem match + case elem: TermRef if elem.symbol.isLevelOwner => elem.ccNestingLevel - 1 <= ownLevel case elem: (TermRef | ThisType) => elem.ccNestingLevel <= ownLevel case elem: CaptureRoot.Var => CaptureRoot.isEnclosingRoot(elem, owner.localRoot.termRef) case _ => true @@ -965,10 +966,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 = diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c0089bd99f9d..a5bbf0555dfc 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} @@ -193,7 +194,6 @@ class CheckCaptures extends Recheck, SymTransformer: val ccState = new CCState class CaptureChecker(ictx: Context) extends Rechecker(ictx): - import ast.tpd.* override def keepType(tree: Tree) = super.keepType(tree) @@ -273,13 +273,19 @@ class CheckCaptures extends Recheck, SymTransformer: 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.skipConstructor.owner) else CaptureSet.empty) /** For all nested environments up to `limit` or a closed environment perform `op`, @@ -342,14 +348,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) - 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) - super.recheckIdent(tree) + instantiateLocalRoots(tree.symbol, pt): + super.recheckIdent(tree, pt) /** A specialized implementation of the selection rule. * @@ -378,30 +385,38 @@ 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 - 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 + instantiateLocalRoots(tree.symbol, pt): + 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, pt: Type)(tp: Type)(using Context): Type = + def canInstantiate = + sym.is(Method, butNot = Accessor) + || sym.isTerm && defn.isFunctionType(sym.info) && pt == AnySelectionProto + if sym.skipConstructor.isLevelOwner && canInstantiate then + val tpw = tp.widen + val tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner, sym))(tpw) + .showing(i"INST $sym: $tp, ${sym.localRoot} = $result", ccSetup) + if tpw eq tp1 then tp else tp1 + else + tp /** A specialized implementation of the apply rule. * @@ -431,7 +446,7 @@ class CheckCaptures extends Recheck, SymTransformer: 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)) @@ -591,7 +606,10 @@ class CheckCaptures extends Recheck, SymTransformer: curEnv = Env(sym, EnvKind.Regular, localSet, curEnv) 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 = @@ -624,7 +642,7 @@ class CheckCaptures extends Recheck, SymTransformer: tree.tpt match case tpt: InferredTypeTree if !canUseInferred => val expected = tpt.tpe.dropAllRetains - checkConformsExpr(tp, expected, tree.rhs, addenda(expected)) + todoAtPostCheck += (() => checkConformsExpr(tp, expected, tree.rhs, addenda(expected))) case _ => tp end checkInferredResult @@ -755,33 +773,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 actualw = actual.widen - val actual1 = mapRoots(defn.captureRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner))(actualw) - (actual1 ne actualw) && { - 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 tree.symbol.isLevelOwner => + // 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.levelOwner))(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), expected, 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 = @@ -792,7 +810,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 @@ -1036,7 +1054,8 @@ class CheckCaptures extends Recheck, SymTransformer: 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) @@ -1217,6 +1236,8 @@ class CheckCaptures extends Recheck, SymTransformer: end check end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) + for chk <- todoAtPostCheck do chk() + if !ctx.reporter.errorsReported then //inContext(ctx.withProperty(LooseRootChecking, Some(()))): // We dont report errors here if previous errors were reported, because other diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 5b8043fdf45d..b76f1abbce84 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -114,22 +114,17 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def transformSym(symd: SymDenotation)(using Context): SymDenotation = if !pastRecheck && Feature.ccEnabledSomewhere then val sym = symd.symbol - val needsInfoTransform = sym.isDefinedInCurrentRun && !toBeUpdated.contains(sym) - // if sym is class && level owner: add a capture root - // translate cap/capIn to local roots - // create local roots if necessary + def mappedInfo = + if toBeUpdated.contains(sym) then symd.info + else transformExplicitType(symd.info, rootTarget = sym) + // TODO if sym is class && level owner: add a capture root if Synthetics.needsTransform(symd) then - Synthetics.transform(symd) + 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 = - if needsInfoTransform then - atPhase(thisPhase.next)(symd.maybeOwner.info) // ensure owner is completed - transformExplicitType(sym.info, rootTarget = if newScheme && false then sym else NoSymbol) - else sym.info - + val newInfo = mappedInfo if newFlags != symd.flags || (newInfo ne sym.info) then symd.copySymDenotation(initFlags = newFlags, info = newInfo) else symd @@ -137,15 +132,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else symd end transformSym - /** Create dependent function with underlying function class `tycon` and given - * arguments `argTypes` and result `resType`. - */ - 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) - /** If `tp` is an unboxed capturing type or a function returning an unboxed capturing type, * convert it to be boxed. */ @@ -237,7 +223,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val args1 = mapNested(args0) val res1 = this(res0) if isTopLevel then - depFun(tycon1, args1, res1) + 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 @@ -466,27 +453,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val sym = tree.symbol val defCtx = if sym.isOneOf(TermParamOrAccessor) then ctx else ctx.withOwner(sym) inContext(defCtx): - val rootsNeedMap = - // need to run before cc so that rhsClosure is defined without transforming - // symbols to cc, since rhsClosure is used in that transformation. - tree.rhs match - case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => - //ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - if newScheme && false then ccState.isLevelOwner(sym) = true - ccState.isLevelOwner(ddef.symbol) = true - // 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. - (newScheme && false) || !tpt.isInstanceOf[InferredTypeTree] - // in this case roots in inferred val type count as polymorphic - case _ => - true + tree.rhs match + case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => + if !sym.is(Mutable) then ccState.isLevelOwner(sym) = true + // TODO Drop the possibleyTypedClosureDef condition anc replace by the + // condition that the val takes a cap parameter + case _ => 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 - rootTarget = if rootsNeedMap then ctx.owner else NoSymbol + rootTarget = ctx.owner ) ccSetup.println(i"mapped $tree = ${tpt.knownType}") traverse(tree.rhs) @@ -555,10 +531,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 = - val mapr = - if !(newScheme && false) && sym.isLevelOwner - then mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) - else identity[Type] info match case mt: MethodOrPoly => val psyms = psymss.head @@ -568,17 +540,16 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: mt.paramInfos else val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => mapr(subst(psym.nextInfo)).asInstanceOf[mt.PInfo]), + psyms.map(psym => subst(psym.nextInfo).asInstanceOf[mt.PInfo]), mt1 => - mapr(integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas)) + integrateRT(mt.resType, psymss.tail, resType, psyms :: prevPsymss, mt1 :: prevLambdas) ) case info: ExprType => info.derivedExprType(resType = - mapr(integrateRT(info.resType, psymss, resType, prevPsymss, prevLambdas))) + integrateRT(info.resType, psymss, resType, prevPsymss, prevLambdas)) case info => - mapr( - if prevLambdas.isEmpty then resType - else SubstParams(prevPsymss, prevLambdas)(resType)) + if prevLambdas.isEmpty then resType + else SubstParams(prevPsymss, prevLambdas)(resType) if sym.exists && signatureChanges then def absInfo(resType: Type): Type = @@ -597,7 +568,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // are checked on demand assert(!isDuringSetup, i"$sym") assert(ctx.phase == thisPhase.next, i"$sym") - ccSetup.println(i"FORCING $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)) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index f59ac80ef1b5..7932a29fea1b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -59,9 +59,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: @@ -99,7 +101,7 @@ object Synthetics: info.derivedLambdaType(resType = transformDefaultGetterCaptures(info.resType, owner, idx)) case info: ExprType => info.derivedExprType(transformDefaultGetterCaptures(info.resType, owner, idx)) - case RetainingType(parent, _) => + case CapturingType(parent, _) => transformDefaultGetterCaptures(parent, owner, idx) case info @ AnnotatedType(parent, annot) => info.derivedAnnotatedType(transformDefaultGetterCaptures(parent, owner, idx), annot) @@ -117,7 +119,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 +131,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 +151,19 @@ 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) + transformUnapplyCaptures(info) case nme.apply | nme.copy => - addCaptureDeps(sym.info) + addCaptureDeps(info) case nme.andThen | nme.compose => - transformComposeCaptures(sym) + 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/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 97277eeb6600..84167fbeb7f2 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. @@ -542,7 +542,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling 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) + then + val tp2a = + if tp1.isBoxedCapturing && !parent1.isBoxedCapturing + then tp2.unboxed + else tp2 + recur(parent1, tp2a) else thirdTry case tp1: AnnotatedType if !tp1.isRefining => recur(tp1.parent, tp2) @@ -995,7 +1000,10 @@ 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 (ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev) + && tp1.isTracked + => CapturingType(tp1w.stripCapturing, tp1.singletonCaptureSet) case _ => tp1w diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index cef6ddabd468..8fbfddf3581c 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -207,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 = @@ -295,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 = @@ -476,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 => diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 781b26b6944d..67f42a4acc69 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -2,7 +2,7 @@ 10 | h(f2()) // error | ^^^^ | Found: (x$0: Int) ->{cap1} Int - | Required: (x$0: Int) ->{cap2} Int + | Required: Int ->{cap2} Int | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- 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/i15772.check b/tests/neg-custom-args/captures/i15772.check index 22c95f8a8ed5..0ec4fef1c46d 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -25,8 +25,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:33 --------------------------------------- 33 | val boxed2 : Observe[C]^ = box2(c) // error | ^^^^^^^ - |Found: (C{val arg: C^{cap[C]}}^{'cap[..main3](from instantiating box2)} ->{'cap[..main3](from instantiating box2)} Unit) ->? - | Unit + |Found: (C{val arg: C^{cap[C]}}^{'cap[..main3](from instantiating c)} ->{'cap[..main3](from instantiating box2)} Unit) ->? Unit |Required: (C ->{cap[main3]} Unit) ->{cap[main3]} Unit | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 4b7611fc3fb7..ba5080d808f8 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -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]^ has incompatible type + | method tail of type -> lazylists.LazyList[Nothing]^{cap[tail]} has incompatible type | | 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 1918fecbc496..a3665b50b6f6 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -2,10 +2,3 @@ 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^{cap[Foo]} has incompatible type - | - | longer explanation available when compiling with `-explain` 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/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index 5fd9b417e260..eb24f76d748c 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -1,16 +1,3 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/usingLogFile.scala:23:27 --------------------------------- -23 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: () ->{x$0, local} Unit - | Required: () -> Unit - | - | Note that the expected type () -> Unit - | is the previously inferred type of value later - | which is also the type seen in separately compiled sources. - | The new inferred type box () ->{x$0, local} Unit - | must conform to this type. - | - | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/usingLogFile.scala:32:37 ------------------------------------------------------ 32 | usingLogFile { f => later3 = () => f.write(0) } // error | ^ @@ -23,24 +10,26 @@ | Required: Test2.Cell[box () ->{cap[]} Unit] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/usingLogFile.scala:47:27 --------------------------------- -47 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: () ->{x$0, local} Unit - | Required: () -> Unit - | - | Note that the expected type () -> Unit - | is the previously inferred type of value later - | which is also the type seen in separately compiled sources. - | The new inferred type box (() ->{x$0, local} Unit)^? - | must conform to this type. - | - | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ +23 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^ + | escaping local reference local.type -- Error: tests/neg-custom-args/captures/usingLogFile.scala:28:23 ------------------------------------------------------ 28 | private val later2 = usingLogFile { f => Cell(() => f.write(0)) } // error | ^^^^^^^^^^^^ | escaping local reference local.type +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:14 ------------------------------------------------------ +47 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^ + | escaping local reference local.type -- Error: tests/neg-custom-args/captures/usingLogFile.scala:62:16 ------------------------------------------------------ 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 at level 2 + | cannot be included in outer capture set {x$0, local}, defined at level 1 in method test 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 74000c79a212..4452a3b2da6b 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -14,7 +14,7 @@ 24 | a = g // error | ^ | Found: box (x: String) ->{cap3} String - | Required: box (x$0: String) ->{cap[test]} String + | Required: box String ->{cap[test]} String | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:26:12 ----------------------------------------- diff --git a/tests/pos-custom-args/captures/capt2.scala b/tests/pos-custom-args/captures/capt2.scala index 45381bf602ed..1df2ea69b119 100644 --- a/tests/pos-custom-args/captures/capt2.scala +++ b/tests/pos-custom-args/captures/capt2.scala @@ -13,7 +13,7 @@ def test2() = z: (() -> Unit) @retains(x) def z2: (() -> Unit) @retains(y) = y z2: (() -> Unit) @retains(y) - val p: () => String = () => "abc" + val p: () ->{cap[test2]} String = () => "abc" val q: C^{p} = ??? val _ = p: (() ->{p} String) 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/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-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..26b2faee1165 100644 --- a/tests/pos-special/stdlib/Test2.scala +++ b/tests/pos-special/stdlib/Test2.scala @@ -8,7 +8,7 @@ object Test { def seqOps(xs: Seq[Int]) = { // try with Seq[Int]^{cap} val strPlusInt: (String, Int) => String = _ + _ val intPlusStr: (Int, String) => String = _ + _ - val isEven: Int => Boolean = _ % 2 == 0 + val isEven: Int ->{cap[seqOps]} Boolean = _ % 2 == 0 val isNonNeg: Int => Boolean = _ > 0 val flips: Int => List[Int] = x => x :: -x :: Nil val x1 = xs.foldLeft("")(strPlusInt) @@ -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/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index fbb22039c327..64e8d4a88cf3 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -8,7 +8,7 @@ object Test { def seqOps(xs: Seq[Int]) = { // try with Seq[Int]^{cap} val strPlusInt: (String, Int) => String = _ + _ val intPlusStr: (Int, String) => String = _ + _ - val isEven: Int => Boolean = _ % 2 == 0 + val isEven: Int ->{cap[seqOps]} Boolean = _ % 2 == 0 val isNonNeg: Int => Boolean = _ > 0 val flips: Int => List[Int] = x => Cons(x, Cons(-x, Nil)) val x1 = xs.foldLeft("")(strPlusInt) @@ -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) From ed8aae32a2fd0b1e7af01f3518f767a44c4f70ba Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 23 Sep 2023 13:39:12 +0200 Subject: [PATCH 21/58] Refactor wellformed checks Innstead of traversing old types at postCheck, note what needs to be done when these types are first transformed at Setup. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 49 +------------ compiler/src/dotty/tools/dotc/cc/Setup.scala | 69 +++++++++++++++++-- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a5bbf0555dfc..6b22e062725a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -132,45 +132,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. - * 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 checkWellformedPost(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = - 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) - 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 - /** Attachment key for bodies of closures, provided they are values */ val ClosureBodyValue = Property.Key[Unit] @@ -1059,10 +1020,9 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) - private var setup: SetupAPI = compiletime.uninitialized + private val setup: SetupAPI = thisPhase.prev.asInstanceOf[Setup] override def checkUnit(unit: CompilationUnit)(using Context): Unit = - setup = thisPhase.prev.asInstanceOf[Setup] setup.setupUnit(ctx.compilationUnit.tpdTree, recheckDef) if ctx.settings.YccPrintSetup.value then @@ -1215,12 +1175,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.tpe.foreachPart: - case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => - checkWellformedPost(parent, annot.tree, tree) - case _ => case t @ TypeApply(fun, args) => fun.knownType.widen match case tl: PolyType => @@ -1237,6 +1191,7 @@ class CheckCaptures extends Recheck, SymTransformer: 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(()))): diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index b76f1abbce84..bab0f281655d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -22,15 +22,14 @@ 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 - //|| ctx.phase == Phases.checkCapturesPhase.prev + def isDuringSetup(using Context): Boolean = ctx.property(IsDuringSetupKey).isDefined case class Box(t: Type) extends UncachedGroundType, TermType: override def fallbackToText(printer: Printer): Text = @@ -257,7 +256,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: private def transformInferredType(tp: Type, rootTarget: Symbol)(using Context): Type = mapInferred(rootTarget)(tp) - private def transformExplicitType(tp: Type, rootTarget: Symbol)(using Context): Type = + private def transformExplicitType(tp: Type, rootTarget: Symbol, tptToCheck: Option[Tree] = None)(using Context): Type = val expandAliases = new TypeMap: override def toString = "expand aliases" @@ -339,8 +338,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: t.derivedCapturingType(this(parent), refs) case t @ AnnotatedType(parent, ann) => if ann.symbol == defn.RetainsAnnot then - checkQualifiedRoots(ann.tree) - CapturingType(this(parent), ann.tree.toCaptureSet) + 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) => @@ -371,7 +373,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tree.rememberType( if tree.isInstanceOf[InferredTypeTree] && !exact then transformInferredType(tp, rootTarget) - else transformExplicitType(tp, rootTarget)) + 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. @@ -725,4 +727,57 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: setupTraverser(recheckDef).traverse(tree)( using ctx.withPhase(thisPhase).withProperty(IsDuringSetupKey, Some(()))) + // ------ Checks to run after main capture checking -------------------------- + + /** A list of actions to perform at postCheck */ + private val todoAtPostCheck = new mutable.ListBuffer[Context => Unit] + + /** 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) + 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 From c739ff29a4d9610375b8ffa75fe2a1eb46c6cc52 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 24 Sep 2023 00:02:41 +0200 Subject: [PATCH 22/58] Drop boxedUnlessFun and boxedIfTypeParam operations With the new way to construct capturing types at Setup, we can add the correct boxing annotations there, so the previous unmodular hack can be dropped. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 24 ------------------- .../dotty/tools/dotc/cc/CapturingType.scala | 12 ++++++++++ .../dotty/tools/dotc/cc/RetainingType.scala | 7 +++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 16 +++++++++---- .../tools/dotc/core/TypeApplications.scala | 5 ++-- .../dotty/tools/dotc/core/TypeComparer.scala | 8 +++---- .../src/dotty/tools/dotc/core/Types.scala | 10 ++++---- tests/neg-custom-args/captures/vars.check | 8 +++---- 8 files changed, 43 insertions(+), 47 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 452da11efa34..3e44f5fe4781 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -191,18 +191,6 @@ 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) = // TODO: drop - if ctx.phase != Phases.checkCapturesPhase - && ctx.phase != Phases.checkCapturesPhase.prev - || defn.isFunctionSymbol(tycon.typeSymbol) - then tp - else tp.boxed - /** 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 * intersections, and type proxies other than abstract types. @@ -498,15 +486,3 @@ extension (tp: AnnotatedType) 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) = // TODO drop - if ctx.phase != Phases.checkCapturesPhase - && ctx.phase != Phases.checkCapturesPhase.prev - || defn.isFunctionClass(tycon.typeSymbol) - then ts - else ts.mapconserve(_.boxed) - diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index 95ca10cf3674..ae7838662c1d 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. @@ -59,6 +60,17 @@ object CapturingType: Some((parent, ann.refs)) case AnnotatedType(parent, ann) if ann.symbol == defn.RetainsAnnot && ctx.phase == Phases.checkCapturesPhase => + // 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 _ => diff --git a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala index 5af59b193aa3..7902b03445fb 100644 --- a/compiler/src/dotty/tools/dotc/cc/RetainingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/RetainingType.scala @@ -26,9 +26,10 @@ object RetainingType: if sym == defn.RetainsAnnot || sym == defn.RetainsByNameAnnot then tp.annot match case _: CaptureAnnotation => - new Error(i"bad retains $tp").printStackTrace - assert(false) - case ann => Some((tp.parent, ann.tree.retainedElems)) + 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 bab0f281655d..3f450d0ab5e2 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -257,7 +257,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: mapInferred(rootTarget)(tp) private def transformExplicitType(tp: Type, rootTarget: Symbol, tptToCheck: Option[Tree] = None)(using Context): Type = - val expandAliases = new TypeMap: + val expandAliases = new DeepTypeMap: override def toString = "expand aliases" /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib @@ -478,6 +478,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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) @@ -491,7 +495,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree: TypeTree => val lowner = transformTT(tree, boxed = false, exact = false, - rootTarget = ctx.owner.unlessStatic // other types in static locations are not boxed + rootTarget = ctx.owner.unlessStatic // roots of other types in static locations are not mapped ) case tree: ValOrDefDef => val sym = tree.symbol @@ -592,13 +596,15 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(cinfo.selfType, CaptureSet.Var(cls)) else selfInfo match case selfInfo: Type => - inContext(ctx.withOwner(cls)): + val selfInfo1 = inContext(ctx.withOwner(cls)): transformExplicitType(selfInfo, rootTarget = ctx.owner) + if selfInfo1 eq selfInfo then NoType else selfInfo1 case _ => NoType if newSelfType.exists then ccSetup.println(i"mapped self type for $cls: $newSelfType, was $selfInfo") - val newInfo = ClassInfo(prefix, cls, ps, decls, newSelfType) + val ps1 = ps.mapConserve(transformExplicitType(_, rootTarget = ctx.owner)) + val newInfo = ClassInfo(prefix, cls, ps1, decls, newSelfType) updateInfo(cls, newInfo) cls.thisType.asInstanceOf[ThisType].invalidateCaches() if cls.is(ModuleClass) then @@ -691,6 +697,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 _ => 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 84167fbeb7f2..62559a037703 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -579,7 +579,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 @@ -913,7 +913,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) @@ -947,7 +947,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 _ => @@ -1800,7 +1800,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) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 736c570c80d0..fc6d6027e6aa 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, RetainingType, boxedUnlessFun, ccNestingLevel} +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, RetainingType, ccNestingLevel} 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 @@ -2708,7 +2708,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 diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 4452a3b2da6b..98991fdf9857 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -17,11 +17,11 @@ | Required: box String ->{cap[test]} String | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:26:12 ----------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:26:13 ----------------------------------------- 26 | b = List(g) // error - | ^^^^^^^ - | Found: List[box (x$0: String) ->{cap3} String] - | Required: List[box String ->{cap[test]} String] + | ^ + | Found: Seq[(x: String) ->{cap3} String] + | Required: Seq[box (x$0: String) ->{cap3} String] | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/vars.scala:35:2 --------------------------------------------------------------- From d123608f793ac2f19262113fe2db7656a07a853d Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 25 Sep 2023 21:34:54 +0200 Subject: [PATCH 23/58] Fix followAlias --- compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala | 2 +- tests/neg-custom-args/captures/i15772.check | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 4e1241518963..0651d5699892 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -33,7 +33,7 @@ object CaptureRoot: def followAlias: CaptureRoot = alias match case alias: Var if alias ne this => alias.followAlias - case _ => this + case _ => alias def locallyConsistent = lowerLevel <= upperLevel diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 0ec4fef1c46d..c62dae2f7627 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -6,8 +6,8 @@ -- [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^{cap[main1]} ->{cap[main1]} Unit) -> Unit + | Found: (C{val arg: C^{cap[C]}}^{c} ->{cap[main1], c} Unit) ->{c} Unit + | Required: (C^{cap[main1]} ->{cap[main1]} Unit) -> Unit | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/i15772.scala:26:26 ------------------------------------------------------------ @@ -18,15 +18,15 @@ -- [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^{cap[main2]} ->{cap[main2]} Unit) -> Unit + | Found: (C{val arg: C^{cap[C]}}^{c} ->{cap[main2], c} Unit) ->{c} Unit + | Required: (C^{cap[main2]} ->{cap[main2]} Unit) -> Unit | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:33 --------------------------------------- 33 | val boxed2 : Observe[C]^ = box2(c) // error | ^^^^^^^ - |Found: (C{val arg: C^{cap[C]}}^{'cap[..main3](from instantiating c)} ->{'cap[..main3](from instantiating box2)} Unit) ->? Unit - |Required: (C ->{cap[main3]} Unit) ->{cap[main3]} Unit + | Found: (C{val arg: C^{cap[C]}}^{'cap[..main3](from instantiating c)} ->{cap[main3]} Unit) ->? Unit + | Required: (C ->{cap[main3]} Unit) ->{cap[main3]} Unit | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- From 4a53ed5d044540d77da900c2624ba3efb644c497 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 26 Sep 2023 15:06:47 +0200 Subject: [PATCH 24/58] Also interpolate root variables in non-variable capture sets. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 40 ++++++++----------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 21 +++++----- tests/pos-custom-args/captures/vars.scala | 2 +- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6b22e062725a..6adbca05e4f7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -165,30 +165,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.upperBound == ctx.owner.levelOwner => + rv.setAlias(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. diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 3f450d0ab5e2..d17447221b3f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -426,6 +426,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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, _) => @@ -435,9 +441,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: inContext(ctx.withOwner(meth)): paramss.foreach(traverse) - transformTT(tpt, boxed = false, - exact = tree.symbol.allOverriddenSymbols.hasNext, - rootTarget = ctx.owner) + transformResultType(tpt, meth) traverse(tree.rhs) //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") @@ -461,11 +465,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // TODO Drop the possibleyTypedClosureDef condition anc replace by the // condition that the val takes a cap parameter case _ => - 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 - rootTarget = ctx.owner - ) + transformResultType(tpt, sym) ccSetup.println(i"mapped $tree = ${tpt.knownType}") traverse(tree.rhs) @@ -563,7 +563,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: .showing(i"update info $sym: ${sym.info} = $result", ccSetup) val newInfo = absInfo(localReturnType) if newInfo ne sym.info then - updateInfo(sym, + val updatedInfo = if sym.isAnonymousFunction || sym.is(Param) || sym.is(ParamAccessor) then // closures are handled specially; the newInfo is constrained from // the expected type and only afterwards we recheck the definition @@ -577,7 +577,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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)) + recheckDef(tree, sym) + updateInfo(sym, updatedInfo) case tree: Bind => val sym = tree.symbol 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 From 5dcb064f60da98b2b82f1cab13d4618054382b5f Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Sep 2023 13:47:43 +0200 Subject: [PATCH 25/58] Refactor addNewElems Break out operation to add a single element. This makes widenCaptures redundant. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 59 ++++++++----------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 29 ++++----- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index e93521fe0072..7b1280fd905a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -101,7 +101,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 @@ -314,8 +319,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 @@ -391,7 +396,7 @@ object CaptureSet: def isConst = true def isAlwaysEmpty = elems.isEmpty - def addNewElems(elems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + def addNewElem(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = CompareResult.fail(this) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -413,7 +418,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 = "" @@ -452,7 +457,7 @@ 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 = "" @@ -482,23 +487,26 @@ 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() + else if !levelOK(elem) then + val saved = triedElem + triedElem = triedElem.orElse(Some(elem)) + val res = + if elem.isRootCapability then CompareResult.fail(this) + else addNewElems(elem.captureSetOfInfo.elems, origin) + if res.isOK then triedElem = saved // reset only in case of success, leave as is on error + else recordLevelError() res else //assert(id != 2, newElems) - elems ++= newElems - if isUniversal then rootAddedHandler() - newElemAddedHandler(newElems.toList) + 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)) + r.andAlso(dep.tryInclude(elem, this)) } private def recordLevelError()(using Context): Unit = @@ -512,23 +520,6 @@ object CaptureSet: 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 - def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult = if (cs eq this) || cs.isUniversal || isConst then CompareResult.OK @@ -542,7 +533,7 @@ object CaptureSet: 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) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6adbca05e4f7..e5d505a6578b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1118,22 +1118,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 From db764e00e0eb34e0d10d42f85739fa992be0d966 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Sep 2023 18:17:30 +0200 Subject: [PATCH 26/58] Refactor level error reporting Avoid the mutable state in CapturSet.Var --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 57 +++++++++++-------- .../captures/lazylists-exceptions.check | 3 - tests/neg-custom-args/captures/real-try.check | 6 -- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 3e44f5fe4781..12d344c28c12 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -56,7 +56,7 @@ 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.LevelError] = None /** Under saferExceptions: The symbol generated for a try. * Installed by Setup, removed by CheckCaptures. diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7b1280fd905a..cc8fab8b87df 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -205,7 +205,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 @@ -461,8 +462,6 @@ object CaptureSet: 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. */ @@ -491,14 +490,9 @@ object CaptureSet: if isConst || !recordElemsState() then CompareResult.fail(this) // fail if variable is solved or given VarState is frozen else if !levelOK(elem) then - val saved = triedElem - triedElem = triedElem.orElse(Some(elem)) - val res = - if elem.isRootCapability then CompareResult.fail(this) - else addNewElems(elem.captureSetOfInfo.elems, origin) - if res.isOK then triedElem = saved // reset only in case of success, leave as is on error - else recordLevelError() - res + 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 += elem @@ -509,11 +503,6 @@ object CaptureSet: r.andAlso(dep.tryInclude(elem, this)) } - private def recordLevelError()(using Context): Unit = - for elem <- triedElem do - capt.println(i"level error for $elem, $this") - ccState.levelError = Some((elem, this)) - private def levelOK(elem: CaptureRef)(using Context): Boolean = elem match case elem: TermRef if elem.symbol.isLevelOwner => elem.ccNestingLevel - 1 <= ownLevel case elem: (TermRef | ThisType) => elem.ccNestingLevel <= ownLevel @@ -853,23 +842,44 @@ object CaptureSet: type CompareResult = CompareResult.TYPE + case class LevelError(cs: CaptureSet, elem: CaptureRef) + /** 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) + opaque type TYPE = + Unit // Success + | CaptureSet // Failure with blocking set + | LevelError // Failure due to level error + val OK: TYPE = () def fail(cs: CaptureSet): TYPE = cs + def levelError(cs: CaptureSet, ref: CaptureRef): TYPE = LevelError(cs, ref) extension (result: TYPE) /** The result is OK */ - def isOK: Boolean = result eq OK + def isOK: Boolean = result == () /** 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" + def blocking: CaptureSet = (result: @unchecked) match + case cs: CaptureSet => cs + case LevelError(cs, _) => cs + def levelError: Option[LevelError] = result match + case result: LevelError => Some(result) + case _ => None + inline def andAlso(op: Context ?=> TYPE)(using Context): TYPE = + if result.isOK then op else result + inline def orElse(op: Context ?=> TYPE)(using Context): TYPE = + if result.isOK then result + else + val alt = op + if alt.isOK then alt + else result + def show(using Context): String = result match + case () => "OK" + case cs: CaptureSet => cs.show + case LevelError(cs, ref) => i"($ref at wrong level for $cs)" end CompareResult /** A VarState serves as a snapshot mechanism that can undo @@ -1044,7 +1054,8 @@ object CaptureSet: def levelErrors: Addenda = new Addenda: override def toAdd(using Context) = - for (ref, cs) <- ccState.levelError.toList yield + for 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 _ => "" diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 57e5e65cd374..702f8e6260b9 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -3,9 +3,6 @@ | ^ | 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/real-try.check b/tests/neg-custom-args/captures/real-try.check index eee1b8c456cc..f969404cde73 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -10,9 +10,6 @@ | 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 @@ -20,9 +17,6 @@ | 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` -- Error: tests/neg-custom-args/captures/real-try.scala:31:21 ---------------------------------------------------------- 31 | Cell(() => foo(1))// // error From 96d85bc0ef9f236bd58964e6a209f920267d3d08 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Sep 2023 11:18:01 +0200 Subject: [PATCH 27/58] Make CompareResult an enum # Conflicts: # compiler/src/dotty/tools/dotc/cc/Setup.scala --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 91 +++++++++---------- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 12d344c28c12..f10ccdac4113 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -56,7 +56,7 @@ 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[CaptureSet.LevelError] = None + var levelError: Option[CaptureSet.CompareResult.LevelError] = None /** Under saferExceptions: The symbol generated for a try. * Installed by Setup, removed by CheckCaptures. diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index cc8fab8b87df..6cb555ef3f88 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -398,7 +398,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addNewElem(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = - CompareResult.fail(this) + CompareResult.Fail(this) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -488,9 +488,9 @@ object CaptureSet: 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 + CompareResult.Fail(this) // fail if variable is solved or given VarState is frozen else if !levelOK(elem) then - val res = CompareResult.levelError(this, elem) + val res = CompareResult.LevelError(this, elem) if elem.isRootCapability then res else res.orElse(addNewElems(elem.captureSetOfInfo.elems, origin)) else @@ -516,7 +516,7 @@ object CaptureSet: deps += cs CompareResult.OK else - CompareResult.fail(this) + CompareResult.Fail(this) override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = rootAddedHandler = handler @@ -660,7 +660,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) } .andAlso { if (origin ne source) && (origin ne initial) && mapIsIdempotent then @@ -677,7 +677,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) else CompareResult.OK } @@ -743,7 +743,7 @@ 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) } override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = @@ -840,46 +840,41 @@ object CaptureSet: /** A TypeMap that is the identity on capture references */ trait IdentityCaptRefMap extends TypeMap - type CompareResult = CompareResult.TYPE - - case class LevelError(cs: CaptureSet, elem: CaptureRef) - - /** 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 = - Unit // Success - | CaptureSet // Failure with blocking set - | LevelError // Failure due to level error - val OK: TYPE = () - def fail(cs: CaptureSet): TYPE = cs - def levelError(cs: CaptureSet, ref: CaptureRef): TYPE = LevelError(cs, ref) - - extension (result: TYPE) - /** The result is OK */ - def isOK: Boolean = result == () - /** If not isOK, the blocking capture set */ - def blocking: CaptureSet = (result: @unchecked) match - case cs: CaptureSet => cs - case LevelError(cs, _) => cs - def levelError: Option[LevelError] = result match - case result: LevelError => Some(result) - case _ => None - inline def andAlso(op: Context ?=> TYPE)(using Context): TYPE = - if result.isOK then op else result - inline def orElse(op: Context ?=> TYPE)(using Context): TYPE = - if result.isOK then result - else - val alt = op - if alt.isOK then alt - else result - def show(using Context): String = result match - case () => "OK" - case cs: CaptureSet => cs.show - case LevelError(cs, ref) => i"($ref at wrong level for $cs)" + enum CompareResult extends Showable: + case OK + case Fail(cs: CaptureSet) + case LevelError(cs: CaptureSet, elem: CaptureRef) + + override def toText(printer: Printer): Text = + inContext(printer.printerContext): + this match + case OK => Str("OK") + case Fail(blocking: CaptureSet) => blocking.show + case LevelError(cs: CaptureSet, elem: CaptureRef) => + Str(i"($elem at wrong level for $cs in ${cs.owner})") + + /** 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 + 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 end CompareResult /** A VarState serves as a snapshot mechanism that can undo @@ -1054,7 +1049,7 @@ object CaptureSet: def levelErrors: Addenda = new Addenda: override def toAdd(using Context) = - for LevelError(cs, ref) <- ccState.levelError.toList 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}" From 50201175648320bd7b829197fd153d19aa509a12 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Sep 2023 11:54:21 +0200 Subject: [PATCH 28/58] Add takesCappedParam criterion to isLevelOwner (Configurable) --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 83 ++++++++++++------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 14 ++-- .../src/dotty/tools/dotc/cc/Synthetics.scala | 2 + 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f10ccdac4113..9b7f216f546c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -28,6 +28,16 @@ private val adaptUnpickledFunctionTypes = false */ private val constrainRootsWhenMapping = true +/** If true, most vals can be level owners. If false, only vals defined by a + * closure as RHS can be level owners + */ +private val valsCanBeLevelOwners = false + +/** If true, only vals, defs, and classes with a universal capability in a parameter + * or self type are considered as level owners. + */ +private val levelOwnersNeedCapParam = false + def allowUniversalInBoxed(using Context) = Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) @@ -345,54 +355,71 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox - // Not yet needed - 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 _ => annot.tree.retainedElems.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 + def takesCappedParamIn(info: Type)(using Context): Boolean = + + def isOwnRoot(tp: Type): Boolean = + tp.isCapabilityClassRef + || tp.dealias.match + case tp: CaptureRef => + tp.isGenericRootCapability || tp.localRootOwner == sym + case _ => + false + + def hasUniversalCap(tp: Type): Boolean = 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 || hasUniversalCap(parent) + case tp: TypeRef => + tp.isRef(defn.Caps_Cap) || tp.isCapabilityClassRef + case tp: LazyRef => + hasUniversalCap(tp.ref) + case tp: TypeVar => + hasUniversalCap(tp.underlying) + case _ => + tp.isCapabilityClassRef + + info.dealias.stripPoly match case mt: MethodType => - mt.paramInfos.exists(search(false, _)) + (mt.paramInfos.exists(hasUniversalCap) || takesCappedParamIn(mt.resType)) + //.showing(i"takes capped param1 $sym: $mt = $result") + case AppliedType(fn, args) if defn.isFunctionClass(fn.typeSymbol) => + args.init.exists(hasUniversalCap) || takesCappedParamIn(args.last) + case defn.RefinedFunctionOf(rinfo) => + takesCappedParamIn(rinfo) + //.showing(i"takes capped param2 $sym: $rinfo = $result") case _ => false + end takesCappedParamIn // 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 = + !levelOwnersNeedCapParam || takesCappedParamIn(symd.primaryConstructor.info) + def termQualifies = + !levelOwnersNeedCapParam || takesCappedParamIn(symd.info) def compute = if symd.isClass then - symd.is(CaptureChecked) || symd.isRoot + symd.is(CaptureChecked) && classQualifies || symd.isRoot else - symd.is(Method, butNot = Accessor) + (symd.is(Method, butNot = Accessor) + || valsCanBeLevelOwners && symd.isTerm && !symd.isOneOf(TermParamOrAccessor | Mutable)) && (!symd.owner.isClass || symd.owner.is(CaptureChecked) || Synthetics.needsTransform(symd) ) - //&& !Synthetics.isExcluded(sym) && !isCaseClassSynthetic && !symd.isConstructor && (!symd.isAnonymousFunction || sym.definedLocalRoot.exists) + && termQualifies + && { 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index e5d505a6578b..ecf15804a340 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -741,7 +741,7 @@ class CheckCaptures extends Recheck, SymTransformer: //println(i"check conforms $actualBoxed <<< $expected1") var ok = isCompatible(actualBoxed, expected1) if !ok then stripTyped(tree) match - case tree: RefTree if tree.symbol.isLevelOwner => + 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`. diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index d17447221b3f..54497a49c978 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -460,10 +460,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val defCtx = if sym.isOneOf(TermParamOrAccessor) then ctx else ctx.withOwner(sym) inContext(defCtx): tree.rhs match - case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => - if !sym.is(Mutable) then ccState.isLevelOwner(sym) = true - // TODO Drop the possibleyTypedClosureDef condition anc replace by the - // condition that the val takes a cap parameter + case possiblyTypedClosureDef(ddef) + if !mentionsCap(rhsOfEtaExpansion(ddef)) + && !sym.is(Mutable) + && (!levelOwnersNeedCapParam + || 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}") @@ -772,7 +775,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if ref.captureSetOfInfo.elems.isEmpty then report.error(em"$ref cannot be tracked since its capture set is empty", pos) - check(parent.captureSet, parent) + 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 diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 7932a29fea1b..4b20533c074e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -157,10 +157,12 @@ object Synthetics: case DefaultGetterName(nme.copy, n) => transformDefaultGetterCaptures(info, symd.owner, n) case nme.unapply => + if levelOwnersNeedCapParam then ccState.isLevelOwner(symd.symbol) = true transformUnapplyCaptures(info) case nme.apply | nme.copy => addCaptureDeps(info) case nme.andThen | nme.compose => + if levelOwnersNeedCapParam then ccState.isLevelOwner(symd.symbol) = true transformComposeCaptures(info, symd.owner) case nme.curried | nme.tupled => transformCurriedTupledCaptures(info, symd.owner) From 0a0eba43586cc979ba39bcf7835ef4fffc485152 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Sep 2023 19:53:06 +0200 Subject: [PATCH 29/58] Decorate inferred type aliases as inferred types --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 23 ++++++++++---------- tests/neg-custom-args/captures/refs.scala | 5 +++-- tests/neg-custom-args/captures/try.check | 14 +++++------- tests/neg-custom-args/captures/try.scala | 6 ++--- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 54497a49c978..d40ad44670eb 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -102,7 +102,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => mapOver(t) if variance > 0 then t1 - else decorate(t1, rootTarget = NoSymbol, addedSet = Function.const(CaptureSet.Fluid)) + 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. @@ -171,7 +171,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ - private def mapInferred(rootTarget: Symbol)(using Context) = new TypeMap: + private def mapInferred(using Context) = new TypeMap: override def toString = "map inferred" /** Refine a possibly applied class type C where the class has tracked parameters @@ -249,12 +249,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: box(this(tp1)) case _ => mapOver(tp) - addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner, rootTarget) + addVar(addCaptureRefinements(normalizeCaptures(tp1)), ctx.owner) end apply end mapInferred - private def transformInferredType(tp: Type, rootTarget: Symbol)(using Context): Type = - mapInferred(rootTarget)(tp) + private def transformInferredType(tp: Type)(using Context): Type = + mapInferred(tp) private def transformExplicitType(tp: Type, rootTarget: Symbol, tptToCheck: Option[Tree] = None)(using Context): Type = val expandAliases = new DeepTypeMap: @@ -372,7 +372,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val tp = if boxed then Box(tree.tpe) else tree.tpe tree.rememberType( if tree.isInstanceOf[InferredTypeTree] && !exact - then transformInferredType(tp, rootTarget) + then transformInferredType(tp) else transformExplicitType(tp, rootTarget, tptToCheck = Some(tree))) /** Substitute parameter symbols in `from` to paramRefs in corresponding @@ -585,7 +585,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree: Bind => val sym = tree.symbol - updateInfo(sym, transformInferredType(sym.info, rootTarget = ctx.owner)) + updateInfo(sym, transformInferredType(sym.info)) case tree: TypeDef => tree.symbol match case cls: ClassSymbol => @@ -711,7 +711,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** 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, rootTarget: Symbol, 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 @@ -721,16 +721,15 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else fallback val dealiased = tp.dealiasKeepAnnots if dealiased ne tp then - val transformed = transformExplicitType(dealiased, rootTarget) - // !!! TODO: We should map roots to root vars here + 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, rootTarget: Symbol)(using Context): Type = - decorate(tp, rootTarget, + 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)) 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/try.check b/tests/neg-custom-args/captures/try.check index feb16d8674ea..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]^{cap[test]} -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 } From a9d5cbd2f8ca3e96f34392306c28f6b23cbcdcd6 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 14:59:38 +0200 Subject: [PATCH 30/58] Setup correct environment when completing definitions --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ecf15804a340..107125d11f45 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -54,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 @@ -220,8 +228,9 @@ 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 @@ -615,7 +624,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) @@ -1014,10 +1024,32 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) + /** 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.setupUnit(ctx.compilationUnit.tpdTree, recheckDef) + setup.setupUnit(ctx.compilationUnit.tpdTree, completeDef) if ctx.settings.YccPrintSetup.value then val echoHeader = "[[syntax tree at end of cc setup]]" From 80348c0f497111215455a6617207fae2548f4057 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 15:05:42 +0200 Subject: [PATCH 31/58] Treat classes with universal self types as level owners Rataional: subclasses might have universal parameters, which would make *them* level owners. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 9b7f216f546c..8433cd660703 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -300,6 +300,32 @@ extension (tp: Type) 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] = @@ -382,16 +408,15 @@ extension (sym: Symbol) info.dealias.stripPoly match case mt: MethodType => - (mt.paramInfos.exists(hasUniversalCap) || takesCappedParamIn(mt.resType)) + (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(hasUniversalCap) || takesCappedParamIn(args.last) + args.init.exists(_.hasUniversalRootOf(sym)) || takesCappedParamIn(args.last) case defn.RefinedFunctionOf(rinfo) => takesCappedParamIn(rinfo) //.showing(i"takes capped param2 $sym: $rinfo = $result") case _ => false - end takesCappedParamIn // TODO Also include vals (right now they are manually entered in levelOwners by Setup) def isLevelOwner(using Context): Boolean = @@ -399,7 +424,9 @@ extension (sym: Symbol) def isCaseClassSynthetic = // TODO drop symd.maybeOwner.isClass && symd.owner.is(Case) && symd.is(Synthetic) && symd.info.firstParamNames.isEmpty def classQualifies = - !levelOwnersNeedCapParam || takesCappedParamIn(symd.primaryConstructor.info) + !levelOwnersNeedCapParam + || takesCappedParamIn(symd.primaryConstructor.info) + || symd.asClass.givenSelfType.hasUniversalRootOf(sym) def termQualifies = !levelOwnersNeedCapParam || takesCappedParamIn(symd.info) def compute = From b475268bcea1926c7784ff9fac3722e7b138fa4c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 15:13:13 +0200 Subject: [PATCH 32/58] Revamp capture roots and captureset vars - Drop usage of nesting level - Work directly with owner inclusion --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 57 ++----- .../src/dotty/tools/dotc/cc/CaptureRoot.scala | 141 ++++++++---------- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 78 +++++----- .../dotty/tools/dotc/cc/CheckCaptures.scala | 14 +- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 14 +- .../tools/dotc/printing/PlainPrinter.scala | 6 +- 7 files changed, 140 insertions(+), 172 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 8433cd660703..b2df95c5ce76 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -91,23 +91,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: +class mapRoots(from0: CaptureRoot, to: CaptureRoot)(using Context) extends BiTypeMap, FollowAliases: thisMap => + 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 constrainRootsWhenMapping && t.unifiesWith(from) => + to case t @ Setup.Box(t1) => t.derivedBox(this(t1)) case _ => @@ -382,30 +377,6 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeUnbox def takesCappedParamIn(info: Type)(using Context): Boolean = - - def isOwnRoot(tp: Type): Boolean = - tp.isCapabilityClassRef - || tp.dealias.match - case tp: CaptureRef => - tp.isGenericRootCapability || tp.localRootOwner == sym - case _ => - false - - def hasUniversalCap(tp: Type): Boolean = 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 || hasUniversalCap(parent) - case tp: TypeRef => - tp.isRef(defn.Caps_Cap) || tp.isCapabilityClassRef - case tp: LazyRef => - hasUniversalCap(tp.ref) - case tp: TypeVar => - hasUniversalCap(tp.underlying) - case _ => - tp.isCapabilityClassRef - info.dealias.stripPoly match case mt: MethodType => (mt.paramInfos.exists(_.hasUniversalRootOf(sym)) || takesCappedParamIn(mt.resType)) @@ -517,17 +488,17 @@ extension (sym: Symbol) else newRoot ccState.localRoots.getOrElseUpdate(owner, lclRoot) - 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 + def maxNested(other: Symbol, pickFirstOnConflict: Boolean = false)(using Context): Symbol = + if !sym.exists || other.isContainedIn(sym) then other + else if !other.exists || sym.isContainedIn(other) then sym else - assert(sym == other, i"conflicting symbols at same nesting level: $sym, $other") + assert(pickFirstOnConflict, i"incomparable nesting: $sym and $other") sym - */ def minNested(other: Symbol)(using Context): Symbol = - if sym.ccNestingLevel > other.ccNestingLevel then other else sym + 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: TermRef | ThisType) /** The nesting level of this reference as defined by capture checking */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 0651d5699892..6bab3d331bb7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -14,98 +14,85 @@ type CaptureRoot = TermRef | CaptureRoot.Var object CaptureRoot: - case class Var(owner: Symbol, source: Symbol = NoSymbol)(using @constructorOnly ictx: Context) extends CaptureRef, Showable: + 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: - override def localRootOwner(using Context) = owner - override def isTrackableRef(using Context): Boolean = true - override def captureSetOfInfo(using Context) = CaptureSet.universal + val id = + nextId += 1 + nextId - def setAlias(target: CaptureRoot) = - alias = target + var innerLimit: Symbol = owner.levelOwner + var outerLimit: Symbol = defn.RootClass + var outerRoots: SimpleIdentitySet[Var] = SimpleIdentitySet.empty - def followAlias: CaptureRoot = alias match - case alias: Var if alias ne this => alias.followAlias - case _ => alias + override def isTrackableRef(using Context): Boolean = true + override def captureSetOfInfo(using Context) = CaptureSet.universal - def locallyConsistent = - lowerLevel <= upperLevel - && lowerRoots.forall(_.upperLevel <= upperLevel) - && upperRoots.forall(_.lowerLevel >= lowerLevel) + 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 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 + 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) + 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 CaptureRoot diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 6cb555ef3f88..1ff8204602aa 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -56,13 +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 = - owner.localRoot.termRef.singletonCaptureSet + assert(levelLimit.exists, this) + levelLimit.localRoot.termRef.singletonCaptureSet /** Is this capture set definitely non-empty? */ final def isNotEmpty: Boolean = !elems.isEmpty @@ -143,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.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 @@ -231,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, pickFirstOnConflict = true), + this.elems ++ that.elems) .addAsDependentTo(this).addAsDependentTo(that) /** The smallest superset (via <:<) of this capture set that also contains `ref`. @@ -406,7 +409,7 @@ object CaptureSet: def withDescription(description: String): Const = Const(elems, description) - def owner = NoSymbol + def levelLimit = NoSymbol override def toString = elems.toString end Const @@ -433,16 +436,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 @@ -503,11 +504,18 @@ object CaptureSet: r.andAlso(dep.tryInclude(elem, this)) } - private def levelOK(elem: CaptureRef)(using Context): Boolean = elem match - case elem: TermRef if elem.symbol.isLevelOwner => elem.ccNestingLevel - 1 <= ownLevel - case elem: (TermRef | ThisType) => elem.ccNestingLevel <= ownLevel - case elem: CaptureRoot.Var => CaptureRoot.isEnclosingRoot(elem, owner.localRoot.termRef) - case _ => true + 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 @@ -578,11 +586,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 @@ -594,8 +602,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 @@ -635,7 +643,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] @@ -701,7 +709,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 @@ -731,7 +739,7 @@ object CaptureSet: /** 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) @@ -762,7 +770,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 @@ -851,7 +859,7 @@ object CaptureSet: case OK => Str("OK") case Fail(blocking: CaptureSet) => blocking.show case LevelError(cs: CaptureSet, elem: CaptureRef) => - Str(i"($elem at wrong level for $cs in ${cs.owner})") + Str(i"($elem at wrong level for $cs in ${cs.levelLimit})") /** The result is OK */ def isOK: Boolean = this == OK @@ -1052,11 +1060,11 @@ object CaptureSet: 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/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 107125d11f45..a1632fa9d5f3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -182,8 +182,8 @@ class CheckCaptures extends Recheck, SymTransformer: ref match case ref: CaptureRoot.Var => ref.followAlias match - case rv: CaptureRoot.Var if rv.upperBound == ctx.owner.levelOwner => - rv.setAlias(ctx.owner.localRoot.termRef) + case rv: CaptureRoot.Var if rv.innerLimit == ctx.owner.levelOwner => + rv.alias = ctx.owner.localRoot.termRef case _ => case _ => traverse(parent) @@ -249,7 +249,7 @@ class CheckCaptures extends Recheck, SymTransformer: def capturedVars(sym: Symbol)(using Context) = myCapturedVars.getOrElseUpdate(sym, if sym.ownersIterator.exists(_.isTerm) - then CaptureSet.Var(sym.skipConstructor.owner) + then CaptureSet.Var(sym.owner) else CaptureSet.empty) /** For all nested environments up to `limit` or a closed environment perform `op`, @@ -376,7 +376,7 @@ class CheckCaptures extends Recheck, SymTransformer: || sym.isTerm && defn.isFunctionType(sym.info) && pt == AnySelectionProto if sym.skipConstructor.isLevelOwner && canInstantiate then val tpw = tp.widen - val tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner, sym))(tpw) + val tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner, sym))(tpw) .showing(i"INST $sym: $tp, ${sym.localRoot} = $result", ccSetup) if tpw eq tp1 then tp else tp1 else @@ -406,7 +406,7 @@ 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 @@ -756,14 +756,14 @@ class CheckCaptures extends Recheck, SymTransformer: // 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.levelOwner))(actualWide) + 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), expected, addenda ++ CaptureSet.levelErrors) + err.typeMismatch(tree.withType(actualBoxed), expected1, addenda ++ CaptureSet.levelErrors) end checkConformsExpr /** Turn `expected` into a dependent function when `actual` is dependent. */ diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 62559a037703..7c3f098d8011 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2110,7 +2110,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // 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)) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index fc6d6027e6aa..ba03f3f99b2b 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, RetainingType, ccNestingLevel} +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, RetainingType, ccNestingLevel, CaptureRoot} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -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 = @@ -2926,7 +2924,7 @@ object Types { override def isGenericRootCapability(using Context): Boolean = name == nme.CAPTURE_ROOT && symbol == defn.captureRoot - override def localRootOwner(using Context): Symbol = + 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 diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 1ed02e8b6e7d..9fa31f4a8ab6 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -439,7 +439,11 @@ class PlainPrinter(_ctx: Context) extends Printer { (toTextRef(sym.termRef) ~ Str(s"/${sym.ccNestingLevel}").provided(showNestingLevel) ).provided(sym.exists) - "'cap[" ~ boundText(tp.lowerBound) ~ ".." ~ boundText(tp.upperBound) ~ "]" + "'cap[" + ~ toTextRef(tp.outerLimit.termRef).provided(!tp.outerLimit.isRoot) + ~ ".." + ~ toTextRef(tp.innerLimit.termRef) + ~ "]" ~ ("(from instantiating " ~ nameString(tp.source) ~ ")").provided(tp.source.exists) } } From 8fb8350a2ea720e9e11ae2225ff40d9f4a48b1ba Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 15:20:38 +0200 Subject: [PATCH 33/58] Make RefiningVars level polymorphic --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index d40ad44670eb..0ff8ed18df8d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -187,7 +187,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if atPhase(thisPhase.next)(getter.termRef.isTracked) then val getterType = tp.memberInfo(getter).strippedDealias RefinedType(core, getter.name, - CapturingType(getterType, CaptureSet.RefiningVar(ctx.owner, getter))) + CapturingType(getterType, CaptureSet.RefiningVar(NoSymbol, getter))) .showing(i"add capture refinement $tp --> $result", ccSetup) else core From ac74be8c452a42057271f80d86f2400dd3ce894e Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 15:21:35 +0200 Subject: [PATCH 34/58] Enable takesCappedParam criterion for level ownership --- compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala | 1 + compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala b/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala index 2e9f7ecfccd2..b79bd2a9ccaa 100644 --- a/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala +++ b/compiler/src/dotty/tools/dotc/cc/AddTryOwners.scala @@ -33,6 +33,7 @@ class AddTryOwners extends Phase, IdentityDenotTransformer: 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) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index b2df95c5ce76..10cd9de807df 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -31,12 +31,12 @@ private val constrainRootsWhenMapping = true /** If true, most vals can be level owners. If false, only vals defined by a * closure as RHS can be level owners */ -private val valsCanBeLevelOwners = false +private val valsCanBeLevelOwners = true /** If true, only vals, defs, and classes with a universal capability in a parameter * or self type are considered as level owners. */ -private val levelOwnersNeedCapParam = false +private val levelOwnersNeedCapParam = true def allowUniversalInBoxed(using Context) = Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) From eda563be728352e50df89cb547e3bdf3d8b23ac7 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 15:23:48 +0200 Subject: [PATCH 35/58] Use LooseCaptureRoot checking for checking bounds of applied types Temporary measure until we have class capture roots worked out --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a1632fa9d5f3..2988623f5320 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -376,7 +376,7 @@ class CheckCaptures extends Recheck, SymTransformer: || sym.isTerm && defn.isFunctionType(sym.info) && pt == AnySelectionProto if sym.skipConstructor.isLevelOwner && canInstantiate then val tpw = tp.widen - val tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner, sym))(tpw) + var tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner, sym))(tpw) .showing(i"INST $sym: $tp, ${sym.localRoot} = $result", ccSetup) if tpw eq tp1 then tp else tp1 else @@ -760,6 +760,8 @@ class CheckCaptures extends Recheck, SymTransformer: capt.println(i"fallBack from $actualWide to $actualInst to match $expected1") ok = (actualInst ne actualWide) && isCompatible(adaptBoxed(actualInst, expected1, tree.srcPos), expected1) + // Useful for debugging: + // if !ok then err.typeMismatch(tree.withType(actualInst), expected1, addenda ++ CaptureSet.levelErrors) case _ => if !ok then capt.println(i"conforms failed for ${tree}: $actual vs $expected") @@ -1231,6 +1233,6 @@ class CheckCaptures extends Recheck, SymTransformer: case tree: New => case tree: TypeTree => checkAppliedTypesIn(tree.withKnownType) case _ => traverseChildren(t) - checkApplied.traverse(unit) + checkApplied.traverse(unit)(using ctx.withProperty(LooseRootChecking, Some(()))) end CaptureChecker end CheckCaptures From 1f9bf265082f2f35aa80fb90f4cdae8f68d51328 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 15:24:26 +0200 Subject: [PATCH 36/58] Update tests can check files --- tests/neg-custom-args/captures/byname.check | 2 +- tests/neg-custom-args/captures/capt1.check | 4 ++-- tests/neg-custom-args/captures/i15772.check | 12 ++++++------ tests/neg-custom-args/captures/lazylist.check | 2 +- tests/neg-custom-args/captures/lazyref.check | 12 ++++++------ .../captures/leaked-curried.check | 2 +- tests/neg-custom-args/captures/levels.check | 8 ++++---- tests/neg-custom-args/captures/levels.scala | 3 +-- tests/neg-custom-args/captures/localcaps.scala | 4 +++- tests/neg-custom-args/captures/real-try.check | 4 ++-- .../captures/simple-escapes.check | 2 +- .../captures/usingLogFile-alt.check | 4 ++-- .../captures/usingLogFile.check | 4 ++-- tests/neg-custom-args/captures/vars.check | 18 +++++++++--------- tests/neg-custom-args/captures/vars.scala | 7 +++---- tests/pos-custom-args/captures/capt2.scala | 2 +- tests/pos-custom-args/captures/lists.scala | 2 +- tests/pos-custom-args/captures/pairs.scala | 4 ++-- tests/pos-special/stdlib/Test2.scala | 2 +- .../captures/colltest5/Test_2.scala | 2 +- 20 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 67f42a4acc69..781b26b6944d 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -2,7 +2,7 @@ 10 | h(f2()) // error | ^^^^ | Found: (x$0: Int) ->{cap1} Int - | Required: Int ->{cap2} Int + | Required: (x$0: Int) ->{cap2} Int | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 93b263aacba6..8e56f9515dbf 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -36,5 +36,5 @@ -- Error: tests/neg-custom-args/captures/capt1.scala:32:30 ------------------------------------------------------------- 32 | val z2 = h[() -> Cap](() => x) // error | ^ - | (x : C^{cap[foo]}) cannot be referenced here; it is not included in the allowed capture set {} - | of an enclosing function literal with expected type () -> box C^{cap[foo]} + | (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/i15772.check b/tests/neg-custom-args/captures/i15772.check index c62dae2f7627..d66fe7a4d063 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -6,8 +6,8 @@ -- [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], c} Unit) ->{c} Unit - | Required: (C^{cap[main1]} ->{cap[main1]} Unit) -> Unit + | Found: (C{val arg: C^{cap[C]}}^{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 ------------------------------------------------------------ @@ -18,15 +18,15 @@ -- [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], c} Unit) ->{c} Unit - | Required: (C^{cap[main2]} ->{cap[main2]} Unit) -> Unit + | Found: (C{val arg: C^{cap[C]}}^{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:33 --------------------------------------- 33 | val boxed2 : Observe[C]^ = box2(c) // error | ^^^^^^^ - | Found: (C{val arg: C^{cap[C]}}^{'cap[..main3](from instantiating c)} ->{cap[main3]} Unit) ->? Unit - | Required: (C ->{cap[main3]} Unit) ->{cap[main3]} Unit + | Found: (C{val arg: C^{cap[C]}}^{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 ---------------------------------------- diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index ba5080d808f8..f8f1c1df229f 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -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/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index a3cf34067b30..a9806bc5494d 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -8,21 +8,21 @@ -- [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: () ->{cap[test]} Int}^{cap2, ref1}) - | Required: LazyRef[Int]^{cap2} + | Found: (ref2 : LazyRef[Int]{val elem: () ->{cap[]} Int}^{cap2, ref1}) + | 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: () ->{cap[test]} Int}^{cap2, ref1}) - | Required: LazyRef[Int]^{ref1} + | Found: (ref3 : LazyRef[Int]{val elem: () ->{cap[]} Int}^{cap2, ref1}) + | 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: () ->{cap[test]} Int}^{cap2, cap1}) - | Required: LazyRef[Int]^{cap1} + | Found: (ref4 : LazyRef[Int]{val elem: () ->{cap[]} Int}^{cap2, cap1}) + | 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 a3665b50b6f6..a13c15c7c7fd 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -1,4 +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 + |(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/levels.check b/tests/neg-custom-args/captures/levels.check index 2a72dbd354d1..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^{cap[scope]}), 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/real-try.check b/tests/neg-custom-args/captures/real-try.check index f969404cde73..12a4cbd62c63 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -24,5 +24,5 @@ |(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 (canThrow$4 : CanThrow[Ex1 | Ex2]^{cap[]}), defined at level 2 - |cannot be included in outer capture set ?, defined at level 1 in method test + |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/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/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 eb24f76d748c..433172663640 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -31,5 +31,5 @@ | ^^^^^^^^^ | 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 at level 2 - | cannot be included in outer capture set {x$0, local}, defined at level 1 in method test + | 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/vars.check b/tests/neg-custom-args/captures/vars.check index 98991fdf9857..7447d65ef026 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -5,26 +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^{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 String ->{cap[test]} 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:13 ----------------------------------------- -26 | b = List(g) // error +-- [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` --- 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 81a68ce6a005..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 - val sc: String => String = scope + 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/capt2.scala b/tests/pos-custom-args/captures/capt2.scala index 1df2ea69b119..45381bf602ed 100644 --- a/tests/pos-custom-args/captures/capt2.scala +++ b/tests/pos-custom-args/captures/capt2.scala @@ -13,7 +13,7 @@ def test2() = z: (() -> Unit) @retains(x) def z2: (() -> Unit) @retains(y) = y z2: (() -> Unit) @retains(y) - val p: () ->{cap[test2]} String = () => "abc" + val p: () => String = () => "abc" val q: C^{p} = ??? val _ = p: (() ->{p} String) 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/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index e722e2fa74bd..93bf919a4750 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -13,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: diff --git a/tests/pos-special/stdlib/Test2.scala b/tests/pos-special/stdlib/Test2.scala index 26b2faee1165..a59da522b183 100644 --- a/tests/pos-special/stdlib/Test2.scala +++ b/tests/pos-special/stdlib/Test2.scala @@ -8,7 +8,7 @@ object Test { def seqOps(xs: Seq[Int]) = { // try with Seq[Int]^{cap} val strPlusInt: (String, Int) => String = _ + _ val intPlusStr: (Int, String) => String = _ + _ - val isEven: Int ->{cap[seqOps]} Boolean = _ % 2 == 0 + val isEven: Int => 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/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index 64e8d4a88cf3..5ea259b965ed 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -8,7 +8,7 @@ object Test { def seqOps(xs: Seq[Int]) = { // try with Seq[Int]^{cap} val strPlusInt: (String, Int) => String = _ + _ val intPlusStr: (Int, String) => String = _ + _ - val isEven: Int ->{cap[seqOps]} Boolean = _ % 2 == 0 + val isEven: Int => Boolean = _ % 2 == 0 val isNonNeg: Int => Boolean = _ > 0 val flips: Int => List[Int] = x => Cons(x, Cons(-x, Nil)) val x1 = xs.foldLeft("")(strPlusInt) From 046dff9922373c335c818635fda6157587ecc22d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 15:31:56 +0200 Subject: [PATCH 37/58] Revert "Use LooseCaptureRoot checking for checking bounds of applied types" No tests fail if LooseCaptureRoot is disabled again. This reverts commit b337b08a14ccb2c3a76651da0cd2aec60e8d1595. --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 2988623f5320..a1632fa9d5f3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -376,7 +376,7 @@ class CheckCaptures extends Recheck, SymTransformer: || sym.isTerm && defn.isFunctionType(sym.info) && pt == AnySelectionProto if sym.skipConstructor.isLevelOwner && canInstantiate then val tpw = tp.widen - var tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner, sym))(tpw) + val tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner, sym))(tpw) .showing(i"INST $sym: $tp, ${sym.localRoot} = $result", ccSetup) if tpw eq tp1 then tp else tp1 else @@ -760,8 +760,6 @@ class CheckCaptures extends Recheck, SymTransformer: capt.println(i"fallBack from $actualWide to $actualInst to match $expected1") ok = (actualInst ne actualWide) && isCompatible(adaptBoxed(actualInst, expected1, tree.srcPos), expected1) - // Useful for debugging: - // if !ok then err.typeMismatch(tree.withType(actualInst), expected1, addenda ++ CaptureSet.levelErrors) case _ => if !ok then capt.println(i"conforms failed for ${tree}: $actual vs $expected") @@ -1233,6 +1231,6 @@ class CheckCaptures extends Recheck, SymTransformer: case tree: New => case tree: TypeTree => checkAppliedTypesIn(tree.withKnownType) case _ => traverseChildren(t) - checkApplied.traverse(unit)(using ctx.withProperty(LooseRootChecking, Some(()))) + checkApplied.traverse(unit) end CaptureChecker end CheckCaptures From 29f34debcef9809223ac672774b1bea7cc3ebe39 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 18:13:38 +0200 Subject: [PATCH 38/58] Simplification: drop ccNestingLevel --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 29 +------------------ .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../tools/dotc/printing/PlainPrinter.scala | 19 ++++-------- .../tools/dotc/printing/RefinedPrinter.scala | 7 ++--- 4 files changed, 10 insertions(+), 47 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 10cd9de807df..edbf1c971aa5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -57,9 +57,6 @@ class CCState: /** Cache for level ownership */ val isLevelOwner: mutable.HashMap[Symbol, Boolean] = new mutable.HashMap - /** Associates certain symbols (the nesting level owners) with their ccNestingLevel */ - val nestingLevels: mutable.HashMap[Symbol, Int] = new mutable.HashMap - /** Associates nesting level owners with the local roots valid in their scopes. */ val localRoots: mutable.HashMap[Symbol, Symbol] = new mutable.HashMap @@ -451,24 +448,6 @@ extension (sym: Symbol) recur(sym, NoSymbol) .showing(i"find outer $sym [ $name ] = $result", capt) - /** 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. - */ - def ccNestingLevelOpt(using Context): Option[Int] = - if ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev - then Some(ccNestingLevel) - else None - /** The parameter with type caps.Cap in the leading term parameter section, * or NoSymbol, if none exists. */ @@ -482,7 +461,7 @@ 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 @@ -500,12 +479,6 @@ extension (sym: Symbol) else if !sym.exists || sym.isContainedIn(other) then other else sym.owner.minNested(other.owner) -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 - extension (tp: AnnotatedType) /** Is this a boxed capturing type? */ def isBoxed(using Context): Boolean = tp.annot match diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index ba03f3f99b2b..3bc43837c5f1 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, RetainingType, ccNestingLevel, CaptureRoot} +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, RetainingType, CaptureRoot} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 9fa31f4a8ab6..02fc1e44c12a 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, RetainingType, CaptureSet, CaptureRoot, isBoxed, ccNestingLevel, levelOwner, retainedElems} +import cc.{CapturingType, RetainingType, CaptureSet, CaptureRoot, isBoxed, levelOwner, retainedElems} class PlainPrinter(_ctx: Context) extends Printer { @@ -374,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 "") + ">" @@ -408,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[${tp.localRootOwner.name}]") else toTextPrefixOf(tp) ~ selectionString(tp) case tp: ThisType => nameString(tp.cls) + ".this" @@ -436,9 +431,7 @@ 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) + toTextRef(sym.termRef).provided(sym.exists) "'cap[" ~ toTextRef(tp.outerLimit.termRef).provided(!tp.outerLimit.isRoot) ~ ".." 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}" From 2d07bd558b994468780b40b90fba20f1f265b9be Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 Oct 2023 18:23:30 +0200 Subject: [PATCH 39/58] Add @sharable annotation to global counter --- compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 6bab3d331bb7..6913740307ae 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -9,12 +9,13 @@ import printing.Showable import util.SimpleIdentitySet import Decorators.i import scala.annotation.constructorOnly +import scala.annotation.internal.sharable type CaptureRoot = TermRef | CaptureRoot.Var object CaptureRoot: - private var nextId = 0 + @sharable private var nextId = 0 case class Var(owner: Symbol, source: Symbol)(using @constructorOnly ictx: Context) extends CaptureRef, Showable: From 86463085bce6580bd584447ccc20b7dbf7715086 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 3 Oct 2023 18:14:07 +0200 Subject: [PATCH 40/58] Handle outer class roots when instantiating class members The problem arises if we have a class like the one in pos-custom-args/captures/refs.scala: ```scala class MonoRef(init: Proc): type MonoProc = Proc var x: MonoProc = init def getX: MonoProc = x def setX(x: MonoProc): Unit = this.x = x ``` The type of `getX` and `setX` refer to the local root capability of class `MonoRef`. When we call `m.getX` or `m.setX` in `m: MonoRef`, these occurrences have to be adapted to capture roots in the scope of the selection. We determine these roots by inspecting the capture set of `m` and picking a root that corresponds to it. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +- .../src/dotty/tools/dotc/cc/CaptureRoot.scala | 97 ++++++++++++++++++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 18 +++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 24 +++-- tests/neg-custom-args/captures/pairs.check | 28 ++++++ tests/neg-custom-args/captures/pairs.scala | 37 +++++++ .../captures/outer-roots.scala | 26 +++++ tests/pos-custom-args/captures/pairs.scala | 9 +- tests/pos-custom-args/captures/refs.scala | 29 ++++++ 9 files changed, 256 insertions(+), 20 deletions(-) create mode 100644 tests/neg-custom-args/captures/pairs.check create mode 100644 tests/neg-custom-args/captures/pairs.scala create mode 100644 tests/pos-custom-args/captures/outer-roots.scala create mode 100644 tests/pos-custom-args/captures/refs.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index edbf1c971aa5..986edf4d0a59 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -89,8 +89,6 @@ trait FollowAliases extends TypeMap: mapOver(t) class mapRoots(from0: CaptureRoot, to: CaptureRoot)(using Context) extends BiTypeMap, FollowAliases: - thisMap => - val from = from0.followAlias //override val toString = i"mapRoots($from, $to)" @@ -467,12 +465,10 @@ extension (sym: Symbol) else newRoot ccState.localRoots.getOrElseUpdate(owner, lclRoot) - def maxNested(other: Symbol, pickFirstOnConflict: Boolean = false)(using Context): Symbol = + 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 - assert(pickFirstOnConflict, i"incomparable nesting: $sym and $other") - sym + else onConflict(sym, other) def minNested(other: Symbol)(using Context): Symbol = if !other.exists || other.isContainedIn(sym) then sym diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 6913740307ae..1295695cf52b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -4,10 +4,12 @@ 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 @@ -43,11 +45,31 @@ object CaptureRoot: case _ => myAlias = r + /** 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 + + /** 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 + 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 @@ -83,7 +105,9 @@ object CaptureRoot: 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) + r2.outerLimit = + r1.outerLimit.maxNested(r2.outerLimit, + onConflict = (_, _) => throw NoCommonRoot(r1, r2)) r2.innerLimit = r1.innerLimit.minNested(r2.innerLimit) true else @@ -93,6 +117,75 @@ object CaptureRoot: 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) + + /** A map that instantiates all outer class roots in the info of `sym` + * according to prefix `pre`. This is called for adapting the info of + * a selection `pre.sym`. The logic of the function is modeled after + * AsSeenFrom. But where AsSeenFrom maps a `this` of class `C` to a corresponding + * prefix, the present method maps a local root corresponding to a class to + * the root implied by the capture set of the corresponding prefix. + * @param sym the class member symbol whose info is mapped + * @param pre the prefix from which `sym` is selected + * @param deafilt the capture root to use if the capture set of the corresponding + * prfefix is empty. + */ + class instantiateOuterClassRoots(sym: Symbol, pre: Type, default: CaptureRoot)(using Context) extends ApproximatingTypeMap: + val cls = sym.owner.asClass + + def apply(tp: Type): Type = + + /** Analogous to `toPrefix` in `AssSeenFromMap`, but result prefix gets + * further mapped to a capture root via `impliedRoot`. + */ + def mapCaptureRoot(pre: Type, cls: Symbol, thiscls: ClassSymbol, fallBack: CaptureRoot): CaptureRoot = + if (pre eq NoType) || (pre eq NoPrefix) || (cls is PackageClass) then + fallBack + else pre match + case pre: SuperType => + mapCaptureRoot(pre.thistpe, cls, thiscls, fallBack) + case _ => + if thiscls.derivesFrom(cls) && pre.baseType(thiscls).exists then + pre.captureSet.impliedRoot(default) + else if pre.termSymbol.is(Package) && !thiscls.is(Package) then + mapCaptureRoot(pre.select(nme.PACKAGE), cls, thiscls, fallBack) + else + mapCaptureRoot(pre.baseType(cls).normalizedPrefix, cls.owner, thiscls, fallBack) + + def instRoot(elem: CaptureRef): CaptureRef = elem match + case elem: TermRef + if elem.name == nme.LOCAL_CAPTURE_ROOT && elem.symbol.owner.isLocalDummy => + mapCaptureRoot(pre, cls, elem.localRootOwner.asClass, elem) + .showing(i"mapped capture root $elem in $cls to $result", capt) + case _ => + elem + + tp match + case t @ CapturingType(parent, refs) => + val elems = refs.elems.toList + val elems1 = elems.mapConserve(instRoot) + val refs1 = if elems1 eq elems then refs else CaptureSet(elems1*) + t.derivedCapturingType(apply(parent), refs1) + case _ => + mapOver(tp) + end instantiateOuterClassRoots end CaptureRoot diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 1ff8204602aa..6af9e3ad56c8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -233,7 +233,7 @@ sealed abstract class CaptureSet extends Showable: 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.levelLimit.maxNested(that.levelLimit, pickFirstOnConflict = true), + this.levelLimit.maxNested(that.levelLimit, onConflict = (sym1, sym2) => sym1), this.elems ++ that.elems) .addAsDependentTo(this).addAsDependentTo(that) @@ -315,6 +315,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() diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a1632fa9d5f3..600b9fc6f04b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -319,7 +319,7 @@ class CheckCaptures extends Recheck, SymTransformer: includeCallCaptures(tree.symbol, tree.srcPos) else markFree(tree.symbol, tree.srcPos) - instantiateLocalRoots(tree.symbol, pt): + instantiateLocalRoots(tree.symbol, NoPrefix, pt): super.recheckIdent(tree, pt) /** A specialized implementation of the selection rule. @@ -349,7 +349,7 @@ class CheckCaptures extends Recheck, SymTransformer: val selType = recheckSelection(tree, qualType, name, disambiguate) val selCs = selType.widen.captureSet - instantiateLocalRoots(tree.symbol, pt): + instantiateLocalRoots(tree.symbol, qualType, pt): if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then selType else @@ -370,14 +370,23 @@ class CheckCaptures extends Recheck, SymTransformer: * - `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, pt: Type)(tp: Type)(using Context): Type = + def instantiateLocalRoots(sym: Symbol, pre: Type, pt: Type)(tp: Type)(using Context): Type = def canInstantiate = sym.is(Method, butNot = Accessor) || sym.isTerm && defn.isFunctionType(sym.info) && pt == AnySelectionProto - if sym.skipConstructor.isLevelOwner && canInstantiate then + if canInstantiate then val tpw = tp.widen - val tp1 = mapRoots(sym.localRoot.termRef, CaptureRoot.Var(ctx.owner, sym))(tpw) - .showing(i"INST $sym: $tp, ${sym.localRoot} = $result", ccSetup) + var tp1 = tpw + val rootVar = CaptureRoot.Var(ctx.owner, sym) + if sym.skipConstructor.isLevelOwner then + tp1 = mapRoots(sym.localRoot.termRef, rootVar)(tp1) + if tp1 ne tpw then + ccSetup.println(i"INST local $sym: $tp, ${sym.localRoot} = $tp1") + if sym.owner.isClass then + val tp2 = CaptureRoot.instantiateOuterClassRoots(sym, pre, rootVar)(tp1) + if tp2 ne tp1 then + ccSetup.println(i"INST class $sym: $tp, ${sym.localRoot} in $pre = $tp2") + tp1 = tp2 if tpw eq tp1 then tp else tp1 else tp @@ -695,6 +704,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) 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/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 93bf919a4750..c8c169a1041c 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -29,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: @@ -48,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 + From 0ffba0df65ca5ecb74d54f0b5c522a9181401602 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 5 Oct 2023 16:30:24 +0200 Subject: [PATCH 41/58] Simplifications - Drop valsCanBeLevelOwners and levelOwnersNeedCapParam config settings - Drop !isCaseClassSynthetic condition in isLevelOwner - Drop handling of anonymous functions in levelOwnerNamed Neither are needed anymore with the the conflg flags gone. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 32 +++++-------------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 +-- .../src/dotty/tools/dotc/cc/Synthetics.scala | 4 +-- .../captures/concat-iterator.scala | 17 ++++++++++ 4 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 tests/pos-custom-args/captures/concat-iterator.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 986edf4d0a59..f54261de79c8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -28,15 +28,7 @@ private val adaptUnpickledFunctionTypes = false */ private val constrainRootsWhenMapping = true -/** If true, most vals can be level owners. If false, only vals defined by a - * closure as RHS can be level owners - */ -private val valsCanBeLevelOwners = true - -/** If true, only vals, defs, and classes with a universal capability in a parameter - * or self type are considered as level owners. - */ -private val levelOwnersNeedCapParam = true +private val constructorsAreLevelOwners = false def allowUniversalInBoxed(using Context) = Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) @@ -390,25 +382,21 @@ extension (sym: Symbol) def isCaseClassSynthetic = // TODO drop symd.maybeOwner.isClass && symd.owner.is(Case) && symd.is(Synthetic) && symd.info.firstParamNames.isEmpty def classQualifies = - !levelOwnersNeedCapParam - || takesCappedParamIn(symd.primaryConstructor.info) + takesCappedParamIn(symd.primaryConstructor.info) || symd.asClass.givenSelfType.hasUniversalRootOf(sym) - def termQualifies = - !levelOwnersNeedCapParam || takesCappedParamIn(symd.info) def compute = if symd.isClass then symd.is(CaptureChecked) && classQualifies || symd.isRoot else (symd.is(Method, butNot = Accessor) - || valsCanBeLevelOwners && symd.isTerm && !symd.isOneOf(TermParamOrAccessor | Mutable)) + || symd.isTerm && !symd.isOneOf(TermParamOrAccessor | Mutable)) && (!symd.owner.isClass || symd.owner.is(CaptureChecked) || Synthetics.needsTransform(symd) ) - && !isCaseClassSynthetic && !symd.isConstructor && (!symd.isAnonymousFunction || sym.definedLocalRoot.exists) - && termQualifies + && takesCappedParamIn(symd.info) && { ccSetup.println(i"Level owner $sym"); true } ccState.isLevelOwner.getOrElseUpdate(sym, compute) @@ -433,17 +421,13 @@ extension (sym: Symbol) * owner. */ def levelOwnerNamed(name: String)(using Context): Symbol = - def recur(sym: Symbol, prev: Symbol): Symbol = + def recur(sym: Symbol): Symbol = if sym.name.toString == name then if sym.isLevelOwner then sym - else if sym.isTerm && !sym.isOneOf(Method | Module) && prev.exists then prev else NoSymbol - else if sym == defn.RootClass then - NoSymbol - else - val prev1 = if sym.isAnonymousFunction && sym.isLevelOwner then sym else NoSymbol - recur(sym.owner, prev1) - recur(sym, 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, diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 0ff8ed18df8d..c6ea8c9b488d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -463,8 +463,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) && !sym.is(Mutable) - && (!levelOwnersNeedCapParam - || ddef.symbol.takesCappedParamIn(ddef.symbol.info)) => + && ddef.symbol.takesCappedParamIn(ddef.symbol.info) => ccSetup.println(i"Level owner at setup $sym / ${ddef.symbol.info}") ccState.isLevelOwner(sym) = true case _ => @@ -571,6 +570,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // closures are handled specially; the newInfo is constrained from // the expected type and only afterwards we recheck the definition newInfo + else if sym.isPrimaryConstructor then + newInfo else new LazyType: def complete(denot: SymDenotation)(using Context) = // infos of other methods are determined from their definitions which diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 4b20533c074e..9cd7b7cec3cb 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -157,12 +157,12 @@ object Synthetics: case DefaultGetterName(nme.copy, n) => transformDefaultGetterCaptures(info, symd.owner, n) case nme.unapply => - if levelOwnersNeedCapParam then ccState.isLevelOwner(symd.symbol) = true + ccState.isLevelOwner(symd.symbol) = true transformUnapplyCaptures(info) case nme.apply | nme.copy => addCaptureDeps(info) case nme.andThen | nme.compose => - if levelOwnersNeedCapParam then ccState.isLevelOwner(symd.symbol) = true + ccState.isLevelOwner(symd.symbol) = true transformComposeCaptures(info, symd.owner) case nme.curried | nme.tupled => transformCurriedTupledCaptures(info, symd.owner) 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} = ??? + + + + From 3d2a4cd672d305a33d3f933d7ba914bd03504d93 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 5 Oct 2023 17:07:55 +0200 Subject: [PATCH 42/58] Make constructors level owners --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 3 --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 8 +++++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f54261de79c8..5cc2f0950f53 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -28,8 +28,6 @@ private val adaptUnpickledFunctionTypes = false */ private val constrainRootsWhenMapping = true -private val constructorsAreLevelOwners = false - def allowUniversalInBoxed(using Context) = Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) @@ -394,7 +392,6 @@ extension (sym: Symbol) || symd.owner.is(CaptureChecked) || Synthetics.needsTransform(symd) ) - && !symd.isConstructor && (!symd.isAnonymousFunction || sym.definedLocalRoot.exists) && takesCappedParamIn(symd.info) && { ccSetup.println(i"Level owner $sym"); true } diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 600b9fc6f04b..bef7507850d6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -378,7 +378,7 @@ class CheckCaptures extends Recheck, SymTransformer: val tpw = tp.widen var tp1 = tpw val rootVar = CaptureRoot.Var(ctx.owner, sym) - if sym.skipConstructor.isLevelOwner then + if sym.isLevelOwner then tp1 = mapRoots(sym.localRoot.termRef, rootVar)(tp1) if tp1 ne tpw then ccSetup.println(i"INST local $sym: $tp, ${sym.localRoot} = $tp1") diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index c6ea8c9b488d..28fdaac3c165 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -566,12 +566,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val newInfo = absInfo(localReturnType) if newInfo ne sym.info then val updatedInfo = - if sym.isAnonymousFunction || sym.is(Param) || sym.is(ParamAccessor) then + 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 if sym.isPrimaryConstructor then - newInfo else new LazyType: def complete(denot: SymDenotation)(using Context) = // infos of other methods are determined from their definitions which From 6ae1a89bf66c1c3f96e708060ba7f7f403db8488 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 6 Oct 2023 16:32:47 +0200 Subject: [PATCH 43/58] Work capture root determination into AsSeenFrom --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 5 ----- .../src/dotty/tools/dotc/core/TypeOps.scala | 16 ++++++++++++++-- tests/pos-custom-args/captures/test.scala | 17 ++++++++++++----- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 6af9e3ad56c8..d280664dd063 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -511,7 +511,7 @@ object CaptureSet: if elem.isRootCapability then res else res.orElse(addNewElems(elem.captureSetOfInfo.elems, origin)) else - //assert(id != 2, newElems) + //assert(id != 19 || !elem.isLocalRootCapability, elem.asInstanceOf[TermRef].localRootOwner) elems += elem if elem.isGenericRootCapability then rootAddedHandler() newElemAddedHandler(elem) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index bef7507850d6..8af9be85ebd7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -382,11 +382,6 @@ class CheckCaptures extends Recheck, SymTransformer: tp1 = mapRoots(sym.localRoot.termRef, rootVar)(tp1) if tp1 ne tpw then ccSetup.println(i"INST local $sym: $tp, ${sym.localRoot} = $tp1") - if sym.owner.isClass then - val tp2 = CaptureRoot.instantiateOuterClassRoots(sym, pre, rootVar)(tp1) - if tp2 ne tp1 then - ccSetup.println(i"INST class $sym: $tp, ${sym.localRoot} in $pre = $tp2") - tp1 = tp2 if tpw eq tp1 then tp else tp1 else tp diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 2b4dc05c1a16..abbaf77dc699 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._ @@ -93,13 +93,25 @@ 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.captureSet.impliedRoot(tp) + .showing(i"map local root $tp from $pre to $result", ccSetup) + 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/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) - () From e7d3b925b491127f4e761a7866c3b6db7f008574 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 7 Oct 2023 11:53:42 +0200 Subject: [PATCH 44/58] Keep track of failure traces when subCapturing fails under -Ycc-debug --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d280664dd063..8413aa1c1e93 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -417,7 +417,7 @@ object CaptureSet: def isAlwaysEmpty = elems.isEmpty def addNewElem(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = - CompareResult.Fail(this) + CompareResult.Fail(this :: Nil) def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK @@ -505,7 +505,7 @@ object CaptureSet: 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 + 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 @@ -518,7 +518,7 @@ object CaptureSet: // assert(id != 5 || elems.size != 3, this) (CompareResult.OK /: deps) { (r, dep) => r.andAlso(dep.tryInclude(elem, this)) - } + }.addToTrace(this) private def levelOK(elem: CaptureRef)(using Context): Boolean = !levelLimit.exists @@ -540,7 +540,7 @@ 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 @@ -684,7 +684,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 @@ -701,7 +701,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 } @@ -767,7 +767,7 @@ 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 = @@ -866,14 +866,16 @@ object CaptureSet: enum CompareResult extends Showable: case OK - case Fail(cs: CaptureSet) + 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(blocking: CaptureSet) => blocking.show + 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})") @@ -882,7 +884,7 @@ object CaptureSet: /** If not isOK, the blocking capture set */ def blocking: CaptureSet = (this: @unchecked) match - case Fail(cs) => cs + case Fail(cs) => cs.last case LevelError(cs, _) => cs /** Optionally, this result if it is a level error */ @@ -899,6 +901,10 @@ object CaptureSet: val alt = op if alt.isOK then alt else this + + 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 From f065d7558828d4b880f89bbe58a1c2eab88296fc Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 7 Oct 2023 14:55:40 +0200 Subject: [PATCH 45/58] Keep track of enclosing capture sets when comparing refined types When comparing refined types C{x: T}^cs <:< C{x: T'}^cs' we need to remember the capture set `cs` on the left hand side when we get to ask whether the refined type has a member `x: T'`, so that we can correctly instantiate any local root `cap[C]` in the member type of `x`. Same if the LHS is simply `C^cs`. In both cases we strip the capture set before we get to take the compute `x`, so we have to re-add it at the point of that member computation. --- .../dotty/tools/dotc/core/TypeComparer.scala | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 7c3f098d8011..d41b54c2c1fb 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -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,17 +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 - val tp2a = - if tp1.isBoxedCapturing && !parent1.isBoxedCapturing - then tp2.unboxed - else tp2 - recur(parent1, tp2a) - 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 => @@ -660,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) @@ -672,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 => @@ -2028,7 +2041,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: @@ -2118,9 +2131,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) @@ -3397,7 +3431,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) } From 1c294ef0506621afee183ec38d0bc2ce52929575 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 7 Oct 2023 14:56:48 +0200 Subject: [PATCH 46/58] Change owner of computed capture set for constructor applications --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 9 +++++++++ compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 8413aa1c1e93..748fd36f6be7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -274,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. @@ -752,6 +757,10 @@ 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) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 8af9be85ebd7..a7775a83d596 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -509,7 +509,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) From 7c345456528e8eaa52805e0a6845abd0ccc61b5f Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 7 Oct 2023 14:58:26 +0200 Subject: [PATCH 47/58] Treat added class refinements as inferred types for further processing Without that step, class refinements can contain references to the generic root `cap`. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 11 ++++---- tests/neg-custom-args/captures/i15772.check | 12 ++++----- tests/neg-custom-args/captures/lazylist.check | 4 +-- tests/neg-custom-args/captures/lazyref.check | 14 +++++------ .../captures/nested-classes-2.scala | 25 +++++++++++++++++++ tests/pos/functorial-functors.scala | 14 +++++++++++ 6 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 tests/pos-custom-args/captures/nested-classes-2.scala create mode 100644 tests/pos/functorial-functors.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 28fdaac3c165..cb46270d89e2 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -166,12 +166,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 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(using Context) = new TypeMap: + private def mapInferred(refine: Boolean)(using Context): TypeMap = new TypeMap: override def toString = "map inferred" /** Refine a possibly applied class type C where the class has tracked parameters @@ -179,13 +179,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: * 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(CaptureChecked) => cls.paramGetters.foldLeft(tp) { (core, getter) => if atPhase(thisPhase.next)(getter.termRef.isTracked) then - val getterType = tp.memberInfo(getter).strippedDealias + val getterType = + mapInferred(refine = false)(tp.memberInfo(getter)).strippedDealias RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.RefiningVar(NoSymbol, getter))) .showing(i"add capture refinement $tp --> $result", ccSetup) @@ -254,7 +255,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: end mapInferred private def transformInferredType(tp: Type)(using Context): Type = - mapInferred(tp) + mapInferred(refine = true)(tp) private def transformExplicitType(tp: Type, rootTarget: Symbol, tptToCheck: Option[Tree] = None)(using Context): Type = val expandAliases = new DeepTypeMap: diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index d66fe7a4d063..e6c2dff87dc7 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -6,8 +6,8 @@ -- [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..boxed1](from instantiating box1), c} Unit) ->{c} Unit - | Required: (C^{cap[boxed1]} ->{cap[boxed1]} 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 ------------------------------------------------------------ @@ -18,15 +18,15 @@ -- [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..boxed2](from instantiating box2), c} Unit) ->{c} Unit - | Required: (C^{cap[boxed2]} ->{cap[boxed2]} 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:33 --------------------------------------- 33 | val boxed2 : Observe[C]^ = box2(c) // error | ^^^^^^^ - | Found: (C{val arg: C^{cap[C]}}^{cap[main3]} ->{cap[main3]} Unit) ->{cap[main3]} Unit - | Required: (C ->{cap[boxed2]} Unit) ->{cap[boxed2]} Unit + | 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 ---------------------------------------- diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index f8f1c1df229f..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]^}^{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 ------------------------------------- diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index a9806bc5494d..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: () ->{cap[]} Int}^{cap2, ref1}) - | Required: LazyRef[Int]^{cap2} + | 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: () ->{cap[]} Int}^{cap2, ref1}) - | Required: LazyRef[Int]^{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: () ->{cap[]} Int}^{cap2, cap1}) - | Required: LazyRef[Int]^{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/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala new file mode 100644 index 000000000000..643584f3b719 --- /dev/null +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -0,0 +1,25 @@ + +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/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 + From b7d8047e7a73cd8af29ef05d9fea093eb0259a06 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 7 Oct 2023 16:18:44 +0200 Subject: [PATCH 48/58] A first attempt at level checking for classes Check that instantiated root variable of a method inside a class is nested in the instance variable of the class. For the moment I was not able to construct a counter example where this makes a difference. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 16 +++++-- .../captures/nesting-inversion.scala | 43 +++++++++++++++++++ .../captures/nested-classes-2.scala | 1 - 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 tests/neg-custom-args/captures/nesting-inversion.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a7775a83d596..aa776643d2c6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -319,7 +319,7 @@ class CheckCaptures extends Recheck, SymTransformer: includeCallCaptures(tree.symbol, tree.srcPos) else markFree(tree.symbol, tree.srcPos) - instantiateLocalRoots(tree.symbol, NoPrefix, pt): + instantiateLocalRoots(tree.symbol, NoPrefix, pt, tree.srcPos): super.recheckIdent(tree, pt) /** A specialized implementation of the selection rule. @@ -349,7 +349,7 @@ class CheckCaptures extends Recheck, SymTransformer: val selType = recheckSelection(tree, qualType, name, disambiguate) val selCs = selType.widen.captureSet - instantiateLocalRoots(tree.symbol, qualType, pt): + instantiateLocalRoots(tree.symbol, qualType, pt, tree.srcPos): if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then selType else @@ -370,7 +370,7 @@ class CheckCaptures extends Recheck, SymTransformer: * - `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)(tp: Type)(using Context): Type = + 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 @@ -379,6 +379,16 @@ class CheckCaptures extends Recheck, SymTransformer: 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") 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/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala index 643584f3b719..e9e74dedb7c1 100644 --- a/tests/pos-custom-args/captures/nested-classes-2.scala +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -20,6 +20,5 @@ def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = 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 From bf323db61b862db7a53c098e2978206c8b3a0933 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 8 Oct 2023 12:38:14 +0200 Subject: [PATCH 49/58] Drop instantiateOuterClassRoots --- .../src/dotty/tools/dotc/cc/CaptureRoot.scala | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 1295695cf52b..68323120c75b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -136,57 +136,6 @@ object CaptureRoot: case (r1: Var, r2) => r1.freshEnclosedBy(r1, r2) - /** A map that instantiates all outer class roots in the info of `sym` - * according to prefix `pre`. This is called for adapting the info of - * a selection `pre.sym`. The logic of the function is modeled after - * AsSeenFrom. But where AsSeenFrom maps a `this` of class `C` to a corresponding - * prefix, the present method maps a local root corresponding to a class to - * the root implied by the capture set of the corresponding prefix. - * @param sym the class member symbol whose info is mapped - * @param pre the prefix from which `sym` is selected - * @param deafilt the capture root to use if the capture set of the corresponding - * prfefix is empty. - */ - class instantiateOuterClassRoots(sym: Symbol, pre: Type, default: CaptureRoot)(using Context) extends ApproximatingTypeMap: - val cls = sym.owner.asClass - - def apply(tp: Type): Type = - - /** Analogous to `toPrefix` in `AssSeenFromMap`, but result prefix gets - * further mapped to a capture root via `impliedRoot`. - */ - def mapCaptureRoot(pre: Type, cls: Symbol, thiscls: ClassSymbol, fallBack: CaptureRoot): CaptureRoot = - if (pre eq NoType) || (pre eq NoPrefix) || (cls is PackageClass) then - fallBack - else pre match - case pre: SuperType => - mapCaptureRoot(pre.thistpe, cls, thiscls, fallBack) - case _ => - if thiscls.derivesFrom(cls) && pre.baseType(thiscls).exists then - pre.captureSet.impliedRoot(default) - else if pre.termSymbol.is(Package) && !thiscls.is(Package) then - mapCaptureRoot(pre.select(nme.PACKAGE), cls, thiscls, fallBack) - else - mapCaptureRoot(pre.baseType(cls).normalizedPrefix, cls.owner, thiscls, fallBack) - - def instRoot(elem: CaptureRef): CaptureRef = elem match - case elem: TermRef - if elem.name == nme.LOCAL_CAPTURE_ROOT && elem.symbol.owner.isLocalDummy => - mapCaptureRoot(pre, cls, elem.localRootOwner.asClass, elem) - .showing(i"mapped capture root $elem in $cls to $result", capt) - case _ => - elem - - tp match - case t @ CapturingType(parent, refs) => - val elems = refs.elems.toList - val elems1 = elems.mapConserve(instRoot) - val refs1 = if elems1 eq elems then refs else CaptureSet(elems1*) - t.derivedCapturingType(apply(parent), refs1) - case _ => - mapOver(tp) - end instantiateOuterClassRoots - end CaptureRoot From 0afe9239e890d6f4bf9a2be13a5c73ca92554565 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 8 Oct 2023 12:50:46 +0200 Subject: [PATCH 50/58] Disable some special cases about RefiningVars --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 31 +++++++++++-------- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 5cc2f0950f53..fdd2b128d627 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -17,16 +17,21 @@ 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 + + /** 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 + + val oldRefiningRoots = false +end ccConfig def allowUniversalInBoxed(using Context) = Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) @@ -86,7 +91,7 @@ class mapRoots(from0: CaptureRoot, to: CaptureRoot)(using Context) extends BiTyp def apply(t: Type): Type = if t eq from then to else t match - case t: CaptureRoot.Var if constrainRootsWhenMapping && t.unifiesWith(from) => + case t: CaptureRoot.Var if ccConfig.constrainRootsWhenMapping && t.unifiesWith(from) => to case t @ Setup.Box(t1) => t.derivedBox(this(t1)) @@ -129,7 +134,7 @@ extension (tree: Tree) * 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( @@ -228,7 +233,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,7 +246,7 @@ 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 diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index d41b54c2c1fb..78720ea6d96c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2117,7 +2117,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling ExprType(info1.resType) case info1 => info1 - if ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev then + if ccConfig.oldRefiningRoots && ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev then // When comparing against a RefiningVar refinement, map the // localRoot of the corresponding class in `tp1` to the owner of the // refining capture set. From db68fd9cf11375ce25095b0c35bea0ad94293bc1 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 8 Oct 2023 13:43:30 +0200 Subject: [PATCH 51/58] Introduce isCaptureChecking test --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 14 +++++++++++++- .../src/dotty/tools/dotc/cc/CapturingType.scala | 4 ++-- compiler/src/dotty/tools/dotc/cc/Synthetics.scala | 3 +-- .../src/dotty/tools/dotc/core/TypeComparer.scala | 11 ++++------- compiler/src/dotty/tools/dotc/core/Types.scala | 4 ++-- .../tools/dotc/transform/OverridingPairs.scala | 3 ++- compiler/src/dotty/tools/dotc/typer/Checking.scala | 5 +++-- .../dotty/tools/dotc/typer/ImportSuggestions.scala | 3 ++- .../src/dotty/tools/dotc/typer/RefChecks.scala | 4 ++-- 9 files changed, 31 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index fdd2b128d627..39dcad78d7cb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -30,12 +30,24 @@ object ccConfig: */ private[cc] val constrainRootsWhenMapping = true - val oldRefiningRoots = false + /** 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`) +/** Are we at checkCaptures phase? */ +def isCaptureChecking(using Context): Boolean = + ctx.phaseId == Phases.checkCapturesPhase.id + +/** 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 + /** A dependent function type with given arguments and result type * TODO Move somewhere else where we treat all function type related ops together. */ diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index ae7838662c1d..e3b30cb64f20 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -56,10 +56,10 @@ object CapturingType: /** 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 ctx.phaseId <= Phases.checkCapturesPhase.id => + if isCaptureCheckingOrSetup => Some((parent, ann.refs)) case AnnotatedType(parent, ann) - if ann.symbol == defn.RetainsAnnot && ctx.phase == Phases.checkCapturesPhase => + 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 diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 9cd7b7cec3cb..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 @@ -75,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)) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 78720ea6d96c..82dc04e8372d 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -657,7 +657,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def compareRefined: Boolean = val tp1w = tp1.widen - if ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev then + if isCaptureCheckingOrSetup then // A relaxed version of subtyping for dependent functions where method types // are treated as contravariant. @@ -1013,10 +1013,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def tp1widened = val tp1w = tp1.underlying.widenExpr tp1 match - case tp1: CaptureRef - if (ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev) - && tp1.isTracked - => + case tp1: CaptureRef if isCaptureCheckingOrSetup && tp1.isTracked => CapturingType(tp1w.stripCapturing, tp1.singletonCaptureSet) case _ => tp1w @@ -2117,7 +2114,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling ExprType(info1.resType) case info1 => info1 - if ccConfig.oldRefiningRoots && ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev 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. @@ -2262,7 +2259,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val paramsMatch = if precise then isSameTypeWhenFrozen(formal1, formal2a) - else if ctx.phase == Phases.checkCapturesPhase || ctx.phase == Phases.checkCapturesPhase.prev then + else if isCaptureCheckingOrSetup then // allow to constrain capture set variables isSubType(formal2a, formal1) else diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 3bc43837c5f1..14ccd75eb7d9 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, RetainingType, CaptureRoot} +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, RetainingType, CaptureRoot, isCaptureChecking} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -2205,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 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/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 c13b3854a7dd..1a7f75181bb2 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} 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 From 7fdb6afe5c6c749dcd74f6133624389f01f48fa7 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 8 Oct 2023 13:43:46 +0200 Subject: [PATCH 52/58] Restrict isUncachable --- compiler/src/dotty/tools/dotc/cc/CapturingType.scala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index e3b30cb64f20..294414a6c37b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -77,15 +77,10 @@ object CapturingType: 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) && decomposeCapturingType(tp).isDefined) + ctx.mode.is(Mode.IgnoreCaptures) && decomposeCapturingType(tp).isDefined end CapturingType From acb8232bdfb839062cf03dbe4a622d4cd6825acc Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 9 Oct 2023 09:47:22 +0200 Subject: [PATCH 53/58] Move most of self type setup to transformSym We need to do it for compiled as well as imported types. We only keep adding variables to selftypes of local classes in postProcess. This allows us to drop one occurrence of LooseRootChecking --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 28 ++++------ compiler/src/dotty/tools/dotc/cc/Setup.scala | 54 +++++++++---------- .../src/dotty/tools/dotc/core/Types.scala | 8 ++- 4 files changed, 41 insertions(+), 51 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 39dcad78d7cb..a78ec449bde9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -95,7 +95,7 @@ trait FollowAliases extends TypeMap: if t2 ne t1 then return t2 mapOver(t) -class mapRoots(from0: CaptureRoot, to: CaptureRoot)(using Context) extends BiTypeMap, FollowAliases: +class mapRoots(from0: CaptureRoot, to: CaptureRoot)(using Context) extends DeepTypeMap, BiTypeMap, FollowAliases: val from = from0.followAlias //override val toString = i"mapRoots($from, $to)" diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index aa776643d2c6..c09b8cb9da45 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1110,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) @@ -1238,16 +1233,15 @@ class CheckCaptures extends Recheck, SymTransformer: 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/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index cb46270d89e2..827dfdb37964 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -113,9 +113,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 = sym) + 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) @@ -123,7 +125,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: 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 + var newInfo = mappedInfo + if sym.is(ModuleVal) && !sym.isStatic then + val selfType1 = transformSelfType(sym.moduleClass) + if selfType1.exists then + newInfo = newInfo.capturing(selfType1.captureSet) + sym.termRef.invalidateCaches() + 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 @@ -131,6 +140,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else symd end transformSym + def transformSelfType(sym: Symbol)(using Context) = (sym.info: @unchecked) match + case ClassInfo(_, cls, _, _, selfInfo: Type) => + inContext(ctx.withOwner(cls)): + transformExplicitType(selfInfo, rootTarget = ctx.owner) + case _ => NoType + /** If `tp` is an unboxed capturing type or a function returning an unboxed capturing type, * convert it to be boxed. */ @@ -490,15 +505,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: postProcess(tree) end traverse - extension (sym: Symbol)(using Context) def unlessStatic = - val owner = sym.levelOwner - if sym.isStaticOwner then NoSymbol else owner - def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => val lowner = transformTT(tree, boxed = false, exact = false, - rootTarget = ctx.owner.unlessStatic // roots of other types in static locations are not mapped + 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 @@ -594,22 +606,13 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: tree.symbol match case cls: ClassSymbol => val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - val newSelfType = - 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 => - val selfInfo1 = inContext(ctx.withOwner(cls)): - transformExplicitType(selfInfo, rootTarget = ctx.owner) - if selfInfo1 eq selfInfo then NoType else selfInfo1 - case _ => - NoType - if newSelfType.exists then + 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. + val newSelfType = CapturingType(cinfo.selfType, CaptureSet.Var(cls)) ccSetup.println(i"mapped self type for $cls: $newSelfType, was $selfInfo") val ps1 = ps.mapConserve(transformExplicitType(_, rootTarget = ctx.owner)) val newInfo = ClassInfo(prefix, cls, ps1, decls, newSelfType) @@ -621,11 +624,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: updateInfo(modul, CapturingType(modul.info, newSelfType.captureSet)) modul.termRef.invalidateCaches() case _ => - val info = tree.symbol.info - val newInfo = transformExplicitType(info, rootTarget = ctx.owner.unlessStatic) - updateInfo(tree.symbol, newInfo) - if newInfo ne info then - ccSetup.println(i"update info of ${tree.symbol} from $info to $newInfo") case _ => end postProcess end setupTraverser diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 14ccd75eb7d9..edfeded26d6a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5114,10 +5114,8 @@ object Types { else if (clsd.is(Module)) givenSelf else if (ctx.erasedTypes) appliedRef else givenSelf.dealiasKeepAnnots match - case givenSelf1 @ CapturingType(tp, _) => - givenSelf1.derivedAnnotatedType(tp & appliedRef, givenSelf1.annot) - case givenSelf1 @ RetainingType(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) } @@ -5963,7 +5961,7 @@ object Types { val prefix1 = this(tp.prefix) val parents1 = tp.declaredParents mapConserve this val selfInfo1: TypeOrSymbol = tp.selfInfo match { - case selfInfo: Type => this(selfInfo) + case selfInfo: Type => inContext(ctx.withOwner(tp.cls))(this(selfInfo)) case selfInfo => selfInfo } tp.derivedClassInfo(prefix1, parents1, tp.decls, selfInfo1) From 79f05646659674211430ff6327a4d42679ffc002 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 9 Oct 2023 15:53:19 +0200 Subject: [PATCH 54/58] Make non-final impure classes level owners That's needed since a subclass might be a level owner. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a78ec449bde9..5fcd6d243076 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -397,8 +397,11 @@ extension (sym: Symbol) def isCaseClassSynthetic = // TODO drop symd.maybeOwner.isClass && symd.owner.is(Case) && symd.is(Synthetic) && symd.info.firstParamNames.isEmpty def classQualifies = - takesCappedParamIn(symd.primaryConstructor.info) - || symd.asClass.givenSelfType.hasUniversalRootOf(sym) + 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 From 9c71611c49b663482a031a889950b2de94802a8a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 9 Oct 2023 17:40:31 +0200 Subject: [PATCH 55/58] Translate local roots when doing override checks --- compiler/src/dotty/tools/dotc/typer/RefChecks.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 1a7f75181bb2..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, isCaptureChecking} +import cc.{mapRoots, localRoot, isCaptureChecking, isLevelOwner} object RefChecks { import tpd._ @@ -269,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 @@ -375,7 +375,11 @@ object RefChecks { def memberTp(self: Type) = if (member.isClass) TypeAlias(member.typeRef.EtaExpand(member.typeParams)) else self.memberInfo(member) - def otherTp(self: Type) = self.memberInfo(other) + 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)}") From 2c9f5b0f1ab391b9c850580320d62810c6583e5a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 9 Oct 2023 18:10:57 +0200 Subject: [PATCH 56/58] Map parents of classes with the class as owner. Also: Print local roots with id of owner under -uniqid --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 +++-- .../src/dotty/tools/dotc/core/Types.scala | 19 +++++++++---------- .../tools/dotc/printing/PlainPrinter.scala | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 827dfdb37964..2850a22200b1 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -613,10 +613,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: // why the class was picked as level owner. But self types should not be able // to mention other fields. val newSelfType = CapturingType(cinfo.selfType, CaptureSet.Var(cls)) - ccSetup.println(i"mapped self type for $cls: $newSelfType, was $selfInfo") - val ps1 = ps.mapConserve(transformExplicitType(_, rootTarget = ctx.owner)) + 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 diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index edfeded26d6a..7a4a19ea9d6e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5956,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 => inContext(ctx.withOwner(tp.cls))(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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 02fc1e44c12a..174454d24751 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -407,7 +407,7 @@ class PlainPrinter(_ctx: Context) extends Printer { 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}]") + else Str(s"cap[${nameString(tp.localRootOwner)}]") else toTextPrefixOf(tp) ~ selectionString(tp) case tp: ThisType => nameString(tp.cls) + ".this" From 5513ca2b1c62d12c07feedb15357a60d486a578f Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 9 Oct 2023 18:19:43 +0200 Subject: [PATCH 57/58] Fix CC asSeenFrom when prefix is `this` --- compiler/src/dotty/tools/dotc/core/TypeOps.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index abbaf77dc699..9eb18629b730 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -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 @@ -96,8 +96,9 @@ 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.captureSet.impliedRoot(tp) - .showing(i"map local root $tp from $pre to $result", ccSetup) + 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 From a521cf1559ed450195ae5a7516eb7f5b04bc831d Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 10 Oct 2023 10:48:25 +0200 Subject: [PATCH 58/58] Drop unnecessary code in transformSym The only time a nested object class could have a self type with a non-empty capture set is when it declared in the current compilation unit. Then that capture set is a Var that gets installed via an updateInfo. But then we do also an updateInfo of the module val, so no need to do the same thing in transformSym. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 13 +------------ .../src/dotty/tools/dotc/core/TypeComparer.scala | 1 - 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 2850a22200b1..7dacbb3e5e05 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -125,12 +125,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: symd.copySymDenotation(info = fluidify(sym.info)) else if symd.owner.isTerm || symd.is(CaptureChecked) || symd.owner.is(CaptureChecked) then val newFlags = newFlagsFor(symd) - var newInfo = mappedInfo - if sym.is(ModuleVal) && !sym.isStatic then - val selfType1 = transformSelfType(sym.moduleClass) - if selfType1.exists then - newInfo = newInfo.capturing(selfType1.captureSet) - sym.termRef.invalidateCaches() + val newInfo = mappedInfo if sym.isClass then sym.thisType.asInstanceOf[ThisType].invalidateCaches() if newFlags != symd.flags || (newInfo ne sym.info) @@ -140,12 +135,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else symd end transformSym - def transformSelfType(sym: Symbol)(using Context) = (sym.info: @unchecked) match - case ClassInfo(_, cls, _, _, selfInfo: Type) => - inContext(ctx.withOwner(cls)): - transformExplicitType(selfInfo, rootTarget = ctx.owner) - case _ => NoType - /** If `tp` is an unboxed capturing type or a function returning an unboxed capturing type, * convert it to be boxed. */ diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 82dc04e8372d..aa8933e19c39 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2411,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