From b98efc4e50a65e942ad558ee9b48534017865f60 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 22 Jul 2023 20:27:16 +0200 Subject: [PATCH 01/76] Drop another impure function adaptation This was overlooked before. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index d6c6dd9ec2c0..796f2b52abdc 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -164,7 +164,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 Feature.pureFunsEnabledSomewhere then + if adaptUnpickledFunctionTypes && Feature.pureFunsEnabledSomewhere then AnnotatedType(tp, CaptureAnnotation(CaptureSet.universal, boxed = false)(defn.RetainsByNameAnnot)) else From af631d59ca95340987ce451337981ab18a96500d Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 22 Jul 2023 20:27:47 +0200 Subject: [PATCH 02/76] Test case This demonstrates currently unsoundness when it comes to assignments via setters --- tests/neg-custom-args/captures/refs.scala | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/neg-custom-args/captures/refs.scala diff --git a/tests/neg-custom-args/captures/refs.scala b/tests/neg-custom-args/captures/refs.scala new file mode 100644 index 000000000000..df38027a5643 --- /dev/null +++ b/tests/neg-custom-args/captures/refs.scala @@ -0,0 +1,42 @@ +import java.io.* + +class Ref[T](init: T): + var x: T = init + def setX(x: T): Unit = this.x = x + +def usingLogFile[sealed T](op: FileOutputStream^ => T): T = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + +type Proc = () => Unit +def test1 = + usingLogFile[Proc]: f => // error + () => + f.write(1) + () + +def test2 = + val r = new Ref[Proc](() => ()) + usingLogFile[Unit]: f => + r.setX(() => f.write(10)) // should be error + r.x() // crash: f is closed at that point + +def test3 = + val r = new Ref[Proc](() => ()) + usingLogFile[Unit]: f => + r.x = () => f.write(10) // should be error + r.x() // crash: f is closed at that point + +def test4 = + var r: Proc = () => () // error + usingLogFile[Unit]: f => + r = () => f.write(10) + r() // crash: f is closed at that point + + + + + + From d071bf292a1068dfae4c81a1add8bae958a29dcc Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 24 Jul 2023 17:37:35 +0200 Subject: [PATCH 03/76] Simplify healTypeParam --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 42 ++++++++----------- .../captures/heal-tparam-cs.scala | 31 +++++++------- 2 files changed, 33 insertions(+), 40 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 66a7899a6bd0..edb7148f57eb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1032,9 +1032,9 @@ class CheckCaptures extends Recheck, SymTransformer: * that this type parameter can't see. * For example, when capture checking the following expression: * - * def usingLogFile[T](op: (f: {cap} File) => T): T = ... + * def usingLogFile[T](op: File^ => T): T = ... * - * usingLogFile[box ?1 () -> Unit] { (f: {cap} File) => () => { f.write(0) } } + * usingLogFile[box ?1 () -> Unit] { (f: File^) => () => { f.write(0) } } * * We may propagate `f` into ?1, making ?1 ill-formed. * This also causes soundness issues, since `f` in ?1 should be widened to `cap`, @@ -1046,34 +1046,26 @@ class CheckCaptures extends Recheck, SymTransformer: */ private def healTypeParam(tree: Tree)(using Context): Unit = val checker = new TypeTraverser: + private var allowed: SimpleIdentitySet[TermParamRef] = SimpleIdentitySet.empty + private def isAllowed(ref: CaptureRef): Boolean = ref match case ref: TermParamRef => allowed.contains(ref) case _ => true - // Widen the given term parameter refs x₁ : C₁ S₁ , ⋯ , xₙ : Cₙ Sₙ to their capture sets C₁ , ⋯ , Cₙ. - // - // If in these capture sets there are any capture references that are term parameter references we should avoid, - // we will widen them recursively. - private def widenParamRefs(refs: List[TermParamRef]): List[CaptureSet] = - @scala.annotation.tailrec - def recur(todos: List[TermParamRef], acc: List[CaptureSet]): List[CaptureSet] = - todos match - case Nil => acc - case ref :: rem => - val cs = ref.captureSetOfInfo - val nextAcc = cs.filter(isAllowed(_)) :: acc - val nextRem: List[TermParamRef] = (cs.elems.toList.filter(!isAllowed(_)) ++ rem).asInstanceOf - recur(nextRem, nextAcc) - recur(refs, Nil) - private def healCaptureSet(cs: CaptureSet): Unit = - def avoidance(elems: List[CaptureRef])(using Context): Unit = - val toInclude = widenParamRefs(elems.filter(!isAllowed(_)).asInstanceOf) - //println(i"HEAL $cs by widening to $toInclude") - toInclude.foreach(checkSubset(_, cs, tree.srcPos)) - cs.ensureWellformed(avoidance) - - private var allowed: SimpleIdentitySet[TermParamRef] = SimpleIdentitySet.empty + cs.ensureWellformed: elems => + ctx ?=> + var seen = new util.HashSet[CaptureRef] + def recur(elems: List[CaptureRef]): Unit = + for ref <- elems do + if !isAllowed(ref) && !seen.contains(ref) then + seen += ref + 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 traverse(tp: Type) = tp match diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index 58d12f8b6ce5..c0fa29bcca3b 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -2,32 +2,33 @@ import language.experimental.captureChecking trait Cap { def use(): Unit } -def localCap[sealed T](op: (cap: Cap^{cap}) => T): T = ??? +def localCap[sealed T](op: (c: Cap^{cap}) => T): T = ??? def main(io: Cap^{cap}, net: Cap^{cap}): Unit = { - val test1 = localCap { cap => // error - () => { cap.use() } + + val test1 = localCap { c => // error + () => { c.use() } } - val test2: (cap: Cap^{cap}) -> () ->{cap} Unit = - localCap { cap => // should work - (cap1: Cap^{cap}) => () => { cap1.use() } + val test2: (c: Cap^{cap}) -> () ->{cap} Unit = + localCap { c => // should work + (c1: Cap^{cap}) => () => { c1.use() } } - val test3: (cap: Cap^{io}) -> () ->{io} Unit = - localCap { cap => // should work - (cap1: Cap^{io}) => () => { cap1.use() } + val test3: (c: Cap^{io}) -> () ->{io} Unit = + localCap { c => // should work + (c1: Cap^{io}) => () => { c1.use() } } - val test4: (cap: Cap^{io}) -> () ->{net} Unit = - localCap { cap => // error - (cap1: Cap^{io}) => () => { cap1.use() } + val test4: (c: Cap^{io}) -> () ->{net} Unit = + localCap { c => // error + (c1: Cap^{io}) => () => { c1.use() } } - def localCap2[sealed T](op: (cap: Cap^{io}) => T): T = ??? + def localCap2[sealed T](op: (c: Cap^{io}) => T): T = ??? val test5: () ->{io} Unit = - localCap2 { cap => // ok - () => { cap.use() } + localCap2 { c => // ok + () => { c.use() } } } From 3b035921f5754c000cbca6bb0ef6da8f5b23cb99 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 2 Aug 2023 13:13:42 +0200 Subject: [PATCH 04/76] Fix installAfter corner case There was a corner case in installAfter where - A denotation valid in a single phase got replaced by another one - Immediately after, the symbol's denotation would be forced in a previous phase This somehow landed on a wrong denotation. The problem got apparent when more symbols underwent a Recheck.updateInfoBetween. The flags field installed by a previous update somehow was not recognized anymore. Specifically, the following was observed in order: 1. For a parameter getter (xs in LazyList, file pos-custeom-args/captures/lazylists1.scala) the Private flag was suppressed via transformInfo at phase cc. 2. The denotation of the getter v which was valid in the single phase cc+1 was updated at at cc by updateInfoInBetween in Recheck so that the Private flag was re-asserted in cc+1. 3. Immediately afterwards, the getter's flags was demanded at phase cc. 4. The Private flag was present, even though it should not be. The problem was fixed by demanding the denotation of the getter as part of isntallAfter. --- compiler/src/dotty/tools/dotc/core/Denotations.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Denotations.scala b/compiler/src/dotty/tools/dotc/core/Denotations.scala index a478d60ce348..640ba8015be7 100644 --- a/compiler/src/dotty/tools/dotc/core/Denotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Denotations.scala @@ -884,7 +884,6 @@ object Denotations { /** Install this denotation to be the result of the given denotation transformer. * This is the implementation of the same-named method in SymDenotations. * It's placed here because it needs access to private fields of SingleDenotation. - * @pre Can only be called in `phase.next`. */ protected def installAfter(phase: DenotTransformer)(using Context): Unit = { val targetId = phase.next.id @@ -892,16 +891,21 @@ object Denotations { else { val current = symbol.current // println(s"installing $this after $phase/${phase.id}, valid = ${current.validFor}") - // printPeriods(current) + // println(current.definedPeriodsString) this.validFor = Period(ctx.runId, targetId, current.validFor.lastPhaseId) if (current.validFor.firstPhaseId >= targetId) current.replaceWith(this) + symbol.denot + // Let symbol point to updated denotation + // Without this we can get problems when we immediately recompute the denotation + // at another phase since the invariant that symbol used to point to a valid + // denotation is lost. else { current.validFor = Period(ctx.runId, current.validFor.firstPhaseId, targetId - 1) insertAfter(current) } + // println(current.definedPeriodsString) } - // printPeriods(this) } /** Apply a transformation `f` to all denotations in this group that start at or after From 7ab82c0509546732392d6355d8d426b44d04d532 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 10 Aug 2023 19:13:46 +0200 Subject: [PATCH 05/76] Change closure handling Constrain closure parameters and result from expected type before rechecking the closure's body. This gives more precise types and avoids the spurious duplication of some variables. It also avoids the unmotivated special case that we needed before to make tests pass. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 83 +++++----------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 16 ++- .../dotty/tools/dotc/transform/Recheck.scala | 99 +++++++++++-------- tests/neg-custom-args/captures/capt1.check | 2 +- tests/neg-custom-args/captures/try.check | 2 +- tests/neg-custom-args/captures/try.scala | 2 +- .../pos-custom-args/captures/bynamefun.scala | 7 +- 7 files changed, 106 insertions(+), 105 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index edb7148f57eb..30a9e7deb0e5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -408,10 +408,16 @@ class CheckCaptures extends Recheck, SymTransformer: else if meth == defn.Caps_unsafeUnbox then mapArgUsing(_.forceBoxStatus(false)) else if meth == defn.Caps_unsafeBoxFunArg then - mapArgUsing: + def forceBox(tp: Type): Type = tp match case defn.FunctionOf(paramtpe :: Nil, restpe, isContextual) => defn.FunctionOf(paramtpe.forceBoxStatus(true) :: Nil, restpe, isContextual) - + case tp @ RefinedType(parent, rname, rinfo: MethodType) => + tp.derivedRefinedType(parent, rname, + rinfo.derivedLambdaType( + paramInfos = rinfo.paramInfos.map(_.forceBoxStatus(true)))) + case tp @ CapturingType(parent, refs) => + tp.derivedCapturingType(forceBox(parent), refs) + mapArgUsing(forceBox) else super.recheckApply(tree, pt) match case appType @ CapturingType(appType1, refs) => @@ -485,63 +491,28 @@ class CheckCaptures extends Recheck, SymTransformer: else ownType end instantiate - override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = + override def recheckClosure(tree: Closure, pt: Type, forceDependent: Boolean)(using Context): Type = val cs = capturedVars(tree.meth.symbol) capt.println(i"typing closure $tree with cvs $cs") - super.recheckClosure(tree, pt).capturing(cs) - .showing(i"rechecked $tree / $pt = $result", capt) - - /** Additionally to normal processing, update types of closures if the expected type - * is a function with only pure parameters. In that case, make the anonymous function - * also have the same parameters as the prototype. - * TODO: Develop a clearer rationale for this. - * TODO: Can we generalize this to arbitrary parameters? - * Currently some tests fail if we do this. (e.g. neg.../stackAlloc.scala, others) - */ - override def recheckBlock(block: Block, pt: Type)(using Context): Type = - block match - case closureDef(mdef) => - pt.dealias match - case defn.FunctionOf(ptformals, _, _) - if ptformals.nonEmpty && ptformals.forall(_.captureSet.isAlwaysEmpty) => - // Redo setup of the anonymous function so that formal parameters don't - // get capture sets. This is important to avoid false widenings to `cap` - // when taking the base type of the actual closures's dependent function - // type so that it conforms to the expected non-dependent function type. - // See withLogFile.scala for a test case. - val meth = mdef.symbol - // First, undo the previous setup which installed a completer for `meth`. - atPhase(preRecheckPhase.prev)(meth.denot.copySymDenotation()) - .installAfter(preRecheckPhase) - - // Next, update all parameter symbols to match expected formals - meth.paramSymss.head.lazyZip(ptformals).foreach: (psym, pformal) => - psym.updateInfoBetween(preRecheckPhase, thisPhase, pformal.mapExprType) - - // Next, update types of parameter ValDefs - mdef.paramss.head.lazyZip(ptformals).foreach: (param, pformal) => - val ValDef(_, tpt, _) = param: @unchecked - tpt.rememberTypeAlways(pformal) - - // Next, install a new completer reflecting the new parameters for the anonymous method - val mt = meth.info.asInstanceOf[MethodType] - val completer = new LazyType: - def complete(denot: SymDenotation)(using Context) = - denot.info = mt.companion(ptformals, mdef.tpt.knownType) - .showing(i"simplify info of $meth to $result", capt) - recheckDef(mdef, meth) - meth.updateInfoBetween(preRecheckPhase, thisPhase, completer) - case _ => - mdef.rhs match - case rhs @ closure(_, _, _) => - // In a curried closure `x => y => e` don't leak capabilities retained by - // the second closure `y => e` into the first one. This is an approximation - // of the CC rule which says that a closure contributes captures to its - // environment only if a let-bound reference to the closure is used. - mdef.rhs.putAttachment(ClosureBodyValue, ()) - case _ => + super.recheckClosure(tree, pt, forceDependent).capturing(cs) + .showing(i"rechecked closure $tree / $pt = $result", capt) + + override def recheckClosureBlock(mdef: DefDef, expr: Closure, pt: Type)(using Context): Type = + mdef.rhs match + case rhs @ closure(_, _, _) => + // In a curried closure `x => y => e` don't leak capabilities retained by + // the second closure `y => e` into the first one. This is an approximation + // of the CC rule which says that a closure contributes captures to its + // environment only if a let-bound reference to the closure is used. + mdef.rhs.putAttachment(ClosureBodyValue, ()) case _ => - super.recheckBlock(block, pt) + + // Constrain closure's parameters and result from the expected type before + // rechecking the body. + val res = recheckClosure(expr, pt, forceDependent = true) + recheckDef(mdef, mdef.symbol) + res + end recheckClosureBlock override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = try diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index b66b9f2b2277..360a791c4f7a 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -350,11 +350,17 @@ extends tpd.TreeTraverser: val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) .showing(i"update info $sym: ${sym.info} --> $result", capt) if newInfo ne sym.info then - val completer = new LazyType: - def complete(denot: SymDenotation)(using Context) = - denot.info = newInfo - recheckDef(tree, sym) - updateInfo(sym, completer) + 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 other methods are determined from their definitions which + // are checked on depand + denot.info = newInfo + recheckDef(tree, sym)) case tree: Bind => val sym = tree.symbol updateInfo(sym, transformInferredType(sym.info, boxed = false)) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 593018ceb965..2e64ffc9bbf4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -52,17 +52,18 @@ object Recheck: */ def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: DenotTransformer, newInfo: Type)(using Context): Unit = if sym.info ne newInfo then + val flags = sym.flags sym.copySymDenotation( initFlags = - if sym.flags.isAllOf(ResetPrivateParamAccessor) - then sym.flags &~ ResetPrivate | Private - else sym.flags + if flags.isAllOf(ResetPrivateParamAccessor) + then flags &~ ResetPrivate | Private + else flags ).installAfter(lastPhase) // reset sym.copySymDenotation( info = newInfo, initFlags = - if newInfo.isInstanceOf[LazyType] then sym.flags &~ Touched - else sym.flags + if newInfo.isInstanceOf[LazyType] then flags &~ Touched + else flags ).installAfter(prevPhase) /** Does symbol have a new denotation valid from phase.next that is different @@ -96,17 +97,44 @@ object Recheck: case Some(tpe) => tree.withType(tpe).asInstanceOf[T] case None => tree - extension (tpe: Type) - - /** Map ExprType => T to () ?=> T (and analogously for pure versions). - * Even though this phase runs after ElimByName, ExprTypes can still occur - * as by-name arguments of applied types. See note in doc comment for - * ElimByName phase. Test case is bynamefun.scala. - */ - def mapExprType(using Context): Type = tpe match - case ExprType(rt) => defn.ByNameFunction(rt) - case _ => tpe + /** Map ExprType => T to () ?=> T (and analogously for pure versions). + * Even though this phase runs after ElimByName, ExprTypes can still occur + * as by-name arguments of applied types. See note in doc comment for + * ElimByName phase. Test case is bynamefun.scala. + */ + private def mapExprType(tp: Type)(using Context): Type = tp match + case ExprType(rt) => defn.ByNameFunction(rt) + case _ => tp + + /** Normalize `=> A` types to `() ?=> A` types + * - at the top level + * - in function and method parameter types + * - under annotations + */ + def normalizeByName(tp: Type)(using Context): Type = tp match + case tp: ExprType => + mapExprType(tp) + case tp: PolyType => + tp.derivedLambdaType(resType = normalizeByName(tp.resType)) + case tp: MethodType => + tp.derivedLambdaType( + paramInfos = tp.paramInfos.mapConserve(mapExprType), + resType = normalizeByName(tp.resType)) + case tp @ RefinedType(parent, nme.apply, rinfo) if defn.isFunctionType(tp) => + tp.derivedRefinedType(parent, nme.apply, normalizeByName(rinfo)) + case tp @ defn.FunctionOf(pformals, restpe, isContextual) => + val pformals1 = pformals.mapConserve(mapExprType) + val restpe1 = normalizeByName(restpe) + if (pformals1 ne pformals) || (restpe1 ne restpe) then + defn.FunctionOf(pformals1, restpe1, isContextual) + else + tp + case tp @ AnnotatedType(parent, ann) => + tp.derivedAnnotatedType(normalizeByName(parent), ann) + case _ => + tp +end Recheck /** A base class that runs a simplified typer pass over an already re-typed program. The pass * does not transform trees but returns instead the re-typed type of each tree as it is @@ -183,27 +211,16 @@ abstract class Recheck extends Phase, SymTransformer: else AnySelectionProto recheckSelection(tree, recheck(qual, proto).widenIfUnstable, name, pt) - /** When we select the `apply` of a function with type such as `(=> A) => B`, - * we need to convert the parameter type `=> A` to `() ?=> A`. See doc comment - * of `mapExprType`. - */ - def normalizeByName(mbr: SingleDenotation)(using Context): SingleDenotation = mbr.info match - case mt: MethodType if mt.paramInfos.exists(_.isInstanceOf[ExprType]) => - mbr.derivedSingleDenotation(mbr.symbol, - mt.derivedLambdaType(paramInfos = mt.paramInfos.map(_.mapExprType))) - case _ => - mbr - def recheckSelection(tree: Select, qualType: Type, name: Name, sharpen: Denotation => Denotation)(using Context): Type = if name.is(OuterSelectName) then tree.tpe else //val pre = ta.maybeSkolemizePrefix(qualType, name) - val mbr = normalizeByName( + val mbr = sharpen( qualType.findMember(name, qualType, excluded = if tree.symbol.is(Private) then EmptyFlags else Private - )).suchThat(tree.symbol == _)) + )).suchThat(tree.symbol == _) val newType = tree.tpe match case prevType: NamedType => val prevDenot = prevType.denot @@ -281,7 +298,7 @@ abstract class Recheck extends Phase, SymTransformer: else fntpe.paramInfos def recheckArgs(args: List[Tree], formals: List[Type], prefs: List[ParamRef]): List[Type] = args match case arg :: args1 => - val argType = recheck(arg, formals.head.mapExprType) + val argType = recheck(arg, normalizeByName(formals.head)) val formals1 = if fntpe.isParamDependent then formals.tail.map(_.substParam(prefs.head, argType)) @@ -313,27 +330,33 @@ abstract class Recheck extends Phase, SymTransformer: recheck(tree.rhs, lhsType.widen) defn.UnitType - def recheckBlock(stats: List[Tree], expr: Tree, pt: Type)(using Context): Type = + private def recheckBlock(stats: List[Tree], expr: Tree)(using Context): Type = recheckStats(stats) val exprType = recheck(expr) + TypeOps.avoid(exprType, localSyms(stats).filterConserve(_.isTerm)) + + def recheckBlock(tree: Block, pt: Type)(using Context): Type = tree match + case Block(Nil, expr: Block) => recheckBlock(expr, pt) + case Block((mdef : DefDef) :: Nil, closure: Closure) => + recheckClosureBlock(mdef, closure.withSpan(tree.span), pt) + case Block(stats, expr) => recheckBlock(stats, expr) // The expected type `pt` is not propagated. Doing so would allow variables in the // expected type to contain references to local symbols of the block, so the // local symbols could escape that way. - TypeOps.avoid(exprType, localSyms(stats).filterConserve(_.isTerm)) - def recheckBlock(tree: Block, pt: Type)(using Context): Type = - recheckBlock(tree.stats, tree.expr, pt) + def recheckClosureBlock(mdef: DefDef, expr: Closure, pt: Type)(using Context): Type = + recheckBlock(mdef :: Nil, expr) def recheckInlined(tree: Inlined, pt: Type)(using Context): Type = - recheckBlock(tree.bindings, tree.expansion, pt)(using inlineContext(tree)) + recheckBlock(tree.bindings, tree.expansion)(using inlineContext(tree)) def recheckIf(tree: If, pt: Type)(using Context): Type = recheck(tree.cond, defn.BooleanType) recheck(tree.thenp, pt) | recheck(tree.elsep, pt) - def recheckClosure(tree: Closure, pt: Type)(using Context): Type = + def recheckClosure(tree: Closure, pt: Type, forceDependent: Boolean = false)(using Context): Type = if tree.tpt.isEmpty then - tree.meth.tpe.widen.toFunctionType(isJava = tree.meth.symbol.is(JavaDefined)) + tree.meth.tpe.widen.toFunctionType(tree.meth.symbol.is(JavaDefined), alwaysDependent = forceDependent) else recheck(tree.tpt) @@ -534,9 +557,7 @@ abstract class Recheck extends Phase, SymTransformer: /** Check that widened types of `tpe` and `pt` are compatible. */ def checkConforms(tpe: Type, pt: Type, tree: Tree)(using Context): Unit = tree match - case _: DefTree | EmptyTree | _: TypeTree | _: Closure => - // Don't report closure nodes, since their span is a point; wait instead - // for enclosing block to preduce an error + case _: DefTree | EmptyTree | _: TypeTree => case _ => checkConformsExpr(tpe.widenExpr, pt.widenExpr, tree) diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 85d3b2a7ddcb..6b4c50b69ae4 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -15,7 +15,7 @@ -- [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 | ^ - | Found: Int ->{x} Int + | Found: (y: Int) ->{x} Int | Required: Matchable 15 | f | diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 9afbe61d2280..c9b7910ad534 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -6,7 +6,7 @@ | This is often caused by a local capability in an argument of method handle | leaking as part of its result. -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:29:43 ------------------------------------------ -29 | val b = handle[Exception, () -> Nothing] { // error +29 | val b = handle[Exception, () -> Nothing] { // error | ^ | Found: (x: CT[Exception]^) ->? () ->{x} Nothing | Required: (x$0: CanThrow[Exception]) => () -> Nothing diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 3c6f0605d8b9..55e065de9f9f 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -26,7 +26,7 @@ def test = (ex: Exception) => ??? } - val b = handle[Exception, () -> Nothing] { // error + val b = handle[Exception, () -> Nothing] { // error (x: CanThrow[Exception]) => () => raise(new Exception)(using x) } { (ex: Exception) => ??? diff --git a/tests/pos-custom-args/captures/bynamefun.scala b/tests/pos-custom-args/captures/bynamefun.scala index 86bad201ffc3..414f0c46c42f 100644 --- a/tests/pos-custom-args/captures/bynamefun.scala +++ b/tests/pos-custom-args/captures/bynamefun.scala @@ -1,11 +1,14 @@ object test: class Plan(elem: Plan) object SomePlan extends Plan(???) + type PP = (-> Plan) -> Plan def f1(expr: (-> Plan) -> Plan): Plan = expr(SomePlan) f1 { onf => Plan(onf) } def f2(expr: (=> Plan) -> Plan): Plan = ??? f2 { onf => Plan(onf) } def f3(expr: (-> Plan) => Plan): Plan = ??? - f1 { onf => Plan(onf) } + f3 { onf => Plan(onf) } def f4(expr: (=> Plan) => Plan): Plan = ??? - f2 { onf => Plan(onf) } + f4 { onf => Plan(onf) } + def f5(expr: PP): Plan = expr(SomePlan) + f5 { onf => Plan(onf) } \ No newline at end of file From 12e80fed733ec02fa1fce0f2b85b561434cfec24 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Aug 2023 14:42:50 +0200 Subject: [PATCH 06/76] Fix SimpleIdentitySet#map Previously, the result of a map could contain duplicates. I verified that with the current code base this could cause problems only for capture checking. --- .../tools/dotc/util/SimpleIdentitySet.scala | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index dd766dc99c7e..d7e1a60f56fa 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -14,7 +14,10 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { def contains[E >: Elem <: AnyRef](x: E): Boolean def foreach(f: Elem => Unit): Unit def exists[E >: Elem <: AnyRef](p: E => Boolean): Boolean - def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] + def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = + var acc: SimpleIdentitySet[B] = SimpleIdentitySet.empty + foreach(x => acc += f(x)) + acc def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A def toList: List[Elem] def iterator: Iterator[Elem] @@ -63,7 +66,7 @@ object SimpleIdentitySet { def contains[E <: AnyRef](x: E): Boolean = false def foreach(f: Nothing => Unit): Unit = () def exists[E <: AnyRef](p: E => Boolean): Boolean = false - def map[B <: AnyRef](f: Nothing => B): SimpleIdentitySet[B] = empty + override def map[B <: AnyRef](f: Nothing => B): SimpleIdentitySet[B] = empty def /: [A, E <: AnyRef](z: A)(f: (A, E) => A): A = z def toList = Nil def iterator = Iterator.empty @@ -79,7 +82,7 @@ object SimpleIdentitySet { def foreach(f: Elem => Unit): Unit = f(x0.asInstanceOf[Elem]) def exists[E >: Elem <: AnyRef](p: E => Boolean): Boolean = p(x0.asInstanceOf[E]) - def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = + override def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = Set1(f(x0.asInstanceOf[Elem])) def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(z, x0.asInstanceOf[E]) @@ -99,8 +102,10 @@ object SimpleIdentitySet { def foreach(f: Elem => Unit): Unit = { f(x0.asInstanceOf[Elem]); f(x1.asInstanceOf[Elem]) } def exists[E >: Elem <: AnyRef](p: E => Boolean): Boolean = p(x0.asInstanceOf[E]) || p(x1.asInstanceOf[E]) - def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = - Set2(f(x0.asInstanceOf[Elem]), f(x1.asInstanceOf[Elem])) + override def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = + val y0 = f(x0.asInstanceOf[Elem]) + val y1 = f(x1.asInstanceOf[Elem]) + if y0 eq y1 then Set1(y0) else Set2(y0, y1) def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: Nil @@ -133,8 +138,12 @@ object SimpleIdentitySet { } def exists[E >: Elem <: AnyRef](p: E => Boolean): Boolean = p(x0.asInstanceOf[E]) || p(x1.asInstanceOf[E]) || p(x2.asInstanceOf[E]) - def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = - Set3(f(x0.asInstanceOf[Elem]), f(x1.asInstanceOf[Elem]), f(x2.asInstanceOf[Elem])) + override def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = + val y0 = f(x0.asInstanceOf[Elem]) + val y1 = f(x1.asInstanceOf[Elem]) + val y2 = f(x2.asInstanceOf[Elem]) + if (y0 ne y1) && (y0 ne y2) && (y1 ne y2) then Set3(y0, y1, y2) + else super.map(f) def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]), x2.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: x2.asInstanceOf[Elem] :: Nil @@ -182,8 +191,6 @@ object SimpleIdentitySet { } def exists[E >: Elem <: AnyRef](p: E => Boolean): Boolean = xs.asInstanceOf[Array[E]].exists(p) - def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] = - SetN(xs.map(x => f(x.asInstanceOf[Elem]).asInstanceOf[AnyRef])) def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = xs.asInstanceOf[Array[E]].foldLeft(z)(f) def toList: List[Elem] = { From 78df620b31d283c11a8e77bb57f853e616778204 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Aug 2023 14:43:57 +0200 Subject: [PATCH 07/76] Use a BiTypeMap for substitutions when possible This reduces the chance of information loss in capture set propagation for applications. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 30a9e7deb0e5..f7b057900a6c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -73,7 +73,6 @@ object CheckCaptures: /** Similar normal substParams, but this is an approximating type map that * maps parameters in contravariant capture sets to the empty set. - * TODO: check what happens with non-variant. */ final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends ApproximatingTypeMap, IdempotentCaptRefMap: @@ -96,6 +95,36 @@ object CheckCaptures: mapOver(tp) end SubstParamsMap + final class SubstParamsBiMap(from: LambdaType, to: List[Type])(using Context) + extends BiTypeMap: + + def apply(tp: Type): Type = tp match + case tp: ParamRef => + if tp.binder == from then to(tp.paramNum) else tp + case tp: NamedType => + if tp.prefix `eq` NoPrefix then tp + else tp.derivedSelect(apply(tp.prefix)) + case _: ThisType => + tp + case _ => + mapOver(tp) + + def inverse(tp: Type): Type = tp match + case tp: NamedType => + var idx = 0 + var to1 = to + while idx < to.length && (tp ne to(idx)) do + idx += 1 + to1 = to1.tail + if idx < to.length then from.paramRefs(idx) + else if tp.prefix `eq` NoPrefix then tp + else tp.derivedSelect(apply(tp.prefix)) + case _: ThisType => + tp + case _ => + mapOver(tp) + end SubstParamsBiMap + /** Check that a @retains annotation only mentions references that can be tracked. * This check is performed at Typer. */ @@ -437,6 +466,14 @@ class CheckCaptures extends Recheck, SymTransformer: case appType => appType end recheckApply + private def isDistinct(xs: List[Type]): Boolean = xs match + case x :: xs1 => xs1.isEmpty || !xs1.contains(x) && isDistinct(xs1) + case Nil => true + + private def isTrackable(tp: Type)(using Context) = tp match + case tp: CaptureRef => tp.canBeTracked + case _ => false + /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. * This means: * - Instantiate result type with actual arguments @@ -444,11 +481,19 @@ class CheckCaptures extends Recheck, SymTransformer: * - remember types of arguments corresponding to tracked * parameters in refinements. * - add capture set of instantiated class to capture set of result type. + * If all argument types are mutually disfferent trackable capture references, use a BiTypeMap, + * since that is more precise. Otherwise use a normal idempotent map, which might lose information + * in the case where the result type contains captureset variables that are further + * constrained afterwards. */ override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = val ownType = - if mt.isResultDependent then SubstParamsMap(mt, argTypes)(mt.resType) - else mt.resType + if !mt.isResultDependent then + mt.resType + else if argTypes.forall(isTrackable) && isDistinct(argTypes) then + SubstParamsBiMap(mt, argTypes)(mt.resType) + else + SubstParamsMap(mt, argTypes)(mt.resType) if sym.isConstructor then val cls = sym.owner.asClass From d5bb4d881003391a1e66ae27b752422e120fa752 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Aug 2023 15:15:44 +0200 Subject: [PATCH 08/76] Cleanup SubstParamsMap --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 10 +++---- .../src/dotty/tools/dotc/core/Types.scala | 26 +++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f7b057900a6c..47ef2df93549 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -77,7 +77,7 @@ object CheckCaptures: final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends ApproximatingTypeMap, IdempotentCaptRefMap: /** This SubstParamsMap is exact if `to` only contains `CaptureRef`s. */ - private val isExactSubstitution: Boolean = to.forall(_.isInstanceOf[CaptureRef]) + private val isExactSubstitution: Boolean = to.forall(_.isTrackableRef) /** As long as this substitution is exact, there is no need to create `Range`s when mapping invariant positions. */ override protected def needsRangeIfInvariant(refs: CaptureSet): Boolean = !isExactSubstitution @@ -136,7 +136,7 @@ object CheckCaptures: for elem <- retainedElems(ann) do elem.tpe match case ref: CaptureRef => - if !ref.canBeTracked then + if !ref.isTrackableRef then report.error(em"$elem cannot be tracked since it is not a parameter or local value", elem.srcPos) case tpe => report.error(em"$elem: $tpe is not a legal element of a capture set", elem.srcPos) @@ -470,10 +470,6 @@ class CheckCaptures extends Recheck, SymTransformer: case x :: xs1 => xs1.isEmpty || !xs1.contains(x) && isDistinct(xs1) case Nil => true - private def isTrackable(tp: Type)(using Context) = tp match - case tp: CaptureRef => tp.canBeTracked - case _ => false - /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. * This means: * - Instantiate result type with actual arguments @@ -490,7 +486,7 @@ class CheckCaptures extends Recheck, SymTransformer: val ownType = if !mt.isResultDependent then mt.resType - else if argTypes.forall(isTrackable) && isDistinct(argTypes) then + else if argTypes.forall(_.isTrackableRef) && isDistinct(argTypes) then SubstParamsBiMap(mt, argTypes)(mt.resType) else SubstParamsMap(mt, argTypes)(mt.resType) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 5265a882a6e9..410b3b9abbaa 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -483,6 +483,11 @@ object Types { */ def isDeclaredVarianceLambda: Boolean = false + /** Is this type a CaptureRef that can be tracked? + * This is true for all ThisTypes or ParamRefs but only for some NamedTypes. + */ + def isTrackableRef(using Context): Boolean = false + /** Does this type contain wildcard types? */ final def containsWildcardTypes(using Context) = existsPart(_.isInstanceOf[WildcardType], StopAt.Static, forceLazy = false) @@ -2157,15 +2162,10 @@ object Types { private var myCaptureSetRunId: Int = NoRunId private var mySingletonCaptureSet: CaptureSet.Const | Null = null - /** Can the reference be tracked? This is true for all ThisTypes or ParamRefs - * but only for some NamedTypes. - */ - def canBeTracked(using Context): Boolean - /** Is the reference tracked? This is true if it can be tracked and the capture * set of the underlying type is not always empty. */ - final def isTracked(using Context): Boolean = canBeTracked && !captureSetOfInfo.isAlwaysEmpty + final def isTracked(using Context): Boolean = isTrackableRef && !captureSetOfInfo.isAlwaysEmpty /** Is this reference the root capability `cap` ? */ def isRootCapability(using Context): Boolean = false @@ -2198,7 +2198,7 @@ object Types { override def captureSet(using Context): CaptureSet = val cs = captureSetOfInfo - if canBeTracked && !cs.isAlwaysEmpty then singletonCaptureSet else cs + if isTrackableRef && !cs.isAlwaysEmpty then singletonCaptureSet else cs end CaptureRef /** A trait for types that bind other types that refer to them. @@ -2895,7 +2895,7 @@ object Types { * They are subsumed in the capture sets of the enclosing class. * TODO: ^^^ What about call-by-name? */ - def canBeTracked(using Context) = + override def isTrackableRef(using Context) = ((prefix eq NoPrefix) || symbol.is(ParamAccessor) && (prefix eq symbol.owner.thisType) || isRootCapability @@ -2905,7 +2905,7 @@ object Types { name == nme.CAPTURE_ROOT && symbol == defn.captureRoot override def normalizedRef(using Context): CaptureRef = - if canBeTracked then symbol.termRef else this + if isTrackableRef then symbol.termRef else this } abstract case class TypeRef(override val prefix: Type, @@ -3058,7 +3058,7 @@ object Types { // can happen in IDE if `cls` is stale } - def canBeTracked(using Context) = true + override def isTrackableRef(using Context) = true override def computeHash(bs: Binders): Int = doHash(bs, tref) @@ -4672,9 +4672,9 @@ object Types { */ abstract case class TermParamRef(binder: TermLambda, paramNum: Int) extends ParamRef, CaptureRef { type BT = TermLambda - def canBeTracked(using Context) = true def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) + override def isTrackableRef(using Context) = true } private final class TermParamRefImpl(binder: TermLambda, paramNum: Int) extends TermParamRef(binder, paramNum) @@ -5739,11 +5739,11 @@ object Types { /** A restriction of this map to a function on tracked CaptureRefs */ def forward(ref: CaptureRef): CaptureRef = this(ref) match - case result: CaptureRef if result.canBeTracked => result + case result: CaptureRef if result.isTrackableRef => result /** A restriction of the inverse to a function on tracked CaptureRefs */ def backward(ref: CaptureRef): CaptureRef = inverse(ref) match - case result: CaptureRef if result.canBeTracked => result + case result: CaptureRef if result.isTrackableRef => result end BiTypeMap abstract class TypeMap(implicit protected var mapCtx: Context) From 5b88ab62e96127082617500aa841168fa080d66b Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 11 Aug 2023 19:14:40 +0200 Subject: [PATCH 09/76] Fix typo in exception message --- compiler/src/dotty/tools/dotc/core/TypeErrors.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala index 24a207da6836..1dcd2301b1a7 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala @@ -177,7 +177,7 @@ object CyclicReference: def apply(denot: SymDenotation)(using Context): CyclicReference = val ex = new CyclicReference(denot) if ex.computeStackTrace then - cyclicErrors.println(s"Cyclic reference involving! $denot") + cyclicErrors.println(s"Cyclic reference involving $denot") val sts = ex.getStackTrace.asInstanceOf[Array[StackTraceElement]] for (elem <- sts take 200) cyclicErrors.println(elem.toString) From 84dbe7f8bfbc3f94b0403b931d0b7214cc64d9c7 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 12 Aug 2023 16:49:15 +0200 Subject: [PATCH 10/76] Fix BiTypeMap#inverse --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 31 ++++++++++--------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 21 +++++++------ .../dotty/tools/dotc/core/Substituters.scala | 4 +-- .../src/dotty/tools/dotc/core/Types.scala | 11 ++----- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 84a04c13a91f..e61471ada1ca 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -633,7 +633,7 @@ object CaptureSet: */ override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = val supApprox = super.computeApprox(this) - if source eq origin then supApprox.map(bimap.inverseTypeMap) + if source eq origin then supApprox.map(bimap.inverse) else source.upperApprox(this).map(bimap) ** supApprox override def toString = s"BiMapped$id($source, elems = $elems)" diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 47ef2df93549..a90e440df055 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -97,6 +97,7 @@ object CheckCaptures: final class SubstParamsBiMap(from: LambdaType, to: List[Type])(using Context) extends BiTypeMap: + thisMap => def apply(tp: Type): Type = tp match case tp: ParamRef => @@ -109,20 +110,22 @@ object CheckCaptures: case _ => mapOver(tp) - def inverse(tp: Type): Type = tp match - case tp: NamedType => - var idx = 0 - var to1 = to - while idx < to.length && (tp ne to(idx)) do - idx += 1 - to1 = to1.tail - if idx < to.length then from.paramRefs(idx) - else if tp.prefix `eq` NoPrefix then tp - else tp.derivedSelect(apply(tp.prefix)) - case _: ThisType => - tp - case _ => - mapOver(tp) + lazy val inverse = new BiTypeMap: + def apply(tp: Type): Type = tp match + case tp: NamedType => + var idx = 0 + var to1 = to + while idx < to.length && (tp ne to(idx)) do + idx += 1 + to1 = to1.tail + if idx < to.length then from.paramRefs(idx) + else if tp.prefix `eq` NoPrefix then tp + else tp.derivedSelect(apply(tp.prefix)) + case _: ThisType => + tp + case _ => + mapOver(tp) + def inverse = thisMap end SubstParamsBiMap /** Check that a @retains annotation only mentions references that can be tracked. diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 360a791c4f7a..af8558242424 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -238,6 +238,7 @@ extends tpd.TreeTraverser: */ private class SubstParams(from: List[List[Symbol]], to: List[LambdaType])(using Context) extends DeepTypeMap, BiTypeMap: + thisMap => def apply(t: Type): Type = t match case t: NamedType => @@ -253,15 +254,17 @@ extends tpd.TreeTraverser: case _ => mapOver(t) - def inverse(t: Type): Type = t match - case t: ParamRef => - def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = - if from.isEmpty then t - else if t.binder eq from.head then to.head(t.paramNum).namedType - else recur(from.tail, to.tail) - recur(to, from) - case _ => - mapOver(t) + lazy val inverse = new BiTypeMap: + def apply(t: Type): Type = t match + case t: ParamRef => + def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = + if from.isEmpty then t + else if t.binder eq from.head then to.head(t.paramNum).namedType + else recur(from.tail, to.tail) + recur(to, from) + case _ => + mapOver(t) + def inverse = thisMap end SubstParams /** Update info of `sym` for CheckCaptures phase only */ diff --git a/compiler/src/dotty/tools/dotc/core/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index 3e32340b21bd..5a641416b3e1 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -165,7 +165,7 @@ object Substituters: final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) - def inverse(tp: Type): Type = tp.subst(to, from) + def inverse = SubstBindingMap(to, from) } final class Subst1Map(from: Symbol, to: Type)(using Context) extends DeepTypeMap { @@ -182,7 +182,7 @@ object Substituters: final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = substSym(tp, from, to, this)(using mapCtx) - def inverse(tp: Type) = tp.substSym(to, from) // implicitly requires that `to` contains no duplicates. + def inverse = SubstSymMap(to, from) // implicitly requires that `to` contains no duplicates. } final class SubstThisMap(from: ClassSymbol, to: Type)(using Context) extends DeepTypeMap { diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 410b3b9abbaa..f098d74a93f0 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5727,15 +5727,8 @@ object Types { trait BiTypeMap extends TypeMap: thisMap => - /** The inverse of the type map as a function */ - def inverse(tp: Type): Type - - /** The inverse of the type map as a BiTypeMap map, which - * has the original type map as its own inverse. - */ - def inverseTypeMap(using Context) = new BiTypeMap: - def apply(tp: Type) = thisMap.inverse(tp) - def inverse(tp: Type) = thisMap.apply(tp) + /** The inverse of the type map */ + def inverse: BiTypeMap /** A restriction of this map to a function on tracked CaptureRefs */ def forward(ref: CaptureRef): CaptureRef = this(ref) match From deb0f01491861bd67caa5cde18e95273a5f8f9fb Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 20 Aug 2023 19:52:04 +0200 Subject: [PATCH 11/76] Refactor printing of capture sets --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 28 +++++++++---------- .../dotty/tools/dotc/core/Decorators.scala | 5 ++++ .../tools/dotc/printing/PlainPrinter.scala | 8 ++++-- .../dotty/tools/dotc/printing/Printer.scala | 4 +++ 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index e61471ada1ca..e25b216a2f83 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -300,15 +300,18 @@ sealed abstract class CaptureSet extends Showable: /** This capture set with a description that tells where it comes from */ def withDescription(description: String): CaptureSet - /** The provided description (using `withDescription`) for this capture set or else "" */ + /** The provided description (set via `withDescription`) for this capture set or else "" */ def description: String + /** More info enabled by -Y flags */ + def optionalInfo(using Context): String = "" + /** A regular @retains or @retainsByName annotation with the elements of this set as arguments. */ def toRegularAnnotation(cls: Symbol)(using Context): Annotation = Annotation(CaptureAnnotation(this, boxed = false)(cls).tree) override def toText(printer: Printer): Text = - Str("{") ~ Text(elems.toList.map(printer.toTextCaptureRef), ", ") ~ Str("}") ~~ description + printer.toTextCaptureSet(this) ~~ description object CaptureSet: type Refs = SimpleIdentitySet[CaptureRef] @@ -489,11 +492,17 @@ object CaptureSet: deps.foreach(_.propagateSolved()) def withDescription(description: String): this.type = - this.description = - if this.description.isEmpty then description - else s"${this.description} and $description" + this.description = this.description.join(" and ", description) this + /** Adds variables to the ShownVars context property if that exists, which + * establishes a record of all variables printed in an error message. + * Returns variable `ids` under -Ycc-debug. + */ + override def optionalInfo(using Context): String = + for vars <- ctx.property(ShownVars) do vars += this + if !isConst && ctx.settings.YccDebug.value then ids else "" + /** Used for diagnostics and debugging: A string that traces the creation * history of a variable by following source links. Each variable on the * path is characterized by the variable's id and the first letter of the @@ -506,15 +515,6 @@ object CaptureSet: case _ => "" s"$id${getClass.getSimpleName.nn.take(1)}$trail" - /** Adds variables to the ShownVars context property if that exists, which - * establishes a record of all variables printed in an error message. - * Prints variables wih ids under -Ycc-debug. - */ - override def toText(printer: Printer): Text = inContext(printer.printerContext) { - for vars <- ctx.property(ShownVars) do vars += this - super.toText(printer) ~ (Str(ids) provided !isConst && ctx.settings.YccDebug.value) - } - override def toString = s"Var$id$elems" end Var diff --git a/compiler/src/dotty/tools/dotc/core/Decorators.scala b/compiler/src/dotty/tools/dotc/core/Decorators.scala index 8ae4cb4ceee2..b1e822368a81 100644 --- a/compiler/src/dotty/tools/dotc/core/Decorators.scala +++ b/compiler/src/dotty/tools/dotc/core/Decorators.scala @@ -56,6 +56,11 @@ object Decorators { def indented(width: Int): String = val padding = " " * width padding + s.replace("\n", "\n" + padding) + + def join(sep: String, other: String) = + if s.isEmpty then other + else if other.isEmpty then s + else s + sep + other end extension /** Convert lazy string to message. To be with caution, since no message-defined diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index b739bcf1b74d..74ae74a995a0 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -151,10 +151,12 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextCaptureSet(cs: CaptureSet): Text = if printDebug && !cs.isConst then cs.toString - else if ctx.settings.YccDebug.value then cs.show else if cs == CaptureSet.Fluid then "" - else if !cs.isConst && cs.elems.isEmpty then "?" - else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + else + val core: Text = + if !cs.isConst && cs.elems.isEmpty then "?" + else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + core ~ cs.optionalInfo /** Print capturing type, overridden in RefinedPrinter to account for * capturing function types. diff --git a/compiler/src/dotty/tools/dotc/printing/Printer.scala b/compiler/src/dotty/tools/dotc/printing/Printer.scala index 04cea9fb9702..eafa399313da 100644 --- a/compiler/src/dotty/tools/dotc/printing/Printer.scala +++ b/compiler/src/dotty/tools/dotc/printing/Printer.scala @@ -10,6 +10,7 @@ import Types.{Type, SingletonType, LambdaParam, NamedType}, import typer.Implicits.* import util.SourcePosition import typer.ImportInfo +import cc.CaptureSet import scala.annotation.internal.sharable @@ -106,6 +107,9 @@ abstract class Printer { /** Textual representation of a reference in a capture set */ def toTextCaptureRef(tp: Type): Text + /** Textual representation of a reference in a capture set */ + def toTextCaptureSet(cs: CaptureSet): Text + /** Textual representation of symbol's declaration */ def dclText(sym: Symbol): Text From 734ec4984aa8ab890e87886d03ea8e815cc822bb Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 20 Aug 2023 20:04:36 +0200 Subject: [PATCH 12/76] Generalize addenda handling for typeMismatch errors Currently not needed (was needed for level checking in other branch), but kept since it is a useful generalization. --- .../tools/dotc/typer/ErrorReporting.scala | 13 ++++++++-- .../dotty/tools/dotc/typer/Implicits.scala | 26 +++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 25cbfdfec600..68ea402eff3f 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -70,6 +70,15 @@ object ErrorReporting { case _ => foldOver(s, tp) tps.foldLeft("")(collectMatchTrace) + /** A mixin trait that can produce added elements for an error message */ + trait Addenda: + self => + def toAdd(using Context): List[String] = Nil + def ++ (follow: Addenda) = new Addenda: + override def toAdd(using Context) = self.toAdd ++ follow.toAdd + + object NothingToAdd extends Addenda + class Errors(using Context) { /** An explanatory note to be added to error messages @@ -162,7 +171,7 @@ object ErrorReporting { def patternConstrStr(tree: Tree): String = ??? - def typeMismatch(tree: Tree, pt: Type, implicitFailure: SearchFailureType = NoMatchingImplicits): Tree = { + def typeMismatch(tree: Tree, pt: Type, addenda: Addenda = NothingToAdd): Tree = { val normTp = normalize(tree.tpe, pt) val normPt = normalize(pt, pt) @@ -184,7 +193,7 @@ object ErrorReporting { "\nMaybe you are missing an else part for the conditional?" case _ => "" - errorTree(tree, TypeMismatch(treeTp, expectedTp, Some(tree), implicitFailure.whyNoConversion, missingElse)) + errorTree(tree, TypeMismatch(treeTp, expectedTp, Some(tree), (addenda.toAdd :+ missingElse)*)) } /** A subtype log explaining why `found` does not conform to `expected` */ diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index b0f476444754..3e2faee56d1b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -446,7 +446,7 @@ object Implicits: } } - abstract class SearchFailureType extends ErrorType { + abstract class SearchFailureType extends ErrorType, Addenda { def expectedType: Type def argument: Tree @@ -463,11 +463,6 @@ object Implicits: if (argument.isEmpty) i"match type ${clarify(expectedType)}" else i"convert from ${argument.tpe} to ${clarify(expectedType)}" } - - /** If search was for an implicit conversion, a note describing the failure - * in more detail - this is either empty or starts with a '\n' - */ - def whyNoConversion(using Context): String = "" } class NoMatchingImplicits(val expectedType: Type, val argument: Tree, constraint: Constraint = OrderingConstraint.empty) @@ -521,17 +516,21 @@ object Implicits: /** A failure value indicating that an implicit search for a conversion was not tried */ case class TooUnspecific(target: Type) extends NoMatchingImplicits(NoType, EmptyTree, OrderingConstraint.empty): - override def whyNoConversion(using Context): String = + + override def toAdd(using Context) = i""" |Note that implicit conversions were not tried because the result of an implicit conversion - |must be more specific than $target""" + |must be more specific than $target""" :: Nil override def msg(using Context) = super.msg.append("\nThe expected type $target is not specific enough, so no search was attempted") + override def toString = s"TooUnspecific" + end TooUnspecific /** An ambiguous implicits failure */ - class AmbiguousImplicits(val alt1: SearchSuccess, val alt2: SearchSuccess, val expectedType: Type, val argument: Tree) extends SearchFailureType { + class AmbiguousImplicits(val alt1: SearchSuccess, val alt2: SearchSuccess, val expectedType: Type, val argument: Tree) extends SearchFailureType: + def msg(using Context): Message = var str1 = err.refStr(alt1.ref) var str2 = err.refStr(alt2.ref) @@ -539,15 +538,16 @@ object Implicits: str1 = ctx.printer.toTextRef(alt1.ref).show str2 = ctx.printer.toTextRef(alt2.ref).show em"both $str1 and $str2 $qualify".withoutDisambiguation() - override def whyNoConversion(using Context): String = + + override def toAdd(using Context) = if !argument.isEmpty && argument.tpe.widen.isRef(defn.NothingClass) then - "" + Nil else val what = if (expectedType.isInstanceOf[SelectionProto]) "extension methods" else "conversions" i""" |Note that implicit $what cannot be applied because they are ambiguous; - |$explanation""" - } + |$explanation""" :: Nil + end AmbiguousImplicits class MismatchedImplicit(ref: TermRef, val expectedType: Type, From 5a30a1dbfe1ca343ea0e426ef2831a7d6d801d3a Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 20 Aug 2023 20:19:03 +0200 Subject: [PATCH 13/76] Refactorings --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 41 ++-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 176 +++++++++--------- .../tools/dotc/printing/PlainPrinter.scala | 2 +- 3 files changed, 109 insertions(+), 110 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a90e440df055..843fd2e96d56 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -339,7 +339,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => mapOver(t) if variance > 0 then t1 - else Setup.decorate(t1, Function.const(CaptureSet.Fluid)) + else setup.decorate(t1, Function.const(CaptureSet.Fluid)) def isPreCC(sym: Symbol): Boolean = sym.isTerm && sym.maybeOwner.isClass @@ -674,13 +674,10 @@ class CheckCaptures extends Recheck, SymTransformer: * of simulated boxing and unboxing. */ override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = - val typeToCheck = tree match - case _: Ident | _: Select | _: Apply | _: TypeApply if tree.symbol.unboxesResult => - tpe - case _: Try => - tpe - case _ => - NoType + def needsUniversalCheck = tree match + case _: RefTree | _: Apply | _: TypeApply => tree.symbol.unboxesResult + case _: Try => true + case _ => false def checkNotUniversal(tp: Type): Unit = tp.widenDealias match case wtp @ CapturingType(parent, refs) => refs.disallowRootCapability { () => @@ -691,7 +688,8 @@ class CheckCaptures extends Recheck, SymTransformer: } checkNotUniversal(parent) case _ => - if !allowUniversalInBoxed then checkNotUniversal(typeToCheck) + if !allowUniversalInBoxed && needsUniversalCheck then + checkNotUniversal(tpe) super.recheckFinish(tpe, tree, pt) // ------------------ Adaptation ------------------------------------- @@ -782,6 +780,12 @@ class CheckCaptures extends Recheck, SymTransformer: */ def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, alwaysConst: Boolean = false)(using Context): Type = + inline def inNestedEnv[T](boxed: Boolean)(op: => T): T = + val saved = curEnv + curEnv = Env(curEnv.owner, EnvKind.NestedInOwner, CaptureSet.Var(), if boxed then null else curEnv) + try op + finally curEnv = saved + /** Adapt function type `actual`, which is `aargs -> ares` (possibly with dependencies) * to `expected` type. * It returns the adapted type along with a capture set consisting of the references @@ -791,10 +795,7 @@ class CheckCaptures extends Recheck, SymTransformer: def adaptFun(actual: Type, aargs: List[Type], ares: Type, expected: Type, covariant: Boolean, boxed: Boolean, reconstruct: (List[Type], Type) => Type): (Type, CaptureSet) = - val saved = curEnv - curEnv = Env(curEnv.owner, EnvKind.NestedInOwner, CaptureSet.Var(), if boxed then null else curEnv) - - try + inNestedEnv(boxed): val (eargs, eres) = expected.dealias.stripCapturing match case defn.FunctionOf(eargs, eres, _) => (eargs, eres) case expected: MethodType => (expected.paramInfos, expected.resType) @@ -808,8 +809,6 @@ class CheckCaptures extends Recheck, SymTransformer: else reconstruct(aargs1, ares1) (resTp, curEnv.captured) - finally - curEnv = saved /** Adapt type function type `actual` to the expected type. * @see [[adaptFun]] @@ -818,10 +817,7 @@ class CheckCaptures extends Recheck, SymTransformer: actual: Type, ares: Type, expected: Type, covariant: Boolean, boxed: Boolean, reconstruct: Type => Type): (Type, CaptureSet) = - val saved = curEnv - curEnv = Env(curEnv.owner, EnvKind.NestedInOwner, CaptureSet.Var(), if boxed then null else curEnv) - - try + inNestedEnv(boxed): val eres = expected.dealias.stripCapturing match case defn.PolyFunctionOf(rinfo: PolyType) => rinfo.resType case expected: PolyType => expected.resType @@ -834,8 +830,6 @@ class CheckCaptures extends Recheck, SymTransformer: else reconstruct(ares1) (resTp, curEnv.captured) - finally - curEnv = saved end adaptTypeFun def adaptInfo(actual: Type, expected: Type, covariant: Boolean): String = @@ -976,8 +970,11 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) + private var setup: Setup = compiletime.uninitialized + override def checkUnit(unit: CompilationUnit)(using Context): Unit = - Setup(preRecheckPhase, thisPhase, recheckDef)(ctx.compilationUnit.tpdTree) + setup = Setup(preRecheckPhase, thisPhase, recheckDef) + setup(ctx.compilationUnit.tpdTree) //println(i"SETUP:\n${Recheck.addRecheckedTypes.transform(ctx.compilationUnit.tpdTree)}") withCaptureSetsExplained { super.checkUnit(unit) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index af8558242424..42ab70efe8ff 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -164,7 +164,7 @@ extends tpd.TreeTraverser: resType = this(tp.resType)) case _ => mapOver(tp) - Setup.addVar(addCaptureRefinements(tp1)) + addVar(addCaptureRefinements(tp1)) end apply end mapInferred @@ -313,94 +313,88 @@ extends tpd.TreeTraverser: tree.srcPos) case _ => traverseChildren(tree) - tree match - case tree: TypeTree => - transformTT(tree, boxed = false, exact = false) // other types are not boxed - case tree: ValOrDefDef => - val sym = tree.symbol - - // 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 - 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 => subst(psym.info).asInstanceOf[mt.PInfo]), - mt1 => - integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) - ) - case info: ExprType => - info.derivedExprType(resType = - integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) - case _ => - val restp = tree.tpt.knownType - if prevLambdas.isEmpty then restp - else SubstParams(prevPsymss, prevLambdas)(restp) - - if sym.exists && tree.tpt.hasRememberedType && !sym.isConstructor 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 other methods are determined from their definitions which - // are checked on depand - denot.info = newInfo - recheckDef(tree, sym)) - case tree: Bind => - val sym = tree.symbol - updateInfo(sym, transformInferredType(sym.info, boxed = false)) - case tree: TypeDef => - tree.symbol match - case cls: ClassSymbol => - val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then - // add capture set to self type of nested classes if no self type is given explicitly - val localRefs = CaptureSet.Var() - val newInfo = ClassInfo(prefix, cls, ps, decls, - CapturingType(cinfo.selfType, localRefs) - .showing(i"inferred self type for $cls: $result", capt)) - 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, localRefs)) - modul.termRef.invalidateCaches() - case _ => - val info = atPhase(preRecheckPhase)(tree.symbol.info) - val newInfo = transformExplicitType(info, boxed = false) - if newInfo ne info then - updateInfo(tree.symbol, newInfo) - capt.println(i"update info of ${tree.symbol} from $info to $newInfo") - case _ => + postProcess(tree) end traverse - def apply(tree: Tree)(using Context): Unit = - traverse(tree)(using ctx.withProperty(Setup.IsDuringSetupKey, Some(()))) - -object Setup: - val IsDuringSetupKey = new Property.Key[Unit] - - def isDuringSetup(using Context): Boolean = - ctx.property(IsDuringSetupKey).isDefined + def postProcess(tree: Tree)(using Context): Unit = tree match + case tree: TypeTree => + transformTT(tree, boxed = false, exact = false) // other types are not boxed + case tree: ValOrDefDef => + val sym = tree.symbol + + // 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 + 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 => subst(psym.info).asInstanceOf[mt.PInfo]), + mt1 => + integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) + ) + case info: ExprType => + info.derivedExprType(resType = + integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) + case _ => + val restp = tree.tpt.knownType + if prevLambdas.isEmpty then restp + else SubstParams(prevPsymss, prevLambdas)(restp) + + if sym.exists && tree.tpt.hasRememberedType && !sym.isConstructor 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 other methods are determined from their definitions which + // are checked on depand + denot.info = newInfo + recheckDef(tree, sym)) + case tree: Bind => + val sym = tree.symbol + updateInfo(sym, transformInferredType(sym.info, boxed = false)) + case tree: TypeDef => + tree.symbol match + case cls: ClassSymbol => + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo + if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then + // add capture set to self type of nested classes if no self type is given explicitly + val selfRefs = CaptureSet.Var() + val newInfo = ClassInfo(prefix, cls, ps, decls, + CapturingType(cinfo.selfType, selfRefs) + .showing(i"inferred self type for $cls: $result", capt)) + 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, selfRefs)) + modul.termRef.invalidateCaches() + case _ => + val info = atPhase(preRecheckPhase)(tree.symbol.info) + val newInfo = transformExplicitType(info, boxed = false) + if newInfo ne info then + updateInfo(tree.symbol, newInfo) + capt.println(i"update info of ${tree.symbol} from $info to $newInfo") + case _ => + end postProcess private def superTypeIsImpure(tp: Type)(using Context): Boolean = { tp.dealias match @@ -494,4 +488,12 @@ object Setup: case CapturingType(_, refs) => CaptureSet.Var(refs.elems) case _ => CaptureSet.Var()) + def apply(tree: Tree)(using Context): Unit = + traverse(tree)(using ctx.withProperty(Setup.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/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 74ae74a995a0..ed44da1a1eca 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -156,7 +156,7 @@ class PlainPrinter(_ctx: Context) extends Printer { val core: Text = if !cs.isConst && cs.elems.isEmpty then "?" else "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" - core ~ cs.optionalInfo + core ~ cs.optionalInfo /** Print capturing type, overridden in RefinedPrinter to account for * capturing function types. From b1edf4484f1bc3c29f7acf37d6cce9ea56da7658 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 20 Aug 2023 20:36:33 +0200 Subject: [PATCH 14/76] Make mappings in Setup more robust These changes were needed if we wanted to map all capability classes C to C^. They are a good idea anyway since they make the mapping more robust. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 37 ++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 42ab70efe8ff..b5ab723288b6 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -208,13 +208,14 @@ extends tpd.TreeTraverser: def apply(t: Type) = t match case _: AppliedType => val t1 = expandThrowsAlias(t) - if t1 ne t then apply(t1) else mapOver(t) - case _: LazyRef => - t + if t1 ne t then this(t1) else mapOver(t) + case t: LazyRef => + val t1 = this(t.ref) + if t1 ne t.ref then t1 else t case t @ AnnotatedType(t1, ann) => // Don't map capture sets, since that would implicitly normalize sets that // are not well-formed. - t.derivedAnnotatedType(apply(t1), ann) + t.derivedAnnotatedType(this(t1), ann) case _ => mapOver(t) @@ -425,13 +426,9 @@ extends tpd.TreeTraverser: if sym.isClass then !sym.isPureClass && sym != defn.AnyClass else - sym != defn.FromJavaObjectSymbol - // For capture checking, we assume Object from Java is the same as Any - && { - val tp1 = tp.dealias - if tp1 ne tp then needsVariable(tp1) - else superTypeIsImpure(tp1) - } + val tp1 = tp.dealias + if tp1 ne tp then needsVariable(tp1) + else superTypeIsImpure(tp1) case tp: (RefinedOrRecType | MatchType) => needsVariable(tp.underlying) case tp: AndType => @@ -442,6 +439,8 @@ extends tpd.TreeTraverser: 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 + case AnnotatedType(parent, _) => + needsVariable(parent) case _ => false }.showing(i"can have inferred capture $tp = $result", capt) @@ -474,10 +473,20 @@ 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 _ if needsVariable(tp) => - CapturingType(tp, addedSet(tp)) - case _ => + case tp: LazyRef => + decorate(tp.ref, addedSet) + case _ if tp.typeSymbol == defn.FromJavaObjectSymbol => + // For capture checking, we assume Object from Java is the same as Any tp + case _ => + def maybeAdd(target: Type, fallback: Type) = + if needsVariable(target) then CapturingType(target, addedSet(target)) + else fallback + val tp1 = tp.dealiasKeepAnnots + if tp1 ne tp then + val tp2 = transformExplicitType(tp1, boxed = false) + maybeAdd(tp2, if tp2 ne tp1 then tp2 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`. From c256d2499a7fe4d23786ad4271c484b64cf4b133 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 21 Aug 2023 10:12:40 +0200 Subject: [PATCH 15/76] More robust treatment of @capability classes --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 53 +++++++++++++------ tests/neg-custom-args/captures/cc-this5.check | 2 +- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index e25b216a2f83..7a3860b722de 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -871,7 +871,7 @@ object CaptureSet: case tp: TermParamRef => tp.captureSet case _: TypeRef => - if tp.classSymbol.hasAnnotation(defn.CapabilityAnnot) then universal else empty + empty case _: TypeParamRef => empty case CapturingType(parent, refs) => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index b5ab723288b6..95c6943471d7 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -204,23 +204,36 @@ extends tpd.TreeTraverser: else fntpe case _ => tp - private def expandThrowsAliases(using Context) = new TypeMap: - def apply(t: Type) = t match - case _: AppliedType => - val t1 = expandThrowsAlias(t) - if t1 ne t then this(t1) else mapOver(t) - case t: LazyRef => - val t1 = this(t.ref) - if t1 ne t.ref then t1 else t - case t @ AnnotatedType(t1, ann) => - // Don't map capture sets, since that would implicitly normalize sets that - // are not well-formed. - t.derivedAnnotatedType(this(t1), ann) - case _ => - mapOver(t) + /** Map references to capability classes C to C^ */ + private def expandCapabilityClass(tp: Type)(using Context): Type = tp match + case _: TypeRef | _: AppliedType if tp.typeSymbol.hasAnnotation(defn.CapabilityAnnot) => + CapturingType(tp, CaptureSet.universal, boxed = false) + case _ => + tp + + private def expandAliases(using Context) = new TypeMap: + 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: LazyRef => + val t1 = this(t.ref) + if t1 ne t.ref then t1 else t + case t @ AnnotatedType(t1, ann) => + // Don't map capture sets, since that would implicitly normalize sets that + // are not well-formed. + t.derivedAnnotatedType(this(t1), ann) + case _ => + val t1 = t.dealias + if t1 ne t then + val t2 = this(t1) + if t2 ne t1 then return t2 + mapOver(t) private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = - val tp1 = expandThrowsAliases(if boxed then box(tp) else tp) + val tp1 = expandAliases(if boxed then box(tp) else tp) if tp1 ne tp then capt.println(i"expanded: $tp --> $tp1") tp1 @@ -348,12 +361,20 @@ extends tpd.TreeTraverser: case info: ExprType => info.derivedExprType(resType = integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) + case info if sym.isConstructor => + info case _ => val restp = tree.tpt.knownType if prevLambdas.isEmpty then restp else SubstParams(prevPsymss, prevLambdas)(restp) - if sym.exists && tree.tpt.hasRememberedType && !sym.isConstructor then + def signatureChanges = + tree.tpt.hasRememberedType && !sym.isConstructor + || 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 diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 84ac97474b80..e26597f61f37 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/cc-this5.scala:16:20 ---------------------------------------------------------- 16 | def f = println(c) // error | ^ - | (c : Cap) cannot be referenced here; it is not included in the allowed capture set {} + | (c : Cap^) cannot be referenced here; it is not included in the allowed capture set {} -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- 21 | val x: A = this // error | ^^^^ From 21880816057258048fa98ab720e192533710da82 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 21 Aug 2023 11:07:27 +0200 Subject: [PATCH 16/76] Allow to use inferred capture sets for definitions that override others --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 843fd2e96d56..a46120938f0d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1129,13 +1129,16 @@ class CheckCaptures extends Recheck, SymTransformer: 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 + 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 From f386334afe16198938d43b7dd8bc459cb6db7711 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 21 Aug 2023 11:59:28 +0200 Subject: [PATCH 17/76] Handle levels in constraint solving - Define a notion of ccNestingLevel, which corresponds to the nesting level of so called "level owners" relative to each other. - The outermost level owner is _root_. - Other level owners are classes that are not staticOwners and methods that are not constructors. - The ccNestingLevel of any symbol is the ccNestingLevel of its closest enclosing level owner, or -1 for NoSymbol. - Capture set variables are created with a level owner. - Capture set variables cannot include elements with higher ccNestingLevels than the variable's owner. - If level-incorrect elements are attempted to be added to a capture set variable, they are instead widened to the underlying capture set. - Use addenda mechanism to report level errors --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 60 ++++++++++ .../src/dotty/tools/dotc/cc/CaptureSet.scala | 113 +++++++++++++++--- .../dotty/tools/dotc/cc/CheckCaptures.scala | 38 +++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 38 +++--- .../src/dotty/tools/dotc/core/Contexts.scala | 2 +- .../dotty/tools/dotc/core/TypeComparer.scala | 4 + .../src/dotty/tools/dotc/core/Types.scala | 10 +- .../tools/dotc/printing/RefinedPrinter.scala | 7 +- .../dotty/tools/dotc/transform/Recheck.scala | 5 +- 9 files changed, 220 insertions(+), 57 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 796f2b52abdc..9d300248f9a5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -11,6 +11,7 @@ import config.Printers.capt import util.Property.Key import tpd.* import config.Feature +import collection.mutable private val Captures: Key[CaptureSet] = Key() private val BoxedType: Key[BoxedTypeCache] = Key() @@ -32,6 +33,24 @@ def allowUniversalInBoxed(using Context) = /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type) extends Exception + +/** Capture checking state, consisting of + * - nestingLevels: A map associating certain symbols (the nesting level owners) + 8 with their ccNestingLevel + * - localRoots: A map associating nesting level owners with the local roots valid + * in their scopes. + * - levelError: Optionally, the last pair of capture reference and capture set where + * the reference could not be added to the set due to a level conflict. + * The capture checking state is stored in a context property. + */ +class CCState: + val nestingLevels: mutable.HashMap[Symbol, Int] = new mutable.HashMap + val localRoots: mutable.HashMap[Symbol, CaptureRef] = new mutable.HashMap + var levelError: Option[(CaptureRef, CaptureSet)] = None + +/** Property key for capture checking state */ +val ccState: Key[CCState] = Key() + extension (tree: Tree) /** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */ @@ -253,6 +272,47 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox + /** The owner of the current level. Qualifying owners are + * - methods other than constructors + * - classes, if they are not staticOwners + * - _root_ + */ + def levelOwner(using Context): Symbol = + if sym.isStaticOwner then defn.RootClass + else if sym.isClass || sym.is(Method) && !sym.isConstructor then sym + else sym.owner.levelOwner + + /** 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 + val cache = ctx.property(ccState).get.nestingLevels + cache.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.property(ccState).isDefined then + Some(ccNestingLevel) + else None + + def maxNested(other: Symbol)(using Context): Symbol = + if sym.ccNestingLevel < other.ccNestingLevel then other else sym + /* does not work yet, we do mix sets with different levels, for instance in cc-this.scala. + else if sym.ccNestingLevel > other.ccNestingLevel then sym + else + assert(sym == other, i"conflicting symbols at same nesting level: $sym, $other") + sym + */ + + def minNested(other: Symbol)(using Context): Symbol = + if sym.ccNestingLevel > other.ccNestingLevel then other else sym + 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/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7a3860b722de..5903fb1e0eb9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -12,7 +12,8 @@ import annotation.internal.sharable import reporting.trace import printing.{Showable, Printer} import printing.Texts.* -import util.{SimpleIdentitySet, Property} +import util.{SimpleIdentitySet, Property, optional}, optional.{break, ?} +import typer.ErrorReporting.Addenda import util.common.alwaysTrue import scala.collection.mutable import config.Config.ccAllowUnsoundMaps @@ -55,6 +56,11 @@ 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. + */ + def owner: Symbol + /** Is this capture set definitely non-empty? */ final def isNotEmpty: Boolean = !elems.isEmpty @@ -123,10 +129,14 @@ sealed abstract class CaptureSet extends Showable: * as frozen. */ def accountsFor(x: CaptureRef)(using Context): Boolean = - reporting.trace(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true) { - elems.exists(_.subsumes(x)) - || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK - } + if comparer.isInstanceOf[ExplainingTypeComparer] then // !!! DEBUG + reporting.trace.force(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true): + elems.exists(_.subsumes(x)) + || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + else + reporting.trace(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true): + elems.exists(_.subsumes(x)) + || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK /** A more optimistic version of accountsFor, which does not take variable supersets * of the `x` reference into account. A set might account for `x` if it accounts @@ -191,7 +201,8 @@ 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.elems ++ that.elems).addAsDependentTo(this).addAsDependentTo(that) + else Var(this.owner.maxNested(that.owner), this.elems ++ that.elems) + .addAsDependentTo(this).addAsDependentTo(that) /** The smallest superset (via <:<) of this capture set that also contains `ref`. */ @@ -276,7 +287,9 @@ sealed abstract class CaptureSet extends Showable: if isUniversal then handler() this - /** Invoke handler on the elements to check wellformedness of the capture set */ + /** 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) this @@ -356,6 +369,8 @@ object CaptureSet: def withDescription(description: String): Const = Const(elems, description) + def owner = NoSymbol + override def toString = elems.toString end Const @@ -374,16 +389,23 @@ object CaptureSet: end Fluid /** The subclass of captureset variables with given initial elements */ - class Var(initialElems: Refs = emptySet) extends CaptureSet: + class Var(directOwner: Symbol, initialElems: Refs = emptySet)(using @constructorOnly ictx: Context) extends CaptureSet: /** A unique identification number for diagnostics */ val id = varId += 1 varId + override val owner = directOwner.levelOwner + /** 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 @@ -403,6 +425,8 @@ 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. */ @@ -428,7 +452,10 @@ object CaptureSet: deps = state.deps(this) def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - if !isConst && recordElemsState() then + if isConst || !recordElemsState() then + CompareResult.fail(this) // fail if variable is solved or given VarState is frozen + else if levelsOK(newElems) then + //assert(id != 2, newElems) elems ++= newElems if isUniversal then rootAddedHandler() newElemAddedHandler(newElems.toList) @@ -436,8 +463,36 @@ object CaptureSet: (CompareResult.OK /: deps) { (r, dep) => r.andAlso(dep.tryInclude(newElems, this)) } - else // fail if variable is solved or given VarState is frozen - CompareResult.fail(this) + else + val res = widenCaptures(newElems) match + case Some(newElems1) => tryInclude(newElems1, origin) + case None => CompareResult.fail(this) + if !res.isOK then recordLevelError() + res + + private def recordLevelError()(using Context): Unit = + for elem <- triedElem do + ctx.property(ccState).get.levelError = Some((elem, this)) + + private def levelsOK(elems: Refs)(using Context): Boolean = + !elems.exists(_.ccNestingLevel > ownLevel) + + private def widenCaptures(elems: Refs)(using Context): Option[Refs] = + val res = optional: + (SimpleIdentitySet[CaptureRef]() /: elems): (acc, elem) => + if elem.ccNestingLevel <= ownLevel then acc + elem + else if elem.isRootCapability then break() + else + val saved = triedElem + triedElem = triedElem.orElse(Some(elem)) + 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 @@ -497,11 +552,18 @@ object CaptureSet: /** Adds variables to the ShownVars context property if that exists, which * establishes a record of all variables printed in an error message. - * Returns variable `ids` under -Ycc-debug. + * Returns variable `ids` under -Ycc-debug, and owner/nesting level info + * under -Yprint-level. */ override def optionalInfo(using Context): String = for vars <- ctx.property(ShownVars) do vars += this - if !isConst && ctx.settings.YccDebug.value then ids else "" + val debugInfo = + if !isConst && ctx.settings.YccDebug.value then ids else "" + val nestingInfo = + if ctx.settings.YprintLevel.value + then s"" + else "" + debugInfo ++ nestingInfo /** Used for diagnostics and debugging: A string that traces the creation * history of a variable by following source links. Each variable on the @@ -519,8 +581,8 @@ object CaptureSet: end Var /** A variable that is derived from some other variable via a map or filter. */ - abstract class DerivedVar(initialElems: Refs)(using @constructorOnly ctx: Context) - extends Var(initialElems): + abstract class DerivedVar(owner: Symbol, initialElems: Refs)(using @constructorOnly ctx: Context) + extends Var(owner, initialElems): // For debugging: A trace where a set was created. Note that logically it would make more // sense to place this variable in Mapped, but that runs afoul of the initializatuon checker. @@ -546,7 +608,7 @@ object CaptureSet: */ class Mapped private[CaptureSet] (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) - extends DerivedVar(initial.elems): + extends DerivedVar(source.owner, initial.elems): addAsDependentTo(initial) // initial mappings could change by propagation private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap] @@ -612,7 +674,7 @@ object CaptureSet: */ final class BiMapped private[CaptureSet] (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) - extends DerivedVar(initialElems): + extends DerivedVar(source.owner, initialElems): override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = if origin eq source then @@ -642,7 +704,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.elems.filter(p)): + extends DerivedVar(source.owner, source.elems.filter(p)): override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = val filtered = newElems.filter(p) @@ -673,7 +735,7 @@ object CaptureSet: extends Filtered(source, !other.accountsFor(_)) class Intersected(cs1: CaptureSet, cs2: CaptureSet)(using Context) - extends Var(elemIntersection(cs1, cs2)): + extends Var(cs1.owner.minNested(cs2.owner), elemIntersection(cs1, cs2)): addAsDependentTo(cs1) addAsDependentTo(cs2) deps += cs1 @@ -933,4 +995,17 @@ object CaptureSet: println(i" ${cv.show.padTo(20, ' ')} :: ${cv.deps.toList}%, %") } else op + + def levelErrors: Addenda = new Addenda: + override def toAdd(using Context) = + for + state <- ctx.property(ccState).toList + (ref, cs) <- state.levelError + yield + val level = ref.ccNestingLevel + i""" + | + |Note that reference ${ref}, defined at level $level + |cannot be included in outer capture set $cs, defined at level ${cs.owner.nestingLevel} in ${cs.owner}""" + end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a46120938f0d..590c180f8381 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -12,6 +12,7 @@ import ast.{tpd, untpd, Trees} import Trees.* import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker} import typer.Checking.{checkBounds, checkAppliedTypesIn} +import typer.ErrorReporting.Addenda import util.{SimpleIdentitySet, EqHashMap, SrcPos, Property} import transform.SymUtils.* import transform.{Recheck, PreRecheck} @@ -263,7 +264,8 @@ class CheckCaptures extends Recheck, SymTransformer: report.error(em"$header included in allowed capture set ${res.blocking}", pos) /** The current environment */ - private var curEnv: Env = Env(NoSymbol, EnvKind.Regular, CaptureSet.empty, null) + private var curEnv: Env = inContext(ictx): + Env(defn.RootClass, EnvKind.Regular, CaptureSet.empty, null) private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap() @@ -272,7 +274,8 @@ class CheckCaptures extends Recheck, SymTransformer: */ def capturedVars(sym: Symbol)(using Context) = myCapturedVars.getOrElseUpdate(sym, - if sym.ownersIterator.exists(_.isTerm) then CaptureSet.Var() + if sym.ownersIterator.exists(_.isTerm) then + CaptureSet.Var(if sym.isConstructor then sym.owner.owner else sym.owner) else CaptureSet.empty) /** For all nested environments up to `limit` or a closed environment perform `op`, @@ -655,9 +658,9 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv tree match case _: RefTree | closureDef(_) if pt.isBoxedCapturing => - curEnv = Env(curEnv.owner, EnvKind.Boxed, CaptureSet.Var(), curEnv) + curEnv = Env(curEnv.owner, EnvKind.Boxed, CaptureSet.Var(curEnv.owner), curEnv) case _ if tree.hasAttachment(ClosureBodyValue) => - curEnv = Env(curEnv.owner, EnvKind.ClosureResult, CaptureSet.Var(), curEnv) + curEnv = Env(curEnv.owner, EnvKind.ClosureResult, CaptureSet.Var(curEnv.owner), curEnv) case _ => val res = try super.recheck(tree, pt) @@ -691,6 +694,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !allowUniversalInBoxed && needsUniversalCheck then checkNotUniversal(tpe) super.recheckFinish(tpe, tree, pt) + end recheckFinish // ------------------ Adaptation ------------------------------------- // @@ -703,11 +707,11 @@ class CheckCaptures extends Recheck, SymTransformer: // - Adapt box status and environment capture sets by simulating box/unbox operations. /** Massage `actual` and `expected` types using the methods below before checking conformance */ - override def checkConformsExpr(actual: Type, expected: Type, tree: Tree)(using Context): Unit = + 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) + 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) @@ -782,7 +786,7 @@ class CheckCaptures extends Recheck, SymTransformer: inline def inNestedEnv[T](boxed: Boolean)(op: => T): T = val saved = curEnv - curEnv = Env(curEnv.owner, EnvKind.NestedInOwner, CaptureSet.Var(), if boxed then null else curEnv) + curEnv = Env(curEnv.owner, EnvKind.NestedInOwner, CaptureSet.Var(curEnv.owner), if boxed then null else curEnv) try op finally curEnv = saved @@ -974,16 +978,16 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkUnit(unit: CompilationUnit)(using Context): Unit = setup = Setup(preRecheckPhase, thisPhase, recheckDef) - 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 - } + inContext(ctx.withProperty(ccState, Some(new CCState))): + 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 /** 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 diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 95c6943471d7..80402f47a1dc 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -106,7 +106,7 @@ extends tpd.TreeTraverser: cls.paramGetters.foldLeft(tp) { (core, getter) => if getter.termRef.isTracked then val getterType = tp.memberInfo(getter).strippedDealias - RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var())) + RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(ctx.owner))) .showing(i"add capture refinement $tp --> $result", capt) else core @@ -164,7 +164,7 @@ extends tpd.TreeTraverser: resType = this(tp.resType)) case _ => mapOver(tp) - addVar(addCaptureRefinements(tp1)) + addVar(addCaptureRefinements(tp1), ctx.owner) end apply end mapInferred @@ -290,14 +290,15 @@ extends tpd.TreeTraverser: case tree: DefDef => if isExcluded(tree.symbol) then return - tree.tpt match - case tpt: TypeTree if tree.symbol.allOverriddenSymbols.hasNext => - tree.paramss.foreach(traverse) - transformTT(tpt, boxed = false, exact = true) - traverse(tree.rhs) - //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") - case _ => - traverseChildren(tree) + inContext(ctx.withOwner(tree.symbol)): + tree.tpt match + case tpt: TypeTree if tree.symbol.allOverriddenSymbols.hasNext => + tree.paramss.foreach(traverse) + transformTT(tpt, boxed = false, exact = true) + traverse(tree.rhs) + //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") + case _ => + traverseChildren(tree) case tree @ ValDef(_, tpt: TypeTree, _) => transformTT(tpt, boxed = tree.symbol.is(Mutable), // types of mutable variables are boxed @@ -325,6 +326,9 @@ extends tpd.TreeTraverser: i"Sealed type variable $pname", "be instantiated to", i"This is often caused by a local capability$where\nleaking as part of its result.", tree.srcPos) + case tree: Template => + inContext(ctx.withOwner(tree.symbol.owner)): + traverseChildren(tree) case _ => traverseChildren(tree) postProcess(tree) @@ -336,7 +340,7 @@ extends tpd.TreeTraverser: case tree: ValOrDefDef => val sym = tree.symbol - // replace an existing symbol info with inferred types where capture sets of + // 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( @@ -398,7 +402,11 @@ extends tpd.TreeTraverser: val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then // add capture set to self type of nested classes if no self type is given explicitly - val selfRefs = CaptureSet.Var() + val selfRefs = CaptureSet.Var(cls) + // 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 newInfo = ClassInfo(prefix, cls, ps, decls, CapturingType(cinfo.selfType, selfRefs) .showing(i"inferred self type for $cls: $result", capt)) @@ -512,11 +520,11 @@ extends tpd.TreeTraverser: /** Add a capture set variable to `tp` if necessary, or maybe pull out * an embedded capture set variable from a part of `tp`. */ - def addVar(tp: Type)(using Context): Type = + def addVar(tp: Type, owner: Symbol)(using Context): Type = decorate(tp, addedSet = _.dealias.match - case CapturingType(_, refs) => CaptureSet.Var(refs.elems) - case _ => CaptureSet.Var()) + 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(()))) diff --git a/compiler/src/dotty/tools/dotc/core/Contexts.scala b/compiler/src/dotty/tools/dotc/core/Contexts.scala index 8a7f2ff4e051..f0a1453672e0 100644 --- a/compiler/src/dotty/tools/dotc/core/Contexts.scala +++ b/compiler/src/dotty/tools/dotc/core/Contexts.scala @@ -815,7 +815,7 @@ object Contexts { * Note: plain TypeComparers always take on the kind of the outer comparer if they are in the same context. * In other words: tracking or explaining is a sticky property in the same context. */ - private def comparer(using Context): TypeComparer = + def comparer(using Context): TypeComparer = util.Stats.record("comparing") val base = ctx.base if base.comparersInUse > 0 diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index de40ac7232b7..98f4b3b6240f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -676,6 +676,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: RefinedType => return isSubInfo(tp1.refinedInfo, tp2.refinedInfo) case _ => + end if val skipped2 = skipMatching(tp1w, tp2) if (skipped2 eq tp2) || !Config.fastPathForRefinedSubtype then @@ -3132,6 +3133,9 @@ object TypeComparer { def tracked[T](op: TrackingTypeComparer => T)(using Context): T = comparing(_.tracked(op)) + + def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + comparing(_.subCaptures(refs1, refs2, frozen)) } object TrackingTypeComparer: diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f098d74a93f0..ce744310c722 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} +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun, ccNestingLevel} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -2199,6 +2199,9 @@ object Types { override def captureSet(using Context): CaptureSet = val cs = captureSetOfInfo if isTrackableRef && !cs.isAlwaysEmpty then singletonCaptureSet else cs + + /** The nesting level of this reference as defined by capture checking */ + def ccNestingLevel(using Context): Int end CaptureRef /** A trait for types that bind other types that refer to them. @@ -2906,6 +2909,8 @@ object Types { override def normalizedRef(using Context): CaptureRef = if isTrackableRef then symbol.termRef else this + + def ccNestingLevel(using Context) = symbol.ccNestingLevel } abstract case class TypeRef(override val prefix: Type, @@ -3072,6 +3077,8 @@ object Types { def sameThis(that: Type)(using Context): Boolean = (that eq this) || that.match case that: ThisType => this.cls eq that.cls case _ => false + + def ccNestingLevel(using Context) = cls.ccNestingLevel } final class CachedThisType(tref: TypeRef) extends ThisType(tref) @@ -4675,6 +4682,7 @@ object Types { def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) override def isTrackableRef(using Context) = true + def ccNestingLevel(using Context) = 0 // !!! Is this the right level? } private final class TermParamRefImpl(binder: TermLambda, paramNum: Int) extends TermParamRef(binder, paramNum) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index ff09a6084136..26b7a3c12746 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, toCaptureSet, IllegalCaptureRef} +import cc.{CaptureSet, toCaptureSet, IllegalCaptureRef, ccNestingLevelOpt} class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { @@ -865,10 +865,13 @@ 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"%${tp.symbol.nestingLevel}" + case tp: NamedType if !tp.symbol.isStatic => s"%${nestingLevel(tp.symbol)}" case tp: TypeVar => s"%${tp.nestingLevel}" case tp: TypeParamRef => ctx.typerState.constraint.typeVarOfParam(tp) match case tvar: TypeVar => s"%${tvar.nestingLevel}" diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 2e64ffc9bbf4..6f2e7b8e6153 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -15,6 +15,7 @@ import typer.ErrorReporting.err import typer.ProtoTypes.* import typer.TypeAssigner.seqLitType import typer.ConstFold +import typer.ErrorReporting.{Addenda, NothingToAdd} import NamerOps.methodType import config.Printers.recheckr import util.Property @@ -561,7 +562,7 @@ abstract class Recheck extends Phase, SymTransformer: case _ => checkConformsExpr(tpe.widenExpr, pt.widenExpr, tree) - def checkConformsExpr(actual: Type, expected: Type, tree: Tree)(using Context): Unit = + def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda = NothingToAdd)(using Context): Unit = //println(i"check conforms $actual <:< $expected") def isCompatible(expected: Type): Boolean = @@ -574,7 +575,7 @@ abstract class Recheck extends Phase, SymTransformer: } if !isCompatible(expected) then recheckr.println(i"conforms failed for ${tree}: $actual vs $expected") - err.typeMismatch(tree.withType(actual), expected) + err.typeMismatch(tree.withType(actual), expected, addenda) else if debugSuccesses then tree match case _: Ident => From ab884afdba235b675cb2e4c3a30748404acb7195 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 21 Aug 2023 17:28:54 +0200 Subject: [PATCH 18/76] Introduce caps.Root type for user-definable capture roots --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 4 ++-- compiler/src/dotty/tools/dotc/core/Definitions.scala | 1 + library/src/scala/caps.scala | 6 ++++-- tests/neg-custom-args/captures/cc-this2.check | 2 +- tests/neg-custom-args/captures/exception-definitions.check | 2 +- tests/pos/dotty-experimental.scala | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 5903fb1e0eb9..c9c808ee9095 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -932,8 +932,8 @@ object CaptureSet: tp.captureSet case tp: TermParamRef => tp.captureSet - case _: TypeRef => - empty + case tp: TypeRef => + if tp.typeSymbol == defn.Caps_Root then universal else empty case _: TypeParamRef => empty case CapturingType(parent, refs) => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b4df6bcd4ca5..eb2e20bfcb29 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -971,6 +971,7 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") + @tu lazy val Caps_Root: TypeSymbol = CapsModule.requiredType("Root") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_unsafeBox: Symbol = CapsUnsafeModule.requiredMethod("unsafeBox") diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 50f65a29c912..9b540c504af6 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -4,12 +4,14 @@ import annotation.experimental @experimental object caps: + opaque type Root = Unit + /** The universal capture reference (deprecated) */ @deprecated("Use `cap` instead") - val `*`: Any = () + val `*`: Root = () /** The universal capture reference */ - val cap: Any = () + val cap: Root = () object unsafe: diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check index d2f87a131791..ec09a56c3b85 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 (caps.cap : Any) is not included in allowed capture set {} of pure base class class C + |reference (caps.cap : caps.Root) is not included in allowed capture set {} of pure base class class C 3 | this: D^ => diff --git a/tests/neg-custom-args/captures/exception-definitions.check b/tests/neg-custom-args/captures/exception-definitions.check index 189a6f091c0b..47ab6e063137 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 (caps.cap : Any) is not included in allowed capture set {} of pure base class class Throwable + |reference (caps.cap : caps.Root) is not included in 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 diff --git a/tests/pos/dotty-experimental.scala b/tests/pos/dotty-experimental.scala index ada386143a0a..df2956ec2832 100644 --- a/tests/pos/dotty-experimental.scala +++ b/tests/pos/dotty-experimental.scala @@ -3,6 +3,6 @@ import language.experimental.captureChecking object test { - val x = caps.cap + val x: caps.Root = caps.cap } From 45e328b3510fb702bff96eb67859f89f2496408b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 22 Aug 2023 10:01:20 +0200 Subject: [PATCH 19/76] Drop anonymous functions as level owners --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 9d300248f9a5..4abc3772ac10 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -33,7 +33,6 @@ def allowUniversalInBoxed(using Context) = /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type) extends Exception - /** Capture checking state, consisting of * - nestingLevels: A map associating certain symbols (the nesting level owners) 8 with their ccNestingLevel @@ -273,13 +272,15 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeUnbox /** The owner of the current level. Qualifying owners are - * - methods other than constructors + * - methods other than constructors and anonymous functions * - classes, if they are not staticOwners * - _root_ */ def levelOwner(using Context): Symbol = if sym.isStaticOwner then defn.RootClass - else if sym.isClass || sym.is(Method) && !sym.isConstructor then sym + else if sym.isClass + || sym.is(Method) && !sym.isConstructor && !sym.isAnonymousFunction + then sym else sym.owner.levelOwner /** The nesting level of `sym` for the purposes of `cc`, From 133334477526ef62e77f36490461ab29478cea3a Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 22 Aug 2023 10:01:36 +0200 Subject: [PATCH 20/76] Introduce RootVar --- .../src/dotty/tools/dotc/cc/RootVar.scala | 50 +++++++++++++++++++ .../tools/dotc/printing/PlainPrinter.scala | 9 +++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/RootVar.scala diff --git a/compiler/src/dotty/tools/dotc/cc/RootVar.scala b/compiler/src/dotty/tools/dotc/cc/RootVar.scala new file mode 100644 index 000000000000..d85508e049dd --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/RootVar.scala @@ -0,0 +1,50 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import Hashable.Binders +import util.Spans.Span +import printing.Showable + +class RootVar(val source: Symbol) extends CaptureRef, Showable: + + var upperBound: Symbol = NoSymbol + var lowerBound: Symbol = NoSymbol + + def constrainFromAbove(bound: Symbol)(using Context): Boolean = + val level = bound.ccNestingLevel + if !upperBound.exists || upperBound.ccNestingLevel > level then + if !lowerBound.exists || lowerBound.ccNestingLevel <= level then + upperBound = bound + true + else false + else true + + def constrainFromBelow(bound: Symbol)(using Context): Boolean = + val level = bound.ccNestingLevel + if !lowerBound.exists || lowerBound.ccNestingLevel < level then + if !upperBound.exists || upperBound.ccNestingLevel >= level then + lowerBound = bound + true + else false + else true + + override def isRootCapability(using Context) = true + + override def captureSetOfInfo(using Context) = CaptureSet.universal + + def ccNestingLevel(using Context): Int = + if upperBound.exists then upperBound.ccNestingLevel + else lowerBound.ccNestingLevel + def computeHash(bs: Binders): Int = hash + def hash: Int = System.identityHashCode(this) + def underlying(using Context): Type = defn.Caps_Root.typeRef + +end RootVar + +//class LevelError(val rvar: RootVar, val newBound: Symbol, val isUpper: Boolean) extends Exception + + + diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index ed44da1a1eca..b9c82dd27323 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, isBoxed} +import cc.{CapturingType, EventuallyCapturingType, CaptureSet, RootVar, isBoxed, ccNestingLevel} class PlainPrinter(_ctx: Context) extends Printer { @@ -192,6 +192,13 @@ class PlainPrinter(_ctx: Context) extends Printer { if tvar.exists then s"#${tvar.asInstanceOf[TypeVar].nestingLevel.toString}" else "" else "" ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ suffix + case tp: RootVar => + def boundText(sym: Symbol): Text = + if sym.exists then nameString(sym) ~ s"/${sym.ccNestingLevel}" + else "" + "'cap[" ~ nameString(tp.source) + ~ "](" ~ boundText(tp.lowerBound) + ~ ".." ~ boundText(tp.upperBound) ~ ")" case tp: SingletonType => toTextSingleton(tp) case AppliedType(tycon, args) => From 2f359400838a98acc03823e66c0ae1f8443e514e Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 21 Aug 2023 11:18:52 +0200 Subject: [PATCH 21/76] Fix -Xprint:cc Previously, the ccState property was missing on the context used for printing at phase cc. --- compiler/src/dotty/tools/dotc/Run.scala | 5 +++-- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 ++ compiler/src/dotty/tools/dotc/core/Phases.scala | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 3e7bba86dcf4..9aaf12da3dcc 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -247,8 +247,9 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint profiler.afterPhase(phase, profileBefore) if (ctx.settings.Xprint.value.containsPhase(phase)) for (unit <- units) - lastPrintedTree = - printTree(lastPrintedTree)(using ctx.fresh.setPhase(phase.next).setCompilationUnit(unit)) + def printCtx(unit: CompilationUnit) = phase.printingContext( + ctx.fresh.setPhase(phase.next).setCompilationUnit(unit)) + lastPrintedTree = printTree(lastPrintedTree)(using printCtx(unit)) report.informTime(s"$phase ", start) Stats.record(s"total trees at end of $phase", ast.Trees.ntrees) for (unit <- units) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 590c180f8381..a5aecce38af9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -209,6 +209,8 @@ class CheckCaptures extends Recheck, SymTransformer: if Synthetics.needsTransform(sym) then Synthetics.transform(sym, toCC = false) else super.transformSym(sym) + override def printingContext(ctx: Context) = ctx.withProperty(ccState, Some(new CCState)) + class CaptureChecker(ictx: Context) extends Rechecker(ictx): import ast.tpd.* diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 2a3828004525..3fc7238cdd82 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -366,6 +366,11 @@ object Phases { def initContext(ctx: FreshContext): Unit = () + /** A hook that allows to transform the usual context passed to the function + * that prints a compilation unit after a phase + */ + def printingContext(ctx: Context): Context = ctx + private var myPeriod: Period = Periods.InvalidPeriod private var myBase: ContextBase = _ private var myErasedTypes = false From 9a48aba104d0c41255677537a6453865f3491b14 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 23 Aug 2023 10:00:42 +0200 Subject: [PATCH 22/76] Add RefiningVar Intended as a stop-gap for some (as-seen-from related?) problems when comparing capture refinements of classes. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 7 ++++ compiler/src/dotty/tools/dotc/cc/Setup.scala | 3 +- .../dotty/tools/dotc/core/TypeComparer.scala | 12 +++++++ .../src/dotty/tools/dotc/core/Types.scala | 33 +++++++++++-------- tests/neg-custom-args/captures/i15116.check | 4 +-- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index c9c808ee9095..4cecb698ce0e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -580,6 +580,11 @@ object CaptureSet: override def toString = s"Var$id$elems" end Var + /** A variable used in refinements of class parameters. See `addCaptureRefinements`. + */ + class RefiningVar(owner: Symbol, getter: Symbol)(using @constructorOnly ctx: Context) extends Var(owner): + description = i"of parameter ${getter.name} of ${getter.owner}" + /** A variable that is derived from some other variable via a map or filter. */ abstract class DerivedVar(owner: Symbol, initialElems: Refs)(using @constructorOnly ctx: Context) extends Var(owner, initialElems): @@ -936,6 +941,8 @@ object CaptureSet: if tp.typeSymbol == defn.Caps_Root then universal else empty case _: TypeParamRef => empty + case CapturingType(parent, refs: RefiningVar) => + refs case CapturingType(parent, refs) => recur(parent) ++ refs case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 80402f47a1dc..f62690139897 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -106,7 +106,8 @@ extends tpd.TreeTraverser: cls.paramGetters.foldLeft(tp) { (core, getter) => if getter.termRef.isTracked then val getterType = tp.memberInfo(getter).strippedDealias - RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(ctx.owner))) + RefinedType(core, getter.name, + CapturingType(getterType, CaptureSet.RefiningVar(ctx.owner, getter))) .showing(i"add capture refinement $tp --> $result", capt) else core diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 98f4b3b6240f..f62b852318e0 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -676,6 +676,18 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: RefinedType => return isSubInfo(tp1.refinedInfo, tp2.refinedInfo) case _ => + else tp2.refinedInfo match + case rinfo2 @ CapturingType(_, refs: CaptureSet.RefiningVar) => + tp1.widen match + case RefinedType(parent1, tp2.refinedName, rinfo1) => + // When comparing against a Var in class instance refinement, + // take the Var as the precise truth, don't also look in the parent. + // The parent might have a capture root at the wrong level. + // TODO: Generalize this to other refinement situations where the + // lower type's refinement appears elsewhere? + return isSubType(rinfo1, rinfo2) && recur(parent1, tp2.parent) + case _ => + case _ => end if val skipped2 = skipMatching(tp1w, tp2) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index ce744310c722..8b0c56043890 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -828,19 +828,26 @@ object Types { pinfo recoverable_& rinfo pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, jointInfo) } - else - val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] - val joint = pdenot.meet( - new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), - pre, - safeIntersection = ctx.base.pendingMemberSearches.contains(name)) - joint match - case joint: SingleDenotation - if isRefinedMethod && rinfo <:< joint.info => - // use `rinfo` to keep the right parameter names for named args. See i8516.scala. - joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) - case _ => - joint + else rinfo match + case CapturingType(_, cs: CaptureSet.RefiningVar) => + // If `rinfo` is a capturing type added by `addCaptureRefinements` it + // already contains everything there is to know about the member type. + // On the other hand, the member in parent might belong to an outer nesting level, + // which should be ignored at the point where instances of the class are constructed. + pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, rinfo) + case _ => + val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] + val joint = pdenot.meet( + new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), + pre, + safeIntersection = ctx.base.pendingMemberSearches.contains(name)) + joint match + case joint: SingleDenotation + if isRefinedMethod && rinfo <:< joint.info => + // use `rinfo` to keep the right parameter names for named args. See i8516.scala. + joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) + case _ => + joint } def goApplied(tp: AppliedType, tycon: HKTypeLambda) = diff --git a/tests/neg-custom-args/captures/i15116.check b/tests/neg-custom-args/captures/i15116.check index 4b637a7c2e40..765477df7466 100644 --- a/tests/neg-custom-args/captures/i15116.check +++ b/tests/neg-custom-args/captures/i15116.check @@ -9,7 +9,7 @@ 5 | val x = Foo(m) // error | ^^^^^^^^^^^^^^ | Non-local value x cannot have an inferred type - | Foo{val m: String^}^{Baz.this} + | 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 -------------------------------------------------------------- @@ -23,6 +23,6 @@ 9 | val x = Foo(m) // error | ^^^^^^^^^^^^^^ | Non-local value x cannot have an inferred type - | Foo{val m: String^}^{Baz2.this} + | Foo{val m: String^{Baz2.this}}^{Baz2.this} | with non-empty capture set {Baz2.this}. | The type needs to be declared explicitly. From 3715293ae3fefcb7c3333e1e327dffe4a75bb94d Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 23 Aug 2023 09:02:58 +0200 Subject: [PATCH 23/76] Infrastructure for local roots --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 93 ++++++++++++++- .../src/dotty/tools/dotc/cc/CaptureRoot.scala | 111 ++++++++++++++++++ .../src/dotty/tools/dotc/cc/CaptureSet.scala | 56 +++++---- .../dotty/tools/dotc/cc/CheckCaptures.scala | 15 ++- .../src/dotty/tools/dotc/cc/RootVar.scala | 50 -------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 49 ++++---- .../dotty/tools/dotc/core/Decorators.scala | 3 + .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../src/dotty/tools/dotc/core/Types.scala | 34 ++++-- .../tools/dotc/printing/PlainPrinter.scala | 28 +++-- .../dotty/tools/dotc/transform/Recheck.scala | 14 ++- 11 files changed, 333 insertions(+), 121 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala delete mode 100644 compiler/src/dotty/tools/dotc/cc/RootVar.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 4abc3772ac10..247f9c514268 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -10,12 +10,15 @@ import config.SourceVersion import config.Printers.capt import util.Property.Key import tpd.* +import StdNames.nme import config.Feature import collection.mutable private val Captures: Key[CaptureSet] = Key() private val BoxedType: Key[BoxedTypeCache] = Key() +private val enableRootMapping = false + /** 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. @@ -44,12 +47,46 @@ class IllegalCaptureRef(tpe: Type) extends Exception */ class CCState: val nestingLevels: mutable.HashMap[Symbol, Int] = new mutable.HashMap - val localRoots: mutable.HashMap[Symbol, CaptureRef] = new mutable.HashMap + val localRoots: mutable.HashMap[Symbol, Symbol] = new mutable.HashMap var levelError: Option[(CaptureRef, CaptureSet)] = None /** Property key for capture checking state */ val ccState: Key[CCState] = Key() +trait FollowAliases extends TypeMap: + def mapOverFollowingAliases(t: Type): Type = t match + case t: LazyRef => + val t1 = this(t.ref) + if t1 ne t.ref then t1 else t + case _ => + val t1 = t.dealias + if t1 ne t then + val t2 = this(t1) + if t2 ne t1 then return t2 + mapOver(t) + +class mapRoots(from: CaptureRoot, to: CaptureRoot)(using Context) extends BiTypeMap, FollowAliases: + thisMap => + + def apply(t: Type): Type = t match + case t: TermRef if (t eq from) && enableRootMapping => + to + 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 + && CaptureRoot.isEnclosingRoot(from, t) + && CaptureRoot.isEnclosingRoot(t, from) => to + case from: CaptureRoot.Var if from.followAlias eq t => to + case _ => from + case _ => + mapOverFollowingAliases(t) + + def inverse = mapRoots(to, from) +end mapRoots + extension (tree: Tree) /** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */ @@ -216,6 +253,39 @@ extension (tp: Type) tp.tp1.isAlwaysPure && tp.tp2.isAlwaysPure case _ => false +/*!!! + def capturedLocalRoot(using Context): Symbol = + tp.captureSet.elems.toList + .filter(_.isLocalRootCapability) + .map(_.termSymbol) + .maxByOption(_.ccNestingLevel) + .getOrElse(NoSymbol) + + /** Remap roots defined in `cls` to the ... */ + def remapRoots(pre: Type, cls: Symbol)(using Context): Type = + if cls.isStaticOwner then tp + else + val from = + if cls.source == ctx.compilationUnit.source then cls.localRoot + else defn.captureRoot + mapRoots(from, capturedLocalRoot)(tp) + + + def containsRoot(root: Symbol)(using Context): Boolean = + val search = new TypeAccumulator[Boolean]: + def apply(x: Boolean, t: Type): Boolean = + if x then true + else t.dealias match + case t1: TermRef if t1.symbol == root => true + case t1: TypeRef if t1.classSymbol.hasAnnotation(defn.CapabilityAnnot) => true + case t1: MethodType => + !foldOver(x, t1.paramInfos) && this(x, t1.resType) + case t1 @ AppliedType(tycon, args) if defn.isFunctionSymbol(tycon.typeSymbol) => + val (inits, last :: Nil) = args.splitAt(args.length - 1): @unchecked + !foldOver(x, inits) && this(x, last) + case t1 => foldOver(x, t1) + search(false, tp) +*/ extension (cls: ClassSymbol) @@ -277,7 +347,8 @@ extension (sym: Symbol) * - _root_ */ def levelOwner(using Context): Symbol = - if sym.isStaticOwner then defn.RootClass + if !sym.exists || sym.isRoot || sym.isStaticOwner + then defn.RootClass else if sym.isClass || sym.is(Method) && !sym.isConstructor && !sym.isAnonymousFunction then sym @@ -302,6 +373,18 @@ extension (sym: Symbol) Some(ccNestingLevel) else None + def localRoot(using Context): 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_Root.typeRef, nestingLevel = owner.ccNestingLevel) + def lclRoot = + if owner.isTerm then + owner.paramSymss.nestedFind(_.info.typeSymbol == defn.Caps_Root).getOrElse(newRoot) + else + newRoot + ctx.property(ccState).get.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. @@ -314,6 +397,12 @@ extension (sym: Symbol) def minNested(other: Symbol)(using Context): Symbol = if sym.ccNestingLevel > other.ccNestingLevel then other else sym +extension (tp: TermRef | ThisType) + /** The nesting level of this reference as defined by capture checking */ + def ccNestingLevel(using Context): Int = tp match + case tp: TermRef => tp.symbol.ccNestingLevel + case tp: ThisType => tp.cls.ccNestingLevel + 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/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala new file mode 100644 index 000000000000..82e409240cd4 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -0,0 +1,111 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import Hashable.Binders +import printing.Showable +import util.SimpleIdentitySet +import Decorators.i +import scala.annotation.constructorOnly + +type CaptureRoot = TermRef | CaptureRoot.Var + +object CaptureRoot: + + case class Var(owner: Symbol, source: Symbol)(using @constructorOnly ictx: Context) extends CaptureRef, Showable: + + 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 + + override def localRootOwner(using Context) = owner + override def isTrackableRef(using Context): Boolean = true + override def captureSetOfInfo(using Context) = CaptureSet.universal + + def followAlias: CaptureRoot = alias match + case alias: Var if alias ne this => alias.followAlias + case _ => this + + def locallyConsistent = + lowerLevel <= upperLevel + && lowerRoots.forall(_.upperLevel <= upperLevel) + && upperRoots.forall(_.lowerLevel >= lowerLevel) + + def computeHash(bs: Binders): Int = hash + def hash: Int = System.identityHashCode(this) + def underlying(using Context): Type = defn.Caps_Root.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 + 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 + 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 + true + else + c1.upperRoots -= c2 + false + end isEnclosingRoot +end CaptureRoot + +//class LevelError(val rvar: RootVar, val newBound: Symbol, val isUpper: Boolean) extends Exception + + + diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 4cecb698ce0e..ac23f17695b8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -118,12 +118,23 @@ sealed abstract class CaptureSet extends Showable: if accountsFor(elem) then CompareResult.OK else addNewElems(elem.singletonCaptureSet.elems, origin) - /* x subsumes y if x is the same as y, or x is a this reference and y refers to a field of x */ - extension (x: CaptureRef) private def subsumes(y: CaptureRef) = - (x eq y) - || y.match - case y: TermRef => y.prefix eq x - case _ => false + /* x subsumes y if one of the following is true: + * - x is the same as y, + * - x is a this reference and y refers to a field of x + * - x and y are local roots and y is an enclosing root of x + */ + extension (x: CaptureRef)(using Context) + private def subsumes(y: CaptureRef) = + (x eq y) + || x.isGenericRootCapability + || y.match + case y: TermRef => (y.prefix eq x) || x.isRootIncluding(y) + case _ => false + + private def isRootIncluding(y: CaptureRef) = + x.isLocalRootCapability && y.isLocalRootCapability + && CaptureRoot.isEnclosingRoot(y.asInstanceOf[CaptureRoot], x.asInstanceOf[CaptureRoot]) + end extension /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. @@ -454,7 +465,13 @@ object CaptureSet: def addNewElems(newElems: Refs, 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 levelsOK(newElems) then + else if newElems.exists(!levelOK(_)) then + val res = widenCaptures(newElems) match + case Some(newElems1) => tryInclude(newElems1, origin) + case None => CompareResult.fail(this) + if !res.isOK then recordLevelError() + res + else //assert(id != 2, newElems) elems ++= newElems if isUniversal then rootAddedHandler() @@ -463,24 +480,20 @@ object CaptureSet: (CompareResult.OK /: deps) { (r, dep) => r.andAlso(dep.tryInclude(newElems, this)) } - else - val res = widenCaptures(newElems) match - case Some(newElems1) => tryInclude(newElems1, origin) - case None => CompareResult.fail(this) - if !res.isOK then recordLevelError() - res private def recordLevelError()(using Context): Unit = for elem <- triedElem do ctx.property(ccState).get.levelError = Some((elem, this)) - private def levelsOK(elems: Refs)(using Context): Boolean = - !elems.exists(_.ccNestingLevel > ownLevel) + private def levelOK(elem: CaptureRef)(using Context): Boolean = elem match + case elem: (TermRef | ThisType) => elem.ccNestingLevel <= ownLevel + case elem: CaptureRoot.Var => CaptureRoot.isEnclosingRoot(elem, owner.localRoot.termRef) + case _ => true private def widenCaptures(elems: Refs)(using Context): Option[Refs] = val res = optional: (SimpleIdentitySet[CaptureRef]() /: elems): (acc, elem) => - if elem.ccNestingLevel <= ownLevel then acc + elem + if levelOK(elem) then acc + elem else if elem.isRootCapability then break() else val saved = triedElem @@ -518,8 +531,8 @@ object CaptureSet: * of this set. The universal set {cap} is a sound fallback. */ final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = - if computingApprox then universal - else if isConst then this + if isConst || elems.exists(_.isRootCapability) then this + else if computingApprox then universal else computingApprox = true try computeApprox(origin).ensuring(_.isConst) @@ -536,6 +549,7 @@ object CaptureSet: def solve()(using Context): Unit = if !isConst then val approx = upperApprox(empty) + .showing(i"solve $this = $result", capt) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") val newElems = approx.elems -- elems if newElems.isEmpty || addNewElems(newElems, empty)(using ctx, VarState()).isOK then @@ -1009,10 +1023,12 @@ object CaptureSet: state <- ctx.property(ccState).toList (ref, cs) <- state.levelError yield - val level = ref.ccNestingLevel + val levelStr = ref match + case ref: (TermRef | ThisType) => i", defined at level ${ref.ccNestingLevel}" + case _ => "" i""" | - |Note that reference ${ref}, defined at level $level + |Note that reference ${ref}$levelStr |cannot be included in outer capture set $cs, defined at level ${cs.owner.nestingLevel} in ${cs.owner}""" end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a5aecce38af9..f154c439127e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -263,7 +263,8 @@ class CheckCaptures extends Recheck, SymTransformer: def header = if cs1.elems.size == 1 then i"reference ${cs1.elems.toList}%, % is not" else i"references $cs1 are not all" - report.error(em"$header included in allowed capture set ${res.blocking}", pos) + def toAdd: String = CaptureSet.levelErrors.toAdd.mkString + report.error(em"$header included in allowed capture set ${res.blocking}$toAdd", pos) /** The current environment */ private var curEnv: Env = inContext(ictx): @@ -410,6 +411,16 @@ class CheckCaptures extends Recheck, SymTransformer: 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] + /** A specialized implementation of the apply rule. * * E |- f: Ra ->Cf Rr^Cr @@ -560,6 +571,7 @@ class CheckCaptures extends Recheck, SymTransformer: // rechecking the body. val res = recheckClosure(expr, pt, forceDependent = true) recheckDef(mdef, mdef.symbol) + //println(i"RECHECK CLOSURE ${mdef.symbol.info}") res end recheckClosureBlock @@ -815,6 +827,7 @@ class CheckCaptures extends Recheck, SymTransformer: else reconstruct(aargs1, ares1) (resTp, curEnv.captured) + end adaptFun /** Adapt type function type `actual` to the expected type. * @see [[adaptFun]] diff --git a/compiler/src/dotty/tools/dotc/cc/RootVar.scala b/compiler/src/dotty/tools/dotc/cc/RootVar.scala deleted file mode 100644 index d85508e049dd..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/RootVar.scala +++ /dev/null @@ -1,50 +0,0 @@ -package dotty.tools -package dotc -package cc - -import core.* -import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* -import Hashable.Binders -import util.Spans.Span -import printing.Showable - -class RootVar(val source: Symbol) extends CaptureRef, Showable: - - var upperBound: Symbol = NoSymbol - var lowerBound: Symbol = NoSymbol - - def constrainFromAbove(bound: Symbol)(using Context): Boolean = - val level = bound.ccNestingLevel - if !upperBound.exists || upperBound.ccNestingLevel > level then - if !lowerBound.exists || lowerBound.ccNestingLevel <= level then - upperBound = bound - true - else false - else true - - def constrainFromBelow(bound: Symbol)(using Context): Boolean = - val level = bound.ccNestingLevel - if !lowerBound.exists || lowerBound.ccNestingLevel < level then - if !upperBound.exists || upperBound.ccNestingLevel >= level then - lowerBound = bound - true - else false - else true - - override def isRootCapability(using Context) = true - - override def captureSetOfInfo(using Context) = CaptureSet.universal - - def ccNestingLevel(using Context): Int = - if upperBound.exists then upperBound.ccNestingLevel - else lowerBound.ccNestingLevel - def computeHash(bs: Binders): Int = hash - def hash: Int = System.identityHashCode(this) - def underlying(using Context): Type = defn.Caps_Root.typeRef - -end RootVar - -//class LevelError(val rvar: RootVar, val newBound: Symbol, val isUpper: Boolean) extends Exception - - - diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index f62690139897..a475514ef377 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -212,39 +212,37 @@ extends tpd.TreeTraverser: case _ => tp - private def expandAliases(using Context) = new TypeMap: + private def expandAliases(using Context) = new TypeMap with FollowAliases: 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: LazyRef => - val t1 = this(t.ref) - if t1 ne t.ref then t1 else t case t @ AnnotatedType(t1, ann) => // Don't map capture sets, since that would implicitly normalize sets that // are not well-formed. t.derivedAnnotatedType(this(t1), ann) case _ => - val t1 = t.dealias - if t1 ne t then - val t2 = this(t1) - if t2 ne t1 then return t2 - mapOver(t) + mapOverFollowingAliases(t) - private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = + private def transformExplicitType(tp: Type, boxed: Boolean, mapRoots: Boolean)(using Context): Type = val tp1 = expandAliases(if boxed then box(tp) else tp) - if tp1 ne tp then capt.println(i"expanded: $tp --> $tp1") - tp1 + 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 + if tp2 ne tp then capt.println(i"expanded: $tp --> $tp2") + tp2 /** Transform type of type tree, and remember the transformed type as the type the tree */ - private def transformTT(tree: TypeTree, boxed: Boolean, exact: Boolean)(using Context): Unit = + private def transformTT(tree: TypeTree, boxed: Boolean, exact: Boolean, mapRoots: Boolean)(using Context): Unit = if !tree.hasRememberedType then tree.rememberType( if tree.isInstanceOf[InferredTypeTree] && !exact then transformInferredType(tree.tpe, boxed) - else transformExplicitType(tree.tpe, boxed)) + else transformExplicitType(tree.tpe, boxed, mapRoots)) /** Substitute parameter symbols in `from` to paramRefs in corresponding * method or poly types `to`. We use a single BiTypeMap to do everything. @@ -295,7 +293,7 @@ extends tpd.TreeTraverser: tree.tpt match case tpt: TypeTree if tree.symbol.allOverriddenSymbols.hasNext => tree.paramss.foreach(traverse) - transformTT(tpt, boxed = false, exact = true) + transformTT(tpt, boxed = false, exact = true, mapRoots = true) traverse(tree.rhs) //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") case _ => @@ -303,8 +301,10 @@ extends tpd.TreeTraverser: case tree @ ValDef(_, tpt: TypeTree, _) => transformTT(tpt, boxed = tree.symbol.is(Mutable), // types of mutable variables are boxed - exact = tree.symbol.allOverriddenSymbols.hasNext // types of symbols that override a parent don't get a capture set + exact = tree.symbol.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set + mapRoots = true ) + capt.println(i"mapped $tree = ${tpt.knownType}") if allowUniversalInBoxed && tree.symbol.is(Mutable) && !tree.symbol.hasAnnotation(defn.UncheckedCapturesAnnot) then @@ -316,7 +316,7 @@ extends tpd.TreeTraverser: case tree @ TypeApply(fn, args) => traverse(fn) for case arg: TypeTree <- args do - transformTT(arg, boxed = true, exact = false) // type arguments in type applications are boxed + transformTT(arg, boxed = true, exact = false, mapRoots = true) // type arguments in type applications are boxed if allowUniversalInBoxed then val polyType = fn.tpe.widen.asInstanceOf[TypeLambda] @@ -337,7 +337,9 @@ extends tpd.TreeTraverser: def postProcess(tree: Tree)(using Context): Unit = tree match case tree: TypeTree => - transformTT(tree, boxed = false, exact = false) // other types are not boxed + transformTT(tree, boxed = false, exact = false, + mapRoots = !ctx.owner.levelOwner.isStaticOwner // other types in static locaations are not boxed + ) case tree: ValOrDefDef => val sym = tree.symbol @@ -353,15 +355,18 @@ extends tpd.TreeTraverser: info match case mt: MethodOrPoly => val psyms = psymss.head + val mapr = + if (sym.isAnonymousFunction) then identity[Type] + else mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) 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 => subst(psym.info).asInstanceOf[mt.PInfo]), + psyms.map(psym => mapr(subst(psym.info)).asInstanceOf[mt.PInfo]), mt1 => - integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) + integrateRT(mapr(mt.resType), psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) ) case info: ExprType => info.derivedExprType(resType = @@ -420,7 +425,7 @@ extends tpd.TreeTraverser: modul.termRef.invalidateCaches() case _ => val info = atPhase(preRecheckPhase)(tree.symbol.info) - val newInfo = transformExplicitType(info, boxed = false) + val newInfo = transformExplicitType(info, boxed = false, mapRoots = !ctx.owner.isStaticOwner) if newInfo ne info then updateInfo(tree.symbol, newInfo) capt.println(i"update info of ${tree.symbol} from $info to $newInfo") @@ -514,7 +519,7 @@ extends tpd.TreeTraverser: else fallback val tp1 = tp.dealiasKeepAnnots if tp1 ne tp then - val tp2 = transformExplicitType(tp1, boxed = false) + val tp2 = transformExplicitType(tp1, boxed = false, mapRoots = true) maybeAdd(tp2, if tp2 ne tp1 then tp2 else tp) else maybeAdd(tp, tp) diff --git a/compiler/src/dotty/tools/dotc/core/Decorators.scala b/compiler/src/dotty/tools/dotc/core/Decorators.scala index b1e822368a81..fc2b6a852216 100644 --- a/compiler/src/dotty/tools/dotc/core/Decorators.scala +++ b/compiler/src/dotty/tools/dotc/core/Decorators.scala @@ -239,6 +239,9 @@ object Decorators { def nestedExists(p: T => Boolean): Boolean = xss match case xs :: xss1 => xs.exists(p) || xss1.nestedExists(p) case nil => false + def nestedFind(p: T => Boolean): Option[T] = xss match + case xs :: xss1 => xs.find(p).orElse(xss1.nestedFind(p)) + case nil => None end extension extension (text: Text) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index cd51d4bf79c2..8f99d4f5a240 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -287,6 +287,7 @@ object StdNames { // Compiler-internal val CAPTURE_ROOT: N = "cap" + val LOCAL_CAPTURE_ROOT: N = "" val CONSTRUCTOR: N = "" val STATIC_CONSTRUCTOR: N = "" val EVT2U: N = "evt2u$" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 8b0c56043890..b69a0a317c63 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2174,8 +2174,22 @@ object Types { */ final def isTracked(using Context): Boolean = isTrackableRef && !captureSetOfInfo.isAlwaysEmpty - /** Is this reference the root capability `cap` ? */ - def isRootCapability(using Context): Boolean = false + /** Is this reference the generic root capability `cap` ? */ + def isGenericRootCapability(using Context): Boolean = false + + /** 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 + + /** Is this reference the a (local or generic) root capability? */ + def isRootCapability(using Context): Boolean = + isGenericRootCapability || isLocalRootCapability /** Normalize reference so that it can be compared with `eq` for equality */ def normalizedRef(using Context): CaptureRef = this @@ -2207,8 +2221,6 @@ object Types { val cs = captureSetOfInfo if isTrackableRef && !cs.isAlwaysEmpty then singletonCaptureSet else cs - /** The nesting level of this reference as defined by capture checking */ - def ccNestingLevel(using Context): Int end CaptureRef /** A trait for types that bind other types that refer to them. @@ -2911,13 +2923,18 @@ object Types { || isRootCapability ) && !symbol.isOneOf(UnstableValueFlags) - override def isRootCapability(using Context): Boolean = + override def isGenericRootCapability(using Context): Boolean = name == nme.CAPTURE_ROOT && symbol == defn.captureRoot + override def localRootOwner(using Context): Symbol = + if name == nme.LOCAL_CAPTURE_ROOT + then + if symbol.owner.isLocalDummy then symbol.owner.owner + else symbol.owner + else NoSymbol + override def normalizedRef(using Context): CaptureRef = if isTrackableRef then symbol.termRef else this - - def ccNestingLevel(using Context) = symbol.ccNestingLevel } abstract case class TypeRef(override val prefix: Type, @@ -3084,8 +3101,6 @@ object Types { def sameThis(that: Type)(using Context): Boolean = (that eq this) || that.match case that: ThisType => this.cls eq that.cls case _ => false - - def ccNestingLevel(using Context) = cls.ccNestingLevel } final class CachedThisType(tref: TypeRef) extends ThisType(tref) @@ -4689,7 +4704,6 @@ object Types { def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) override def isTrackableRef(using Context) = true - def ccNestingLevel(using Context) = 0 // !!! Is this the right level? } private final class TermParamRefImpl(binder: TermLambda, paramNum: Int) extends TermParamRef(binder, paramNum) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index b9c82dd27323..1eeaa8456d06 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, RootVar, isBoxed, ccNestingLevel} +import cc.{CapturingType, EventuallyCapturingType, CaptureSet, CaptureRoot, isBoxed, ccNestingLevel} class PlainPrinter(_ctx: Context) extends Printer { @@ -192,13 +192,6 @@ class PlainPrinter(_ctx: Context) extends Printer { if tvar.exists then s"#${tvar.asInstanceOf[TypeVar].nestingLevel.toString}" else "" else "" ParamRefNameString(tp) ~ lambdaHash(tp.binder) ~ suffix - case tp: RootVar => - def boundText(sym: Symbol): Text = - if sym.exists then nameString(sym) ~ s"/${sym.ccNestingLevel}" - else "" - "'cap[" ~ nameString(tp.source) - ~ "](" ~ boundText(tp.lowerBound) - ~ ".." ~ boundText(tp.upperBound) ~ ")" case tp: SingletonType => toTextSingleton(tp) case AppliedType(tycon, args) => @@ -231,7 +224,12 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close case tp @ EventuallyCapturingType(parent, refs) => val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value - val refsText = if refs.isUniversal then rootSetText else toTextCaptureSet(refs) + val refsText = + if refs.isUniversal && + (refs.elems.size == 1 + || !ctx.settings.YccDebug.value && !refs.elems.exists(_.isLocalRootCapability)) + then rootSetText + else toTextCaptureSet(refs) toTextCapturing(parent, refsText, boxText) 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 @@ -363,7 +361,8 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextRef(tp: SingletonType): Text = controlled { tp match { case tp: TermRef => - toTextPrefixOf(tp) ~ selectionString(tp) + if tp.isLocalRootCapability then Str(s"") + else toTextPrefixOf(tp) ~ selectionString(tp) case tp: ThisType => nameString(tp.cls) + ".this" case SuperType(thistpe: SingletonType, _) => @@ -382,6 +381,15 @@ class PlainPrinter(_ctx: Context) extends Printer { if (homogenizedView) toText(tp.info) else if (ctx.settings.XprintTypes.value) "<" ~ toText(tp.repr) ~ ":" ~ toText(tp.info) ~ ">" else toText(tp.repr) + case tp: CaptureRoot.Var => + if tp.followAlias ne tp then toTextRef(tp.followAlias) + else + def boundText(sym: Symbol): Text = + if sym.exists then toTextRef(sym.termRef) ~ s"/${sym.ccNestingLevel}" + else "" + "'cap[" ~ nameString(tp.source) + ~ "](" ~ boundText(tp.lowerBound) + ~ ".." ~ boundText(tp.upperBound) ~ ")" } } diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 6f2e7b8e6153..2247140e681e 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -98,7 +98,6 @@ object Recheck: case Some(tpe) => tree.withType(tpe).asInstanceOf[T] case None => tree - /** Map ExprType => T to () ?=> T (and analogously for pure versions). * Even though this phase runs after ElimByName, ExprTypes can still occur * as by-name arguments of applied types. See note in doc comment for @@ -286,12 +285,15 @@ abstract class Recheck extends Phase, SymTransformer: protected def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = mt.instantiate(argTypes) + protected def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = funtpe + def recheckApply(tree: Apply, pt: Type)(using Context): Type = - val funTp = recheck(tree.fun) + val funtpe0 = recheck(tree.fun) // reuse the tree's type on signature polymorphic methods, instead of using the (wrong) rechecked one - val funtpe = if tree.fun.symbol.originalSignaturePolymorphic.exists then tree.fun.tpe else funTp - funtpe.widen match - case fntpe: MethodType => + val funtpe1 = if tree.fun.symbol.originalSignaturePolymorphic.exists then tree.fun.tpe else funtpe0 + funtpe1.widen match + case fntpe1: MethodType => + val fntpe = prepareFunction(fntpe1, tree.fun.symbol) assert(fntpe.paramInfos.hasSameLengthAs(tree.args)) val formals = if false && tree.symbol.is(JavaDefined) // see NOTE in mapJavaArgs @@ -312,7 +314,7 @@ abstract class Recheck extends Phase, SymTransformer: constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol)) //.showing(i"typed app $tree : $fntpe with ${tree.args}%, % : $argTypes%, % = $result") case tp => - assert(false, i"unexpected type of ${tree.fun}: $funtpe") + assert(false, i"unexpected type of ${tree.fun}: $tp") def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = recheck(tree.fun).widen match From cb061c08e9e62a32a1a182ed499ff1697c7160d6 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 23 Aug 2023 12:37:04 +0200 Subject: [PATCH 24/76] Fixes to infrastructure for local roots --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 8 +++++ .../src/dotty/tools/dotc/cc/CaptureOps.scala | 35 ++++++++++++++----- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 7 ++-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 10 +++++- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 6659818b333e..41767d81fb2b 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -799,6 +799,14 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => } } + /** An extractor for def of a closure contained the block of the closure, + * possibly with type ascriptions. + */ + object possiblyTypedClosureDef: + def unapply(tree: Tree)(using Context): Option[DefDef] = tree match + case Typed(expr, _) => unapply(expr) + case _ => closureDef.unapply(tree) + /** If tree is a closure, its body, otherwise tree itself */ def closureBody(tree: Tree)(using Context): Tree = tree match { case closureDef(meth) => meth.rhs diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 247f9c514268..7b09bda125be 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -341,17 +341,25 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox + def isLevelOwner(using Context): Boolean = + if sym.isClass then true + else if sym.is(Method) then + if sym.isAnonymousFunction then + // Setup added anonymous functions counting as level owners to nestingLevels + ctx.property(ccState).get.nestingLevels.contains(sym) + else !sym.isConstructor + else false + /** The owner of the current level. Qualifying owners are * - methods other than constructors and anonymous functions + * - anonymous functions, provided they either define a local + * root of type caps.Root, or they are the rhs of a val definition. * - classes, if they are not staticOwners * - _root_ */ def levelOwner(using Context): Symbol = - if !sym.exists || sym.isRoot || sym.isStaticOwner - then defn.RootClass - else if sym.isClass - || sym.is(Method) && !sym.isConstructor && !sym.isAnonymousFunction - then sym + if !sym.exists || sym.isRoot || sym.isStaticOwner then defn.RootClass + else if sym.isLevelOwner then sym else sym.owner.levelOwner /** The nesting level of `sym` for the purposes of `cc`, @@ -373,16 +381,25 @@ extension (sym: Symbol) Some(ccNestingLevel) else None + def setNestingLevel(level: Int)(using Context): Unit = + ctx.property(ccState).get.nestingLevels(sym) = level + + /** The parameter with type caps.Root in the leading term parameter section, + * or NoSymbol, if none exists. + */ + def definedLocalRoot(using Context): Symbol = + sym.paramSymss.dropWhile(psyms => psyms.nonEmpty && psyms.head.isType) match + case psyms :: Nil => psyms.find(_.info.typeSymbol == defn.Caps_Root).getOrElse(NoSymbol) + case _ => NoSymbol + def localRoot(using Context): 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_Root.typeRef, nestingLevel = owner.ccNestingLevel) def lclRoot = - if owner.isTerm then - owner.paramSymss.nestedFind(_.info.typeSymbol == defn.Caps_Root).getOrElse(newRoot) - else - newRoot + if owner.isTerm then owner.definedLocalRoot.orElse(newRoot) + else newRoot ctx.property(ccState).get.localRoots.getOrElseUpdate(owner, lclRoot) def maxNested(other: Symbol)(using Context): Symbol = diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index ac23f17695b8..41eb3969f713 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -126,14 +126,15 @@ sealed abstract class CaptureSet extends Showable: extension (x: CaptureRef)(using Context) private def subsumes(y: CaptureRef) = (x eq y) - || x.isGenericRootCapability + || x.isGenericRootCapability // !!! dubious || y.match case y: TermRef => (y.prefix eq x) || x.isRootIncluding(y) + case y: CaptureRoot.Var => x.isRootIncluding(y) case _ => false - private def isRootIncluding(y: CaptureRef) = + private def isRootIncluding(y: CaptureRoot) = x.isLocalRootCapability && y.isLocalRootCapability - && CaptureRoot.isEnclosingRoot(y.asInstanceOf[CaptureRoot], x.asInstanceOf[CaptureRoot]) + && CaptureRoot.isEnclosingRoot(y, x.asInstanceOf[CaptureRoot]) end extension /** {x} <:< this where <:< is subcapturing, but treating all variables diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a475514ef377..eca1da609b59 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -290,6 +290,9 @@ extends tpd.TreeTraverser: if isExcluded(tree.symbol) then return inContext(ctx.withOwner(tree.symbol)): + if tree.symbol.isAnonymousFunction && tree.symbol.definedLocalRoot.exists then + // closures that define parameters of type caps.Root count as level owners + tree.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) tree.tpt match case tpt: TypeTree if tree.symbol.allOverriddenSymbols.hasNext => tree.paramss.foreach(traverse) @@ -298,7 +301,12 @@ extends tpd.TreeTraverser: //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") case _ => traverseChildren(tree) - case tree @ ValDef(_, tpt: TypeTree, _) => + case tree @ ValDef(_, tpt: TypeTree, rhs) => + rhs match + case possiblyTypedClosureDef(ddef) => + // toplevel closures bound to vals count as level owners + ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) + case _ => transformTT(tpt, boxed = tree.symbol.is(Mutable), // types of mutable variables are boxed exact = tree.symbol.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set From dd0aad2bc09c7cc1db7bdf623a335026e65e7352 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 23 Aug 2023 14:49:57 +0200 Subject: [PATCH 25/76] Fix constructor handling when roots are mapped --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 29 +++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index eca1da609b59..aa65b56d9421 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -351,6 +351,24 @@ extends tpd.TreeTraverser: 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. @@ -364,7 +382,7 @@ extends tpd.TreeTraverser: case mt: MethodOrPoly => val psyms = psymss.head val mapr = - if (sym.isAnonymousFunction) then identity[Type] + if sym.isAnonymousFunction then identity[Type] else mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) mt.companion(mt.paramNames)( mt1 => @@ -379,12 +397,9 @@ extends tpd.TreeTraverser: case info: ExprType => info.derivedExprType(resType = integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) - case info if sym.isConstructor => - info - case _ => - val restp = tree.tpt.knownType - if prevLambdas.isEmpty then restp - else SubstParams(prevPsymss, prevLambdas)(restp) + case info => + if prevLambdas.isEmpty then localReturnType + else SubstParams(prevPsymss, prevLambdas)(localReturnType) def signatureChanges = tree.tpt.hasRememberedType && !sym.isConstructor From 7f230c34c8fc21d235ae41103e5d7129b1c2c4ae Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 23 Aug 2023 17:08:09 +0200 Subject: [PATCH 26/76] Fixes to backwards root mapping in Setup --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 8 ++++---- tests/pos-custom-args/captures/vars1.scala | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index aa65b56d9421..59352bbd9d8b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -382,8 +382,8 @@ extends tpd.TreeTraverser: case mt: MethodOrPoly => val psyms = psymss.head val mapr = - if sym.isAnonymousFunction then identity[Type] - else mapRoots(sym.localRoot.termRef, defn.captureRoot.termRef) + 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 @@ -392,7 +392,7 @@ extends tpd.TreeTraverser: val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) psyms.map(psym => mapr(subst(psym.info)).asInstanceOf[mt.PInfo]), mt1 => - integrateRT(mapr(mt.resType), psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) + mapr(integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas)) ) case info: ExprType => info.derivedExprType(resType = @@ -409,7 +409,7 @@ extends tpd.TreeTraverser: if sym.exists && signatureChanges then val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) - .showing(i"update info $sym: ${sym.info} --> $result", capt) + .showing(i"update info $sym: ${sym.info} = $result", capt) if newInfo ne sym.info then updateInfo(sym, if sym.isAnonymousFunction then diff --git a/tests/pos-custom-args/captures/vars1.scala b/tests/pos-custom-args/captures/vars1.scala index 56548e5a9c30..451b8988364f 100644 --- a/tests/pos-custom-args/captures/vars1.scala +++ b/tests/pos-custom-args/captures/vars1.scala @@ -8,11 +8,11 @@ object Test: var defaultIncompleteHandler: ErrorHandler = ??? @uncheckedCaptures var incompleteHandler: ErrorHandler = defaultIncompleteHandler - val x = incompleteHandler.unsafeUnbox + private val x = incompleteHandler.unsafeUnbox val _ : ErrorHandler = x val _ = x(1, "a") - def defaultIncompleteHandler1(): ErrorHandler = ??? + def defaultIncompleteHandler1(): (Int, String) => Unit = ??? val defaultIncompleteHandler2: ErrorHandler = ??? @uncheckedCaptures var incompleteHandler1: ErrorHandler = defaultIncompleteHandler1() @@ -25,6 +25,6 @@ object Test: incompleteHandler1 = defaultIncompleteHandler2 incompleteHandler1 = defaultIncompleteHandler2 - val saved = incompleteHandler1 + private val saved = incompleteHandler1 From 74c2db66111272c7ea744ccfe10c756b825a2feb Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 23 Aug 2023 18:25:04 +0200 Subject: [PATCH 27/76] Allow to turn off root mapping in inferred types --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f154c439127e..68a8cd530fe4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -345,7 +345,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => mapOver(t) if variance > 0 then t1 - else setup.decorate(t1, Function.const(CaptureSet.Fluid)) + else setup.decorate(t1, mapRoots = false, 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 59352bbd9d8b..a97c4a09b6df 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -82,7 +82,7 @@ extends tpd.TreeTraverser: * * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ - private def mapInferred(using Context) = new TypeMap: + private def mapInferred(mapRoots: Boolean)(using Context) = new TypeMap: /** Drop @retains annotations everywhere */ object cleanup extends TypeMap: @@ -165,12 +165,12 @@ extends tpd.TreeTraverser: resType = this(tp.resType)) case _ => mapOver(tp) - addVar(addCaptureRefinements(tp1), ctx.owner) + addVar(addCaptureRefinements(tp1), ctx.owner, mapRoots) end apply end mapInferred - private def transformInferredType(tp: Type, boxed: Boolean)(using Context): Type = - val tp1 = mapInferred(tp) + 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 */ @@ -241,7 +241,7 @@ extends tpd.TreeTraverser: if !tree.hasRememberedType then tree.rememberType( if tree.isInstanceOf[InferredTypeTree] && !exact - then transformInferredType(tree.tpe, boxed) + then transformInferredType(tree.tpe, boxed, mapRoots) else transformExplicitType(tree.tpe, boxed, mapRoots)) /** Substitute parameter symbols in `from` to paramRefs in corresponding @@ -302,15 +302,18 @@ extends tpd.TreeTraverser: case _ => traverseChildren(tree) case tree @ ValDef(_, tpt: TypeTree, rhs) => - rhs match + val mapRoots = rhs match case possiblyTypedClosureDef(ddef) => - // toplevel closures bound to vals count as level owners ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) + // toplevel closures bound to vals count as level owners + !tpt.isInstanceOf[InferredTypeTree] + // in this case roots in inferred val type count as polymorphic case _ => + true transformTT(tpt, boxed = tree.symbol.is(Mutable), // types of mutable variables are boxed exact = tree.symbol.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set - mapRoots = true + mapRoots ) capt.println(i"mapped $tree = ${tpt.knownType}") if allowUniversalInBoxed && tree.symbol.is(Mutable) @@ -346,7 +349,7 @@ extends tpd.TreeTraverser: 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 locaations are not boxed + mapRoots = !ctx.owner.levelOwner.isStaticOwner // other types in static locations are not boxed ) case tree: ValOrDefDef => val sym = tree.symbol @@ -424,7 +427,7 @@ extends tpd.TreeTraverser: recheckDef(tree, sym)) case tree: Bind => val sym = tree.symbol - updateInfo(sym, transformInferredType(sym.info, boxed = false)) + updateInfo(sym, transformInferredType(sym.info, boxed = false, mapRoots = true)) case tree: TypeDef => tree.symbol match case cls: ClassSymbol => @@ -506,7 +509,7 @@ extends tpd.TreeTraverser: /** Add a capture set variable to `tp` if necessary, or maybe pull out * an embedded capture set variable from a part of `tp`. */ - def decorate(tp: Type, addedSet: Type => CaptureSet)(using Context): Type = tp match + def decorate(tp: Type, mapRoots: Boolean, addedSet: Type => CaptureSet)(using Context): Type = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) case tp: RecType => @@ -532,7 +535,7 @@ extends tpd.TreeTraverser: case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) case tp: LazyRef => - decorate(tp.ref, addedSet) + decorate(tp.ref, mapRoots, addedSet) case _ if tp.typeSymbol == defn.FromJavaObjectSymbol => // For capture checking, we assume Object from Java is the same as Any tp @@ -542,15 +545,15 @@ extends tpd.TreeTraverser: else fallback val tp1 = tp.dealiasKeepAnnots if tp1 ne tp then - val tp2 = transformExplicitType(tp1, boxed = false, mapRoots = true) + val tp2 = transformExplicitType(tp1, boxed = false, mapRoots) maybeAdd(tp2, if tp2 ne tp1 then tp2 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)(using Context): Type = - decorate(tp, + def addVar(tp: Type, owner: Symbol, mapRoots: Boolean)(using Context): Type = + decorate(tp, mapRoots, addedSet = _.dealias.match case CapturingType(_, refs) => CaptureSet.Var(owner, refs.elems) case _ => CaptureSet.Var(owner)) From 5ad35d6a74afd3ce3498448eb9e529270878e3c0 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 23 Aug 2023 18:46:58 +0200 Subject: [PATCH 28/76] Fix handling of PolyFunction in when comparing refined types PolyFunction now is used also for erased and dependent functions. Need to correct for that in special purpose comparison code running at phase cc. --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index f62b852318e0..8f1e7c75c34b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -667,10 +667,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if defn.isFunctionType(tp2) then if tp2.derivesFrom(defn.PolyFunctionClass) then - tp1.member(nme.apply).info match - case info1: PolyType => - return isSubInfo(info1, tp2.refinedInfo) - case _ => + return isSubInfo(tp1.member(nme.apply).info, tp2.refinedInfo) else tp1w.widenDealias match case tp1: RefinedType => From d978b4c4c86889e52d8d200289ce2449496d4466 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 11:13:04 +0200 Subject: [PATCH 29/76] Don't map @capability classes that already carry a capture set A reference C to a @capability class C normally expands to C^. With this change we now do this only as long as C does not already carry a capture annotation. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 21 ++++++++++++++------ tests/pos-custom-args/captures/pairs.scala | 5 +++-- tests/pos-custom-args/captures/test.scala | 9 +++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 tests/pos-custom-args/captures/test.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a97c4a09b6df..37cd94c219c0 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -83,6 +83,7 @@ extends tpd.TreeTraverser: * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ private def mapInferred(mapRoots: Boolean)(using Context) = new TypeMap: + override def toString = "map inferred" /** Drop @retains annotations everywhere */ object cleanup extends TypeMap: @@ -205,14 +206,18 @@ extends tpd.TreeTraverser: else fntpe case _ => tp + def isCapabilityClassRef(tp: Type)(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 = tp match - case _: TypeRef | _: AppliedType if tp.typeSymbol.hasAnnotation(defn.CapabilityAnnot) => - CapturingType(tp, CaptureSet.universal, boxed = false) - case _ => - tp + private def expandCapabilityClass(tp: Type)(using Context): Type = + if isCapabilityClassRef(tp) + then CapturingType(tp, CaptureSet.universal, boxed = false) + else tp 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) @@ -220,9 +225,12 @@ extends tpd.TreeTraverser: if t2 ne t then return t2 t match case t @ AnnotatedType(t1, ann) => + val t2 = + if ann.symbol == defn.RetainsAnnot && isCapabilityClassRef(t1) then t1 + else this(t1) // Don't map capture sets, since that would implicitly normalize sets that // are not well-formed. - t.derivedAnnotatedType(this(t1), ann) + t.derivedAnnotatedType(t2, ann) case _ => mapOverFollowingAliases(t) @@ -268,6 +276,7 @@ extends tpd.TreeTraverser: mapOver(t) lazy val inverse = new BiTypeMap: + override def toString = "SubstParams.inverse" def apply(t: Type): Type = t match case t: ParamRef => def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index bc20d20ffd92..43488e2dde54 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -19,8 +19,9 @@ object Generic: object Monomorphic: class Pair(x: Cap => Unit, y: Cap => Unit): - def fst: Cap ->{x} Unit = x - def snd: Cap ->{y} Unit = y + 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 () diff --git a/tests/pos-custom-args/captures/test.scala b/tests/pos-custom-args/captures/test.scala new file mode 100644 index 000000000000..cf532bbdf34a --- /dev/null +++ b/tests/pos-custom-args/captures/test.scala @@ -0,0 +1,9 @@ +class C +type Cap = C^ + +class Foo(x: Cap): + this: Foo^{x} => + +def test(c: Cap) = + val x = Foo(c) + () From a4711fcba3d6d7c6275dded5406161d1e938d057 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 13:00:13 +0200 Subject: [PATCH 30/76] Don't drop annotations when following aliases in type maps --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7b09bda125be..1e38b3cc51b2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -59,7 +59,7 @@ trait FollowAliases extends TypeMap: val t1 = this(t.ref) if t1 ne t.ref then t1 else t case _ => - val t1 = t.dealias + val t1 = t.dealiasKeepAnnots if t1 ne t then val t2 = this(t1) if t2 ne t1 then return t2 From 53c1a0830bd5fb8c833da89a0af0797114cf4e80 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 13:38:59 +0200 Subject: [PATCH 31/76] Revise handling of self types - Transform eplicit self types like other types in Setup - Map capture roots at different levels to each other when checking self type conformance - Allow inferred self types referring to the root capability to the class (previously, `cap` was required) --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 23 ++++++++++++------- .../dotty/tools/dotc/typer/RefChecks.scala | 8 +++++-- .../pos-custom-args/captures/selftypes.scala | 9 ++++++++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 68a8cd530fe4..f406ad8f174e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1044,7 +1044,7 @@ class CheckCaptures extends Recheck, SymTransformer: } selfType match case CapturingType(_, refs: CaptureSet.Var) - if !refs.isUniversal && !matchesExplicitRefsInBaseClass(refs, root) => + if !refs.elems.exists(_.isRootCapability) && !matchesExplicitRefsInBaseClass(refs, root) => // Forbid inferred self types unless they are already implied by an explicit // self type in a parent. report.error( diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 37cd94c219c0..37a0242d0f83 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -441,22 +441,29 @@ extends tpd.TreeTraverser: tree.symbol match case cls: ClassSymbol => val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then - // add capture set to self type of nested classes if no self type is given explicitly - val selfRefs = CaptureSet.Var(cls) - // it's unclear what the right level owner should be. A self type should + 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. - val newInfo = ClassInfo(prefix, cls, ps, decls, - CapturingType(cinfo.selfType, selfRefs) - .showing(i"inferred self type for $cls: $result", capt)) + 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, selfRefs)) + updateInfo(modul, CapturingType(modul.info, newSelfType.captureSet)) modul.termRef.invalidateCaches() case _ => val info = atPhase(preRecheckPhase)(tree.symbol.info) diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 1b816dfc9307..061d759e9ca4 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -11,7 +11,7 @@ import util.Spans._ import scala.collection.mutable import ast._ import MegaPhase._ -import config.Printers.{checks, noPrinter} +import config.Printers.{checks, noPrinter, capt} import Decorators._ import OverridingPairs.isOverridingPair import typer.ErrorReporting._ @@ -20,6 +20,7 @@ import config.SourceVersion.{`3.0`, `future`} import config.Printers.refcheck import reporting._ import Constants.Constant +import cc.{mapRoots, localRoot} object RefChecks { import tpd._ @@ -103,7 +104,10 @@ object RefChecks { val cinfo = cls.classInfo def checkSelfConforms(other: ClassSymbol) = - val otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) + var otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) + if ctx.phase == Phases.checkCapturesPhase then + otherSelf = mapRoots(other.localRoot.termRef, cls.localRoot.termRef)(otherSelf) + .showing(i"map self $otherSelf = $result", capt) if otherSelf.exists then if !(cinfo.selfType <:< otherSelf) then report.error(DoesNotConformToSelfType("illegal inheritance", cinfo.selfType, cls, otherSelf, "parent", other), diff --git a/tests/pos-custom-args/captures/selftypes.scala b/tests/pos-custom-args/captures/selftypes.scala index c1b8eefce506..fff7445c419a 100644 --- a/tests/pos-custom-args/captures/selftypes.scala +++ b/tests/pos-custom-args/captures/selftypes.scala @@ -13,3 +13,12 @@ class D(@constructorOnly op: Int => Int) extends C: val x = 1//op(1) +// Demonstrates root mapping for self types +class IM: + this: IM^ => + + def coll: IM^{this} = ??? + foo(coll) + +def foo(im: IM^): Unit = ??? + From dc5cd1d5c135c079de64f5b375a4ddacb2f29cf9 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 15:17:55 +0200 Subject: [PATCH 32/76] Don't treat synthetic case class accessors as level owners --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 1e38b3cc51b2..465743bb67eb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -342,12 +342,15 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeUnbox def isLevelOwner(using Context): Boolean = + def isCaseClassSynthetic = + sym.owner.isClass && sym.owner.is(Case) && sym.is(Synthetic) && sym.info.firstParamNames.isEmpty if sym.isClass then true else if sym.is(Method) then if sym.isAnonymousFunction then // Setup added anonymous functions counting as level owners to nestingLevels ctx.property(ccState).get.nestingLevels.contains(sym) - else !sym.isConstructor + else + !sym.isConstructor && !isCaseClassSynthetic else false /** The owner of the current level. Qualifying owners are From 89c0429e440e8eb6656456046e7bbe1be7768cb6 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 15:18:54 +0200 Subject: [PATCH 33/76] Invalidate CaptureRef caches when updating their symbol's info --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 37a0242d0f83..a237288ee6be 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -292,6 +292,9 @@ extends tpd.TreeTraverser: /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = sym.updateInfoBetween(preRecheckPhase, thisPhase, info) + sym.namedType match + case ref: CaptureRef => ref.invalidateCaches() + case _ => def traverse(tree: Tree)(using Context): Unit = tree match From 3b01d6a5a6371e00f34fe6309601f4aa48d87b5f Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 17:54:48 +0200 Subject: [PATCH 34/76] Avoid early forcing of CaptureRef infos --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index a237288ee6be..237f46948847 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -342,7 +342,8 @@ extends tpd.TreeTraverser: transformTT(arg, boxed = true, exact = false, mapRoots = true) // type arguments in type applications are boxed if allowUniversalInBoxed then - val polyType = fn.tpe.widen.asInstanceOf[TypeLambda] + val polyType = atPhase(preRecheckPhase): + fn.tpe.widen.asInstanceOf[TypeLambda] for case (arg: TypeTree, pinfo, pname) <- args.lazyZip(polyType.paramInfos).lazyZip((polyType.paramNames)) do if pinfo.bounds.hi.hasAnnotation(defn.Caps_SealedAnnot) then def where = if fn.symbol.exists then i" in an argument of ${fn.symbol}" else "" From 38ba0696ed7c017eeb0a280558cbc593cb197275 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 18:02:53 +0200 Subject: [PATCH 35/76] Make CCState global We do have local roots that stay in the self types of classes after the compilation unit is compiled. We need to keep the state between compilation units in order to not duplicate those local roots in other units. --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f406ad8f174e..de345505d405 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -201,6 +201,8 @@ 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 @@ -993,7 +995,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkUnit(unit: CompilationUnit)(using Context): Unit = setup = Setup(preRecheckPhase, thisPhase, recheckDef) - inContext(ctx.withProperty(ccState, Some(new CCState))): + inContext(ctx.withProperty(ccState, Some(state))): setup(ctx.compilationUnit.tpdTree) //println(i"SETUP:\n${Recheck.addRecheckedTypes.transform(ctx.compilationUnit.tpdTree)}") withCaptureSetsExplained: From 5dd10672edc3d48e4390bca22348b2025507b999 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 18:40:31 +0200 Subject: [PATCH 36/76] Another fix for comparing with RefiningVar capture sets in refinements --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 5 ++++- .../src/dotty/tools/dotc/core/TypeComparer.scala | 15 +++++++++++++-- tests/pos-custom-args/captures/cc-this.scala | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 41eb3969f713..b90e6be59d78 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -597,8 +597,11 @@ object CaptureSet: /** A variable used in refinements of class parameters. See `addCaptureRefinements`. */ - class RefiningVar(owner: Symbol, getter: Symbol)(using @constructorOnly ctx: Context) extends Var(owner): + class RefiningVar(owner: Symbol, val getter: Symbol)(using @constructorOnly ctx: Context) extends Var(owner): description = i"of parameter ${getter.name} of ${getter.owner}" + override def optionalInfo(using Context): String = + super.optionalInfo + ( + if ctx.settings.YprintDebug.value then "(refining)" else "") /** A variable that is derived from some other variable via a map or filter. */ abstract class DerivedVar(owner: Symbol, initialElems: Refs)(using @constructorOnly ctx: Context) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8f1e7c75c34b..b2f135ebedbb 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} +import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam, isAlwaysPure, mapRoots, localRoot} import NameKinds.WildcardParamName /** Provides methods to compare types. @@ -2085,7 +2085,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def qualifies(m: SingleDenotation): Boolean = val info2 = tp2.refinedInfo val isExpr2 = info2.isInstanceOf[ExprType] - val info1 = m.info match + var info1 = m.info match case info1: ValueType if isExpr2 || m.symbol.is(Mutable) => // OK: { val x: T } <: { def x: T } // OK: { var x: T } <: { def x: T } @@ -2095,9 +2095,20 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // OK{ { def x(): T } <: { def x: T} // if x is Java defined ExprType(info1.resType) case info1 => info1 + + if ctx.phase == Phases.checkCapturesPhase then + // When comparing against a RefiningVar refinement, map the + // localRoot of the corresponding class in `tp1` to the owner of the + // refining capture set. + tp2.refinedInfo match + case rinfo2 @ CapturingType(_, refs: CaptureSet.RefiningVar) => + info1 = mapRoots(refs.getter.owner.localRoot.termRef, refs.owner.localRoot.termRef)(info1) + case _ => + isSubInfo(info1, info2, m.symbol.info.orElse(info1)) || matchAbstractTypeMember(m.info) || (tp1.isStable && m.symbol.isStableMember && isSubType(TermRef(tp1, m.symbol), tp2.refinedInfo)) + end qualifies tp1.member(name).hasAltWithInline(qualifies) } diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala index 2124ee494041..12c62e99d186 100644 --- a/tests/pos-custom-args/captures/cc-this.scala +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -14,4 +14,5 @@ def test(using Cap) = def c1 = new C(f) def c2 = c1 def c3 = c2.y + val c4: C^ = c3 val _ = c3: C^ From 9c498ef625d9c0db3f507ac95f7d4a371b3ddc9b Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 24 Aug 2023 20:54:58 +0200 Subject: [PATCH 37/76] Recognize user-defined local roots in healTypeParams --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 17 ++++++++++------- compiler/src/dotty/tools/dotc/core/Types.scala | 6 ++++-- .../neg-custom-args/captures/usingLogFile.check | 12 ++++++------ 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index b90e6be59d78..7391bad55434 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -597,7 +597,7 @@ object CaptureSet: /** A variable used in refinements of class parameters. See `addCaptureRefinements`. */ - class RefiningVar(owner: Symbol, val getter: Symbol)(using @constructorOnly ctx: Context) extends Var(owner): + class RefiningVar(owner: Symbol, val getter: Symbol)(using @constructorOnly ictx: Context) extends Var(owner): description = i"of parameter ${getter.name} of ${getter.owner}" override def optionalInfo(using Context): String = super.optionalInfo + ( diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index de345505d405..c0dba406d19f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1090,14 +1090,17 @@ class CheckCaptures extends Recheck, SymTransformer: ctx ?=> var seen = new util.HashSet[CaptureRef] def recur(elems: List[CaptureRef]): Unit = - for ref <- elems do - if !isAllowed(ref) && !seen.contains(ref) then + for case ref: TermParamRef <- elems do + if !allowed.contains(ref) && !seen.contains(ref) then seen += ref - 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) + if ref.underlying.isRef(defn.Caps_Root) 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 traverse(tp: Type) = diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index b69a0a317c63..2e68790547a1 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2927,10 +2927,12 @@ object Types { name == nme.CAPTURE_ROOT && symbol == defn.captureRoot override def localRootOwner(using Context): Symbol = - if name == nme.LOCAL_CAPTURE_ROOT - then + if name == nme.LOCAL_CAPTURE_ROOT then if symbol.owner.isLocalDummy then symbol.owner.owner else symbol.owner + else if info.isRef(defn.Caps_Root) then + val owner = symbol.maybeOwner + if owner.isTerm then owner else NoSymbol else NoSymbol override def normalizedRef(using Context): CaptureRef = diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index ff4c9fd3105f..5499e58d4354 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -24,13 +24,13 @@ | the part () => Unit of that type captures the root capability `cap`. | This is often caused by a local capability in an argument of method usingLogFile | leaking as part of its result. --- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:6 ------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:14 ------------------------------------------------------ 47 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Non-local value later cannot have an inferred type - | () => Unit - | with non-empty capture set {x$0, cap}. - | The type needs to be declared explicitly. + | ^^^^^^^^^^^^ + | Sealed type variable T cannot be instantiated to box () => Unit since + | that type captures the root capability `cap`. + | This is often caused by a local capability in an argument of method usingLogFile + | leaking as part of its result. -- 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 0910304cbf458251b1e67294436ba1ecaa2a7bcf Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 25 Aug 2023 14:07:26 +0200 Subject: [PATCH 38/76] Interpolate root vars Interpolate roots vars, instantiating them to their upper bound if that is the current owner. --- compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala | 3 +++ compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 82e409240cd4..6ff0c50ce28c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -28,6 +28,9 @@ object CaptureRoot: override def isTrackableRef(using Context): Boolean = true override def captureSetOfInfo(using Context) = CaptureSet.universal + def setAlias(target: CaptureRoot) = + alias = target + def followAlias: CaptureRoot = alias match case alias: Var if alias ne this => alias.followAlias case _ => this diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c0dba406d19f..0dcc77ff943b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -231,6 +231,16 @@ class CheckCaptures extends Recheck, SymTransformer: 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) + case _ => + case _ => traverse(parent) case t @ defn.RefinedFunctionOf(rinfo) => traverse(rinfo) From acacb136551388b55e46b675cef6dd3264ca68df Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 25 Aug 2023 18:43:28 +0200 Subject: [PATCH 39/76] Detect local root capability in curried methods --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 465743bb67eb..58f725164d9a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -392,7 +392,7 @@ extension (sym: Symbol) */ def definedLocalRoot(using Context): Symbol = sym.paramSymss.dropWhile(psyms => psyms.nonEmpty && psyms.head.isType) match - case psyms :: Nil => psyms.find(_.info.typeSymbol == defn.Caps_Root).getOrElse(NoSymbol) + case psyms :: _ => psyms.find(_.info.typeSymbol == defn.Caps_Root).getOrElse(NoSymbol) case _ => NoSymbol def localRoot(using Context): Symbol = From 21348b1c3e334d423bd4091fb3ab5656ab00e7b2 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 25 Aug 2023 18:45:00 +0200 Subject: [PATCH 40/76] Refactor error reporting when checking references --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 24 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7391bad55434..f23607e9c4de 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -495,10 +495,10 @@ object CaptureSet: val res = optional: (SimpleIdentitySet[CaptureRef]() /: elems): (acc, elem) => if levelOK(elem) then acc + elem - else if elem.isRootCapability then break() 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 0dcc77ff943b..98ab10801ee3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,7 +18,7 @@ import transform.SymUtils.* import transform.{Recheck, PreRecheck} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} import StdNames.nme import NameKinds.DefaultGetterName import reporting.trace @@ -262,21 +262,25 @@ class CheckCaptures extends Recheck, SymTransformer: def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + def checkOK(res: CompareResult, prefix: => String, pos: SrcPos)(using Context): Unit = + if !res.isOK then + def toAdd: String = CaptureSet.levelErrors.toAdd.mkString + report.error(em"$prefix included in the allowed capture set ${res.blocking}$toAdd", pos) + /** Check subcapturing `{elem} <: cs`, report error on failure */ def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos)(using Context) = - val res = elem.singletonCaptureSet.subCaptures(cs, frozen = false) - if !res.isOK then - report.error(em"$elem cannot be referenced here; it is not included in the allowed capture set ${res.blocking}", pos) + checkOK( + elem.singletonCaptureSet.subCaptures(cs, frozen = false), + i"$elem cannot be referenced here; it is not", + pos) /** Check subcapturing `cs1 <: cs2`, report error on failure */ def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos)(using Context) = - val res = cs1.subCaptures(cs2, frozen = false) - if !res.isOK then - def header = + checkOK( + cs1.subCaptures(cs2, frozen = false), if cs1.elems.size == 1 then i"reference ${cs1.elems.toList}%, % is not" - else i"references $cs1 are not all" - def toAdd: String = CaptureSet.levelErrors.toAdd.mkString - report.error(em"$header included in allowed capture set ${res.blocking}$toAdd", pos) + else i"references $cs1 are not all", + pos) /** The current environment */ private var curEnv: Env = inContext(ictx): From 05ba90652e591e1bfb1daa2b16af74df7c07e642 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 25 Aug 2023 18:46:39 +0200 Subject: [PATCH 41/76] Constrain closure parameters and results early This makes error messages more localized and specific. --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 98ab10801ee3..ff729ee2a0cd 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -586,6 +586,7 @@ class CheckCaptures extends Recheck, SymTransformer: // Constrain closure's parameters and result from the expected type before // rechecking the body. val res = recheckClosure(expr, pt, forceDependent = true) + checkConformsExpr(res, pt, expr) recheckDef(mdef, mdef.symbol) //println(i"RECHECK CLOSURE ${mdef.symbol.info}") res From 5fd589e3e5b662c25df4c3469ed603390de64c72 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 25 Aug 2023 18:47:07 +0200 Subject: [PATCH 42/76] Don't add capture set variables to Caps_Root --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 237f46948847..0f7eb38f0234 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -488,7 +488,8 @@ extends tpd.TreeTraverser: sym == defn.AnyClass // we assume Any is a shorthand of {cap} Any, so if Any is an upper // bound, the type is taken to be impure. - else superTypeIsImpure(tp.superType) + else + sym != defn.Caps_Root && superTypeIsImpure(tp.superType) case tp: (RefinedOrRecType | MatchType) => superTypeIsImpure(tp.underlying) case tp: AndType => From c64de22c5e62cfa9c6080ba00d6a7215134607b3 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 25 Aug 2023 18:47:57 +0200 Subject: [PATCH 43/76] Improve printing of local roots. --- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 1eeaa8456d06..482183482393 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -150,7 +150,7 @@ class PlainPrinter(_ctx: Context) extends Printer { + defn.FromJavaObjectSymbol def toTextCaptureSet(cs: CaptureSet): Text = - if printDebug && !cs.isConst then cs.toString + if printDebug && ctx.settings.YccDebug.value && !cs.isConst then cs.toString else if cs == CaptureSet.Fluid then "" else val core: Text = @@ -361,7 +361,8 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextRef(tp: SingletonType): Text = controlled { tp match { case tp: TermRef => - if tp.isLocalRootCapability then Str(s"") + if tp.symbol.name == nme.LOCAL_CAPTURE_ROOT then + Str(s"cap[${tp.localRootOwner.name}]@${tp.symbol.ccNestingLevel}") else toTextPrefixOf(tp) ~ selectionString(tp) case tp: ThisType => nameString(tp.cls) + ".this" From 684d5cdeac666252114c666b977d999ec9badd3f Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Aug 2023 12:04:32 +0200 Subject: [PATCH 44/76] Streamline CCState handling --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 37 ++++++++++--------- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 8 ++-- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 58f725164d9a..77c4d5be3eb7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -36,22 +36,26 @@ def allowUniversalInBoxed(using Context) = /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type) extends Exception -/** Capture checking state, consisting of - * - nestingLevels: A map associating certain symbols (the nesting level owners) - 8 with their ccNestingLevel - * - localRoots: A map associating nesting level owners with the local roots valid - * in their scopes. - * - levelError: Optionally, the last pair of capture reference and capture set where - * the reference could not be added to the set due to a level conflict. - * The capture checking state is stored in a context property. - */ +/** Capture checking state, which is stored in a context property */ class CCState: + + /** 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 + + /** 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 +end CCState /** Property key for capture checking state */ -val ccState: Key[CCState] = Key() +val ccStateKey: Key[CCState] = Key() + +/** The currently valid CCState */ +def ccState(using Context) = ctx.property(ccStateKey).get trait FollowAliases extends TypeMap: def mapOverFollowingAliases(t: Type): Type = t match @@ -348,7 +352,7 @@ extension (sym: Symbol) else if sym.is(Method) then if sym.isAnonymousFunction then // Setup added anonymous functions counting as level owners to nestingLevels - ctx.property(ccState).get.nestingLevels.contains(sym) + ccState.nestingLevels.contains(sym) else !sym.isConstructor && !isCaseClassSynthetic else false @@ -371,8 +375,7 @@ extension (sym: Symbol) def ccNestingLevel(using Context): Int = if sym.exists then val lowner = sym.levelOwner - val cache = ctx.property(ccState).get.nestingLevels - cache.getOrElseUpdate(lowner, + ccState.nestingLevels.getOrElseUpdate(lowner, if lowner.isRoot then 0 else lowner.owner.ccNestingLevel + 1) else -1 @@ -380,12 +383,10 @@ extension (sym: Symbol) * a capture checker is running. */ def ccNestingLevelOpt(using Context): Option[Int] = - if ctx.property(ccState).isDefined then - Some(ccNestingLevel) - else None + if ctx.property(ccStateKey).isDefined then Some(ccNestingLevel) else None def setNestingLevel(level: Int)(using Context): Unit = - ctx.property(ccState).get.nestingLevels(sym) = level + ccState.nestingLevels(sym) = level /** The parameter with type caps.Root in the leading term parameter section, * or NoSymbol, if none exists. @@ -403,7 +404,7 @@ extension (sym: Symbol) def lclRoot = if owner.isTerm then owner.definedLocalRoot.orElse(newRoot) else newRoot - ctx.property(ccState).get.localRoots.getOrElseUpdate(owner, lclRoot) + ccState.localRoots.getOrElseUpdate(owner, lclRoot) def maxNested(other: Symbol)(using Context): Symbol = if sym.ccNestingLevel < other.ccNestingLevel then other else sym diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index f23607e9c4de..c0c735580291 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -484,7 +484,7 @@ object CaptureSet: private def recordLevelError()(using Context): Unit = for elem <- triedElem do - ctx.property(ccState).get.levelError = Some((elem, this)) + ccState.levelError = Some((elem, this)) private def levelOK(elem: CaptureRef)(using Context): Boolean = elem match case elem: (TermRef | ThisType) => elem.ccNestingLevel <= ownLevel @@ -1024,7 +1024,7 @@ object CaptureSet: def levelErrors: Addenda = new Addenda: override def toAdd(using Context) = for - state <- ctx.property(ccState).toList + state <- ctx.property(ccStateKey).toList (ref, cs) <- state.levelError yield val levelStr = ref match diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ff729ee2a0cd..addc1923e03c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -46,11 +46,11 @@ object CheckCaptures: enum EnvKind: case Regular // normal case - case NestedInOwner // environment is a temporary one nested in the owner's environment, + case NestedInOwner // environment is a temporary one nested in the owner's environment, // and does not have a different actual owner symbol // (this happens when doing box adaptation). case ClosureResult // environment is for the result of a closure - case Boxed // envrionment is inside a box (in which case references are not counted) + case Boxed // environment is inside a box (in which case references are not counted) /** A class describing environments. * @param owner the current owner @@ -211,7 +211,7 @@ class CheckCaptures extends Recheck, SymTransformer: if Synthetics.needsTransform(sym) then Synthetics.transform(sym, toCC = false) else super.transformSym(sym) - override def printingContext(ctx: Context) = ctx.withProperty(ccState, Some(new CCState)) + override def printingContext(ctx: Context) = ctx.withProperty(ccStateKey, Some(new CCState)) class CaptureChecker(ictx: Context) extends Rechecker(ictx): import ast.tpd.* @@ -1010,7 +1010,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkUnit(unit: CompilationUnit)(using Context): Unit = setup = Setup(preRecheckPhase, thisPhase, recheckDef) - inContext(ctx.withProperty(ccState, Some(state))): + inContext(ctx.withProperty(ccStateKey, Some(state))): setup(ctx.compilationUnit.tpdTree) //println(i"SETUP:\n${Recheck.addRecheckedTypes.transform(ctx.compilationUnit.tpdTree)}") withCaptureSetsExplained: From be26204de64ac3c84b4deb15f947d76cdb39bee6 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Aug 2023 12:08:52 +0200 Subject: [PATCH 45/76] Improve error reporting With the earlier constraints on closure types, we get more localized errors in closures, but these sometimes need additional context to be intelligible. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 57 +++++++++++++------ .../tools/dotc/printing/PlainPrinter.scala | 38 +++++++++---- .../captures/box-adapt-boxing.scala | 4 +- tests/neg-custom-args/captures/byname.check | 8 +-- tests/neg-custom-args/captures/capt1.check | 30 ++++------ tests/neg-custom-args/captures/cc-this.check | 4 +- tests/neg-custom-args/captures/cc-this2.check | 2 +- tests/neg-custom-args/captures/cc-this5.check | 1 + tests/neg-custom-args/captures/eta.check | 10 ++-- .../captures/exception-definitions.check | 4 +- tests/neg-custom-args/captures/i15772.check | 10 ++++ tests/neg-custom-args/captures/i15772.scala | 4 +- tests/neg-custom-args/captures/i16114.scala | 16 +++--- .../neg-custom-args/captures/lazylists2.check | 2 +- tests/neg-custom-args/captures/try.check | 19 +++---- tests/neg-custom-args/captures/try.scala | 4 +- tests/pos-custom-args/captures/colltest.scala | 35 ++++++++++++ 17 files changed, 162 insertions(+), 86 deletions(-) create mode 100644 tests/pos-custom-args/captures/colltest.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index addc1923e03c..1715bfc03969 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -262,30 +262,36 @@ class CheckCaptures extends Recheck, SymTransformer: def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") - def checkOK(res: CompareResult, prefix: => String, pos: SrcPos)(using Context): Unit = + def checkOK(res: CompareResult, prefix: => String, pos: SrcPos, provenance: => String = "")(using Context): Unit = if !res.isOK then def toAdd: String = CaptureSet.levelErrors.toAdd.mkString - report.error(em"$prefix included in the allowed capture set ${res.blocking}$toAdd", pos) + def descr: String = + val d = res.blocking.description + if d.isEmpty then provenance else "" + report.error(em"$prefix included in the allowed capture set ${res.blocking}$descr$toAdd", pos) /** Check subcapturing `{elem} <: cs`, report error on failure */ - def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos)(using Context) = + def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( elem.singletonCaptureSet.subCaptures(cs, frozen = false), i"$elem cannot be referenced here; it is not", - pos) + pos, provenance) /** Check subcapturing `cs1 <: cs2`, report error on failure */ - def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos)(using Context) = + def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos, provenance: => String = "")(using Context) = checkOK( cs1.subCaptures(cs2, frozen = false), - if cs1.elems.size == 1 then i"reference ${cs1.elems.toList}%, % is not" + if cs1.elems.size == 1 then i"reference ${cs1.elems.toList.head} is not" else i"references $cs1 are not all", - pos) + pos, provenance) /** The current environment */ private var curEnv: Env = inContext(ictx): Env(defn.RootClass, EnvKind.Regular, CaptureSet.empty, null) + /** Currently checked closures and their expected types, used for error reporting */ + private var openClosures: List[(Symbol, Type)] = Nil + private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap() /** If `sym` is a class or method nested inside a term, a capture set variable representing @@ -312,6 +318,19 @@ class CheckCaptures extends Recheck, SymTransformer: recur(nextEnv, skip = env.kind == EnvKind.ClosureResult) recur(curEnv, skip = false) + /** A description where this environment comes from */ + private def provenance(env: Env)(using Context): String = + val owner = env.owner + if owner.isAnonymousFunction then + val expected = openClosures + .find(_._1 == owner) + .map(_._2) + .getOrElse(owner.info.toFunctionType(isJava = false)) + i"\nof an enclosing function literal with expected type $expected" + else + i"\nof the enclosing ${owner.showLocated}" + + /** Include `sym` in the capture sets of all enclosing environments nested in the * the environment in which `sym` is defined. */ @@ -321,7 +340,7 @@ class CheckCaptures extends Recheck, SymTransformer: if ref.isTracked then forallOuterEnvsUpTo(sym.enclosure): env => capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") - checkElem(ref, env.captured, pos) + checkElem(ref, env.captured, pos, provenance(env)) /** Make sure (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside @@ -338,7 +357,7 @@ class CheckCaptures extends Recheck, SymTransformer: case ref: ThisType => isVisibleFromEnv(ref.cls) case _ => false capt.println(i"Include call capture $included in ${env.owner}") - checkSubset(included, env.captured, pos) + checkSubset(included, env.captured, pos, provenance(env)) /** Include references captured by the called method in the current environment stack */ def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = @@ -585,11 +604,15 @@ class CheckCaptures extends Recheck, SymTransformer: // Constrain closure's parameters and result from the expected type before // rechecking the body. - val res = recheckClosure(expr, pt, forceDependent = true) - checkConformsExpr(res, pt, expr) - recheckDef(mdef, mdef.symbol) - //println(i"RECHECK CLOSURE ${mdef.symbol.info}") - res + openClosures = (mdef.symbol, pt) :: openClosures + try + val res = recheckClosure(expr, pt, forceDependent = true) + checkConformsExpr(res, pt, expr) + recheckDef(mdef, mdef.symbol) + //println(i"RECHECK CLOSURE ${mdef.symbol.info}") + res + finally + openClosures = openClosures.tail end recheckClosureBlock override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = @@ -608,7 +631,8 @@ class CheckCaptures extends Recheck, SymTransformer: if !Synthetics.isExcluded(sym) then val saved = curEnv val localSet = capturedVars(sym) - if !localSet.isAlwaysEmpty then curEnv = Env(sym, EnvKind.Regular, localSet, curEnv) + if !localSet.isAlwaysEmpty then + curEnv = Env(sym, EnvKind.Regular, localSet, curEnv) try super.recheckDefDef(tree, sym) finally interpolateVarsIn(tree.tpt) @@ -625,7 +649,8 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv val localSet = capturedVars(cls) for parent <- impl.parents do // (1) - checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos) + 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) try val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 482183482393..00525f83888e 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} +import cc.{CapturingType, EventuallyCapturingType, CaptureSet, CaptureRoot, isBoxed, ccNestingLevel, levelOwner} class PlainPrinter(_ctx: Context) extends Printer { @@ -173,8 +173,11 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: TypeType => toTextRHS(tp) case tp: TermRef - if !tp.denotationIsCurrent && !homogenizedView || // always print underlying when testing picklers - tp.symbol.is(Module) || tp.symbol.name == nme.IMPORT => + if !tp.denotationIsCurrent + && !homogenizedView // always print underlying when testing picklers + && !tp.isRootCapability + || tp.symbol.is(Module) + || tp.symbol.name == nme.IMPORT => toTextRef(tp) ~ ".type" case tp: TermRef if tp.denot.isOverloaded => "" @@ -224,12 +227,21 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close case tp @ EventuallyCapturingType(parent, refs) => val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value - val refsText = - if refs.isUniversal && - (refs.elems.size == 1 - || !ctx.settings.YccDebug.value && !refs.elems.exists(_.isLocalRootCapability)) - then rootSetText - else toTextCaptureSet(refs) + val rootsInRefs = refs.elems.filter(_.isRootCapability).toList + val showAsCap = rootsInRefs match + case (tp: TermRef) :: Nil => + if tp.symbol == defn.captureRoot then + refs.elems.size == 1 || !printDebug + // {caps.cap} gets printed as `{cap}` even under printDebug as long as there + // are no other elements in the set + else + tp.symbol.name == nme.LOCAL_CAPTURE_ROOT + && ctx.owner.levelOwner == tp.localRootOwner + && !printDebug + // local roots get printed as themselves under printDebug + case _ => + false + val refsText = if showAsCap then rootSetText else toTextCaptureSet(refs) toTextCapturing(parent, refsText, boxText) 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 @@ -361,8 +373,12 @@ class PlainPrinter(_ctx: Context) extends Printer { def toTextRef(tp: SingletonType): Text = controlled { tp match { case tp: TermRef => - if tp.symbol.name == nme.LOCAL_CAPTURE_ROOT then - Str(s"cap[${tp.localRootOwner.name}]@${tp.symbol.ccNestingLevel}") + if tp.symbol.name == nme.LOCAL_CAPTURE_ROOT then // TODO: Move to toTextCaptureRef + if ctx.owner.levelOwner == tp.localRootOwner && !printDebug then + Str("cap") + else + Str(s"cap[${tp.localRootOwner.name}]") ~ + Str(s"%${tp.symbol.ccNestingLevel}").provided(showNestingLevel) else toTextPrefixOf(tp) ~ selectionString(tp) case tp: ThisType => nameString(tp.cls) + ".this" diff --git a/tests/neg-custom-args/captures/box-adapt-boxing.scala b/tests/neg-custom-args/captures/box-adapt-boxing.scala index ea133051a21a..4b631472bad4 100644 --- a/tests/neg-custom-args/captures/box-adapt-boxing.scala +++ b/tests/neg-custom-args/captures/box-adapt-boxing.scala @@ -1,11 +1,11 @@ trait Cap def main(io: Cap^, fs: Cap^): Unit = { - val test1: Unit -> Unit = _ => { // error + val test1: Unit -> Unit = _ => { type Op = [T] -> (T ->{io} Unit) -> Unit val f: (Cap^{io}) -> Unit = ??? val op: Op = ??? - op[Cap^{io}](f) + op[Cap^{io}](f) // error // expected type of f: {io} (box {io} Cap) -> Unit // actual type: ({io} Cap) -> Unit // adapting f to the expected type will also diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index b1d8fb3b5404..61b83fc24688 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -5,10 +5,8 @@ | Required: (x$0: Int) ->{cap2} Int | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:19:5 ---------------------------------------- +-- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- 19 | h(g()) // error | ^^^ - | Found: () ?->{cap2} I - | Required: () ?->{cap1} I - | - | longer explanation available when compiling with `-explain` + | reference (cap2 : Cap^) 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 6b4c50b69ae4..124bd61a0109 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,17 +1,13 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:4:2 ------------------------------------------ +-- Error: tests/neg-custom-args/captures/capt1.scala:4:11 -------------------------------------------------------------- 4 | () => if x == null then y else y // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: () ->{x} C^? - | Required: () -> C - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:7:2 ------------------------------------------ + | ^ + | (x : C^) 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 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: () ->{x} C^? - | Required: Matchable - | - | longer explanation available when compiling with `-explain` + | ^ + | (x : C^) 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 | ^ @@ -37,10 +33,8 @@ 27 | def m() = if x == null then y else y | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:32:24 ---------------------------------------- +-- Error: tests/neg-custom-args/captures/capt1.scala:32:30 ------------------------------------------------------------- 32 | val z2 = h[() -> Cap](() => x) // error - | ^^^^^^^ - | Found: () ->{x} box C^ - | Required: () -> box C^ - | - | longer explanation available when compiling with `-explain` + | ^ + | (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^ diff --git a/tests/neg-custom-args/captures/cc-this.check b/tests/neg-custom-args/captures/cc-this.check index 47207f913f1d..335302c5c259 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 allowed capture set {} of the self type of class C2 + | reference (C2.this.x : () => 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 allowed capture set {} of pure base class class C3 + | reference (C4.this.f : () => 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 ec09a56c3b85..58ee62678680 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 (caps.cap : caps.Root) is not included in allowed capture set {} of pure base class class C + |reference (caps.cap : caps.Root) 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-this5.check b/tests/neg-custom-args/captures/cc-this5.check index e26597f61f37..8affe7005e2e 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -2,6 +2,7 @@ 16 | def f = println(c) // error | ^ | (c : Cap^) cannot be referenced here; it is not included in the allowed capture set {} + | of the enclosing class A -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- 21 | val x: A = this // error | ^^^^ diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check index a77d66382095..91dfdf06d3cd 100644 --- a/tests/neg-custom-args/captures/eta.check +++ b/tests/neg-custom-args/captures/eta.check @@ -5,10 +5,8 @@ | Required: () -> Proc^{f} | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/eta.scala:6:14 ------------------------------------------- +-- Error: tests/neg-custom-args/captures/eta.scala:6:20 ---------------------------------------------------------------- 6 | bar( () => f ) // error - | ^^^^^^^ - | Found: () ->{f} box () ->{f} Unit - | Required: () -> box () ->? Unit - | - | longer explanation available when compiling with `-explain` + | ^ + | (f : Proc^) 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 47ab6e063137..f3bba812ff47 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 (caps.cap : caps.Root) is not included in allowed capture set {} of pure base class class Throwable + |reference (caps.cap : caps.Root) 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 allowed capture set {} of pure base class class Throwable + | reference (Err3.this.c : Any^) 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 04cbe14f40a3..bba37a99b569 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -1,3 +1,8 @@ +-- 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 -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:20:46 --------------------------------------- 20 | val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error | ^^^^^^^ @@ -5,6 +10,11 @@ | Required: (C^ => 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 -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:27:35 --------------------------------------- 27 | val boxed2 : Observe[C^] = box2(c) // error | ^^^^^^^ diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index e4efb6b9ccab..a054eac835c1 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -16,14 +16,14 @@ class C(val arg: C^) { def main1(x: C^) : () -> Int = () => - val c : C^{x} = new C(x) + val c : C^{x} = new C(x) // error val boxed1 : ((C^) => Unit) -> Unit = box1(c) // error boxed1((cap: C^) => unsafe(c)) 0 def main2(x: C^) : () -> Int = () => - val c : C^{x} = new C(x) + val c : C^{x} = new C(x) // error val boxed2 : Observe[C^] = box2(c) // error boxed2((cap: C^) => unsafe(c)) 0 diff --git a/tests/neg-custom-args/captures/i16114.scala b/tests/neg-custom-args/captures/i16114.scala index d22c7f02d5fb..3bb276d1fc5c 100644 --- a/tests/neg-custom-args/captures/i16114.scala +++ b/tests/neg-custom-args/captures/i16114.scala @@ -12,16 +12,16 @@ def withCap[T](op: Cap^ => T): T = { def main(fs: Cap^): Unit = { def badOp(io: Cap^{cap}): Unit ->{} Unit = { - val op1: Unit ->{io} Unit = (x: Unit) => // error // limitation + val op1: Unit ->{io} Unit = (x: Unit) => expect[Cap^] { io.use() - fs + fs // error (limitation) } - val op2: Unit ->{fs} Unit = (x: Unit) => // error // limitation + val op2: Unit ->{fs} Unit = (x: Unit) => expect[Cap^] { fs.use() - io + io // error (limitation) } val op3: Unit ->{io} Unit = (x: Unit) => // ok @@ -30,13 +30,13 @@ def main(fs: Cap^): Unit = { io } - val op4: Unit ->{} Unit = (x: Unit) => // ok + val op4: Unit ->{} Unit = (x: Unit) => // o k expect[Cap^](io) - val op: Unit -> Unit = (x: Unit) => // error + val op: Unit -> Unit = (x: Unit) => expect[Cap^] { - io.use() - io + io.use() // error + io // error } op } diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 72efbc08f8e2..18852c69ea1e 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -45,5 +45,5 @@ -- Error: tests/neg-custom-args/captures/lazylists2.scala:60:10 -------------------------------------------------------- 60 | class Mapped2 extends Mapped: // error | ^ - | references {f, xs} are not all included in allowed capture set {} of the self type of class Mapped2 + | references {f, xs} are not all included in the allowed capture set {} of the self type of class Mapped2 61 | this: Mapped => diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index c9b7910ad534..b6016aee4c0b 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -5,15 +5,11 @@ | that type captures the root capability `cap`. | This is often caused by a local capability in an argument of method handle | leaking as part of its result. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:29:43 ------------------------------------------ -29 | val b = handle[Exception, () -> Nothing] { // error - | ^ - | Found: (x: CT[Exception]^) ->? () ->{x} Nothing - | Required: (x$0: CanThrow[Exception]) => () -> Nothing -30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) -31 | } { - | - | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- +30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error + | ^ + | (x : CanThrow[Exception]) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type () ->? Nothing -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 ------------------------------------------- 47 |val global: () -> Int = handle { 48 | (x: CanThrow[Exception]) => @@ -22,8 +18,11 @@ 51 | 22 52 |} { // error | ^ - | Found: () ->{x$0} Int + | Found: () ->{x$0, x$0²} Int | Required: () -> Int + | + | where: x$0 is a reference to a value parameter + | x$0² is a reference to a value parameter 53 | (ex: Exception) => () => 22 54 |} | diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 55e065de9f9f..7c0d579b782f 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -26,8 +26,8 @@ def test = (ex: Exception) => ??? } - val b = handle[Exception, () -> Nothing] { // error - (x: CanThrow[Exception]) => () => raise(new Exception)(using x) + val b = handle[Exception, () -> Nothing] { + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error } { (ex: Exception) => ??? } diff --git a/tests/pos-custom-args/captures/colltest.scala b/tests/pos-custom-args/captures/colltest.scala new file mode 100644 index 000000000000..af4ae6f03c56 --- /dev/null +++ b/tests/pos-custom-args/captures/colltest.scala @@ -0,0 +1,35 @@ +// Showing a problem with recursive references +object CollectionStrawMan5 { + + /** Base trait for generic collections */ + trait Iterable[+A] extends IterableLike[A] { + this: Iterable[A]^ => + def iterator: Iterator[A]^{this} + def coll: Iterable[A]^{this} = this + } + + trait IterableLike[+A]: + this: IterableLike[A]^ => + def coll: Iterable[A]^{this} + def partition(p: A => Boolean): Unit = + val pn = Partition(coll, p) + () + + /** Concrete collection type: View */ + trait View[+A] extends Iterable[A] with IterableLike[A] { + this: View[A]^ => + } + + case class Partition[A](val underlying: Iterable[A]^, p: A => Boolean) { + self: Partition[A]^{underlying, p} => + + class Partitioned(expected: Boolean) extends View[A]: + this: Partitioned^{self} => + def iterator: Iterator[A]^{this} = + underlying.iterator.filter((x: A) => p(x) == expected) + + val left: Partitioned^{self} = Partitioned(true) + val right: Partitioned^{self} = Partitioned(false) + } + +} \ No newline at end of file From f2aa33c3643655fd9a48a1e448d7d79e6e52ea79 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Aug 2023 19:18:07 +0200 Subject: [PATCH 46/76] Take names of synthesized contextual functions from expected type If expected type has parameter names, use them for the generated contextual closure. --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 13 ++++++++----- compiler/src/dotty/tools/dotc/typer/Typer.scala | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index c104c603422d..6024eab29722 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -205,12 +205,12 @@ object desugar { def makeImplicitParameters( tpts: List[Tree], implicitFlag: FlagSet, - mkParamName: () => TermName, + mkParamName: Int => TermName, forPrimaryConstructor: Boolean = false )(using Context): List[ValDef] = for (tpt, i) <- tpts.zipWithIndex yield { val paramFlags: FlagSet = if (forPrimaryConstructor) LocalParamAccessor else Param - val epname = mkParamName() + val epname = mkParamName(i) ValDef(epname, tpt, EmptyTree).withFlags(paramFlags | implicitFlag) } @@ -254,7 +254,7 @@ object desugar { // using clauses, we only need names that are unique among the // parameters of the method since shadowing does not affect // implicit resolution in Scala 3. - mkParamName = () => + mkParamName = i => val index = seenContextBounds + 1 // Start at 1 like FreshNameCreator. val ret = ContextBoundParamName(EmptyTermName, index) seenContextBounds += 1 @@ -1602,9 +1602,12 @@ object desugar { case vd: ValDef => vd } - def makeContextualFunction(formals: List[Tree], body: Tree, erasedParams: List[Boolean])(using Context): Function = { + def makeContextualFunction(formals: List[Tree], paramNamesOrNil: List[TermName], body: Tree, erasedParams: List[Boolean])(using Context): Function = { val mods = Given - val params = makeImplicitParameters(formals, mods, mkParamName = () => ContextFunctionParamName.fresh()) + val params = makeImplicitParameters(formals, mods, + mkParamName = i => + if paramNamesOrNil.isEmpty then ContextFunctionParamName.fresh() + else paramNamesOrNil(i)) FunctionWithMods(params, body, Modifiers(mods), erasedParams) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c896631dfab3..6ae7e96c40aa 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3204,6 +3204,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer protected def makeContextualFunction(tree: untpd.Tree, pt: Type)(using Context): Tree = { val defn.FunctionOf(formals, _, true) = pt.dropDependentRefinement: @unchecked + val paramNamesOrNil = pt match + case RefinedType(_, _, rinfo: MethodType) => rinfo.paramNames + case _ => Nil // The getter of default parameters may reach here. // Given the code below @@ -3236,7 +3239,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => paramTypes.map(_ => false) } - val ifun = desugar.makeContextualFunction(paramTypes, tree, erasedParams) + val ifun = desugar.makeContextualFunction(paramTypes, paramNamesOrNil, tree, erasedParams) typr.println(i"make contextual function $tree / $pt ---> $ifun") typedFunctionValue(ifun, pt) } From 9884855c7739a09d64fddfdf15df93d01f7fe851 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Aug 2023 15:34:02 +0200 Subject: [PATCH 47/76] Enable local roots --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 45 ++++++++++--------- tests/neg-custom-args/captures/cc-this2.check | 2 +- tests/neg-custom-args/captures/cc-this5.check | 4 +- tests/neg-custom-args/captures/eta.check | 2 +- .../captures/exception-definitions.check | 4 +- tests/neg-custom-args/captures/lazylist.check | 6 +-- .../neg-custom-args/captures/lazylists2.check | 4 +- tests/neg-custom-args/captures/levels.check | 8 ++++ tests/neg-custom-args/captures/levels.scala | 33 ++++++++++++++ 10 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 tests/neg-custom-args/captures/levels.check create mode 100644 tests/neg-custom-args/captures/levels.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 77c4d5be3eb7..90827f17ba47 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -17,7 +17,7 @@ import collection.mutable private val Captures: Key[CaptureSet] = Key() private val BoxedType: Key[BoxedTypeCache] = Key() -private val enableRootMapping = false +private val enableRootMapping = 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 diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 1715bfc03969..d06f0ed550a3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1075,28 +1075,29 @@ class CheckCaptures extends Recheck, SymTransformer: } assert(roots.nonEmpty) for case root: ClassSymbol <- roots do - checkSelfAgainstParents(root, root.baseClasses) - val selfType = root.asClass.classInfo.selfType - interpolator(startingVariance = -1).traverse(selfType) - if !root.isEffectivelySealed then - def matchesExplicitRefsInBaseClass(refs: CaptureSet, cls: ClassSymbol): Boolean = - cls.baseClasses.tail.exists { psym => - val selfType = psym.asClass.givenSelfType - selfType.exists && selfType.captureSet.elems == refs.elems - } - selfType match - case CapturingType(_, refs: CaptureSet.Var) - if !refs.elems.exists(_.isRootCapability) && !matchesExplicitRefsInBaseClass(refs, root) => - // Forbid inferred self types unless they are already implied by an explicit - // self type in a parent. - report.error( - em"""$root needs an explicitly declared self type since its - |inferred self type $selfType - |is not visible in other compilation units that define subclasses.""", - root.srcPos) - case _ => - parentTrees -= root - capt.println(i"checked $root with $selfType") + inContext(ctx.withOwner(root)): + checkSelfAgainstParents(root, root.baseClasses) + val selfType = root.asClass.classInfo.selfType + interpolator(startingVariance = -1).traverse(selfType) + if !root.isEffectivelySealed then + def matchesExplicitRefsInBaseClass(refs: CaptureSet, cls: ClassSymbol): Boolean = + cls.baseClasses.tail.exists { psym => + val selfType = psym.asClass.givenSelfType + selfType.exists && selfType.captureSet.elems == refs.elems + } + selfType match + case CapturingType(_, refs: CaptureSet.Var) + if !refs.elems.exists(_.isRootCapability) && !matchesExplicitRefsInBaseClass(refs, root) => + // Forbid inferred self types unless they are already implied by an explicit + // self type in a parent. + report.error( + em"""$root needs an explicitly declared self type since its + |inferred self type $selfType + |is not visible in other compilation units that define subclasses.""", + root.srcPos) + case _ => + parentTrees -= root + capt.println(i"checked $root with $selfType") end checkSelfTypes /** Heal ill-formed capture sets in the type parameter. diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check index 58ee62678680..1aae25b60efb 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 (caps.cap : caps.Root) is not included in the allowed capture set {} of pure base class class C + |reference (cap : caps.Root) 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-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 8affe7005e2e..522279339bef 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -1,8 +1,8 @@ -- Error: tests/neg-custom-args/captures/cc-this5.scala:16:20 ---------------------------------------------------------- 16 | def f = println(c) // error | ^ - | (c : Cap^) cannot be referenced here; it is not included in the allowed capture set {} - | of the enclosing class A + | (c : Cap^{cap[test]}) cannot be referenced here; it is not included in the allowed capture set {} + | of the enclosing class A -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- 21 | val x: A = this // error | ^^^^ diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check index 91dfdf06d3cd..382ee177aa91 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 : Proc^) cannot be referenced here; it is not included in the allowed capture set {} + | (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 diff --git a/tests/neg-custom-args/captures/exception-definitions.check b/tests/neg-custom-args/captures/exception-definitions.check index f3bba812ff47..835572f28d4c 100644 --- a/tests/neg-custom-args/captures/exception-definitions.check +++ b/tests/neg-custom-args/captures/exception-definitions.check @@ -1,12 +1,12 @@ -- Error: tests/neg-custom-args/captures/exception-definitions.scala:2:6 ----------------------------------------------- 2 |class Err extends Exception: // error |^ - |reference (caps.cap : caps.Root) is not included in the allowed capture set {} of pure base class class Throwable + |reference (cap : caps.Root) 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 | ^ - |(c : Any^) cannot be referenced here; it is not included in the allowed capture set {} of pure base class class Throwable + |(c : Any^{cap[test]}) cannot be referenced here; it is not included in the allowed capture set {} of pure base class class Throwable -- Error: tests/neg-custom-args/captures/exception-definitions.scala:8:8 ----------------------------------------------- 8 | class Err3(c: Any^) extends Exception // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 4b7611fc3fb7..aeb410f07d65 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: (ref1 : lazylists.LazyCons[Int]{val xs: () ->{cap1} lazylists.LazyList[Int]^{cap[LazyCons]}}^{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 ------------------------------------- @@ -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/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 18852c69ea1e..5038ab1bea93 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -25,11 +25,11 @@ -- Error: tests/neg-custom-args/captures/lazylists2.scala:40:20 -------------------------------------------------------- 40 | def head: B = f(xs.head) // error | ^ - |(f : A => B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped + |(f : A ->{cap[map3]} B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped -- Error: tests/neg-custom-args/captures/lazylists2.scala:41:48 -------------------------------------------------------- 41 | def tail: LazyList[B]^{this}= xs.tail.map(f) // error | ^ - |(f : A => B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped + |(f : A ->{cap[map3]} B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:45:4 ------------------------------------ 45 | final class Mapped extends LazyList[B]: // error | ^ diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check new file mode 100644 index 000000000000..36cf8e13035c --- /dev/null +++ b/tests/neg-custom-args/captures/levels.check @@ -0,0 +1,8 @@ +-- Error: tests/neg-custom-args/captures/levels.scala:15:11 ------------------------------------------------------------ +15 | r.setV(g) // error + | ^ + | reference (cap3 : CC^) is not included in the allowed capture set ? + | of an enclosing function literal with expected type box (x$0: String) ->? String + | + | Note that reference (cap3 : CC^), defined at level 2 + | cannot be included in outer capture set ?, defined at level 1 in method test diff --git a/tests/neg-custom-args/captures/levels.scala b/tests/neg-custom-args/captures/levels.scala new file mode 100644 index 000000000000..35fb2d490398 --- /dev/null +++ b/tests/neg-custom-args/captures/levels.scala @@ -0,0 +1,33 @@ +class CC + +def test(cap1: CC^) = + + class Ref[T](init: T): + private var v: T = init + def setV(x: T): Unit = v = x + def getV: T = v + + val r = Ref((x: String) => x) + + def scope = + val cap3: CC^ = ??? + def g(x: String): String = if cap3 == cap3 then "" else "a" + r.setV(g) // error + () + +/* + Explicit: + cap is local root of enclosing method or class, can be overridden by qualifying it. + i.e. cap[name] + + On method instantiation: All uses of cap --> cap of caller + On class instantiation: All uses of cap, or local cap of clsss --> cap of caller + + Alternative solution: root variables + - track minimal & maximal level + - updated via subsumption tests, root added handler for prefix/member + + roots: Implicitly: outer <: inner + + def withFile[T]((local: Root) ?=> op: File^{local}) => T]): T +*/ From 7d9a288e155961bd857055a55662141a6fc6ade9 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Aug 2023 19:25:50 +0200 Subject: [PATCH 48/76] Delay checking against expected type for eta-expanded closures --- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index d06f0ed550a3..ef32ea64a045 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -606,8 +606,23 @@ class CheckCaptures extends Recheck, SymTransformer: // rechecking the body. openClosures = (mdef.symbol, pt) :: openClosures try + def isEtaExpansion(mdef: DefDef): Boolean = mdef.paramss match + case (param :: _) :: _ if param.asInstanceOf[Tree].span.isZeroExtent => + mdef.rhs match + case _: Apply => true + case closureDef(mdef1) => isEtaExpansion(mdef1) + case _ => false + case _ => false val res = recheckClosure(expr, pt, forceDependent = true) - checkConformsExpr(res, pt, expr) + if !isEtaExpansion(mdef) then + // If closure is an eta expanded method reference it's better to not constrain + // its internals early since that would give error messages in generated code + // which are less intelligible. + // Example is the line `a = x` in neg-custom-args/captures/vars.scala. + // For all other closures, early constraints are preferred since they + // give more localized error messages. + checkConformsExpr(res, pt, expr) + //else report.warning(i"skip test $mdef", mdef.srcPos) recheckDef(mdef, mdef.symbol) //println(i"RECHECK CLOSURE ${mdef.symbol.info}") res From 5e091781b23df70f337544879d0f9b4be95fd6ba Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Aug 2023 19:26:35 +0200 Subject: [PATCH 49/76] Revise printing of CaptureRoot.Vars --- .../src/dotty/tools/dotc/printing/PlainPrinter.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 00525f83888e..e4cae3ecc3fd 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -402,11 +402,11 @@ class PlainPrinter(_ctx: Context) extends Printer { if tp.followAlias ne tp then toTextRef(tp.followAlias) else def boundText(sym: Symbol): Text = - if sym.exists then toTextRef(sym.termRef) ~ s"/${sym.ccNestingLevel}" - else "" - "'cap[" ~ nameString(tp.source) - ~ "](" ~ boundText(tp.lowerBound) - ~ ".." ~ boundText(tp.upperBound) ~ ")" + (toTextRef(sym.termRef) + ~ Str(s"/${sym.ccNestingLevel}").provided(showNestingLevel) + ).provided(sym.exists) + "'cap[" ~ boundText(tp.lowerBound) ~ ".." ~ boundText(tp.upperBound) ~ "]" + ~ ("(from instantiating " ~ nameString(tp.source) ~ ")").provided(tp.source.exists) } } From b6132d924fe982b15698d502b6c4630f68937972 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Aug 2023 19:27:01 +0200 Subject: [PATCH 50/76] Make caps.cap a given for caps.Root --- library/src/scala/caps.scala | 2 + tests/neg-custom-args/captures/levels.check | 8 +-- tests/neg-custom-args/captures/vars.check | 60 ++++++++++++++------- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 9b540c504af6..4a6f3a89e7e1 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -13,6 +13,8 @@ import annotation.experimental /** The universal capture reference */ val cap: Root = () + given Root = cap + object unsafe: extension [T](x: T) diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index 36cf8e13035c..8d11c196b10f 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -1,8 +1,10 @@ --- Error: tests/neg-custom-args/captures/levels.scala:15:11 ------------------------------------------------------------ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/levels.scala:15:11 --------------------------------------- 15 | r.setV(g) // error | ^ - | reference (cap3 : CC^) is not included in the allowed capture set ? - | of an enclosing function literal with expected type box (x$0: String) ->? String + | Found: box (x: String) ->{cap3} String + | Required: box (x$0: String) ->? String | | Note that reference (cap3 : CC^), defined at level 2 | cannot be included in outer capture set ?, defined at level 1 in method test + | + | 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 a7e38dbfdb8a..7328264bb90f 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,15 +1,3 @@ --- Error: tests/neg-custom-args/captures/vars.scala:13:6 --------------------------------------------------------------- -13 | var a: String => String = f // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Mutable variable a cannot have type box String => String since - | that type captures the root capability `cap`. - | This restriction serves to prevent local capabilities from escaping the scope where they are defined. --- Error: tests/neg-custom-args/captures/vars.scala:14:6 --------------------------------------------------------------- -14 | var b: List[String => String] = Nil // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Mutable variable b cannot have type List[String => String] since - | the part String => String of that type captures the root capability `cap`. - | This restriction serves to prevent local capabilities from escaping the scope where they are defined. -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:11:24 ----------------------------------------- 11 | val z2c: () -> Unit = z2 // error | ^^ @@ -17,10 +5,46 @@ | Required: () -> Unit | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/vars.scala:32:2 --------------------------------------------------------------- -32 | local { cap3 => // error +-- 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]} + | 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 + | ^ + | Found: box (x: String) ->{cap3} String + | Required: box (x$0: String) ->{cap[test]} String + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:26:12 ----------------------------------------- +26 | b = List(g) // error + | ^^^^^^^ + | Found: List[box (x$0: String) ->{cap3} String] + | Required: List[String ->{cap[test]} String] + | + | 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.Root), defined at level 2 + | cannot be included in outer capture set ?, defined at level 1 in method test + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:31:29 ----------------------------------------- +31 | val sc: String => String = scope // error (but should also be OK) + | ^^^^^ + | Found: (x$0: String) ->{cap[scope]} String + | Required: String => String + | + | Note that reference (cap[scope] : caps.Root), 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 | ^^^^^ - | Sealed type variable T cannot be instantiated to box (x$0: String) => String since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method local - | leaking as part of its result. + | escaping local reference root.type From 6bf993c2f66f9d66dcdfb7f80b4ae9b90e6f1f08 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 27 Aug 2023 15:49:50 +0200 Subject: [PATCH 51/76] Refine handling of val-bound closures Don't treat them as level roots if they are implicit eta expansions that don't mention `cap` explicitly. --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 17 +++++++++++++++++ .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 7 ------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 17 +++++++++++++++-- .../captures/eta-expansions.scala | 9 +++++++++ 4 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 tests/pos-custom-args/captures/eta-expansions.scala diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 41767d81fb2b..e60d6e86754c 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -813,6 +813,23 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => case _ => tree } + /** An extractor for eta expanded `mdef` an eta-expansion of a method reference? To recognize this, we use + * the following criterion: A method definition is an eta expansion, if + * it contains at least one term paramter, the parameter has a zero extent span, + * and the right hand side is either an application or a closure with' + * an anonymous method that's itself characterized as an eta expansion. + */ + def isEtaExpansion(mdef: DefDef)(using Context): Boolean = + !rhsOfEtaExpansion(mdef).isEmpty + + def rhsOfEtaExpansion(mdef: DefDef)(using Context): Tree = mdef.paramss match + case (param :: _) :: _ if param.asInstanceOf[Tree].span.isZeroExtent => + mdef.rhs match + case rhs: Apply => rhs + case closureDef(mdef1) => rhsOfEtaExpansion(mdef1) + case _ => EmptyTree + case _ => EmptyTree + /** The variables defined by a pattern, in reverse order of their appearance. */ def patVars(tree: Tree)(using Context): List[Symbol] = { val acc = new TreeAccumulator[List[Symbol]] { outer => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ef32ea64a045..52c51ec968d4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -606,13 +606,6 @@ class CheckCaptures extends Recheck, SymTransformer: // rechecking the body. openClosures = (mdef.symbol, pt) :: openClosures try - def isEtaExpansion(mdef: DefDef): Boolean = mdef.paramss match - case (param :: _) :: _ if param.asInstanceOf[Tree].span.isZeroExtent => - mdef.rhs match - case _: Apply => true - case closureDef(mdef1) => isEtaExpansion(mdef1) - case _ => false - case _ => false val res = recheckClosure(expr, pt, forceDependent = true) if !isEtaExpansion(mdef) then // If closure is an eta expanded method reference it's better to not constrain diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 0f7eb38f0234..48d0b0299b64 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -314,10 +314,23 @@ extends tpd.TreeTraverser: case _ => traverseChildren(tree) case tree @ ValDef(_, tpt: TypeTree, rhs) => + 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 val mapRoots = rhs match - case possiblyTypedClosureDef(ddef) => + case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - // toplevel closures bound to vals count as level owners + // 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 _ => diff --git a/tests/pos-custom-args/captures/eta-expansions.scala b/tests/pos-custom-args/captures/eta-expansions.scala new file mode 100644 index 000000000000..1aac7ded1b50 --- /dev/null +++ b/tests/pos-custom-args/captures/eta-expansions.scala @@ -0,0 +1,9 @@ +@annotation.capability class Cap + +def test(d: Cap) = + def map2(xs: List[Int])(f: Int => Int): List[Int] = xs.map(f) + val f1 = map2 // capture polymorphic implicit eta expansion + def f2c: List[Int] => (Int => Int) => List[Int] = f1 + val a0 = identity[Cap ->{d} Unit] // capture monomorphic implicit eta expansion + val a0c: (Cap ->{d} Unit) ->{d} Cap ->{d} Unit = a0 + val b0 = (x: Cap ->{d} Unit) => identity[Cap ->{d} Unit](x) // not an implicit eta expansion, hence capture polymorphic From 1a0be5667d43d57cda2c7d9708d72d3eaa049b4d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 27 Aug 2023 15:50:10 +0200 Subject: [PATCH 52/76] Add shortenCap config option to Printer --- .../src/dotty/tools/dotc/printing/PlainPrinter.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index e4cae3ecc3fd..6a30329fd675 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -48,6 +48,11 @@ class PlainPrinter(_ctx: Context) extends Printer { protected def homogenizedView: Boolean = ctx.settings.YtestPickler.value 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. + */ + private val shortenCap = true + def homogenize(tp: Type): Type = if (homogenizedView) tp match { @@ -166,7 +171,7 @@ class PlainPrinter(_ctx: Context) extends Printer { boxText ~ toTextLocal(parent) ~ "^" ~ (refsText provided refsText != rootSetText) - final protected def rootSetText = Str("{cap}") + final protected def rootSetText = Str("{cap}") // TODO Use disambiguation def toText(tp: Type): Text = controlled { homogenize(tp) match { @@ -238,6 +243,7 @@ class PlainPrinter(_ctx: Context) extends Printer { tp.symbol.name == nme.LOCAL_CAPTURE_ROOT && ctx.owner.levelOwner == tp.localRootOwner && !printDebug + && shortenCap // !!! // local roots get printed as themselves under printDebug case _ => false @@ -374,7 +380,7 @@ 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 then + if ctx.owner.levelOwner == tp.localRootOwner && !printDebug && shortenCap then Str("cap") else Str(s"cap[${tp.localRootOwner.name}]") ~ From 50419ace1c99fb5e1e24f5247a0d476d2bd9c72c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 27 Aug 2023 15:51:03 +0200 Subject: [PATCH 53/76] Adapt tests to local roots --- .../captures/boundschecks2.scala | 2 +- tests/neg-custom-args/captures/ctest.scala | 6 ---- tests/neg-custom-args/captures/filevar.scala | 6 ++-- tests/neg-custom-args/captures/i15049.scala | 2 +- tests/neg-custom-args/captures/i15772.check | 32 ++++++++++++++----- tests/neg-custom-args/captures/i15772.scala | 2 +- tests/neg-custom-args/captures/i15923.scala | 6 ++-- .../captures/leaked-curried.check | 15 ++++++--- .../captures/leaked-curried.scala | 5 +-- tests/neg-custom-args/captures/refs.scala | 18 +++++------ .../captures/sealed-leaks.scala | 6 ++-- .../captures/simple-escapes.check | 11 +++++++ .../captures/simple-escapes.scala | 24 ++++++++++++++ .../captures/stack-alloc.scala | 2 +- .../captures/usingLogFile.scala | 10 +++--- tests/neg-custom-args/captures/vars.scala | 21 ++++++------ 16 files changed, 112 insertions(+), 56 deletions(-) delete mode 100644 tests/neg-custom-args/captures/ctest.scala create mode 100644 tests/neg-custom-args/captures/simple-escapes.check create mode 100644 tests/neg-custom-args/captures/simple-escapes.scala diff --git a/tests/neg-custom-args/captures/boundschecks2.scala b/tests/neg-custom-args/captures/boundschecks2.scala index 923758d722f9..159fc0691f42 100644 --- a/tests/neg-custom-args/captures/boundschecks2.scala +++ b/tests/neg-custom-args/captures/boundschecks2.scala @@ -8,6 +8,6 @@ object test { val foo: C[Tree^] = ??? // error type T = C[Tree^] // error - val bar: T -> T = ??? + val bar: T -> T = ??? // error val baz: C[Tree^] -> Unit = ??? // error } diff --git a/tests/neg-custom-args/captures/ctest.scala b/tests/neg-custom-args/captures/ctest.scala deleted file mode 100644 index ad10b43a7773..000000000000 --- a/tests/neg-custom-args/captures/ctest.scala +++ /dev/null @@ -1,6 +0,0 @@ -class CC -type Cap = CC^ - -def test(cap1: Cap, cap2: Cap) = - var b: List[String => String] = Nil // error - val bc = b.head // was error, now OK diff --git a/tests/neg-custom-args/captures/filevar.scala b/tests/neg-custom-args/captures/filevar.scala index 830563f51de3..53987eb3f623 100644 --- a/tests/neg-custom-args/captures/filevar.scala +++ b/tests/neg-custom-args/captures/filevar.scala @@ -5,14 +5,14 @@ class File: def write(x: String): Unit = ??? class Service: - var file: File^ = uninitialized // error + var file: File^ = uninitialized def log = file.write("log") -def withFile[T](op: (f: File^) => T): T = +def withFile[T](op: (l: caps.Root) ?-> (f: File^{l}) => T): T = op(new File) def test = withFile: f => val o = Service() - o.file = f + o.file = f // error o.log diff --git a/tests/neg-custom-args/captures/i15049.scala b/tests/neg-custom-args/captures/i15049.scala index d978e0e1ad0f..6b8441529196 100644 --- a/tests/neg-custom-args/captures/i15049.scala +++ b/tests/neg-custom-args/captures/i15049.scala @@ -2,7 +2,7 @@ class Session: def request = "Response" class Foo: private val session: Session^{cap} = new Session - def withSession[sealed T](f: (Session^{cap}) => T): T = f(session) + def withSession[T](f: (local: caps.Root) ?-> (Session^{local}) => T): T = f(session) def Test: Unit = val f = new Foo diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index bba37a99b569..fffd9ab62091 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^}^{c} => Unit) ->{c} Unit - | Required: (C^ => Unit) -> Unit + | Found: (C{val arg: C^{cap[C]}}^{c} ->{'cap[..main1](from instantiating box1), c} Unit) ->{c} Unit + | Required: (C^ => Unit) -> Unit | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/i15772.scala:26:26 ------------------------------------------------------------ @@ -18,15 +18,28 @@ -- [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^}^{c} => Unit) ->{c} Unit - | Required: Observe[C^] + | Found: (C{val arg: C^{cap[C]}}^{c} ->{'cap[..main2](from instantiating box2), c} Unit) ->{c} Unit + | Required: (C^ => Unit) -> Unit | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:33 --------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:34 --------------------------------------- 33 | val boxed2 : Observe[C]^ = box2(c) // error - | ^^^^^^^ - | Found: (C{val arg: C^}^ => Unit) ->? Unit - | Required: Observe[C]^ + | ^ + | Found: box C^{cap[c]} + | Required: box C{val arg: C^?}^? + | + | Note that reference (cap[c] : caps.Root), defined at level 2 + | cannot be included in outer capture set ?, defined at level 1 in method main3 + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:34:29 --------------------------------------- +34 | boxed2((cap: C^) => unsafe(c)) // error + | ^ + | Found: C^{cap[c]} + | Required: C^{'cap[..main3](from instantiating unsafe)} + | + | Note that reference (cap[c] : caps.Root), 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 ---------------------------------------- @@ -35,4 +48,7 @@ | Found: () ->{x} Unit | Required: () -> Unit | + | Note that reference (cap[c] : caps.Root), defined at level 2 + | cannot be included in outer capture set ?, defined at level 1 in method main3 + | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index a054eac835c1..7e62fdeffb24 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)) + boxed2((cap: C^) => unsafe(c)) // error 0 trait File: diff --git a/tests/neg-custom-args/captures/i15923.scala b/tests/neg-custom-args/captures/i15923.scala index 3994b34f5928..d06b42a97eaa 100644 --- a/tests/neg-custom-args/captures/i15923.scala +++ b/tests/neg-custom-args/captures/i15923.scala @@ -3,12 +3,12 @@ type Id[X] = [T] -> (op: X => T) -> T def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) def bar() = { - def withCap[sealed X](op: (Cap^) => X): X = { - val cap: Cap^ = new Cap { def use() = { println("cap is used"); 0 } } + def withCap[X](op: (lcap: caps.Root) ?-> Cap^{lcap} => X): X = { + val cap: Cap = new Cap { def use() = { println("cap is used"); 0 } } val result = op(cap) result } - val leak = withCap(cap => mkId(cap)) // error + val leak = withCap(cap => mkId(cap)) // error // error leak { cap => cap.use() } } \ No newline at end of file diff --git a/tests/neg-custom-args/captures/leaked-curried.check b/tests/neg-custom-args/captures/leaked-curried.check index 590f871c57d5..509069797def 100644 --- a/tests/neg-custom-args/captures/leaked-curried.check +++ b/tests/neg-custom-args/captures/leaked-curried.check @@ -1,4 +1,11 @@ --- Error: tests/neg-custom-args/captures/leaked-curried.scala:12:52 ---------------------------------------------------- -12 | val get: () ->{} () ->{io} Cap^ = () => () => io // error - | ^^ - |(io : Cap^) cannot be referenced here; it is not included in the allowed capture set {} of pure base class trait Pure +-- Error: tests/neg-custom-args/captures/leaked-curried.scala:13:20 ---------------------------------------------------- +13 | () => () => io // error + | ^^ + |(io : Cap^{cap[main]}) cannot be referenced here; it is not included in the allowed capture set {} of pure base class trait Pure +-- [E164] Declaration Error: tests/neg-custom-args/captures/leaked-curried.scala:12:10 --------------------------------- +12 | val get: () ->{} () ->{io} Cap^ = // error + | ^ + | error overriding value get in trait Box of type () -> () ->{cap[Box]} Cap^{cap[Box]}; + | value get of type () -> () ->{io} Cap^ has incompatible type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/leaked-curried.scala b/tests/neg-custom-args/captures/leaked-curried.scala index 000f2ef72cb0..a566999d7c39 100644 --- a/tests/neg-custom-args/captures/leaked-curried.scala +++ b/tests/neg-custom-args/captures/leaked-curried.scala @@ -1,7 +1,7 @@ trait Cap: def use(): Unit -def withCap[sealed T](op: (x: Cap^) => T): T = ??? +def withCap[T](op: (x: Cap^) => T): T = ??? trait Box: val get: () ->{} () ->{cap} Cap^ @@ -9,6 +9,7 @@ trait Box: def main(): Unit = val leaked = withCap: (io: Cap^) => class Foo extends Box, Pure: - val get: () ->{} () ->{io} Cap^ = () => () => io // error + val get: () ->{} () ->{io} Cap^ = // error + () => () => io // error new Foo val bad = leaked.get()().use() // using a leaked capability diff --git a/tests/neg-custom-args/captures/refs.scala b/tests/neg-custom-args/captures/refs.scala index df38027a5643..9ee9acdb1a0d 100644 --- a/tests/neg-custom-args/captures/refs.scala +++ b/tests/neg-custom-args/captures/refs.scala @@ -4,7 +4,7 @@ class Ref[T](init: T): var x: T = init def setX(x: T): Unit = this.x = x -def usingLogFile[sealed T](op: FileOutputStream^ => T): T = +def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream^{local} => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -12,27 +12,27 @@ def usingLogFile[sealed T](op: FileOutputStream^ => T): T = type Proc = () => Unit def test1 = - usingLogFile[Proc]: f => // error - () => - f.write(1) - () + usingLogFile[Proc]: (local: caps.Root) ?=> // error (but with a hard to parse error message) + (f: FileOutputStream^{local}) => + () => f.write(1) // this line has type () ->{local} Unit, but usingLogFile + // requires Proc, which expands to () -> 'cap[..test1](from instantiating usingLogFile) def test2 = val r = new Ref[Proc](() => ()) usingLogFile[Unit]: f => - r.setX(() => f.write(10)) // should be error + r.setX(() => f.write(10)) // error r.x() // crash: f is closed at that point def test3 = val r = new Ref[Proc](() => ()) usingLogFile[Unit]: f => - r.x = () => f.write(10) // should be error + r.x = () => f.write(10) // error r.x() // crash: f is closed at that point def test4 = - var r: Proc = () => () // error + var r: Proc = () => () usingLogFile[Unit]: f => - r = () => f.write(10) + r = () => f.write(10) // error r() // crash: f is closed at that point diff --git a/tests/neg-custom-args/captures/sealed-leaks.scala b/tests/neg-custom-args/captures/sealed-leaks.scala index bf46b52194c1..3436673227c0 100644 --- a/tests/neg-custom-args/captures/sealed-leaks.scala +++ b/tests/neg-custom-args/captures/sealed-leaks.scala @@ -2,7 +2,7 @@ import java.io.* def Test2 = - def usingLogFile[sealed T](op: FileOutputStream^ => T): T = + def usingLogFile[T](op: (l: caps.Root) ?-> FileOutputStream^{l} => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -11,10 +11,10 @@ def Test2 = val later = usingLogFile { f => () => f.write(0) } // error val later2 = usingLogFile[(() => Unit) | Null] { f => () => f.write(0) } // error - var x: (FileOutputStream^) | Null = null // error + var x: (FileOutputStream^) | Null = null def foo(f: FileOutputStream^, g: FileOutputStream^) = var y = if ??? then f else g // error - usingLogFile { f => x = f } + usingLogFile { f => x = f } // error later() \ No newline at end of file diff --git a/tests/neg-custom-args/captures/simple-escapes.check b/tests/neg-custom-args/captures/simple-escapes.check new file mode 100644 index 000000000000..611d4f0f0dc3 --- /dev/null +++ b/tests/neg-custom-args/captures/simple-escapes.check @@ -0,0 +1,11 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/simple-escapes.scala:16:10 ------------------------------- +16 | foo = f // error + | ^ + | Found: box FileOutputStream^{f} + | Required: box FileOutputStream^{cap[Test1]} + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/simple-escapes.scala:19:15 ---------------------------------------------------- +19 | val later2 = usingLogFile { local => f => // error + | ^^^^^^^^^^^^ + | escaping local reference local.type diff --git a/tests/neg-custom-args/captures/simple-escapes.scala b/tests/neg-custom-args/captures/simple-escapes.scala new file mode 100644 index 000000000000..770d02bb851a --- /dev/null +++ b/tests/neg-custom-args/captures/simple-escapes.scala @@ -0,0 +1,24 @@ +class FileOutputStream(str: String): + def close(): Unit = () + def write(x: Int): Unit = () + +def Test1 = + + def usingLogFile[T](op: (local: caps.Root) -> FileOutputStream^{local} => T): T = + val logFile = FileOutputStream("log") + val result = op(caps.cap)(logFile) + logFile.close() + result + + var foo: FileOutputStream^ = FileOutputStream("") + + val later1 = usingLogFile { local => f => + foo = f // error + () => () + } + val later2 = usingLogFile { local => f => // error + () => f.write(0) + } + later1() + later2() + diff --git a/tests/neg-custom-args/captures/stack-alloc.scala b/tests/neg-custom-args/captures/stack-alloc.scala index 71b544dbe88d..5fbc3a3c591d 100644 --- a/tests/neg-custom-args/captures/stack-alloc.scala +++ b/tests/neg-custom-args/captures/stack-alloc.scala @@ -5,7 +5,7 @@ class Pooled val stack = mutable.ArrayBuffer[Pooled]() var nextFree = 0 -def withFreshPooled[sealed T](op: Pooled^ => T): T = +def withFreshPooled[T](op: (lcap: caps.Root) ?-> Pooled^{lcap} => T): T = if nextFree >= stack.size then stack.append(new Pooled) val pooled = stack(nextFree) nextFree = nextFree + 1 diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index e7c23573ca6e..0aa9290a7f0b 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -3,7 +3,7 @@ import annotation.capability object Test1: - def usingLogFile[sealed T](op: FileOutputStream => T): T = + def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -14,7 +14,7 @@ object Test1: object Test2: - def usingLogFile[sealed T](op: FileOutputStream^ => T): T = + def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream^{local} => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -38,7 +38,7 @@ object Test2: object Test3: - def usingLogFile[sealed T](op: FileOutputStream^ => T) = + def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream^{local} => T) = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -50,7 +50,7 @@ object Test4: class Logger(f: OutputStream^): def log(msg: String): Unit = ??? - def usingFile[sealed T](name: String, op: OutputStream^ => T): T = + def usingFile[T](name: String, op: (local: caps.Root) ?-> OutputStream^{local} => T): T = val f = new FileOutputStream(name) val result = op(f) f.close() @@ -63,7 +63,7 @@ object Test4: later(1) - def usingLogger[sealed T](f: OutputStream^, op: Logger^{f} => T): T = + def usingLogger[T](f: OutputStream^, op: (local: caps.Root) ?-> Logger^{f} => T): T = val logger = Logger(f) op(logger) diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index b7761952167e..860babf68331 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -9,9 +9,9 @@ def test(cap1: Cap, cap2: Cap) = val zc: () ->{cap1} String = z val z2 = () => { x = identity } val z2c: () -> Unit = z2 // error + var a: String => String = f - var a: String => String = f // error - var b: List[String => String] = Nil // error + var b: List[String => String] = Nil val u = a // was error, now ok a("") // was error, now ok b.head // was error, now ok @@ -19,17 +19,20 @@ def test(cap1: Cap, cap2: Cap) = def scope = val cap3: Cap = CC() def g(x: String): String = if cap3 == cap3 then "" else "a" - a = g - b = List(g) + def h(): String = "" + a = x => g(x) // error + a = g // error + + b = List(g) // error val gc = g g - val s = scope - val sc: String => String = scope + 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) - def local[sealed T](op: Cap -> T): T = op(CC()) + def local[T](op: (local: caps.Root) -> CC^{local} -> T): T = op(caps.cap)(CC()) - local { cap3 => // error + local { root => cap3 => // error def g(x: String): String = if cap3 == cap3 then "" else "a" g } @@ -39,4 +42,4 @@ def test(cap1: Cap, cap2: Cap) = val r = Ref() r.elem = f - val fc = r.elem + val fc = r.elem \ No newline at end of file From 68044f0104f1795bcae68eab24c8d5061d440886 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 27 Aug 2023 19:29:51 +0200 Subject: [PATCH 54/76] Level-based capture checking for try/catch --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 5 ++ .../dotty/tools/dotc/cc/CheckCaptures.scala | 16 +++-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 40 +++++++++++- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/transform/Recheck.scala | 9 ++- .../neg-custom-args/captures/capt-test.scala | 8 +-- .../captures/lazylists-exceptions.check | 18 ++--- .../captures/lazylists-exceptions.scala | 4 +- tests/neg-custom-args/captures/real-try.check | 64 +++++++++--------- tests/neg-custom-args/captures/real-try.scala | 18 +++-- tests/neg-custom-args/captures/try.check | 28 ++++---- tests/neg-custom-args/captures/try.scala | 4 +- tests/neg-custom-args/captures/try3.scala | 9 +-- .../captures/usingLogFile-alt.check | 17 +++-- .../captures/usingLogFile-alt.scala | 4 +- .../captures/usingLogFile.check | 65 +++++++++---------- .../captures/usingLogFile.scala | 14 ++-- tests/neg/capt-wf.scala | 2 +- 18 files changed, 192 insertions(+), 134 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 90827f17ba47..f4d2685553ac 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -49,6 +49,11 @@ class CCState: * the reference could not be added to the set due to a level conflict. */ var levelError: Option[(CaptureRef, CaptureSet)] = None + + /** Under saferExceptions: The symbol generated for a try. + * Installed by Setup, removed by CheckCaptures. + */ + val tryBlockOwner: mutable.HashMap[Try, Symbol] = new mutable.HashMap end CCState /** Property key for capture checking state */ diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 52c51ec968d4..dba4ce3c3e03 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -163,14 +163,15 @@ object CheckCaptures: * 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)(using Context): Unit = + def warnIfRedundantCaptureSet(ann: Tree, tpt: Tree)(using Context): Unit = var retained = retainedElems(ann).toArray for i <- 0 until retained.length do val ref = retained(i).toCaptureRef val others = for j <- 0 until retained.length if j != i yield retained(j).toCaptureRef val remaining = CaptureSet(others*) if remaining.accountsFor(ref) then - report.warning(em"redundant capture: $remaining already accounts for $ref", ann.srcPos) + val srcTree = if ann.span.exists then ann else tpt + report.warning(em"redundant capture: $remaining already accounts for $ref", srcTree.srcPos) /** Report an error if some part of `tp` contains the root capability in its capture set */ def disallowRootCapabilitiesIn(tp: Type, what: String, have: String, addendum: String, pos: SrcPos)(using Context) = @@ -689,7 +690,14 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckTyped(tree) override def recheckTry(tree: Try, pt: Type)(using Context): Type = - val tp = super.recheckTry(tree, pt) + val tryOwner = ccState.tryBlockOwner.remove(tree).getOrElse(ctx.owner) + val saved = curEnv + curEnv = Env(tryOwner, EnvKind.Regular, CaptureSet.Var(curEnv.owner), curEnv) + val tp = try + inContext(ctx.withOwner(tryOwner)): + super.recheckTry(tree, pt) + finally + curEnv = saved if allowUniversalInBoxed && Feature.enabled(Feature.saferExceptions) then disallowRootCapabilitiesIn(tp, "Result of `try`", "have type", @@ -1192,7 +1200,7 @@ class CheckCaptures extends Recheck, SymTransformer: checkWellformedPost(tp, tree.srcPos) tp match case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => - warnIfRedundantCaptureSet(annot.tree) + warnIfRedundantCaptureSet(annot.tree, tree) case _ => } case t: ValOrDefDef diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 48d0b0299b64..b3287b5e0aaa 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -6,13 +6,14 @@ import core._ import Phases.*, DenotTransformers.*, SymDenotations.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* import Types.*, StdNames.* +import Annotations.Annotation +import config.Feature import config.Printers.capt import ast.tpd import transform.Recheck.* import CaptureSet.IdentityCaptRefMap import Synthetics.isExcluded import util.Property -import dotty.tools.dotc.core.Annotations.Annotation /** A tree traverser that prepares a compilation unit to be capture checked. * It does the following: @@ -289,13 +290,27 @@ extends tpd.TreeTraverser: def inverse = thisMap end SubstParams + /** If the outer context directly enclosing the definition of `sym` + * has a owner, that owner, otherwise null. + */ + def newOwnerFor(sym: Symbol)(using Context): Symbol | Null = + var octx = ctx + while octx.owner == sym do octx = octx.outer + if octx.owner.name == nme.TRY_BLOCK then octx.owner else null + /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = - sym.updateInfoBetween(preRecheckPhase, thisPhase, info) + sym.updateInfoBetween(preRecheckPhase, thisPhase, info, newOwnerFor(sym)) sym.namedType match case ref: CaptureRef => ref.invalidateCaches() 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) + def traverse(tree: Tree)(using Context): Unit = tree match case tree: DefDef => @@ -367,11 +382,24 @@ extends tpd.TreeTraverser: 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.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, @@ -451,6 +479,9 @@ extends tpd.TreeTraverser: // are checked on depand 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)) @@ -482,11 +513,14 @@ extends tpd.TreeTraverser: 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 - updateInfo(tree.symbol, newInfo) capt.println(i"update info of ${tree.symbol} from $info to $newInfo") case _ => end postProcess diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 8f99d4f5a240..95c7c2cb2cd9 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -299,6 +299,7 @@ object StdNames { val SELF: N = "$this" val SKOLEM: N = "" val TRAIT_CONSTRUCTOR: N = "$init$" + val TRY_BLOCK: N = "" val THROWS: N = "$throws" val U2EVT: N = "u2evt$" val ALLARGS: N = "$allArgs" diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 2247140e681e..87a7f32e30f7 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -49,10 +49,12 @@ object Recheck: extension (sym: Symbol) /** Update symbol's info to newInfo from prevPhase.next to lastPhase. - * Reset to previous info for phases after lastPhase. + * Also update owner to newOwnerOrNull if it is not null. + * Reset to previous info and owner for phases after lastPhase. */ - def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: DenotTransformer, newInfo: Type)(using Context): Unit = - if sym.info ne newInfo then + def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: 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( initFlags = @@ -61,6 +63,7 @@ object Recheck: else flags ).installAfter(lastPhase) // reset sym.copySymDenotation( + owner = newOwner, info = newInfo, initFlags = if newInfo.isInstanceOf[LazyType] then flags &~ Touched diff --git a/tests/neg-custom-args/captures/capt-test.scala b/tests/neg-custom-args/captures/capt-test.scala index f14951f410c4..c6f612fdc31b 100644 --- a/tests/neg-custom-args/captures/capt-test.scala +++ b/tests/neg-custom-args/captures/capt-test.scala @@ -14,14 +14,14 @@ def raise[E <: Exception](e: E): Nothing throws E = throw e def foo(x: Boolean): Int throws Fail = if x then 1 else raise(Fail()) -def handle[E <: Exception, sealed R <: Top](op: (CanThrow[E]) => R)(handler: E => R): R = - val x: CanThrow[E] = ??? +def handle[E <: Exception, R <: Top](op: (lcap: caps.Root) ?-> (CT[E] @retains(lcap)) => R)(handler: E => R): R = + val x: CT[E] = ??? try op(x) catch case ex: E => handler(ex) def test: Unit = - val b = handle[Exception, () => Nothing] { // error - (x: CanThrow[Exception]) => () => raise(new Exception)(using x) + val b = handle[Exception, () => Nothing] { + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error } { (ex: Exception) => ??? } diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index f58ed265d3be..3bf1cfd6a816 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,11 +1,13 @@ --- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- -36 | try // error - | ^ - | Result of `try` cannot have type LazyList[Int]^ since - | that type captures the root capability `cap`. - | This is often caused by a locally generated exception capability leaking as part of its result. -37 | tabulate(10) { i => +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:37:4 -------------------------- +37 | tabulate(10) { i => // error + | ^ + | Found: LazyList[Int]^ + | Required: LazyList[Int]^? + | + | Note that reference (cap : caps.Root), 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 | } -41 | catch case ex: Ex1 => LazyNil + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.scala b/tests/neg-custom-args/captures/lazylists-exceptions.scala index 6a72facf7285..f70f66cd6950 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.scala +++ b/tests/neg-custom-args/captures/lazylists-exceptions.scala @@ -33,8 +33,8 @@ def tabulate[A](n: Int)(gen: Int => A): LazyList[A]^{gen} = class Ex1 extends Exception def problem = - try // error - tabulate(10) { i => + try + tabulate(10) { i => // error if i > 9 then throw Ex1() i * i } diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index c8df3777bcfa..3ee0d7ac66cf 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -1,36 +1,36 @@ --- [E129] Potential Issue Warning: tests/neg-custom-args/captures/real-try.scala:30:4 ---------------------------------- -30 | b.x +-- [E129] Potential Issue Warning: tests/neg-custom-args/captures/real-try.scala:36:4 ---------------------------------- +36 | b.x | ^^^ | A pure expression does nothing in statement position; you may be omitting necessary parentheses | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/real-try.scala:12:2 ----------------------------------------------------------- -12 | try // error - | ^ - | Result of `try` cannot have type () => Unit since - | that type captures the root capability `cap`. - | This is often caused by a locally generated exception capability leaking as part of its result. -13 | () => foo(1) -14 | catch -15 | case _: Ex1 => ??? -16 | case _: Ex2 => ??? --- Error: tests/neg-custom-args/captures/real-try.scala:18:2 ----------------------------------------------------------- -18 | try // error - | ^ - | Result of `try` cannot have type () => Cell[Unit]^? since - | that type captures the root capability `cap`. - | This is often caused by a locally generated exception capability leaking as part of its result. -19 | () => Cell(foo(1)) -20 | catch -21 | case _: Ex1 => ??? -22 | case _: Ex2 => ??? --- Error: tests/neg-custom-args/captures/real-try.scala:24:10 ---------------------------------------------------------- -24 | val b = try // error - | ^ - | Result of `try` cannot have type Cell[box () => Unit]^? since - | the part box () => Unit of that type captures the root capability `cap`. - | This is often caused by a locally generated exception capability leaking as part of its result. -25 | Cell(() => foo(1))//: Cell[box {ev} () => Unit] <: Cell[box {cap} () => Unit] -26 | catch -27 | case _: Ex1 => ??? -28 | case _: Ex2 => ??? +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/real-try.scala:19:4 -------------------------------------- +19 | () => foo(1) // error + | ^^^^^^^^^^^^ + | Found: () => Unit + | Required: () ->? Unit + | + | Note that reference (cap : caps.Root), 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]^? + | Required: () ->? Cell[Unit]^? + | + | Note that reference (cap : caps.Root), 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]^? + | Required: Cell[() ->? Unit]^? + | + | Note that reference (cap : caps.Root), 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.scala b/tests/neg-custom-args/captures/real-try.scala index a826fdaa4af7..8020f98f0f10 100644 --- a/tests/neg-custom-args/captures/real-try.scala +++ b/tests/neg-custom-args/captures/real-try.scala @@ -9,20 +9,26 @@ def foo(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit = class Cell[+T](val x: T) def test(): Unit = - try // error - () => foo(1) + try + () => foo(1) // no error, since result type is Unit catch case _: Ex1 => ??? case _: Ex2 => ??? - try // error - () => Cell(foo(1)) + val x = try + () => foo(1) // error catch case _: Ex1 => ??? case _: Ex2 => ??? - val b = try // error - Cell(() => foo(1))//: Cell[box {ev} () => Unit] <: Cell[box {cap} () => Unit] + val y = try + () => Cell(foo(1)) // error + catch + case _: Ex1 => ??? + case _: Ex2 => ??? + + val b = try + Cell(() => foo(1))// // error catch case _: Ex1 => ??? case _: Ex2 => ??? diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index b6016aee4c0b..e33de70adbec 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,15 +1,17 @@ --- Error: tests/neg-custom-args/captures/try.scala:23:16 --------------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:23:49 ------------------------------------------ 23 | val a = handle[Exception, CanThrow[Exception]] { // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Sealed type variable R cannot be instantiated to box CT[Exception]^ since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method handle - | leaking as part of its result. + | ^ + |Found: (lcap: caps.Root) ?->? (x$0: CT[Exception]^{lcap}) ->? box CT[Exception]^{lcap} + |Required: (lcap: caps.Root) ?-> CT[Exception]^{lcap} ->{'cap[..test](from instantiating handle)} box CT[Exception]^ +24 | (x: CanThrow[Exception]) => x +25 | }{ + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- 30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error | ^ - | (x : CanThrow[Exception]) cannot be referenced here; it is not included in the allowed capture set {} - | of an enclosing function literal with expected type () ->? Nothing + | (x : CT[Exception]^{lcap}) cannot be referenced here; it is not included in the allowed capture set {} + | of an enclosing function literal with expected type () ->? Nothing -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:52:2 ------------------------------------------- 47 |val global: () -> Int = handle { 48 | (x: CanThrow[Exception]) => @@ -18,11 +20,8 @@ 51 | 22 52 |} { // error | ^ - | Found: () ->{x$0, x$0²} Int + | Found: () ->{x$0, lcap} Int | Required: () -> Int - | - | where: x$0 is a reference to a value parameter - | x$0² is a reference to a value parameter 53 | (ex: Exception) => () => 22 54 |} | @@ -30,7 +29,4 @@ -- Error: tests/neg-custom-args/captures/try.scala:35:11 --------------------------------------------------------------- 35 | val xx = handle { // error | ^^^^^^ - | Sealed type variable R cannot be instantiated to box () => Int since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method handle - | leaking as part of its result. + | escaping local reference lcap.type diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 7c0d579b782f..686a38d4f203 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -14,8 +14,8 @@ def raise[E <: Exception](e: E): Nothing throws E = throw e def foo(x: Boolean): Int throws Fail = if x then 1 else raise(Fail()) -def handle[E <: Exception, sealed R <: Top](op: CanThrow[E] => R)(handler: E => R): R = - val x: CanThrow[E] = ??? +def handle[E <: Exception, R <: Top](op: (lcap: caps.Root) ?-> CT[E]^{lcap} => R)(handler: E => R): R = + val x: CT[E] = ??? try op(x) catch case ex: E => handler(ex) diff --git a/tests/neg-custom-args/captures/try3.scala b/tests/neg-custom-args/captures/try3.scala index 4c6835353c3f..0d30c95de4e3 100644 --- a/tests/neg-custom-args/captures/try3.scala +++ b/tests/neg-custom-args/captures/try3.scala @@ -4,9 +4,9 @@ class CT[E] type CanThrow[E] = CT[E]^ type Top = Any^ -def handle[E <: Exception, sealed T <: Top](op: CanThrow[E] ?=> T)(handler: E => T): T = - val x: CanThrow[E] = ??? - try op(using x) +def handle[E <: Exception, T <: Top](op: (lcap: caps.Root) ?-> CT[E]^{lcap} ?=> T)(handler: E => T): T = + val x: CT[E] = ??? + try op(using caps.cap)(using x) catch case ex: E => handler(ex) def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = @@ -14,7 +14,8 @@ def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = @main def Test: Int = def f(a: Boolean) = - handle { // error + handle { // error: implementation restriction: curried dependent CFT not supported + // should work but give capture error if !a then raise(IOException()) (b: Boolean) => if !b then raise(IOException()) diff --git a/tests/neg-custom-args/captures/usingLogFile-alt.check b/tests/neg-custom-args/captures/usingLogFile-alt.check index 9444bc9dc46a..34b61cb28bf3 100644 --- a/tests/neg-custom-args/captures/usingLogFile-alt.check +++ b/tests/neg-custom-args/captures/usingLogFile-alt.check @@ -1,7 +1,10 @@ --- Error: tests/neg-custom-args/captures/usingLogFile-alt.scala:18:2 --------------------------------------------------- -18 | usingFile( // error - | ^^^^^^^^^ - | Sealed type variable T cannot be instantiated to box () => Unit since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method usingFile - | leaking as part of its result. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/usingLogFile-alt.scala:21:24 ----------------------------- +21 | usingLogger(file)(l => () => l.log("test")) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: (l: Test.Logger{val f: java.io.OutputStream^?}^{file}) ->? box () ->? Unit + | Required: (x$0: Test.Logger^{file}) ->{'cap[..](from instantiating usingLogger)} box () ->? Unit + | + | Note that reference (cap[Logger] : caps.Root), defined at level 1 + | cannot be included in outer capture set ?, defined at level 0 in package + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/usingLogFile-alt.scala b/tests/neg-custom-args/captures/usingLogFile-alt.scala index 6b529ee6f892..046a36155392 100644 --- a/tests/neg-custom-args/captures/usingLogFile-alt.scala +++ b/tests/neg-custom-args/captures/usingLogFile-alt.scala @@ -15,9 +15,9 @@ object Test: def usingLogger[sealed T](f: OutputStream^)(op: Logger^{f} => T): T = ??? - usingFile( // error + usingFile( "foo", file => { - usingLogger(file)(l => () => l.log("test")) + usingLogger(file)(l => () => l.log("test")) // error } ) diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index 5499e58d4354..7a463319e2b9 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -1,47 +1,46 @@ --- Error: tests/neg-custom-args/captures/usingLogFile.scala:31:6 ------------------------------------------------------- -31 | var later3: () => Unit = () => () // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Mutable variable later3 cannot have type box () => Unit since - | that type captures the root capability `cap`. - | This restriction serves to prevent local capabilities from escaping the scope where they are defined. --- Error: tests/neg-custom-args/captures/usingLogFile.scala:35:6 ------------------------------------------------------- -35 | var later4: Cell[() => Unit] = Cell(() => ()) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Mutable variable later4 cannot have type Test2.Cell[() => Unit] since - | the part () => Unit of that type captures the root capability `cap`. - | This restriction serves to prevent local capabilities from escaping the scope where they are defined. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:32:37 ------------------------------------------------------ +32 | usingLogFile { f => later3 = () => f.write(0) } // error + | ^ + |(f : java.io.FileOutputStream^{local}) cannot be referenced here; it is not included in the allowed capture set {cap[]} + |of an enclosing function literal with expected type box () ->{cap[]} Unit +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/usingLogFile.scala:36:35 --------------------------------- +36 | usingLogFile { f => later4 = Cell(() => f.write(0)) } // error + | ^^^^^^^^^^^^^^^^^^^^^^ + | Found: Test2.Cell[box () ->{f} Unit]^? + | Required: Test2.Cell[() ->{cap[]} Unit] + | + | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:12:14 ------------------------------------------------------ +12 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^ + | escaping local reference local.type -- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ 23 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^ - | Sealed type variable T cannot be instantiated to box () => Unit since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method usingLogFile - | leaking as part of its result. + | 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 | ^^^^^^^^^^^^ - | Sealed type variable T cannot be instantiated to box Test2.Cell[() => Unit]^? since - | the part () => Unit of that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method usingLogFile - | leaking as part of its result. + | escaping local reference local.type -- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:14 ------------------------------------------------------ 47 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^ - | Sealed type variable T cannot be instantiated to box () => Unit since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method usingLogFile - | leaking as part of its result. + | 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 | ^^^^^^^^^ - | Sealed type variable T cannot be instantiated to box (x$0: Int) => Unit since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method usingFile - | leaking as part of its result. + | escaping local reference local.type -- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:16 ------------------------------------------------------ -71 | val later = usingFile("logfile", // error +71 | val later = usingFile("logfile", // error | ^^^^^^^^^ - | Sealed type variable T cannot be instantiated to box () => Unit since - | that type captures the root capability `cap`. - | This is often caused by a local capability in an argument of method usingFile - | leaking as part of its result. + | reference (_$1 : java.io.OutputStream^{local}) is not included in the allowed capture set {x$0, local, local²} + | + | Note that reference (_$1 : java.io.OutputStream^{local}), defined at level 6 + | cannot be included in outer capture set {x$0, local, local}, defined at level 1 in method test + | + | where: local is a reference to a value parameter + | local² is a reference to a value parameter +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:72:6 ------------------------------------------------------- +72 | usingLogger(_, l => () => l.log("test"))) // error ??? but had the comment: ok, since we can widen `l` to `file` instead of to `cap` + | ^^^^^^^^^^^ + | escaping local reference local.type diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index 0aa9290a7f0b..f6266c086d35 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -9,7 +9,7 @@ object Test1: logFile.close() result - val later = usingLogFile { f => () => f.write(0) } + val later = usingLogFile { f => () => f.write(0) } // error later() object Test2: @@ -28,12 +28,12 @@ object Test2: private val later2 = usingLogFile { f => Cell(() => f.write(0)) } // error later2.x() - var later3: () => Unit = () => () // error - usingLogFile { f => later3 = () => f.write(0) } + var later3: () => Unit = () => () + usingLogFile { f => later3 = () => f.write(0) } // error later3() - var later4: Cell[() => Unit] = Cell(() => ()) // error - usingLogFile { f => later4 = Cell(() => f.write(0)) } + var later4: Cell[() => Unit] = Cell(() => ()) + usingLogFile { f => later4 = Cell(() => f.write(0)) } // error later4.x() object Test3: @@ -68,6 +68,6 @@ object Test4: op(logger) def test = - val later = usingFile("logfile", // error - usingLogger(_, l => () => l.log("test"))) // ok, since we can widen `l` to `file` instead of to `cap` + val later = usingFile("logfile", // error + usingLogger(_, l => () => l.log("test"))) // error ??? but had the comment: ok, since we can widen `l` to `file` instead of to `cap` later() diff --git a/tests/neg/capt-wf.scala b/tests/neg/capt-wf.scala index b1b375d12f55..d229aff663dc 100644 --- a/tests/neg/capt-wf.scala +++ b/tests/neg/capt-wf.scala @@ -15,7 +15,7 @@ def test(c: Cap, other: String): Unit = val x4: C^{s2} = ??? // OK val x5: C^{c, c} = ??? // error: redundant // val x6: C^{c}^{c} = ??? // would be syntax error - val x7: Cap^{c} = ??? // error: redundant + val x7: Cap^{c} = ??? // no longer redundant, Cap and c are on different levels // val x8: C^{c}^{cap} = ??? // would be syntax error val x9: C^{c, cap} = ??? // error: redundant val x10: C^{cap, c} = ??? // error: redundant From 95fbe8a084cbafccc56854f69abfb1591012a3d6 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 27 Aug 2023 23:06:12 +0200 Subject: [PATCH 55/76] Disable posWithCompilerCC for now Need to follow up later --- .../dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala index 6a5e92a51a37..9529f94a3890 100644 --- a/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/BootstrappedOnlyCompilationTests.scala @@ -32,7 +32,8 @@ class BootstrappedOnlyCompilationTests { ).checkCompile() } - @Test def posWithCompilerCC: Unit = + // @Test + def posWithCompilerCC: Unit = implicit val testGroup: TestGroup = TestGroup("compilePosWithCompilerCC") aggregateTests( compileDir("tests/pos-with-compiler-cc/dotc", withCompilerOptions.and("-language:experimental.captureChecking")) From 494272475b091034339fdd079294c59ebb7e41d3 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 28 Aug 2023 11:09:01 +0200 Subject: [PATCH 56/76] Revert "Implement sealed type variables" This reverts commit 05bce382e9239c46ba6b2507f95406aed21d5e27. # Conflicts: # compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala # compiler/src/dotty/tools/dotc/cc/Setup.scala # library/src/scala/caps.scala # tests/neg-custom-args/captures/capt-test.scala # tests/neg-custom-args/captures/capt1.check # tests/neg-custom-args/captures/ctest.scala # tests/neg-custom-args/captures/filevar.scala # tests/neg-custom-args/captures/heal-tparam-cs.scala # tests/neg-custom-args/captures/i15049.scala # tests/neg-custom-args/captures/i15772.check # tests/neg-custom-args/captures/i15923.scala # tests/neg-custom-args/captures/lazylists-exceptions.check # tests/neg-custom-args/captures/real-try.check # tests/neg-custom-args/captures/real-try.scala # tests/neg-custom-args/captures/sealed-leaks.scala # tests/neg-custom-args/captures/stack-alloc.scala # tests/neg-custom-args/captures/try.check # tests/neg-custom-args/captures/try.scala # tests/neg-custom-args/captures/try3.scala # tests/neg-custom-args/captures/usingLogFile.check # tests/neg-custom-args/captures/usingLogFile.scala # tests/neg-custom-args/captures/vars.check # tests/neg-custom-args/captures/vars.scala # tests/pos-custom-args/captures/vars1.scala --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 23 +------------ compiler/src/dotty/tools/dotc/cc/Setup.scala | 18 ---------- .../dotty/tools/dotc/core/Definitions.scala | 2 -- .../src/dotty/tools/dotc/core/Types.scala | 17 +--------- .../dotty/tools/dotc/parsing/Parsers.scala | 33 +++++++++---------- .../src/dotty/tools/dotc/typer/Checking.scala | 7 +--- library/src/scala/caps.scala | 6 ---- .../captures/heal-tparam-cs.scala | 6 ++-- .../captures/usingLogFile-alt.check | 16 ++++----- .../captures/usingLogFile-alt.scala | 8 ++--- tests/pos-custom-args/captures/i15749a.scala | 5 +-- tests/pos-custom-args/captures/vars1.scala | 9 +---- tests/pos-special/stdlib/Test1.scala | 2 +- tests/pos-with-compiler-cc/dotc/Run.scala | 5 ++- .../dotc/core/Scopes.scala | 7 ++-- .../dotc/core/TypeComparer.scala | 11 +++---- .../dotc/core/tasty/TreeUnpickler.scala | 10 +++--- .../dotc/typer/Namer.scala | 4 +-- .../dotc/typer/Typer.scala | 15 ++++----- 19 files changed, 63 insertions(+), 141 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index dba4ce3c3e03..f13db088aa63 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -173,21 +173,6 @@ object CheckCaptures: val srcTree = if ann.span.exists then ann else tpt report.warning(em"redundant capture: $remaining already accounts for $ref", srcTree.srcPos) - /** Report an error if some part of `tp` contains the root capability in its capture set */ - def disallowRootCapabilitiesIn(tp: Type, what: String, have: String, addendum: String, pos: SrcPos)(using Context) = - val check = new TypeTraverser: - def traverse(t: Type) = - if variance >= 0 then - t.captureSet.disallowRootCapability: () => - def part = if t eq tp then "" else i"the part $t of " - report.error( - em"""$what cannot $have $tp since - |${part}that type captures the root capability `cap`. - |$addendum""", - pos) - traverseChildren(t) - check.traverse(tp) - /** Attachment key for bodies of closures, provided they are values */ val ClosureBodyValue = Property.Key[Unit] @@ -693,17 +678,11 @@ class CheckCaptures extends Recheck, SymTransformer: val tryOwner = ccState.tryBlockOwner.remove(tree).getOrElse(ctx.owner) val saved = curEnv curEnv = Env(tryOwner, EnvKind.Regular, CaptureSet.Var(curEnv.owner), curEnv) - val tp = try + try inContext(ctx.withOwner(tryOwner)): super.recheckTry(tree, pt) finally curEnv = saved - if allowUniversalInBoxed && Feature.enabled(Feature.saferExceptions) then - disallowRootCapabilitiesIn(tp, - "Result of `try`", "have type", - "This is often caused by a locally generated exception capability leaking as part of its result.", - tree.srcPos) - tp /* Currently not needed, since capture checking takes place after ElimByName. * Keep around in case we need to get back to it diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index b3287b5e0aaa..e8f143bc77f2 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -356,29 +356,11 @@ extends tpd.TreeTraverser: mapRoots ) capt.println(i"mapped $tree = ${tpt.knownType}") - if allowUniversalInBoxed && tree.symbol.is(Mutable) - && !tree.symbol.hasAnnotation(defn.UncheckedCapturesAnnot) - then - CheckCaptures.disallowRootCapabilitiesIn(tpt.knownType, - i"Mutable variable ${tree.symbol.name}", "have type", - "This restriction serves to prevent local capabilities from escaping the scope where they are defined.", - tree.srcPos) 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 - - if allowUniversalInBoxed then - val polyType = atPhase(preRecheckPhase): - fn.tpe.widen.asInstanceOf[TypeLambda] - for case (arg: TypeTree, pinfo, pname) <- args.lazyZip(polyType.paramInfos).lazyZip((polyType.paramNames)) do - if pinfo.bounds.hi.hasAnnotation(defn.Caps_SealedAnnot) then - def where = if fn.symbol.exists then i" in an argument of ${fn.symbol}" else "" - CheckCaptures.disallowRootCapabilitiesIn(arg.knownType, - i"Sealed type variable $pname", "be instantiated to", - i"This is often caused by a local capability$where\nleaking as part of its result.", - tree.srcPos) case tree: Template => inContext(ctx.withOwner(tree.symbol.owner)): traverseChildren(tree) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index eb2e20bfcb29..4474e2b6c0ba 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -977,7 +977,6 @@ 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 Caps_SealedAnnot: ClassSymbol = requiredClass("scala.caps.Sealed") @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") @@ -1028,7 +1027,6 @@ class Definitions { @tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked") @tu lazy val UncheckedStableAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedStable") @tu lazy val UncheckedVarianceAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedVariance") - @tu lazy val UncheckedCapturesAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedCaptures") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val WithPureFunsAnnot: ClassSymbol = requiredClass("scala.annotation.internal.WithPureFuns") @tu lazy val CaptureCheckedAnnot: ClassSymbol = requiredClass("scala.annotation.internal.CaptureChecked") diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 2e68790547a1..19a972d0c330 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -4082,15 +4082,10 @@ object Types { protected def toPInfo(tp: Type)(using Context): PInfo - /** If `tparam` is a sealed type parameter symbol of a polymorphic method, add - * a @caps.Sealed annotation to the upperbound in `tp`. - */ - protected def addSealed(tparam: ParamInfo, tp: Type)(using Context): Type = tp - def fromParams[PI <: ParamInfo.Of[N]](params: List[PI], resultType: Type)(using Context): Type = if (params.isEmpty) resultType else apply(params.map(_.paramName))( - tl => params.map(param => toPInfo(addSealed(param, tl.integrate(params, param.paramInfo)))), + tl => params.map(param => toPInfo(tl.integrate(params, param.paramInfo))), tl => tl.integrate(params, resultType)) } @@ -4412,16 +4407,6 @@ object Types { resultTypeExp: PolyType => Type)(using Context): PolyType = unique(new PolyType(paramNames)(paramInfosExp, resultTypeExp)) - override protected def addSealed(tparam: ParamInfo, tp: Type)(using Context): Type = - tparam match - case tparam: Symbol if tparam.is(Sealed) => - tp match - case tp @ TypeBounds(lo, hi) => - tp.derivedTypeBounds(lo, - AnnotatedType(hi, Annotation(defn.Caps_SealedAnnot, tparam.span))) - case _ => tp - case _ => tp - def unapply(tl: PolyType): Some[(List[LambdaParam], Type)] = Some((tl.typeParams, tl.resType)) } diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 40c90bc25e3c..93e858be904d 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3179,9 +3179,7 @@ object Parsers { * id [HkTypeParamClause] TypeParamBounds * * DefTypeParamClause::= ‘[’ DefTypeParam {‘,’ DefTypeParam} ‘]’ - * DefTypeParam ::= {Annotation} - * [`sealed`] -- under captureChecking - * id [HkTypeParamClause] TypeParamBounds + * DefTypeParam ::= {Annotation} id [HkTypeParamClause] TypeParamBounds * * TypTypeParamClause::= ‘[’ TypTypeParam {‘,’ TypTypeParam} ‘]’ * TypTypeParam ::= {Annotation} id [HkTypePamClause] TypeBounds @@ -3191,25 +3189,24 @@ object Parsers { */ def typeParamClause(ownerKind: ParamOwner): List[TypeDef] = inBrackets { - def checkVarianceOK(): Boolean = - val ok = ownerKind != ParamOwner.Def && ownerKind != ParamOwner.TypeParam - if !ok then syntaxError(em"no `+/-` variance annotation allowed here") - in.nextToken() - ok + def variance(vflag: FlagSet): FlagSet = + if ownerKind == ParamOwner.Def || ownerKind == ParamOwner.TypeParam then + syntaxError(em"no `+/-` variance annotation allowed here") + in.nextToken() + EmptyFlags + else + in.nextToken() + vflag def typeParam(): TypeDef = { val isAbstractOwner = ownerKind == ParamOwner.Type || ownerKind == ParamOwner.TypeParam val start = in.offset - var mods = annotsAsMods() | Param - if ownerKind == ParamOwner.Class then mods |= PrivateLocal - if Feature.ccEnabled && in.token == SEALED then - if ownerKind == ParamOwner.Def then mods |= Sealed - else syntaxError(em"`sealed` modifier only allowed for method type parameters") - in.nextToken() - if isIdent(nme.raw.PLUS) && checkVarianceOK() then - mods |= Covariant - else if isIdent(nme.raw.MINUS) && checkVarianceOK() then - mods |= Contravariant + val mods = + annotsAsMods() + | (if (ownerKind == ParamOwner.Class) Param | PrivateLocal else Param) + | (if isIdent(nme.raw.PLUS) then variance(Covariant) + else if isIdent(nme.raw.MINUS) then variance(Contravariant) + else EmptyFlags) atSpan(start, nameStart) { val name = if (isAbstractOwner && in.token == USCORE) { diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index c17c5f25ab5f..1ea24187a185 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -517,12 +517,7 @@ object Checking { // note: this is not covered by the next test since terms can be abstract (which is a dual-mode flag) // but they can never be one of ClassOnlyFlags if !sym.isClass && sym.isOneOf(ClassOnlyFlags) then - val illegal = sym.flags & ClassOnlyFlags - if sym.is(TypeParam) && illegal == Sealed && Feature.ccEnabled && cc.allowUniversalInBoxed then - if !sym.owner.is(Method) then - fail(em"only method type parameters can be sealed") - else - fail(em"only classes can be ${illegal.flagsString}") + fail(em"only classes can be ${(sym.flags & ClassOnlyFlags).flagsString}") if (sym.is(AbsOverride) && !sym.owner.is(Trait)) fail(AbstractOverrideOnlyInTraits(sym)) if sym.is(Trait) then diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 4a6f3a89e7e1..cbf20f40be2b 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -44,9 +44,3 @@ import annotation.experimental def unsafeBoxFunArg: T => U = f end unsafe - - /** An annotation that expresses the sealed modifier on a type parameter - * Should not be directly referred to in source - */ - @deprecated("The Sealed annotation should not be directly used in source code.\nUse the `sealed` modifier on type parameters instead.") - class Sealed extends annotation.Annotation diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index c0fa29bcca3b..2cbb072c580e 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -2,7 +2,7 @@ import language.experimental.captureChecking trait Cap { def use(): Unit } -def localCap[sealed T](op: (c: Cap^{cap}) => T): T = ??? +def localCap[T](op: (lcap: caps.Root) ?-> (c: Cap^{lcap}) => T): T = ??? def main(io: Cap^{cap}, net: Cap^{cap}): Unit = { @@ -11,7 +11,7 @@ def main(io: Cap^{cap}, net: Cap^{cap}): Unit = { } val test2: (c: Cap^{cap}) -> () ->{cap} Unit = - localCap { c => // should work + localCap { c => // error, was: should work (c1: Cap^{cap}) => () => { c1.use() } } @@ -25,7 +25,7 @@ def main(io: Cap^{cap}, net: Cap^{cap}): Unit = { (c1: Cap^{io}) => () => { c1.use() } } - def localCap2[sealed T](op: (c: Cap^{io}) => T): T = ??? + def localCap2[T](op: (c: Cap^{io}) => T): T = ??? val test5: () ->{io} Unit = localCap2 { c => // ok diff --git a/tests/neg-custom-args/captures/usingLogFile-alt.check b/tests/neg-custom-args/captures/usingLogFile-alt.check index 34b61cb28bf3..81ba7f866bc0 100644 --- a/tests/neg-custom-args/captures/usingLogFile-alt.check +++ b/tests/neg-custom-args/captures/usingLogFile-alt.check @@ -1,10 +1,10 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/usingLogFile-alt.scala:21:24 ----------------------------- -21 | usingLogger(file)(l => () => l.log("test")) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: (l: Test.Logger{val f: java.io.OutputStream^?}^{file}) ->? box () ->? Unit - | Required: (x$0: Test.Logger^{file}) ->{'cap[..](from instantiating usingLogger)} box () ->? Unit +-- Error: tests/neg-custom-args/captures/usingLogFile-alt.scala:18:2 --------------------------------------------------- +18 | usingFile( // error + | ^^^^^^^^^ + | reference (file : java.io.OutputStream^{lcap}) is not included in the allowed capture set {x$0, x$0²} | - | Note that reference (cap[Logger] : caps.Root), defined at level 1 - | cannot be included in outer capture set ?, defined at level 0 in package + | Note that reference (file : java.io.OutputStream^{lcap}), defined at level 4 + | cannot be included in outer capture set {x$0, x$0}, defined at level 0 in package | - | longer explanation available when compiling with `-explain` + | 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-alt.scala b/tests/neg-custom-args/captures/usingLogFile-alt.scala index 046a36155392..f93d0fe0d895 100644 --- a/tests/neg-custom-args/captures/usingLogFile-alt.scala +++ b/tests/neg-custom-args/captures/usingLogFile-alt.scala @@ -7,17 +7,17 @@ object Test: class Logger(f: OutputStream^): def log(msg: String): Unit = ??? - def usingFile[sealed T](name: String, op: OutputStream^ => T): T = + def usingFile[T](name: String, op: (lcap: caps.Root) ?-> OutputStream^{lcap} => T): T = val f = new FileOutputStream(name) val result = op(f) f.close() result - def usingLogger[sealed T](f: OutputStream^)(op: Logger^{f} => T): T = ??? + def usingLogger[T](f: OutputStream^)(op: Logger^{f} => T): T = ??? - usingFile( + usingFile( // error "foo", file => { - usingLogger(file)(l => () => l.log("test")) // error + usingLogger(file)(l => () => l.log("test")) } ) diff --git a/tests/pos-custom-args/captures/i15749a.scala b/tests/pos-custom-args/captures/i15749a.scala index fe5f4d75dae1..0ac65f7f4150 100644 --- a/tests/pos-custom-args/captures/i15749a.scala +++ b/tests/pos-custom-args/captures/i15749a.scala @@ -1,3 +1,4 @@ +// TODO: Adapt to levels class Unit object u extends Unit @@ -10,12 +11,12 @@ def test = def wrapper[T](x: T): Wrapper[T] = [X] => (op: T ->{cap} X) => op(x) - def strictMap[A <: Top, sealed B <: Top](mx: Wrapper[A])(f: A ->{cap} B): Wrapper[B] = + def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: A ->{cap} B): Wrapper[B] = mx((x: A) => wrapper(f(x))) def force[A](thunk: Unit ->{cap} A): A = thunk(u) - def forceWrapper[sealed A](mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = + def forceWrapper[A](mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = // Γ ⊢ mx: Wrapper[□ {cap} Unit => A] // `force` should be typed as ∀(□ {cap} Unit -> A) A, but it can not strictMap[Unit ->{cap} A, A](mx)(t => force[A](t)) // error diff --git a/tests/pos-custom-args/captures/vars1.scala b/tests/pos-custom-args/captures/vars1.scala index 451b8988364f..8e3253bcd22d 100644 --- a/tests/pos-custom-args/captures/vars1.scala +++ b/tests/pos-custom-args/captures/vars1.scala @@ -1,26 +1,19 @@ import caps.unsafe.* -import annotation.unchecked.uncheckedCaptures object Test: type ErrorHandler = (Int, String) => Unit - @uncheckedCaptures var defaultIncompleteHandler: ErrorHandler = ??? - @uncheckedCaptures var incompleteHandler: ErrorHandler = defaultIncompleteHandler private val x = incompleteHandler.unsafeUnbox val _ : ErrorHandler = x val _ = x(1, "a") - def defaultIncompleteHandler1(): (Int, String) => Unit = ??? + def defaultIncompleteHandler1(): ErrorHandler = ??? val defaultIncompleteHandler2: ErrorHandler = ??? - @uncheckedCaptures var incompleteHandler1: ErrorHandler = defaultIncompleteHandler1() - @uncheckedCaptures var incompleteHandler2: ErrorHandler = defaultIncompleteHandler2 - @uncheckedCaptures private var incompleteHandler7 = defaultIncompleteHandler1() - @uncheckedCaptures private var incompleteHandler8 = defaultIncompleteHandler2 incompleteHandler1 = defaultIncompleteHandler2 diff --git a/tests/pos-special/stdlib/Test1.scala b/tests/pos-special/stdlib/Test1.scala index e5ae39027f94..437c739cc0b8 100644 --- a/tests/pos-special/stdlib/Test1.scala +++ b/tests/pos-special/stdlib/Test1.scala @@ -6,7 +6,7 @@ import java.io.* object Test0: - def usingLogFile[sealed T](op: FileOutputStream^ => T): T = + def usingLogFile[T](op: (lcap: caps.Root) ?-> FileOutputStream^ => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() diff --git a/tests/pos-with-compiler-cc/dotc/Run.scala b/tests/pos-with-compiler-cc/dotc/Run.scala index 96f8c6a7b06f..16a955afca1a 100644 --- a/tests/pos-with-compiler-cc/dotc/Run.scala +++ b/tests/pos-with-compiler-cc/dotc/Run.scala @@ -32,7 +32,7 @@ import scala.collection.mutable import scala.util.control.NonFatal import scala.io.Codec import annotation.constructorOnly -import annotation.unchecked.uncheckedCaptures +import caps.unsafe.unsafeUnbox /** A compiler run. Exports various methods to compile source files */ class Run(comp: Compiler, @constructorOnly ictx0: Context) extends ImplicitRunInfo with ConstraintRunInfo { @@ -165,7 +165,6 @@ class Run(comp: Compiler, @constructorOnly ictx0: Context) extends ImplicitRunIn val staticRefs = util.EqHashMap[Name, Denotation](initialCapacity = 1024) /** Actions that need to be performed at the end of the current compilation run */ - @uncheckedCaptures private var finalizeActions = mutable.ListBuffer[() => Unit]() /** Will be set to true if any of the compiled compilation units contains @@ -276,7 +275,7 @@ class Run(comp: Compiler, @constructorOnly ictx0: Context) extends ImplicitRunIn Rewrites.writeBack() suppressions.runFinished(hasErrors = ctx.reporter.hasErrors) while (finalizeActions.nonEmpty) { - val action = finalizeActions.remove(0) + val action = finalizeActions.remove(0).unsafeUnbox action() } compiling = false diff --git a/tests/pos-with-compiler-cc/dotc/core/Scopes.scala b/tests/pos-with-compiler-cc/dotc/core/Scopes.scala index f5a108a13c19..0e8edd55ed08 100644 --- a/tests/pos-with-compiler-cc/dotc/core/Scopes.scala +++ b/tests/pos-with-compiler-cc/dotc/core/Scopes.scala @@ -1,3 +1,8 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2012 LAMP/EPFL + * @author Martin Odersky + */ + package dotty.tools package dotc package core @@ -12,7 +17,6 @@ import Denotations._ import printing.Texts._ import printing.Printer import SymDenotations.NoDenotation -import annotation.unchecked.uncheckedCaptures import collection.mutable @@ -216,7 +220,6 @@ object Scopes { private var elemsCache: List[Symbol] | Null = null /** The synthesizer to be used, or `null` if no synthesis is done on this scope */ - @uncheckedCaptures private var synthesize: SymbolSynthesizer | Null = null /** Use specified synthesize for this scope */ diff --git a/tests/pos-with-compiler-cc/dotc/core/TypeComparer.scala b/tests/pos-with-compiler-cc/dotc/core/TypeComparer.scala index 0e1fc277865a..67b9f063e9d0 100644 --- a/tests/pos-with-compiler-cc/dotc/core/TypeComparer.scala +++ b/tests/pos-with-compiler-cc/dotc/core/TypeComparer.scala @@ -25,7 +25,7 @@ import reporting.trace import annotation.constructorOnly import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam, isAlwaysPure} import language.experimental.pureFunctions -import annotation.unchecked.uncheckedCaptures +import caps.unsafe.* /** Provides methods to compare types. */ @@ -33,18 +33,17 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling import TypeComparer._ Stats.record("TypeComparer") - @uncheckedCaptures - private var myContext: Context = initctx - def comparerContext: Context = myContext + private var myContext: Context = initctx.unsafeBox + def comparerContext: Context = myContext.unsafeUnbox - protected given [DummySoItsADef]: Context = myContext + protected given [DummySoItsADef]: Context = myContext.unsafeUnbox protected var state: TyperState = compiletime.uninitialized def constraint: Constraint = state.constraint def constraint_=(c: Constraint): Unit = state.constraint = c def init(c: Context): Unit = - myContext = c + myContext = c.unsafeBox state = c.typerState monitored = false GADTused = false diff --git a/tests/pos-with-compiler-cc/dotc/core/tasty/TreeUnpickler.scala b/tests/pos-with-compiler-cc/dotc/core/tasty/TreeUnpickler.scala index fcc449af3632..b87cde4a6ad1 100644 --- a/tests/pos-with-compiler-cc/dotc/core/tasty/TreeUnpickler.scala +++ b/tests/pos-with-compiler-cc/dotc/core/tasty/TreeUnpickler.scala @@ -47,7 +47,7 @@ import dotty.tools.tasty.TastyFormat._ import scala.annotation.constructorOnly import scala.annotation.internal.sharable import language.experimental.pureFunctions -import annotation.unchecked.uncheckedCaptures +import caps.unsafe.{unsafeUnbox, unsafeBox} /** Unpickler for typed trees * @param reader the reader from which to unpickle @@ -1086,15 +1086,15 @@ class TreeUnpickler(reader: TastyReader, def readIndexedStats[T](exprOwner: Symbol, end: Addr, k: (List[Tree], Context) => T = sameTrees)(using Context): T = val buf = new mutable.ListBuffer[Tree] - @uncheckedCaptures var curCtx = ctx + var curCtx = ctx.unsafeBox while currentAddr.index < end.index do - val stat = readIndexedStat(exprOwner)(using curCtx) + val stat = readIndexedStat(exprOwner)(using curCtx.unsafeUnbox) buf += stat stat match - case stat: Import => curCtx = curCtx.importContext(stat, stat.symbol) + case stat: Import => curCtx = curCtx.unsafeUnbox.importContext(stat, stat.symbol).unsafeBox case _ => assert(currentAddr.index == end.index) - k(buf.toList, curCtx) + k(buf.toList, curCtx.unsafeUnbox) def readStats[T](exprOwner: Symbol, end: Addr, k: (List[Tree], Context) => T = sameTrees)(using Context): T = { fork.indexStats(end) diff --git a/tests/pos-with-compiler-cc/dotc/typer/Namer.scala b/tests/pos-with-compiler-cc/dotc/typer/Namer.scala index 8487192b9d8a..548f645a23d9 100644 --- a/tests/pos-with-compiler-cc/dotc/typer/Namer.scala +++ b/tests/pos-with-compiler-cc/dotc/typer/Namer.scala @@ -29,7 +29,7 @@ import TypeErasure.erasure import reporting._ import config.Feature.sourceVersion import config.SourceVersion._ -import annotation.unchecked.uncheckedCaptures + /** This class creates symbols from definitions and imports and gives them * lazy types. @@ -930,8 +930,6 @@ class Namer { typer: Typer => class TypeDefCompleter(original: TypeDef)(ictx: DetachedContext) extends Completer(original)(ictx) with TypeParamsCompleter { private var myTypeParams: List[TypeSymbol] | Null = null - - @uncheckedCaptures private var nestedCtx: Context | Null = null assert(!original.isClassDef) diff --git a/tests/pos-with-compiler-cc/dotc/typer/Typer.scala b/tests/pos-with-compiler-cc/dotc/typer/Typer.scala index 0baae1730f4a..aa286446a334 100644 --- a/tests/pos-with-compiler-cc/dotc/typer/Typer.scala +++ b/tests/pos-with-compiler-cc/dotc/typer/Typer.scala @@ -54,8 +54,7 @@ import config.Config import language.experimental.pureFunctions import scala.annotation.constructorOnly -import annotation.unchecked.uncheckedCaptures - +import caps.unsafe.{unsafeBox, unsafeUnbox} object Typer { @@ -1674,11 +1673,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer * and the patterns of the Match tree and the MatchType correspond. */ def typedDependentMatchFinish(tree: untpd.Match, sel: Tree, wideSelType: Type, cases: List[untpd.CaseDef], pt: MatchType)(using Context): Tree = { - @uncheckedCaptures var caseCtx = ctx + var caseCtx = ctx.unsafeBox val cases1 = tree.cases.zip(pt.cases) .map { case (cas, tpe) => - val case1 = typedCase(cas, sel, wideSelType, tpe)(using caseCtx) - caseCtx = Nullables.afterPatternContext(sel, case1.pat) + val case1 = typedCase(cas, sel, wideSelType, tpe)(using caseCtx.unsafeUnbox) + caseCtx = Nullables.afterPatternContext(sel, case1.pat).unsafeBox case1 } .asInstanceOf[List[CaseDef]] @@ -1693,10 +1692,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } def typedCases(cases: List[untpd.CaseDef], sel: Tree, wideSelType: Type, pt: Type)(using Context): List[CaseDef] = - @uncheckedCaptures var caseCtx = ctx + var caseCtx = ctx.unsafeBox cases.mapconserve { cas => - val case1 = typedCase(cas, sel, wideSelType, pt)(using caseCtx) - caseCtx = Nullables.afterPatternContext(sel, case1.pat) + val case1 = typedCase(cas, sel, wideSelType, pt)(using caseCtx.unsafeUnbox) + caseCtx = Nullables.afterPatternContext(sel, case1.pat).unsafeBox case1 } From 0ad89a07c36d367a92594331d98bb45c54402c6a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 28 Aug 2023 21:29:35 +0200 Subject: [PATCH 57/76] Fix upperApprox --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index c0c735580291..11f4ce98dca2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -532,7 +532,9 @@ object CaptureSet: * of this set. The universal set {cap} is a sound fallback. */ final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = - if isConst || elems.exists(_.isRootCapability) then this + if isConst then this + else if elems.exists(_.isRootCapability) then + CaptureSet(elems.filter(_.isRootCapability).toList*) else if computingApprox then universal else computingApprox = true From 0d58f24101bb5b5aa789b6de9ee9e72721c840a8 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 28 Aug 2023 21:34:36 +0200 Subject: [PATCH 58/76] Allow to instantiate `cap` in checkConformsExpr 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`. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 14 +++++++++++++ .../dotty/tools/dotc/transform/Recheck.scala | 21 ++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f13db088aa63..88e6a9227580 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -757,6 +757,20 @@ class CheckCaptures extends Recheck, SymTransformer: // accessible through those types (c.f. addOuterRefs, also #14930 for a discussion). // - Adapt box status and environment capture sets by simulating box/unbox operations. + override def isCompatible(actual: Type, expected: Type, tree: Tree)(using Context): Boolean = + super.isCompatible(actual, expected, tree) + || { + val mapr = mapRoots(defn.captureRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner)) + val actual1 = mapr(actual) + (actual1 ne actual) && { + val res = super.isCompatible(actual1, expected, tree) + 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 */ override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Unit = val expected1 = alignDependentFunction(addOuterRefs(expected, actual), actual.stripCapturing) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 87a7f32e30f7..2456e4011367 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -567,18 +567,19 @@ abstract class Recheck extends Phase, SymTransformer: case _ => checkConformsExpr(tpe.widenExpr, pt.widenExpr, tree) + def isCompatible(actual: Type, expected: Type)(using Context): Boolean = + actual <:< expected + || expected.isRepeatedParam + && isCompatible(actual, + expected.translateFromRepeated(toArray = actual.isRef(defn.ArrayClass))) + || { + val widened = widenSkolems(expected) + (widened ne expected) && isCompatible(actual, widened) + } + def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda = NothingToAdd)(using Context): Unit = //println(i"check conforms $actual <:< $expected") - - def isCompatible(expected: Type): Boolean = - actual <:< expected - || expected.isRepeatedParam - && isCompatible(expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass))) - || { - val widened = widenSkolems(expected) - (widened ne expected) && isCompatible(widened) - } - if !isCompatible(expected) then + if !isCompatible(actual, expected) then recheckr.println(i"conforms failed for ${tree}: $actual vs $expected") err.typeMismatch(tree.withType(actual), expected, addenda) else if debugSuccesses then From 2ca0f6feb552d13ddfc025df9f2eb261e88c2cbf Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 28 Aug 2023 22:13:47 +0200 Subject: [PATCH 59/76] Fix unsafeAssumePure handling Test against a fresh root var instead of against universal --- compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala | 2 +- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 6ff0c50ce28c..4df5ea97a6b2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -14,7 +14,7 @@ type CaptureRoot = TermRef | CaptureRoot.Var object CaptureRoot: - case class Var(owner: Symbol, source: Symbol)(using @constructorOnly ictx: Context) extends CaptureRef, Showable: + case class Var(owner: Symbol, source: Symbol = NoSymbol)(using @constructorOnly ictx: Context) extends CaptureRef, Showable: var upperBound: Symbol = owner var lowerBound: Symbol = NoSymbol diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 88e6a9227580..c04cbc8f5e8a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -466,7 +466,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.universal)) + val argType0 = recheck(arg, pt.capturing(CaptureSet(CaptureRoot.Var(ctx.owner)))) val argType = if argType0.captureSet.isAlwaysEmpty then argType0 else argType0.widen.stripCapturing From bb00ccf85915b94283fa6448bc088d6797a15c12 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 28 Aug 2023 22:14:54 +0200 Subject: [PATCH 60/76] Add config switch for mapRoots to constrain root vars or not --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index f4d2685553ac..ea26cd9910d2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -17,14 +17,17 @@ import collection.mutable private val Captures: Key[CaptureSet] = Key() private val BoxedType: Key[BoxedTypeCache] = Key() -private val enableRootMapping = 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 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 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 @@ -77,21 +80,22 @@ trait FollowAliases extends TypeMap: class mapRoots(from: CaptureRoot, to: CaptureRoot)(using Context) extends BiTypeMap, FollowAliases: thisMap => - def apply(t: Type): Type = t match - case t: TermRef if (t eq from) && enableRootMapping => - to - 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 - && CaptureRoot.isEnclosingRoot(from, t) - && CaptureRoot.isEnclosingRoot(t, from) => to - case from: CaptureRoot.Var if from.followAlias eq t => to - case _ => from - case _ => - mapOverFollowingAliases(t) + 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 _ => + mapOverFollowingAliases(t) def inverse = mapRoots(to, from) end mapRoots From d7bef7e6ae43708f8adc9f84dbd82207a66c1e5e Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 28 Aug 2023 22:37:36 +0200 Subject: [PATCH 61/76] Introduce property for loose capture root checking Replaces previous unsound rule where `cap` subsumes everything. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 12 +++++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 42 +++++++++++-------- .../captures/usingLogFile.check | 23 +++------- .../captures/usingLogFile.scala | 4 +- tests/neg/capt-wf.scala | 2 +- tests/pos-custom-args/captures/ctest.scala | 7 ++++ tests/pos-custom-args/captures/try3.scala | 3 +- .../stdlib/collection/Iterator.scala | 2 +- 8 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 tests/pos-custom-args/captures/ctest.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 11f4ce98dca2..915e8821c3bd 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -122,15 +122,18 @@ sealed abstract class CaptureSet extends Showable: * - x is the same as y, * - x is a this reference and y refers to a field of x * - x and y are local roots and y is an enclosing root of x + * - the LooseRootChecking property is asserted, and either `x` is `cap` + * or `x` is a local root and y is `cap`. */ extension (x: CaptureRef)(using Context) private def subsumes(y: CaptureRef) = (x eq y) - || x.isGenericRootCapability // !!! dubious || y.match 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) + && ctx.property(LooseRootChecking).isDefined private def isRootIncluding(y: CaptureRoot) = x.isLocalRootCapability && y.isLocalRootCapability @@ -367,6 +370,13 @@ object CaptureSet: def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) + /** If this context property is asserted, we conflate capture roots in subCapture + * tests. Specifically, `cap` then subsumes everything and all local roots subsume `cap`. + * This generally not sound. We currently use loose root checking only in self type + * conformance tests in CheckCaptures.checkSelfTypes. + */ + val LooseRootChecking: Property.Key[Unit] = Property.Key() + /** The subclass of constant capture sets with given elements `elems` */ class Const private[CaptureSet] (val elems: Refs, val description: String = "") extends CaptureSet: def isConst = true diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c04cbc8f5e8a..16b0d702cef0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -18,7 +18,7 @@ import transform.SymUtils.* import transform.{Recheck, PreRecheck} import Recheck.* import scala.collection.mutable -import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult, LooseRootChecking} import StdNames.nme import NameKinds.DefaultGetterName import reporting.trace @@ -747,23 +747,26 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckFinish(tpe, tree, pt) end recheckFinish - // ------------------ Adaptation ------------------------------------- - // - // Adaptations before checking conformance of actual vs expected: - // - // - Convert function to dependent function if expected type is a dependent function type - // (c.f. alignDependentFunction). - // - Relax expected capture set containing `this.type`s by adding references only - // accessible through those types (c.f. addOuterRefs, also #14930 for a discussion). - // - Adapt box status and environment capture sets by simulating box/unbox operations. - - override def isCompatible(actual: Type, expected: Type, tree: Tree)(using Context): Boolean = - super.isCompatible(actual, expected, tree) + // ------------------ Adaptation ------------------------------------- + // + // Adaptations before checking conformance of actual vs expected: + // + // - Convert function to dependent function if expected type is a dependent function type + // (c.f. alignDependentFunction). + // - Relax expected capture set containing `this.type`s by adding references only + // accessible through those types (c.f. addOuterRefs, also #14930 for a discussion). + // - 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) || { - val mapr = mapRoots(defn.captureRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner)) - val actual1 = mapr(actual) + // When testing whether `A <: B`, it could be that `B` uses a local capture root, + // but a uses `cap`, i.e. is capture polymorphic. In this case, adaptation is allowed + // to instantiate `A` to match the root in `B`. + val actual1 = mapRoots(defn.captureRoot.termRef, CaptureRoot.Var(ctx.owner.levelOwner))(actual) (actual1 ne actual) && { - val res = super.isCompatible(actual1, expected, tree) + 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))}") @@ -1084,7 +1087,12 @@ class CheckCaptures extends Recheck, SymTransformer: } assert(roots.nonEmpty) for case root: ClassSymbol <- roots do - inContext(ctx.withOwner(root)): + 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. checkSelfAgainstParents(root, root.baseClasses) val selfType = root.asClass.classInfo.selfType interpolator(startingVariance = -1).traverse(selfType) diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index 7a463319e2b9..9c7d9d9f408f 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -10,10 +10,13 @@ | Required: Test2.Cell[() ->{cap[]} Unit] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/usingLogFile.scala:12:14 ------------------------------------------------------ +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:12:6 ------------------------------------------------------- 12 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^ - | escaping local reference local.type + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Non-local value later cannot have an inferred type + | () ->{x$0, 'cap[..](from instantiating usingLogFile)} Unit + | with non-empty capture set {x$0, 'cap[..](from instantiating usingLogFile)}. + | The type needs to be declared explicitly. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ 23 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^ @@ -30,17 +33,3 @@ 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:71:16 ------------------------------------------------------ -71 | val later = usingFile("logfile", // error - | ^^^^^^^^^ - | reference (_$1 : java.io.OutputStream^{local}) is not included in the allowed capture set {x$0, local, local²} - | - | Note that reference (_$1 : java.io.OutputStream^{local}), defined at level 6 - | cannot be included in outer capture set {x$0, local, local}, defined at level 1 in method test - | - | where: local is a reference to a value parameter - | local² is a reference to a value parameter --- Error: tests/neg-custom-args/captures/usingLogFile.scala:72:6 ------------------------------------------------------- -72 | usingLogger(_, l => () => l.log("test"))) // error ??? but had the comment: ok, since we can widen `l` to `file` instead of to `cap` - | ^^^^^^^^^^^ - | escaping local reference local.type diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index f6266c086d35..589be6ef8fcb 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -68,6 +68,6 @@ object Test4: op(logger) def test = - val later = usingFile("logfile", // error - usingLogger(_, l => () => l.log("test"))) // error ??? but had the comment: ok, since we can widen `l` to `file` instead of to `cap` + val later = usingFile("logfile", + usingLogger(_, l => () => l.log("test"))) // ok, since we can widen `l` to `file` instead of to `cap` later() diff --git a/tests/neg/capt-wf.scala b/tests/neg/capt-wf.scala index d229aff663dc..b1b375d12f55 100644 --- a/tests/neg/capt-wf.scala +++ b/tests/neg/capt-wf.scala @@ -15,7 +15,7 @@ def test(c: Cap, other: String): Unit = val x4: C^{s2} = ??? // OK val x5: C^{c, c} = ??? // error: redundant // val x6: C^{c}^{c} = ??? // would be syntax error - val x7: Cap^{c} = ??? // no longer redundant, Cap and c are on different levels + val x7: Cap^{c} = ??? // error: redundant // val x8: C^{c}^{cap} = ??? // would be syntax error val x9: C^{c, cap} = ??? // error: redundant val x10: C^{cap, c} = ??? // error: redundant diff --git a/tests/pos-custom-args/captures/ctest.scala b/tests/pos-custom-args/captures/ctest.scala new file mode 100644 index 000000000000..62aa77fec0a5 --- /dev/null +++ b/tests/pos-custom-args/captures/ctest.scala @@ -0,0 +1,7 @@ +class C +type Cap = C^ + +class S + +def f(y: Cap) = + val a: ((x: Cap) -> S^) = (x: Cap) => S() \ No newline at end of file diff --git a/tests/pos-custom-args/captures/try3.scala b/tests/pos-custom-args/captures/try3.scala index b8937bec00f3..b44ea57ccae4 100644 --- a/tests/pos-custom-args/captures/try3.scala +++ b/tests/pos-custom-args/captures/try3.scala @@ -13,7 +13,7 @@ def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = throw ex def test1: Int = - def f(a: Boolean): Boolean -> CanThrow[IOException] ?-> Int = + def f(a: Boolean) = handle { if !a then raise(IOException()) (b: Boolean) => (_: CanThrow[IOException]) ?=> @@ -22,6 +22,7 @@ def test1: Int = } { ex => (b: Boolean) => (_: CanThrow[IOException]) ?=> -1 } + def fc(a: Boolean): Boolean -> CanThrow[IOException] ?-> Int = f(a) handle { val g = f(true) g(false) // can raise an exception diff --git a/tests/pos-special/stdlib/collection/Iterator.scala b/tests/pos-special/stdlib/collection/Iterator.scala index 4903a1dae0a6..6e8d15ea99a4 100644 --- a/tests/pos-special/stdlib/collection/Iterator.scala +++ b/tests/pos-special/stdlib/collection/Iterator.scala @@ -1202,7 +1202,7 @@ object Iterator extends IterableFactory[Iterator] { } else Iterator.empty.next() override def concat[B >: A](that: => IterableOnce[B]^): Iterator[B]^{this, that} = { - val c = new ConcatIteratorCell[B](that, null).asInstanceOf[ConcatIteratorCell[A]] + val c: ConcatIteratorCell[A] = new ConcatIteratorCell[B](that, null).asInstanceOf if (tail == null) { tail = c last = c From b1692922424164ef6ca4b54893ab36a5e7c7afca Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 28 Aug 2023 22:38:15 +0200 Subject: [PATCH 62/76] Adapt .gitignore to ognore coverage files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f378adb24bc8..e61b324f617f 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ docs/_spec/.jekyll-metadata # scaladoc related scaladoc/output/ +#coverage +coverage/ + From d2100d9b547aa761748c3820e2a7d5b519f420e2 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 29 Aug 2023 22:00:58 +0200 Subject: [PATCH 63/76] Simplify function call --- 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 16b0d702cef0..ba8b5f2170f5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -311,7 +311,7 @@ class CheckCaptures extends Recheck, SymTransformer: val expected = openClosures .find(_._1 == owner) .map(_._2) - .getOrElse(owner.info.toFunctionType(isJava = false)) + .getOrElse(owner.info.toFunctionType()) i"\nof an enclosing function literal with expected type $expected" else i"\nof the enclosing ${owner.showLocated}" From 92496113e8b6a93c5b5827547d014560883e3d90 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 30 Aug 2023 10:55:05 +0200 Subject: [PATCH 64/76] Tweaks to tests and doc --- docs/_docs/reference/experimental/cc.md | 4 ++++ tests/neg-custom-args/captures/refs.scala | 20 +++++++++++++++---- .../captures/usingLogFile.check | 7 ------- .../captures/usingLogFile.scala | 3 +-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/_docs/reference/experimental/cc.md b/docs/_docs/reference/experimental/cc.md index 1b50da9b7ed0..b1b2fa8e80cc 100644 --- a/docs/_docs/reference/experimental/cc.md +++ b/docs/_docs/reference/experimental/cc.md @@ -705,6 +705,8 @@ Generally, the string following the capture set consists of alternating numbers - `F` : a variable resulting from _filtering_ the elements of the variable indicated by the string to the right, - `I` : a variable resulting from an _intersection_ of two capture sets, - `D` : a variable resulting from the set _difference_ of two capture sets. + - `R` : a regular variable that _refines_ a class parameter, so that the capture + set of a constructor argument is known in the class instance type. At the end of a compilation run, `-Ycc-debug` will print all variable dependencies of variables referred to in previous output. Here is an example: ``` @@ -723,3 +725,5 @@ This section lists all variables that appeared in previous diagnostics and their - variable `31` has a constant fixed superset `{xs, f}` - variable `32` has no dependencies. + + diff --git a/tests/neg-custom-args/captures/refs.scala b/tests/neg-custom-args/captures/refs.scala index 9ee9acdb1a0d..a1167efa6e09 100644 --- a/tests/neg-custom-args/captures/refs.scala +++ b/tests/neg-custom-args/captures/refs.scala @@ -1,16 +1,22 @@ import java.io.* +type Proc = () => Unit + class Ref[T](init: T): var x: T = init def setX(x: T): Unit = this.x = x +class MonoRef(init: Proc): + type MonoProc = Proc + var x: MonoProc = init + def setX(x: MonoProc): Unit = this.x = x + def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream^{local} => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() result -type Proc = () => Unit def test1 = usingLogFile[Proc]: (local: caps.Root) ?=> // error (but with a hard to parse error message) (f: FileOutputStream^{local}) => @@ -19,19 +25,25 @@ def test1 = def test2 = val r = new Ref[Proc](() => ()) - usingLogFile[Unit]: f => + usingLogFile: f => r.setX(() => f.write(10)) // error r.x() // crash: f is closed at that point + val mr = new MonoRef(() => ()) + usingLogFile[Unit]: f => + mr.setX(() => f.write(10)) // error def test3 = val r = new Ref[Proc](() => ()) - usingLogFile[Unit]: f => + usingLogFile[Unit]: f => r.x = () => f.write(10) // error r.x() // crash: f is closed at that point + val mr = MonoRef(() => ()) + usingLogFile: f => + mr.x = () => f.write(10) // error def test4 = var r: Proc = () => () - usingLogFile[Unit]: f => + usingLogFile[Unit]: f => r = () => f.write(10) // error r() // crash: f is closed at that point diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index 9c7d9d9f408f..9550b5864586 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -10,13 +10,6 @@ | Required: Test2.Cell[() ->{cap[]} Unit] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/usingLogFile.scala:12:6 ------------------------------------------------------- -12 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Non-local value later cannot have an inferred type - | () ->{x$0, 'cap[..](from instantiating usingLogFile)} Unit - | with non-empty capture set {x$0, 'cap[..](from instantiating usingLogFile)}. - | The type needs to be declared explicitly. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:14 ------------------------------------------------------ 23 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index 589be6ef8fcb..de2871bef21f 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -9,7 +9,7 @@ object Test1: logFile.close() result - val later = usingLogFile { f => () => f.write(0) } // error + private val later = usingLogFile { f => () => f.write(0) } // OK, `f` has global lifetime later() object Test2: @@ -62,7 +62,6 @@ object Test4: val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error later(1) - def usingLogger[T](f: OutputStream^, op: (local: caps.Root) ?-> Logger^{f} => T): T = val logger = Logger(f) op(logger) From 803e06a1aa1ef8b56102b0aabdfba60efa5c2503 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 30 Aug 2023 18:45:44 +0200 Subject: [PATCH 65/76] Rename caps.Root to caps.Cap --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 8 ++++---- compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala | 2 +- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 ++-- compiler/src/dotty/tools/dotc/core/Definitions.scala | 2 +- compiler/src/dotty/tools/dotc/core/Types.scala | 2 +- library/src/scala/caps.scala | 8 ++++---- tests/neg-custom-args/captures/capt-test.scala | 2 +- tests/neg-custom-args/captures/cc-this2.check | 2 +- .../captures/exception-definitions.check | 2 +- tests/neg-custom-args/captures/filevar.scala | 2 +- tests/neg-custom-args/captures/heal-tparam-cs.scala | 2 +- tests/neg-custom-args/captures/i15049.scala | 2 +- tests/neg-custom-args/captures/i15772.check | 6 +++--- tests/neg-custom-args/captures/i15923.scala | 2 +- .../captures/lazylists-exceptions.check | 2 +- tests/neg-custom-args/captures/real-try.check | 6 +++--- tests/neg-custom-args/captures/refs.scala | 4 ++-- tests/neg-custom-args/captures/sealed-leaks.scala | 2 +- tests/neg-custom-args/captures/simple-escapes.scala | 2 +- tests/neg-custom-args/captures/stack-alloc.scala | 2 +- tests/neg-custom-args/captures/try.check | 4 ++-- tests/neg-custom-args/captures/try.scala | 2 +- tests/neg-custom-args/captures/try3.scala | 2 +- tests/neg-custom-args/captures/usingLogFile-alt.scala | 2 +- tests/neg-custom-args/captures/usingLogFile.scala | 10 +++++----- tests/neg-custom-args/captures/vars.check | 4 ++-- tests/neg-custom-args/captures/vars.scala | 2 +- tests/pos-special/stdlib/Test1.scala | 2 +- tests/pos/dotty-experimental.scala | 2 +- 31 files changed, 49 insertions(+), 49 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index ea26cd9910d2..2a860e42cfe0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -369,7 +369,7 @@ extension (sym: Symbol) /** The owner of the current level. Qualifying owners are * - methods other than constructors and anonymous functions * - anonymous functions, provided they either define a local - * root of type caps.Root, or they are the rhs of a val definition. + * root of type caps.Cap, or they are the rhs of a val definition. * - classes, if they are not staticOwners * - _root_ */ @@ -397,19 +397,19 @@ extension (sym: Symbol) def setNestingLevel(level: Int)(using Context): Unit = ccState.nestingLevels(sym) = level - /** The parameter with type caps.Root in the leading term parameter section, + /** The parameter with type caps.Cap in the leading term parameter section, * or NoSymbol, if none exists. */ def definedLocalRoot(using Context): Symbol = sym.paramSymss.dropWhile(psyms => psyms.nonEmpty && psyms.head.isType) match - case psyms :: _ => psyms.find(_.info.typeSymbol == defn.Caps_Root).getOrElse(NoSymbol) + case psyms :: _ => psyms.find(_.info.typeSymbol == defn.Caps_Cap).getOrElse(NoSymbol) case _ => NoSymbol def localRoot(using Context): 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_Root.typeRef, nestingLevel = owner.ccNestingLevel) + nme.LOCAL_CAPTURE_ROOT, Synthetic, defn.Caps_Cap.typeRef, nestingLevel = owner.ccNestingLevel) def lclRoot = if owner.isTerm then owner.definedLocalRoot.orElse(newRoot) else newRoot diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index 4df5ea97a6b2..d2ed6d6978e8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -42,7 +42,7 @@ object CaptureRoot: def computeHash(bs: Binders): Int = hash def hash: Int = System.identityHashCode(this) - def underlying(using Context): Type = defn.Caps_Root.typeRef + def underlying(using Context): Type = defn.Caps_Cap.typeRef end Var def isEnclosingRoot(c1: CaptureRoot, c2: CaptureRoot)(using Context): Boolean = diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 915e8821c3bd..5fdbea2a1bc6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -968,7 +968,7 @@ object CaptureSet: case tp: TermParamRef => tp.captureSet case tp: TypeRef => - if tp.typeSymbol == defn.Caps_Root then universal else empty + if tp.typeSymbol == defn.Caps_Cap then universal else empty case _: TypeParamRef => empty case CapturingType(parent, refs: RefiningVar) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ba8b5f2170f5..a181b521ab52 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1151,7 +1151,7 @@ class CheckCaptures extends Recheck, SymTransformer: for case ref: TermParamRef <- elems do if !allowed.contains(ref) && !seen.contains(ref) then seen += ref - if ref.underlying.isRef(defn.Caps_Root) then + if ref.underlying.isRef(defn.Caps_Cap) then report.error(i"escaping local reference $ref", tree.srcPos) else val widened = ref.captureSetOfInfo diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index e8f143bc77f2..dc32be5e8e53 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -318,7 +318,7 @@ extends tpd.TreeTraverser: return inContext(ctx.withOwner(tree.symbol)): if tree.symbol.isAnonymousFunction && tree.symbol.definedLocalRoot.exists then - // closures that define parameters of type caps.Root count as level owners + // closures that define parameters of type caps.Cap count as level owners tree.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) tree.tpt match case tpt: TypeTree if tree.symbol.allOverriddenSymbols.hasNext => @@ -518,7 +518,7 @@ extends tpd.TreeTraverser: // we assume Any is a shorthand of {cap} Any, so if Any is an upper // bound, the type is taken to be impure. else - sym != defn.Caps_Root && superTypeIsImpure(tp.superType) + sym != defn.Caps_Cap && superTypeIsImpure(tp.superType) case tp: (RefinedOrRecType | MatchType) => superTypeIsImpure(tp.underlying) case tp: AndType => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 4474e2b6c0ba..1813e77aa4ee 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -971,7 +971,7 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") - @tu lazy val Caps_Root: TypeSymbol = CapsModule.requiredType("Root") + @tu lazy val Caps_Cap: TypeSymbol = CapsModule.requiredType("Cap") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_unsafeBox: Symbol = CapsUnsafeModule.requiredMethod("unsafeBox") diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 19a972d0c330..f2c4fdecb834 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2930,7 +2930,7 @@ object Types { if name == nme.LOCAL_CAPTURE_ROOT then if symbol.owner.isLocalDummy then symbol.owner.owner else symbol.owner - else if info.isRef(defn.Caps_Root) then + else if info.isRef(defn.Caps_Cap) then val owner = symbol.maybeOwner if owner.isTerm then owner else NoSymbol else NoSymbol diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index cbf20f40be2b..a23de5674476 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -4,16 +4,16 @@ import annotation.experimental @experimental object caps: - opaque type Root = Unit + opaque type Cap = Unit /** The universal capture reference (deprecated) */ @deprecated("Use `cap` instead") - val `*`: Root = () + val `*`: Cap = () /** The universal capture reference */ - val cap: Root = () + val cap: Cap = () - given Root = cap + given Cap = cap object unsafe: diff --git a/tests/neg-custom-args/captures/capt-test.scala b/tests/neg-custom-args/captures/capt-test.scala index c6f612fdc31b..470e3caef967 100644 --- a/tests/neg-custom-args/captures/capt-test.scala +++ b/tests/neg-custom-args/captures/capt-test.scala @@ -14,7 +14,7 @@ def raise[E <: Exception](e: E): Nothing throws E = throw e def foo(x: Boolean): Int throws Fail = if x then 1 else raise(Fail()) -def handle[E <: Exception, R <: Top](op: (lcap: caps.Root) ?-> (CT[E] @retains(lcap)) => R)(handler: E => R): R = +def handle[E <: Exception, R <: Top](op: (lcap: caps.Cap) ?-> (CT[E] @retains(lcap)) => R)(handler: E => R): R = val x: CT[E] = ??? try op(x) catch case ex: E => handler(ex) diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check index 1aae25b60efb..fbf7d5f4d831 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.Root) is not included in the allowed capture set {} of pure base class class C + |reference (cap : 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/exception-definitions.check b/tests/neg-custom-args/captures/exception-definitions.check index 835572f28d4c..4533cf2c083d 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.Root) is not included in the allowed capture set {} of pure base class class Throwable + |reference (cap : 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 diff --git a/tests/neg-custom-args/captures/filevar.scala b/tests/neg-custom-args/captures/filevar.scala index 53987eb3f623..a7ef9d987b1d 100644 --- a/tests/neg-custom-args/captures/filevar.scala +++ b/tests/neg-custom-args/captures/filevar.scala @@ -8,7 +8,7 @@ class Service: var file: File^ = uninitialized def log = file.write("log") -def withFile[T](op: (l: caps.Root) ?-> (f: File^{l}) => T): T = +def withFile[T](op: (l: caps.Cap) ?-> (f: File^{l}) => T): T = op(new File) def test = diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index 2cbb072c580e..f72325a0be8a 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -2,7 +2,7 @@ import language.experimental.captureChecking trait Cap { def use(): Unit } -def localCap[T](op: (lcap: caps.Root) ?-> (c: Cap^{lcap}) => T): T = ??? +def localCap[T](op: (lcap: caps.Cap) ?-> (c: Cap^{lcap}) => T): T = ??? def main(io: Cap^{cap}, net: Cap^{cap}): Unit = { diff --git a/tests/neg-custom-args/captures/i15049.scala b/tests/neg-custom-args/captures/i15049.scala index 6b8441529196..e60367946377 100644 --- a/tests/neg-custom-args/captures/i15049.scala +++ b/tests/neg-custom-args/captures/i15049.scala @@ -2,7 +2,7 @@ class Session: def request = "Response" class Foo: private val session: Session^{cap} = new Session - def withSession[T](f: (local: caps.Root) ?-> (Session^{local}) => T): T = f(session) + def withSession[T](f: (local: caps.Cap) ?-> (Session^{local}) => T): T = f(session) def Test: Unit = val f = new Foo diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index fffd9ab62091..cb6b40361add 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -28,7 +28,7 @@ | Found: box C^{cap[c]} | Required: box C{val arg: C^?}^? | - | Note that reference (cap[c] : caps.Root), defined at level 2 + | 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` @@ -38,7 +38,7 @@ | Found: C^{cap[c]} | Required: C^{'cap[..main3](from instantiating unsafe)} | - | Note that reference (cap[c] : caps.Root), defined at level 2 + | 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` @@ -48,7 +48,7 @@ | Found: () ->{x} Unit | Required: () -> Unit | - | Note that reference (cap[c] : caps.Root), defined at level 2 + | 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/i15923.scala b/tests/neg-custom-args/captures/i15923.scala index d06b42a97eaa..754fd0687037 100644 --- a/tests/neg-custom-args/captures/i15923.scala +++ b/tests/neg-custom-args/captures/i15923.scala @@ -3,7 +3,7 @@ type Id[X] = [T] -> (op: X => T) -> T def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) def bar() = { - def withCap[X](op: (lcap: caps.Root) ?-> Cap^{lcap} => X): X = { + def withCap[X](op: (lcap: caps.Cap) ?-> Cap^{lcap} => X): X = { val cap: Cap = new Cap { def use() = { println("cap is used"); 0 } } val result = op(cap) result diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 3bf1cfd6a816..74318b6bb254 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -4,7 +4,7 @@ | Found: LazyList[Int]^ | Required: LazyList[Int]^? | - | Note that reference (cap : caps.Root), 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/real-try.check b/tests/neg-custom-args/captures/real-try.check index 3ee0d7ac66cf..f57aef60745b 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -10,7 +10,7 @@ | Found: () => Unit | Required: () ->? Unit | - | Note that reference (cap : caps.Root), 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` @@ -20,7 +20,7 @@ | Found: () => Cell[Unit]^? | Required: () ->? Cell[Unit]^? | - | Note that reference (cap : caps.Root), 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` @@ -30,7 +30,7 @@ | Found: Cell[box () => Unit]^? | Required: Cell[() ->? Unit]^? | - | Note that reference (cap : caps.Root), 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/refs.scala b/tests/neg-custom-args/captures/refs.scala index a1167efa6e09..b64b27ef4af0 100644 --- a/tests/neg-custom-args/captures/refs.scala +++ b/tests/neg-custom-args/captures/refs.scala @@ -11,14 +11,14 @@ class MonoRef(init: Proc): var x: MonoProc = init def setX(x: MonoProc): Unit = this.x = x -def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream^{local} => T): T = +def usingLogFile[T](op: (local: caps.Cap) ?-> FileOutputStream^{local} => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() result def test1 = - usingLogFile[Proc]: (local: caps.Root) ?=> // error (but with a hard to parse error message) + usingLogFile[Proc]: (local: caps.Cap) ?=> // error (but with a hard to parse error message) (f: FileOutputStream^{local}) => () => f.write(1) // this line has type () ->{local} Unit, but usingLogFile // requires Proc, which expands to () -> 'cap[..test1](from instantiating usingLogFile) diff --git a/tests/neg-custom-args/captures/sealed-leaks.scala b/tests/neg-custom-args/captures/sealed-leaks.scala index 3436673227c0..df438e6973bc 100644 --- a/tests/neg-custom-args/captures/sealed-leaks.scala +++ b/tests/neg-custom-args/captures/sealed-leaks.scala @@ -2,7 +2,7 @@ import java.io.* def Test2 = - def usingLogFile[T](op: (l: caps.Root) ?-> FileOutputStream^{l} => T): T = + def usingLogFile[T](op: (l: caps.Cap) ?-> FileOutputStream^{l} => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() diff --git a/tests/neg-custom-args/captures/simple-escapes.scala b/tests/neg-custom-args/captures/simple-escapes.scala index 770d02bb851a..0d4666179292 100644 --- a/tests/neg-custom-args/captures/simple-escapes.scala +++ b/tests/neg-custom-args/captures/simple-escapes.scala @@ -4,7 +4,7 @@ class FileOutputStream(str: String): def Test1 = - def usingLogFile[T](op: (local: caps.Root) -> FileOutputStream^{local} => T): T = + def usingLogFile[T](op: (local: caps.Cap) -> FileOutputStream^{local} => T): T = val logFile = FileOutputStream("log") val result = op(caps.cap)(logFile) logFile.close() diff --git a/tests/neg-custom-args/captures/stack-alloc.scala b/tests/neg-custom-args/captures/stack-alloc.scala index 5fbc3a3c591d..befafbf13003 100644 --- a/tests/neg-custom-args/captures/stack-alloc.scala +++ b/tests/neg-custom-args/captures/stack-alloc.scala @@ -5,7 +5,7 @@ class Pooled val stack = mutable.ArrayBuffer[Pooled]() var nextFree = 0 -def withFreshPooled[T](op: (lcap: caps.Root) ?-> Pooled^{lcap} => T): T = +def withFreshPooled[T](op: (lcap: caps.Cap) ?-> Pooled^{lcap} => T): T = if nextFree >= stack.size then stack.append(new Pooled) val pooled = stack(nextFree) nextFree = nextFree + 1 diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index e33de70adbec..5994e3901179 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,8 +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.Root) ?->? (x$0: CT[Exception]^{lcap}) ->? box CT[Exception]^{lcap} - |Required: (lcap: caps.Root) ?-> CT[Exception]^{lcap} ->{'cap[..test](from instantiating handle)} box CT[Exception]^ + |Found: (lcap: caps.Cap) ?->? (x$0: CT[Exception]^{lcap}) ->? box CT[Exception]^{lcap} + |Required: (lcap: caps.Cap) ?-> CT[Exception]^{lcap} ->{'cap[..test](from instantiating handle)} box CT[Exception]^ 24 | (x: CanThrow[Exception]) => x 25 | }{ | diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 686a38d4f203..fe58145bca54 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -14,7 +14,7 @@ def raise[E <: Exception](e: E): Nothing throws E = throw e def foo(x: Boolean): Int throws Fail = if x then 1 else raise(Fail()) -def handle[E <: Exception, R <: Top](op: (lcap: caps.Root) ?-> CT[E]^{lcap} => R)(handler: E => R): R = +def handle[E <: Exception, R <: Top](op: (lcap: caps.Cap) ?-> CT[E]^{lcap} => R)(handler: E => R): R = val x: CT[E] = ??? try op(x) catch case ex: E => handler(ex) diff --git a/tests/neg-custom-args/captures/try3.scala b/tests/neg-custom-args/captures/try3.scala index 0d30c95de4e3..004cda6a399c 100644 --- a/tests/neg-custom-args/captures/try3.scala +++ b/tests/neg-custom-args/captures/try3.scala @@ -4,7 +4,7 @@ class CT[E] type CanThrow[E] = CT[E]^ type Top = Any^ -def handle[E <: Exception, T <: Top](op: (lcap: caps.Root) ?-> CT[E]^{lcap} ?=> T)(handler: E => T): T = +def handle[E <: Exception, T <: Top](op: (lcap: caps.Cap) ?-> CT[E]^{lcap} ?=> T)(handler: E => T): T = val x: CT[E] = ??? try op(using caps.cap)(using x) catch case ex: E => handler(ex) diff --git a/tests/neg-custom-args/captures/usingLogFile-alt.scala b/tests/neg-custom-args/captures/usingLogFile-alt.scala index f93d0fe0d895..36f6ecf1426e 100644 --- a/tests/neg-custom-args/captures/usingLogFile-alt.scala +++ b/tests/neg-custom-args/captures/usingLogFile-alt.scala @@ -7,7 +7,7 @@ object Test: class Logger(f: OutputStream^): def log(msg: String): Unit = ??? - def usingFile[T](name: String, op: (lcap: caps.Root) ?-> OutputStream^{lcap} => T): T = + def usingFile[T](name: String, op: (lcap: caps.Cap) ?-> OutputStream^{lcap} => T): T = val f = new FileOutputStream(name) val result = op(f) f.close() diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index de2871bef21f..b87b81d0eda8 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -3,7 +3,7 @@ import annotation.capability object Test1: - def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream => T): T = + def usingLogFile[T](op: (local: caps.Cap) ?-> FileOutputStream => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -14,7 +14,7 @@ object Test1: object Test2: - def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream^{local} => T): T = + def usingLogFile[T](op: (local: caps.Cap) ?-> FileOutputStream^{local} => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -38,7 +38,7 @@ object Test2: object Test3: - def usingLogFile[T](op: (local: caps.Root) ?-> FileOutputStream^{local} => T) = + def usingLogFile[T](op: (local: caps.Cap) ?-> FileOutputStream^{local} => T) = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() @@ -50,7 +50,7 @@ object Test4: class Logger(f: OutputStream^): def log(msg: String): Unit = ??? - def usingFile[T](name: String, op: (local: caps.Root) ?-> OutputStream^{local} => T): T = + def usingFile[T](name: String, op: (local: caps.Cap) ?-> OutputStream^{local} => T): T = val f = new FileOutputStream(name) val result = op(f) f.close() @@ -62,7 +62,7 @@ object Test4: val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error later(1) - def usingLogger[T](f: OutputStream^, op: (local: caps.Root) ?-> Logger^{f} => T): T = + def usingLogger[T](f: OutputStream^, op: (local: caps.Cap) ?-> Logger^{f} => T): T = val logger = Logger(f) op(logger) diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 7328264bb90f..f8abe22e9d53 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -30,7 +30,7 @@ | Found: (x$0: String) ->{cap[scope]} String | Required: (x$0: String) ->? String | - | Note that reference (cap[scope] : caps.Root), defined at level 2 + | 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` @@ -40,7 +40,7 @@ | Found: (x$0: String) ->{cap[scope]} String | Required: String => String | - | Note that reference (cap[scope] : caps.Root), defined at level 2 + | 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` diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index 860babf68331..9b280a42a2f2 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -30,7 +30,7 @@ def test(cap1: Cap, cap2: Cap) = 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) - def local[T](op: (local: caps.Root) -> CC^{local} -> T): T = op(caps.cap)(CC()) + def local[T](op: (local: caps.Cap) -> CC^{local} -> T): T = op(caps.cap)(CC()) local { root => cap3 => // error def g(x: String): String = if cap3 == cap3 then "" else "a" diff --git a/tests/pos-special/stdlib/Test1.scala b/tests/pos-special/stdlib/Test1.scala index 437c739cc0b8..786e3aaa2bf1 100644 --- a/tests/pos-special/stdlib/Test1.scala +++ b/tests/pos-special/stdlib/Test1.scala @@ -6,7 +6,7 @@ import java.io.* object Test0: - def usingLogFile[T](op: (lcap: caps.Root) ?-> FileOutputStream^ => T): T = + def usingLogFile[T](op: (lcap: caps.Cap) ?-> FileOutputStream^ => T): T = val logFile = FileOutputStream("log") val result = op(logFile) logFile.close() diff --git a/tests/pos/dotty-experimental.scala b/tests/pos/dotty-experimental.scala index df2956ec2832..9cffddc0b8ba 100644 --- a/tests/pos/dotty-experimental.scala +++ b/tests/pos/dotty-experimental.scala @@ -3,6 +3,6 @@ import language.experimental.captureChecking object test { - val x: caps.Root = caps.cap + val x: caps.Cap = caps.cap } From 0a048fad3b9878932eadafc1a58b8d81dfa67939 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 1 Sep 2023 14:58:55 +0200 Subject: [PATCH 66/76] Add syntax `cap[qual]` for outer capture roots --- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 11 ++++ compiler/src/dotty/tools/dotc/ast/untpd.scala | 8 ++- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 63 ++++++++----------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 34 ++++++---- compiler/src/dotty/tools/dotc/cc/Setup.scala | 10 ++- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 16 ++++- library/src/scala/caps.scala | 2 + .../neg-custom-args/captures/localcaps.scala | 7 +++ tests/pos-custom-args/captures/pairs.scala | 15 +++++ 11 files changed, 114 insertions(+), 54 deletions(-) create mode 100644 tests/neg-custom-args/captures/localcaps.scala diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index e60d6e86754c..c569fe047b66 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -376,6 +376,17 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] => case _ => tree.tpe.isInstanceOf[ThisType] } + + /** Under capture checking, an extractor for qualified roots `cap[Q]`. + */ + object QualifiedRoot: + + def unapply(tree: Apply)(using Context): Option[String] = tree match + case Apply(fn, Literal(lit) :: Nil) if fn.symbol == defn.Caps_capIn => + Some(lit.value.asInstanceOf[String]) + case _ => + None + end QualifiedRoot } trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] => diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 8cc0750de53c..e7d38da854a4 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -149,7 +149,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case Floating } - /** {x1, ..., xN} T (only relevant under captureChecking) */ + /** {x1, ..., xN} T (only relevant under captureChecking) + * Created when parsing function types so that capture set and result type + * is combined in a single node. + */ case class CapturesAndResult(refs: List[Tree], parent: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree /** A type tree appearing somewhere in the untyped DefDef of a lambda, it will be typed using `tpFun`. @@ -512,6 +515,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def captureRoot(using Context): Select = Select(scalaDot(nme.caps), nme.CAPTURE_ROOT) + def captureRootIn(using Context): Select = + Select(scalaDot(nme.caps), nme.capIn) + def makeRetaining(parent: Tree, refs: List[Tree], annotName: TypeName)(using Context): Annotated = Annotated(parent, New(scalaAnnotationDot(annotName), List(refs))) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 2a860e42cfe0..7f7468675aae 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -103,9 +103,14 @@ end mapRoots extension (tree: Tree) /** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */ - def toCaptureRef(using Context): CaptureRef = tree.tpe match - case ref: CaptureRef => ref - case tpe => throw IllegalCaptureRef(tpe) + def toCaptureRef(using Context): CaptureRef = tree match + case QualifiedRoot(outer) => + ctx.owner.levelOwnerNamed(outer) + .orElse(defn.captureRoot) // non-existing outer roots are reported in Setup's checkQualifiedRoots + .localRoot.termRef + case _ => tree.tpe match + case ref: CaptureRef => ref + case tpe => throw IllegalCaptureRef(tpe) // if this was compiled from cc syntax, problem should have been reported at Typer /** Convert a @retains or @retainsByName annotation tree to the capture set it represents. * For efficience, the result is cached as an Attachment on the tree. @@ -266,39 +271,6 @@ extension (tp: Type) tp.tp1.isAlwaysPure && tp.tp2.isAlwaysPure case _ => false -/*!!! - def capturedLocalRoot(using Context): Symbol = - tp.captureSet.elems.toList - .filter(_.isLocalRootCapability) - .map(_.termSymbol) - .maxByOption(_.ccNestingLevel) - .getOrElse(NoSymbol) - - /** Remap roots defined in `cls` to the ... */ - def remapRoots(pre: Type, cls: Symbol)(using Context): Type = - if cls.isStaticOwner then tp - else - val from = - if cls.source == ctx.compilationUnit.source then cls.localRoot - else defn.captureRoot - mapRoots(from, capturedLocalRoot)(tp) - - - def containsRoot(root: Symbol)(using Context): Boolean = - val search = new TypeAccumulator[Boolean]: - def apply(x: Boolean, t: Type): Boolean = - if x then true - else t.dealias match - case t1: TermRef if t1.symbol == root => true - case t1: TypeRef if t1.classSymbol.hasAnnotation(defn.CapabilityAnnot) => true - case t1: MethodType => - !foldOver(x, t1.paramInfos) && this(x, t1.resType) - case t1 @ AppliedType(tycon, args) if defn.isFunctionSymbol(tycon.typeSymbol) => - val (inits, last :: Nil) = args.splitAt(args.length - 1): @unchecked - !foldOver(x, inits) && this(x, last) - case t1 => foldOver(x, t1) - search(false, tp) -*/ extension (cls: ClassSymbol) @@ -405,6 +377,7 @@ extension (sym: Symbol) case psyms :: _ => psyms.find(_.info.typeSymbol == defn.Caps_Cap).getOrElse(NoSymbol) case _ => NoSymbol + /** The local root corresponding to sym's level owner */ def localRoot(using Context): Symbol = val owner = sym.levelOwner assert(owner.exists) @@ -415,6 +388,24 @@ 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. diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a181b521ab52..0afd9137e6dd 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -138,12 +138,15 @@ object CheckCaptures: report.error(em"Singleton type $parent cannot have capture set", parent.srcPos) case _ => for elem <- retainedElems(ann) do - elem.tpe match - case ref: CaptureRef => - if !ref.isTrackableRef then - report.error(em"$elem cannot be tracked since it is not a parameter or local value", elem.srcPos) - case tpe => - report.error(em"$elem: $tpe is not a legal element of a capture set", elem.srcPos) + elem match + case QualifiedRoot(outer) => + // Will be checked by Setup's checkOuterRoots + case _ => elem.tpe match + case ref: CaptureRef => + if !ref.isTrackableRef then + report.error(em"$elem cannot be tracked since it is not a parameter or local value", elem.srcPos) + case tpe => + report.error(em"$elem: $tpe is not a legal element of a capture set", elem.srcPos) /** If `tp` is a capturing type, check that all references it mentions have non-empty * capture sets. Also: warn about redundant capture annotations. @@ -155,7 +158,7 @@ object CheckCaptures: 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", pos) + 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 @@ -166,11 +169,15 @@ object CheckCaptures: def warnIfRedundantCaptureSet(ann: Tree, tpt: Tree)(using Context): Unit = var retained = retainedElems(ann).toArray for i <- 0 until retained.length do - val ref = retained(i).toCaptureRef + val refTree = retained(i) + val ref = refTree.toCaptureRef val others = for j <- 0 until retained.length if j != i yield retained(j).toCaptureRef val remaining = CaptureSet(others*) if remaining.accountsFor(ref) then - val srcTree = if ann.span.exists then ann else tpt + val srcTree = + if refTree.span.exists then refTree + else if ann.span.exists then ann + else tpt report.warning(em"redundant capture: $remaining already accounts for $ref", srcTree.srcPos) /** Attachment key for bodies of closures, provided they are values */ @@ -1192,9 +1199,12 @@ class CheckCaptures extends Recheck, SymTransformer: def postCheck(unit: tpd.Tree)(using Context): Unit = val checker = new TreeTraverser: def traverse(tree: Tree)(using Context): Unit = - traverseChildren(tree) + val lctx = tree match + case _: DefTree | _: TypeDef if tree.symbol.exists => ctx.withOwner(tree.symbol) + case _ => ctx + traverseChildren(tree)(using lctx) check(tree) - def check(tree: Tree) = tree match + def check(tree: Tree)(using Context) = tree match case _: InferredTypeTree => case tree: TypeTree if !tree.span.isZeroExtent => tree.knownType.foreachPart { tp => @@ -1253,7 +1263,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => end check end checker - checker.traverse(unit) + checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) if !ctx.reporter.errorsReported then // 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 diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index dc32be5e8e53..3e9a9fe1274e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -217,6 +217,11 @@ extends tpd.TreeTraverser: 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) = @@ -226,12 +231,13 @@ extends tpd.TreeTraverser: if t2 ne t then return t2 t match case t @ AnnotatedType(t1, ann) => - val t2 = + checkQualifiedRoots(ann.tree) + val t3 = if ann.symbol == defn.RetainsAnnot && isCapabilityClassRef(t1) then t1 else this(t1) // Don't map capture sets, since that would implicitly normalize sets that // are not well-formed. - t.derivedAnnotatedType(t2, ann) + t.derivedAnnotatedType(t3, ann) case _ => mapOverFollowingAliases(t) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 1813e77aa4ee..4a9d4162107b 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -972,6 +972,7 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") @tu lazy val Caps_Cap: TypeSymbol = CapsModule.requiredType("Cap") + @tu lazy val Caps_capIn: TermSymbol = CapsModule.requiredMethod("capIn") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") @tu lazy val Caps_unsafeBox: Symbol = CapsUnsafeModule.requiredMethod("unsafeBox") diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 95c7c2cb2cd9..4fc7ea4185d8 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -434,6 +434,7 @@ object StdNames { val bytes: N = "bytes" val canEqual_ : N = "canEqual" val canEqualAny : N = "canEqualAny" + val capIn: N = "capIn" val caps: N = "caps" val captureChecking: N = "captureChecking" val checkInitialized: N = "checkInitialized" diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 93e858be904d..bd5159a60931 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1423,13 +1423,23 @@ object Parsers { case _ => None } - /** CaptureRef ::= ident | `this` + /** CaptureRef ::= ident | `this` | `cap` [`[` ident `]`] */ def captureRef(): Tree = if in.token == THIS then simpleRef() else termIdent() match - case Ident(nme.CAPTURE_ROOT) => captureRoot - case id => id + case id @ Ident(nme.CAPTURE_ROOT) => + if in.token == LBRACKET then + val ref = atSpan(id.span.start)(captureRootIn) + val qual = + inBrackets: + atSpan(in.offset): + Literal(Constant(ident().toString)) + atSpan(id.span.start)(Apply(ref, qual :: Nil)) + else + atSpan(id.span.start)(captureRoot) + case id => + id /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking */ diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index a23de5674476..2db66e8c540d 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -15,6 +15,8 @@ import annotation.experimental given Cap = cap + def capIn(scope: String): Cap = () + object unsafe: extension [T](x: T) diff --git a/tests/neg-custom-args/captures/localcaps.scala b/tests/neg-custom-args/captures/localcaps.scala new file mode 100644 index 000000000000..50cbe8e0f8f9 --- /dev/null +++ b/tests/neg-custom-args/captures/localcaps.scala @@ -0,0 +1,7 @@ +class 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 z2 = identity((x: Int) => (c: C^{cap[z2]}) => x) // error diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index 43488e2dde54..b78c10d30ef2 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -31,3 +31,18 @@ object Monomorphic: val x1c: Cap ->{c} Unit = x1 val y1 = p.snd val y1c: Cap ->{d} Unit = y1 + +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 + + 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 + val y1 = p.snd + val y1c: Cap ->{d} Unit = y1 From e8b8491d303b101ae14833609df777e48ec1c9f0 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 4 Sep 2023 18:23:58 +0200 Subject: [PATCH 67/76] Fix and add to tests --- compiler/src/dotty/tools/dotc/ast/TreeInfo.scala | 2 +- tests/neg/capt-wf.scala | 2 +- tests/pos-custom-args/captures/pairs.scala | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index c569fe047b66..4aaef28b9e1e 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -824,7 +824,7 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => case _ => tree } - /** An extractor for eta expanded `mdef` an eta-expansion of a method reference? To recognize this, we use + /** Is `mdef` an eta-expansion of a method reference? To recognize this, we use * the following criterion: A method definition is an eta expansion, if * it contains at least one term paramter, the parameter has a zero extent span, * and the right hand side is either an application or a closure with' diff --git a/tests/neg/capt-wf.scala b/tests/neg/capt-wf.scala index b1b375d12f55..0e1e4be2ca67 100644 --- a/tests/neg/capt-wf.scala +++ b/tests/neg/capt-wf.scala @@ -13,7 +13,7 @@ def test(c: Cap, other: String): Unit = val x3a: () -> String = s1 val s2 = () => if x1 == null then "" else "abc" val x4: C^{s2} = ??? // OK - val x5: C^{c, c} = ??? // error: redundant + val x5: C^{c, c} = ??? // error: redundant // error: redundant // val x6: C^{c}^{c} = ??? // would be syntax error val x7: Cap^{c} = ??? // error: redundant // val x8: C^{c}^{cap} = ??? // would be syntax error diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index b78c10d30ef2..78991e4377c0 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -38,6 +38,10 @@ object Monomorphic2: 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 () @@ -46,3 +50,4 @@ object Monomorphic2: val x1c: Cap ->{c} Unit = x1 val y1 = p.snd val y1c: Cap ->{d} Unit = y1 + From 9cb920694b987c19143de74e08ac6eb86bf47f2a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 4 Sep 2023 18:50:16 +0200 Subject: [PATCH 68/76] Address review comments --- compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala | 2 -- compiler/src/dotty/tools/dotc/cc/Setup.scala | 11 ++++++----- .../src/dotty/tools/dotc/util/SimpleIdentitySet.scala | 6 ++++-- tests/neg-custom-args/captures/box-unsoundness.scala | 6 ++++++ 4 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 tests/neg-custom-args/captures/box-unsoundness.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala index d2ed6d6978e8..4e1241518963 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRoot.scala @@ -108,7 +108,5 @@ object CaptureRoot: end isEnclosingRoot end CaptureRoot -//class LevelError(val rvar: RootVar, val newBound: Symbol, val isUpper: Boolean) extends Exception - diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 3e9a9fe1274e..bdc94f187e47 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -242,14 +242,15 @@ extends tpd.TreeTraverser: mapOverFollowingAliases(t) private def transformExplicitType(tp: Type, boxed: Boolean, mapRoots: Boolean)(using Context): Type = - val tp1 = expandAliases(if boxed then box(tp) else tp) + 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 - if tp2 ne tp then capt.println(i"expanded: $tp --> $tp2") - tp2 + val tp3 = if boxed then box(tp2) else tp2 + if tp3 ne tp then capt.println(i"expanded: $tp --> $tp3") + tp3 /** 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 = @@ -463,8 +464,8 @@ extends tpd.TreeTraverser: newInfo else new LazyType: def complete(denot: SymDenotation)(using Context) = - // infos other methods are determined from their definitions which - // are checked on depand + // infos of other methods are determined from their definitions which + // are checked on demand denot.info = newInfo recheckDef(tree, sym)) else updateOwner(sym) diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index d7e1a60f56fa..b243145c9e5f 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -142,8 +142,10 @@ object SimpleIdentitySet { val y0 = f(x0.asInstanceOf[Elem]) val y1 = f(x1.asInstanceOf[Elem]) val y2 = f(x2.asInstanceOf[Elem]) - if (y0 ne y1) && (y0 ne y2) && (y1 ne y2) then Set3(y0, y1, y2) - else super.map(f) + if y1 eq y0 then + if y2 eq y0 then Set1(y0) else Set2(y0, y2) + else if (y2 eq y0) || (y2 eq y1) then Set2(y0, y1) + else Set3(y0, y1, y2) def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]), x2.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: x2.asInstanceOf[Elem] :: Nil diff --git a/tests/neg-custom-args/captures/box-unsoundness.scala b/tests/neg-custom-args/captures/box-unsoundness.scala new file mode 100644 index 000000000000..e9436b7236cc --- /dev/null +++ b/tests/neg-custom-args/captures/box-unsoundness.scala @@ -0,0 +1,6 @@ +@annotation.capability class CanIO { def use(): Unit = () } +def use[X](x: X): (op: X -> Unit) -> Unit = op => op(x) +def test(io: CanIO): Unit = + val f = use[CanIO](io) + val g: () -> Unit = () => f(x => x.use()) // error + // was UNSOUND: g uses the capability io but has an empty capture set \ No newline at end of file From 9f5a466bd8a937d76dedd8bbb37d9c1598f2197b Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 4 Sep 2023 18:53:23 +0200 Subject: [PATCH 69/76] Add back generic new.test.scala --- tests/new/test.scala | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/new/test.scala diff --git a/tests/new/test.scala b/tests/new/test.scala new file mode 100644 index 000000000000..e6bfc29fd808 --- /dev/null +++ b/tests/new/test.scala @@ -0,0 +1,2 @@ +object Test: + def f: Any = 1 From cfb4c9ca2ec23a8aa982cab210fd736234ffe8d9 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 4 Sep 2023 20:22:20 +0200 Subject: [PATCH 70/76] Simplify setup of DefDef and ValDef nodes --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index bdc94f187e47..93535a24c9ea 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -320,22 +320,20 @@ extends tpd.TreeTraverser: def traverse(tree: Tree)(using Context): Unit = tree match - case tree: DefDef => + case tree @ DefDef(_, paramss, tpt: TypeTree, _) => if isExcluded(tree.symbol) then return inContext(ctx.withOwner(tree.symbol)): if tree.symbol.isAnonymousFunction && tree.symbol.definedLocalRoot.exists then // closures that define parameters of type caps.Cap count as level owners tree.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) - tree.tpt match - case tpt: TypeTree if tree.symbol.allOverriddenSymbols.hasNext => - tree.paramss.foreach(traverse) - transformTT(tpt, boxed = false, exact = true, mapRoots = true) - traverse(tree.rhs) - //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") - case _ => - traverseChildren(tree) - case tree @ ValDef(_, tpt: TypeTree, rhs) => + 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 @@ -345,7 +343,7 @@ extends tpd.TreeTraverser: case _: InferredTypeTree => false case _: TypeTree => containsCap(expandAliases(tree.tpe)) case _ => false - val mapRoots = rhs match + val mapRoots = tree.rhs match case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) // Toplevel closures bound to vals count as level owners From 5fcd9764d082b9de56422321de1c5356b0545f3c Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 5 Sep 2023 08:38:09 +0200 Subject: [PATCH 71/76] Make Cap a class --- library/src/scala/caps.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 2db66e8c540d..a32d27a5c28b 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -4,18 +4,18 @@ import annotation.experimental @experimental object caps: - opaque type Cap = Unit + class Cap // should be @erased /** The universal capture reference (deprecated) */ @deprecated("Use `cap` instead") - val `*`: Cap = () + val `*`: Cap = cap /** The universal capture reference */ - val cap: Cap = () + val cap: Cap = Cap() given Cap = cap - def capIn(scope: String): Cap = () + def capIn(scope: String): Cap = cap object unsafe: From 81de544995418862fae6dc7002c8596f764b2bd0 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 7 Sep 2023 17:07:25 +0200 Subject: [PATCH 72/76] Drop redundant expandThrowsAlias --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 93535a24c9ea..36558b0f5daf 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -124,8 +124,7 @@ extends tpd.TreeTraverser: isTopLevel = false try ts.mapConserve(this) finally isTopLevel = saved - def apply(t: Type) = - val tp = expandThrowsAlias(t) + def apply(tp: Type) = val tp1 = tp match case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => // Drop explicit retains annotations From 209d79314b60098bf41306231d58268347e0852e Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 7 Sep 2023 17:36:22 +0200 Subject: [PATCH 73/76] Normalize captures also for explicit types --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 36 +++++++++++--------- tests/neg-custom-args/captures/cc-glb.check | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 36558b0f5daf..bf6f3ba7710e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -237,8 +237,8 @@ extends tpd.TreeTraverser: // Don't map capture sets, since that would implicitly normalize sets that // are not well-formed. t.derivedAnnotatedType(t3, ann) - case _ => - mapOverFollowingAliases(t) + case t => + normalizeCaptures(mapOverFollowingAliases(t)) private def transformExplicitType(tp: Type, boxed: Boolean, mapRoots: Boolean)(using Context): Type = val tp1 = expandAliases(tp) @@ -560,10 +560,8 @@ extends tpd.TreeTraverser: false }.showing(i"can have inferred capture $tp = $result", capt) - /** 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 = tp match + /** Pull out an embedded capture set from a part of `tp` */ + def normalizeCaptures(tp: Type)(using Context): Type = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) case tp: RecType => @@ -575,13 +573,9 @@ extends tpd.TreeTraverser: // by `mapInferred`. Hence if the underlying type admits capture variables // a variable was already added, and the first case above would apply. case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(refs1.elems.isEmpty) - assert(refs2.elems.isEmpty) assert(tp1.isBoxed == tp2.isBoxed) CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => - assert(refs1.elems.isEmpty) - assert(refs2.elems.isEmpty) assert(tp1.isBoxed == tp2.isBoxed) CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => @@ -589,19 +583,27 @@ extends tpd.TreeTraverser: case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) case tp: LazyRef => - decorate(tp.ref, mapRoots, addedSet) - case _ if tp.typeSymbol == defn.FromJavaObjectSymbol => + normalizeCaptures(tp.ref) + case _ => + 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 decorate(tp: Type, mapRoots: Boolean, 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 - case _ => + else def maybeAdd(target: Type, fallback: Type) = if needsVariable(target) then CapturingType(target, addedSet(target)) else fallback - val tp1 = tp.dealiasKeepAnnots - if tp1 ne tp then + 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 tp) - else maybeAdd(tp, tp) + maybeAdd(tp2, if tp2 ne tp1 then tp2 else tp0) + else maybeAdd(tp0, tp0) /** Add a capture set variable to `tp` if necessary, or maybe pull out * an embedded capture set variable from a part of `tp`. diff --git a/tests/neg-custom-args/captures/cc-glb.check b/tests/neg-custom-args/captures/cc-glb.check index 7e0d2ff85691..669cf81a082b 100644 --- a/tests/neg-custom-args/captures/cc-glb.check +++ b/tests/neg-custom-args/captures/cc-glb.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-glb.scala:7:19 ---------------------------------------- 7 | val x2: Foo[T] = x1 // error | ^^ - | Found: (x1 : (Foo[T]^) & (Foo[Any]^{io})) + | Found: (x1 : (Foo[T] & Foo[Any])^{io}) | Required: Foo[T] | | longer explanation available when compiling with `-explain` From 13012c8df102c52c8f68fab286e8cf1dcfb97082 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 7 Sep 2023 17:54:35 +0200 Subject: [PATCH 74/76] Fix printing of some refined function types Some refined function types are of the form (A->B)^{cs} { def apply(x: A): B' = ... } In this case the capture set `cs` was previously dropped. Now it is printed. --- .../src/dotty/tools/dotc/printing/RefinedPrinter.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 26b7a3c12746..114037fd0bd0 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, toCaptureSet, IllegalCaptureRef, ccNestingLevelOpt} +import cc.{CaptureSet, CapturingType, toCaptureSet, IllegalCaptureRef, ccNestingLevelOpt} class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { @@ -268,7 +268,10 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { toText(tycon) case tp: RefinedType if defn.isFunctionType(tp) && !printDebug => toTextMethodAsFunction(tp.refinedInfo, - isPure = Feature.pureFunsEnabled && !tp.typeSymbol.name.isImpureFunction) + isPure = Feature.pureFunsEnabled && !tp.typeSymbol.name.isImpureFunction, + refs = tp.parent match + case CapturingType(_, cs) => toTextCaptureSet(cs) + case _ => "") case tp: TypeRef => if (tp.symbol.isAnonymousClass && !showUniqueIds) toText(tp.info) From bc522cc6c02d1e0dba67b7ac74f6d5a37883601d Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 7 Sep 2023 20:26:44 +0200 Subject: [PATCH 75/76] Refactor determination of level owners Do it in setup instead of on demand. Advantage: no need to determine level ownership for externally compiled symbols. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 19 ++--- .../dotty/tools/dotc/cc/CheckCaptures.scala | 21 ++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 84 +++++++++++++++---- .../captures/usingLogFile-alt.check | 2 +- 4 files changed, 84 insertions(+), 42 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7f7468675aae..dfaa3c701576 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -42,6 +42,10 @@ class IllegalCaptureRef(tpe: Type) extends Exception /** Capture checking state, which is stored in a context property */ class CCState: + val rhsClosure: mutable.HashSet[Symbol] = new mutable.HashSet + + val levelOwners: mutable.HashSet[Symbol] = new mutable.HashSet + /** Associates certain symbols (the nesting level owners) with their ccNestingLevel */ val nestingLevels: mutable.HashMap[Symbol, Int] = new mutable.HashMap @@ -326,17 +330,7 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox - def isLevelOwner(using Context): Boolean = - def isCaseClassSynthetic = - sym.owner.isClass && sym.owner.is(Case) && sym.is(Synthetic) && sym.info.firstParamNames.isEmpty - if sym.isClass then true - else if sym.is(Method) then - if sym.isAnonymousFunction then - // Setup added anonymous functions counting as level owners to nestingLevels - ccState.nestingLevels.contains(sym) - else - !sym.isConstructor && !isCaseClassSynthetic - else false + def isLevelOwner(using Context): Boolean = ccState.levelOwners.contains(sym) /** The owner of the current level. Qualifying owners are * - methods other than constructors and anonymous functions @@ -366,9 +360,6 @@ extension (sym: Symbol) def ccNestingLevelOpt(using Context): Option[Int] = if ctx.property(ccStateKey).isDefined then Some(ccNestingLevel) else None - def setNestingLevel(level: Int)(using Context): Unit = - ccState.nestingLevels(sym) = level - /** The parameter with type caps.Cap in the leading term parameter section, * or NoSymbol, if none exists. */ diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 0afd9137e6dd..d5bd8522ca92 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1265,15 +1265,16 @@ class CheckCaptures extends Recheck, SymTransformer: end checker checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) if !ctx.reporter.errorsReported then - // 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) + //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) 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 bf6f3ba7710e..adaa7219d68b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -206,13 +206,13 @@ extends tpd.TreeTraverser: else fntpe case _ => tp - def isCapabilityClassRef(tp: Type)(using Context) = tp match + 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 isCapabilityClassRef(tp) + if tp.isCapabilityClassRef then CapturingType(tp, CaptureSet.universal, boxed = false) else tp @@ -232,7 +232,7 @@ extends tpd.TreeTraverser: case t @ AnnotatedType(t1, ann) => checkQualifiedRoots(ann.tree) val t3 = - if ann.symbol == defn.RetainsAnnot && isCapabilityClassRef(t1) then t1 + 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. @@ -317,21 +317,61 @@ extends tpd.TreeTraverser: 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, _) => - if isExcluded(tree.symbol) then + val meth = tree.symbol + if isExcluded(meth) then return - inContext(ctx.withOwner(tree.symbol)): - if tree.symbol.isAnonymousFunction && tree.symbol.definedLocalRoot.exists then - // closures that define parameters of type caps.Cap count as level owners - tree.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) + + def isCaseClassSynthetic = // TODO drop + meth.owner.isClass && meth.owner.is(Case) && meth.is(Synthetic) && meth.info.firstParamNames.isEmpty + + inContext(ctx.withOwner(meth)): + val canHaveLocalRoot = + if meth.isAnonymousFunction then + ccState.rhsClosure.remove(meth) + || meth.definedLocalRoot.exists // TODO drop + else !meth.isConstructor && !isCaseClassSynthetic + if canHaveLocalRoot && meth.takesCappedParam then + //println(i"level owner: $meth") + ccState.levelOwners += meth paramss.foreach(traverse) transformTT(tpt, boxed = false, exact = tree.symbol.allOverriddenSymbols.hasNext, mapRoots = true) traverse(tree.rhs) //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") + case tree @ ValDef(_, tpt: TypeTree, _) => def containsCap(tp: Type) = tp.existsPart: case CapturingType(_, refs) => refs.isUniversal @@ -342,9 +382,12 @@ extends tpd.TreeTraverser: case _: InferredTypeTree => false case _: TypeTree => containsCap(expandAliases(tree.tpe)) case _ => false + + val sym = tree.symbol val mapRoots = tree.rhs match case possiblyTypedClosureDef(ddef) if !mentionsCap(rhsOfEtaExpansion(ddef)) => - ddef.symbol.setNestingLevel(ctx.owner.nestingLevel + 1) + //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 @@ -354,22 +397,29 @@ extends tpd.TreeTraverser: // in this case roots in inferred val type count as polymorphic case _ => true - transformTT(tpt, - boxed = tree.symbol.is(Mutable), // types of mutable variables are boxed - exact = tree.symbol.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) + transformTT(tpt, + boxed = sym.is(Mutable), // types of mutable variables are boxed + exact = sym.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set + mapRoots + ) + capt.println(i"mapped $tree = ${tpt.knownType}") + traverse(tree.rhs) + case tree @ TypeApply(fn, args) => traverse(fn) for case arg: TypeTree <- args do transformTT(arg, boxed = true, exact = false, mapRoots = true) // type arguments in type applications are boxed + case tree: Template => - inContext(ctx.withOwner(tree.symbol.owner)): + 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) diff --git a/tests/neg-custom-args/captures/usingLogFile-alt.check b/tests/neg-custom-args/captures/usingLogFile-alt.check index 81ba7f866bc0..93fc3ca5edb5 100644 --- a/tests/neg-custom-args/captures/usingLogFile-alt.check +++ b/tests/neg-custom-args/captures/usingLogFile-alt.check @@ -3,7 +3,7 @@ | ^^^^^^^^^ | 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 4 + | 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 | | where: x$0 is a reference to a value parameter From 4a459393d74dbcad2a49ddae222ea0d0c5cec1c8 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 8 Sep 2023 17:44:51 +0200 Subject: [PATCH 76/76] Fix printing of nesting levels --- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 6a30329fd675..83ff03e05592 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -349,7 +349,10 @@ class PlainPrinter(_ctx: Context) extends Printer { */ protected def idString(sym: Symbol): String = (if (showUniqueIds || Printer.debugPrintUnique) "#" + sym.id else "") + - (if (showNestingLevel) "%" + sym.nestingLevel else "") + (if showNestingLevel then + if ctx.phase == Phases.checkCapturesPhase then "%" + sym.ccNestingLevel + else "%" + sym.nestingLevel + else "") def nameString(sym: Symbol): String = simpleNameString(sym) + idString(sym) // + "<" + (if (sym.exists) sym.owner else "") + ">"