From e77657f179906c9a47614a3930aec0824441688f Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Jun 2024 13:40:33 +0200 Subject: [PATCH 01/35] More robust level handling --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 49 +++++++++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 57 +++++------ .../dotty/tools/dotc/cc/CheckCaptures.scala | 32 ++++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 96 +++++++++++-------- .../tools/dotc/printing/PlainPrinter.scala | 2 +- .../dotty/tools/dotc/transform/Recheck.scala | 2 +- tests/neg-custom-args/captures/levels.check | 2 +- .../neg-custom-args/captures/outer-var.check | 2 +- tests/neg-custom-args/captures/reaches.check | 2 +- tests/neg-custom-args/captures/vars.check | 4 +- tests/printing/dependent-annot.check | 7 +- 11 files changed, 152 insertions(+), 103 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index c272183b6dfb..88f5e7d52867 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -14,6 +14,7 @@ import tpd.* import StdNames.nme import config.Feature import collection.mutable +import CCState.* private val Captures: Key[CaptureSet] = Key() @@ -64,11 +65,47 @@ class CCState: */ var levelError: Option[CaptureSet.CompareResult.LevelError] = None + private var curLevel: Level = outermostLevel + private val symLevel: mutable.Map[Symbol, Int] = mutable.Map() + +object CCState: + + opaque type Level = Int + + val undefinedLevel: Level = -1 + + val outermostLevel: Level = 0 + + /** The level of the current environment. Levels start at 0 and increase for + * each nested function or class. -1 means the level is undefined. + */ + def currentLevel(using Context): Level = ccState.curLevel + + inline def inNestedLevel[T](inline op: T)(using Context): T = + val ccs = ccState + val saved = ccs.curLevel + ccs.curLevel = ccs.curLevel.nextInner + try op finally ccs.curLevel = saved + + inline def inNestedLevelUnless[T](inline p: Boolean)(inline op: T)(using Context): T = + val ccs = ccState + val saved = ccs.curLevel + if !p then ccs.curLevel = ccs.curLevel.nextInner + try op finally ccs.curLevel = saved + + extension (x: Level) + def isDefined: Boolean = x >= 0 + def <= (y: Level) = (x: Int) <= y + def nextInner: Level = if isDefined then x + 1 else x + + extension (sym: Symbol)(using Context) + def ccLevel: Level = ccState.symLevel.getOrElse(sym, -1) + def recordLevel() = ccState.symLevel(sym) = currentLevel end CCState /** The currently valid CCState */ def ccState(using Context) = - Phases.checkCapturesPhase.asInstanceOf[CheckCaptures].ccState + Phases.checkCapturesPhase.asInstanceOf[CheckCaptures].ccState1 class NoCommonRoot(rs: Symbol*)(using Context) extends Exception( i"No common capture root nested in ${rs.mkString(" and ")}" @@ -339,6 +376,12 @@ extension (tp: Type) case _ => tp + def level(using Context): Level = + tp match + case tp: TermRef => tp.symbol.ccLevel + case tp: ThisType => tp.cls.ccLevel.nextInner + case _ => undefinedLevel + extension (cls: ClassSymbol) def pureBaseClass(using Context): Option[Symbol] = @@ -423,9 +466,7 @@ extension (sym: Symbol) || sym.is(Method, butNot = Accessor) /** 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.Capability, or they are the rhs of a val definition. + * - methods, other than accessors * - classes, if they are not staticOwners * - _root_ */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index f78ed1a91bd6..5db8dadf5b66 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -16,6 +16,7 @@ import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda import util.common.alwaysTrue import scala.collection.mutable +import CCState.* /** A class for capture sets. Capture sets can be constants or variables. * Capture sets support inclusion constraints <:< where <:< is subcapturing. @@ -55,10 +56,14 @@ sealed abstract class CaptureSet extends Showable: */ def isAlwaysEmpty: Boolean - /** An optional level limit, or NoSymbol if none exists. All elements of the set - * must be in scopes visible from the level limit. + /** An optional level limit, or undefinedLevel if none exists. All elements of the set + * must be at levels equal or smaller than the level of the set, if it is defined. */ - def levelLimit: Symbol + def level: Level + + /** An optional owner, or NoSymbol if none exists. Used for diagnstics + */ + def owner: Symbol /** Is this capture set definitely non-empty? */ final def isNotEmpty: Boolean = !elems.isEmpty @@ -239,9 +244,7 @@ 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.levelLimit.maxNested(that.levelLimit, onConflict = (sym1, sym2) => sym1), - this.elems ++ that.elems) + else Var(initialElems = this.elems ++ that.elems) .addAsDependentTo(this).addAsDependentTo(that) /** The smallest superset (via <:<) of this capture set that also contains `ref`. @@ -411,7 +414,9 @@ object CaptureSet: def withDescription(description: String): Const = Const(elems, description) - def levelLimit = NoSymbol + def level = undefinedLevel + + def owner = NoSymbol override def toString = elems.toString end Const @@ -431,7 +436,7 @@ object CaptureSet: end Fluid /** The subclass of captureset variables with given initial elements */ - class Var(directOwner: Symbol, initialElems: Refs = emptySet)(using @constructorOnly ictx: Context) extends CaptureSet: + class Var(override val owner: Symbol = NoSymbol, initialElems: Refs = emptySet, val level: Level = undefinedLevel, underBox: Boolean = false)(using @constructorOnly ictx: Context) extends CaptureSet: /** A unique identification number for diagnostics */ val id = @@ -440,9 +445,6 @@ object CaptureSet: //assert(id != 40) - override val levelLimit = - if directOwner.exists then directOwner.levelOwner else NoSymbol - /** A variable is solved if it is aproximated to a from-then-on constant set. */ private var isSolved: Boolean = false @@ -516,12 +518,10 @@ object CaptureSet: private def levelOK(elem: CaptureRef)(using Context): Boolean = if elem.isRootCapability then !noUniversal else elem match - case elem: TermRef if levelLimit.exists => - var sym = elem.symbol - if sym.isLevelOwner then sym = sym.owner - levelLimit.isContainedIn(sym.levelOwner) - case elem: ThisType if levelLimit.exists => - levelLimit.isContainedIn(elem.cls.levelOwner) + case elem: TermRef if level.isDefined => + elem.symbol.ccLevel <= level + case elem: ThisType if level.isDefined => + elem.cls.ccLevel.nextInner <= level case ReachCapability(elem1) => levelOK(elem1) case MaybeCapability(elem1) => @@ -599,8 +599,8 @@ object CaptureSet: val debugInfo = if !isConst && ctx.settings.YccDebug.value then ids else "" val limitInfo = - if ctx.settings.YprintLevel.value && levelLimit.exists - then i"" + if ctx.settings.YprintLevel.value && level.isDefined + then i"" else "" debugInfo ++ limitInfo @@ -619,13 +619,6 @@ object CaptureSet: override def toString = s"Var$id$elems" end Var - /** Variables that represent refinements of class parameters can have the universal - * capture set, since they represent only what is the result of the constructor. - * Test case: Without that tweak, logger.scala would not compile. - */ - class RefiningVar(directOwner: Symbol)(using Context) extends Var(directOwner): - override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context) = this - /** 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): @@ -654,7 +647,7 @@ object CaptureSet: */ class Mapped private[CaptureSet] (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) - extends DerivedVar(source.levelLimit, initial.elems): + extends DerivedVar(source.owner, initial.elems): addAsDependentTo(initial) // initial mappings could change by propagation private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap] @@ -751,7 +744,7 @@ object CaptureSet: */ final class BiMapped private[CaptureSet] (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) - extends DerivedVar(source.levelLimit, initialElems): + extends DerivedVar(source.owner, initialElems): override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = if origin eq source then @@ -785,7 +778,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.levelLimit, source.elems.filter(p)): + extends DerivedVar(source.owner, source.elems.filter(p)): override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = if accountsFor(elem) then @@ -815,7 +808,7 @@ object CaptureSet: extends Filtered(source, !other.accountsFor(_)) class Intersected(cs1: CaptureSet, cs2: CaptureSet)(using Context) - extends Var(cs1.levelLimit.minNested(cs2.levelLimit), elemIntersection(cs1, cs2)): + extends Var(initialElems = elemIntersection(cs1, cs2)): addAsDependentTo(cs1) addAsDependentTo(cs2) deps += cs1 @@ -905,7 +898,7 @@ object CaptureSet: if ctx.settings.YccDebug.value then printer.toText(trace, ", ") else blocking.show case LevelError(cs: CaptureSet, elem: CaptureRef) => - Str(i"($elem at wrong level for $cs in ${cs.levelLimit})") + Str(i"($elem at wrong level for $cs at level ${cs.level.toString})") /** The result is OK */ def isOK: Boolean = this == OK @@ -1148,6 +1141,6 @@ object CaptureSet: i""" | |Note that reference ${ref}$levelStr - |cannot be included in outer capture set $cs which is associated with ${cs.levelLimit}""" + |cannot be included in outer capture set $cs""" end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index e41f32cab672..c36b0cbf552e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -19,6 +19,7 @@ import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* import scala.collection.mutable import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult} +import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} import reporting.trace @@ -191,7 +192,7 @@ class CheckCaptures extends Recheck, SymTransformer: if Feature.ccEnabled then super.run - val ccState = new CCState + val ccState1 = new CCState // Dotty problem: Rename to ccState ==> Crash in ExplicitOuter class CaptureChecker(ictx: Context) extends Rechecker(ictx): @@ -311,7 +312,7 @@ class CheckCaptures extends Recheck, SymTransformer: def capturedVars(sym: Symbol)(using Context): CaptureSet = myCapturedVars.getOrElseUpdate(sym, if sym.ownersIterator.exists(_.isTerm) - then CaptureSet.Var(sym.owner) + then CaptureSet.Var(sym.owner, level = sym.ccLevel) else CaptureSet.empty) /** For all nested environments up to `limit` or a closed environment perform `op`, @@ -592,6 +593,9 @@ class CheckCaptures extends Recheck, SymTransformer: tree.srcPos) super.recheckTypeApply(tree, pt) + override def recheckBlock(tree: Block, pt: Type)(using Context): Type = + inNestedLevel(super.recheckBlock(tree, pt)) + 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") @@ -695,13 +699,14 @@ class CheckCaptures extends Recheck, SymTransformer: val localSet = capturedVars(sym) if !localSet.isAlwaysEmpty then curEnv = Env(sym, EnvKind.Regular, localSet, curEnv) - try checkInferredResult(super.recheckDefDef(tree, sym), tree) - finally - if !sym.isAnonymousFunction then - // Anonymous functions propagate their type to the enclosing environment - // so it is not in general sound to interpolate their types. - interpolateVarsIn(tree.tpt) - curEnv = saved + inNestedLevel: + try checkInferredResult(super.recheckDefDef(tree, sym), tree) + finally + if !sym.isAnonymousFunction then + // Anonymous functions propagate their type to the enclosing environment + // so it is not in general sound to interpolate their types. + interpolateVarsIn(tree.tpt) + curEnv = saved /** If val or def definition with inferred (result) type is visible * in other compilation units, check that the actual inferred type @@ -771,7 +776,8 @@ class CheckCaptures extends Recheck, SymTransformer: checkSubset(thisSet, CaptureSet.empty.withDescription(i"of pure base class $pureBase"), selfType.srcPos, cs1description = " captured by this self type") - super.recheckClassDef(tree, impl, cls) + inNestedLevelUnless(cls.is(Module)): + super.recheckClassDef(tree, impl, cls) finally curEnv = saved @@ -823,9 +829,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.owner), curEnv) + curEnv = Env(curEnv.owner, EnvKind.Boxed, CaptureSet.Var(curEnv.owner, level = currentLevel), curEnv) case _ if tree.hasAttachment(ClosureBodyValue) => - curEnv = Env(curEnv.owner, EnvKind.ClosureResult, CaptureSet.Var(curEnv.owner), curEnv) + curEnv = Env(curEnv.owner, EnvKind.ClosureResult, CaptureSet.Var(curEnv.owner, level = currentLevel), curEnv) case _ => val res = try @@ -995,7 +1001,7 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv curEnv = Env( curEnv.owner, EnvKind.NestedInOwner, - CaptureSet.Var(curEnv.owner), + CaptureSet.Var(curEnv.owner, level = currentLevel), if boxed then null else curEnv) try val (eargs, eres) = expected.dealias.stripCapturing match diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 0175d40c186c..2c0cdfb7b129 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -16,6 +16,7 @@ import Synthetics.isExcluded import util.Property import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable +import CCState.* /** Operations accessed from CheckCaptures */ trait SetupAPI: @@ -189,7 +190,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val getterType = mapInferred(refine = false)(tp.memberInfo(getter)).strippedDealias RefinedType(core, getter.name, - CapturingType(getterType, CaptureSet.RefiningVar(ctx.owner))) + CapturingType(getterType, + new CaptureSet.Var(ctx.owner): + override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context) = this + )) .showing(i"add capture refinement $tp --> $result", capt) else core @@ -402,14 +406,17 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if isExcluded(meth) then return - inContext(ctx.withOwner(meth)): - paramss.foreach(traverse) - transformResultType(tpt, meth) - traverse(tree.rhs) - //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") + meth.recordLevel() + inNestedLevel: + inContext(ctx.withOwner(meth)): + paramss.foreach(traverse) + transformResultType(tpt, meth) + traverse(tree.rhs) + //println(i"TYPE of ${tree.symbol.showLocated} = ${tpt.knownType}") case tree @ ValDef(_, tpt: TypeTree, _) => val sym = tree.symbol + sym.recordLevel() val defCtx = if sym.isOneOf(TermParamOrAccessor) then ctx else ctx.withOwner(sym) inContext(defCtx): transformResultType(tpt, sym) @@ -426,13 +433,19 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: transformTT(arg, boxed = true, exact = false) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => - inContext(ctx.withOwner(tree.symbol)): - traverseChildren(tree) + val sym = tree.symbol + sym.recordLevel() + inNestedLevelUnless(sym.is(Module)): + inContext(ctx.withOwner(sym)) + traverseChildren(tree) case tree @ SeqLiteral(elems, tpt: TypeTree) => traverse(elems) tpt.rememberType(box(transformInferredType(tpt.tpe))) + case tree: Block => + inNestedLevel(traverseChildren(tree)) + case _ => traverseChildren(tree) postProcess(tree) @@ -531,36 +544,37 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree: TypeDef => tree.symbol match case cls: ClassSymbol => - val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - def innerModule = cls.is(ModuleClass) && !cls.isStatic - val selfInfo1 = - if (selfInfo ne NoType) && !innerModule then - // if selfInfo is explicitly given then use that one, except if - // self info applies to non-static modules, these still need to be inferred - selfInfo - else if cls.isPureClass then - // is cls is known to be pure, nothing needs to be added to self type - selfInfo - else if !cls.isEffectivelySealed && !cls.baseClassHasExplicitSelfType then - // assume {cap} for completely unconstrained self types of publicly extensible classes - CapturingType(cinfo.selfType, CaptureSet.universal) - else - // Infer the self type for the rest, which is all classes without explicit - // self types (to which we also add nested module classes), provided they are - // neither pure, nor are publicily extensible with an unconstrained self type. - CapturingType(cinfo.selfType, CaptureSet.Var(cls)) - val ps1 = inContext(ctx.withOwner(cls)): - ps.mapConserve(transformExplicitType(_)) - if (selfInfo1 ne selfInfo) || (ps1 ne ps) then - val newInfo = ClassInfo(prefix, cls, ps1, decls, selfInfo1) - updateInfo(cls, newInfo) - capt.println(i"update class info of $cls with parents $ps selfinfo $selfInfo to $newInfo") - cls.thisType.asInstanceOf[ThisType].invalidateCaches() - if cls.is(ModuleClass) then - // if it's a module, the capture set of the module reference is the capture set of the self type - val modul = cls.sourceModule - updateInfo(modul, CapturingType(modul.info, selfInfo1.asInstanceOf[Type].captureSet)) - modul.termRef.invalidateCaches() + inNestedLevelUnless(cls.is(Module)): + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo + def innerModule = cls.is(ModuleClass) && !cls.isStatic + val selfInfo1 = + if (selfInfo ne NoType) && !innerModule then + // if selfInfo is explicitly given then use that one, except if + // self info applies to non-static modules, these still need to be inferred + selfInfo + else if cls.isPureClass then + // is cls is known to be pure, nothing needs to be added to self type + selfInfo + else if !cls.isEffectivelySealed && !cls.baseClassHasExplicitSelfType then + // assume {cap} for completely unconstrained self types of publicly extensible classes + CapturingType(cinfo.selfType, CaptureSet.universal) + else + // Infer the self type for the rest, which is all classes without explicit + // self types (to which we also add nested module classes), provided they are + // neither pure, nor are publicily extensible with an unconstrained self type. + CapturingType(cinfo.selfType, CaptureSet.Var(cls, level = currentLevel)) + val ps1 = inContext(ctx.withOwner(cls)): + ps.mapConserve(transformExplicitType(_)) + if (selfInfo1 ne selfInfo) || (ps1 ne ps) then + val newInfo = ClassInfo(prefix, cls, ps1, decls, selfInfo1) + updateInfo(cls, newInfo) + capt.println(i"update class info of $cls with parents $ps selfinfo $selfInfo to $newInfo") + cls.thisType.asInstanceOf[ThisType].invalidateCaches() + if cls.is(ModuleClass) then + // if it's a module, the capture set of the module reference is the capture set of the self type + val modul = cls.sourceModule + updateInfo(modul, CapturingType(modul.info, selfInfo1.asInstanceOf[Type].captureSet)) + modul.termRef.invalidateCaches() case _ => case _ => end postProcess @@ -672,11 +686,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Add a capture set variable to `tp` if necessary, or maybe pull out * an embedded capture set variable from a part of `tp`. */ - def addVar(tp: Type, owner: Symbol)(using Context): Type = + private def addVar(tp: Type, owner: Symbol)(using Context): Type = decorate(tp, addedSet = _.dealias.match - case CapturingType(_, refs) => CaptureSet.Var(owner, refs.elems) - case _ => CaptureSet.Var(owner)) + case CapturingType(_, refs) => CaptureSet.Var(owner, refs.elems, level = currentLevel) + case _ => CaptureSet.Var(owner, level = currentLevel)) def setupUnit(tree: Tree, recheckDef: DefRecheck)(using Context): Unit = setupTraverser(recheckDef).traverse(tree)(using ctx.withPhase(thisPhase)) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index c06b43cafe17..71ebb7054000 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -15,7 +15,7 @@ import util.SourcePosition import scala.util.control.NonFatal import scala.annotation.switch import config.{Config, Feature} -import cc.{CapturingType, RetainingType, CaptureSet, ReachCapability, MaybeCapability, isBoxed, levelOwner, retainedElems, isRetainsLike} +import cc.{CapturingType, RetainingType, CaptureSet, ReachCapability, MaybeCapability, isBoxed, retainedElems, isRetainsLike} class PlainPrinter(_ctx: Context) extends Printer { diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 79dfe3393578..f025c9e9369f 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -264,7 +264,7 @@ abstract class Recheck extends Phase, SymTransformer: def recheckClassDef(tree: TypeDef, impl: Template, sym: ClassSymbol)(using Context): Type = recheck(impl.constr) - impl.parentsOrDerived.foreach(recheck(_)) + impl.parents.foreach(recheck(_)) recheck(impl.self) recheckStats(impl.body) sym.typeRef diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index a5f8d73ccf7a..479a231a0404 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -12,6 +12,6 @@ | Required: box (x$0: String) ->? String | | Note that reference (cap3 : CC^), defined in method scope - | cannot be included in outer capture set ? of value r which is associated with method test2 + | cannot be included in outer capture set ? of value r | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index b9f1f57be769..d57d615cda64 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -32,7 +32,7 @@ | Required: () ->{p} Unit | | Note that reference (q : Proc), defined in method inner - | cannot be included in outer capture set {p} of variable y which is associated with method test + | cannot be included in outer capture set {p} of variable y | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/outer-var.scala:16:53 --------------------------------------------------------- diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index a1c5a56369e9..ccd9e891380b 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -12,7 +12,7 @@ | Required: box List[box () ->{xs*} Unit]^? | | Note that reference (f : File^), defined in method $anonfun - | cannot be included in outer capture set {xs*} of value cur which is associated with method runAll1 + | cannot be included in outer capture set {xs*} of value cur | | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/reaches.scala:35:6 ------------------------------------------------------------ diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 22d13d8e26e7..e2d817f2d8bd 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -4,7 +4,7 @@ | reference (cap3 : Cap) is not included in the allowed capture set {cap1} of variable a | | Note that reference (cap3 : Cap), defined in method scope - | cannot be included in outer capture set {cap1} of variable a which is associated with method test + | cannot be included in outer capture set {cap1} of variable a -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:23:8 ------------------------------------------ 23 | a = g // error | ^ @@ -12,7 +12,7 @@ | Required: (x$0: String) ->{cap1} String | | Note that reference (cap3 : Cap), defined in method scope - | cannot be included in outer capture set {cap1} of variable a which is associated with method test + | cannot be included in outer capture set {cap1} of variable a | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:12 ----------------------------------------- diff --git a/tests/printing/dependent-annot.check b/tests/printing/dependent-annot.check index a8a7e8b0bfee..f2dd0f702884 100644 --- a/tests/printing/dependent-annot.check +++ b/tests/printing/dependent-annot.check @@ -11,12 +11,7 @@ package { def f(y: C, z: C): Unit = { def g(): C @ann([y,z : Any]*) = ??? - val ac: - (C => Array[String]) - { - def apply(x: C): Array[String @ann([x : Any]*)] - } - = ??? + val ac: (x: C) => Array[String @ann([x : Any]*)] = ??? val dc: Array[String] = ac.apply(g()) () } From 8e2371256130edc791632fe30a9b1c546779e0f5 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Jun 2024 13:42:20 +0200 Subject: [PATCH 02/35] Add existential capabilities, 2nd draft --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 29 ++ .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 + .../dotty/tools/dotc/cc/CheckCaptures.scala | 13 +- .../src/dotty/tools/dotc/cc/Existential.scala | 366 ++++++++++++++++++ compiler/src/dotty/tools/dotc/cc/Setup.scala | 31 +- .../src/dotty/tools/dotc/config/Config.scala | 2 +- .../dotty/tools/dotc/core/Definitions.scala | 13 +- .../src/dotty/tools/dotc/core/NameKinds.scala | 1 + .../dotty/tools/dotc/core/TypeComparer.scala | 97 ++++- .../tools/dotc/printing/RefinedPrinter.scala | 4 +- library/src/scala/caps.scala | 6 + tests/neg-custom-args/captures/reaches.check | 8 +- tests/neg-custom-args/captures/reaches.scala | 7 +- tests/neg/cc-ex-conformance.scala | 25 ++ tests/new/test.scala | 9 +- tests/pos/cc-ex-unpack.scala | 18 + 16 files changed, 580 insertions(+), 51 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/Existential.scala create mode 100644 tests/neg/cc-ex-conformance.scala create mode 100644 tests/pos/cc-ex-unpack.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 88f5e7d52867..080577b5773f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -26,6 +26,8 @@ object ccConfig: */ inline val allowUnsoundMaps = false + val useExistentials = false + /** If true, use `sealed` as encapsulation mechanism instead of the * previous global retriction that `cap` can't be boxed or unboxed. */ @@ -532,6 +534,33 @@ object ReachCapability extends AnnotatedCapability(defn.ReachCapabilityAnnot) */ object MaybeCapability extends AnnotatedCapability(defn.MaybeCapabilityAnnot) +/** Offers utility method to be used for type maps that follow aliases */ +trait ConservativeFollowAliasMap(using Context) extends TypeMap: + + /** If `mapped` is a type alias, apply the map to the alias, while keeping + * annotations. If the result is different, return it, otherwise return `mapped`. + * Furthermore, if `original` is a LazyRef or TypeVar and the mapped result is + * the same as the underlying type, keep `original`. This avoids spurious differences + * which would lead to spurious dealiasing in the result + */ + protected def applyToAlias(original: Type, mapped: Type) = + val mapped1 = mapped match + case t: (TypeRef | AppliedType) => + val t1 = t.dealiasKeepAnnots + if t1 eq t then t + else + // If we see a type alias, map the alias type and keep it if it's different + val t2 = apply(t1) + if t2 ne t1 then t2 else t + case _ => + mapped + original match + case original: (LazyRef | TypeVar) if mapped1 eq original.underlying => + original + case _ => + mapped1 +end ConservativeFollowAliasMap + /** An extractor for all kinds of function types as well as method and poly types. * @return 1st half: The argument types or empty if this is a type function * 2nd half: The result type diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 5db8dadf5b66..cf803e47eca0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -14,6 +14,7 @@ import printing.{Showable, Printer} import printing.Texts.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda +import TypeComparer.canSubsumeExistentially import util.common.alwaysTrue import scala.collection.mutable import CCState.* @@ -172,6 +173,7 @@ sealed abstract class CaptureSet extends Showable: x.info match case x1: CaptureRef => x1.subsumes(y) case _ => false + case x: TermParamRef => canSubsumeExistentially(x, y) case _ => false /** {x} <:< this where <:< is subcapturing, but treating all variables diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c36b0cbf552e..e6e091cd5897 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -612,10 +612,10 @@ class CheckCaptures extends Recheck, SymTransformer: mdef.rhs.putAttachment(ClosureBodyValue, ()) case _ => - // Constrain closure's parameters and result from the expected type before - // rechecking the body. openClosures = (mdef.symbol, pt) :: openClosures try + // Constrain closure's parameters and result from the expected type before + // rechecking the body. 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 @@ -699,7 +699,7 @@ class CheckCaptures extends Recheck, SymTransformer: val localSet = capturedVars(sym) if !localSet.isAlwaysEmpty then curEnv = Env(sym, EnvKind.Regular, localSet, curEnv) - inNestedLevel: + inNestedLevel: // TODO: needed here? try checkInferredResult(super.recheckDefDef(tree, sym), tree) finally if !sym.isAnonymousFunction then @@ -920,8 +920,7 @@ class CheckCaptures extends Recheck, SymTransformer: case expected @ defn.FunctionOf(args, resultType, isContextual) if defn.isNonRefinedFunction(expected) => actual match - case RefinedType(parent, nme.apply, rinfo: MethodType) - if defn.isFunctionNType(actual) => + case defn.RefinedFunctionOf(rinfo: MethodType) => depFun(args, resultType, isContextual, rinfo.paramNames) case _ => expected case _ => expected @@ -1132,12 +1131,12 @@ class CheckCaptures extends Recheck, SymTransformer: * @param sym symbol of the field definition that is being checked */ override def checkSubType(actual: Type, expected: Type)(using Context): Boolean = - val expected1 = alignDependentFunction(addOuterRefs(expected, actual), actual.stripCapturing) + val expected1 = alignDependentFunction(addOuterRefs(/*Existential.strip*/(expected), actual), actual.stripCapturing) val actual1 = val saved = curEnv try curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) - val adapted = adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true) + val adapted = adaptBoxed(/*Existential.strip*/(actual), expected1, srcPos, covariant = true, alwaysConst = true) actual match case _: MethodType => // We remove the capture set resulted from box adaptation for method types, diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala new file mode 100644 index 000000000000..0dba1a62e7ed --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -0,0 +1,366 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* +import CaptureSet.IdempotentCaptRefMap +import StdNames.nme +import ast.tpd.* +import Decorators.* +import typer.ErrorReporting.errorType +import NameKinds.exSkolemName +import reporting.Message + +/** + +Handling existentials in CC: + + - We generally use existentials only in function and method result types + - All occurrences of an EX-bound variable appear co-variantly in the bound type + +In Setup: + + - Convert occurrences of `cap` in function results to existentials. Precise rules below. + - Conversions are done in two places: + + + As part of mapping from local types of parameters and results to infos of methods. + The local types just use `cap`, whereas the result type in the info uses EX-bound variables. + + When converting functions or methods appearing in explicitly declared types. + Here again, we only replace cap's in fucntion results. + + - Conversion is done with a BiTypeMap in `Existential.mapCap`. + +In adapt: + + - If an EX is toplevel in actual type, replace its bound variable + occurrences with `cap`. + +Level checking and avoidance: + + - Environments, capture refs, and capture set variables carry levels + + + levels start at 0 + + The level of a block or template statement sequence is one higher than the level of + its environment + + The level of a TermRef is the level of the environment where its symbol is defined. + + The level of a ThisType is the level of the statements of the class to which it beloongs. + + The level of a TermParamRef is currently -1 (i.e. TermParamRefs are not yet checked using this system) + + The level of a capture set variable is the level of the environment where it is created. + + - Variables also carry info whether they accept `cap` or not. Variables introduced under a box + don't, the others do. + + - Capture set variables do not accept elements of level higher than the variable's level + - We use avoidance to heal such cases: If the level-incorrect ref appears + + covariantly: widen to underlying capture set, reject if that is cap and the variable does not allow it + + contravariantly: narrow to {} + + invarianty: reject with error + +In cv-computation (markFree): + + - Reach capabilities x* of a parameter x cannot appear in the capture set of + the owning method. They have to be widened to dcs(x), or, where this is not + possible, it's an error. + +In well-formedness checking of explicitly written type T: + + - If T is not the type of a parameter, check that no cap occurrence or EX-bound variable appears + under a box. + +Subtype rules + + - new alphabet: existentially bound variables `a`. + - they can be stored in environments Gamma. + - they are alpha-renable, usual hygiene conditions apply + + Gamma |- EX a.T <: U + if Gamma, a |- T <: U + + Gamma |- T <: EX a.U + if exists capture set C consisting of capture refs and ex-bound variables + bound in Gamma such that Gamma |- T <: [a := C]U + +Representation: + + EX a.T[a] is represented as a dependent function type + + (a: Exists) => T[a]] + + where Exists is defined in caps like this: + + sealed trait Exists extends Capability + + The defn.RefinedFunctionOf extractor will exclude existential types from + its results, so only normal refined functions match. + + Let `boundvar(ex)` be the TermParamRef defined by the existential type `ex`. + +Subtype checking algorithm, general scheme: + + Maintain two structures in TypeComparer: + + openExistentials: List[TermParamRef] + assocExistentials: Map[TermParamRef, List[TermParamRef]] + + `openExistentials` corresponds to the list of existential variables stored in the environment. + `assocExistentials` maps existential variables bound by existentials appearing on the right + to the value of `openExistentials` at the time when the existential on the right was dropped. + +Subtype checking algorithm, steps to add for tp1 <:< tp2: + + If tp1 is an existential EX a.tp1a: + + val saved = openExistentials + openExistentials = boundvar(tp1) :: openExistentials + try tp1a <:< tp2 + finally openExistentials = saved + + If tp2 is an existential EX a.tp2a: + + val saved = assocExistentials + assocExistentials = assocExistentials + (boundvar(tp2) -> openExistentials) + try tp1 <:< tp2a + finally assocExistentials = saved + + If tp2 is an existentially bound variable: + assocExistentials(tp2).isDefined + && (assocExistentials(tp2).contains(tp1) || tp1 is not existentially bound) + +Existential source syntax: + + Existential types are ususally not written in source, since we still allow the `^` + syntax that can express most of them more concesely (see below for translation rules). + But we should also allow to write existential types explicity, even if it ends up mainly + for debugging. To express them, we use the encoding with `Exists`, so a typical + expression of an existential would be + + (x: Exists) => A ->{x} B + + Existential types can only at the top level of the result type + of a function or method. + +Restrictions on Existential Types: + + - An existential capture ref must be the only member of its set. This is + intended to model the idea that existential variables effectibely range + over capture sets, not capture references. But so far our calculus + and implementation does not yet acoommodate first-class capture sets. + - Existential capture refs must appear co-variantly in their bound type + + So the following would all be illegal: + + EX x.C^{x, io} // error: multiple members + EX x.() => EX y.C^{x, y} // error: multiple members + EX x.C^{x} ->{x} D // error: contra-variant occurrence + EX x.Set[C^{x}] // error: invariant occurrence + +Expansion of ^: + + We expand all occurrences of `cap` in the result types of functions or methods + to existentially quantified types. Nested scopes are expanded before outer ones. + + The expansion algorithm is then defined as follows: + + 1. In a result type, replace every occurrence of ^ with a fresh existentially + bound variable and quantify over all variables such introduced. + + 2. After this step, type aliases are expanded. If aliases have aliases in arguments, + the outer alias is expanded before the aliases in the arguments. Each time an alias + is expanded that reveals a `^`, apply step (1). + + 3. The algorithm ends when no more alieases remain to be expanded. + + Examples: + + - `A => B` is an alias type that expands to `(A -> B)^`, therefore + `() -> A => B` expands to `() -> EX c. A ->{c} B`. + + - `() => Iterator[A => B]` expands to `() => EX c. Iterator[A ->{c} B]` + + - `A -> B^` expands to `A -> EX c.B^{c}`. + + - If we define `type Fun[T] = A -> T`, then `() -> Fun[B^]` expands to `() -> EX c.Fun[B^{c}]`, which + dealiases to `() -> EX c.A -> B^{c}`. + + - If we define + + type F = A -> Fun[B^] + + then the type alias expands to + + type F = A -> EX c.A -> B^{c} +*/ +object Existential: + + type Carrier = RefinedType + + def openExpected(pt: Type)(using Context): Type = pt.dealias match + case Existential(boundVar, unpacked) => + val tm = new IdempotentCaptRefMap: + val cvar = CaptureSet.Var(ctx.owner) + def apply(t: Type) = mapOver(t) match + case t @ CapturingType(parent, refs) if refs.elems.contains(boundVar) => + assert(refs.isConst && refs.elems.size == 1, i"malformed existential $t") + t.derivedCapturingType(parent, cvar) + case t => + t + openExpected(tm(unpacked)) + case _ => pt + + def toCap(tp: Type)(using Context) = tp.dealias match + case Existential(boundVar, unpacked) => + unpacked.substParam(boundVar, defn.captureRoot.termRef) + case _ => tp + + /** Replace all occurrences of `cap` in parts of this type by an existentially bound + * variable. If there are such occurrences, or there might be in the future due to embedded + * capture set variables, create an existential with the variable wrapping the type. + * Stop at function or method types since these have been mapped before. + */ + def mapCap(tp: Type, fail: Message => Unit)(using Context): Type = + var needsWrap = false + + class Wrap(boundVar: TermParamRef) extends BiTypeMap, ConservativeFollowAliasMap: + def apply(t: Type) = // go deep first, so that we map parts of alias types before dealiasing + mapOver(t) match + case t1: TermRef if t1.isRootCapability => + if variance > 0 then + needsWrap = true + boundVar + else + val varianceStr = if variance < 0 then "contra" else "in" + fail(em"cap appears in ${varianceStr}variant position in $tp") + t1 + case t1 @ FunctionOrMethod(_, _) => + // These have been mapped before + t1 + case t1 @ CapturingType(_, _: CaptureSet.Var) => + if variance > 0 then needsWrap = true // the set might get a cap later. + t1 + case t1 => + applyToAlias(t, t1) + + lazy val inverse = new BiTypeMap with ConservativeFollowAliasMap: + def apply(t: Type) = mapOver(t) match + case t1: TermParamRef if t1 eq boundVar => defn.captureRoot.termRef + case t1 @ FunctionOrMethod(_, _) => t1 + case t1 => applyToAlias(t, t1) + def inverse = Wrap.this + override def toString = "Wrap.inverse" + end Wrap + + if ccConfig.useExistentials then + val wrapped = apply(Wrap(_)(tp)) + if needsWrap then wrapped else tp + else tp + end mapCap + + def mapCapInResult(tp: Type, fail: Message => Unit)(using Context): Type = + def mapCapInFinalResult(tp: Type): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = mapCapInFinalResult(tp.resultType)) + case _ => + mapCap(tp, fail) + tp match + case tp: MethodOrPoly => + mapCapInFinalResult(tp) + case defn.FunctionNOf(args, res, contextual) => + tp.derivedFunctionOrMethod(args, mapCap(res, fail)) + case _ => tp + + def strip(tp: Type)(using Context) = tp match + case Existential(_, tpunpacked) => tpunpacked + case _ => tp + + def skolemize(tp: Type)(using Context) = tp.widenDealias match // TODO needed? + case Existential(boundVar, unpacked) => + val skolem = tp match + case tp: CaptureRef if tp.isTracked => tp + case _ => newSkolemSym(boundVar.underlying).termRef + val tm = new IdempotentCaptRefMap: + var deep = false + private inline def deepApply(t: Type): Type = + val saved = deep + deep = true + try apply(t) finally deep = saved + def apply(t: Type) = + if t eq boundVar then + if deep then skolem.reach else skolem + else t match + case defn.FunctionOf(args, res, contextual) => + val res1 = deepApply(res) + if res1 ne res then defn.FunctionOf(args, res1, contextual) + else t + case defn.RefinedFunctionOf(mt) => + mt.derivedLambdaType(resType = deepApply(mt.resType)) + case _ => + mapOver(t) + tm(unpacked) + case _ => tp + end skolemize + + def newSkolemSym(tp: Type)(using Context): TermSymbol = // TODO needed? + newSymbol(ctx.owner.enclosingMethodOrClass, exSkolemName.fresh(), Synthetic, tp) +/* + def fromDepFun(arg: Tree)(using Context): Type = arg.tpe match + case RefinedType(parent, nme.apply, info: MethodType) if defn.isNonRefinedFunction(parent) => + info match + case info @ MethodType(_ :: Nil) + if info.paramInfos.head.derivesFrom(defn.Caps_Capability) => + apply(ref => info.resultType.substParams(info, ref :: Nil)) + case _ => + errorType(em"Malformed existential: dependent function must have a singgle parameter of type caps.Capability", arg.srcPos) + case _ => + errorType(em"Malformed existential: dependent function type expected", arg.srcPos) +*/ + private class PackMap(sym: Symbol, rt: RecType)(using Context) extends DeepTypeMap, IdempotentCaptRefMap: + def apply(tp: Type): Type = tp match + case ref: TermRef if ref.symbol == sym => TermRef(rt.recThis, defn.captureRoot) + case _ => mapOver(tp) + + /** Unpack current type from an existential `rt` so that all references bound by `rt` + * are recplaced by `ref`. + */ + private class OpenMap(rt: RecType, ref: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap: + def apply(tp: Type): Type = + if isExBound(tp, rt) then ref else mapOver(tp) + + /** Is `tp` a reference to the bound variable of `rt`? */ + private def isExBound(tp: Type, rt: Type)(using Context) = tp match + case tp @ TermRef(RecThis(rt1), _) => (rt1 eq rt) && tp.symbol == defn.captureRoot + case _ => false + + /** Open existential, replacing the bund variable by `ref` */ + def open(rt: RecType, ref: Type)(using Context): Type = OpenMap(rt, ref)(rt.parent) + + /** Create an existential type `ex c.` so that all references to `sym` in `tp` + * become references to the existentially bound variable `c`. + */ + def fromSymbol(tp: Type, sym: Symbol)(using Context): RecType = + RecType(PackMap(sym, _)(tp)) + + def isExistentialMethod(mt: TermLambda)(using Context): Boolean = mt.paramInfos match + case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty + case _ => false + + def isExistentialVar(ref: CaptureRef)(using Context) = ref match + case ref: TermParamRef => isExistentialMethod(ref.binder) + case _ => false + + def unapply(tp: Carrier)(using Context): Option[(TermParamRef, Type)] = + tp.refinedInfo match + case mt: MethodType + if isExistentialMethod(mt) && defn.isNonRefinedFunction(tp.parent) => + Some(mt.paramRefs.head, mt.resultType) + case _ => None + + def apply(mk: TermParamRef => Type)(using Context): MethodType = + MethodType(defn.Caps_Exists.typeRef :: Nil): mt => + mk(mt.paramRefs.head) + + /** Create existential if bound variable appear in result */ + def wrap(mk: TermParamRef => Type)(using Context): Type = + val mt = apply(mk) + if mt.isResultDependent then mt.toFunctionType() else mt.resType +end Existential diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 2c0cdfb7b129..9f33ad4e7fcb 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -14,6 +14,7 @@ import transform.{PreRecheck, Recheck}, Recheck.* import CaptureSet.{IdentityCaptRefMap, IdempotentCaptRefMap} import Synthetics.isExcluded import util.Property +import reporting.Message import printing.{Printer, Texts}, Texts.{Text, Str} import collection.mutable import CCState.* @@ -241,6 +242,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val rinfo1 = apply(rinfo) if rinfo1 ne rinfo then rinfo1.toFunctionType(alwaysDependent = true) else tp + case Existential(_, unpacked) => + // drop the existential, the bound variables will be replaced by capture set variables + apply(unpacked) case tp: MethodType => tp.derivedLambdaType( paramInfos = mapNested(tp.paramInfos), @@ -256,13 +260,19 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: end apply end mapInferred - mapInferred(refine = true)(tp) + try mapInferred(refine = true)(tp) + catch case ex: AssertionError => + println(i"error while mapping inferred $tp") + throw ex end transformInferredType private def transformExplicitType(tp: Type, tptToCheck: Option[Tree] = None)(using Context): Type = val expandAliases = new DeepTypeMap: override def toString = "expand aliases" + def fail(msg: Message) = + for tree <- tptToCheck do report.error(msg, tree.srcPos) + /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib * are defined with `?=>` rather than `?->`. * We also have to add a capture set to the last expanded throws alias. I.e. @@ -288,7 +298,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(fntpe, cs, boxed = false) else fntpe - private def recur(t: Type): Type = normalizeCaptures(mapOver(t)) + private def recur(t: Type): Type = + Existential.mapCapInResult(normalizeCaptures(mapOver(t)), fail) def apply(t: Type) = t match @@ -383,7 +394,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: try transformTT(tpt, boxed = !ccConfig.allowUniversalInBoxed && sym.is(Mutable, butNot = Method), - // types of mutable variables are boxed in pre 3.3 codee + // types of mutable variables are boxed in pre 3.3 code exact = sym.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set TODO drop ) @@ -476,11 +487,14 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else tree.tpt.knownType def paramSignatureChanges = tree.match - case tree: DefDef => tree.paramss.nestedExists: - case param: ValDef => param.tpt.hasRememberedType - case param: TypeDef => param.rhs.hasRememberedType + case tree: DefDef => + tree.paramss.nestedExists: + case param: ValDef => param.tpt.hasRememberedType + case param: TypeDef => param.rhs.hasRememberedType case _ => false + // A symbol's signature changes if some of its parameter types or its result type + // have a new type installed here (meaning hasRememberedType is true) def signatureChanges = tree.tpt.hasRememberedType && !sym.isConstructor || paramSignatureChanges @@ -515,7 +529,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else SubstParams(prevPsymss, prevLambdas)(resType) if sym.exists && signatureChanges then - val newInfo = integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil) + val newInfo = + Existential.mapCapInResult( + integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil), + report.error(_, tree.srcPos)) .showing(i"update info $sym: ${sym.info} = $result", capt) if newInfo ne sym.info then val updatedInfo = diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index ee8ed4b215d7..e8a234ff821f 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -229,7 +229,7 @@ object Config { inline val reuseSymDenotations = true /** If `checkLevelsOnConstraints` is true, check levels of type variables - * and create fresh ones as needed when bounds are first entered intot he constraint. + * and create fresh ones as needed when bounds are first entered into the constraint. * If `checkLevelsOnInstantiation` is true, allow level-incorrect constraints but * fix levels on type variable instantiation. */ diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 1f0a673f90b1..3ee532ccfbaa 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -15,7 +15,7 @@ import Comments.{Comment, docCtx} import util.Spans.NoSpan import config.Feature import Symbols.requiredModuleRef -import cc.{CaptureSet, RetainingType} +import cc.{CaptureSet, RetainingType, Existential} import ast.tpd.ref import scala.annotation.tailrec @@ -993,6 +993,7 @@ class Definitions { @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") @tu lazy val Caps_Capability: ClassSymbol = requiredClass("scala.caps.Capability") @tu lazy val Caps_reachCapability: TermSymbol = CapsModule.requiredMethod("reachCapability") + @tu lazy val Caps_Exists = requiredClass("scala.caps.Exists") @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") @@ -1189,11 +1190,17 @@ class Definitions { /** Matches a refined `PolyFunction`/`FunctionN[...]`/`ContextFunctionN[...]`. * Extracts the method type type and apply info. + * Will NOT math an existential type encoded as a dependent function. */ def unapply(tpe: RefinedType)(using Context): Option[MethodOrPoly] = tpe.refinedInfo match - case mt: MethodOrPoly - if tpe.refinedName == nme.apply && isFunctionType(tpe.parent) => Some(mt) + case mt: MethodType + if tpe.refinedName == nme.apply + && isFunctionType(tpe.parent) + && !Existential.isExistentialMethod(mt) => Some(mt) + case mt: PolyType + if tpe.refinedName == nme.apply + && isFunctionType(tpe.parent) => Some(mt) case _ => None end RefinedFunctionOf diff --git a/compiler/src/dotty/tools/dotc/core/NameKinds.scala b/compiler/src/dotty/tools/dotc/core/NameKinds.scala index 74d440562824..a6348304c4d7 100644 --- a/compiler/src/dotty/tools/dotc/core/NameKinds.scala +++ b/compiler/src/dotty/tools/dotc/core/NameKinds.scala @@ -332,6 +332,7 @@ object NameKinds { val InlineScrutineeName: UniqueNameKind = new UniqueNameKind("$scrutinee") val InlineBinderName: UniqueNameKind = new UniqueNameKind("$proxy") val MacroNames: UniqueNameKind = new UniqueNameKind("$macro$") + val exSkolemName: UniqueNameKind = new UniqueNameKind("$exSkolem") // TODO needed? val UniqueExtMethName: UniqueNameKind = new UniqueNameKindWithUnmangle("$extension") diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 140b42e0e9a9..4f6f857b45d8 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -46,6 +46,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling monitored = false GADTused = false opaquesUsed = false + openedExistentials = Nil + assocExistentials = Map.empty recCount = 0 needsGc = false if Config.checkTypeComparerReset then checkReset() @@ -64,6 +66,18 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling /** Indicates whether the subtype check used opaque types */ private var opaquesUsed: Boolean = false + /** In capture checking: The existential types that are open because they + * appear in an existential type on the left in an enclosing comparison. + */ + private var openedExistentials: List[TermParamRef] = Nil + + /** In capture checking: A map from existential types that are appear + * in an existential type on the right in an enclosing comparison. + * Each existential gets mapped to the opened existentials to which it + * may resolve at this point. + */ + private var assocExistentials: Map[TermParamRef, List[TermParamRef]] = Map.empty + private var myInstance: TypeComparer = this def currentInstance: TypeComparer = myInstance @@ -325,14 +339,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling isSubPrefix(tp1.prefix, tp2.prefix) || thirdTryNamed(tp2) else - ( (tp1.name eq tp2.name) + (tp1.name eq tp2.name) && !sym1.is(Private) && tp2.isPrefixDependentMemberRef && isSubPrefix(tp1.prefix, tp2.prefix) && tp1.signature == tp2.signature && !(sym1.isClass && sym2.isClass) // class types don't subtype each other - ) || - thirdTryNamed(tp2) + || thirdTryNamed(tp2) case _ => secondTry end compareNamed @@ -344,7 +357,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp2: ProtoType => isMatchedByProto(tp2, tp1) case tp2: BoundType => - tp2 == tp1 || secondTry + tp2 == tp1 + || existentialVarsConform(tp1, tp2) + || secondTry case tp2: TypeVar => recur(tp1, typeVarInstance(tp2)) case tp2: WildcardType => @@ -546,6 +561,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if reduced.exists then recur(reduced, tp2) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } else thirdTry + case Existential(boundVar, tp1unpacked) => + compareExistentialLeft(boundVar, tp1unpacked, tp2) case _: FlexType => true case _ => @@ -627,6 +644,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling thirdTryNamed(tp2) case tp2: TypeParamRef => compareTypeParamRef(tp2) + case Existential(boundVar, tp2unpacked) => + compareExistentialRight(tp1, boundVar, tp2unpacked) case tp2: RefinedType => def compareRefinedSlow: Boolean = val name2 = tp2.refinedName @@ -1419,20 +1438,21 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling canConstrain(param2) && canInstantiate(param2) || compareLower(bounds(param2), tyconIsTypeRef = false) case tycon2: TypeRef => - isMatchingApply(tp1) || - byGadtBounds || - defn.isCompiletimeAppliedType(tycon2.symbol) && compareCompiletimeAppliedType(tp2, tp1, fromBelow = true) || { - tycon2.info match { - case info2: TypeBounds => - compareLower(info2, tyconIsTypeRef = true) - case info2: ClassInfo => - tycon2.name.startsWith("Tuple") && - defn.isTupleNType(tp2) && recur(tp1, tp2.toNestedPairs) || - tryBaseType(info2.cls) - case _ => - fourthTry - } - } || tryLiftedToThis2 + isMatchingApply(tp1) + || byGadtBounds + || defn.isCompiletimeAppliedType(tycon2.symbol) + && compareCompiletimeAppliedType(tp2, tp1, fromBelow = true) + || tycon2.info.match + case info2: TypeBounds => + compareLower(info2, tyconIsTypeRef = true) + case info2: ClassInfo => + tycon2.name.startsWith("Tuple") + && defn.isTupleNType(tp2) + && recur(tp1, tp2.toNestedPairs) + || tryBaseType(info2.cls) + case _ => + fourthTry + || tryLiftedToThis2 case tv: TypeVar => if tv.isInstantiated then @@ -1469,12 +1489,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling inFrozenGadt { isSubType(bounds1.hi.applyIfParameterized(args1), tp2, approx.addLow) } } && recordGadtUsageIf(true) - !sym.isClass && { defn.isCompiletimeAppliedType(sym) && compareCompiletimeAppliedType(tp1, tp2, fromBelow = false) || { recur(tp1.superTypeNormalized, tp2) && recordGadtUsageIf(MatchType.thatReducesUsingGadt(tp1)) } || tryLiftedToThis1 - } || byGadtBounds + } + || byGadtBounds case tycon1: TypeProxy => recur(tp1.superTypeNormalized, tp2) case _ => @@ -2767,6 +2787,40 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } + private def compareExistentialLeft(boundVar: TermParamRef, tp1unpacked: Type, tp2: Type)(using Context): Boolean = + val saved = openedExistentials + try + openedExistentials = boundVar :: openedExistentials + recur(tp1unpacked, tp2) + finally + openedExistentials = saved + + private def compareExistentialRight(tp1: Type, boundVar: TermParamRef, tp2unpacked: Type)(using Context): Boolean = + val saved = assocExistentials + try + assocExistentials = assocExistentials.updated(boundVar, openedExistentials) + recur(tp1, tp2unpacked) + finally + assocExistentials = saved + + def canSubsumeExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean = + Existential.isExistentialVar(tp1) + && assocExistentials.get(tp1).match + case Some(xs) => !Existential.isExistentialVar(tp2) || xs.contains(tp2) + case None => false + + /** Are tp1, tp2 termRefs that can be linked? This should never be called + * normally, since exietential variables appear only in capture sets + * which are in annotations that are ignored during normal typing. The real + * work is done in CaptureSet#subsumes which calls linkOK directly. + */ + private def existentialVarsConform(tp1: Type, tp2: Type) = + tp2 match + case tp2: TermParamRef => tp1 match + case tp1: CaptureRef => canSubsumeExistentially(tp2, tp1) + case _ => false + case _ => false + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = refs1.subCaptures(refs2, frozen) @@ -3234,6 +3288,9 @@ object TypeComparer { def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true)(using Context): Type = comparing(_.lub(tp1, tp2, canConstrain = canConstrain, isSoft = isSoft)) + def canSubsumeExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = + comparing(_.canSubsumeExistentially(tp1, tp2)) + /** The least upper bound of a list of types */ final def lub(tps: List[Type])(using Context): Type = tps.foldLeft(defn.NothingType: Type)(lub(_,_)) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 0c6e36c8f18f..9852dfc1170d 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -564,7 +564,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case SingletonTypeTree(ref) => toTextLocal(ref) ~ "." ~ keywordStr("type") case RefinedTypeTree(tpt, refines) => - toTextLocal(tpt) ~ " " ~ blockText(refines) + if defn.isFunctionSymbol(tpt.symbol) && tree.hasType && !printDebug + then changePrec(GlobalPrec) { toText(tree.typeOpt) } + else toTextLocal(tpt) ~ blockText(refines) case AppliedTypeTree(tpt, args) => if (tpt.symbol == defn.orType && args.length == 2) changePrec(OrTypePrec) { toText(args(0)) ~ " | " ~ atPrec(OrTypePrec + 1) { toText(args(1)) } } diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 808bdba34e3f..840601f1622d 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -22,6 +22,12 @@ import annotation.experimental */ extension (x: Any) def reachCapability: Any = x + /** A trait to allow expressing existential types such as + * + * (x: Exists) => A ->{x} B + */ + sealed trait Exists extends Capability + object unsafe: extension [T](x: T) diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index ccd9e891380b..45c1776d8c43 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -34,15 +34,15 @@ | that type captures the root capability `cap`. | This is often caused by a local capability in an argument of constructor Id | leaking as part of its result. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:60:27 -------------------------------------- -60 | val f1: File^{id*} = id(f) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:61:27 -------------------------------------- +61 | val f1: File^{id*} = id(f) // error, since now id(f): File^ | ^^^^^ | Found: File^{id, f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:77:5 ------------------------------------------------------------ -77 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * +-- Error: tests/neg-custom-args/captures/reaches.scala:78:5 ------------------------------------------------------------ +78 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) | ^^^^^^ | Reach capability cap and universal capability cap cannot both | appear in the type [B](f: ((box A ->{ps*} A, box A ->{ps*} A)) => B): List[B] of this expression diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index de5e4362cdf2..6a5ffd51c2c6 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -55,9 +55,10 @@ def test = def attack2 = val id: File^ -> File^ = x => x + // val id: File^ -> EX C.File^C val leaked = usingFile[File^{id*}]: f => - val f1: File^{id*} = id(f) // error + val f1: File^{id*} = id(f) // error, since now id(f): File^ f1 class List[+A]: @@ -74,6 +75,4 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = z => g(f(z)) def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = - ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * - - + ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) diff --git a/tests/neg/cc-ex-conformance.scala b/tests/neg/cc-ex-conformance.scala new file mode 100644 index 000000000000..9cfdda43c764 --- /dev/null +++ b/tests/neg/cc-ex-conformance.scala @@ -0,0 +1,25 @@ +import language.experimental.captureChecking +import caps.{Exists, Capability} + +class C + +type EX1 = () => (c: Exists) => (C^{c}, C^{c}) + +type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) + +type EX3 = () => (c: Exists) => () => C^{c} + +type EX4 = () => () => (c: Exists) => C^{c} + +def Test = + val ex1: EX1 = ??? + val ex2: EX2 = ??? + val _: EX1 = ex1 + val _: EX2 = ex1 // ok + val _: EX1 = ex2 // ok + + val ex3: EX3 = ??? + val ex4: EX4 = ??? + val _: EX4 = ex3 // ok + val _: EX4 = ex4 + val _: EX3 = ex4 // error diff --git a/tests/new/test.scala b/tests/new/test.scala index 16a823547553..18644422ab06 100644 --- a/tests/new/test.scala +++ b/tests/new/test.scala @@ -2,8 +2,9 @@ import language.experimental.namedTuples type Person = (name: String, age: Int) -def test = - val bob = (name = "Bob", age = 33): (name: String, age: Int) +trait A: + type T + +class B: + type U =:= A { type T = U } - val silly = bob match - case (name = n, age = a) => n.length + a diff --git a/tests/pos/cc-ex-unpack.scala b/tests/pos/cc-ex-unpack.scala new file mode 100644 index 000000000000..ae9b4ea5d805 --- /dev/null +++ b/tests/pos/cc-ex-unpack.scala @@ -0,0 +1,18 @@ +import language.experimental.captureChecking +import caps.{Exists, Capability} + +class C + +type EX1 = (c: Exists) -> (C^{c}, C^{c}) + +type EX2 = () -> (c1: Exists) -> (c2: Exists) -> (C^{c1}, C^{c2}) + +type EX3 = () -> (c: Exists) -> () -> C^{c} + +type EX4 = () -> () -> (c: Exists) -> C^{c} + +def Test = + def f = + val ex1: EX1 = ??? + val c1 = ex1 + c1 From 16907ec63cbe510c6c80b0894f42ac29a0b313d2 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Jun 2024 19:47:28 +0200 Subject: [PATCH 03/35] Generalize isAlwaysEmpty and streamline isBoxedCaptured - isBoxedCaptured no longer requires the construction of intermediate capture sets. - isAlwaysEmpty is also true for solved variables that have no elements --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 10 +++++++++- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 080577b5773f..40e271707b26 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -197,7 +197,15 @@ extension (tp: Type) getBoxed(tp) /** Is the boxedCaptureSet of this type nonempty? */ - def isBoxedCapturing(using Context) = !tp.boxedCaptureSet.isAlwaysEmpty + def isBoxedCapturing(using Context): Boolean = + tp match + case tp @ CapturingType(parent, refs) => + tp.isBoxed && !refs.isAlwaysEmpty || parent.isBoxedCapturing + case tp: TypeRef if tp.symbol.isAbstractOrParamType => false + case tp: TypeProxy => tp.superType.isBoxedCapturing + case tp: AndType => tp.tp1.isBoxedCapturing && tp.tp2.isBoxedCapturing + case tp: OrType => tp.tp1.isBoxedCapturing || tp.tp2.isBoxedCapturing + case _ => false /** If this type is a capturing type, the version with boxed statues as given by `boxed`. * If it is a TermRef of a capturing type, and the box status flips, widen to a capturing diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index cf803e47eca0..9b0afbf3567e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -459,7 +459,7 @@ object CaptureSet: var deps: Deps = emptySet def isConst = isSolved - def isAlwaysEmpty = false + def isAlwaysEmpty = isSolved && elems.isEmpty def isMaybeSet = false // overridden in BiMapped From 0a6067036a8d8685948e21cf1e542d2ba7fd8658 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Jun 2024 11:59:27 +0200 Subject: [PATCH 04/35] Refine class refinements - Use a uniform criterion when to add them - Don't add them for @constructorOnly or @cc.untrackedCaptures arguments @untrackedCaptures is a new annotation --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 46 +++++-------------- .../dotty/tools/dotc/cc/CheckCaptures.scala | 7 +-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 +- .../dotty/tools/dotc/core/Definitions.scala | 1 + library/src/scala/caps.scala | 5 ++ 5 files changed, 24 insertions(+), 40 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 40e271707b26..d8c567f145d4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -467,41 +467,17 @@ extension (sym: Symbol) && !sym.allowsRootCapture && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox - - /** Does this symbol define a level where we do not want to let local variables - * escape into outer capture sets? - */ - def isLevelOwner(using Context): Boolean = - sym.isClass - || sym.is(Method, butNot = Accessor) - - /** The owner of the current level. Qualifying owners are - * - methods, other than accessors - * - classes, if they are not staticOwners - * - _root_ - */ - def levelOwner(using Context): Symbol = - def recur(sym: Symbol): Symbol = - if !sym.exists || sym.isRoot || sym.isStaticOwner then defn.RootClass - else if sym.isLevelOwner then sym - else recur(sym.owner) - recur(sym) - - /** The outermost symbol owned by both `sym` and `other`. if none exists - * since the owning scopes of `sym` and `other` are not nested, invoke - * `onConflict` to return a symbol. - */ - def maxNested(other: Symbol, onConflict: (Symbol, Symbol) => Context ?=> Symbol)(using Context): Symbol = - if !sym.exists || other.isContainedIn(sym) then other - else if !other.exists || sym.isContainedIn(other) then sym - else onConflict(sym, other) - - /** The innermost symbol owning both `sym` and `other`. - */ - def minNested(other: Symbol)(using Context): Symbol = - if !other.exists || other.isContainedIn(sym) then sym - else if !sym.exists || sym.isContainedIn(other) then other - else sym.owner.minNested(other.owner) + && !defn.isPolymorphicAfterErasure(sym) + + def isRefiningParamAccessor(using Context): Boolean = + sym.is(ParamAccessor) + && { + val param = sym.owner.primaryConstructor.paramSymss + .nestedFind(_.name == sym.name) + .getOrElse(NoSymbol) + !param.hasAnnotation(defn.ConstructorOnlyAnnot) + && !param.hasAnnotation(defn.UntrackedCapturesAnnot) + } extension (tp: AnnotatedType) /** Is this a boxed capturing type? */ diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index e6e091cd5897..87d37b61941a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -550,8 +550,8 @@ class CheckCaptures extends Recheck, SymTransformer: var allCaptures: CaptureSet = if core.derivesFromCapability then CaptureSet.universal else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do - val getter = cls.info.member(getterName).suchThat(_.is(ParamAccessor)).symbol - if getter.termRef.isTracked && !getter.is(Private) then + val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol + if !getter.is(Private) && getter.termRef.isTracked then refined = RefinedType(refined, getterName, argType) allCaptures ++= argType.captureSet (refined, allCaptures) @@ -764,7 +764,8 @@ class CheckCaptures extends Recheck, SymTransformer: val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") checkSubset(localSet, thisSet, tree.srcPos) // (2) for param <- cls.paramGetters do - if !param.hasAnnotation(defn.ConstructorOnlyAnnot) then + if !param.hasAnnotation(defn.ConstructorOnlyAnnot) + && !param.hasAnnotation(defn.UntrackedCapturesAnnot) then checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) for pureBase <- cls.pureBaseClass do // (4) def selfType = impl.body diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 9f33ad4e7fcb..0851d8063d13 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -69,9 +69,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case _ => foldOver(x, tp) def apply(tp: Type): Boolean = apply(false, tp) - if symd.isAllOf(PrivateParamAccessor) + if symd.symbol.isRefiningParamAccessor + && symd.is(Private) && symd.owner.is(CaptureChecked) - && !symd.hasAnnotation(defn.ConstructorOnlyAnnot) && containsCovarRetains(symd.symbol.originDenotation.info) then symd.flags &~ Private else symd.flags @@ -186,6 +186,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if !defn.isFunctionClass(cls) && cls.is(CaptureChecked) => cls.paramGetters.foldLeft(tp) { (core, getter) => if atPhase(thisPhase.next)(getter.termRef.isTracked) + && getter.isRefiningParamAccessor && !getter.is(Tracked) then val getterType = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3ee532ccfbaa..88de8e66054e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1053,6 +1053,7 @@ class Definitions { @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 UntrackedCapturesAnnot: ClassSymbol = requiredClass("scala.caps.untrackedCaptures") @tu lazy val VolatileAnnot: ClassSymbol = requiredClass("scala.volatile") @tu lazy val BeanGetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanGetter") @tu lazy val BeanSetterMetaAnnot: ClassSymbol = requiredClass("scala.annotation.meta.beanSetter") diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 840601f1622d..46702271474a 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -28,6 +28,11 @@ import annotation.experimental */ sealed trait Exists extends Capability + /** This should go into annotations. For now it is here, so that we + * can experiment with it quickly between minor releases + */ + final class untrackedCaptures extends annotation.StaticAnnotation + object unsafe: extension [T](x: T) From df0c7b192c540922c68b565ad696bda49ec75bef Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Jun 2024 13:00:38 +0200 Subject: [PATCH 05/35] Improve handling of no-cap-under-box/unbox errors - Improve error messages - Better propagation of @uncheckedCaptures - -un-deprecacte caps.unsafeUnbox and friends. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 65 +++++++++++++------ compiler/src/dotty/tools/dotc/cc/Setup.scala | 8 ++- library/src/scala/caps.scala | 9 +-- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 87d37b61941a..6e4a10efe607 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -12,7 +12,7 @@ import ast.{tpd, untpd, Trees} import Trees.* import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker} import typer.Checking.{checkBounds, checkAppliedTypesIn} -import typer.ErrorReporting.{Addenda, err} +import typer.ErrorReporting.{Addenda, NothingToAdd, err} import typer.ProtoTypes.{AnySelectionProto, LhsProto} import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} @@ -22,7 +22,7 @@ import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} -import reporting.trace +import reporting.{trace, Message} /** The capture checker */ object CheckCaptures: @@ -866,7 +866,10 @@ class CheckCaptures extends Recheck, SymTransformer: } checkNotUniversal(parent) case _ => - if !ccConfig.allowUniversalInBoxed && needsUniversalCheck then + if !ccConfig.allowUniversalInBoxed + && !tpe.hasAnnotation(defn.UncheckedCapturesAnnot) + && needsUniversalCheck + then checkNotUniversal(tpe) super.recheckFinish(tpe, tree, pt) end recheckFinish @@ -884,6 +887,17 @@ class CheckCaptures extends Recheck, SymTransformer: private inline val debugSuccesses = false + type BoxErrors = mutable.ListBuffer[Message] | Null + + private def boxErrorAddenda(boxErrors: BoxErrors) = + if boxErrors == null then NothingToAdd + else new Addenda: + override def toAdd(using Context): List[String] = + boxErrors.toList.map: msg => + i""" + | + |Note that ${msg.toString}""" + /** Massage `actual` and `expected` types before checking conformance. * Massaging is done by the methods following this one: * - align dependent function types and add outer references in the expected type @@ -893,7 +907,8 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type = var expected1 = alignDependentFunction(expected, actual.stripCapturing) - val actualBoxed = adapt(actual, expected1, tree.srcPos) + val boxErrors = new mutable.ListBuffer[Message] + val actualBoxed = adapt(actual, expected1, tree.srcPos, boxErrors) //println(i"check conforms $actualBoxed <<< $expected1") if actualBoxed eq actual then @@ -907,7 +922,8 @@ class CheckCaptures extends Recheck, SymTransformer: actualBoxed else capt.println(i"conforms failed for ${tree}: $actual vs $expected") - err.typeMismatch(tree.withType(actualBoxed), expected1, addenda ++ CaptureSet.levelErrors) + err.typeMismatch(tree.withType(actualBoxed), expected1, + addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors)) actual end checkConformsExpr @@ -991,7 +1007,7 @@ class CheckCaptures extends Recheck, SymTransformer: * * @param alwaysConst always make capture set variables constant after adaptation */ - def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean)(using Context): Type = + def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = /** Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation * @param boxed if true we adapt to a boxed expected type @@ -1008,8 +1024,8 @@ class CheckCaptures extends Recheck, SymTransformer: case FunctionOrMethod(eargs, eres) => (eargs, eres) case _ => (aargs.map(_ => WildcardType), WildcardType) val aargs1 = aargs.zipWithConserve(eargs): - adaptBoxed(_, _, pos, !covariant, alwaysConst) - val ares1 = adaptBoxed(ares, eres, pos, covariant, alwaysConst) + adaptBoxed(_, _, pos, !covariant, alwaysConst, boxErrors) + val ares1 = adaptBoxed(ares, eres, pos, covariant, alwaysConst, boxErrors) val resTp = if (aargs1 eq aargs) && (ares1 eq ares) then actualShape // optimize to avoid redundant matches else actualShape.derivedFunctionOrMethod(aargs1, ares1) @@ -1057,22 +1073,26 @@ class CheckCaptures extends Recheck, SymTransformer: val criticalSet = // the set which is not allowed to have `cap` if covariant then captures // can't box with `cap` else expected.captureSet // can't unbox with `cap` - if criticalSet.isUniversal && expected.isValueType && !ccConfig.allowUniversalInBoxed then + def msg = em"""$actual cannot be box-converted to $expected + |since at least one of their capture sets contains the root capability `cap`""" + def allowUniversalInBoxed = + ccConfig.allowUniversalInBoxed + || expected.hasAnnotation(defn.UncheckedCapturesAnnot) + || actual.widen.hasAnnotation(defn.UncheckedCapturesAnnot) + if criticalSet.isUniversal && expected.isValueType && !allowUniversalInBoxed then // We can't box/unbox the universal capability. Leave `actual` as it is - // so we get an error in checkConforms. This tends to give better error + // so we get an error in checkConforms. Add the error message generated + // from boxing as an addendum. This tends to give better error // messages than disallowing the root capability in `criticalSet`. + if boxErrors != null then boxErrors += msg if ctx.settings.YccDebug.value then println(i"cannot box/unbox $actual vs $expected") actual else - if !ccConfig.allowUniversalInBoxed then + if !allowUniversalInBoxed then // Disallow future addition of `cap` to `criticalSet`. - criticalSet.disallowRootCapability { () => - report.error( - em"""$actual cannot be box-converted to $expected - |since one of their capture sets contains the root capability `cap`""", - pos) - } + criticalSet.disallowRootCapability: () => + report.error(msg, pos) if !insertBox then // unboxing //debugShowEnvs() markFree(criticalSet, pos) @@ -1109,13 +1129,15 @@ class CheckCaptures extends Recheck, SymTransformer: * * @param alwaysConst always make capture set variables constant after adaptation */ - def adapt(actual: Type, expected: Type, pos: SrcPos)(using Context): Type = + def adapt(actual: Type, expected: Type, pos: SrcPos, boxErrors: BoxErrors)(using Context): Type = if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual else val normalized = makeCaptureSetExplicit(actual) - val widened = improveCaptures(normalized.widenDealias, actual) - val adapted = adaptBoxed(widened.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false) + val widened = improveCaptures(normalized.widen.dealiasKeepAnnots, actual) + val adapted = adaptBoxed( + widened.withReachCaptures(actual), expected, pos, + covariant = true, alwaysConst = false, boxErrors) if adapted eq widened then normalized else adapted.showing(i"adapt boxed $actual vs $expected ===> $adapted", capt) end adapt @@ -1137,7 +1159,8 @@ class CheckCaptures extends Recheck, SymTransformer: val saved = curEnv try curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) - val adapted = adaptBoxed(/*Existential.strip*/(actual), expected1, srcPos, covariant = true, alwaysConst = true) + val adapted = + adaptBoxed(/*Existential.strip*/(actual), expected1, srcPos, covariant = true, alwaysConst = true, null) actual match case _: MethodType => // We remove the capture set resulted from box adaptation for method types, diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 0851d8063d13..35f22538f074 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -394,7 +394,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: def transformResultType(tpt: TypeTree, sym: Symbol)(using Context): Unit = try transformTT(tpt, - boxed = !ccConfig.allowUniversalInBoxed && sym.is(Mutable, butNot = Method), + boxed = + sym.is(Mutable, butNot = Method) + && !ccConfig.allowUniversalInBoxed + && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), // types of mutable variables are boxed in pre 3.3 code exact = sym.allOverriddenSymbols.hasNext, // types of symbols that override a parent don't get a capture set TODO drop @@ -405,7 +408,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val addDescription = new TypeTraverser: def traverse(tp: Type) = tp match case tp @ CapturingType(parent, refs) => - if !refs.isConst then refs.withDescription(i"of $sym") + if !refs.isConst && refs.description.isEmpty then + refs.withDescription(i"of $sym") traverse(parent) case _ => traverseChildren(tp) diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 46702271474a..5ae5b860f501 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -43,22 +43,19 @@ import annotation.experimental def unsafeAssumePure: T = x /** If argument is of type `cs T`, converts to type `box cs T`. This - * avoids the error that would be raised when boxing `*`. + * avoids the error that would be raised when boxing `cap`. */ - @deprecated(since = "3.3") def unsafeBox: T = x /** If argument is of type `box cs T`, converts to type `cs T`. This - * avoids the error that would be raised when unboxing `*`. + * avoids the error that would be raised when unboxing `cap`. */ - @deprecated(since = "3.3") def unsafeUnbox: T = x extension [T, U](f: T => U) /** If argument is of type `box cs T`, converts to type `cs T`. This - * avoids the error that would be raised when unboxing `*`. + * avoids the error that would be raised when unboxing `cap`. */ - @deprecated(since = "3.3") def unsafeBoxFunArg: T => U = f end unsafe From 3ce1f3119db3867031d49c01a2d2e475940c7a23 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Jun 2024 13:27:17 +0200 Subject: [PATCH 06/35] Go back to original no cap in box/unbox restrictions We go back to the original lifetime restriction that box/unbox cannot apply to universal capture sets, and drop the later restriction that type variable instantiations may not deeply capture cap. The original restriction is proven to be sound and is probably expressive enough when we add reach capabilities. This required some changes in tests and also in the standard library. The original restriction is in place for source <= 3.2 and >= 3.5. Source 3.3 and 3.4 use the alternative restriction on type variable instances. Some neg tests have not been brought forward to 3.4. They are all in tests/neg-customargs/captures and start with //> using options -source 3.4 We need to look at these tests one-by-one and analyze whether the new 3.5 behavior is correct. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +- .../src/scala/collection/Iterator.scala | 4 +- .../src/scala/collection/SeqView.scala | 12 +++-- .../immutable/LazyListIterable.scala | 53 ++++++++++--------- .../captures/box-adapt-cases.scala | 2 +- .../neg-custom-args/captures/capt-test.scala | 4 +- tests/neg-custom-args/captures/capt1.check | 40 +++++++------- tests/neg-custom-args/captures/capt1.scala | 2 + .../captures/effect-swaps-explicit.check | 22 ++++---- .../captures/effect-swaps-explicit.scala | 2 + tests/neg-custom-args/captures/filevar.scala | 2 +- tests/neg-custom-args/captures/i15749.scala | 15 ++++++ tests/neg-custom-args/captures/i15772.check | 8 +-- tests/neg-custom-args/captures/i15922.scala | 2 + .../captures/i15923-cases.scala | 7 +++ tests/neg-custom-args/captures/i16114.scala | 2 + .../captures/i19330-alt2.scala | 2 + tests/neg-custom-args/captures/i19330.scala | 2 + .../captures/lazylists-exceptions.check | 5 +- tests/neg-custom-args/captures/levels.check | 8 +-- tests/neg-custom-args/captures/levels.scala | 2 + .../neg-custom-args/captures/outer-var.check | 30 ++++++----- tests/neg-custom-args/captures/reaches.check | 28 +++++----- tests/neg-custom-args/captures/reaches.scala | 2 + tests/neg-custom-args/captures/real-try.check | 52 +++++++++--------- tests/neg-custom-args/captures/real-try.scala | 2 + tests/neg-custom-args/captures/try.check | 16 +++--- tests/neg-custom-args/captures/try.scala | 4 +- .../captures}/unsound-reach-2.scala | 2 + .../captures}/unsound-reach-3.scala | 2 + .../captures}/unsound-reach-4.check | 4 +- .../captures}/unsound-reach-4.scala | 2 + .../captures/unsound-reach.check | 12 +++++ .../captures}/unsound-reach.scala | 2 +- .../captures/vars-simple.check | 9 ++-- tests/neg-custom-args/captures/vars.check | 16 +++--- tests/neg-custom-args/captures/vars.scala | 2 + tests/neg/unsound-reach.check | 5 -- tests/pos-custom-args/captures/casts.scala | 4 ++ .../captures/filevar-expanded.scala | 3 +- tests/pos-custom-args/captures/i15749.scala | 4 +- .../captures/i15923-cases.scala | 4 -- tests/pos-custom-args/captures/i15925.scala | 5 +- tests/pos-custom-args/captures/levels.scala | 23 ++++++++ .../captures/unsafe-captures.scala | 8 +++ .../captures/untracked-captures.scala | 34 ++++++++++++ .../colltest5/CollectionStrawManCC5_1.scala | 2 +- 47 files changed, 310 insertions(+), 167 deletions(-) create mode 100644 tests/neg-custom-args/captures/i15749.scala create mode 100644 tests/neg-custom-args/captures/i15923-cases.scala rename tests/{neg => neg-custom-args/captures}/unsound-reach-2.scala (89%) rename tests/{neg => neg-custom-args/captures}/unsound-reach-3.scala (89%) rename tests/{neg => neg-custom-args/captures}/unsound-reach-4.check (55%) rename tests/{neg => neg-custom-args/captures}/unsound-reach-4.scala (85%) create mode 100644 tests/neg-custom-args/captures/unsound-reach.check rename tests/{neg => neg-custom-args/captures}/unsound-reach.scala (83%) delete mode 100644 tests/neg/unsound-reach.check create mode 100644 tests/pos-custom-args/captures/casts.scala create mode 100644 tests/pos-custom-args/captures/levels.scala create mode 100644 tests/pos-custom-args/captures/unsafe-captures.scala create mode 100644 tests/pos-custom-args/captures/untracked-captures.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index d8c567f145d4..7a8ed7b3651a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -32,7 +32,9 @@ object ccConfig: * previous global retriction that `cap` can't be boxed or unboxed. */ def allowUniversalInBoxed(using Context) = - Feature.sourceVersion.isAtLeast(SourceVersion.`3.3`) + Feature.sourceVersion.stable == SourceVersion.`3.3` + || Feature.sourceVersion.stable == SourceVersion.`3.4` + //|| Feature.sourceVersion.stable == SourceVersion.`3.5` // drop `//` if you want to test with the sealed type params strategy end ccConfig diff --git a/scala2-library-cc/src/scala/collection/Iterator.scala b/scala2-library-cc/src/scala/collection/Iterator.scala index 58ef4beb930d..4d1b0ed4ff95 100644 --- a/scala2-library-cc/src/scala/collection/Iterator.scala +++ b/scala2-library-cc/src/scala/collection/Iterator.scala @@ -1008,7 +1008,7 @@ object Iterator extends IterableFactory[Iterator] { def newBuilder[A]: Builder[A, Iterator[A]] = new ImmutableBuilder[A, Iterator[A]](empty[A]) { override def addOne(elem: A): this.type = { elems = elems ++ single(elem); this } - } + }.asInstanceOf // !!! CC unsafe op /** Creates iterator that produces the results of some element computation a number of times. * @@ -1160,7 +1160,7 @@ object Iterator extends IterableFactory[Iterator] { @tailrec def merge(): Unit = if (current.isInstanceOf[ConcatIterator[_]]) { val c = current.asInstanceOf[ConcatIterator[A]] - current = c.current + current = c.current.asInstanceOf // !!! CC unsafe op currentHasNextChecked = c.currentHasNextChecked if (c.tail != null) { if (last == null) last = c.last diff --git a/scala2-library-cc/src/scala/collection/SeqView.scala b/scala2-library-cc/src/scala/collection/SeqView.scala index 34405e06eedb..c7af0077ce1a 100644 --- a/scala2-library-cc/src/scala/collection/SeqView.scala +++ b/scala2-library-cc/src/scala/collection/SeqView.scala @@ -186,12 +186,14 @@ object SeqView { } @SerialVersionUID(3L) - class Sorted[A, B >: A] private (private[this] var underlying: SomeSeqOps[A]^, + class Sorted[A, B >: A] private (underlying: SomeSeqOps[A]^, private[this] val len: Int, ord: Ordering[B]) extends SeqView[A] { outer: Sorted[A, B]^ => + private var myUnderlying: SomeSeqOps[A]^{underlying} = underlying + // force evaluation immediately by calling `length` so infinite collections // hang on `sorted`/`sortWith`/`sortBy` rather than on arbitrary method calls def this(underlying: SomeSeqOps[A]^, ord: Ordering[B]) = this(underlying, underlying.length, ord) @@ -221,10 +223,10 @@ object SeqView { val res = { val len = this.len if (len == 0) Nil - else if (len == 1) List(underlying.head) + else if (len == 1) List(myUnderlying.head) else { val arr = new Array[Any](len) // Array[Any] =:= Array[AnyRef] - underlying.copyToArray(arr) + myUnderlying.copyToArray(arr) java.util.Arrays.sort(arr.asInstanceOf[Array[AnyRef]], ord.asInstanceOf[Ordering[AnyRef]]) // casting the Array[AnyRef] to Array[A] and creating an ArraySeq from it // is safe because: @@ -238,12 +240,12 @@ object SeqView { } } evaluated = true - underlying = null + myUnderlying = null res } private[this] def elems: SomeSeqOps[A]^{this} = { - val orig = underlying + val orig = myUnderlying if (evaluated) _sorted else orig } diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index ac24995e6892..2f7b017a6729 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -24,6 +24,7 @@ import scala.language.implicitConversions import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures +import caps.untrackedCaptures /** This class implements an immutable linked list. We call it "lazy" * because it computes its elements only when they are needed. @@ -245,7 +246,7 @@ import annotation.unchecked.uncheckedCaptures * @define evaluatesAllElements This method evaluates all elements of the collection. */ @SerialVersionUID(3L) -final class LazyListIterable[+A] private(private[this] var lazyState: () => LazyListIterable.State[A]^) +final class LazyListIterable[+A] private(@untrackedCaptures lazyState: () => LazyListIterable.State[A]^) extends AbstractIterable[A] with Iterable[A] with IterableOps[A, LazyListIterable, LazyListIterable[A]] @@ -253,6 +254,8 @@ final class LazyListIterable[+A] private(private[this] var lazyState: () => Lazy with Serializable { import LazyListIterable._ + private var myLazyState = lazyState + @volatile private[this] var stateEvaluated: Boolean = false @inline private def stateDefined: Boolean = stateEvaluated private[this] var midEvaluation = false @@ -264,11 +267,11 @@ final class LazyListIterable[+A] private(private[this] var lazyState: () => Lazy throw new RuntimeException("self-referential LazyListIterable or a derivation thereof has no more elements") } midEvaluation = true - val res = try lazyState() finally midEvaluation = false + val res = try myLazyState() finally midEvaluation = false // if we set it to `true` before evaluating, we may infinite loop // if something expects `state` to already be evaluated stateEvaluated = true - lazyState = null // allow GC + myLazyState = null // allow GC res } @@ -755,7 +758,7 @@ final class LazyListIterable[+A] private(private[this] var lazyState: () => Lazy * The iterator returned by this method mostly preserves laziness; * a single element ahead of the iterator is evaluated. */ - override def grouped(size: Int): Iterator[LazyListIterable[A]] = { + override def grouped(size: Int): Iterator[LazyListIterable[A]]^{this} = { require(size > 0, "size must be positive, but was " + size) slidingImpl(size = size, step = size) } @@ -765,12 +768,12 @@ final class LazyListIterable[+A] private(private[this] var lazyState: () => Lazy * The iterator returned by this method mostly preserves laziness; * `size - step max 1` elements ahead of the iterator are evaluated. */ - override def sliding(size: Int, step: Int): Iterator[LazyListIterable[A]] = { + override def sliding(size: Int, step: Int): Iterator[LazyListIterable[A]]^{this} = { require(size > 0 && step > 0, s"size=$size and step=$step, but both must be positive") slidingImpl(size = size, step = step) } - @inline private def slidingImpl(size: Int, step: Int): Iterator[LazyListIterable[A]] = + @inline private def slidingImpl(size: Int, step: Int): Iterator[LazyListIterable[A]]^{this} = if (knownIsEmpty) Iterator.empty else new SlidingIterator[A](this, size = size, step = step) @@ -996,7 +999,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { private def filterImpl[A](ll: LazyListIterable[A]^, p: A => Boolean, isFlipped: Boolean): LazyListIterable[A]^{ll, p} = { // DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD - var restRef: LazyListIterable[A]^{ll*} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric + var restRef: LazyListIterable[A]^{ll} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric newLL { var elem: A = null.asInstanceOf[A] var found = false @@ -1013,7 +1016,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { private def collectImpl[A, B](ll: LazyListIterable[A]^, pf: PartialFunction[A, B]^): LazyListIterable[B]^{ll, pf} = { // DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD - var restRef: LazyListIterable[A]^{ll*} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric + var restRef: LazyListIterable[A]^{ll} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric newLL { val marker = Statics.pfMarker val toMarker = anyToMarker.asInstanceOf[A => B] // safe because Function1 is erased @@ -1032,7 +1035,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { private def flatMapImpl[A, B](ll: LazyListIterable[A]^, f: A => IterableOnce[B]^): LazyListIterable[B]^{ll, f} = { // DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD - var restRef: LazyListIterable[A]^{ll*} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric + var restRef: LazyListIterable[A]^{ll} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric newLL { var it: Iterator[B]^{ll, f} = null var itHasNext = false @@ -1056,7 +1059,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { private def dropImpl[A](ll: LazyListIterable[A]^, n: Int): LazyListIterable[A]^{ll} = { // DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD - var restRef: LazyListIterable[A]^{ll*} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric + var restRef: LazyListIterable[A]^{ll} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric var iRef = n // val iRef = new IntRef(n) newLL { var rest = restRef // var rest = restRef.elem @@ -1073,7 +1076,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { private def dropWhileImpl[A](ll: LazyListIterable[A]^, p: A => Boolean): LazyListIterable[A]^{ll, p} = { // DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD - var restRef: LazyListIterable[A]^{ll*} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric + var restRef: LazyListIterable[A]^{ll} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric newLL { var rest = restRef // var rest = restRef.elem while (!rest.isEmpty && p(rest.head)) { @@ -1086,8 +1089,8 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { private def takeRightImpl[A](ll: LazyListIterable[A]^, n: Int): LazyListIterable[A]^{ll} = { // DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD - var restRef: LazyListIterable[A]^{ll*} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric - var scoutRef: LazyListIterable[A]^{ll*} = ll // same situation + var restRef: LazyListIterable[A]^{ll} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric + var scoutRef: LazyListIterable[A]^{ll} = ll // same situation var remainingRef = n // val remainingRef = new IntRef(n) newLL { var scout = scoutRef // var scout = scoutRef.elem @@ -1236,33 +1239,35 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { */ def newBuilder[A]: Builder[A, LazyListIterable[A]] = new LazyBuilder[A] - private class LazyIterator[+A](private[this] var lazyList: LazyListIterable[A]^) extends AbstractIterator[A] { - override def hasNext: Boolean = !lazyList.isEmpty + private class LazyIterator[+A](lazyList: LazyListIterable[A]^) extends AbstractIterator[A] { + private var myLazyList = lazyList + override def hasNext: Boolean = !myLazyList.isEmpty override def next(): A = - if (lazyList.isEmpty) Iterator.empty.next() + if (myLazyList.isEmpty) Iterator.empty.next() else { - val res = lazyList.head - lazyList = lazyList.tail + val res = myLazyList.head + myLazyList = myLazyList.tail res } } - private class SlidingIterator[A](private[this] var lazyList: LazyListIterable[A]^, size: Int, step: Int) + private class SlidingIterator[A](lazyList: LazyListIterable[A]^, size: Int, step: Int) extends AbstractIterator[LazyListIterable[A]] { + private var myLazyList = lazyList private val minLen = size - step max 0 private var first = true def hasNext: Boolean = - if (first) !lazyList.isEmpty - else lazyList.lengthGt(minLen) + if (first) !myLazyList.isEmpty + else myLazyList.lengthGt(minLen) def next(): LazyListIterable[A] = { if (!hasNext) Iterator.empty.next() else { first = false - val list = lazyList - lazyList = list.drop(step) + val list = myLazyList + myLazyList = list.drop(step) list.take(size) } } @@ -1281,7 +1286,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { import LazyBuilder._ private[this] var next: DeferredState[A] = _ - private[this] var list: LazyListIterable[A] = _ + @uncheckedCaptures private[this] var list: LazyListIterable[A]^ = _ clear() diff --git a/tests/neg-custom-args/captures/box-adapt-cases.scala b/tests/neg-custom-args/captures/box-adapt-cases.scala index 3dac26a98318..681d699842ed 100644 --- a/tests/neg-custom-args/captures/box-adapt-cases.scala +++ b/tests/neg-custom-args/captures/box-adapt-cases.scala @@ -4,7 +4,7 @@ def test1(): Unit = { type Id[X] = [T] -> (op: X => T) -> T val x: Id[Cap^] = ??? - x(cap => cap.use()) // was error, now OK + x(cap => cap.use()) // error, OK under sealed } def test2(io: Cap^): Unit = { diff --git a/tests/neg-custom-args/captures/capt-test.scala b/tests/neg-custom-args/captures/capt-test.scala index 80ee1aba84e1..b202a14d0940 100644 --- a/tests/neg-custom-args/captures/capt-test.scala +++ b/tests/neg-custom-args/captures/capt-test.scala @@ -20,8 +20,8 @@ def handle[E <: Exception, R <: Top](op: (CT[E] @retains(caps.cap)) => R)(handl catch case ex: E => handler(ex) def test: Unit = - val b = handle[Exception, () => Nothing] { // error + val b = handle[Exception, () => Nothing] { (x: CanThrow[Exception]) => () => raise(new Exception)(using x) - } { + } { // error (ex: Exception) => ??? } diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 0e99d1876d3c..3d0ed538b2e5 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,52 +1,52 @@ --- Error: tests/neg-custom-args/captures/capt1.scala:4:11 -------------------------------------------------------------- -4 | () => if x == null then y else y // error +-- Error: tests/neg-custom-args/captures/capt1.scala:6:11 -------------------------------------------------------------- +6 | () => if x == null then y else y // error | ^ | (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 +-- Error: tests/neg-custom-args/captures/capt1.scala:9:11 -------------------------------------------------------------- +9 | () => if x == null then y else y // error | ^ | (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 +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:16:2 ----------------------------------------- +16 | def f(y: Int) = if x == null then y else y // error | ^ | Found: (y: Int) ->{x} Int | Required: Matchable -15 | f +17 | f | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:21:2 ----------------------------------------- -21 | class F(y: Int) extends A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:23:2 ----------------------------------------- +23 | class F(y: Int) extends A: // error | ^ | Found: A^{x} | Required: A -22 | def m() = if x == null then y else y -23 | F(22) +24 | def m() = if x == null then y else y +25 | F(22) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:26:2 ----------------------------------------- -26 | new A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:28:2 ----------------------------------------- +28 | new A: // error | ^ | Found: A^{x} | Required: A -27 | def m() = if x == null then y else y +29 | def m() = if x == null then y else y | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/capt1.scala:32:12 ------------------------------------------------------------- -32 | val z2 = h[() -> Cap](() => x) // error // error +-- Error: tests/neg-custom-args/captures/capt1.scala:34:12 ------------------------------------------------------------- +34 | val z2 = h[() -> Cap](() => x) // error // error | ^^^^^^^^^^^^ | Sealed type variable X cannot be instantiated to () -> box C^ since | the part box C^ of that type captures the root capability `cap`. | This is often caused by a local capability in an argument of method h | leaking as part of its result. --- Error: tests/neg-custom-args/captures/capt1.scala:32:30 ------------------------------------------------------------- -32 | val z2 = h[() -> Cap](() => x) // error // error +-- Error: tests/neg-custom-args/captures/capt1.scala:34:30 ------------------------------------------------------------- +34 | val z2 = h[() -> Cap](() => x) // error // error | ^ | (x : C^) cannot be referenced here; it is not included in the allowed capture set {} | of an enclosing function literal with expected type () -> box C^ --- Error: tests/neg-custom-args/captures/capt1.scala:34:12 ------------------------------------------------------------- -34 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error +-- Error: tests/neg-custom-args/captures/capt1.scala:36:12 ------------------------------------------------------------- +36 | val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^ | Sealed type variable X cannot be instantiated to box () ->{x} Cap since | the part Cap of that type captures the root capability `cap`. diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index 48c4d889bf8d..cad0bad4ba56 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) import annotation.retains class C def f(x: C @retains(caps.cap), y: C): () -> C = diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.check b/tests/neg-custom-args/captures/effect-swaps-explicit.check index 8c4d1f315fd8..47559ab97568 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.check +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.check @@ -1,29 +1,29 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:62:8 ------------------------- -61 | Result: -62 | Future: // error, type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:64:8 ------------------------- +63 | Result: +64 | Future: // error, type mismatch | ^ | Found: Result.Ok[box Future[box T^?]^{fr, contextual$1}] | Required: Result[Future[T], Nothing] -63 | fr.await.ok +65 | fr.await.ok |-------------------------------------------------------------------------------------------------------------------- |Inline stack trace |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |This location contains code that was inlined from effect-swaps-explicit.scala:39 -39 | boundary(Ok(body)) + |This location contains code that was inlined from effect-swaps-explicit.scala:41 +41 | boundary(Ok(body)) | ^^^^^^^^ -------------------------------------------------------------------------------------------------------------------- | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:72:10 ------------------------ -72 | Future: fut ?=> // error: type mismatch +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:74:10 ------------------------ +74 | Future: fut ?=> // error: type mismatch | ^ | Found: Future[box T^?]^{fr, lbl} | Required: Future[box T^?]^? -73 | fr.await.ok +75 | fr.await.ok | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:66:15 --------------------------------------------- -66 | Result.make: //lbl ?=> // error, escaping label from Result +-- Error: tests/neg-custom-args/captures/effect-swaps-explicit.scala:68:15 --------------------------------------------- +68 | Result.make: //lbl ?=> // error, escaping label from Result | ^^^^^^^^^^^ |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9}, box E^?]]^): | box Future[box T^?]^{fr, contextual$9} leaks into outer capture set of type parameter T of method make in object Result diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.scala b/tests/neg-custom-args/captures/effect-swaps-explicit.scala index 052beaab01b2..7474e1711b34 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.scala +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) object boundary: final class Label[-T] // extends caps.Capability diff --git a/tests/neg-custom-args/captures/filevar.scala b/tests/neg-custom-args/captures/filevar.scala index 0d9cbed164e3..2859f4c5e826 100644 --- a/tests/neg-custom-args/captures/filevar.scala +++ b/tests/neg-custom-args/captures/filevar.scala @@ -6,7 +6,7 @@ class File: class Service: var file: File^ = uninitialized // error - def log = file.write("log") + def log = file.write("log") // error, was OK under sealed def withFile[T](op: (l: caps.Capability) ?-> (f: File^{l}) => T): T = op(using caps.cap)(new File) diff --git a/tests/neg-custom-args/captures/i15749.scala b/tests/neg-custom-args/captures/i15749.scala new file mode 100644 index 000000000000..c5b59042085a --- /dev/null +++ b/tests/neg-custom-args/captures/i15749.scala @@ -0,0 +1,15 @@ +class Unit +object unit extends Unit + +type Top = Any^ + +type LazyVal[T] = Unit => T + +class Foo[T](val x: T) + +// Foo[□ Unit => T] +type BoxedLazyVal[T] = Foo[LazyVal[T]] + +def force[A](v: BoxedLazyVal[A]): A = + // Γ ⊢ v.x : □ {cap} Unit -> A + v.x(unit) // error: (unbox v.x)(unit), was ok under the sealed policy \ No newline at end of file diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check index 0f8f0bf6eac5..58582423b101 100644 --- a/tests/neg-custom-args/captures/i15772.check +++ b/tests/neg-custom-args/captures/i15772.check @@ -25,11 +25,11 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:34 --------------------------------------- 33 | val boxed2 : Observe[C]^ = box2(c) // error | ^ - | Found: box C^ - | Required: box C{val arg: C^?}^? + | Found: C^ + | Required: box C{val arg: C^?}^ | - | Note that the universal capability `cap` - | cannot be included in capture set ? + | Note that C^ cannot be box-converted to box C{val arg: C^?}^ + | since at least one of their capture sets contains the root capability `cap` | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- diff --git a/tests/neg-custom-args/captures/i15922.scala b/tests/neg-custom-args/captures/i15922.scala index 974870cd769c..89bf91493fcd 100644 --- a/tests/neg-custom-args/captures/i15922.scala +++ b/tests/neg-custom-args/captures/i15922.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to force sealed encapsulation checking) trait Cap { def use(): Int } type Id[X] = [T] -> (op: X => T) -> T def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) diff --git a/tests/neg-custom-args/captures/i15923-cases.scala b/tests/neg-custom-args/captures/i15923-cases.scala new file mode 100644 index 000000000000..83cfa554e8b9 --- /dev/null +++ b/tests/neg-custom-args/captures/i15923-cases.scala @@ -0,0 +1,7 @@ +trait Cap { def use(): Int } +type Id[X] = [T] -> (op: X => T) -> T +def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) + +def foo(x: Id[Cap^]) = { + x(_.use()) // error, was OK under sealed policy +} diff --git a/tests/neg-custom-args/captures/i16114.scala b/tests/neg-custom-args/captures/i16114.scala index d363bb665dc3..ec04fe9c9827 100644 --- a/tests/neg-custom-args/captures/i16114.scala +++ b/tests/neg-custom-args/captures/i16114.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) trait Cap { def use(): Int; def close(): Unit } def mkCap(): Cap^ = ??? diff --git a/tests/neg-custom-args/captures/i19330-alt2.scala b/tests/neg-custom-args/captures/i19330-alt2.scala index b49dce4b71ef..86634b45dbe3 100644 --- a/tests/neg-custom-args/captures/i19330-alt2.scala +++ b/tests/neg-custom-args/captures/i19330-alt2.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) import language.experimental.captureChecking trait Logger diff --git a/tests/neg-custom-args/captures/i19330.scala b/tests/neg-custom-args/captures/i19330.scala index 8acb0dd8f66b..5fbdc00db311 100644 --- a/tests/neg-custom-args/captures/i19330.scala +++ b/tests/neg-custom-args/captures/i19330.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to force sealed encapsulation checking) import language.experimental.captureChecking trait Logger diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 3095c1f2f4f9..4a8738118609 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,9 +1,8 @@ -- 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. + | The expression's type LazyList[Int]^ is not allowed to capture the root capability `cap`. + | This usually means that a capability persists longer than its allowed lifetime. 37 | tabulate(10) { i => 38 | if i > 9 then throw Ex1() 39 | i * i diff --git a/tests/neg-custom-args/captures/levels.check b/tests/neg-custom-args/captures/levels.check index 479a231a0404..2dae3ec3bbc6 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -1,12 +1,12 @@ --- Error: tests/neg-custom-args/captures/levels.scala:17:13 ------------------------------------------------------------ -17 | val _ = Ref[String => String]((x: String) => x) // error +-- Error: tests/neg-custom-args/captures/levels.scala:19:13 ------------------------------------------------------------ +19 | val _ = Ref[String => String]((x: String) => x) // error | ^^^^^^^^^^^^^^^^^^^^^ | Sealed type variable T cannot be instantiated to box String => String since | that type captures the root capability `cap`. | This is often caused by a local capability in an argument of constructor Ref | leaking as part of its result. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/levels.scala:22:11 --------------------------------------- -22 | r.setV(g) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/levels.scala:24:11 --------------------------------------- +24 | r.setV(g) // error | ^ | Found: box (x: String) ->{cap3} String | Required: box (x$0: String) ->? String diff --git a/tests/neg-custom-args/captures/levels.scala b/tests/neg-custom-args/captures/levels.scala index b28e87f03ef7..4709fd80d9b8 100644 --- a/tests/neg-custom-args/captures/levels.scala +++ b/tests/neg-custom-args/captures/levels.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) class CC def test1(cap1: CC^) = diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index d57d615cda64..ee32c3ce03f2 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -1,8 +1,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:11:8 ------------------------------------- 11 | x = q // error | ^ - | Found: (q : Proc) - | Required: () ->{p, q²} Unit + | Found: box () ->{q} Unit + | Required: box () ->{p, q²} Unit | | where: q is a parameter in method inner | q² is a parameter in method test @@ -12,14 +12,17 @@ 12 | x = (q: Proc) // error | ^^^^^^^ | Found: Proc - | Required: () ->{p, q} Unit + | Required: box () ->{p, q} Unit + | + | Note that () => Unit cannot be box-converted to box () ->{p, q} Unit + | since at least one of their capture sets contains the root capability `cap` | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:13:9 ------------------------------------- 13 | y = (q: Proc) // error | ^^^^^^^ | Found: Proc - | Required: () ->{p} Unit + | Required: box () ->{p} Unit | | Note that the universal capability `cap` | cannot be included in capture set {p} of variable y @@ -28,17 +31,20 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- 14 | y = q // error | ^ - | Found: (q : Proc) - | Required: () ->{p} Unit + | Found: box () ->{q} Unit + | Required: box () ->{p} Unit | | Note that reference (q : Proc), defined in method inner | cannot be included in outer capture set {p} of variable y | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/outer-var.scala:16:53 --------------------------------------------------------- +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:16:65 ------------------------------------ 16 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Sealed type variable A 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 apply - | leaking as part of its result. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: scala.collection.mutable.ListBuffer[box () => Unit] + | Required: box scala.collection.mutable.ListBuffer[box () ->? Unit]^? + | + | Note that the universal capability `cap` + | cannot be included in capture set ? of variable finalizeActions + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 45c1776d8c43..f20dbdf311ad 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -1,12 +1,12 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:21:11 -------------------------------------- -21 | cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:23:11 -------------------------------------- +23 | cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*} | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: List[box () ->{xs*} Unit] | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:32:7 --------------------------------------- -32 | (() => f.write()) :: Nil // error since {f*} !<: {xs*} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:34:7 --------------------------------------- +34 | (() => f.write()) :: Nil // error since {f*} !<: {xs*} | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: box List[box () ->{xs*} Unit]^? @@ -15,34 +15,34 @@ | cannot be included in outer capture set {xs*} of value cur | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:35:6 ------------------------------------------------------------ -35 | var cur: List[Proc] = xs // error: Illegal type for var +-- Error: tests/neg-custom-args/captures/reaches.scala:37:6 ------------------------------------------------------------ +37 | var cur: List[Proc] = xs // error: Illegal type for var | ^ | Mutable variable cur cannot have type List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. --- Error: tests/neg-custom-args/captures/reaches.scala:42:15 ----------------------------------------------------------- -42 | val cur = Ref[List[Proc]](xs) // error: illegal type for type argument to Ref +-- Error: tests/neg-custom-args/captures/reaches.scala:44:15 ----------------------------------------------------------- +44 | val cur = Ref[List[Proc]](xs) // error: illegal type for type argument to Ref | ^^^^^^^^^^^^^^^ | Sealed type variable T cannot be instantiated to List[box () => Unit] since | the part box () => Unit of that type captures the root capability `cap`. | This is often caused by a local capability in an argument of constructor Ref | leaking as part of its result. --- Error: tests/neg-custom-args/captures/reaches.scala:52:31 ----------------------------------------------------------- -52 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error +-- Error: tests/neg-custom-args/captures/reaches.scala:54:31 ----------------------------------------------------------- +54 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error | ^^^^^^^^^^^^^^^^^^^^ | Sealed type variable A 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 constructor Id | leaking as part of its result. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:61:27 -------------------------------------- -61 | val f1: File^{id*} = id(f) // error, since now id(f): File^ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:63:27 -------------------------------------- +63 | val f1: File^{id*} = id(f) // error, since now id(f): File^ | ^^^^^ | Found: File^{id, f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:78:5 ------------------------------------------------------------ -78 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) +-- Error: tests/neg-custom-args/captures/reaches.scala:80:5 ------------------------------------------------------------ +80 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) | ^^^^^^ | Reach capability cap and universal capability cap cannot both | appear in the type [B](f: ((box A ->{ps*} A, box A ->{ps*} A)) => B): List[B] of this expression diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index 6a5ffd51c2c6..eadb76c69e5b 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) class File: def write(): Unit = ??? diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index 50dcc16f5f54..7f8ab50bc222 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -1,46 +1,46 @@ --- [E190] Potential Issue Warning: tests/neg-custom-args/captures/real-try.scala:36:4 ---------------------------------- -36 | b.x +-- [E190] Potential Issue Warning: tests/neg-custom-args/captures/real-try.scala:38:4 ---------------------------------- +38 | b.x | ^^^ | Discarded non-Unit value of type () -> Unit. You may want to use `()`. | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/real-try.scala:12:2 ----------------------------------------------------------- -12 | try // error +-- Error: tests/neg-custom-args/captures/real-try.scala:14:2 ----------------------------------------------------------- +14 | 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:10 ---------------------------------------------------------- -18 | val x = try // error +15 | () => foo(1) +16 | catch +17 | case _: Ex1 => ??? +18 | case _: Ex2 => ??? +-- Error: tests/neg-custom-args/captures/real-try.scala:20:10 ---------------------------------------------------------- +20 | val x = 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. -19 | () => foo(1) -20 | catch -21 | case _: Ex1 => ??? -22 | case _: Ex2 => ??? --- Error: tests/neg-custom-args/captures/real-try.scala:24:10 ---------------------------------------------------------- -24 | val y = try // error +21 | () => foo(1) +22 | catch +23 | case _: Ex1 => ??? +24 | case _: Ex2 => ??? +-- Error: tests/neg-custom-args/captures/real-try.scala:26:10 ---------------------------------------------------------- +26 | val y = 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. -25 | () => Cell(foo(1)) -26 | catch -27 | case _: Ex1 => ??? -28 | case _: Ex2 => ??? --- Error: tests/neg-custom-args/captures/real-try.scala:30:10 ---------------------------------------------------------- -30 | val b = try // error +27 | () => Cell(foo(1)) +28 | catch +29 | case _: Ex1 => ??? +30 | case _: Ex2 => ??? +-- Error: tests/neg-custom-args/captures/real-try.scala:32:10 ---------------------------------------------------------- +32 | 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. -31 | Cell(() => foo(1)) -32 | catch -33 | case _: Ex1 => ??? -34 | case _: Ex2 => ??? +33 | Cell(() => foo(1)) +34 | catch +35 | case _: Ex1 => ??? +36 | case _: Ex2 => ??? diff --git a/tests/neg-custom-args/captures/real-try.scala b/tests/neg-custom-args/captures/real-try.scala index 23961e884ea3..51f1a0fdea5a 100644 --- a/tests/neg-custom-args/captures/real-try.scala +++ b/tests/neg-custom-args/captures/real-try.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) import language.experimental.saferExceptions class Ex1 extends Exception("Ex1") diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 3b96927de738..77a5fc06e05a 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,10 +1,12 @@ --- Error: tests/neg-custom-args/captures/try.scala:23:16 --------------------------------------------------------------- -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. +-- Error: tests/neg-custom-args/captures/try.scala:25:3 ---------------------------------------------------------------- +23 | val a = handle[Exception, CanThrow[Exception]] { +24 | (x: CanThrow[Exception]) => x +25 | }{ // error (but could be better) + | ^ + | The expression's type box CT[Exception]^ is not allowed to capture the root capability `cap`. + | This usually means that a capability persists longer than its allowed lifetime. +26 | (ex: Exception) => ??? +27 | } -- Error: tests/neg-custom-args/captures/try.scala:30:65 --------------------------------------------------------------- 30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error | ^ diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 3d25dff4cd2c..45a1b346a512 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -20,9 +20,9 @@ def handle[E <: Exception, R <: Top](op: CT[E]^ => R)(handler: E => R): R = catch case ex: E => handler(ex) def test = - val a = handle[Exception, CanThrow[Exception]] { // error + val a = handle[Exception, CanThrow[Exception]] { (x: CanThrow[Exception]) => x - }{ + }{ // error (but could be better) (ex: Exception) => ??? } diff --git a/tests/neg/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala similarity index 89% rename from tests/neg/unsound-reach-2.scala rename to tests/neg-custom-args/captures/unsound-reach-2.scala index 083cec6ee5b2..384af31ee1fc 100644 --- a/tests/neg/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) import language.experimental.captureChecking trait Consumer[-T]: def apply(x: T): Unit diff --git a/tests/neg/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala similarity index 89% rename from tests/neg/unsound-reach-3.scala rename to tests/neg-custom-args/captures/unsound-reach-3.scala index 71c27fe5007d..985beb7ae55d 100644 --- a/tests/neg/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) import language.experimental.captureChecking trait File: def close(): Unit diff --git a/tests/neg/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check similarity index 55% rename from tests/neg/unsound-reach-4.check rename to tests/neg-custom-args/captures/unsound-reach-4.check index 47256baf408a..9abf86c772d5 100644 --- a/tests/neg/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -1,5 +1,5 @@ --- Error: tests/neg/unsound-reach-4.scala:20:19 ------------------------------------------------------------------------ -20 | escaped = boom.use(f) // error +-- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:22:19 --------------------------------------------------- +22 | escaped = boom.use(f) // error | ^^^^^^^^ | Reach capability backdoor* and universal capability cap cannot both | appear in the type (x: F): box File^{backdoor*} of this expression diff --git a/tests/neg/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala similarity index 85% rename from tests/neg/unsound-reach-4.scala rename to tests/neg-custom-args/captures/unsound-reach-4.scala index fa395fa117ca..14050b4afff2 100644 --- a/tests/neg/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) import language.experimental.captureChecking trait File: def close(): Unit diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check new file mode 100644 index 000000000000..22b00b74deb1 --- /dev/null +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -0,0 +1,12 @@ +-- Error: tests/neg-custom-args/captures/unsound-reach.scala:18:13 ----------------------------------------------------- +18 | boom.use(f): (f1: File^{backdoor*}) => // error + | ^^^^^^^^ + | Reach capability backdoor* and universal capability cap cannot both + | appear in the type (x: File^)(op: box File^{backdoor*} => Unit): Unit of this expression +-- [E164] Declaration Error: tests/neg-custom-args/captures/unsound-reach.scala:10:8 ----------------------------------- +10 | def use(x: File^)(op: File^ => Unit): Unit = op(x) // error, was OK using sealed checking + | ^ + | error overriding method use in trait Foo of type (x: File^)(op: box File^ => Unit): Unit; + | method use of type (x: File^)(op: File^ => Unit): Unit has incompatible type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala similarity index 83% rename from tests/neg/unsound-reach.scala rename to tests/neg-custom-args/captures/unsound-reach.scala index 48a74f86d311..c3c31a7f32ff 100644 --- a/tests/neg/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -7,7 +7,7 @@ def withFile[R](path: String)(op: File^ => R): R = ??? trait Foo[+X]: def use(x: File^)(op: X => Unit): Unit class Bar extends Foo[File^]: - def use(x: File^)(op: File^ => Unit): Unit = op(x) + def use(x: File^)(op: File^ => Unit): Unit = op(x) // error, was OK using sealed checking def bad(): Unit = val backdoor: Foo[File^] = new Bar diff --git a/tests/neg-custom-args/captures/vars-simple.check b/tests/neg-custom-args/captures/vars-simple.check index 2bc014e9a4e7..2ef301b6ec1f 100644 --- a/tests/neg-custom-args/captures/vars-simple.check +++ b/tests/neg-custom-args/captures/vars-simple.check @@ -2,14 +2,17 @@ 15 | a = (g: String => String) // error | ^^^^^^^^^^^^^^^^^^^ | Found: String => String - | Required: String ->{cap1, cap2} String + | Required: box String ->{cap1, cap2} String + | + | Note that String => String cannot be box-converted to box String ->{cap1, cap2} String + | since at least one of their capture sets contains the root capability `cap` | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars-simple.scala:16:8 ----------------------------------- 16 | a = g // error | ^ - | Found: (x: String) ->{cap3} String - | Required: (x: String) ->{cap1, cap2} String + | Found: box (x: String) ->{cap3} String + | Required: box (x: String) ->{cap1, cap2} String | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars-simple.scala:17:12 ---------------------------------- diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index e2d817f2d8bd..e4b1e71a2000 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,12 +1,12 @@ --- Error: tests/neg-custom-args/captures/vars.scala:22:14 -------------------------------------------------------------- -22 | a = x => g(x) // error +-- Error: tests/neg-custom-args/captures/vars.scala:24:14 -------------------------------------------------------------- +24 | a = x => g(x) // error | ^^^^ | reference (cap3 : Cap) is not included in the allowed capture set {cap1} of variable a | | Note that reference (cap3 : Cap), defined in method scope | cannot be included in outer capture set {cap1} of variable a --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:23:8 ------------------------------------------ -23 | a = g // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:8 ------------------------------------------ +25 | a = g // error | ^ | Found: (x: String) ->{cap3} String | Required: (x$0: String) ->{cap1} String @@ -15,14 +15,14 @@ | cannot be included in outer capture set {cap1} of variable a | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:25:12 ----------------------------------------- -25 | b = List(g) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- +27 | b = List(g) // error | ^^^^^^^ | Found: List[box (x$0: String) ->{cap3} String] | Required: List[box String ->{cap1, cap2} String] | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/vars.scala:34:2 --------------------------------------------------------------- -34 | local { cap3 => // error +-- Error: tests/neg-custom-args/captures/vars.scala:36:2 --------------------------------------------------------------- +36 | local { cap3 => // error | ^^^^^ | local reference cap3 leaks into outer capture set of type parameter T of method local diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index ab5a2f43acc7..5eb1e3fedda9 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) class CC type Cap = CC^ diff --git a/tests/neg/unsound-reach.check b/tests/neg/unsound-reach.check deleted file mode 100644 index 8cabbe1571a0..000000000000 --- a/tests/neg/unsound-reach.check +++ /dev/null @@ -1,5 +0,0 @@ --- Error: tests/neg/unsound-reach.scala:18:13 -------------------------------------------------------------------------- -18 | boom.use(f): (f1: File^{backdoor*}) => // error - | ^^^^^^^^ - | Reach capability backdoor* and universal capability cap cannot both - | appear in the type (x: File^)(op: box File^{backdoor*} => Unit): Unit of this expression diff --git a/tests/pos-custom-args/captures/casts.scala b/tests/pos-custom-args/captures/casts.scala new file mode 100644 index 000000000000..572b58d008f6 --- /dev/null +++ b/tests/pos-custom-args/captures/casts.scala @@ -0,0 +1,4 @@ +import language.experimental.captureChecking +def Test = + val x: Any = ??? + val y = x.asInstanceOf[Int => Int] diff --git a/tests/pos-custom-args/captures/filevar-expanded.scala b/tests/pos-custom-args/captures/filevar-expanded.scala index 13051994f346..a883471e8d2e 100644 --- a/tests/pos-custom-args/captures/filevar-expanded.scala +++ b/tests/pos-custom-args/captures/filevar-expanded.scala @@ -32,5 +32,6 @@ object test2: def test(io3: IO^) = withFile(io3): f => val o = Service(io3) - o.file = f + o.file = f // this is a bit dubious. It's legal since we treat class refinements + // as capture set variables that can be made to include refs coming from outside. o.log diff --git a/tests/pos-custom-args/captures/i15749.scala b/tests/pos-custom-args/captures/i15749.scala index 0a552ae1a3c5..58274c7cc817 100644 --- a/tests/pos-custom-args/captures/i15749.scala +++ b/tests/pos-custom-args/captures/i15749.scala @@ -1,3 +1,5 @@ +//> using options -source 3.4 +// (to make sure we use the sealed policy) class Unit object unit extends Unit @@ -12,4 +14,4 @@ type BoxedLazyVal[T] = Foo[LazyVal[T]] def force[A](v: BoxedLazyVal[A]): A = // Γ ⊢ v.x : □ {cap} Unit -> A - v.x(unit) // was error: (unbox v.x)(unit), where (unbox v.x) should be untypable, now ok \ No newline at end of file + v.x(unit) // should be error: (unbox v.x)(unit), where (unbox v.x) should be untypable, now ok \ No newline at end of file diff --git a/tests/pos-custom-args/captures/i15923-cases.scala b/tests/pos-custom-args/captures/i15923-cases.scala index 7c5635f7b3dd..4b5a36f208ec 100644 --- a/tests/pos-custom-args/captures/i15923-cases.scala +++ b/tests/pos-custom-args/captures/i15923-cases.scala @@ -2,10 +2,6 @@ trait Cap { def use(): Int } type Id[X] = [T] -> (op: X => T) -> T def mkId[X](x: X): Id[X] = [T] => (op: X => T) => op(x) -def foo(x: Id[Cap^]) = { - x(_.use()) // was error, now OK -} - def bar(io: Cap^, x: Id[Cap^{io}]) = { x(_.use()) } diff --git a/tests/pos-custom-args/captures/i15925.scala b/tests/pos-custom-args/captures/i15925.scala index 63b6962ff9f8..1c448c7377c2 100644 --- a/tests/pos-custom-args/captures/i15925.scala +++ b/tests/pos-custom-args/captures/i15925.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +import annotation.unchecked.uncheckedCaptures class Unit object u extends Unit @@ -6,8 +7,8 @@ object u extends Unit type Foo[X] = [T] -> (op: X => T) -> T type Lazy[X] = Unit => X -def force[X](fx: Foo[Lazy[X]]): X = +def force[X](fx: Foo[Lazy[X] @uncheckedCaptures]): X = fx[X](f => f(u)) -def force2[X](fx: Foo[Unit => X]): X = +def force2[X](fx: Foo[(Unit => X) @uncheckedCaptures]): X = fx[X](f => f(u)) diff --git a/tests/pos-custom-args/captures/levels.scala b/tests/pos-custom-args/captures/levels.scala new file mode 100644 index 000000000000..cabd537442a5 --- /dev/null +++ b/tests/pos-custom-args/captures/levels.scala @@ -0,0 +1,23 @@ +class CC + +def test1(cap1: CC^) = + + class Ref[T](init: T): + private var v: T = init + def setV(x: T): Unit = v = x + def getV: T = v + +def test2(cap1: CC^) = + + class Ref[T](init: T): + private var v: T = init + def setV(x: T): Unit = v = x + def getV: T = v + + val _ = Ref[String => String]((x: String) => x) // ok + val r = Ref((x: String) => x) + + def scope(cap3: CC^) = + def g(x: String): String = if cap3 == cap3 then "" else "a" + r.setV(g) // error + () diff --git a/tests/pos-custom-args/captures/unsafe-captures.scala b/tests/pos-custom-args/captures/unsafe-captures.scala new file mode 100644 index 000000000000..5e0144331344 --- /dev/null +++ b/tests/pos-custom-args/captures/unsafe-captures.scala @@ -0,0 +1,8 @@ +import annotation.unchecked.uncheckedCaptures +class LL[+A] private (private var lazyState: (() => LL.State[A]^) @uncheckedCaptures): + private val res = lazyState() // without unchecked captures we get a van't unbox cap error + + +object LL: + + private trait State[+A] diff --git a/tests/pos-custom-args/captures/untracked-captures.scala b/tests/pos-custom-args/captures/untracked-captures.scala new file mode 100644 index 000000000000..7a090a5dd24f --- /dev/null +++ b/tests/pos-custom-args/captures/untracked-captures.scala @@ -0,0 +1,34 @@ +import caps.untrackedCaptures +class LL[+A] private (@untrackedCaptures lazyState: () => LL.State[A]^): + private val res = lazyState() + + +object LL: + + private trait State[+A] + private object State: + object Empty extends State[Nothing] + + private def newLL[A](state: () => State[A]^): LL[A]^{state} = ??? + + private def sCons[A](hd: A, tl: LL[A]^): State[A]^{tl} = ??? + + def filterImpl[A](ll: LL[A]^, p: A => Boolean): LL[A]^{ll, p} = + // DO NOT REFERENCE `ll` ANYWHERE ELSE, OR IT WILL LEAK THE HEAD + var restRef: LL[A]^{ll} = ll // restRef is captured by closure arg to newLL, so A is not recognized as parametric + + val cl = () => + var elem: A = null.asInstanceOf[A] + var found = false + var rest = restRef // Without untracked captures a type ascription would be needed here + // because the compiler tries to keep track of lazyState in refinements + // of LL and gets confused (c.f Setup.addCaptureRefinements) + + while !found do + found = p(elem) + rest = rest + restRef = rest + val res = if found then sCons(elem, filterImpl(rest, p)) else State.Empty + ??? : State[A]^{ll, p} + val nll = newLL(cl) + nll diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 20a6a33d3e02..5443758afa72 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -552,7 +552,7 @@ object CollectionStrawMan5 { } def flatMap[B](f: A => IterableOnce[B]^): Iterator[B]^{this, f} = new Iterator[B] { - private var myCurrent: Iterator[B]^{this} = Iterator.empty + private var myCurrent: Iterator[B]^{this, f} = Iterator.empty private def current = { while (!myCurrent.hasNext && self.hasNext) myCurrent = f(self.next()).iterator From 37dc2d5fae19d6972002b882632cbfbac9b6fe89 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 10 Jun 2024 17:58:09 +0200 Subject: [PATCH 07/35] More robust scheme to re-check definitions once. The previous scheme relied on subtle and unstated assumptions between symbol updates and re-checking. If they were violated some definitions could not be rechecked at all. The new scheme is more robust. We always re-check except when the checker implementation returns true for `skipRecheck`. And that test is based on an explicitly maintained set of completed symbols. --- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 8 +++++++- .../src/dotty/tools/dotc/transform/Recheck.scala | 14 ++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6e4a10efe607..bf25d448f402 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1185,6 +1185,11 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) + private val completed = new mutable.HashSet[Symbol] + + override def skipRecheck(sym: Symbol)(using Context): Boolean = + completed.contains(sym) + /** Check a ValDef or DefDef as an action performed in a completer. Since * these checks can appear out of order, we need to firsty create the correct * environment for checking the definition. @@ -1205,7 +1210,8 @@ class CheckCaptures extends Recheck, SymTransformer: case None => Env(sym, EnvKind.Regular, localSet, restoreEnvFor(sym.owner)) curEnv = restoreEnvFor(sym.owner) capt.println(i"Complete $sym in ${curEnv.outersIterator.toList.map(_.owner)}") - recheckDef(tree, sym) + try recheckDef(tree, sym) + finally completed += sym finally curEnv = saved diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index f025c9e9369f..3aec18dc2bd0 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -454,12 +454,16 @@ abstract class Recheck extends Phase, SymTransformer: case _ => traverse(stats) + /** A hook to prevent rechecking a ValDef or DefDef. + * Typycally used when definitions are completed on first use. + */ + def skipRecheck(sym: Symbol)(using Context) = false + def recheckDef(tree: ValOrDefDef, sym: Symbol)(using Context): Type = - inContext(ctx.localContext(tree, sym)) { + inContext(ctx.localContext(tree, sym)): tree match case tree: ValDef => recheckValDef(tree, sym) case tree: DefDef => recheckDefDef(tree, sym) - } /** Recheck tree without adapting it, returning its new type. * @param tree the original tree @@ -476,10 +480,8 @@ abstract class Recheck extends Phase, SymTransformer: case tree: ValOrDefDef => if tree.isEmpty then NoType else - if sym.isUpdatedAfter(preRecheckPhase) then - sym.ensureCompleted() // in this case the symbol's completer should recheck the right hand side - else - recheckDef(tree, sym) + sym.ensureCompleted() + if !skipRecheck(sym) then recheckDef(tree, sym) sym.termRef case tree: TypeDef => // TODO: Should we allow for completers as for ValDefs or DefDefs? From 61b66470e275a5814851cbc107bf03fb8211b709 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 10 Jun 2024 18:14:36 +0200 Subject: [PATCH 08/35] Show unsuccessful subCapture tests in TypeMismatch explanations --- .../dotty/tools/dotc/core/TypeComparer.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 4f6f857b45d8..621c9a1b920b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3803,6 +3803,11 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa private val b = new StringBuilder private var lastForwardGoal: String | Null = null + private def appendFailure(x: String) = + if lastForwardGoal != null then // last was deepest goal that failed + b.append(s" = $x") + lastForwardGoal = null + override def traceIndented[T](str: String)(op: => T): T = val str1 = str.replace('\n', ' ') if short && str1 == lastForwardGoal then @@ -3814,12 +3819,13 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa b.append("\n").append(" " * indent).append("==> ").append(str1) val res = op if short then - if res == false then - if lastForwardGoal != null then // last was deepest goal that failed - b.append(" = false") - lastForwardGoal = null - else - b.length = curLength // don't show successful subtraces + res match + case false => + appendFailure("false") + case res: CaptureSet.CompareResult if res != CaptureSet.CompareResult.OK => + appendFailure(show(res)) + case _ => + b.length = curLength // don't show successful subtraces else b.append("\n").append(" " * indent).append("<== ").append(str1).append(" = ").append(show(res)) indent -= 2 From f0ddb50ccb378257a401ce9c9ab678bfead8daf9 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 10 Jun 2024 18:33:35 +0200 Subject: [PATCH 09/35] Enable existential capabilities Enabled from 3.5. There are still a number of open questions - Clarify type inference with existentials propagating into capture sets. Right now, no pos or run test exercises this. - Also map arguments of function to existentials (at least double flip ones). - Adapt reach capabilities and drop previous restrictions. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 17 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 14 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 95 ++++++---- .../src/dotty/tools/dotc/cc/Existential.scala | 176 +++++++----------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 1 + .../src/dotty/tools/dotc/core/NameKinds.scala | 2 +- .../src/dotty/tools/dotc/typer/Namer.scala | 2 +- .../captures/refine-reach-shallow.scala | 2 +- tests/neg/cc-ex-conformance.scala | 4 +- .../pos-custom-args/captures/capt-test.scala | 1 + 10 files changed, 161 insertions(+), 153 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7a8ed7b3651a..49dbc9773229 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -15,6 +15,7 @@ import StdNames.nme import config.Feature import collection.mutable import CCState.* +import reporting.Message private val Captures: Key[CaptureSet] = Key() @@ -26,7 +27,8 @@ object ccConfig: */ inline val allowUnsoundMaps = false - val useExistentials = false + def useExistentials(using Context) = + Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`) /** If true, use `sealed` as encapsulation mechanism instead of the * previous global retriction that `cap` can't be boxed or unboxed. @@ -69,6 +71,11 @@ class CCState: */ var levelError: Option[CaptureSet.CompareResult.LevelError] = None + /** Warnings relating to upper approximations of capture sets with + * existentially bound variables. + */ + val approxWarnings: mutable.ListBuffer[Message] = mutable.ListBuffer() + private var curLevel: Level = outermostLevel private val symLevel: mutable.Map[Symbol, Int] = mutable.Map() @@ -356,6 +363,7 @@ extension (tp: Type) ok = false case _ => traverseChildren(t) + end CheckContraCaps object narrowCaps extends TypeMap: /** Has the variance been flipped at this point? */ @@ -368,12 +376,19 @@ extension (tp: Type) t.dealias match case t1 @ CapturingType(p, cs) if cs.isUniversal && !isFlipped => t1.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet) + case t @ FunctionOrMethod(args, res @ Existential(_, _)) + if args.forall(_.isAlwaysPure) => + // Also map existentials in results to reach capabilities if all + // preceding arguments are known to be always pure + apply(t.derivedFunctionOrMethod(args, Existential.toCap(res))) case _ => t match case t @ CapturingType(p, cs) => t.derivedCapturingType(apply(p), cs) // don't map capture set variables case t => mapOver(t) finally isFlipped = saved + end narrowCaps + ref match case ref: CaptureRef if ref.isTrackableRef => val checker = new CheckContraCaps diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 9b0afbf3567e..4069b9ffb014 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -562,7 +562,14 @@ object CaptureSet: universal else computingApprox = true - try computeApprox(origin).ensuring(_.isConst) + try + val approx = computeApprox(origin).ensuring(_.isConst) + if approx.elems.exists(Existential.isExistentialVar(_)) then + ccState.approxWarnings += + em"""Capture set variable $this gets upper-approximated + |to existential variable from $approx, using {cap} instead.""" + universal + else approx finally computingApprox = false /** The intersection of all upper approximations of dependent sets */ @@ -757,9 +764,8 @@ object CaptureSet: CompareResult.OK else source.tryInclude(bimap.backward(elem), this) - .showing(i"propagating new elem $elem backward from $this to $source = $result", capt) - .andAlso: - addNewElem(elem) + .showing(i"propagating new elem $elem backward from $this to $source = $result", captDebug) + .andAlso(addNewElem(elem)) /** For a BiTypeMap, supertypes of the mapped type also constrain * the source via the inverse type mapping and vice versa. That is, if diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index bf25d448f402..ec37a46ef5af 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -223,6 +223,9 @@ class CheckCaptures extends Recheck, SymTransformer: if tpt.isInstanceOf[InferredTypeTree] then interpolator().traverse(tpt.knownType) .showing(i"solved vars in ${tpt.knownType}", capt) + for msg <- ccState.approxWarnings do + report.warning(msg, tpt.srcPos) + ccState.approxWarnings.clear() /** Assert subcapturing `cs1 <: cs2` */ def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = @@ -492,7 +495,7 @@ class CheckCaptures extends Recheck, SymTransformer: tp.derivedCapturingType(forceBox(parent), refs) mapArgUsing(forceBox) else - super.recheckApply(tree, pt) match + Existential.toCap(super.recheckApply(tree, pt)) match case appType @ CapturingType(appType1, refs) => tree.fun match case Select(qual, _) @@ -505,7 +508,7 @@ class CheckCaptures extends Recheck, SymTransformer: val callCaptures = tree.args.foldLeft(qual.tpe.captureSet): (cs, arg) => cs ++ arg.tpe.captureSet appType.derivedCapturingType(appType1, callCaptures) - .showing(i"narrow $tree: $appType, refs = $refs, qual = ${qual.tpe.captureSet} --> $result", capt) + .showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qual.tpe.captureSet} = $result", capt) case _ => appType case appType => appType end recheckApply @@ -591,7 +594,7 @@ class CheckCaptures extends Recheck, SymTransformer: 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) - super.recheckTypeApply(tree, pt) + Existential.toCap(super.recheckTypeApply(tree, pt)) override def recheckBlock(tree: Block, pt: Type)(using Context): Type = inNestedLevel(super.recheckBlock(tree, pt)) @@ -624,7 +627,12 @@ class CheckCaptures extends Recheck, SymTransformer: // 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) + val res1 = Existential.toCapDeeply(res) + val pt1 = Existential.toCapDeeply(pt) + // We need to open existentials here in order not to get vars mixed up in them + // We do the proper check with existentials when we are finished with the closure block. + capt.println(i"pre-check closure $expr of type $res1 against $pt1") + checkConformsExpr(res1, pt1, expr) recheckDef(mdef, mdef.symbol) res finally @@ -1009,35 +1017,50 @@ class CheckCaptures extends Recheck, SymTransformer: */ def adaptBoxed(actual: Type, expected: Type, pos: SrcPos, covariant: Boolean, alwaysConst: Boolean, boxErrors: BoxErrors)(using Context): Type = - /** Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation - * @param boxed if true we adapt to a boxed expected type - */ - def adaptShape(actualShape: Type, boxed: Boolean): (Type, CaptureSet) = actualShape match - case FunctionOrMethod(aargs, ares) => - val saved = curEnv - curEnv = Env( - curEnv.owner, EnvKind.NestedInOwner, - CaptureSet.Var(curEnv.owner, level = currentLevel), - if boxed then null else curEnv) - try - val (eargs, eres) = expected.dealias.stripCapturing match - case FunctionOrMethod(eargs, eres) => (eargs, eres) - case _ => (aargs.map(_ => WildcardType), WildcardType) - val aargs1 = aargs.zipWithConserve(eargs): - adaptBoxed(_, _, pos, !covariant, alwaysConst, boxErrors) - val ares1 = adaptBoxed(ares, eres, pos, covariant, alwaysConst, boxErrors) - val resTp = - if (aargs1 eq aargs) && (ares1 eq ares) then actualShape // optimize to avoid redundant matches - else actualShape.derivedFunctionOrMethod(aargs1, ares1) - (resTp, CaptureSet(curEnv.captured.elems)) - finally curEnv = saved - case _ => - (actualShape, CaptureSet()) + def recur(actual: Type, expected: Type, covariant: Boolean): Type = + + /** Adapt the inner shape type: get the adapted shape type, and the capture set leaked during adaptation + * @param boxed if true we adapt to a boxed expected type + */ + def adaptShape(actualShape: Type, boxed: Boolean): (Type, CaptureSet) = actualShape match + case FunctionOrMethod(aargs, ares) => + val saved = curEnv + curEnv = Env( + curEnv.owner, EnvKind.NestedInOwner, + CaptureSet.Var(curEnv.owner, level = currentLevel), + if boxed then null else curEnv) + try + val (eargs, eres) = expected.dealias.stripCapturing match + case FunctionOrMethod(eargs, eres) => (eargs, eres) + case _ => (aargs.map(_ => WildcardType), WildcardType) + val aargs1 = aargs.zipWithConserve(eargs): + recur(_, _, !covariant) + val ares1 = recur(ares, eres, covariant) + val resTp = + if (aargs1 eq aargs) && (ares1 eq ares) then actualShape // optimize to avoid redundant matches + else actualShape.derivedFunctionOrMethod(aargs1, ares1) + (resTp, CaptureSet(curEnv.captured.elems)) + finally curEnv = saved + case _ => + (actualShape, CaptureSet()) + end adaptShape - def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected" + def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected" + + actual match + case actual @ Existential(_, actualUnpacked) => + return Existential.derivedExistentialType(actual): + recur(actualUnpacked, expected, covariant) + case _ => + expected match + case expected @ Existential(_, expectedUnpacked) => + return recur(actual, expectedUnpacked, covariant) + case _: WildcardType => + return actual + case _ => + + trace(adaptStr, capt, show = true) { - if expected.isInstanceOf[WildcardType] then actual - else trace(adaptStr, recheckr, show = true): // Decompose the actual type into the inner shape type, the capture set and the box status val actualShape = if actual.isFromJavaObject then actual else actual.stripCapturing val actualIsBoxed = actual.isBoxedCapturing @@ -1099,6 +1122,10 @@ class CheckCaptures extends Recheck, SymTransformer: adaptedType(!actualIsBoxed) else adaptedType(actualIsBoxed) + } + end recur + + recur(actual, expected, covariant) end adaptBoxed /** If actual derives from caps.Capability, yet is not a capturing type itself, @@ -1139,7 +1166,7 @@ class CheckCaptures extends Recheck, SymTransformer: widened.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false, boxErrors) if adapted eq widened then normalized - else adapted.showing(i"adapt boxed $actual vs $expected ===> $adapted", capt) + else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt) end adapt /** Check overrides again, taking capture sets into account. @@ -1154,13 +1181,13 @@ class CheckCaptures extends Recheck, SymTransformer: * @param sym symbol of the field definition that is being checked */ override def checkSubType(actual: Type, expected: Type)(using Context): Boolean = - val expected1 = alignDependentFunction(addOuterRefs(/*Existential.strip*/(expected), actual), actual.stripCapturing) + val expected1 = alignDependentFunction(addOuterRefs(expected, actual), actual.stripCapturing) val actual1 = val saved = curEnv try curEnv = Env(clazz, EnvKind.NestedInOwner, capturedVars(clazz), outer0 = curEnv) val adapted = - adaptBoxed(/*Existential.strip*/(actual), expected1, srcPos, covariant = true, alwaysConst = true, null) + adaptBoxed(actual, expected1, srcPos, covariant = true, alwaysConst = true, null) actual match case _: MethodType => // We remove the capture set resulted from box adaptation for method types, diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 0dba1a62e7ed..0c269a484092 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -9,7 +9,7 @@ import StdNames.nme import ast.tpd.* import Decorators.* import typer.ErrorReporting.errorType -import NameKinds.exSkolemName +import NameKinds.ExistentialBinderName import reporting.Message /** @@ -195,22 +195,61 @@ object Existential: type Carrier = RefinedType - def openExpected(pt: Type)(using Context): Type = pt.dealias match + def unapply(tp: Carrier)(using Context): Option[(TermParamRef, Type)] = + tp.refinedInfo match + case mt: MethodType + if isExistentialMethod(mt) && defn.isNonRefinedFunction(tp.parent) => + Some(mt.paramRefs.head, mt.resultType) + case _ => None + + /** Create method type in the refinement of an existential type */ + private def exMethodType(mk: TermParamRef => Type)(using Context): MethodType = + val boundName = ExistentialBinderName.fresh() + MethodType(boundName :: Nil)( + mt => defn.Caps_Exists.typeRef :: Nil, + mt => mk(mt.paramRefs.head)) + + /** Create existential */ + def apply(mk: TermParamRef => Type)(using Context): Type = + exMethodType(mk).toFunctionType(alwaysDependent = true) + + /** Create existential if bound variable appears in result of `mk` */ + def wrap(mk: TermParamRef => Type)(using Context): Type = + val mt = exMethodType(mk) + if mt.isResultDependent then mt.toFunctionType() else mt.resType + + extension (tp: Carrier) + def derivedExistentialType(core: Type)(using Context): Type = tp match + case Existential(boundVar, unpacked) => + if core eq unpacked then tp + else apply(bv => core.substParam(boundVar, bv)) + case _ => + core + + /** Map top-level existentials to `cap`. Do the same for existentials + * in function results if all preceding arguments are known to be always pure. + */ + def toCap(tp: Type)(using Context): Type = tp.dealias match case Existential(boundVar, unpacked) => - val tm = new IdempotentCaptRefMap: - val cvar = CaptureSet.Var(ctx.owner) - def apply(t: Type) = mapOver(t) match - case t @ CapturingType(parent, refs) if refs.elems.contains(boundVar) => - assert(refs.isConst && refs.elems.size == 1, i"malformed existential $t") - t.derivedCapturingType(parent, cvar) - case t => - t - openExpected(tm(unpacked)) - case _ => pt - - def toCap(tp: Type)(using Context) = tp.dealias match + val transformed = unpacked.substParam(boundVar, defn.captureRoot.termRef) + transformed match + case FunctionOrMethod(args, res @ Existential(_, _)) + if args.forall(_.isAlwaysPure) => + transformed.derivedFunctionOrMethod(args, toCap(res)) + case _ => + transformed + case _ => tp + + /** Map existentials at the top-level and in all nested result types to `cap` + */ + def toCapDeeply(tp: Type)(using Context): Type = tp.dealias match case Existential(boundVar, unpacked) => - unpacked.substParam(boundVar, defn.captureRoot.termRef) + toCapDeeply(unpacked.substParam(boundVar, defn.captureRoot.termRef)) + case tp1 @ FunctionOrMethod(args, res) => + val tp2 = tp1.derivedFunctionOrMethod(args, toCapDeeply(res)) + if tp2 ne tp1 then tp2 else tp + case tp1 @ CapturingType(parent, refs) => + tp1.derivedCapturingType(toCapDeeply(parent), refs) case _ => tp /** Replace all occurrences of `cap` in parts of this type by an existentially bound @@ -229,8 +268,9 @@ object Existential: needsWrap = true boundVar else - val varianceStr = if variance < 0 then "contra" else "in" - fail(em"cap appears in ${varianceStr}variant position in $tp") + if variance == 0 then + fail(em"cap appears in invariant position in $tp") + // we accept variance < 0, and leave the cap as it is t1 case t1 @ FunctionOrMethod(_, _) => // These have been mapped before @@ -251,11 +291,16 @@ object Existential: end Wrap if ccConfig.useExistentials then - val wrapped = apply(Wrap(_)(tp)) - if needsWrap then wrapped else tp + tp match + case Existential(_, _) => tp + case _ => + val wrapped = apply(Wrap(_)(tp)) + if needsWrap then wrapped else tp else tp end mapCap + /** Map `cap` to existential in the results of functions or methods. + */ def mapCapInResult(tp: Type, fail: Message => Unit)(using Context): Type = def mapCapInFinalResult(tp: Type): Type = tp match case tp: MethodOrPoly => @@ -263,104 +308,17 @@ object Existential: case _ => mapCap(tp, fail) tp match - case tp: MethodOrPoly => - mapCapInFinalResult(tp) - case defn.FunctionNOf(args, res, contextual) => - tp.derivedFunctionOrMethod(args, mapCap(res, fail)) + case tp: MethodOrPoly => mapCapInFinalResult(tp) case _ => tp - def strip(tp: Type)(using Context) = tp match - case Existential(_, tpunpacked) => tpunpacked - case _ => tp - - def skolemize(tp: Type)(using Context) = tp.widenDealias match // TODO needed? - case Existential(boundVar, unpacked) => - val skolem = tp match - case tp: CaptureRef if tp.isTracked => tp - case _ => newSkolemSym(boundVar.underlying).termRef - val tm = new IdempotentCaptRefMap: - var deep = false - private inline def deepApply(t: Type): Type = - val saved = deep - deep = true - try apply(t) finally deep = saved - def apply(t: Type) = - if t eq boundVar then - if deep then skolem.reach else skolem - else t match - case defn.FunctionOf(args, res, contextual) => - val res1 = deepApply(res) - if res1 ne res then defn.FunctionOf(args, res1, contextual) - else t - case defn.RefinedFunctionOf(mt) => - mt.derivedLambdaType(resType = deepApply(mt.resType)) - case _ => - mapOver(t) - tm(unpacked) - case _ => tp - end skolemize - - def newSkolemSym(tp: Type)(using Context): TermSymbol = // TODO needed? - newSymbol(ctx.owner.enclosingMethodOrClass, exSkolemName.fresh(), Synthetic, tp) -/* - def fromDepFun(arg: Tree)(using Context): Type = arg.tpe match - case RefinedType(parent, nme.apply, info: MethodType) if defn.isNonRefinedFunction(parent) => - info match - case info @ MethodType(_ :: Nil) - if info.paramInfos.head.derivesFrom(defn.Caps_Capability) => - apply(ref => info.resultType.substParams(info, ref :: Nil)) - case _ => - errorType(em"Malformed existential: dependent function must have a singgle parameter of type caps.Capability", arg.srcPos) - case _ => - errorType(em"Malformed existential: dependent function type expected", arg.srcPos) -*/ - private class PackMap(sym: Symbol, rt: RecType)(using Context) extends DeepTypeMap, IdempotentCaptRefMap: - def apply(tp: Type): Type = tp match - case ref: TermRef if ref.symbol == sym => TermRef(rt.recThis, defn.captureRoot) - case _ => mapOver(tp) - - /** Unpack current type from an existential `rt` so that all references bound by `rt` - * are recplaced by `ref`. - */ - private class OpenMap(rt: RecType, ref: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap: - def apply(tp: Type): Type = - if isExBound(tp, rt) then ref else mapOver(tp) - - /** Is `tp` a reference to the bound variable of `rt`? */ - private def isExBound(tp: Type, rt: Type)(using Context) = tp match - case tp @ TermRef(RecThis(rt1), _) => (rt1 eq rt) && tp.symbol == defn.captureRoot - case _ => false - - /** Open existential, replacing the bund variable by `ref` */ - def open(rt: RecType, ref: Type)(using Context): Type = OpenMap(rt, ref)(rt.parent) - - /** Create an existential type `ex c.` so that all references to `sym` in `tp` - * become references to the existentially bound variable `c`. - */ - def fromSymbol(tp: Type, sym: Symbol)(using Context): RecType = - RecType(PackMap(sym, _)(tp)) - + /** Is `mt` a method represnting an existential type when used in a refinement? */ def isExistentialMethod(mt: TermLambda)(using Context): Boolean = mt.paramInfos match case (info: TypeRef) :: rest => info.symbol == defn.Caps_Exists && rest.isEmpty case _ => false + /** Is `ref` this an existentially bound variable? */ def isExistentialVar(ref: CaptureRef)(using Context) = ref match case ref: TermParamRef => isExistentialMethod(ref.binder) case _ => false - def unapply(tp: Carrier)(using Context): Option[(TermParamRef, Type)] = - tp.refinedInfo match - case mt: MethodType - if isExistentialMethod(mt) && defn.isNonRefinedFunction(tp.parent) => - Some(mt.paramRefs.head, mt.resultType) - case _ => None - - def apply(mk: TermParamRef => Type)(using Context): MethodType = - MethodType(defn.Caps_Exists.typeRef :: Nil): mt => - mk(mt.paramRefs.head) - - /** Create existential if bound variable appear in result */ - def wrap(mk: TermParamRef => Type)(using Context): Type = - val mt = apply(mk) - if mt.isResultDependent then mt.toFunctionType() else mt.resType end Existential diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 35f22538f074..466948161acf 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -75,6 +75,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: && containsCovarRetains(symd.symbol.originDenotation.info) then symd.flags &~ Private else symd.flags + end newFlagsFor def isPreCC(sym: Symbol)(using Context): Boolean = sym.isTerm && sym.maybeOwner.isClass diff --git a/compiler/src/dotty/tools/dotc/core/NameKinds.scala b/compiler/src/dotty/tools/dotc/core/NameKinds.scala index a6348304c4d7..e9575c7d6c4a 100644 --- a/compiler/src/dotty/tools/dotc/core/NameKinds.scala +++ b/compiler/src/dotty/tools/dotc/core/NameKinds.scala @@ -325,6 +325,7 @@ object NameKinds { val TailLocalName: UniqueNameKind = new UniqueNameKind("$tailLocal") val TailTempName: UniqueNameKind = new UniqueNameKind("$tmp") val ExceptionBinderName: UniqueNameKind = new UniqueNameKind("ex") + val ExistentialBinderName: UniqueNameKind = new UniqueNameKind("ex$") val SkolemName: UniqueNameKind = new UniqueNameKind("?") val SuperArgName: UniqueNameKind = new UniqueNameKind("$superArg$") val DocArtifactName: UniqueNameKind = new UniqueNameKind("$doc") @@ -332,7 +333,6 @@ object NameKinds { val InlineScrutineeName: UniqueNameKind = new UniqueNameKind("$scrutinee") val InlineBinderName: UniqueNameKind = new UniqueNameKind("$proxy") val MacroNames: UniqueNameKind = new UniqueNameKind("$macro$") - val exSkolemName: UniqueNameKind = new UniqueNameKind("$exSkolem") // TODO needed? val UniqueExtMethName: UniqueNameKind = new UniqueNameKindWithUnmangle("$extension") diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 83964417a6f1..32467de77264 100644 --- a/compiler/src/dotty/tools/dotc/typer/Namer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Namer.scala @@ -1613,7 +1613,7 @@ class Namer { typer: Typer => else if pclazz.isEffectivelySealed && pclazz.associatedFile != cls.associatedFile then if pclazz.is(Sealed) && !pclazz.is(JavaDefined) then report.error(UnableToExtendSealedClass(pclazz), cls.srcPos) - else if sourceVersion.isAtLeast(future) then + else if sourceVersion.isAtLeast(`3.6`) then checkFeature(nme.adhocExtensions, i"Unless $pclazz is declared 'open', its extension in a separate file", cls.topLevelClass, diff --git a/tests/neg-custom-args/captures/refine-reach-shallow.scala b/tests/neg-custom-args/captures/refine-reach-shallow.scala index 9f4b28ce52e3..525d33fdb7c5 100644 --- a/tests/neg-custom-args/captures/refine-reach-shallow.scala +++ b/tests/neg-custom-args/captures/refine-reach-shallow.scala @@ -14,5 +14,5 @@ def test4(): Unit = val ys: List[IO^{xs*}] = xs // ok def test5(): Unit = val f: [R] -> (IO^ -> R) -> IO^ = ??? - val g: [R] -> (IO^ -> R) -> IO^{f*} = f // ok + val g: [R] -> (IO^ -> R) -> IO^{f*} = f // error val h: [R] -> (IO^{f*} -> R) -> IO^ = f // error diff --git a/tests/neg/cc-ex-conformance.scala b/tests/neg/cc-ex-conformance.scala index 9cfdda43c764..a953466daa9a 100644 --- a/tests/neg/cc-ex-conformance.scala +++ b/tests/neg/cc-ex-conformance.scala @@ -7,9 +7,9 @@ type EX1 = () => (c: Exists) => (C^{c}, C^{c}) type EX2 = () => (c1: Exists) => (c2: Exists) => (C^{c1}, C^{c2}) -type EX3 = () => (c: Exists) => () => C^{c} +type EX3 = () => (c: Exists) => (x: Object^) => C^{c} -type EX4 = () => () => (c: Exists) => C^{c} +type EX4 = () => (x: Object^) => (c: Exists) => C^{c} def Test = val ex1: EX1 = ??? diff --git a/tests/pos-custom-args/captures/capt-test.scala b/tests/pos-custom-args/captures/capt-test.scala index e229c685d846..49f199f106f1 100644 --- a/tests/pos-custom-args/captures/capt-test.scala +++ b/tests/pos-custom-args/captures/capt-test.scala @@ -36,3 +36,4 @@ def test(c: Cap, d: Cap) = val a4 = zs.map(identity) val a4c: LIST[Cap ->{d, y} Unit] = a4 + val a5: LIST[Cap ->{d, y} Unit] = zs.map(identity) From c50e01c736bb7e7933a4b486f5d3863e1ec0d396 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 11:21:39 +0200 Subject: [PATCH 10/35] Tighten rules against escaping local references Fixes the problem in effect-swaps.scala --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 6 ++++-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 6 ++---- tests/neg-custom-args/captures/effect-swaps.check | 4 ++++ tests/neg-custom-args/captures/effect-swaps.scala | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ec37a46ef5af..6d3ea34f4c0a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1131,7 +1131,9 @@ class CheckCaptures extends Recheck, SymTransformer: /** If actual derives from caps.Capability, yet is not a capturing type itself, * make its capture set explicit. */ - private def makeCaptureSetExplicit(actual: Type)(using Context): Type = actual match + private def makeCaptureSetExplicit(actual: Type)(using Context): Type = + if false then actual + else actual match case CapturingType(_, _) => actual case _ if actual.derivesFromCapability => val cap: CaptureRef = actual match @@ -1346,7 +1348,7 @@ class CheckCaptures extends Recheck, SymTransformer: case ref: TermParamRef if !allowed.contains(ref) && !seen.contains(ref) => seen += ref - if ref.underlying.isRef(defn.Caps_Capability) then + if ref.isMaxCapability 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 466948161acf..10796ca1bef1 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -612,10 +612,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: !refs.isEmpty case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol - if sym.isClass then - !sym.isPureClass - else - sym != defn.Caps_Capability && instanceCanBeImpure(tp.superType) + if sym.isClass then !sym.isPureClass + else instanceCanBeImpure(tp.superType) case tp: (RefinedOrRecType | MatchType) => instanceCanBeImpure(tp.underlying) case tp: AndType => diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index ef5a95d333bf..22941be36794 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -22,3 +22,7 @@ 73 | fr.await.ok | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:15 ------------------------------------------------------ +66 | Result.make: // error + | ^^^^^^^^^^^ + | escaping local reference contextual$9.type diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index d4eed2bae2f2..0b362b80e3ce 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -63,7 +63,7 @@ def test[T, E](using Async) = fr.await.ok def fail4[T, E](fr: Future[Result[T, E]]^) = - Result.make: //lbl ?=> // should be error, escaping label from Result but infers Result[Any, Any] + Result.make: // error Future: fut ?=> fr.await.ok From 03a3e412ccbaa597137165138545f52bd4b61d8b Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 14:36:54 +0200 Subject: [PATCH 11/35] Go back to expansion of capability class references at Setup This gives us the necessary levers to switch to existential capabilities. # Conflicts: # compiler/src/dotty/tools/dotc/cc/CaptureOps.scala --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 5 +++++ compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 6 +++--- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 ++++- tests/neg-custom-args/captures/byname.check | 7 ++++++- tests/neg-custom-args/captures/byname.scala | 3 +++ tests/neg-custom-args/captures/cc-this5.check | 2 +- tests/neg-custom-args/captures/extending-cap-classes.check | 2 +- tests/neg-custom-args/captures/i16725.scala | 4 ++-- 9 files changed, 26 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 49dbc9773229..bc1641b6f414 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -27,6 +27,11 @@ object ccConfig: */ inline val allowUnsoundMaps = false + /** If true, expand capability classes in Setup instead of treating them + * in adapt. + */ + inline val expandCapabilityInSetup = true + def useExistentials(using Context) = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 4069b9ffb014..eb3718e9601f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -163,7 +163,7 @@ sealed abstract class CaptureSet extends Showable: case y: TermRef => (y.prefix eq x) || y.info.match - case y1: CaptureRef => x.subsumes(y1) + case y1: SingletonCaptureRef => x.subsumes(y1) case _ => false case MaybeCapability(y1) => x.stripMaybe.subsumes(y1) case _ => false @@ -171,7 +171,7 @@ sealed abstract class CaptureSet extends Showable: case ReachCapability(x1) => x1.subsumes(y.stripReach) case x: TermRef => x.info match - case x1: CaptureRef => x1.subsumes(y) + case x1: SingletonCaptureRef => x1.subsumes(y) case _ => false case x: TermParamRef => canSubsumeExistentially(x, y) case _ => false @@ -1059,7 +1059,7 @@ object CaptureSet: case tp: TermParamRef => tp.captureSet case tp: TypeRef => - if tp.derivesFromCapability then universal // TODO: maybe return another value that indicates that the underltinf ref is maximal? + if !ccConfig.expandCapabilityInSetup && tp.derivesFromCapability then universal else empty case _: TypeParamRef => empty diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6d3ea34f4c0a..b73184447c47 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1132,7 +1132,7 @@ class CheckCaptures extends Recheck, SymTransformer: * make its capture set explicit. */ private def makeCaptureSetExplicit(actual: Type)(using Context): Type = - if false then actual + if ccConfig.expandCapabilityInSetup then actual else actual match case CapturingType(_, _) => actual case _ if actual.derivesFromCapability => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 10796ca1bef1..992f851831ad 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -323,7 +323,10 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case t: TypeVar => this(t.underlying) case t => - recur(t) + // Map references to capability classes C to C^ + if ccConfig.expandCapabilityInSetup && t.derivesFromCapability + then CapturingType(t, defn.expandedUniversalSet, boxed = false) + else recur(t) end expandAliases val tp1 = expandAliases(tp) // TODO: Do we still need to follow aliases? diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index b9e5c81b721d..c9530f6aad50 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -1,8 +1,13 @@ -- Error: tests/neg-custom-args/captures/byname.scala:19:5 ------------------------------------------------------------- 19 | h(g()) // error | ^^^ - | reference (cap2 : Cap) is not included in the allowed capture set {cap1} + | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} | of an enclosing function literal with expected type () ?->{cap1} I +-- Error: tests/neg-custom-args/captures/byname.scala:22:12 ------------------------------------------------------------ +22 | h2(() => g())() // error + | ^^^ + | reference (cap2 : Cap^) is not included in the allowed capture set {cap1} + | of an enclosing function literal with expected type () ->{cap1} I -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:4:2 ----------------------------------------- 4 | def f() = if cap1 == cap1 then g else g // error | ^ diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala index 0ed3a09cb414..75ad527dbd2d 100644 --- a/tests/neg-custom-args/captures/byname.scala +++ b/tests/neg-custom-args/captures/byname.scala @@ -17,6 +17,9 @@ def test2(cap1: Cap, cap2: Cap): I^{cap1} = def h(x: ->{cap1} I) = x // ok h(f()) // OK h(g()) // error + def h2(x: () ->{cap1} I) = x // ok + h2(() => f()) // OK + h2(() => g())() // error diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 1329734ce37d..8affe7005e2e 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 {} | 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/extending-cap-classes.check b/tests/neg-custom-args/captures/extending-cap-classes.check index 3bdddfd9dd3c..0936f48576e5 100644 --- a/tests/neg-custom-args/captures/extending-cap-classes.check +++ b/tests/neg-custom-args/captures/extending-cap-classes.check @@ -15,7 +15,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/extending-cap-classes.scala:13:15 ------------------------ 13 | val z2: C1 = y2 // error | ^^ - | Found: (y2 : C2)^{y2} + | Found: (y2 : C2^) | Required: C1 | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i16725.scala b/tests/neg-custom-args/captures/i16725.scala index 733c2c562bbc..1accf197c626 100644 --- a/tests/neg-custom-args/captures/i16725.scala +++ b/tests/neg-custom-args/captures/i16725.scala @@ -7,8 +7,8 @@ type Wrapper[T] = [R] -> (f: T => R) -> R def mk[T](x: T): Wrapper[T] = [R] => f => f(x) def useWrappedIO(wrapper: Wrapper[IO]): () -> Unit = () => - wrapper: io => + wrapper: io => // error io.brewCoffee() def main(): Unit = - val escaped = usingIO(io => useWrappedIO(mk(io))) // error + val escaped = usingIO(io => useWrappedIO(mk(io))) escaped() // boom From ab63489e62aa913795c64308414b1372e7293e5b Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 15:23:23 +0200 Subject: [PATCH 12/35] Drop expandedUniversalSet An expandedUniversalSet was the same as `{cap}` but not reference-equal to CaptureSet.universal. This construct was previously needed to avoid multiple expansions, but this does not seem to be the case any longer so the construct can be dropped. --- compiler/src/dotty/tools/dotc/cc/CapturingType.scala | 3 --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 ++--- compiler/src/dotty/tools/dotc/core/Definitions.scala | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index ee0cad4d4d03..f859b0d110aa 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -28,7 +28,6 @@ object CapturingType: /** Smart constructor that * - drops empty capture sets - * - drops a capability class expansion if it is further refined with another capturing type * - fuses compatible capturing types. * An outer type capturing type A can be fused with an inner capturing type B if their * boxing status is the same or if A is boxed. @@ -36,8 +35,6 @@ object CapturingType: def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type = if refs.isAlwaysEmpty then parent else parent match - case parent @ CapturingType(parent1, refs1) if refs1 eq defn.expandedUniversalSet => - apply(parent1, refs, boxed) case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed => apply(parent1, refs ++ refs1, boxed) case _ => diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 992f851831ad..e7bf584f9d44 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -325,7 +325,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case t => // Map references to capability classes C to C^ if ccConfig.expandCapabilityInSetup && t.derivesFromCapability - then CapturingType(t, defn.expandedUniversalSet, boxed = false) + then CapturingType(t, CaptureSet.universal, boxed = false) else recur(t) end expandAliases @@ -749,8 +749,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if ref.captureSetOfInfo.elems.isEmpty then report.error(em"$ref cannot be tracked since its capture set is empty", pos) - if parent.captureSet ne defn.expandedUniversalSet then - check(parent.captureSet, parent) + check(parent.captureSet, parent) val others = for j <- 0 until retained.length if j != i yield retained(j).toCaptureRef diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 88de8e66054e..57402ffe27bf 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -999,7 +999,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 expandedUniversalSet: CaptureSet = CaptureSet(captureRoot.termRef) @tu lazy val PureClass: Symbol = requiredClass("scala.Pure") From a8a92e1369f7ee4ba820be0e7f0b4ae7d65673f1 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 15:54:50 +0200 Subject: [PATCH 13/35] Adapt new capability class scheme to existentials --- compiler/src/dotty/tools/dotc/cc/Existential.scala | 10 ++++++++-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 0c269a484092..218713f85e1f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -229,7 +229,7 @@ object Existential: /** Map top-level existentials to `cap`. Do the same for existentials * in function results if all preceding arguments are known to be always pure. */ - def toCap(tp: Type)(using Context): Type = tp.dealias match + def toCap(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => val transformed = unpacked.substParam(boundVar, defn.captureRoot.termRef) transformed match @@ -238,11 +238,15 @@ object Existential: transformed.derivedFunctionOrMethod(args, toCap(res)) case _ => transformed + case tp1 @ CapturingType(parent, refs) => + tp1.derivedCapturingType(toCap(parent), refs) + case tp1 @ AnnotatedType(parent, ann) => + tp1.derivedAnnotatedType(toCap(parent), ann) case _ => tp /** Map existentials at the top-level and in all nested result types to `cap` */ - def toCapDeeply(tp: Type)(using Context): Type = tp.dealias match + def toCapDeeply(tp: Type)(using Context): Type = tp.dealiasKeepAnnots match case Existential(boundVar, unpacked) => toCapDeeply(unpacked.substParam(boundVar, defn.captureRoot.termRef)) case tp1 @ FunctionOrMethod(args, res) => @@ -250,6 +254,8 @@ object Existential: if tp2 ne tp1 then tp2 else tp case tp1 @ CapturingType(parent, refs) => tp1.derivedCapturingType(toCapDeeply(parent), refs) + case tp1 @ AnnotatedType(parent, ann) => + tp1.derivedAnnotatedType(toCapDeeply(parent), ann) case _ => tp /** Replace all occurrences of `cap` in parts of this type by an existentially bound diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index e7bf584f9d44..7e9f4e6e9c4b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -324,7 +324,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: this(t.underlying) case t => // Map references to capability classes C to C^ - if ccConfig.expandCapabilityInSetup && t.derivesFromCapability + if ccConfig.expandCapabilityInSetup && t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists then CapturingType(t, CaptureSet.universal, boxed = false) else recur(t) end expandAliases From 3b09cb3ee70612a09ef4818f4a1992a5f2c4a66e Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 17:43:26 +0200 Subject: [PATCH 14/35] Map capability classes to existentials # Conflicts: # compiler/src/dotty/tools/dotc/cc/Setup.scala --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 13 ++++++------- .../src/dotty/tools/dotc/core/TypeComparer.scala | 5 ++++- tests/pos/infer-exists.scala | 12 ++++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 tests/pos/infer-exists.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 7e9f4e6e9c4b..c8e7a8d89a89 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -300,9 +300,6 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: CapturingType(fntpe, cs, boxed = false) else fntpe - private def recur(t: Type): Type = - Existential.mapCapInResult(normalizeCaptures(mapOver(t)), fail) - def apply(t: Type) = t match case t @ CapturingType(parent, refs) => @@ -323,10 +320,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case t: TypeVar => this(t.underlying) case t => - // Map references to capability classes C to C^ - if ccConfig.expandCapabilityInSetup && t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists - then CapturingType(t, CaptureSet.universal, boxed = false) - else recur(t) + Existential.mapCapInResult( + // Map references to capability classes C to C^ + if ccConfig.expandCapabilityInSetup && t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists + then CapturingType(t, CaptureSet.universal, boxed = false) + else normalizeCaptures(mapOver(t)), + fail) end expandAliases val tp1 = expandAliases(tp) // TODO: Do we still need to follow aliases? diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 621c9a1b920b..41eb95c7b120 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2822,7 +2822,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => false protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - refs1.subCaptures(refs2, frozen) + try refs1.subCaptures(refs2, frozen) + catch case ex: AssertionError => + println(i"fail while subCaptures $refs1 <:< $refs2") + throw ex /** Is the boxing status of tp1 and tp2 the same, or alternatively, is * the capture sets `refs1` of `tp1` a subcapture of the empty set? diff --git a/tests/pos/infer-exists.scala b/tests/pos/infer-exists.scala new file mode 100644 index 000000000000..6d5225f75128 --- /dev/null +++ b/tests/pos/infer-exists.scala @@ -0,0 +1,12 @@ +import language.experimental.captureChecking + +class C extends caps.Capability +class D + +def test1 = + val a: (x: C) -> C = ??? + val b = a + +def test2 = + val a: (x: D^) -> D^ = ??? + val b = a From 5c527b62bbc8ba5ee48f945f7874b4e8cf0b1614 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 14 Jun 2024 20:50:30 +0200 Subject: [PATCH 15/35] Change encoding of impure dependent function types The encoding of (x: T) => U in capture checked code has changed. Previously: T => U' { def apply(x: T): U } Now: (T -> U' { def apply(x: T): U })^{cap} We often handle dependent functions by transforming the apply method and then mapping back to a function type using `.toFunctionType`. But that would always generate a pure function, so the impurity info could get lost. --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a5380f73a2a5..d3e411d5ea4d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1683,10 +1683,14 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(body.span) val paramTpts = appDef.termParamss.head.map(p => TypeTree(p.tpt.tpe).withSpan(p.tpt.span)) - val funSym = defn.FunctionSymbol(numArgs, isContextual, isImpure) + val funSym = defn.FunctionSymbol(numArgs, isContextual) val tycon = TypeTree(funSym.typeRef) AppliedTypeTree(tycon, paramTpts :+ resTpt) - RefinedTypeTree(core, List(appDef), ctx.owner.asClass) + val res = RefinedTypeTree(core, List(appDef), ctx.owner.asClass) + if isImpure then + typed(untpd.makeRetaining(untpd.TypedSplice(res), Nil, tpnme.retainsCap), pt) + else + res end typedDependent args match { From 21bd99416e82a1e588cb0f1ec56d9fb40a6e4ec0 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Jun 2024 11:45:47 +0200 Subject: [PATCH 16/35] Fix mapping of cap to existentials Still missing: Mapping parameters --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 3 +- .../src/dotty/tools/dotc/cc/Existential.scala | 103 +++++++++++------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 39 +++---- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../src/dotty/tools/dotc/core/Types.scala | 9 ++ tests/neg-custom-args/captures/lazylist.check | 8 +- tests/neg/existential-mapping.check | 88 +++++++++++++++ tests/neg/existential-mapping.scala | 47 ++++++++ .../captures/curried-closures.scala | 5 +- 9 files changed, 233 insertions(+), 71 deletions(-) create mode 100644 tests/neg/existential-mapping.check create mode 100644 tests/neg/existential-mapping.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index bc1641b6f414..a76e6a1315ac 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -28,7 +28,7 @@ object ccConfig: inline val allowUnsoundMaps = false /** If true, expand capability classes in Setup instead of treating them - * in adapt. + * in adapt. */ inline val expandCapabilityInSetup = true @@ -568,6 +568,7 @@ trait ConservativeFollowAliasMap(using Context) extends TypeMap: end ConservativeFollowAliasMap /** An extractor for all kinds of function types as well as method and poly types. + * It includes aliases of function types such as `=>`. TODO: Can we do without? * @return 1st half: The argument types or empty if this is a type function * 2nd half: The result type */ diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 218713f85e1f..94aab59443ba 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -10,6 +10,7 @@ import ast.tpd.* import Decorators.* import typer.ErrorReporting.errorType import NameKinds.ExistentialBinderName +import NameOps.isImpureFunction import reporting.Message /** @@ -258,6 +259,13 @@ object Existential: tp1.derivedAnnotatedType(toCapDeeply(parent), ann) case _ => tp + /** Knowing that `tp` is a function type, is an alias to a function other + * than `=>`? + */ + private def isAliasFun(tp: Type)(using Context) = tp match + case AppliedType(tycon, _) => !defn.isFunctionSymbol(tycon.typeSymbol) + case _ => false + /** Replace all occurrences of `cap` in parts of this type by an existentially bound * variable. If there are such occurrences, or there might be in the future due to embedded * capture set variables, create an existential with the variable wrapping the type. @@ -266,56 +274,69 @@ object Existential: def mapCap(tp: Type, fail: Message => Unit)(using Context): Type = var needsWrap = false - class Wrap(boundVar: TermParamRef) extends BiTypeMap, ConservativeFollowAliasMap: - def apply(t: Type) = // go deep first, so that we map parts of alias types before dealiasing - mapOver(t) match - case t1: TermRef if t1.isRootCapability => - if variance > 0 then - needsWrap = true - boundVar - else - if variance == 0 then - fail(em"cap appears in invariant position in $tp") - // we accept variance < 0, and leave the cap as it is - t1 - case t1 @ FunctionOrMethod(_, _) => - // These have been mapped before - t1 - case t1 @ CapturingType(_, _: CaptureSet.Var) => - if variance > 0 then needsWrap = true // the set might get a cap later. - t1 - case t1 => - applyToAlias(t, t1) - - lazy val inverse = new BiTypeMap with ConservativeFollowAliasMap: - def apply(t: Type) = mapOver(t) match - case t1: TermParamRef if t1 eq boundVar => defn.captureRoot.termRef - case t1 @ FunctionOrMethod(_, _) => t1 - case t1 => applyToAlias(t, t1) + abstract class CapMap extends BiTypeMap: + override def mapOver(t: Type): Type = t match + case t @ FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => + t // `t` should be mapped in this case by a different call to `mapCap`. + case Existential(_, _) => + t + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) + case _ => + super.mapOver(t) + + class Wrap(boundVar: TermParamRef) extends CapMap: + def apply(t: Type) = t match + case t: TermRef if t.isRootCapability => + if variance > 0 then + needsWrap = true + boundVar + else + if variance == 0 then + fail(em"""$tp captures the root capability `cap` in invariant position""") + // we accept variance < 0, and leave the cap as it is + super.mapOver(t) + case t @ CapturingType(parent, refs: CaptureSet.Var) => + if variance > 0 then needsWrap = true + super.mapOver(t) + case _ => + mapOver(t) + //.showing(i"mapcap $t = $result") + + lazy val inverse = new BiTypeMap: + def apply(t: Type) = t match + case t: TermParamRef if t eq boundVar => defn.captureRoot.termRef + case _ => mapOver(t) def inverse = Wrap.this override def toString = "Wrap.inverse" end Wrap if ccConfig.useExistentials then - tp match - case Existential(_, _) => tp - case _ => - val wrapped = apply(Wrap(_)(tp)) - if needsWrap then wrapped else tp + val wrapped = apply(Wrap(_)(tp)) + if needsWrap then wrapped else tp else tp end mapCap - /** Map `cap` to existential in the results of functions or methods. - */ - def mapCapInResult(tp: Type, fail: Message => Unit)(using Context): Type = - def mapCapInFinalResult(tp: Type): Type = tp match - case tp: MethodOrPoly => - tp.derivedLambdaType(resType = mapCapInFinalResult(tp.resultType)) + def mapCapInResults(fail: Message => Unit)(using Context): TypeMap = new: + + def mapFunOrMethod(tp: Type, args: List[Type], res: Type): Type = + val args1 = atVariance(-variance)(args.map(this)) + val res1 = res match + case res: MethodType => mapFunOrMethod(res, res.paramInfos, res.resType) + case res: PolyType => mapFunOrMethod(res, Nil, res.resType) // TODO: Also map bounds of PolyTypes + case _ => mapCap(apply(res), fail) + tp.derivedFunctionOrMethod(args1, res1) + + def apply(t: Type): Type = t match + case FunctionOrMethod(args, res) if variance > 0 && !isAliasFun(t) => + mapFunOrMethod(t, args, res) + case CapturingType(parent, refs) => + t.derivedCapturingType(this(parent), refs) + case t: (LazyRef | TypeVar) => + mapConserveSuper(t) case _ => - mapCap(tp, fail) - tp match - case tp: MethodOrPoly => mapCapInFinalResult(tp) - case _ => tp + mapOver(t) + end mapCapInResults /** Is `mt` a method represnting an existential type when used in a refinement? */ def isExistentialMethod(mt: TermLambda)(using Context): Boolean = mt.paramInfos match diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index c8e7a8d89a89..23d05168e1f2 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -269,12 +269,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: end transformInferredType private def transformExplicitType(tp: Type, tptToCheck: Option[Tree] = None)(using Context): Type = - val expandAliases = new DeepTypeMap: + val toCapturing = new DeepTypeMap: override def toString = "expand aliases" - def fail(msg: Message) = - for tree <- tptToCheck do report.error(msg, tree.srcPos) - /** Expand $throws aliases. This is hard-coded here since $throws aliases in stdlib * are defined with `?=>` rather than `?->`. * We also have to add a capture set to the last expanded throws alias. I.e. @@ -314,23 +311,20 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: t.derivedAnnotatedType(parent1, ann) case throwsAlias(res, exc) => this(expandThrowsAlias(res, exc, Nil)) - case t: LazyRef => - val t1 = this(t.ref) - if t1 ne t.ref then t1 else t - case t: TypeVar => - this(t.underlying) case t => - Existential.mapCapInResult( - // Map references to capability classes C to C^ - if ccConfig.expandCapabilityInSetup && t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists - then CapturingType(t, CaptureSet.universal, boxed = false) - else normalizeCaptures(mapOver(t)), - fail) - end expandAliases - - val tp1 = expandAliases(tp) // TODO: Do we still need to follow aliases? - if tp1 ne tp then capt.println(i"expanded in ${ctx.owner}: $tp --> $tp1") - tp1 + // Map references to capability classes C to C^ + if ccConfig.expandCapabilityInSetup && t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists + then CapturingType(t, CaptureSet.universal, boxed = false) + else normalizeCaptures(mapOver(t)) + end toCapturing + + def fail(msg: Message) = + for tree <- tptToCheck do report.error(msg, tree.srcPos) + + val tp1 = toCapturing(tp) + val tp2 = Existential.mapCapInResults(fail)(tp1) + if tp2 ne tp then capt.println(i"expanded in ${ctx.owner}: $tp --> $tp1 --> $tp2") + tp2 end transformExplicitType /** Transform type of type tree, and remember the transformed type as the type the tree */ @@ -538,9 +532,8 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if sym.exists && signatureChanges then val newInfo = - Existential.mapCapInResult( - integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil), - report.error(_, tree.srcPos)) + Existential.mapCapInResults(report.error(_, tree.srcPos)): + integrateRT(sym.info, sym.paramSymss, localReturnType, Nil, Nil) .showing(i"update info $sym: ${sym.info} = $result", capt) if newInfo ne sym.info then val updatedInfo = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 57402ffe27bf..ad80d0565f63 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1161,6 +1161,8 @@ class Definitions { if mt.hasErasedParams then RefinedType(PolyFunctionClass.typeRef, nme.apply, mt) else FunctionNOf(args, resultType, isContextual) + // Unlike PolyFunctionOf and RefinedFunctionOf this extractor follows aliases. + // Can we do without? Same for FunctionNOf and isFunctionNType. def unapply(ft: Type)(using Context): Option[(List[Type], Type, Boolean)] = { ft match case PolyFunctionOf(mt: MethodType) => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index cb47bd92352e..7bcb7e453647 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6273,6 +6273,15 @@ object Types extends TypeUtils { try derivedCapturingType(tp, this(parent), refs.map(this)) finally variance = saved + /** Utility method. Maps the supertype of a type proxy. Returns the + * type proxy itself if the mapping leaves the supertype unchanged. + * This avoids needless changes in mapped types. + */ + protected def mapConserveSuper(t: TypeProxy): Type = + val t1 = t.superType + val t2 = apply(t1) + if t2 ne t1 then t2 else t + /** Map this function over given type */ def mapOver(tp: Type): Type = { record(s"TypeMap mapOver ${getClass}") diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 09352ec648ce..643ef78841f0 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -8,8 +8,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:35:29 ------------------------------------- 35 | val ref1c: LazyList[Int] = ref1 // error | ^^^^ - | Found: (ref1 : lazylists.LazyCons[Int]{val xs: () ->{cap1} lazylists.LazyList[Int]^?}^{cap1}) - | Required: lazylists.LazyList[Int] + | Found: lazylists.LazyCons[Int]{val xs: () ->{cap1} lazylists.LazyList[Int]^?}^{ref1} + | Required: lazylists.LazyList[Int] | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:37:36 ------------------------------------- @@ -29,8 +29,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:41:48 ------------------------------------- 41 | val ref4c: LazyList[Int]^{cap1, ref3, cap3} = ref4 // error | ^^^^ - | Found: (ref4 : lazylists.LazyList[Int]^{cap3, cap2, ref1, cap1}) - | Required: lazylists.LazyList[Int]^{cap1, ref3, cap3} + | Found: (ref4 : lazylists.LazyList[Int]^{cap3, cap2, ref1}) + | Required: lazylists.LazyList[Int]^{cap1, ref3, cap3} | | longer explanation available when compiling with `-explain` -- [E164] Declaration Error: tests/neg-custom-args/captures/lazylist.scala:22:6 ---------------------------------------- diff --git a/tests/neg/existential-mapping.check b/tests/neg/existential-mapping.check new file mode 100644 index 000000000000..bab04868b123 --- /dev/null +++ b/tests/neg/existential-mapping.check @@ -0,0 +1,88 @@ +-- Error: tests/neg/existential-mapping.scala:44:13 -------------------------------------------------------------------- +44 | val z1: A^ => Array[C^] = ??? // error + | ^^^^^^^^^^^^^^^ + | Array[box C^] captures the root capability `cap` in invariant position +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:9:25 ------------------------------------------------ +9 | val _: (x: C^) -> C = x1 // error + | ^^ + | Found: (x1 : (x: C^) -> (ex$3: caps.Exists) -> C^{ex$3}) + | Required: (x: C^) -> C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:12:20 ----------------------------------------------- +12 | val _: C^ -> C = x2 // error + | ^^ + | Found: (x2 : C^ -> (ex$9: caps.Exists) -> C^{ex$9}) + | Required: C^ -> C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:15:30 ----------------------------------------------- +15 | val _: A^ -> (x: C^) -> C = x3 // error + | ^^ + | Found: (x3 : A^ -> (x: C^) -> (ex$15: caps.Exists) -> C^{ex$15}) + | Required: A^ -> (x: C^) -> C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:18:25 ----------------------------------------------- +18 | val _: A^ -> C^ -> C = x4 // error + | ^^ + | Found: (x4 : A^ -> C^ -> (ex$25: caps.Exists) -> C^{ex$25}) + | Required: A^ -> C^ -> C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:21:30 ----------------------------------------------- +21 | val _: A^ -> (x: C^) -> C = x5 // error + | ^^ + | Found: (x5 : A^ -> (ex$35: caps.Exists) -> Fun[C^{ex$35}]) + | Required: A^ -> (x: C^) -> C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:24:30 ----------------------------------------------- +24 | val _: A^ -> (x: C^) => C = x6 // error + | ^^ + | Found: (x6 : A^ -> (ex$43: caps.Exists) -> IFun[C^{ex$43}]) + | Required: A^ -> (ex$48: caps.Exists) -> (x: C^) ->{ex$48} C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:27:25 ----------------------------------------------- +27 | val _: (x: C^) => C = y1 // error + | ^^ + | Found: (y1 : (x: C^) => (ex$54: caps.Exists) -> C^{ex$54}) + | Required: (x: C^) => C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:30:20 ----------------------------------------------- +30 | val _: C^ => C = y2 // error + | ^^ + | Found: (y2 : C^ => (ex$60: caps.Exists) -> C^{ex$60}) + | Required: C^ => C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:33:30 ----------------------------------------------- +33 | val _: A^ => (x: C^) => C = y3 // error + | ^^ + | Found: (y3 : A^ => (ex$67: caps.Exists) -> (x: C^) ->{ex$67} (ex$66: caps.Exists) -> C^{ex$66}) + | Required: A^ => (ex$78: caps.Exists) -> (x: C^) ->{ex$78} C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:36:25 ----------------------------------------------- +36 | val _: A^ => C^ => C = y4 // error + | ^^ + | Found: (y4 : A^ => C^ => (ex$84: caps.Exists) -> C^{ex$84}) + | Required: A^ => C^ => C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:39:30 ----------------------------------------------- +39 | val _: A^ => (x: C^) -> C = y5 // error + | ^^ + | Found: (y5 : A^ => (ex$94: caps.Exists) -> Fun[C^{ex$94}]) + | Required: A^ => (x: C^) -> C + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:42:30 ----------------------------------------------- +42 | val _: A^ => (x: C^) => C = y6 // error + | ^^ + | Found: (y6 : A^ => (ex$102: caps.Exists) -> IFun[C^{ex$102}]) + | Required: A^ => (ex$107: caps.Exists) -> (x: C^) ->{ex$107} C + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/existential-mapping.scala b/tests/neg/existential-mapping.scala new file mode 100644 index 000000000000..96a36d8a7b9b --- /dev/null +++ b/tests/neg/existential-mapping.scala @@ -0,0 +1,47 @@ +import language.experimental.captureChecking + +class A +class C +type Fun[X] = (x: C^) -> X +type IFun[X] = (x: C^) => X +def Test = + val x1: (x: C^) -> C^ = ??? + val _: (x: C^) -> C = x1 // error + + val x2: C^ -> C^ = ??? + val _: C^ -> C = x2 // error + + val x3: A^ -> (x: C^) -> C^ = ??? + val _: A^ -> (x: C^) -> C = x3 // error + + val x4: A^ -> C^ -> C^ = ??? + val _: A^ -> C^ -> C = x4 // error + + val x5: A^ -> Fun[C^] = ??? + val _: A^ -> (x: C^) -> C = x5 // error + + val x6: A^ -> IFun[C^] = ??? + val _: A^ -> (x: C^) => C = x6 // error + + val y1: (x: C^) => C^ = ??? + val _: (x: C^) => C = y1 // error + + val y2: C^ => C^ = ??? + val _: C^ => C = y2 // error + + val y3: A^ => (x: C^) => C^ = ??? + val _: A^ => (x: C^) => C = y3 // error + + val y4: A^ => C^ => C^ = ??? + val _: A^ => C^ => C = y4 // error + + val y5: A^ => Fun[C^] = ??? + val _: A^ => (x: C^) -> C = y5 // error + + val y6: A^ => IFun[C^] = ??? + val _: A^ => (x: C^) => C = y6 // error + + val z1: A^ => Array[C^] = ??? // error + + + diff --git a/tests/pos-custom-args/captures/curried-closures.scala b/tests/pos-custom-args/captures/curried-closures.scala index 0ad729375b3c..262dd4b66b92 100644 --- a/tests/pos-custom-args/captures/curried-closures.scala +++ b/tests/pos-custom-args/captures/curried-closures.scala @@ -1,6 +1,7 @@ -//> using options -experimental +import annotation.experimental +import language.experimental.captureChecking -object Test: +@experimental object Test: def map2(xs: List[Int])(f: Int => Int): List[Int] = xs.map(f) val f1 = map2 val fc1: List[Int] -> (Int => Int) -> List[Int] = f1 From c870c972fe8b2502e77ea53d3057ce2b305d8c8e Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Jun 2024 13:11:02 +0200 Subject: [PATCH 17/35] Let only value types derive from Capabilities Fixes crash with opaque types reported by @natsukagami --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- tests/pos-custom-args/captures/opaque-cap.scala | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 tests/pos-custom-args/captures/opaque-cap.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a76e6a1315ac..1516b769c7ee 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -272,7 +272,7 @@ extension (tp: Type) val sym = tp.typeSymbol if sym.isClass then sym.derivesFrom(defn.Caps_Capability) else tp.superType.derivesFromCapability - case tp: TypeProxy => + case tp: (TypeProxy & ValueType) => tp.superType.derivesFromCapability case tp: AndType => tp.tp1.derivesFromCapability || tp.tp2.derivesFromCapability diff --git a/tests/pos-custom-args/captures/opaque-cap.scala b/tests/pos-custom-args/captures/opaque-cap.scala new file mode 100644 index 000000000000..dc3d48a2d311 --- /dev/null +++ b/tests/pos-custom-args/captures/opaque-cap.scala @@ -0,0 +1,6 @@ +import language.experimental.captureChecking + +trait A extends caps.Capability + +object O: + opaque type B = A \ No newline at end of file From 29d01c2e6e7733cbe89a63a7aef31c29c8c4efeb Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 09:38:30 +0200 Subject: [PATCH 18/35] Type inference for existentials - Add existentials to inferred types - Map existentials in one compared type to existentials in the other - Also: Don't re-analyze existentials in mapCap. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 29 ++--- .../src/dotty/tools/dotc/cc/Existential.scala | 16 ++- compiler/src/dotty/tools/dotc/cc/Setup.scala | 8 +- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/core/TypeComparer.scala | 111 +++++++++++++++--- .../captures/heal-tparam-cs.scala | 4 +- tests/neg-custom-args/captures/reaches2.check | 4 +- tests/neg/existential-mapping.check | 28 ++--- 8 files changed, 148 insertions(+), 53 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index eb3718e9601f..8f161810f6f9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -14,7 +14,7 @@ import printing.{Showable, Printer} import printing.Texts.* import util.{SimpleIdentitySet, Property} import typer.ErrorReporting.Addenda -import TypeComparer.canSubsumeExistentially +import TypeComparer.subsumesExistentially import util.common.alwaysTrue import scala.collection.mutable import CCState.* @@ -173,7 +173,7 @@ sealed abstract class CaptureSet extends Showable: x.info match case x1: SingletonCaptureRef => x1.subsumes(y) case _ => false - case x: TermParamRef => canSubsumeExistentially(x, y) + case x: TermParamRef => subsumesExistentially(x, y) case _ => false /** {x} <:< this where <:< is subcapturing, but treating all variables @@ -498,10 +498,13 @@ object CaptureSet: deps = state.deps(this) final def addThisElem(elem: CaptureRef)(using Context, VarState): CompareResult = - if isConst || !recordElemsState() then - CompareResult.Fail(this :: Nil) // fail if variable is solved or given VarState is frozen + if isConst // Fail if variable is solved, + || !recordElemsState() // or given VarState is frozen, + || Existential.isBadExistential(elem) // or `elem` is an out-of-scope existential, + then + CompareResult.Fail(this :: Nil) else if !levelOK(elem) then - CompareResult.LevelError(this, elem) + CompareResult.LevelError(this, elem) // or `elem` is not visible at the level of the set. else //if id == 34 then assert(!elem.isUniversalRootCapability) assert(elem.isTrackableRef, elem) @@ -694,19 +697,10 @@ object CaptureSet: if cond then propagate else CompareResult.OK val mapped = extrapolateCaptureRef(elem, tm, variance) + def isFixpoint = mapped.isConst && mapped.elems.size == 1 && mapped.elems.contains(elem) - def addMapped = - val added = mapped.elems.filter(!accountsFor(_)) - addNewElems(added) - .andAlso: - if mapped.isConst then CompareResult.OK - else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK } - else CompareResult.Fail(this :: Nil) - .andAlso: - propagateIf(!added.isEmpty) - def failNoFixpoint = val reason = if variance <= 0 then i"the set's variance is $variance" @@ -716,11 +710,14 @@ object CaptureSet: CompareResult.Fail(this :: Nil) if origin eq source then // elements have to be mapped - addMapped + val added = mapped.elems.filter(!accountsFor(_)) + addNewElems(added) .andAlso: if mapped.isConst then CompareResult.OK else if mapped.asVar.recordDepsState() then { addAsDependentTo(mapped); CompareResult.OK } else CompareResult.Fail(this :: Nil) + .andAlso: + propagateIf(!added.isEmpty) else if accountsFor(elem) then CompareResult.OK else if variance > 0 then diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 94aab59443ba..2bdc82bef53f 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -9,6 +9,7 @@ import StdNames.nme import ast.tpd.* import Decorators.* import typer.ErrorReporting.errorType +import Names.TermName import NameKinds.ExistentialBinderName import NameOps.isImpureFunction import reporting.Message @@ -204,8 +205,10 @@ object Existential: case _ => None /** Create method type in the refinement of an existential type */ - private def exMethodType(mk: TermParamRef => Type)(using Context): MethodType = - val boundName = ExistentialBinderName.fresh() + private def exMethodType(using Context)( + mk: TermParamRef => Type, + boundName: TermName = ExistentialBinderName.fresh() + ): MethodType = MethodType(boundName :: Nil)( mt => defn.Caps_Exists.typeRef :: Nil, mt => mk(mt.paramRefs.head)) @@ -332,6 +335,8 @@ object Existential: mapFunOrMethod(t, args, res) case CapturingType(parent, refs) => t.derivedCapturingType(this(parent), refs) + case Existential(_, _) => + t case t: (LazyRef | TypeVar) => mapConserveSuper(t) case _ => @@ -348,4 +353,11 @@ object Existential: case ref: TermParamRef => isExistentialMethod(ref.binder) case _ => false + def isBadExistential(ref: CaptureRef) = ref match + case ref: TermParamRef => ref.paramName == nme.OOS_EXISTENTIAL + case _ => false + + def badExistential(using Context): TermParamRef = + exMethodType(identity, nme.OOS_EXISTENTIAL).paramRefs.head + end Existential diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 23d05168e1f2..7fc78599c377 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -262,7 +262,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: end apply end mapInferred - try mapInferred(refine = true)(tp) + try + val tp1 = mapInferred(refine = true)(tp) + val tp2 = Existential.mapCapInResults(_ => assert(false))(tp1) + if tp2 ne tp then capt.println(i"expanded implicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") + tp2 catch case ex: AssertionError => println(i"error while mapping inferred $tp") throw ex @@ -323,7 +327,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val tp1 = toCapturing(tp) val tp2 = Existential.mapCapInResults(fail)(tp1) - if tp2 ne tp then capt.println(i"expanded in ${ctx.owner}: $tp --> $tp1 --> $tp2") + if tp2 ne tp then capt.println(i"expanded explicit in ${ctx.owner}: $tp --> $tp1 --> $tp2") tp2 end transformExplicitType diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 3753d1688399..6548b46186bb 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -294,6 +294,7 @@ object StdNames { val EVT2U: N = "evt2u$" val EQEQ_LOCAL_VAR: N = "eqEqTemp$" val LAZY_FIELD_OFFSET: N = "OFFSET$" + val OOS_EXISTENTIAL: N = "" val OUTER: N = "$outer" val REFINE_CLASS: N = "" val ROOTPKG: N = "_root_" diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 41eb95c7b120..958515a9e625 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -11,7 +11,7 @@ import collection.mutable import util.{Stats, NoSourcePosition, EqHashMap} import config.Config import config.Feature.{betterMatchTypeExtractorsEnabled, migrateTo3, sourceVersion} -import config.Printers.{subtyping, gadts, matchTypes, noPrinter} +import config.Printers.{subtyping, gadts, matchTypes, capt, noPrinter} import config.SourceVersion import TypeErasure.{erasedLub, erasedGlb} import TypeApplications.* @@ -47,7 +47,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling GADTused = false opaquesUsed = false openedExistentials = Nil - assocExistentials = Map.empty + assocExistentials = Nil recCount = 0 needsGc = false if Config.checkTypeComparerReset then checkReset() @@ -76,7 +76,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * Each existential gets mapped to the opened existentials to which it * may resolve at this point. */ - private var assocExistentials: Map[TermParamRef, List[TermParamRef]] = Map.empty + private var assocExistentials: ExAssoc = Nil private var myInstance: TypeComparer = this def currentInstance: TypeComparer = myInstance @@ -358,7 +358,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling isMatchedByProto(tp2, tp1) case tp2: BoundType => tp2 == tp1 - || existentialVarsConform(tp1, tp2) || secondTry case tp2: TypeVar => recur(tp1, typeVarInstance(tp2)) @@ -2787,6 +2786,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } + // ----------- Capture checking ----------------------------------------------- + + /** A type associating instantiatable existentials on the right of a comparison + * with the existentials they can be instantiated with. + */ + type ExAssoc = List[(TermParamRef, List[TermParamRef])] + private def compareExistentialLeft(boundVar: TermParamRef, tp1unpacked: Type, tp2: Type)(using Context): Boolean = val saved = openedExistentials try @@ -2798,16 +2804,32 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private def compareExistentialRight(tp1: Type, boundVar: TermParamRef, tp2unpacked: Type)(using Context): Boolean = val saved = assocExistentials try - assocExistentials = assocExistentials.updated(boundVar, openedExistentials) + assocExistentials = (boundVar, openedExistentials) :: assocExistentials recur(tp1, tp2unpacked) finally assocExistentials = saved - def canSubsumeExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean = - Existential.isExistentialVar(tp1) - && assocExistentials.get(tp1).match - case Some(xs) => !Existential.isExistentialVar(tp2) || xs.contains(tp2) - case None => false + /** Is `tp1` an existential var that subsumes `tp2`? This is the case if `tp1` is + * instantiatable (i.e. it's a key in `assocExistentials`) and one of the + * following is true: + * - `tp2` is not an existential var, + * - `tp1` is associated via `assocExistentials` with `tp2`, + * - `tp2` appears as key in `assocExistentials` further out than `tp1`. + * The third condition allows to instantiate c2 to c1 in + * EX c1: A -> Ex c2. B + */ + def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context): Boolean = + def canInstantiateWith(assoc: ExAssoc): Boolean = assoc match + case (bv, bvs) :: assoc1 => + if bv == tp1 then + !Existential.isExistentialVar(tp2) + || bvs.contains(tp2) + || assoc1.exists(_._1 == tp2) + else + canInstantiateWith(assoc1) + case Nil => + false + Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials) /** Are tp1, tp2 termRefs that can be linked? This should never be called * normally, since exietential variables appear only in capture sets @@ -2817,16 +2839,70 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private def existentialVarsConform(tp1: Type, tp2: Type) = tp2 match case tp2: TermParamRef => tp1 match - case tp1: CaptureRef => canSubsumeExistentially(tp2, tp1) + case tp1: CaptureRef => subsumesExistentially(tp2, tp1) case _ => false case _ => false + /** bi-map taking existentials to the left of a comparison to matching + * existentials on the right. This is not a bijection. However + * we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`. + * That's enough to qualify as a BiTypeMap. + */ + private class MapExistentials(assoc: ExAssoc)(using Context) extends BiTypeMap: + + private def bad(t: Type) = + Existential.badExistential + .showing(i"existential match not found for $t in $assoc", capt) + + def apply(t: Type) = t match + case t: TermParamRef if Existential.isExistentialVar(t) => + // Find outermost existential on the right that can be instantiated to `t`, + // or `badExistential` if none exists. + def findMapped(assoc: ExAssoc): CaptureRef = assoc match + case (bv, assocBvs) :: assoc1 => + val outer = findMapped(assoc1) + if !Existential.isBadExistential(outer) then outer + else if assocBvs.contains(t) then bv + else bad(t) + case Nil => + bad(t) + findMapped(assoc) + case _ => + mapOver(t) + + /** The inverse takes existentials on the right to the innermost existential + * on the left to which they can be instantiated. + */ + lazy val inverse = new BiTypeMap: + def apply(t: Type) = t match + case t: TermParamRef if Existential.isExistentialVar(t) => + assoc.find(_._1 == t) match + case Some((_, bvs)) if bvs.nonEmpty => bvs.head + case _ => bad(t) + case _ => + mapOver(t) + + def inverse = MapExistentials.this + override def toString = "MapExistentials.inverse" + end inverse + end MapExistentials + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = - try refs1.subCaptures(refs2, frozen) + try + if assocExistentials.isEmpty then + refs1.subCaptures(refs2, frozen) + else + val mapped = refs1.map(MapExistentials(assocExistentials)) + if mapped.elems.exists(Existential.isBadExistential) + then CaptureSet.CompareResult.Fail(refs2 :: Nil) + else subCapturesMapped(mapped, refs2, frozen) catch case ex: AssertionError => println(i"fail while subCaptures $refs1 <:< $refs2") throw ex + protected def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + refs1.subCaptures(refs2, frozen) + /** Is the boxing status of tp1 and tp2 the same, or alternatively, is * the capture sets `refs1` of `tp1` a subcapture of the empty set? * In the latter case, boxing status does not matter. @@ -3291,9 +3367,6 @@ object TypeComparer { def lub(tp1: Type, tp2: Type, canConstrain: Boolean = false, isSoft: Boolean = true)(using Context): Type = comparing(_.lub(tp1, tp2, canConstrain = canConstrain, isSoft = isSoft)) - def canSubsumeExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = - comparing(_.canSubsumeExistentially(tp1, tp2)) - /** The least upper bound of a list of types */ final def lub(tps: List[Type])(using Context): Type = tps.foldLeft(defn.NothingType: Type)(lub(_,_)) @@ -3366,6 +3439,9 @@ object TypeComparer { def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = comparing(_.subCaptures(refs1, refs2, frozen)) + + def subsumesExistentially(tp1: TermParamRef, tp2: CaptureRef)(using Context) = + comparing(_.subsumesExistentially(tp1, tp2)) } object MatchReducer: @@ -3881,5 +3957,10 @@ class ExplainingTypeComparer(initctx: Context, short: Boolean) extends TypeCompa super.subCaptures(refs1, refs2, frozen) } + override def subCapturesMapped(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = + traceIndented(i"subcaptures mapped $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { + super.subCapturesMapped(refs1, refs2, frozen) + } + def lastTrace(header: String): String = header + { try b.toString finally b.clear() } } diff --git a/tests/neg-custom-args/captures/heal-tparam-cs.scala b/tests/neg-custom-args/captures/heal-tparam-cs.scala index 498292166297..fde4b93e196c 100644 --- a/tests/neg-custom-args/captures/heal-tparam-cs.scala +++ b/tests/neg-custom-args/captures/heal-tparam-cs.scala @@ -11,12 +11,12 @@ def main(io: Capp^, net: Capp^): Unit = { } val test2: (c: Capp^) -> () => Unit = - localCap { c => // should work + localCap { c => // error (c1: Capp^) => () => { c1.use() } } val test3: (c: Capp^{io}) -> () ->{io} Unit = - localCap { c => // should work + localCap { c => // error (c1: Capp^{io}) => () => { c1.use() } } diff --git a/tests/neg-custom-args/captures/reaches2.check b/tests/neg-custom-args/captures/reaches2.check index 504955b220ad..f646a9736395 100644 --- a/tests/neg-custom-args/captures/reaches2.check +++ b/tests/neg-custom-args/captures/reaches2.check @@ -2,9 +2,9 @@ 8 | ps.map((x, y) => compose1(x, y)) // error // error | ^ |reference (ps : List[(box A => A, box A => A)]) @reachCapability is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? + |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? (ex$15: caps.Exists) -> A^? -- Error: tests/neg-custom-args/captures/reaches2.scala:8:13 ----------------------------------------------------------- 8 | ps.map((x, y) => compose1(x, y)) // error // error | ^ |reference (ps : List[(box A => A, box A => A)]) @reachCapability is not included in the allowed capture set {} - |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? A^? + |of an enclosing function literal with expected type ((box A ->{ps*} A, box A ->{ps*} A)) -> box (x$0: A^?) ->? (ex$15: caps.Exists) -> A^? diff --git a/tests/neg/existential-mapping.check b/tests/neg/existential-mapping.check index bab04868b123..7c1de8b31529 100644 --- a/tests/neg/existential-mapping.check +++ b/tests/neg/existential-mapping.check @@ -12,77 +12,77 @@ -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:12:20 ----------------------------------------------- 12 | val _: C^ -> C = x2 // error | ^^ - | Found: (x2 : C^ -> (ex$9: caps.Exists) -> C^{ex$9}) + | Found: (x2 : C^ -> (ex$7: caps.Exists) -> C^{ex$7}) | Required: C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:15:30 ----------------------------------------------- 15 | val _: A^ -> (x: C^) -> C = x3 // error | ^^ - | Found: (x3 : A^ -> (x: C^) -> (ex$15: caps.Exists) -> C^{ex$15}) + | Found: (x3 : A^ -> (x: C^) -> (ex$11: caps.Exists) -> C^{ex$11}) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:18:25 ----------------------------------------------- 18 | val _: A^ -> C^ -> C = x4 // error | ^^ - | Found: (x4 : A^ -> C^ -> (ex$25: caps.Exists) -> C^{ex$25}) + | Found: (x4 : A^ -> C^ -> (ex$19: caps.Exists) -> C^{ex$19}) | Required: A^ -> C^ -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:21:30 ----------------------------------------------- 21 | val _: A^ -> (x: C^) -> C = x5 // error | ^^ - | Found: (x5 : A^ -> (ex$35: caps.Exists) -> Fun[C^{ex$35}]) + | Found: (x5 : A^ -> (ex$27: caps.Exists) -> Fun[C^{ex$27}]) | Required: A^ -> (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:24:30 ----------------------------------------------- 24 | val _: A^ -> (x: C^) => C = x6 // error | ^^ - | Found: (x6 : A^ -> (ex$43: caps.Exists) -> IFun[C^{ex$43}]) - | Required: A^ -> (ex$48: caps.Exists) -> (x: C^) ->{ex$48} C + | Found: (x6 : A^ -> (ex$33: caps.Exists) -> IFun[C^{ex$33}]) + | Required: A^ -> (ex$36: caps.Exists) -> (x: C^) ->{ex$36} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:27:25 ----------------------------------------------- 27 | val _: (x: C^) => C = y1 // error | ^^ - | Found: (y1 : (x: C^) => (ex$54: caps.Exists) -> C^{ex$54}) + | Found: (y1 : (x: C^) => (ex$38: caps.Exists) -> C^{ex$38}) | Required: (x: C^) => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:30:20 ----------------------------------------------- 30 | val _: C^ => C = y2 // error | ^^ - | Found: (y2 : C^ => (ex$60: caps.Exists) -> C^{ex$60}) + | Found: (y2 : C^ => (ex$42: caps.Exists) -> C^{ex$42}) | Required: C^ => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:33:30 ----------------------------------------------- 33 | val _: A^ => (x: C^) => C = y3 // error | ^^ - | Found: (y3 : A^ => (ex$67: caps.Exists) -> (x: C^) ->{ex$67} (ex$66: caps.Exists) -> C^{ex$66}) - | Required: A^ => (ex$78: caps.Exists) -> (x: C^) ->{ex$78} C + | Found: (y3 : A^ => (ex$47: caps.Exists) -> (x: C^) ->{ex$47} (ex$46: caps.Exists) -> C^{ex$46}) + | Required: A^ => (ex$50: caps.Exists) -> (x: C^) ->{ex$50} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:36:25 ----------------------------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => C^ => (ex$84: caps.Exists) -> C^{ex$84}) + | Found: (y4 : A^ => C^ => (ex$52: caps.Exists) -> C^{ex$52}) | Required: A^ => C^ => C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:39:30 ----------------------------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (ex$94: caps.Exists) -> Fun[C^{ex$94}]) + | Found: (y5 : A^ => (ex$60: caps.Exists) -> Fun[C^{ex$60}]) | Required: A^ => (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:42:30 ----------------------------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$102: caps.Exists) -> IFun[C^{ex$102}]) - | Required: A^ => (ex$107: caps.Exists) -> (x: C^) ->{ex$107} C + | Found: (y6 : A^ => (ex$66: caps.Exists) -> IFun[C^{ex$66}]) + | Required: A^ => (ex$69: caps.Exists) -> (x: C^) ->{ex$69} C | | longer explanation available when compiling with `-explain` From dddd6bcb9740322c3b0ddb14a3b1124424e476c0 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 18:08:45 +0200 Subject: [PATCH 19/35] Drop checkReachCapsIsolated restriction --- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 5 +++-- tests/neg-custom-args/captures/unsound-reach.check | 5 ----- tests/neg-custom-args/captures/unsound-reach.scala | 2 +- tests/neg-custom-args/captures/widen-reach.check | 14 ++++++++++++++ tests/neg-custom-args/captures/widen-reach.scala | 14 ++++++++++++++ tests/neg/i20503.scala | 4 ++-- 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 tests/neg-custom-args/captures/widen-reach.check create mode 100644 tests/neg-custom-args/captures/widen-reach.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index b73184447c47..48824078c337 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -259,7 +259,7 @@ class CheckCaptures extends Recheck, SymTransformer: ctx.printer.toTextCaptureRef(ref).show // Uses 4-space indent as a trial - def checkReachCapsIsolated(tpe: Type, pos: SrcPos)(using Context): Unit = + private def checkReachCapsIsolated(tpe: Type, pos: SrcPos)(using Context): Unit = object checker extends TypeTraverser: var refVariances: Map[Boolean, Int] = Map.empty @@ -854,7 +854,8 @@ class CheckCaptures extends Recheck, SymTransformer: tree.tpe finally curEnv = saved if tree.isTerm then - checkReachCapsIsolated(res.widen, tree.srcPos) + if !ccConfig.useExistentials then + checkReachCapsIsolated(res.widen, tree.srcPos) if !pt.isBoxedCapturing then markFree(res.boxedCaptureSet, tree.srcPos) res diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 22b00b74deb1..f0e4c4deeb41 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -1,8 +1,3 @@ --- Error: tests/neg-custom-args/captures/unsound-reach.scala:18:13 ----------------------------------------------------- -18 | boom.use(f): (f1: File^{backdoor*}) => // error - | ^^^^^^^^ - | Reach capability backdoor* and universal capability cap cannot both - | appear in the type (x: File^)(op: box File^{backdoor*} => Unit): Unit of this expression -- [E164] Declaration Error: tests/neg-custom-args/captures/unsound-reach.scala:10:8 ----------------------------------- 10 | def use(x: File^)(op: File^ => Unit): Unit = op(x) // error, was OK using sealed checking | ^ diff --git a/tests/neg-custom-args/captures/unsound-reach.scala b/tests/neg-custom-args/captures/unsound-reach.scala index c3c31a7f32ff..22ed4614b71b 100644 --- a/tests/neg-custom-args/captures/unsound-reach.scala +++ b/tests/neg-custom-args/captures/unsound-reach.scala @@ -15,6 +15,6 @@ def bad(): Unit = var escaped: File^{backdoor*} = null withFile("hello.txt"): f => - boom.use(f): (f1: File^{backdoor*}) => // error + boom.use(f): (f1: File^{backdoor*}) => // was error before existentials escaped = f1 diff --git a/tests/neg-custom-args/captures/widen-reach.check b/tests/neg-custom-args/captures/widen-reach.check new file mode 100644 index 000000000000..a4ea91981702 --- /dev/null +++ b/tests/neg-custom-args/captures/widen-reach.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/widen-reach.scala:13:26 ---------------------------------- +13 | val y2: IO^ -> IO^ = y1.foo // error + | ^^^^^^ + | Found: IO^ ->{x*} IO^{x*} + | Required: IO^ -> (ex$6: caps.Exists) -> IO^{ex$6} + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/widen-reach.scala:14:30 ---------------------------------- +14 | val y3: IO^ -> IO^{x*} = y1.foo // error + | ^^^^^^ + | Found: IO^ ->{x*} IO^{x*} + | Required: IO^ -> IO^{x*} + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/widen-reach.scala b/tests/neg-custom-args/captures/widen-reach.scala new file mode 100644 index 000000000000..5b9cd667d901 --- /dev/null +++ b/tests/neg-custom-args/captures/widen-reach.scala @@ -0,0 +1,14 @@ +import language.experimental.captureChecking + +trait IO + +trait Foo[+T]: + val foo: IO^ -> T + +trait Bar extends Foo[IO^]: + val foo: IO^ -> IO^ = x => x + +def test(x: Foo[IO^]): Unit = + val y1: Foo[IO^{x*}] = x + val y2: IO^ -> IO^ = y1.foo // error + val y3: IO^ -> IO^{x*} = y1.foo // error \ No newline at end of file diff --git a/tests/neg/i20503.scala b/tests/neg/i20503.scala index 7a1bffcff529..e8770b934ad1 100644 --- a/tests/neg/i20503.scala +++ b/tests/neg/i20503.scala @@ -9,8 +9,8 @@ class List[+A]: def runOps(ops: List[() => Unit]): Unit = // See i20156, due to limitation in expressiveness of current system, - // we cannot map over the list of impure elements. - ops.foreach(op => op()) // error + // we could map over the list of impure elements. OK with existentials. + ops.foreach(op => op()) def main(): Unit = val f: List[() => Unit] -> Unit = runOps // error From c547c2aba9c569376d3d7d1ecb3b81cd7b195eac Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 18:30:15 +0200 Subject: [PATCH 20/35] Drop no universal in deep capture set test everywhere from source 3.5 --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- tests/neg-custom-args/captures/filevar.scala | 2 +- .../neg-custom-args/captures/outer-var.check | 26 +++---------------- .../neg-custom-args/captures/outer-var.scala | 4 +-- 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 48824078c337..54bbb54997a5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -170,7 +170,7 @@ object CheckCaptures: traverse(parent) case t => traverseChildren(t) - check.traverse(tp) + if ccConfig.allowUniversalInBoxed then check.traverse(tp) end disallowRootCapabilitiesIn /** Attachment key for bodies of closures, provided they are values */ diff --git a/tests/neg-custom-args/captures/filevar.scala b/tests/neg-custom-args/captures/filevar.scala index 2859f4c5e826..e54f161ef124 100644 --- a/tests/neg-custom-args/captures/filevar.scala +++ b/tests/neg-custom-args/captures/filevar.scala @@ -5,7 +5,7 @@ class File: def write(x: String): Unit = ??? class Service: - var file: File^ = uninitialized // error + var file: File^ = uninitialized // OK, was error under sealed def log = file.write("log") // error, was OK under sealed def withFile[T](op: (l: caps.Capability) ?-> (f: File^{l}) => T): T = diff --git a/tests/neg-custom-args/captures/outer-var.check b/tests/neg-custom-args/captures/outer-var.check index ee32c3ce03f2..32351a179eab 100644 --- a/tests/neg-custom-args/captures/outer-var.check +++ b/tests/neg-custom-args/captures/outer-var.check @@ -22,29 +22,9 @@ 13 | y = (q: Proc) // error | ^^^^^^^ | Found: Proc - | Required: box () ->{p} Unit + | Required: box () => Unit | - | Note that the universal capability `cap` - | cannot be included in capture set {p} of variable y - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:14:8 ------------------------------------- -14 | y = q // error - | ^ - | Found: box () ->{q} Unit - | Required: box () ->{p} Unit - | - | Note that reference (q : Proc), defined in method inner - | cannot be included in outer capture set {p} of variable y - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/outer-var.scala:16:65 ------------------------------------ -16 | var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: scala.collection.mutable.ListBuffer[box () => Unit] - | Required: box scala.collection.mutable.ListBuffer[box () ->? Unit]^? - | - | Note that the universal capability `cap` - | cannot be included in capture set ? of variable finalizeActions + | Note that () => Unit cannot be box-converted to box () => Unit + | since at least one of their capture sets contains the root capability `cap` | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/outer-var.scala b/tests/neg-custom-args/captures/outer-var.scala index 39c3a6da4ca3..e26cd631602a 100644 --- a/tests/neg-custom-args/captures/outer-var.scala +++ b/tests/neg-custom-args/captures/outer-var.scala @@ -11,8 +11,8 @@ def test(p: Proc, q: () => Unit) = x = q // error x = (q: Proc) // error y = (q: Proc) // error - y = q // error + y = q // OK, was error under sealed - var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // error + var finalizeActions = collection.mutable.ListBuffer[() => Unit]() // OK, was error under sealed From 5ade6a4bc5e0ccf9a41da688df1b239a920ea701 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 18:40:46 +0200 Subject: [PATCH 21/35] Disallow to box or unbox existentials --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 8 +++-- .../src/dotty/tools/dotc/cc/Existential.scala | 34 ++++++++++++++----- .../captures/widen-reach.check | 6 ++++ .../captures/widen-reach.scala | 2 +- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 8f161810f6f9..e83da64a920a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -85,6 +85,9 @@ sealed abstract class CaptureSet extends Showable: final def isUniversal(using Context) = elems.exists(_.isRootCapability) + final def isUnboxable(using Context) = + elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + /** Try to include an element in this capture set. * @param elem The element to be added * @param origin The set that originated the request, or `empty` if the request came from outside. @@ -331,7 +334,7 @@ sealed abstract class CaptureSet extends Showable: /** Invoke handler if this set has (or later aquires) the root capability `cap` */ def disallowRootCapability(handler: () => Context ?=> Unit)(using Context): this.type = - if isUniversal then handler() + if isUnboxable then handler() this /** Invoke handler on the elements to ensure wellformedness of the capture set. @@ -521,7 +524,8 @@ object CaptureSet: res.addToTrace(this) private def levelOK(elem: CaptureRef)(using Context): Boolean = - if elem.isRootCapability then !noUniversal + if elem.isRootCapability || Existential.isExistentialVar(elem) then + !noUniversal else elem match case elem: TermRef if level.isDefined => elem.symbol.ccLevel <= level diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 2bdc82bef53f..9809d97a4504 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -33,9 +33,9 @@ In Setup: - Conversion is done with a BiTypeMap in `Existential.mapCap`. -In adapt: +In reckeckApply and recheckTypeApply: - - If an EX is toplevel in actual type, replace its bound variable + - If an EX is toplevel in the result type, replace its bound variable occurrences with `cap`. Level checking and avoidance: @@ -54,6 +54,7 @@ Level checking and avoidance: don't, the others do. - Capture set variables do not accept elements of level higher than the variable's level + - We use avoidance to heal such cases: If the level-incorrect ref appears + covariantly: widen to underlying capture set, reject if that is cap and the variable does not allow it + contravariantly: narrow to {} @@ -65,10 +66,9 @@ In cv-computation (markFree): the owning method. They have to be widened to dcs(x), or, where this is not possible, it's an error. -In well-formedness checking of explicitly written type T: +In box adaptation: - - If T is not the type of a parameter, check that no cap occurrence or EX-bound variable appears - under a box. + - Check that existential variables are not boxed or unboxed. Subtype rules @@ -129,6 +129,18 @@ Subtype checking algorithm, steps to add for tp1 <:< tp2: assocExistentials(tp2).isDefined && (assocExistentials(tp2).contains(tp1) || tp1 is not existentially bound) +Subtype checking algorithm, comparing two capture sets CS1 <:< CS2: + + We need to map the (possibly to-be-added) existentials in CS1 to existentials + in CS2 so that we can compare them. We use `assocExistentals` for that: + To map an EX-variable V1 in CS1, pick the last (i.e. outermost, leading to the smallest + type) EX-variable in `assocExistentials` that has V1 in its possible instances. + To go the other way (and therby produce a BiTypeMap), map an EX-variable + V2 in CS2 to the first (i.e. innermost) EX-variable it can be instantiated to. + If either direction is not defined, we choose a special "bad-existetal" value + that represents and out-of-scope existential. This leads to failure + of the comparison. + Existential source syntax: Existential types are ususally not written in source, since we still allow the `^` @@ -142,7 +154,8 @@ Existential source syntax: Existential types can only at the top level of the result type of a function or method. -Restrictions on Existential Types: +Restrictions on Existential Types: (to be implemented if we want to +keep the source syntax for users). - An existential capture ref must be the only member of its set. This is intended to model the idea that existential variables effectibely range @@ -353,11 +366,14 @@ object Existential: case ref: TermParamRef => isExistentialMethod(ref.binder) case _ => false + /** An value signalling an out-of-scope existential that should + * lead to a compare failure. + */ + def badExistential(using Context): TermParamRef = + exMethodType(identity, nme.OOS_EXISTENTIAL).paramRefs.head + def isBadExistential(ref: CaptureRef) = ref match case ref: TermParamRef => ref.paramName == nme.OOS_EXISTENTIAL case _ => false - def badExistential(using Context): TermParamRef = - exMethodType(identity, nme.OOS_EXISTENTIAL).paramRefs.head - end Existential diff --git a/tests/neg-custom-args/captures/widen-reach.check b/tests/neg-custom-args/captures/widen-reach.check index a4ea91981702..6f496b87fd16 100644 --- a/tests/neg-custom-args/captures/widen-reach.check +++ b/tests/neg-custom-args/captures/widen-reach.check @@ -12,3 +12,9 @@ | Required: IO^ -> IO^{x*} | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/widen-reach.scala:8:18 -------------------------------------------------------- +8 |trait Bar extends Foo[IO^]: // error + | ^ + | IO^{ex$3} cannot be box-converted to box IO^ + | since at least one of their capture sets contains the root capability `cap` +9 | val foo: IO^ -> IO^ = x => x diff --git a/tests/neg-custom-args/captures/widen-reach.scala b/tests/neg-custom-args/captures/widen-reach.scala index 5b9cd667d901..9a9305640473 100644 --- a/tests/neg-custom-args/captures/widen-reach.scala +++ b/tests/neg-custom-args/captures/widen-reach.scala @@ -5,7 +5,7 @@ trait IO trait Foo[+T]: val foo: IO^ -> T -trait Bar extends Foo[IO^]: +trait Bar extends Foo[IO^]: // error val foo: IO^ -> IO^ = x => x def test(x: Foo[IO^]): Unit = From 516df7c5e95a6b6c96c1e68348f34f2008eb4ea0 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 19:45:22 +0200 Subject: [PATCH 22/35] Fix existential mapping of non-dependent impure function aliases => There was a forgotten case. --- compiler/src/dotty/tools/dotc/cc/Existential.scala | 7 +++++++ tests/neg/existential-mapping.check | 10 +++++----- tests/neg/existential-mapping.scala | 1 - 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Existential.scala b/compiler/src/dotty/tools/dotc/cc/Existential.scala index 9809d97a4504..732510789e28 100644 --- a/compiler/src/dotty/tools/dotc/cc/Existential.scala +++ b/compiler/src/dotty/tools/dotc/cc/Existential.scala @@ -315,6 +315,12 @@ object Existential: case t @ CapturingType(parent, refs: CaptureSet.Var) => if variance > 0 then needsWrap = true super.mapOver(t) + case defn.FunctionNOf(args, res, contextual) if t.typeSymbol.name.isImpureFunction => + if variance > 0 then + needsWrap = true + super.mapOver: + defn.FunctionNOf(args, res, contextual).capturing(boundVar.singletonCaptureSet) + else mapOver(t) case _ => mapOver(t) //.showing(i"mapcap $t = $result") @@ -341,6 +347,7 @@ object Existential: case res: MethodType => mapFunOrMethod(res, res.paramInfos, res.resType) case res: PolyType => mapFunOrMethod(res, Nil, res.resType) // TODO: Also map bounds of PolyTypes case _ => mapCap(apply(res), fail) + //.showing(i"map cap res $res / ${apply(res)} of $tp = $result") tp.derivedFunctionOrMethod(args1, res1) def apply(t: Type): Type = t match diff --git a/tests/neg/existential-mapping.check b/tests/neg/existential-mapping.check index 7c1de8b31529..edfce67f6eef 100644 --- a/tests/neg/existential-mapping.check +++ b/tests/neg/existential-mapping.check @@ -68,21 +68,21 @@ -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:36:25 ----------------------------------------------- 36 | val _: A^ => C^ => C = y4 // error | ^^ - | Found: (y4 : A^ => C^ => (ex$52: caps.Exists) -> C^{ex$52}) - | Required: A^ => C^ => C + | Found: (y4 : A^ => (ex$53: caps.Exists) -> C^ ->{ex$53} (ex$52: caps.Exists) -> C^{ex$52}) + | Required: A^ => (ex$56: caps.Exists) -> C^ ->{ex$56} C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:39:30 ----------------------------------------------- 39 | val _: A^ => (x: C^) -> C = y5 // error | ^^ - | Found: (y5 : A^ => (ex$60: caps.Exists) -> Fun[C^{ex$60}]) + | Found: (y5 : A^ => (ex$58: caps.Exists) -> Fun[C^{ex$58}]) | Required: A^ => (x: C^) -> C | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg/existential-mapping.scala:42:30 ----------------------------------------------- 42 | val _: A^ => (x: C^) => C = y6 // error | ^^ - | Found: (y6 : A^ => (ex$66: caps.Exists) -> IFun[C^{ex$66}]) - | Required: A^ => (ex$69: caps.Exists) -> (x: C^) ->{ex$69} C + | Found: (y6 : A^ => (ex$64: caps.Exists) -> IFun[C^{ex$64}]) + | Required: A^ => (ex$67: caps.Exists) -> (x: C^) ->{ex$67} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg/existential-mapping.scala b/tests/neg/existential-mapping.scala index 96a36d8a7b9b..290f7dc767a6 100644 --- a/tests/neg/existential-mapping.scala +++ b/tests/neg/existential-mapping.scala @@ -44,4 +44,3 @@ def Test = val z1: A^ => Array[C^] = ??? // error - From bca3978d8dd58fe103550c4afe9f7e182e952904 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 20:29:21 +0200 Subject: [PATCH 23/35] Drop restrictions in widenReachCaptures These should be no longer necessary with existentials. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +++++--- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- tests/neg-custom-args/captures/widen-reach.check | 13 +++++++------ tests/neg-custom-args/captures/widen-reach.scala | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 1516b769c7ee..7e5c647753c2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -381,11 +381,13 @@ extension (tp: Type) t.dealias match case t1 @ CapturingType(p, cs) if cs.isUniversal && !isFlipped => t1.derivedCapturingType(apply(p), ref.reach.singletonCaptureSet) - case t @ FunctionOrMethod(args, res @ Existential(_, _)) + case t1 @ FunctionOrMethod(args, res @ Existential(_, _)) if args.forall(_.isAlwaysPure) => // Also map existentials in results to reach capabilities if all // preceding arguments are known to be always pure - apply(t.derivedFunctionOrMethod(args, Existential.toCap(res))) + apply(t1.derivedFunctionOrMethod(args, Existential.toCap(res))) + case Existential(_, _) => + t case _ => t match case t @ CapturingType(p, cs) => t.derivedCapturingType(apply(p), cs) // don't map capture set variables @@ -397,7 +399,7 @@ extension (tp: Type) ref match case ref: CaptureRef if ref.isTrackableRef => val checker = new CheckContraCaps - checker.traverse(tp) + if !ccConfig.useExistentials then checker.traverse(tp) if checker.ok then val tp1 = narrowCaps(tp) if tp1 ne tp then capt.println(i"narrow $tp of $ref to $tp1") diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 54bbb54997a5..78aad96a4ff8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1103,7 +1103,7 @@ class CheckCaptures extends Recheck, SymTransformer: ccConfig.allowUniversalInBoxed || expected.hasAnnotation(defn.UncheckedCapturesAnnot) || actual.widen.hasAnnotation(defn.UncheckedCapturesAnnot) - if criticalSet.isUniversal && expected.isValueType && !allowUniversalInBoxed then + if criticalSet.isUnboxable && expected.isValueType && !allowUniversalInBoxed then // We can't box/unbox the universal capability. Leave `actual` as it is // so we get an error in checkConforms. Add the error message generated // from boxing as an addendum. This tends to give better error diff --git a/tests/neg-custom-args/captures/widen-reach.check b/tests/neg-custom-args/captures/widen-reach.check index 6f496b87fd16..dbe811ab99ec 100644 --- a/tests/neg-custom-args/captures/widen-reach.check +++ b/tests/neg-custom-args/captures/widen-reach.check @@ -12,9 +12,10 @@ | Required: IO^ -> IO^{x*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/widen-reach.scala:8:18 -------------------------------------------------------- -8 |trait Bar extends Foo[IO^]: // error - | ^ - | IO^{ex$3} cannot be box-converted to box IO^ - | since at least one of their capture sets contains the root capability `cap` -9 | val foo: IO^ -> IO^ = x => x +-- [E164] Declaration Error: tests/neg-custom-args/captures/widen-reach.scala:9:6 -------------------------------------- +9 | val foo: IO^ -> IO^ = x => x // error + | ^ + | error overriding value foo in trait Foo of type IO^ -> box IO^; + | value foo of type IO^ -> (ex$3: caps.Exists) -> IO^{ex$3} has incompatible type + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/widen-reach.scala b/tests/neg-custom-args/captures/widen-reach.scala index 9a9305640473..fa5eee1232df 100644 --- a/tests/neg-custom-args/captures/widen-reach.scala +++ b/tests/neg-custom-args/captures/widen-reach.scala @@ -5,8 +5,8 @@ trait IO trait Foo[+T]: val foo: IO^ -> T -trait Bar extends Foo[IO^]: // error - val foo: IO^ -> IO^ = x => x +trait Bar extends Foo[IO^]: + val foo: IO^ -> IO^ = x => x // error def test(x: Foo[IO^]): Unit = val y1: Foo[IO^{x*}] = x From 87e34ed7dd92d3aa1c512b2f5b952f9de0bb8ca7 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jun 2024 09:33:23 +0200 Subject: [PATCH 24/35] Cleanup ccConfig settings --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 16 +++++----- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 5 ++-- .../dotty/tools/dotc/cc/CheckCaptures.scala | 29 +++++-------------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 +-- 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7e5c647753c2..633aaae57a5d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -27,21 +27,19 @@ object ccConfig: */ inline val allowUnsoundMaps = false - /** If true, expand capability classes in Setup instead of treating them - * in adapt. - */ - inline val expandCapabilityInSetup = true - + /** If true, use existential capture set variables */ def useExistentials(using Context) = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`) - /** If true, use `sealed` as encapsulation mechanism instead of the - * previous global retriction that `cap` can't be boxed or unboxed. + /** If true, use "sealed" as encapsulation mechanism, meaning that we + * check that type variable instantiations don't have `cap` in any of + * their capture sets. This is an alternative of the original restriction + * that `cap` can't be boxed or unboxed. It is used in 3.3 and 3.4 but + * dropped again in 3.5. */ - def allowUniversalInBoxed(using Context) = + def useSealed(using Context) = Feature.sourceVersion.stable == SourceVersion.`3.3` || Feature.sourceVersion.stable == SourceVersion.`3.4` - //|| Feature.sourceVersion.stable == SourceVersion.`3.5` // drop `//` if you want to test with the sealed type params strategy end ccConfig diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index e83da64a920a..80e563e108a0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1059,9 +1059,8 @@ object CaptureSet: tp.captureSet case tp: TermParamRef => tp.captureSet - case tp: TypeRef => - if !ccConfig.expandCapabilityInSetup && tp.derivesFromCapability then universal - else empty + case _: TypeRef => + empty case _: TypeParamRef => empty case CapturingType(parent, refs) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 78aad96a4ff8..6d6222374944 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -170,7 +170,7 @@ object CheckCaptures: traverse(parent) case t => traverseChildren(t) - if ccConfig.allowUniversalInBoxed then check.traverse(tp) + if ccConfig.useSealed then check.traverse(tp) end disallowRootCapabilitiesIn /** Attachment key for bodies of closures, provided they are values */ @@ -581,7 +581,7 @@ class CheckCaptures extends Recheck, SymTransformer: end instantiate override def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = - if ccConfig.allowUniversalInBoxed then + if ccConfig.useSealed then val TypeApply(fn, args) = tree val polyType = atPhase(thisPhase.prev): fn.tpe.widen.asInstanceOf[TypeLambda] @@ -806,7 +806,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def recheckTry(tree: Try, pt: Type)(using Context): Type = val tp = super.recheckTry(tree, pt) - if ccConfig.allowUniversalInBoxed && Feature.enabled(Feature.saferExceptions) then + if ccConfig.useSealed && Feature.enabled(Feature.saferExceptions) then disallowRootCapabilitiesIn(tp, ctx.owner, "result of `try`", "have type", "This is often caused by a locally generated exception capability leaking as part of its result.", @@ -875,7 +875,7 @@ class CheckCaptures extends Recheck, SymTransformer: } checkNotUniversal(parent) case _ => - if !ccConfig.allowUniversalInBoxed + if !ccConfig.useSealed && !tpe.hasAnnotation(defn.UncheckedCapturesAnnot) && needsUniversalCheck then @@ -1100,7 +1100,7 @@ class CheckCaptures extends Recheck, SymTransformer: def msg = em"""$actual cannot be box-converted to $expected |since at least one of their capture sets contains the root capability `cap`""" def allowUniversalInBoxed = - ccConfig.allowUniversalInBoxed + ccConfig.useSealed || expected.hasAnnotation(defn.UncheckedCapturesAnnot) || actual.widen.hasAnnotation(defn.UncheckedCapturesAnnot) if criticalSet.isUnboxable && expected.isValueType && !allowUniversalInBoxed then @@ -1129,20 +1129,6 @@ class CheckCaptures extends Recheck, SymTransformer: recur(actual, expected, covariant) end adaptBoxed - /** If actual derives from caps.Capability, yet is not a capturing type itself, - * make its capture set explicit. - */ - private def makeCaptureSetExplicit(actual: Type)(using Context): Type = - if ccConfig.expandCapabilityInSetup then actual - else actual match - case CapturingType(_, _) => actual - case _ if actual.derivesFromCapability => - val cap: CaptureRef = actual match - case ref: CaptureRef if ref.isTracked => ref - case _ => defn.captureRoot.termRef // TODO: skolemize? - CapturingType(actual, cap.singletonCaptureSet) - case _ => actual - /** If actual is a tracked CaptureRef `a` and widened is a capturing type T^C, * improve `T^C` to `T^{a}`, following the VAR rule of CC. */ @@ -1163,12 +1149,11 @@ class CheckCaptures extends Recheck, SymTransformer: if expected == LhsProto || expected.isSingleton && actual.isSingleton then actual else - val normalized = makeCaptureSetExplicit(actual) - val widened = improveCaptures(normalized.widen.dealiasKeepAnnots, actual) + val widened = improveCaptures(actual.widen.dealiasKeepAnnots, actual) val adapted = adaptBoxed( widened.withReachCaptures(actual), expected, pos, covariant = true, alwaysConst = false, boxErrors) - if adapted eq widened then normalized + if adapted eq widened then actual else adapted.showing(i"adapt boxed $actual vs $expected = $adapted", capt) end adapt diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 7fc78599c377..376a0453fac8 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -317,7 +317,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: this(expandThrowsAlias(res, exc, Nil)) case t => // Map references to capability classes C to C^ - if ccConfig.expandCapabilityInSetup && t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists + if t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists then CapturingType(t, CaptureSet.universal, boxed = false) else normalizeCaptures(mapOver(t)) end toCapturing @@ -397,7 +397,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: transformTT(tpt, boxed = sym.is(Mutable, butNot = Method) - && !ccConfig.allowUniversalInBoxed + && !ccConfig.useSealed && !sym.hasAnnotation(defn.UncheckedCapturesAnnot), // types of mutable variables are boxed in pre 3.3 code exact = sym.allOverriddenSymbols.hasNext, From ec1e66dce485f780998f38277a30b81616d416d0 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jun 2024 18:21:50 +0200 Subject: [PATCH 25/35] Don't add ^ to singleton capabilities during Setup --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- tests/pos-custom-args/captures/cap-problem.scala | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/pos-custom-args/captures/cap-problem.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 376a0453fac8..ee2cf4c2858d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -317,7 +317,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: this(expandThrowsAlias(res, exc, Nil)) case t => // Map references to capability classes C to C^ - if t.derivesFromCapability && t.typeSymbol != defn.Caps_Exists + if t.derivesFromCapability && !t.isSingleton && t.typeSymbol != defn.Caps_Exists then CapturingType(t, CaptureSet.universal, boxed = false) else normalizeCaptures(mapOver(t)) end toCapturing diff --git a/tests/pos-custom-args/captures/cap-problem.scala b/tests/pos-custom-args/captures/cap-problem.scala new file mode 100644 index 000000000000..483b4e938b1b --- /dev/null +++ b/tests/pos-custom-args/captures/cap-problem.scala @@ -0,0 +1,13 @@ +import language.experimental.captureChecking + +trait Suspend: + type Suspension + + def resume(s: Suspension): Unit + +import caps.Capability + +trait Async(val support: Suspend) extends Capability + +class CancelSuspension(ac: Async, suspension: ac.support.Suspension): + ac.support.resume(suspension) From de4e56ff3c8e2dd7fc223a37599d6e757c953b1d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Jun 2024 19:36:52 +0200 Subject: [PATCH 26/35] Refine parameter accessors that have nonempty deep capture sets A parameter accessor with a nonempty deep capture set needs to be tracked in refinements even if it is pure, as long as it might contain captures that can be referenced using a reach capability. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 3 +++ 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 | 2 +- compiler/src/dotty/tools/dotc/core/Symbols.scala | 3 ++- tests/pos/reach-problem.scala | 9 +++++++++ 6 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 tests/pos/reach-problem.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 633aaae57a5d..4dda8f1803e0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -501,6 +501,9 @@ extension (sym: Symbol) && !param.hasAnnotation(defn.UntrackedCapturesAnnot) } + def hasTrackedParts(using Context): Boolean = + !CaptureSet.deepCaptureSet(sym.info).isAlwaysEmpty + 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 80e563e108a0..6e9343629388 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1090,7 +1090,7 @@ object CaptureSet: recur(tp) //.showing(i"capture set of $tp = $result", captDebug) - private def deepCaptureSet(tp: Type)(using Context): CaptureSet = + def deepCaptureSet(tp: Type)(using Context): CaptureSet = val collect = new TypeAccumulator[CaptureSet]: def apply(cs: CaptureSet, t: Type) = t.dealias match case t @ CapturingType(p, cs1) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6d6222374944..3981dcbb34a2 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -554,7 +554,7 @@ class CheckCaptures extends Recheck, SymTransformer: if core.derivesFromCapability then CaptureSet.universal else initCs for (getterName, argType) <- mt.paramNames.lazyZip(argTypes) do val getter = cls.info.member(getterName).suchThat(_.isRefiningParamAccessor).symbol - if !getter.is(Private) && getter.termRef.isTracked then + if !getter.is(Private) && getter.hasTrackedParts then refined = RefinedType(refined, getterName, argType) allCaptures ++= argType.captureSet (refined, allCaptures) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ee2cf4c2858d..f588094fbdf3 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -186,7 +186,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case cls: ClassSymbol if !defn.isFunctionClass(cls) && cls.is(CaptureChecked) => cls.paramGetters.foldLeft(tp) { (core, getter) => - if atPhase(thisPhase.next)(getter.termRef.isTracked) + if atPhase(thisPhase.next)(getter.hasTrackedParts) && getter.isRefiningParamAccessor && !getter.is(Tracked) then diff --git a/compiler/src/dotty/tools/dotc/core/Symbols.scala b/compiler/src/dotty/tools/dotc/core/Symbols.scala index da0ecac47b7d..1c5d1941c524 100644 --- a/compiler/src/dotty/tools/dotc/core/Symbols.scala +++ b/compiler/src/dotty/tools/dotc/core/Symbols.scala @@ -846,7 +846,8 @@ object Symbols extends SymUtils { /** Map given symbols, subjecting their attributes to the mappings * defined in the given TreeTypeMap `ttmap`. * Cross symbol references are brought over from originals to copies. - * Do not copy any symbols if all attributes of all symbols stay the same. + * Do not copy any symbols if all attributes of all symbols stay the same + * and mapAlways is false. */ def mapSymbols(originals: List[Symbol], ttmap: TreeTypeMap, mapAlways: Boolean = false)(using Context): List[Symbol] = if (originals.forall(sym => diff --git a/tests/pos/reach-problem.scala b/tests/pos/reach-problem.scala new file mode 100644 index 000000000000..60dd1d4667a7 --- /dev/null +++ b/tests/pos/reach-problem.scala @@ -0,0 +1,9 @@ +import language.experimental.captureChecking + +class Box[T](items: Seq[T^]): + def getOne: T^{items*} = ??? + +object Box: + def getOne[T](items: Seq[T^]): T^{items*} = + val bx = Box(items) + bx.getOne \ No newline at end of file From ea182f200e2987ebc15ae935948509a83c1d09b9 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Jun 2024 11:33:27 +0200 Subject: [PATCH 27/35] More precise capture set unions When we take `{elem} <: B ++ C` where `elem` is not yet included in `B ++ C`, B is a constant and C is a variable, propagate with `{elem} <: C`. Likewise if C is a constant and B is a variable. This tries to minimize the slack between a union and its operands. Note: Propagation does not happen very often in our test suite so far: Once in pos tests and 15 times in scala2-library-cc. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 6e9343629388..98dc5db878e0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -249,8 +249,7 @@ 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(initialElems = this.elems ++ that.elems) - .addAsDependentTo(this).addAsDependentTo(that) + else Union(this, that) /** The smallest superset (via <:<) of this capture set that also contains `ref`. */ @@ -263,7 +262,7 @@ sealed abstract class CaptureSet extends Showable: if this.subCaptures(that, frozen = true).isOK then this else if that.subCaptures(this, frozen = true).isOK then that else if this.isConst && that.isConst then Const(elemIntersection(this, that)) - else Intersected(this, that) + else Intersection(this, that) /** The largest subset (via <:<) of this capture set that does not account for * any of the elements in the constant capture set `that` @@ -816,7 +815,29 @@ object CaptureSet: class Diff(source: Var, other: Const)(using Context) extends Filtered(source, !other.accountsFor(_)) - class Intersected(cs1: CaptureSet, cs2: CaptureSet)(using Context) + class Union(cs1: CaptureSet, cs2: CaptureSet)(using Context) + extends Var(initialElems = cs1.elems ++ cs2.elems): + addAsDependentTo(cs1) + addAsDependentTo(cs2) + + override def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = + if accountsFor(elem) then CompareResult.OK + else + val res = super.tryInclude(elem, origin) + // If this is the union of a constant and a variable, + // propagate `elem` to the variable part to avoid slack + // between the operands and the union. + if res.isOK && (origin ne cs1) && (origin ne cs2) then + if cs1.isConst then cs2.tryInclude(elem, origin) + else if cs2.isConst then cs1.tryInclude(elem, origin) + else res + else res + + override def propagateSolved()(using Context) = + if cs1.isConst && cs2.isConst && !isConst then markSolved() + end Union + + class Intersection(cs1: CaptureSet, cs2: CaptureSet)(using Context) extends Var(initialElems = elemIntersection(cs1, cs2)): addAsDependentTo(cs1) addAsDependentTo(cs2) @@ -841,7 +862,7 @@ object CaptureSet: override def propagateSolved()(using Context) = if cs1.isConst && cs2.isConst && !isConst then markSolved() - end Intersected + end Intersection def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) From 60b0486b125c06e96a7d5c991d9d3deb80749f7e Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 26 Jun 2024 23:21:06 +0200 Subject: [PATCH 28/35] First implementation of capture polymorphism --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 11 ++++-- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 12 ++++++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 ++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 26 +++++++++----- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- .../dotty/tools/dotc/core/Definitions.scala | 4 ++- .../src/dotty/tools/dotc/core/StdNames.scala | 3 +- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 16 +++++++-- .../dotty/tools/dotc/parsing/Parsers.scala | 17 ++++++--- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- library/src/scala/caps.scala | 8 ++++- tests/pos/cc-poly-1.scala | 23 ++++++++++++ tests/pos/cc-poly-source.scala | 36 +++++++++++++++++++ 14 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 tests/pos/cc-poly-1.scala create mode 100644 tests/pos/cc-poly-source.scala diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index c42e8f71246d..b7ad12369b88 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -521,12 +521,17 @@ 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))) + def makeCapsOf(id: Ident)(using Context): Tree = + TypeApply(Select(scalaDot(nme.caps), nme.capsOf), id :: Nil) + + def makeCapsBound()(using Context): Tree = + makeRetaining( + Select(scalaDot(nme.caps), tpnme.CapSet), + Nil, tpnme.retainsCap) + def makeConstructor(tparams: List[TypeDef], vparamss: List[List[ValDef]], rhs: Tree = EmptyTree)(using Context): DefDef = DefDef(nme.CONSTRUCTOR, joinParams(tparams, vparamss), TypeTree(), rhs) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 4dda8f1803e0..09a56fb1b359 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -131,6 +131,8 @@ extension (tree: Tree) def toCaptureRef(using Context): CaptureRef = tree match case ReachCapabilityApply(arg) => arg.toCaptureRef.reach + case CapsOfApply(arg) => + arg.toCaptureRef case _ => tree.tpe match case ref: CaptureRef if ref.isTrackableRef => ref @@ -145,7 +147,7 @@ extension (tree: Tree) case Some(refs) => refs case None => val refs = CaptureSet(tree.retainedElems.map(_.toCaptureRef)*) - .showing(i"toCaptureSet $tree --> $result", capt) + //.showing(i"toCaptureSet $tree --> $result", capt) tree.putAttachment(Captures, refs) refs @@ -526,6 +528,14 @@ object ReachCapabilityApply: case Apply(reach, arg :: Nil) if reach.symbol == defn.Caps_reachCapability => Some(arg) case _ => None +/** An extractor for `caps.capsOf[X]`, which is used to express a generic capture set + * as a tree in a @retains annotation. + */ +object CapsOfApply: + def unapply(tree: TypeApply)(using Context): Option[Tree] = tree match + case TypeApply(capsOf, arg :: Nil) if capsOf.symbol == defn.Caps_capsOf => Some(arg) + case _ => None + class AnnotatedCapability(annot: Context ?=> ClassSymbol): def apply(tp: Type)(using Context) = AnnotatedType(tp, Annotation(annot, util.Spans.NoSpan)) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 98dc5db878e0..0b19f75f14d0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -879,7 +879,9 @@ object CaptureSet: val r1 = tm(r) val upper = r1.captureSet def isExact = - upper.isAlwaysEmpty || upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1) + upper.isAlwaysEmpty + || upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1) + || r.derivesFrom(defn.Caps_CapSet) if variance > 0 || isExact then upper else if variance < 0 then CaptureSet.empty else upper.maybe diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 3981dcbb34a2..8eb2f2420369 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -123,16 +123,24 @@ object CheckCaptures: case _: SingletonType => report.error(em"Singleton type $parent cannot have capture set", parent.srcPos) case _ => + def check(elem: Tree, pos: SrcPos): Unit = 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", pos) + case tpe => + report.error(em"$elem: $tpe is not a legal element of a capture set", pos) for elem <- ann.retainedElems do - val elem1 = elem match - case ReachCapabilityApply(arg) => arg - case _ => elem - elem1.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 CapsOfApply(arg) => + def isLegalCapsOfArg = + arg.symbol.isAbstractOrParamType && arg.symbol.info.derivesFrom(defn.Caps_CapSet) + if !isLegalCapsOfArg then + report.error( + em"""$arg is not a legal prefix for `^` here, + |is must be a type parameter or abstract type with a caps.CapSet upper bound.""", + elem.srcPos) + case ReachCapabilityApply(arg) => check(arg, elem.srcPos) + case _ => check(elem, elem.srcPos) /** Report an error if some part of `tp` contains the root capability in its capture set * or if it refers to an unsealed type parameter that could possibly be instantiated with diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index f588094fbdf3..6927983ad196 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -384,7 +384,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: sym.updateInfo(thisPhase, info, newFlagsFor(sym)) toBeUpdated -= sym sym.namedType match - case ref: CaptureRef => ref.invalidateCaches() // TODO: needed? + case ref: CaptureRef if ref.isTrackableRef => ref.invalidateCaches() // TODO: needed? case _ => extension (sym: Symbol) def nextInfo(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index ad80d0565f63..e8a853469f6f 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -991,8 +991,10 @@ class Definitions { @tu lazy val CapsModule: Symbol = requiredModule("scala.caps") @tu lazy val captureRoot: TermSymbol = CapsModule.requiredValue("cap") - @tu lazy val Caps_Capability: ClassSymbol = requiredClass("scala.caps.Capability") + @tu lazy val Caps_Capability: TypeSymbol = CapsModule.requiredType("Capability") + @tu lazy val Caps_CapSet = requiredClass("scala.caps.CapSet") @tu lazy val Caps_reachCapability: TermSymbol = CapsModule.requiredMethod("reachCapability") + @tu lazy val Caps_capsOf: TermSymbol = CapsModule.requiredMethod("capsOf") @tu lazy val Caps_Exists = requiredClass("scala.caps.Exists") @tu lazy val CapsUnsafeModule: Symbol = requiredModule("scala.caps.unsafe") @tu lazy val Caps_unsafeAssumePure: Symbol = CapsUnsafeModule.requiredMethod("unsafeAssumePure") diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 6548b46186bb..d3e198a7e7a7 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -358,6 +358,7 @@ object StdNames { val AppliedTypeTree: N = "AppliedTypeTree" val ArrayAnnotArg: N = "ArrayAnnotArg" val CAP: N = "CAP" + val CapSet: N = "CapSet" val Constant: N = "Constant" val ConstantType: N = "ConstantType" val Eql: N = "Eql" @@ -441,8 +442,8 @@ object StdNames { val bytes: N = "bytes" val canEqual_ : N = "canEqual" val canEqualAny : N = "canEqualAny" - val capIn: N = "capIn" val caps: N = "caps" + val capsOf: N = "capsOf" val captureChecking: N = "captureChecking" val checkInitialized: N = "checkInitialized" val classOf: N = "classOf" diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 958515a9e625..7088232c26e1 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2839,7 +2839,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling private def existentialVarsConform(tp1: Type, tp2: Type) = tp2 match case tp2: TermParamRef => tp1 match - case tp1: CaptureRef => subsumesExistentially(tp2, tp1) + case tp1: CaptureRef if tp1.isTrackableRef => subsumesExistentially(tp2, tp1) case _ => false case _ => false diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7bcb7e453647..57618df2ebcd 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2313,7 +2313,11 @@ object Types extends TypeUtils { override def captureSet(using Context): CaptureSet = val cs = captureSetOfInfo - if isTrackableRef && !cs.isAlwaysEmpty then singletonCaptureSet else cs + if isTrackableRef then + if cs.isAlwaysEmpty then cs else singletonCaptureSet + else dealias match + case _: (TypeRef | TypeParamRef) => CaptureSet.empty + case _ => cs end CaptureRef @@ -3032,7 +3036,7 @@ object Types extends TypeUtils { abstract case class TypeRef(override val prefix: Type, private var myDesignator: Designator) - extends NamedType { + extends NamedType, CaptureRef { type ThisType = TypeRef type ThisName = TypeName @@ -3081,6 +3085,9 @@ object Types extends TypeUtils { /** Hook that can be called from creation methods in TermRef and TypeRef */ def validated(using Context): this.type = this + + override def isTrackableRef(using Context) = + symbol.isAbstractOrParamType && derivesFrom(defn.Caps_CapSet) } final class CachedTermRef(prefix: Type, designator: Designator, hc: Int) extends TermRef(prefix, designator) { @@ -4841,7 +4848,8 @@ object Types extends TypeUtils { /** Only created in `binder.paramRefs`. Use `binder.paramRefs(paramNum)` to * refer to `TypeParamRef(binder, paramNum)`. */ - abstract case class TypeParamRef(binder: TypeLambda, paramNum: Int) extends ParamRef { + abstract case class TypeParamRef(binder: TypeLambda, paramNum: Int) + extends ParamRef, CaptureRef { type BT = TypeLambda def kindString: String = "Type" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) @@ -4861,6 +4869,8 @@ object Types extends TypeUtils { case bound: OrType => occursIn(bound.tp1, fromBelow) || occursIn(bound.tp2, fromBelow) case _ => false } + + override def isTrackableRef(using Context) = derivesFrom(defn.Caps_CapSet) } private final class TypeParamRefImpl(binder: TypeLambda, paramNum: Int) extends TypeParamRef(binder, paramNum) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 4c13934f3473..5c0374ceba87 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1541,7 +1541,7 @@ object Parsers { case _ => None } - /** CaptureRef ::= ident | `this` | `cap` [`[` ident `]`] + /** CaptureRef ::= ident [`*` | `^`] | `this` */ def captureRef(): Tree = if in.token == THIS then simpleRef() @@ -1551,6 +1551,10 @@ object Parsers { in.nextToken() atSpan(startOffset(id)): PostfixOp(id, Ident(nme.CC_REACH)) + else if isIdent(nme.UPARROW) then + in.nextToken() + atSpan(startOffset(id)): + makeCapsOf(cpy.Ident(id)(id.name.toTypeName)) else id /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under captureChecking @@ -1968,7 +1972,7 @@ object Parsers { } /** SimpleType ::= SimpleLiteral - * | ‘?’ SubtypeBounds + * | ‘?’ TypeBounds * | SimpleType1 * | SimpleType ‘(’ Singletons ‘)’ -- under language.experimental.dependent, checked in Typer * Singletons ::= Singleton {‘,’ Singleton} @@ -2188,9 +2192,15 @@ object Parsers { inBraces(refineStatSeq()) /** TypeBounds ::= [`>:' Type] [`<:' Type] + * | `^` -- under captureChecking */ def typeBounds(): TypeBoundsTree = - atSpan(in.offset) { TypeBoundsTree(bound(SUPERTYPE), bound(SUBTYPE)) } + atSpan(in.offset): + if in.isIdent(nme.UPARROW) && Feature.ccEnabled then + in.nextToken() + TypeBoundsTree(EmptyTree, makeCapsBound()) + else + TypeBoundsTree(bound(SUPERTYPE), bound(SUBTYPE)) private def bound(tok: Int): Tree = if (in.token == tok) { in.nextToken(); toplevelTyp() } @@ -3384,7 +3394,6 @@ object Parsers { * * DefTypeParamClause::= ‘[’ DefTypeParam {‘,’ DefTypeParam} ‘]’ * DefTypeParam ::= {Annotation} - * [`sealed`] -- under captureChecking * id [HkTypeParamClause] TypeParamBounds * * TypTypeParamClause::= ‘[’ TypTypeParam {‘,’ TypTypeParam} ‘]’ diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index d3e411d5ea4d..6ca87127d3c9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2328,7 +2328,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val res = Throw(expr1).withSpan(tree.span) if Feature.ccEnabled && !cap.isEmpty && !ctx.isAfterTyper then // Record access to the CanThrow capabulity recovered in `cap` by wrapping - // the type of the `throw` (i.e. Nothing) in a `@requiresCapability` annotatoon. + // the type of the `throw` (i.e. Nothing) in a `@requiresCapability` annotation. Typed(res, TypeTree( AnnotatedType(res.tpe, diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 5ae5b860f501..967246041082 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -1,6 +1,6 @@ package scala -import annotation.experimental +import annotation.{experimental, compileTimeOnly} @experimental object caps: @@ -16,6 +16,12 @@ import annotation.experimental @deprecated("Use `Capability` instead") type Cap = Capability + /** Carrier trait for capture set type parameters */ + trait CapSet extends Any + + @compileTimeOnly("Should be be used only internally by the Scala compiler") + def capsOf[CS]: Any = ??? + /** Reach capabilities x* which appear as terms in @retains annotations are encoded * as `caps.reachCapability(x)`. When converted to CaptureRef types in capture sets * they are represented as `x.type @annotation.internal.reachCapability`. diff --git a/tests/pos/cc-poly-1.scala b/tests/pos/cc-poly-1.scala new file mode 100644 index 000000000000..69b7557b8466 --- /dev/null +++ b/tests/pos/cc-poly-1.scala @@ -0,0 +1,23 @@ +import language.experimental.captureChecking +import annotation.experimental +import caps.{CapSet, Capability} + +@experimental object Test: + + class C extends Capability + class D + + def f[X^](x: D^{X^}): D^{X^} = x + def g[X^](x: D^{X^}, y: D^{X^}): D^{X^} = x + + def test(c1: C, c2: C) = + val d: D^{c1, c2} = D() + val x = f[CapSet^{c1, c2}](d) + val _: D^{c1, c2} = x + val d1: D^{c1} = D() + val d2: D^{c2} = D() + val y = g(d1, d2) + val _: D^{d1, d2} = y + val _: D^{c1, c2} = y + + diff --git a/tests/pos/cc-poly-source.scala b/tests/pos/cc-poly-source.scala new file mode 100644 index 000000000000..939f1f682dc8 --- /dev/null +++ b/tests/pos/cc-poly-source.scala @@ -0,0 +1,36 @@ +import language.experimental.captureChecking +import annotation.experimental +import caps.{CapSet, Capability} + +@experimental object Test: + + class Label //extends Capability + + class Listener + + class Source[X^]: + private var listeners: Set[Listener^{X^}] = Set.empty + def register(x: Listener^{X^}): Unit = + listeners += x + + def allListeners: Set[Listener^{X^}] = listeners + + def test1(lbl1: Label^, lbl2: Label^) = + val src = Source[CapSet^{lbl1, lbl2}] + def l1: Listener^{lbl1} = ??? + val l2: Listener^{lbl2} = ??? + src.register{l1} + src.register{l2} + val ls = src.allListeners + val _: Set[Listener^{lbl1, lbl2}] = ls + + def test2(lbls: List[Label^]) = + def makeListener(lbl: Label^): Listener^{lbl} = ??? + val listeners = lbls.map(makeListener) + val src = Source[CapSet^{lbls*}] + for l <- listeners do + src.register(l) + val ls = src.allListeners + val _: Set[Listener^{lbls*}] = ls + + From 38cf302f57b042657082cfc7525b70e9058aeaf6 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Jun 2024 13:40:20 +0200 Subject: [PATCH 29/35] Reclassify maximal capabilities Don't treat user-defined capabilities deriving from caps.Capability as maximal. That was a vestige from when we treated capability classes natively. It caused code that should compile to fail because if `x extends Capability` then `x` could not be widened to `x*`. As a consequence we have one missed error in effect-swaps again, which re-establishes the original (faulty) situation. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 6 ++-- .../captures/effect-swaps.check | 4 --- .../captures/effect-swaps.scala | 2 +- tests/pos/cc-poly-source-capability.scala | 36 +++++++++++++++++++ tests/pos/reach-capability.scala | 17 +++++++++ 6 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 tests/pos/cc-poly-source-capability.scala create mode 100644 tests/pos/reach-capability.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 6927983ad196..6d1eb2a7bd38 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -743,7 +743,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: if others.accountsFor(ref) then report.warning(em"redundant capture: $dom already accounts for $ref", pos) - if ref.captureSetOfInfo.elems.isEmpty then + if ref.captureSetOfInfo.elems.isEmpty && !ref.derivesFrom(defn.Caps_Capability) then report.error(em"$ref cannot be tracked since its capture set is empty", pos) check(parent.captureSet, parent) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 57618df2ebcd..82cc25f897ab 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3027,8 +3027,7 @@ object Types extends TypeUtils { name == nme.CAPTURE_ROOT && symbol == defn.captureRoot override def isMaxCapability(using Context): Boolean = - import cc.* - this.derivesFromCapability && symbol.isStableMember + symbol == defn.captureRoot || info.derivesFrom(defn.Caps_Exists) override def normalizedRef(using Context): CaptureRef = if isTrackableRef then symbol.termRef else this @@ -4839,8 +4838,7 @@ object Types extends TypeUtils { def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) override def isTrackableRef(using Context) = true override def isMaxCapability(using Context) = - import cc.* - this.derivesFromCapability + underlying.derivesFrom(defn.Caps_Exists) } private final class TermParamRefImpl(binder: TermLambda, paramNum: Int) extends TermParamRef(binder, paramNum) diff --git a/tests/neg-custom-args/captures/effect-swaps.check b/tests/neg-custom-args/captures/effect-swaps.check index 22941be36794..ef5a95d333bf 100644 --- a/tests/neg-custom-args/captures/effect-swaps.check +++ b/tests/neg-custom-args/captures/effect-swaps.check @@ -22,7 +22,3 @@ 73 | fr.await.ok | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/effect-swaps.scala:66:15 ------------------------------------------------------ -66 | Result.make: // error - | ^^^^^^^^^^^ - | escaping local reference contextual$9.type diff --git a/tests/neg-custom-args/captures/effect-swaps.scala b/tests/neg-custom-args/captures/effect-swaps.scala index 0b362b80e3ce..4bafd6421af3 100644 --- a/tests/neg-custom-args/captures/effect-swaps.scala +++ b/tests/neg-custom-args/captures/effect-swaps.scala @@ -63,7 +63,7 @@ def test[T, E](using Async) = fr.await.ok def fail4[T, E](fr: Future[Result[T, E]]^) = - Result.make: // error + Result.make: // should be errorm but inders Result[Any, Any] Future: fut ?=> fr.await.ok diff --git a/tests/pos/cc-poly-source-capability.scala b/tests/pos/cc-poly-source-capability.scala new file mode 100644 index 000000000000..48b2d13599fd --- /dev/null +++ b/tests/pos/cc-poly-source-capability.scala @@ -0,0 +1,36 @@ +import language.experimental.captureChecking +import annotation.experimental +import caps.{CapSet, Capability} + +@experimental object Test: + + class Label extends Capability + + class Listener + + class Source[X^]: + private var listeners: Set[Listener^{X^}] = Set.empty + def register(x: Listener^{X^}): Unit = + listeners += x + + def allListeners: Set[Listener^{X^}] = listeners + + def test1(lbl1: Label, lbl2: Label) = + val src = Source[CapSet^{lbl1, lbl2}] + def l1: Listener^{lbl1} = ??? + val l2: Listener^{lbl2} = ??? + src.register{l1} + src.register{l2} + val ls = src.allListeners + val _: Set[Listener^{lbl1, lbl2}] = ls + + def test2(lbls: List[Label]) = + def makeListener(lbl: Label): Listener^{lbl} = ??? + val listeners = lbls.map(makeListener) + val src = Source[CapSet^{lbls*}] + for l <- listeners do + src.register(l) + val ls = src.allListeners + val _: Set[Listener^{lbls*}] = ls + + diff --git a/tests/pos/reach-capability.scala b/tests/pos/reach-capability.scala new file mode 100644 index 000000000000..d551113eb05b --- /dev/null +++ b/tests/pos/reach-capability.scala @@ -0,0 +1,17 @@ +import language.experimental.captureChecking +import annotation.experimental +import caps.{Capability} + +@experimental object Test2: + + class List[+A]: + def map[B](f: A => B): List[B] = ??? + + class Label extends Capability + + class Listener + + def test2(lbls: List[Label]) = + def makeListener(lbl: Label): Listener^{lbl} = ??? + val listeners = lbls.map(makeListener) // should work + From dd4a60a584dbf6c401de7f0d52a615d19db8bddd Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Jun 2024 13:59:57 +0200 Subject: [PATCH 30/35] Bring back RefiningVar We might want to treat it specially since a RefiningVar should ideally be closed for further additions when the constructor has been analyzed. --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 7 +++++++ compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 +---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 0b19f75f14d0..a2233f862e53 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -634,6 +634,13 @@ object CaptureSet: override def toString = s"Var$id$elems" end Var + /** Variables that represent refinements of class parameters can have the universal + * capture set, since they represent only what is the result of the constructor. + * Test case: Without that tweak, logger.scala would not compile. + */ + class RefiningVar(owner: Symbol)(using Context) extends Var(owner): + override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context) = this + /** 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): diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 6d1eb2a7bd38..7d2f6c24ce2d 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -193,10 +193,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: val getterType = mapInferred(refine = false)(tp.memberInfo(getter)).strippedDealias RefinedType(core, getter.name, - CapturingType(getterType, - new CaptureSet.Var(ctx.owner): - override def disallowRootCapability(handler: () => Context ?=> Unit)(using Context) = this - )) + CapturingType(getterType, new CaptureSet.RefiningVar(ctx.owner))) .showing(i"add capture refinement $tp --> $result", capt) else core From 185f17754c7323e6793f31e272ebac1291240ff7 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Jun 2024 18:43:40 +0200 Subject: [PATCH 31/35] Allow for embedded CapSet^{refs} entries in capture sets --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 22 ++++++---- compiler/src/dotty/tools/dotc/cc/Setup.scala | 41 ++++++++++--------- tests/pos/cc-poly-source-capability.scala | 30 ++++++-------- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 09a56fb1b359..3b25f0d58035 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -64,7 +64,7 @@ def depFun(args: List[Type], resultType: Type, isContextual: Boolean, paramNames mt.toFunctionType(alwaysDependent = true) /** An exception thrown if a @retains argument is not syntactically a CaptureRef */ -class IllegalCaptureRef(tpe: Type) extends Exception(tpe.toString) +class IllegalCaptureRef(tpe: Type)(using Context) extends Exception(tpe.show) /** Capture checking state, which is known to other capture checking components */ class CCState: @@ -127,15 +127,21 @@ class NoCommonRoot(rs: Symbol*)(using Context) extends Exception( extension (tree: Tree) - /** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */ - def toCaptureRef(using Context): CaptureRef = tree match + /** Map tree with CaptureRef type to its type, + * map CapSet^{refs} to the `refs` references, + * throw IllegalCaptureRef otherwise + */ + def toCaptureRefs(using Context): List[CaptureRef] = tree match case ReachCapabilityApply(arg) => - arg.toCaptureRef.reach + arg.toCaptureRefs.map(_.reach) case CapsOfApply(arg) => - arg.toCaptureRef - case _ => tree.tpe match + arg.toCaptureRefs + case _ => tree.tpe.dealiasKeepAnnots match case ref: CaptureRef if ref.isTrackableRef => - ref + ref :: Nil + case AnnotatedType(parent, ann) + if ann.symbol.isRetains && parent.derivesFrom(defn.Caps_CapSet) => + ann.tree.toCaptureSet.elems.toList case tpe => throw IllegalCaptureRef(tpe) // if this was compiled from cc syntax, problem should have been reported at Typer @@ -146,7 +152,7 @@ extension (tree: Tree) tree.getAttachment(Captures) match case Some(refs) => refs case None => - val refs = CaptureSet(tree.retainedElems.map(_.toCaptureRef)*) + val refs = CaptureSet(tree.retainedElems.flatMap(_.toCaptureRefs)*) //.showing(i"toCaptureSet $tree --> $result", capt) tree.putAttachment(Captures, refs) refs diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 7d2f6c24ce2d..cb74e2c71e73 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -729,25 +729,28 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: var retained = ann.retainedElems.toArray for i <- 0 until retained.length do val refTree = retained(i) - val ref = refTree.toCaptureRef - - def pos = - if refTree.span.exists then refTree.srcPos - else if ann.span.exists then ann.srcPos - else tpt.srcPos - - def check(others: CaptureSet, dom: Type | CaptureSet): Unit = - if others.accountsFor(ref) then - report.warning(em"redundant capture: $dom already accounts for $ref", pos) - - if ref.captureSetOfInfo.elems.isEmpty && !ref.derivesFrom(defn.Caps_Capability) then - report.error(em"$ref cannot be tracked since its capture set is empty", pos) - check(parent.captureSet, parent) - - val others = - for j <- 0 until retained.length if j != i yield retained(j).toCaptureRef - val remaining = CaptureSet(others*) - check(remaining, remaining) + for ref <- refTree.toCaptureRefs do + def pos = + if refTree.span.exists then refTree.srcPos + else if ann.span.exists then ann.srcPos + else tpt.srcPos + + def check(others: CaptureSet, dom: Type | CaptureSet): Unit = + if others.accountsFor(ref) then + report.warning(em"redundant capture: $dom already accounts for $ref", pos) + + if ref.captureSetOfInfo.elems.isEmpty && !ref.derivesFrom(defn.Caps_Capability) then + report.error(em"$ref cannot be tracked since its capture set is empty", pos) + check(parent.captureSet, parent) + + val others = + for + j <- 0 until retained.length if j != i + r <- retained(j).toCaptureRefs + yield r + val remaining = CaptureSet(others*) + check(remaining, remaining) + end for end for end checkWellformedPost diff --git a/tests/pos/cc-poly-source-capability.scala b/tests/pos/cc-poly-source-capability.scala index 48b2d13599fd..9a21b2d5b802 100644 --- a/tests/pos/cc-poly-source-capability.scala +++ b/tests/pos/cc-poly-source-capability.scala @@ -4,7 +4,9 @@ import caps.{CapSet, Capability} @experimental object Test: - class Label extends Capability + class Async extends Capability + + def listener(async: Async): Listener^{async} = ??? class Listener @@ -15,22 +17,16 @@ import caps.{CapSet, Capability} def allListeners: Set[Listener^{X^}] = listeners - def test1(lbl1: Label, lbl2: Label) = - val src = Source[CapSet^{lbl1, lbl2}] - def l1: Listener^{lbl1} = ??? - val l2: Listener^{lbl2} = ??? - src.register{l1} - src.register{l2} - val ls = src.allListeners - val _: Set[Listener^{lbl1, lbl2}] = ls - - def test2(lbls: List[Label]) = - def makeListener(lbl: Label): Listener^{lbl} = ??? - val listeners = lbls.map(makeListener) - val src = Source[CapSet^{lbls*}] - for l <- listeners do - src.register(l) + def test1(async1: Async, others: List[Async]) = + val src = Source[CapSet^{async1, others*}] + val lst1 = listener(async1) + val lsts = others.map(listener) + val _: List[Listener^{others*}] = lsts + src.register{lst1} + src.register(listener(async1)) + lsts.foreach(src.register) + others.map(listener).foreach(src.register) val ls = src.allListeners - val _: Set[Listener^{lbls*}] = ls + val _: Set[Listener^{async1, others*}] = ls From 27126ae59c53cd997757ef330600bba201162e59 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 18:01:51 +0200 Subject: [PATCH 32/35] Improve printing of capture sets before cc Use the syntactic sugar instead of expanding with capsOf --- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 3 +++ tests/pos/cc-poly-1.scala | 3 +++ 2 files changed, 6 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 71ebb7054000..aca5972d4516 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -165,6 +165,8 @@ class PlainPrinter(_ctx: Context) extends Printer { private def toTextRetainedElem[T <: Untyped](ref: Tree[T]): Text = ref match case ref: RefTree[?] if ref.typeOpt.exists => toTextCaptureRef(ref.typeOpt) + case TypeApply(fn, arg :: Nil) if fn.symbol == defn.Caps_capsOf => + toTextRetainedElem(arg) case _ => toText(ref) @@ -416,6 +418,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: SingletonType => toTextRef(tp) case ReachCapability(tp1) => toTextRef(tp1) ~ "*" case MaybeCapability(tp1) => toTextRef(tp1) ~ "?" + case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" case _ => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = diff --git a/tests/pos/cc-poly-1.scala b/tests/pos/cc-poly-1.scala index 69b7557b8466..ed32d94f7a99 100644 --- a/tests/pos/cc-poly-1.scala +++ b/tests/pos/cc-poly-1.scala @@ -9,6 +9,7 @@ import caps.{CapSet, Capability} def f[X^](x: D^{X^}): D^{X^} = x def g[X^](x: D^{X^}, y: D^{X^}): D^{X^} = x + def h[X^](): D^{X^} = ??? def test(c1: C, c2: C) = val d: D^{c1, c2} = D() @@ -19,5 +20,7 @@ import caps.{CapSet, Capability} val y = g(d1, d2) val _: D^{d1, d2} = y val _: D^{c1, c2} = y + val z = h() + From e8167e459bb55e77746bc87c9d29099be77242ef Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 18:49:07 +0200 Subject: [PATCH 33/35] Add some neg tests --- tests/neg/cc-poly-1.check | 12 ++++++++++++ tests/neg/cc-poly-1.scala | 13 +++++++++++++ tests/neg/cc-poly-2.check | 21 +++++++++++++++++++++ tests/neg/cc-poly-2.scala | 16 ++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 tests/neg/cc-poly-1.check create mode 100644 tests/neg/cc-poly-1.scala create mode 100644 tests/neg/cc-poly-2.check create mode 100644 tests/neg/cc-poly-2.scala diff --git a/tests/neg/cc-poly-1.check b/tests/neg/cc-poly-1.check new file mode 100644 index 000000000000..abb507078bf4 --- /dev/null +++ b/tests/neg/cc-poly-1.check @@ -0,0 +1,12 @@ +-- [E057] Type Mismatch Error: tests/neg/cc-poly-1.scala:12:6 ---------------------------------------------------------- +12 | f[Any](D()) // error + | ^ + | Type argument Any does not conform to upper bound caps.CapSet^ + | + | longer explanation available when compiling with `-explain` +-- [E057] Type Mismatch Error: tests/neg/cc-poly-1.scala:13:6 ---------------------------------------------------------- +13 | f[String](D()) // error + | ^ + | Type argument String does not conform to upper bound caps.CapSet^ + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/cc-poly-1.scala b/tests/neg/cc-poly-1.scala new file mode 100644 index 000000000000..580b124bc8f3 --- /dev/null +++ b/tests/neg/cc-poly-1.scala @@ -0,0 +1,13 @@ +import language.experimental.captureChecking +import caps.{CapSet, Capability} + +object Test: + + class C extends Capability + class D + + def f[X^](x: D^{X^}): D^{X^} = x + + def test(c1: C, c2: C) = + f[Any](D()) // error + f[String](D()) // error diff --git a/tests/neg/cc-poly-2.check b/tests/neg/cc-poly-2.check new file mode 100644 index 000000000000..0615ce19b5ea --- /dev/null +++ b/tests/neg/cc-poly-2.check @@ -0,0 +1,21 @@ +-- [E007] Type Mismatch Error: tests/neg/cc-poly-2.scala:13:15 --------------------------------------------------------- +13 | f[Nothing](d) // error + | ^ + | Found: (d : Test.D^) + | Required: Test.D + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/cc-poly-2.scala:14:19 --------------------------------------------------------- +14 | f[CapSet^{c1}](d) // error + | ^ + | Found: (d : Test.D^) + | Required: Test.D^{c1} + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg/cc-poly-2.scala:16:20 --------------------------------------------------------- +16 | val _: D^{c1} = x // error + | ^ + | Found: (x : Test.D^{d}) + | Required: Test.D^{c1} + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/cc-poly-2.scala b/tests/neg/cc-poly-2.scala new file mode 100644 index 000000000000..c5e5df6540da --- /dev/null +++ b/tests/neg/cc-poly-2.scala @@ -0,0 +1,16 @@ +import language.experimental.captureChecking +import caps.{CapSet, Capability} + +object Test: + + class C extends Capability + class D + + def f[X^](x: D^{X^}): D^{X^} = x + + def test(c1: C, c2: C) = + val d: D^ = D() + f[Nothing](d) // error + f[CapSet^{c1}](d) // error + val x = f(d) + val _: D^{c1} = x // error From 4f1d91a9116c7b2b9a0495a3849fc5f12206a557 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 19:08:42 +0200 Subject: [PATCH 34/35] Refactor CaptureRef operations Make all operations final methods on Type or CaptureRef --- .../dotty/tools/dotc/core/TypeComparer.scala | 12 -- .../src/dotty/tools/dotc/core/Types.scala | 145 +++++++++--------- 2 files changed, 69 insertions(+), 88 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 7088232c26e1..6a17a888772c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2831,18 +2831,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false Existential.isExistentialVar(tp1) && canInstantiateWith(assocExistentials) - /** Are tp1, tp2 termRefs that can be linked? This should never be called - * normally, since exietential variables appear only in capture sets - * which are in annotations that are ignored during normal typing. The real - * work is done in CaptureSet#subsumes which calls linkOK directly. - */ - private def existentialVarsConform(tp1: Type, tp2: Type) = - tp2 match - case tp2: TermParamRef => tp1 match - case tp1: CaptureRef if tp1.isTrackableRef => subsumesExistentially(tp2, tp1) - case _ => false - case _ => false - /** bi-map taking existentials to the left of a comparison to matching * existentials on the right. This is not a bijection. However * we have `forwards(backwards(bv)) == bv` for an existentially bound `bv`. diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 82cc25f897ab..7032a4b5984b 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -523,9 +523,43 @@ object Types extends TypeUtils { 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. + * This is true for + * - all ThisTypes and all TermParamRef, + * - stable TermRefs with NoPrefix or ThisTypes as prefixes, + * - the root capability `caps.cap` + * - abstract or parameter TypeRefs that derive from caps.CapSet + * - annotated types that represent reach or maybe capabilities + */ + final def isTrackableRef(using Context): Boolean = this match + case _: (ThisType | TermParamRef) => + true + case tp: TermRef => + ((tp.prefix eq NoPrefix) + || tp.symbol.is(ParamAccessor) && tp.prefix.isThisTypeOf(tp.symbol.owner) + || tp.isRootCapability + ) && !tp.symbol.isOneOf(UnstableValueFlags) + case tp: TypeRef => + tp.symbol.isAbstractOrParamType && tp.derivesFrom(defn.Caps_CapSet) + case tp: TypeParamRef => + tp.derivesFrom(defn.Caps_CapSet) + case AnnotatedType(parent, annot) => + annot.symbol == defn.ReachCapabilityAnnot + || annot.symbol == defn.MaybeCapabilityAnnot + case _ => + false + + /** The capture set of a type. This is: + * - For trackable capture references: The singleton capture set consisting of + * just the reference, provided the underlying capture set of their info is not empty. + * - For other capture references: The capture set of their info + * - For all other types: The result of CaptureSet.ofType */ - def isTrackableRef(using Context): Boolean = false + final def captureSet(using Context): CaptureSet = this match + case tp: CaptureRef if tp.isTrackableRef => + val cs = tp.captureSetOfInfo + if cs.isAlwaysEmpty then cs else tp.singletonCaptureSet + case tp: SingletonCaptureRef => tp.captureSetOfInfo + case _ => CaptureSet.ofType(this, followResult = false) /** Does this type contain wildcard types? */ final def containsWildcardTypes(using Context) = @@ -1653,9 +1687,6 @@ object Types extends TypeUtils { case _ => if (isRepeatedParam) this.argTypesHi.head else this } - /** The capture set of this type. Overridden and cached in CaptureRef */ - def captureSet(using Context): CaptureSet = CaptureSet.ofType(this, followResult = false) - // ----- Normalizing typerefs over refined types ---------------------------- /** If this normalizes* to a refinement type that has a refinement for `name` (which might be followed @@ -2271,31 +2302,54 @@ object Types extends TypeUtils { isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) /** Is this a reach reference of the form `x*`? */ - def isReach(using Context): Boolean = false // overridden in AnnotatedType + final def isReach(using Context): Boolean = this match + case AnnotatedType(_, annot) => annot.symbol == defn.ReachCapabilityAnnot + case _ => false /** Is this a maybe reference of the form `x?`? */ - def isMaybe(using Context): Boolean = false // overridden in AnnotatedType + final def isMaybe(using Context): Boolean = this match + case AnnotatedType(_, annot) => annot.symbol == defn.MaybeCapabilityAnnot + case _ => false - def stripReach(using Context): CaptureRef = this // overridden in AnnotatedType - def stripMaybe(using Context): CaptureRef = this // overridden in AnnotatedType + final def stripReach(using Context): CaptureRef = + if isReach then + val AnnotatedType(parent: CaptureRef, _) = this: @unchecked + parent + else this + + final def stripMaybe(using Context): CaptureRef = + if isMaybe then + val AnnotatedType(parent: CaptureRef, _) = this: @unchecked + parent + else this /** Is this reference the generic root capability `cap` ? */ - def isRootCapability(using Context): Boolean = false + final def isRootCapability(using Context): Boolean = this match + case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot + case _ => false /** Is this reference capability that does not derive from another capability ? */ - def isMaxCapability(using Context): Boolean = false + final def isMaxCapability(using Context): Boolean = this match + case tp: TermRef => tp.isRootCapability || tp.info.derivesFrom(defn.Caps_Exists) + case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + case _ => false /** Normalize reference so that it can be compared with `eq` for equality */ - def normalizedRef(using Context): CaptureRef = this + final def normalizedRef(using Context): CaptureRef = this match + case tp @ AnnotatedType(parent: CaptureRef, annot) if isTrackableRef => + tp.derivedAnnotatedType(parent.normalizedRef, annot) + case tp: TermRef if isTrackableRef => + tp.symbol.termRef + case _ => this /** The capture set consisting of exactly this reference */ - def singletonCaptureSet(using Context): CaptureSet.Const = + final def singletonCaptureSet(using Context): CaptureSet.Const = if mySingletonCaptureSet == null then mySingletonCaptureSet = CaptureSet(this.normalizedRef) mySingletonCaptureSet.uncheckedNN /** The capture set of the type underlying this reference */ - def captureSetOfInfo(using Context): CaptureSet = + final def captureSetOfInfo(using Context): CaptureSet = if ctx.runId == myCaptureSetRunId then myCaptureSet.nn else if myCaptureSet.asInstanceOf[AnyRef] eq CaptureSet.Pending then CaptureSet.empty else @@ -2308,17 +2362,9 @@ object Types extends TypeUtils { myCaptureSetRunId = ctx.runId computed - def invalidateCaches() = + final def invalidateCaches() = myCaptureSetRunId = NoRunId - override def captureSet(using Context): CaptureSet = - val cs = captureSetOfInfo - if isTrackableRef then - if cs.isAlwaysEmpty then cs else singletonCaptureSet - else dealias match - case _: (TypeRef | TypeParamRef) => CaptureSet.empty - case _ => cs - end CaptureRef trait SingletonCaptureRef extends SingletonType, CaptureRef @@ -3011,26 +3057,6 @@ object Types extends TypeUtils { def implicitName(using Context): TermName = name def underlyingRef: TermRef = this - /** A term reference can be tracked if it is a local term ref to a value - * or a method term parameter. References to term parameters of classes - * cannot be tracked individually. - * They are subsumed in the capture sets of the enclosing class. - * TODO: ^^^ What about call-by-name? - */ - override def isTrackableRef(using Context) = - ((prefix eq NoPrefix) - || symbol.is(ParamAccessor) && prefix.isThisTypeOf(symbol.owner) - || isRootCapability - ) && !symbol.isOneOf(UnstableValueFlags) - - override def isRootCapability(using Context): Boolean = - name == nme.CAPTURE_ROOT && symbol == defn.captureRoot - - override def isMaxCapability(using Context): Boolean = - symbol == defn.captureRoot || info.derivesFrom(defn.Caps_Exists) - - override def normalizedRef(using Context): CaptureRef = - if isTrackableRef then symbol.termRef else this } abstract case class TypeRef(override val prefix: Type, @@ -3085,8 +3111,6 @@ object Types extends TypeUtils { def validated(using Context): this.type = this - override def isTrackableRef(using Context) = - symbol.isAbstractOrParamType && derivesFrom(defn.Caps_CapSet) } final class CachedTermRef(prefix: Type, designator: Designator, hc: Int) extends TermRef(prefix, designator) { @@ -3188,8 +3212,6 @@ object Types extends TypeUtils { // can happen in IDE if `cls` is stale } - override def isTrackableRef(using Context) = true - override def computeHash(bs: Binders): Int = doHash(bs, tref) override def eql(that: Type): Boolean = that match { @@ -4836,9 +4858,6 @@ object Types extends TypeUtils { type BT = TermLambda def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) - override def isTrackableRef(using Context) = true - override def isMaxCapability(using Context) = - underlying.derivesFrom(defn.Caps_Exists) } private final class TermParamRefImpl(binder: TermLambda, paramNum: Int) extends TermParamRef(binder, paramNum) @@ -4867,8 +4886,6 @@ object Types extends TypeUtils { case bound: OrType => occursIn(bound.tp1, fromBelow) || occursIn(bound.tp2, fromBelow) case _ => false } - - override def isTrackableRef(using Context) = derivesFrom(defn.Caps_CapSet) } private final class TypeParamRefImpl(binder: TypeLambda, paramNum: Int) extends TypeParamRef(binder, paramNum) @@ -5831,30 +5848,6 @@ object Types extends TypeUtils { isRefiningCache } - override def isTrackableRef(using Context) = - (isReach || isMaybe) && parent.isTrackableRef - - /** Is this a reach reference of the form `x*`? */ - override def isReach(using Context): Boolean = - annot.symbol == defn.ReachCapabilityAnnot - - /** Is this a reach reference of the form `x*`? */ - override def isMaybe(using Context): Boolean = - annot.symbol == defn.MaybeCapabilityAnnot - - override def stripReach(using Context): CaptureRef = - if isReach then parent.asInstanceOf[CaptureRef] else this - - override def stripMaybe(using Context): CaptureRef = - if isMaybe then parent.asInstanceOf[CaptureRef] else this - - override def normalizedRef(using Context): CaptureRef = - if isReach then AnnotatedType(stripReach.normalizedRef, annot) else this - - override def captureSet(using Context): CaptureSet = - if isReach then super.captureSet - else CaptureSet.ofType(this, followResult = false) - // equals comes from case class; no matching override is needed override def computeHash(bs: Binders): Int = From c971af4143c3588a660a3beefbe9407b6891c79f Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 21:34:09 +0200 Subject: [PATCH 35/35] Break out CaptureRef into a separate file Move extension methods on CaptureRef into CaptureRef itself or into CaptureOps --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 54 +++++++ .../src/dotty/tools/dotc/cc/CaptureRef.scala | 124 ++++++++++++++++ .../src/dotty/tools/dotc/cc/CaptureSet.scala | 27 ---- .../src/dotty/tools/dotc/core/TypeOps.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 136 +----------------- 5 files changed, 181 insertions(+), 162 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/CaptureRef.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 3b25f0d58035..8fb7d1492080 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -168,6 +168,60 @@ extension (tree: Tree) extension (tp: Type) + /** Is this type a CaptureRef that can be tracked? + * This is true for + * - all ThisTypes and all TermParamRef, + * - stable TermRefs with NoPrefix or ThisTypes as prefixes, + * - the root capability `caps.cap` + * - abstract or parameter TypeRefs that derive from caps.CapSet + * - annotated types that represent reach or maybe capabilities + */ + final def isTrackableRef(using Context): Boolean = tp match + case _: (ThisType | TermParamRef) => + true + case tp: TermRef => + ((tp.prefix eq NoPrefix) + || tp.symbol.is(ParamAccessor) && tp.prefix.isThisTypeOf(tp.symbol.owner) + || tp.isRootCapability + ) && !tp.symbol.isOneOf(UnstableValueFlags) + case tp: TypeRef => + tp.symbol.isAbstractOrParamType && tp.derivesFrom(defn.Caps_CapSet) + case tp: TypeParamRef => + tp.derivesFrom(defn.Caps_CapSet) + case AnnotatedType(parent, annot) => + annot.symbol == defn.ReachCapabilityAnnot + || annot.symbol == defn.MaybeCapabilityAnnot + case _ => + false + + /** The capture set of a type. This is: + * - For trackable capture references: The singleton capture set consisting of + * just the reference, provided the underlying capture set of their info is not empty. + * - For other capture references: The capture set of their info + * - For all other types: The result of CaptureSet.ofType + */ + final def captureSet(using Context): CaptureSet = tp match + case tp: CaptureRef if tp.isTrackableRef => + val cs = tp.captureSetOfInfo + if cs.isAlwaysEmpty then cs else tp.singletonCaptureSet + case tp: SingletonCaptureRef => tp.captureSetOfInfo + case _ => CaptureSet.ofType(tp, followResult = false) + + /** A type capturing `ref` */ + def capturing(ref: CaptureRef)(using Context): Type = + if tp.captureSet.accountsFor(ref) then tp + else CapturingType(tp, ref.singletonCaptureSet) + + /** A type capturing the capture set `cs`. If this type is already a capturing type + * the two capture sets are combined. + */ + def capturing(cs: CaptureSet)(using Context): Type = + if cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK + then tp + else tp match + case CapturingType(parent, cs1) => parent.capturing(cs1 ++ cs) + case _ => CapturingType(tp, cs) + /** @pre `tp` is a CapturingType */ def derivedCapturingType(parent: Type, refs: CaptureSet)(using Context): Type = tp match case tp @ CapturingType(p, r) => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala new file mode 100644 index 000000000000..6578da89bbf8 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureRef.scala @@ -0,0 +1,124 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Decorators.* +import util.{SimpleIdentitySet, Property} +import typer.ErrorReporting.Addenda +import TypeComparer.subsumesExistentially +import util.common.alwaysTrue +import scala.collection.mutable +import CCState.* +import Periods.NoRunId +import compiletime.uninitialized +import StdNames.nme + +/** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs, + * as well as two kinds of AnnotatedTypes representing reach and maybe capabilities. + */ +trait CaptureRef extends TypeProxy, ValueType: + private var myCaptureSet: CaptureSet | Null = uninitialized + private var myCaptureSetRunId: Int = NoRunId + private var mySingletonCaptureSet: CaptureSet.Const | Null = null + + /** 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 = + this.isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) + + /** Is this a reach reference of the form `x*`? */ + final def isReach(using Context): Boolean = this match + case AnnotatedType(_, annot) => annot.symbol == defn.ReachCapabilityAnnot + case _ => false + + /** Is this a maybe reference of the form `x?`? */ + final def isMaybe(using Context): Boolean = this match + case AnnotatedType(_, annot) => annot.symbol == defn.MaybeCapabilityAnnot + case _ => false + + final def stripReach(using Context): CaptureRef = + if isReach then + val AnnotatedType(parent: CaptureRef, _) = this: @unchecked + parent + else this + + final def stripMaybe(using Context): CaptureRef = + if isMaybe then + val AnnotatedType(parent: CaptureRef, _) = this: @unchecked + parent + else this + + /** Is this reference the generic root capability `cap` ? */ + final def isRootCapability(using Context): Boolean = this match + case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot + case _ => false + + /** Is this reference capability that does not derive from another capability ? */ + final def isMaxCapability(using Context): Boolean = this match + case tp: TermRef => tp.isRootCapability || tp.info.derivesFrom(defn.Caps_Exists) + case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) + case _ => false + + /** Normalize reference so that it can be compared with `eq` for equality */ + final def normalizedRef(using Context): CaptureRef = this match + case tp @ AnnotatedType(parent: CaptureRef, annot) if tp.isTrackableRef => + tp.derivedAnnotatedType(parent.normalizedRef, annot) + case tp: TermRef if tp.isTrackableRef => + tp.symbol.termRef + case _ => this + + /** The capture set consisting of exactly this reference */ + final def singletonCaptureSet(using Context): CaptureSet.Const = + if mySingletonCaptureSet == null then + mySingletonCaptureSet = CaptureSet(this.normalizedRef) + mySingletonCaptureSet.uncheckedNN + + /** The capture set of the type underlying this reference */ + final def captureSetOfInfo(using Context): CaptureSet = + if ctx.runId == myCaptureSetRunId then myCaptureSet.nn + else if myCaptureSet.asInstanceOf[AnyRef] eq CaptureSet.Pending then CaptureSet.empty + else + myCaptureSet = CaptureSet.Pending + val computed = CaptureSet.ofInfo(this) + if !isCaptureChecking || underlying.isProvisional then + myCaptureSet = null + else + myCaptureSet = computed + myCaptureSetRunId = ctx.runId + computed + + final def invalidateCaches() = + myCaptureSetRunId = NoRunId + + /** x subsumes x + * this subsumes this.f + * x subsumes y ==> x* subsumes y, x subsumes y? + * x subsumes y ==> x* subsumes y*, x? subsumes y? + * x: x1.type /\ x1 subsumes y ==> x subsumes y + */ + final def subsumes(y: CaptureRef)(using Context): Boolean = + (this eq y) + || this.isRootCapability + || y.match + case y: TermRef => + (y.prefix eq this) + || y.info.match + case y1: SingletonCaptureRef => this.subsumes(y1) + case _ => false + case MaybeCapability(y1) => this.stripMaybe.subsumes(y1) + case _ => false + || this.match + case ReachCapability(x1) => x1.subsumes(y.stripReach) + case x: TermRef => + x.info match + case x1: SingletonCaptureRef => x1.subsumes(y) + case _ => false + case x: TermParamRef => subsumesExistentially(x, y) + case _ => false + +end CaptureRef + +trait SingletonCaptureRef extends SingletonType, CaptureRef + diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index a2233f862e53..aa65db2375e8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -152,33 +152,6 @@ sealed abstract class CaptureSet extends Showable: cs.addDependent(this)(using ctx, UnrecordedState) this - /** x subsumes x - * this subsumes this.f - * x subsumes y ==> x* subsumes y, x subsumes y? - * x subsumes y ==> x* subsumes y*, x? subsumes y? - * x: x1.type /\ x1 subsumes y ==> x subsumes y - */ - extension (x: CaptureRef) - private def subsumes(y: CaptureRef)(using Context): Boolean = - (x eq y) - || x.isRootCapability - || y.match - case y: TermRef => - (y.prefix eq x) - || y.info.match - case y1: SingletonCaptureRef => x.subsumes(y1) - case _ => false - case MaybeCapability(y1) => x.stripMaybe.subsumes(y1) - case _ => false - || x.match - case ReachCapability(x1) => x1.subsumes(y.stripReach) - case x: TermRef => - x.info match - case x1: SingletonCaptureRef => x1.subsumes(y) - case _ => false - case x: TermParamRef => subsumesExistentially(x, y) - case _ => false - /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 3bc7a7223abb..8089735bdb0f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -18,7 +18,7 @@ import typer.ForceDegree import typer.Inferencing.* import typer.IfBottom import reporting.TestingReporter -import cc.{CapturingType, derivedCapturingType, CaptureSet, isBoxed, isBoxedCapturing} +import cc.{CapturingType, derivedCapturingType, CaptureSet, captureSet, isBoxed, isBoxedCapturing} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7032a4b5984b..0c348ad458e4 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -38,7 +38,8 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized -import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, isCaptureChecking, isRetains, isRetainsLike} +import cc.{CapturingType, CaptureRef, CaptureSet, SingletonCaptureRef, isTrackableRef, + derivedCapturingType, isBoxedCapturing, isCaptureChecking, isRetains, isRetainsLike} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -522,45 +523,6 @@ object Types extends TypeUtils { */ def isDeclaredVarianceLambda: Boolean = false - /** Is this type a CaptureRef that can be tracked? - * This is true for - * - all ThisTypes and all TermParamRef, - * - stable TermRefs with NoPrefix or ThisTypes as prefixes, - * - the root capability `caps.cap` - * - abstract or parameter TypeRefs that derive from caps.CapSet - * - annotated types that represent reach or maybe capabilities - */ - final def isTrackableRef(using Context): Boolean = this match - case _: (ThisType | TermParamRef) => - true - case tp: TermRef => - ((tp.prefix eq NoPrefix) - || tp.symbol.is(ParamAccessor) && tp.prefix.isThisTypeOf(tp.symbol.owner) - || tp.isRootCapability - ) && !tp.symbol.isOneOf(UnstableValueFlags) - case tp: TypeRef => - tp.symbol.isAbstractOrParamType && tp.derivesFrom(defn.Caps_CapSet) - case tp: TypeParamRef => - tp.derivesFrom(defn.Caps_CapSet) - case AnnotatedType(parent, annot) => - annot.symbol == defn.ReachCapabilityAnnot - || annot.symbol == defn.MaybeCapabilityAnnot - case _ => - false - - /** The capture set of a type. This is: - * - For trackable capture references: The singleton capture set consisting of - * just the reference, provided the underlying capture set of their info is not empty. - * - For other capture references: The capture set of their info - * - For all other types: The result of CaptureSet.ofType - */ - final def captureSet(using Context): CaptureSet = this match - case tp: CaptureRef if tp.isTrackableRef => - val cs = tp.captureSetOfInfo - if cs.isAlwaysEmpty then cs else tp.singletonCaptureSet - case tp: SingletonCaptureRef => tp.captureSetOfInfo - case _ => CaptureSet.ofType(this, followResult = false) - /** Does this type contain wildcard types? */ final def containsWildcardTypes(using Context) = existsPart(_.isInstanceOf[WildcardType], StopAt.Static, forceLazy = false) @@ -2077,20 +2039,6 @@ object Types extends TypeUtils { case _ => this - /** A type capturing `ref` */ - def capturing(ref: CaptureRef)(using Context): Type = - if captureSet.accountsFor(ref) then this - else CapturingType(this, ref.singletonCaptureSet) - - /** A type capturing the capture set `cs`. If this type is already a capturing type - * the two capture sets are combined. - */ - def capturing(cs: CaptureSet)(using Context): Type = - if cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(captureSet, frozen = true).isOK then this - else this match - case CapturingType(parent, cs1) => parent.capturing(cs1 ++ cs) - case _ => CapturingType(this, cs) - /** The set of distinct symbols referred to by this type, after all aliases are expanded */ def coveringSet(using Context): Set[Symbol] = (new CoveringSetAccumulator).apply(Set.empty[Symbol], this) @@ -2289,86 +2237,6 @@ object Types extends TypeUtils { def isOverloaded(using Context): Boolean = false } - /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs */ - trait CaptureRef extends TypeProxy, ValueType: - private var myCaptureSet: CaptureSet | Null = uninitialized - private var myCaptureSetRunId: Int = NoRunId - private var mySingletonCaptureSet: CaptureSet.Const | Null = null - - /** 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 = - isTrackableRef && (isMaxCapability || !captureSetOfInfo.isAlwaysEmpty) - - /** Is this a reach reference of the form `x*`? */ - final def isReach(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.ReachCapabilityAnnot - case _ => false - - /** Is this a maybe reference of the form `x?`? */ - final def isMaybe(using Context): Boolean = this match - case AnnotatedType(_, annot) => annot.symbol == defn.MaybeCapabilityAnnot - case _ => false - - final def stripReach(using Context): CaptureRef = - if isReach then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this - - final def stripMaybe(using Context): CaptureRef = - if isMaybe then - val AnnotatedType(parent: CaptureRef, _) = this: @unchecked - parent - else this - - /** Is this reference the generic root capability `cap` ? */ - final def isRootCapability(using Context): Boolean = this match - case tp: TermRef => tp.name == nme.CAPTURE_ROOT && tp.symbol == defn.captureRoot - case _ => false - - /** Is this reference capability that does not derive from another capability ? */ - final def isMaxCapability(using Context): Boolean = this match - case tp: TermRef => tp.isRootCapability || tp.info.derivesFrom(defn.Caps_Exists) - case tp: TermParamRef => tp.underlying.derivesFrom(defn.Caps_Exists) - case _ => false - - /** Normalize reference so that it can be compared with `eq` for equality */ - final def normalizedRef(using Context): CaptureRef = this match - case tp @ AnnotatedType(parent: CaptureRef, annot) if isTrackableRef => - tp.derivedAnnotatedType(parent.normalizedRef, annot) - case tp: TermRef if isTrackableRef => - tp.symbol.termRef - case _ => this - - /** The capture set consisting of exactly this reference */ - final def singletonCaptureSet(using Context): CaptureSet.Const = - if mySingletonCaptureSet == null then - mySingletonCaptureSet = CaptureSet(this.normalizedRef) - mySingletonCaptureSet.uncheckedNN - - /** The capture set of the type underlying this reference */ - final def captureSetOfInfo(using Context): CaptureSet = - if ctx.runId == myCaptureSetRunId then myCaptureSet.nn - else if myCaptureSet.asInstanceOf[AnyRef] eq CaptureSet.Pending then CaptureSet.empty - else - myCaptureSet = CaptureSet.Pending - val computed = CaptureSet.ofInfo(this) - if !isCaptureChecking || underlying.isProvisional then - myCaptureSet = null - else - myCaptureSet = computed - myCaptureSetRunId = ctx.runId - computed - - final def invalidateCaches() = - myCaptureSetRunId = NoRunId - - end CaptureRef - - trait SingletonCaptureRef extends SingletonType, CaptureRef - /** A trait for types that bind other types that refer to them. * Instances are: LambdaType, RecType. */