From 0e71645b410c8e39fc4b88900e5de4b11fbd139e Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Jun 2024 13:40:33 +0200 Subject: [PATCH 01/61] 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 5b9d305fc55adcda119d22bdd2189174f507bdcf Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Jun 2024 13:42:20 +0200 Subject: [PATCH 02/61] 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 dca8bf206bac..c5b3611463b0 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -47,6 +47,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() @@ -65,6 +67,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 @@ -326,14 +340,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 @@ -345,7 +358,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 => @@ -547,6 +562,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 _ => @@ -628,6 +645,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 @@ -1420,20 +1439,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 @@ -1470,12 +1490,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 _ => @@ -2769,6 +2789,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) @@ -3236,6 +3290,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 b375d97b9bb23b2575c4f7c6134c271d347f632f Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Jun 2024 19:47:28 +0200 Subject: [PATCH 03/61] 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 f9ddc717b20f268bcff650454dfeef76f7477a2a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Jun 2024 11:59:27 +0200 Subject: [PATCH 04/61] 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 045801247565b0d6f8c13d893959a1b478a862ca Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Jun 2024 13:00:38 +0200 Subject: [PATCH 05/61] 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 24506ae51bdedc0376485fc23167dffcc2cca2df Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 7 Jun 2024 13:27:17 +0200 Subject: [PATCH 06/61] 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 1008a0dce8be020d5ed0687744d31b86cc46bc02 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 10 Jun 2024 17:58:09 +0200 Subject: [PATCH 07/61] 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 a30cf473751723a44e4119158ad6b67cf68d18ad Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 10 Jun 2024 18:14:36 +0200 Subject: [PATCH 08/61] 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 c5b3611463b0..174bbaeca21d 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3753,6 +3753,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 @@ -3764,12 +3769,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 20b8630a03823356895b6259518e4b131e42091e Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 10 Jun 2024 18:33:35 +0200 Subject: [PATCH 09/61] 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 ca715e8a94c8c530ebaf6c2064993040d3dc5252 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 11:21:39 +0200 Subject: [PATCH 10/61] 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 b9c21097aa0ab3f55e0aea5255831f87d86b29d7 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 14:36:54 +0200 Subject: [PATCH 11/61] 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 420f2cda4b7edd1a473950b13d0b5cd212835516 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 15:23:23 +0200 Subject: [PATCH 12/61] 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 e9474ff2ccea087dc334ddf4688dc5baf2af2cfe Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 15:54:50 +0200 Subject: [PATCH 13/61] 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 0161bb06916954ebbd001c6417f97507f3899836 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 13 Jun 2024 17:43:26 +0200 Subject: [PATCH 14/61] 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 174bbaeca21d..abbb4387a125 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2824,7 +2824,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 7296b40597238fda2c059185a2ee8e6101e7df4d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 14 Jun 2024 20:50:30 +0200 Subject: [PATCH 15/61] 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 c518de7dbbfe..cbf79577c2a3 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 4add745c1c8222351554c04e2d1ab26c86d8c964 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Jun 2024 11:45:47 +0200 Subject: [PATCH 16/61] 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 9aca8a9b4b60..5e12e4d6b84a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -6271,6 +6271,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 d5c15dd15b693d11936c29fdba586f0647f02376 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 15 Jun 2024 13:11:02 +0200 Subject: [PATCH 17/61] 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 d242e0117235592d3a0434efe4f6629001b1ac97 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 09:38:30 +0200 Subject: [PATCH 18/61] 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 abbb4387a125..e532324e95a7 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.* @@ -48,7 +48,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() @@ -77,7 +77,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 @@ -359,7 +359,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)) @@ -2789,6 +2788,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 @@ -2800,16 +2806,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 @@ -2819,16 +2841,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. @@ -3293,9 +3369,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(_,_)) @@ -3368,6 +3441,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: @@ -3831,5 +3907,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 687516e74455b5f4d951c8b4d35f24d9a1a48c13 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 18:08:45 +0200 Subject: [PATCH 19/61] 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 9b27b69d78ec19348429217b954179bafa113f81 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 18:30:15 +0200 Subject: [PATCH 20/61] 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 1d3ae88ee62e3e1567862d4ef7ef953fd049ac2a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 18:40:46 +0200 Subject: [PATCH 21/61] 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 4ad9e3cfc8f628d72bddf84c5796dde0a30a7570 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 19:45:22 +0200 Subject: [PATCH 22/61] 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 b950c7da50f8012d10769907a043eb5b2295cce5 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 17 Jun 2024 20:29:21 +0200 Subject: [PATCH 23/61] 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 adfb70020c02dcb8a1775e0314a87cee40b2f476 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jun 2024 09:33:23 +0200 Subject: [PATCH 24/61] 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 7bf8be72c54699a30804687514549ed25aa31bff Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 18 Jun 2024 18:21:50 +0200 Subject: [PATCH 25/61] 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 cd78a9e84354c0ce5dcd485c458c881e8d38bd64 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 21 Jun 2024 19:36:52 +0200 Subject: [PATCH 26/61] 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 28f4bb53c720ec9a3e69fac717f47c0cd66ff66c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 23 Jun 2024 11:33:27 +0200 Subject: [PATCH 27/61] 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 04aa63def9e3d3c9a132482ebee34a318e0700f0 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 26 Jun 2024 23:21:06 +0200 Subject: [PATCH 28/61] 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 e532324e95a7..4b8e251ea337 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2841,7 +2841,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 5e12e4d6b84a..7d773d1ac4ec 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2312,7 +2312,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 @@ -3031,7 +3035,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 @@ -3080,6 +3084,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) { @@ -4836,7 +4843,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) @@ -4856,6 +4864,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 ae8e16ac9ea4..5d36bd230b7e 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 cbf79577c2a3..5113a6380a78 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 9436f11f2c9c2df9341ea0bdfb7d9e861e4196e2 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Jun 2024 13:40:20 +0200 Subject: [PATCH 29/61] 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 7d773d1ac4ec..fabe7c782280 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3026,8 +3026,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 @@ -4834,8 +4833,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 7ac94c950b4f00ad4dbd0174de0b0d6bb21532ba Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Jun 2024 13:59:57 +0200 Subject: [PATCH 30/61] 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 aad33957457abef40ba75cc2f1fc181fdf34103f Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Jun 2024 18:43:40 +0200 Subject: [PATCH 31/61] 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 1c110713733fc9119a1d34a00e8106e984cbc9c3 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 18:01:51 +0200 Subject: [PATCH 32/61] 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 72462a723271a9d4358c7165d9f7980f5a46f043 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 18:49:07 +0200 Subject: [PATCH 33/61] 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 0a1854d724ce1677e49c7cac2688d47e7c60f99b Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 29 Jun 2024 20:05:13 +0200 Subject: [PATCH 34/61] Update doc page --- docs/_docs/reference/experimental/cc.md | 141 ++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 9 deletions(-) diff --git a/docs/_docs/reference/experimental/cc.md b/docs/_docs/reference/experimental/cc.md index 5bdf91f628ec..aea14abc7e22 100644 --- a/docs/_docs/reference/experimental/cc.md +++ b/docs/_docs/reference/experimental/cc.md @@ -216,13 +216,13 @@ This widening is called _avoidance_; it is not specific to capture checking but ## Capability Classes -Classes like `CanThrow` or `FileSystem` have the property that their values are always intended to be capabilities. We can make this intention explicit and save boilerplate by declaring these classes with a `@capability` annotation. +Classes like `CanThrow` or `FileSystem` have the property that their values are always intended to be capabilities. We can make this intention explicit and save boilerplate by letting these classes extend the `Capability` class defined in object `cap`. -The capture set of a capability class type is always `{cap}`. This means we could equivalently express the `FileSystem` and `Logger` classes as follows: +The capture set of a `Capability` subclass type is always `{cap}`. This means we could equivalently express the `FileSystem` and `Logger` classes as follows: ```scala -import annotation.capability +import caps.Capability -@capability class FileSystem +class FileSystem extends Capability class Logger(using FileSystem): def log(s: String): Unit = ??? @@ -290,7 +290,7 @@ The captured references of a class include _local capabilities_ and _argument ca the local capabilities of a superclass are also local capabilities of its subclasses. Example: ```scala -@capability class Cap +class Cap extends caps.Capability def test(a: Cap, b: Cap, c: Cap) = class Super(y: Cap): @@ -317,7 +317,7 @@ The inference observes the following constraints: For instance, in ```scala -@capability class Cap +class Cap extends caps.Capability def test(c: Cap) = class A: val x: A = this @@ -502,7 +502,7 @@ Under the language import `language.experimental.captureChecking`, the code is i ``` To integrate exception and capture checking, only two changes are needed: - - `CanThrow` is declared as a `@capability` class, so all references to `CanThrow` instances are tracked. + - `CanThrow` is declared as a class extending `Capability`, so all references to `CanThrow` instances are tracked. - Escape checking is extended to `try` expressions. The result type of a `try` is not allowed to capture the universal capability. @@ -635,9 +635,132 @@ To summarize, there are two "sweet spots" of data structure design: strict lists side-effecting or resource-aware code and lazy lists in purely functional code. Both are already correctly capture-typed without requiring any explicit annotations. Capture annotations only come into play where the semantics gets more complicated because we deal with delayed effects such as in impure lazy lists or side-effecting iterators over strict lists. This property is probably one of the greatest plus points of our approach to capture checking compared to previous techniques which tend to be more noisy. -## Function Type Shorthands +## Existential Capabilities -TBD +In fact, what is written as the top type `cap` can mean different capabilities, depending on scope. For instance, consider the function type +`() -> Iterator[T]^`. This is taken to mean +```scala + () -> Exists x. Iterator[T]^x +``` +In other words, it means an unknown type bound `x` by an "existential" in the scope of the function result. A `cap` in a function result is therefore different from a `cap` at the top-level or in a function parameter. + +Internally, an existential type is represented as a kind of dependent function type. The type above would be modelled as +```scala + () -> (x: Exists) -> Iterator[T]^x +``` +Here, `Exists` is a sealed trait in the `caps` object that serves to mark +dependent functions as representations of existentials. It should be noted +that this is strictly an internal representation. It is explained here because it can show up in error messages. It is generally not recommended to use this syntax in source code. Instead one should rely on the automatic expansion of `^` and `cap` to existentials, which can be +influenced by introducing the right alias types. The rules for this expansion are as follows: + + - If a function result type contains covariant occurrences of `cap`, + we replace these occurrences with a fresh existential variable which + is bound by a quantifier scoping over the result type. + - We might want to do the same expansion in function arguments, but right now this is not done. + - Occurrences of `cap` elsewhere are not translated. They can be seen as representing an existential at the top-level scope. + +**Examples:** + + - `A => B` is an alias type that expands to `(A -> B)^`, therefore + `() -> A => B` expands to `() -> Exists c. A ->{c} B`. + + - `() -> Iterator[A => B]` expands to `() -> Exists c. Iterator[A ->{c} B]` + + - `A -> B^` expands to `A -> Exists c.B^{c}`. + + - If we define `type Fun[T] = A -> T`, then `() -> Fun[B^]` expands to `() -> Exists c.Fun[B^{c}]`, which dealiases to `() -> Exists c.A -> B^{c}`. This demonstrates how aliases can be used to force existential binders to be in some specific outer scope. + + - If we define + ```scala + type F = A -> Fun[B^] + ``` + then the type alias expands to + ```scala + type F = A -> Exists c.A -> B^{c} + ``` + +**Typing Rules:** + + - When we typecheck the body of a function or method, any covariant occurrences of `cap` in the result type are bound with a fresh existential. + - Conversely, when we typecheck the application of a function or method, + with an existential result type `Exists ex.T`, the result of the application is `T` where every occurrence of the existentially bound + variable `ex` is replaced by `cap`. + +## Reach Capabilities + +Say you have a method `f` that takes an impure function argument which gets stored in a `var`: +```scala +def f(op: A => B) + var x: A ->{op} B = op + ... +``` +This is legal even though `var`s cannot have types with `cap` or existential capabilities. The trick is that the type of the variable `x` +is not `A => B` (this would be rejected), but is the "narrowed" type +`A ->{op} B`. In other words, all capabilities retained by values of `x` +are all also referred to by `op`, which justifies the replacement of `cap` by `op`. + +A more complicated situation is if we want to store successive values +held in a list. Example: +```scala +def f(ops: List[A => B]) + var xs = ops + var x: ??? = xs.head + while xs.nonEmpty do + xs = xs.tail + x = xs.head + ... +``` +Here, `x` cannot be given a type with an `ops` capability. In fact, `ops` is pure, i.e. it's capture set is empty, so it cannot be used as the name of a capability. What we would like to express is that `x` refers to +any operation "reachable" through `ops`. This can be expressed using a +_reach capability_ `ops*`. +```scala +def f(ops: List[A => B]) + var xs = ops + var x: A ->{ops*} B = xs.head + ... +``` +Reach capabilities take the form `x*` where `x` is syntactically a regular capability. If `x: T` then `x*` stands for any capability that appears covariantly in `T` and that is accessed through `x`. The least supertype of this capability is the set of all capabilities appearing covariantly in `T`. + +## Capability Polymorphism + +It is sometimes convenient to write operations that are parameterized with a capture set of capabilities. For instance consider a type of event sources +`Source` on which `Listener`s can be registered. Listeners can hold certain capabilities, which show up as a parameter to `Source`: +```scala + 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 +``` +The type variable `X^` can be instantiated with a set of capabilities. It can occur in capture sets in its scope. For instance, in the example above +we see a variable `listeners` that has as type a `Set` of `Listeners` capturing `X^`. The `register` method takes a listener of this type +and assigns it to the variable. + +Capture set variables `X^` are represented as regular type variables with a +special upper bound `CapSet`. For instance, `Source` could be equivalently +defined as follows: +```scala + class Source[X <: CapSet^]: + ... +``` +`CapSet` is a sealed trait in the `caps` object. It cannot be instantiated or inherited, so its only purpose is to identify capture set type variables and types. Capture set variables can be inferred like regular type variables. When they should be instantiated explicitly one uses a capturing +type `CapSet`. For instance: +```scala + class Async extends caps.Capability + + def listener(async: Async): Listener^{async} = ??? + + def test1(async1: Async, others: List[Async]) = + val src = Source[CapSet^{async1, others*}] + ... +``` +Here, `src` is created as a `Source` on which listeners can be registered that refer to the `async` capability or to any of the capabilities in list `others`. So we can continue the example code above as follows: +```scala + src.register(listener(async1)) + others.map(listener).foreach(src.register) + val ls: Set[Listener^{async, others*}] = src.allListeners +``` ## Compilation Options From c965322ebd576a0869dafba93baf00a6a895565d Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 6 Jul 2024 19:12:38 +0200 Subject: [PATCH 35/61] Fix glb logic involving capturing types --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 8 +++++++ .../dotty/tools/dotc/core/TypeComparer.scala | 13 +++++----- tests/pos/gears-probem-1.scala | 24 +++++++++++++++++++ tests/pos/gears-probem.scala | 18 ++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 tests/pos/gears-probem-1.scala create mode 100644 tests/pos/gears-probem.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 3b25f0d58035..8edc861ace45 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -227,6 +227,14 @@ extension (tp: Type) case tp: OrType => tp.tp1.isBoxedCapturing || tp.tp2.isBoxedCapturing case _ => false + /** Is the box status of `tp` and `tp2` compatible? I.ee they are + * box boxed, or both unboxed, or one of them has an empty capture set. + */ + def isBoxCompatibleWith(tp2: Type)(using Context): Boolean = + isBoxedCapturing == tp2.isBoxedCapturing + || tp.captureSet.isAlwaysEmpty + || tp2.captureSet.isAlwaysEmpty + /** If this type is a capturing type, the version with boxed statues as given by `boxed`. * If it is a TermRef of a capturing type, and the box status flips, widen to a capturing * type that captures the TermRef. diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 4b8e251ea337..d6868e569a05 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2403,7 +2403,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling else def mergedGlb(tp1: Type, tp2: Type): Type = val tp1a = dropIfSuper(tp1, tp2) - if tp1a ne tp1 then glb(tp1a, tp2) + if tp1a ne tp1 then + glb(tp1a, tp2) else val tp2a = dropIfSuper(tp2, tp1) if tp2a ne tp2 then glb(tp1, tp2a) @@ -2721,11 +2722,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: TypeVar if tp1.isInstantiated => tp1.underlying & tp2 case CapturingType(parent1, refs1) => - val refs2 = tp2.captureSet - if subCaptures(refs2, refs1, frozen = true).isOK - && tp1.isBoxedCapturing == tp2.isBoxedCapturing - then (parent1 & tp2).capturing(refs2) - else tp1.derivedCapturingType(parent1 & tp2, refs1) + val jointRefs = refs1 ** tp2.captureSet + if jointRefs.isAlwaysEmpty then parent1 & tp2 + else if tp1.isBoxCompatibleWith(tp2) then + tp1.derivedCapturingType(parent1 & tp2, jointRefs) + else NoType case tp1: AnnotatedType if !tp1.isRefining => tp1.underlying & tp2 case _ => diff --git a/tests/pos/gears-probem-1.scala b/tests/pos/gears-probem-1.scala new file mode 100644 index 000000000000..f5c7fdfd0a3c --- /dev/null +++ b/tests/pos/gears-probem-1.scala @@ -0,0 +1,24 @@ +import language.experimental.captureChecking + +trait Future[+T]: + def await: T + +trait Channel[+T]: + def read(): Ok[T] + +class Collector[T](val futures: Seq[Future[T]^]): + val results: Channel[Future[T]^{futures*}] = ??? +end Collector + +class Result[+T, +E]: + def get: T = ??? + +case class Err[+E](e: E) extends Result[Nothing, E] +case class Ok[+T](x: T) extends Result[T, Nothing] + +extension [T](fs: Seq[Future[T]^]) + def awaitAll = + val collector//: Collector[T]{val futures: Seq[Future[T]^{fs*}]} + = Collector(fs) + // val ch = collector.results // also errors + val fut: Future[T]^{fs*} = collector.results.read().get // found ...^{caps.cap} \ No newline at end of file diff --git a/tests/pos/gears-probem.scala b/tests/pos/gears-probem.scala new file mode 100644 index 000000000000..2e445c985de2 --- /dev/null +++ b/tests/pos/gears-probem.scala @@ -0,0 +1,18 @@ +import language.experimental.captureChecking + +trait Future[+T]: + def await: T + +trait Channel[T]: + def read(): Either[Nothing, T] + +class Collector[T](val futures: Seq[Future[T]^]): + val results: Channel[Future[T]^{futures*}] = ??? +end Collector + +extension [T](fs: Seq[Future[T]^]) + def awaitAll = + val collector: Collector[T]{val futures: Seq[Future[T]^{fs*}]} + = Collector(fs) + // val ch = collector.results // also errors + val fut: Future[T]^{fs*} = collector.results.read().right.get // found ...^{caps.cap} \ No newline at end of file From 2cd685a26c14ecc12be8dfa61287f883ce406dcf Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jul 2024 10:31:29 +0200 Subject: [PATCH 36/61] Add option to avoid intersections for class refinements Add an option to avoid the type intersection when we do a select of a parameter accessor that is mentioned in a class refinement type. It seems to give us a little bit if performance, but nothing significant. So the option is off by default. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 6 ++++ .../src/dotty/tools/dotc/core/Types.scala | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 8edc861ace45..053795429bc9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -27,6 +27,12 @@ object ccConfig: */ inline val allowUnsoundMaps = false + /** If true, when computing the memberinfo of a refined type created + * by addCaptureRefinements take the refineInfo directly without intersecting + * with the parent info. + */ + inline val optimizedRefinements = false + /** If true, use existential capture set variables */ def useExistentials(using Context) = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index fabe7c782280..71de9ef0e0f9 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -43,6 +43,7 @@ import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe +import dotty.tools.dotc.cc.ccConfig object Types extends TypeUtils { @@ -865,19 +866,23 @@ object Types extends TypeUtils { } else val isRefinedMethod = rinfo.isInstanceOf[MethodOrPoly] - val joint = pdenot.meet( - new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), - pre, - safeIntersection = ctx.base.pendingMemberSearches.contains(name)) - joint match - case joint: SingleDenotation - if isRefinedMethod - && (rinfo <:< joint.info - || name == nme.apply && defn.isFunctionType(tp.parent)) => - // use `rinfo` to keep the right parameter names for named args. See i8516.scala. - joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) + rinfo match + case CapturingType(_, refs: CaptureSet.RefiningVar) if ccConfig.optimizedRefinements => + pdenot.asSingleDenotation.derivedSingleDenotation(pdenot.symbol, rinfo) case _ => - joint + val joint = pdenot.meet( + new JointRefDenotation(NoSymbol, rinfo, Period.allInRun(ctx.runId), pre, isRefinedMethod), + pre, + safeIntersection = ctx.base.pendingMemberSearches.contains(name)) + joint match + case joint: SingleDenotation + if isRefinedMethod + && (rinfo <:< joint.info + || name == nme.apply && defn.isFunctionType(tp.parent)) => + // use `rinfo` to keep the right parameter names for named args. See i8516.scala. + joint.derivedSingleDenotation(joint.symbol, rinfo, pre, isRefinedMethod) + case _ => + joint } def goApplied(tp: AppliedType, tycon: HKTypeLambda) = From 8b03405b4bd5ef1cf25d8dae6f8c3a293202a0b3 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Jul 2024 10:31:47 +0200 Subject: [PATCH 37/61] Fix printing of reach capabilities --- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 6 +++--- compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala | 2 ++ tests/neg-custom-args/captures/reaches2.check | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index aca5972d4516..b5ed3bdb4fa7 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -416,10 +416,10 @@ class PlainPrinter(_ctx: Context) extends Printer { homogenize(tp) match case tp: TermRef if tp.symbol == defn.captureRoot => Str("cap") case tp: SingletonType => toTextRef(tp) - case ReachCapability(tp1) => toTextRef(tp1) ~ "*" - case MaybeCapability(tp1) => toTextRef(tp1) ~ "?" case tp: (TypeRef | TypeParamRef) => toText(tp) ~ "^" - case _ => toText(tp) + case ReachCapability(tp1) => toTextCaptureRef(tp1) ~ "*" + case MaybeCapability(tp1) => toTextCaptureRef(tp1) ~ "?" + case tp => toText(tp) protected def isOmittablePrefix(sym: Symbol): Boolean = defn.unqualifiedOwnerTypes.exists(_.symbol == sym) || isEmptyPrefix(sym) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 9852dfc1170d..18a1647572ef 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -330,6 +330,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) case tp @ PolyProto(targs, resType) => "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) + case tp: AnnotatedType if tp.isReach || tp.isMaybe => + toTextCaptureRef(tp) case _ => super.toText(tp) } diff --git a/tests/neg-custom-args/captures/reaches2.check b/tests/neg-custom-args/captures/reaches2.check index f646a9736395..03860ee4a01b 100644 --- a/tests/neg-custom-args/captures/reaches2.check +++ b/tests/neg-custom-args/captures/reaches2.check @@ -1,10 +1,10 @@ -- Error: tests/neg-custom-args/captures/reaches2.scala:8:10 ----------------------------------------------------------- 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 {} + |reference ps* 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^?) ->? (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 {} + |reference ps* 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^?) ->? (ex$15: caps.Exists) -> A^? From 520100beee5a10728bde7d9919cfc1eca71c27c3 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Jul 2024 11:06:18 +0200 Subject: [PATCH 38/61] Introduced @unboxed parameters --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 1 + library/src/scala/caps.scala | 2 ++ 2 files changed, 3 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e8a853469f6f..b7aa74ea2a92 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1050,6 +1050,7 @@ class Definitions { @tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental") @tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws") @tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient") + @tu lazy val UnboxedAnnot: ClassSymbol = requiredClass("scala.caps.unboxed") @tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked") @tu lazy val UncheckedStableAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedStable") @tu lazy val UncheckedVarianceAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedVariance") diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 967246041082..5e675f7d4341 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -39,6 +39,8 @@ import annotation.{experimental, compileTimeOnly} */ final class untrackedCaptures extends annotation.StaticAnnotation + final class unboxed extends annotation.StaticAnnotation + object unsafe: extension [T](x: T) From 77082767faf1f56ce5db16a11df48bfff29459f8 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Jul 2024 15:10:13 +0200 Subject: [PATCH 39/61] Fix accidentally enabled warning about open classes --- compiler/src/dotty/tools/dotc/typer/Namer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Namer.scala b/compiler/src/dotty/tools/dotc/typer/Namer.scala index 32467de77264..83964417a6f1 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(`3.6`) then + else if sourceVersion.isAtLeast(future) then checkFeature(nme.adhocExtensions, i"Unless $pclazz is declared 'open', its extension in a separate file", cls.topLevelClass, From 2b04423d4ee2d561411dde1ea8708ac64f690997 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Jul 2024 15:25:18 +0200 Subject: [PATCH 40/61] Disable special treatment of eta expansions in recheckClosure --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 7 +++++++ compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- .../neg-custom-args/captures/effect-swaps-explicit.check | 4 ++-- tests/neg-custom-args/captures/levels.check | 7 ++----- tests/neg-custom-args/captures/vars-simple.check | 8 +++----- tests/neg-custom-args/captures/vars.check | 7 ++----- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 053795429bc9..df185c589d9e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -33,6 +33,13 @@ object ccConfig: */ inline val optimizedRefinements = false + /** If enabled, use a special path in recheckClosure for closures + * that are eta expansions. This can improve some error messages but + * currently leads to unsoundess for handlng reach capabilities. + * TODO: The unsoundness needs followin up. + */ + inline val handleEtaExpansionsSpecially = false + /** If true, use existential capture set variables */ def useExistentials(using Context) = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 8eb2f2420369..ce969047524d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -628,7 +628,7 @@ class CheckCaptures extends Recheck, SymTransformer: // Constrain closure's parameters and result from the expected type before // rechecking the body. val res = recheckClosure(expr, pt, forceDependent = true) - if !isEtaExpansion(mdef) then + if !(isEtaExpansion(mdef) && ccConfig.handleEtaExpansionsSpecially) then // If closure is an eta expanded method reference it's better to not constrain // its internals early since that would give error messages in generated code // which are less intelligible. diff --git a/tests/neg-custom-args/captures/effect-swaps-explicit.check b/tests/neg-custom-args/captures/effect-swaps-explicit.check index 47559ab97568..264dfa663d39 100644 --- a/tests/neg-custom-args/captures/effect-swaps-explicit.check +++ b/tests/neg-custom-args/captures/effect-swaps-explicit.check @@ -25,5 +25,5 @@ -- 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 + |local reference contextual$9 from (using contextual$9: boundary.Label[Result[box Future[box T^?]^{fr, contextual$9, contextual$9}, box E^?]]^): + | box Future[box T^?]^{fr, contextual$9, 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/levels.check b/tests/neg-custom-args/captures/levels.check index 2dae3ec3bbc6..ddfa7c051211 100644 --- a/tests/neg-custom-args/captures/levels.check +++ b/tests/neg-custom-args/captures/levels.check @@ -5,13 +5,10 @@ | 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:24:11 --------------------------------------- +-- 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 + | reference (cap3 : CC^) is not included in the allowed capture set ? of value r | | Note that reference (cap3 : CC^), defined in method scope | 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/vars-simple.check b/tests/neg-custom-args/captures/vars-simple.check index 2ef301b6ec1f..e9671f775c22 100644 --- a/tests/neg-custom-args/captures/vars-simple.check +++ b/tests/neg-custom-args/captures/vars-simple.check @@ -8,13 +8,11 @@ | 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 ----------------------------------- +-- Error: tests/neg-custom-args/captures/vars-simple.scala:16:8 -------------------------------------------------------- 16 | a = g // error | ^ - | Found: box (x: String) ->{cap3} String - | Required: box (x: String) ->{cap1, cap2} String - | - | longer explanation available when compiling with `-explain` + | reference (cap3 : Cap) is not included in the allowed capture set {cap1, cap2} + | of an enclosing function literal with expected type box String ->{cap1, cap2} String -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars-simple.scala:17:12 ---------------------------------- 17 | b = List(g) // error | ^^^^^^^ diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index e4b1e71a2000..0d3c2e0f2e11 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -5,16 +5,13 @@ | | 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:25:8 ------------------------------------------ +-- 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 + | 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 - | - | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:27:12 ----------------------------------------- 27 | b = List(g) // error | ^^^^^^^ From 074be8202ffda69d93ea58c43a8c3f3e77a671d6 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Jul 2024 16:55:48 +0200 Subject: [PATCH 41/61] Implement @unboxed annotation exemption for reach capabilities of parameters --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 + .../dotty/tools/dotc/cc/CheckCaptures.scala | 77 ++++++++++++++++--- .../dotty/tools/dotc/transform/Recheck.scala | 5 +- tests/neg/leak-problem.scala | 31 ++++++++ 4 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 tests/neg/leak-problem.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index df185c589d9e..b52d53d1ac99 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -40,6 +40,8 @@ object ccConfig: */ inline val handleEtaExpansionsSpecially = false + val useUnboxedParams = true + /** If true, use existential capture set variables */ def useExistentials(using Context) = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ce969047524d..80da90e1b62f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -62,6 +62,9 @@ object CheckCaptures: val res = cur cur = cur.outer res + + def ownerString(using Context): String = + if owner.isAnonymousFunction then "enclosing function" else owner.show end Env /** Similar normal substParams, but this is an approximating type map that @@ -386,21 +389,68 @@ class CheckCaptures extends Recheck, SymTransformer: val included = cs.filter: c => c.stripReach match case ref: TermRef => - val isVisible = isVisibleFromEnv(ref.symbol.owner) - if !isVisible && c.isReach then + //if c.isReach then println(i"REACH $c in ${env.owner}") + //assert(!env.owner.isAnonymousFunction) + val refSym = ref.symbol + val refOwner = refSym.owner + val isVisible = isVisibleFromEnv(refOwner) + if !isVisible && c.isReach && refSym.is(Param) && refOwner == env.owner then + if refSym.hasAnnotation(defn.UnboxedAnnot) then + capt.println(i"exempt: $ref in $refOwner") + else // Reach capabilities that go out of scope have to be approximated - // by their underlyiong capture set. See i20503.scala. - checkSubset(CaptureSet.ofInfo(c), env.captured, pos, provenance(env)) + // by their underlying capture set, which cannot be universal. + // Reach capabilities of @unboxed parameters are exempted. + val cs = CaptureSet.ofInfo(c) + if ccConfig.useUnboxedParams then + cs.disallowRootCapability: () => + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) + checkSubset(cs, env.captured, pos, provenance(env)) isVisible case ref: ThisType => isVisibleFromEnv(ref.cls) case _ => false - capt.println(i"Include call or box capture $included from $cs in ${env.owner}") checkSubset(included, env.captured, pos, provenance(env)) + capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") + end markFree /** Include references captured by the called method in the current environment stack */ def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + private val prefixCalls = util.EqHashSet[GenericApply]() + private val unboxedArgs = util.EqHashSet[Tree]() + + def handleCall(meth: Symbol, call: GenericApply, eval: () => Type)(using Context): Type = + if prefixCalls.remove(call) then return eval() + + val unboxedParamNames = + meth.rawParamss.flatMap: params => + params.collect: + case param if param.hasAnnotation(defn.UnboxedAnnot) => + param.name + .toSet + + def markUnboxedArgs(call: GenericApply): Unit = call.fun.tpe.widen match + case MethodType(pnames) => + for (pname, arg) <- pnames.lazyZip(call.args) do + if unboxedParamNames.contains(pname) then + unboxedArgs.add(arg) + case _ => + + def markPrefixCalls(tree: Tree): Unit = tree match + case tree: GenericApply => + prefixCalls.add(tree) + markUnboxedArgs(tree) + markPrefixCalls(tree.fun) + case _ => + + markUnboxedArgs(call) + markPrefixCalls(call.fun) + val res = eval() + includeCallCaptures(meth, call.srcPos) + res + end handleCall + override def recheckIdent(tree: Ident, pt: Type)(using Context): Type = if tree.symbol.is(Method) then if tree.symbol.info.isParameterless then @@ -470,7 +520,6 @@ class CheckCaptures extends Recheck, SymTransformer: */ override def recheckApply(tree: Apply, pt: Type)(using Context): Type = val meth = tree.fun.symbol - includeCallCaptures(meth, tree.srcPos) // Unsafe box/unbox handlng, only for versions < 3.3 def mapArgUsing(f: Type => Type) = @@ -503,7 +552,7 @@ class CheckCaptures extends Recheck, SymTransformer: tp.derivedCapturingType(forceBox(parent), refs) mapArgUsing(forceBox) else - Existential.toCap(super.recheckApply(tree, pt)) match + handleCall(meth, tree, () => Existential.toCap(super.recheckApply(tree, pt))) match case appType @ CapturingType(appType1, refs) => tree.fun match case Select(qual, _) @@ -521,6 +570,13 @@ class CheckCaptures extends Recheck, SymTransformer: case appType => appType end recheckApply + override def recheckArg(arg: Tree, formal: Type)(using Context): Type = + val argType = recheck(arg, formal) + if unboxedArgs.remove(arg) && ccConfig.useUnboxedParams then + capt.println(i"charging deep capture set of $arg: ${argType} = ${CaptureSet.deepCaptureSet(argType)}") + markFree(CaptureSet.deepCaptureSet(argType), arg.srcPos) + argType + private def isDistinct(xs: List[Type]): Boolean = xs match case x :: xs1 => xs1.isEmpty || !xs1.contains(x) && isDistinct(xs1) case Nil => true @@ -589,6 +645,7 @@ class CheckCaptures extends Recheck, SymTransformer: end instantiate override def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = + val meth = tree.symbol if ccConfig.useSealed then val TypeApply(fn, args) = tree val polyType = atPhase(thisPhase.prev): @@ -596,13 +653,13 @@ class CheckCaptures extends Recheck, SymTransformer: def isExempt(sym: Symbol) = sym.isTypeTestOrCast || sym == defn.Compiletime_erasedValue for case (arg: TypeTree, formal, pname) <- args.lazyZip(polyType.paramRefs).lazyZip((polyType.paramNames)) do - if !isExempt(tree.symbol) then - def where = if fn.symbol.exists then i" in an argument of ${fn.symbol}" else "" + if !isExempt(meth) then + def where = if meth.exists then i" in an argument of $meth" else "" disallowRootCapabilitiesIn(arg.knownType, NoSymbol, 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) - Existential.toCap(super.recheckTypeApply(tree, pt)) + handleCall(meth, tree, () => Existential.toCap(super.recheckTypeApply(tree, pt))) override def recheckBlock(tree: Block, pt: Type)(using Context): Type = inNestedLevel(super.recheckBlock(tree, pt)) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 3aec18dc2bd0..93d41f06063d 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -289,6 +289,9 @@ abstract class Recheck extends Phase, SymTransformer: /** A hook to massage the type of an applied method; currently not overridden */ protected def prepareFunction(funtpe: MethodType, meth: Symbol)(using Context): MethodType = funtpe + protected def recheckArg(arg: Tree, formal: Type)(using Context): Type = + recheck(arg, formal) + def recheckApply(tree: Apply, pt: Type)(using Context): Type = val funtpe0 = recheck(tree.fun) // reuse the tree's type on signature polymorphic methods, instead of using the (wrong) rechecked one @@ -303,7 +306,7 @@ abstract class Recheck extends Phase, SymTransformer: else fntpe.paramInfos def recheckArgs(args: List[Tree], formals: List[Type], prefs: List[ParamRef]): List[Type] = args match case arg :: args1 => - val argType = recheck(arg, normalizeByName(formals.head)) + val argType = recheckArg(arg, normalizeByName(formals.head)) val formals1 = if fntpe.isParamDependent then formals.tail.map(_.substParam(prefs.head, argType)) diff --git a/tests/neg/leak-problem.scala b/tests/neg/leak-problem.scala new file mode 100644 index 000000000000..354d54d86707 --- /dev/null +++ b/tests/neg/leak-problem.scala @@ -0,0 +1,31 @@ +import language.experimental.captureChecking + +// Some capabilities that should be used locally +trait Async: + // some method + def read(): Unit +def usingAsync[X](op: Async^ => X): X = ??? + +case class Box[+T](get: T) + +def useBoxedAsync(x: Box[Async^]): Unit = + val t0 = x + val t1 = t0.get // error + t1.read() + +def useBoxedAsync1(x: Box[Async^]): Unit = x.get.read() // error + +def test(): Unit = + val useBoxedAsync2 = (x: Box[Async^]) => + val t0 = x + val t1 = x.get // error + t1.read() + + val f: Box[Async^] => Unit = (x: Box[Async^]) => useBoxedAsync(x) + + def boom(x: Async^): () ->{f} Unit = + () => f(Box(x)) + + val leaked = usingAsync[() ->{f} Unit](boom) + + leaked() // scope violation \ No newline at end of file From 1640340872f16b526337e80cfbbda06d4810029a Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Jul 2024 17:02:46 +0200 Subject: [PATCH 42/61] Add @unboxed to failing tests and standard library file Buffer.scala --- .../src/scala/collection/mutable/Buffer.scala | 4 +-- tests/neg-custom-args/captures/i15749a.scala | 4 ++- tests/neg-custom-args/captures/reaches.check | 36 +++++++++++-------- tests/neg-custom-args/captures/reaches.scala | 7 ++-- .../captures/widen-reach.check | 14 +++----- tests/neg/i20503.scala | 6 ++-- tests/neg/leak-problem-unboxed.scala | 32 +++++++++++++++++ .../pos-custom-args/captures/dep-reach.scala | 5 +-- tests/pos-custom-args/captures/reaches.scala | 4 ++- tests/pos/cc-poly-source-capability.scala | 3 +- tests/pos/cc-poly-source.scala | 3 +- tests/pos/gears-probem-1.scala | 3 +- tests/pos/i18699.scala | 6 ++-- tests/pos/reach-capability.scala | 5 +-- tests/pos/reach-problem.scala | 17 +++++++-- 15 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 tests/neg/leak-problem-unboxed.scala diff --git a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala index f9aa9cf28c72..3ff614bfc556 100644 --- a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala +++ b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala @@ -15,7 +15,7 @@ package mutable import scala.annotation.nowarn import language.experimental.captureChecking - +import caps.unboxed /** A `Buffer` is a growable and shrinkable `Seq`. */ trait Buffer[A] @@ -180,7 +180,7 @@ trait IndexedBuffer[A] extends IndexedSeq[A] override def iterableFactory: SeqFactory[IndexedBuffer] = IndexedBuffer - def flatMapInPlace(f: A => IterableOnce[A]^): this.type = { + def flatMapInPlace(@unboxed f: A => IterableOnce[A]^): this.type = { // There's scope for a better implementation which copies elements in place. var i = 0 val s = size diff --git a/tests/neg-custom-args/captures/i15749a.scala b/tests/neg-custom-args/captures/i15749a.scala index 0158928f4e39..109a73b2b130 100644 --- a/tests/neg-custom-args/captures/i15749a.scala +++ b/tests/neg-custom-args/captures/i15749a.scala @@ -1,4 +1,6 @@ import caps.cap +import caps.unboxed + class Unit object u extends Unit @@ -16,7 +18,7 @@ def test = def force[A](thunk: Unit ->{cap} A): A = thunk(u) - def forceWrapper[A](mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = + def forceWrapper[A](@unboxed mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = // Γ ⊢ mx: Wrapper[□ {cap} Unit => A] // `force` should be typed as ∀(□ {cap} Unit -> A) A, but it can not strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // error // should work diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index f20dbdf311ad..6fdbdefea206 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:23:11 -------------------------------------- -23 | cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:24:11 -------------------------------------- +24 | 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:34:7 --------------------------------------- -34 | (() => f.write()) :: Nil // error since {f*} !<: {xs*} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:35:7 --------------------------------------- +35 | (() => f.write()) :: Nil // error since {f*} !<: {xs*} | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: box List[box () ->{xs*} Unit]^? @@ -15,34 +15,42 @@ | 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:37:6 ------------------------------------------------------------ -37 | var cur: List[Proc] = xs // error: Illegal type for var +-- Error: tests/neg-custom-args/captures/reaches.scala:38:6 ------------------------------------------------------------ +38 | 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:44:15 ----------------------------------------------------------- -44 | val cur = Ref[List[Proc]](xs) // error: illegal type for type argument to Ref +-- Error: tests/neg-custom-args/captures/reaches.scala:45:15 ----------------------------------------------------------- +45 | 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:54:31 ----------------------------------------------------------- -54 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error +-- Error: tests/neg-custom-args/captures/reaches.scala:55:31 ----------------------------------------------------------- +55 | 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:63:27 -------------------------------------- -63 | val f1: File^{id*} = id(f) // error, since now id(f): File^ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:64:27 -------------------------------------- +64 | 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:80:5 ------------------------------------------------------------ -80 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) +-- Error: tests/neg-custom-args/captures/reaches.scala:81:5 ------------------------------------------------------------ +81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error | ^^^^^^ | 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 +-- Error: tests/neg-custom-args/captures/reaches.scala:81:10 ----------------------------------------------------------- +81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error + | ^ + | Local reach capability ps* leaks into capture scope of method mapCompose +-- Error: tests/neg-custom-args/captures/reaches.scala:81:13 ----------------------------------------------------------- +81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error + | ^ + | Local reach capability ps* leaks into capture scope of method mapCompose diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index eadb76c69e5b..f3b4e532e1a2 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,5 +1,6 @@ //> using options -source 3.4 // (to make sure we use the sealed policy) +import caps.unboxed class File: def write(): Unit = ??? @@ -12,7 +13,7 @@ class Ref[T](init: T): def get: T = x def set(y: T) = { x = y } -def runAll0(xs: List[Proc]): Unit = +def runAll0(@unboxed xs: List[Proc]): Unit = var cur: List[() ->{xs*} Unit] = xs // OK, by revised VAR while cur.nonEmpty do val next: () ->{xs*} Unit = cur.head @@ -22,7 +23,7 @@ def runAll0(xs: List[Proc]): Unit = usingFile: f => cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*} -def runAll1(xs: List[Proc]): Unit = +def runAll1(@unboxed xs: List[Proc]): Unit = val cur = Ref[List[() ->{xs*} Unit]](xs) // OK, by revised VAR while cur.get.nonEmpty do val next: () ->{xs*} Unit = cur.get.head @@ -77,4 +78,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 * (should work now) + ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error diff --git a/tests/neg-custom-args/captures/widen-reach.check b/tests/neg-custom-args/captures/widen-reach.check index dbe811ab99ec..06d21ff445d8 100644 --- a/tests/neg-custom-args/captures/widen-reach.check +++ b/tests/neg-custom-args/captures/widen-reach.check @@ -1,17 +1,11 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/widen-reach.scala:13:26 ---------------------------------- +-- 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 ---------------------------------- + | Local reach capability x* leaks into capture scope of method test +-- 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` + | Local reach capability x* leaks into capture scope of method test -- [E164] Declaration Error: tests/neg-custom-args/captures/widen-reach.scala:9:6 -------------------------------------- 9 | val foo: IO^ -> IO^ = x => x // error | ^ diff --git a/tests/neg/i20503.scala b/tests/neg/i20503.scala index e8770b934ad1..463e4e3f9686 100644 --- a/tests/neg/i20503.scala +++ b/tests/neg/i20503.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +import caps.unboxed class List[+A]: def head: A = ??? @@ -7,10 +8,11 @@ class List[+A]: def foreach[U](f: A => U): Unit = ??? def nonEmpty: Boolean = ??? -def runOps(ops: List[() => Unit]): Unit = +def runOps(@unboxed ops: List[() => Unit]): Unit = // See i20156, due to limitation in expressiveness of current system, // 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 + val f: List[() => Unit] -> Unit = (ops: List[() => Unit]) => runOps(ops) // error + val _: List[() => Unit] -> Unit = runOps // error diff --git a/tests/neg/leak-problem-unboxed.scala b/tests/neg/leak-problem-unboxed.scala new file mode 100644 index 000000000000..8591145583e2 --- /dev/null +++ b/tests/neg/leak-problem-unboxed.scala @@ -0,0 +1,32 @@ +import language.experimental.captureChecking +import caps.unboxed + +// Some capabilities that should be used locally +trait Async: + // some method + def read(): Unit +def usingAsync[X](op: Async^ => X): X = ??? + +case class Box[+T](get: T) + +def useBoxedAsync(@unboxed x: Box[Async^]): Unit = + val t0 = x + val t1 = t0.get // ok + t1.read() + +def useBoxedAsync1(@unboxed x: Box[Async^]): Unit = x.get.read() // ok + +def test(): Unit = + + val f: Box[Async^] => Unit = (x: Box[Async^]) => useBoxedAsync(x) // error + val _: Box[Async^] => Unit = useBoxedAsync(_) // error + val _: Box[Async^] => Unit = useBoxedAsync // error + val _ = useBoxedAsync(_) // error + val _ = useBoxedAsync // error + + def boom(x: Async^): () ->{f} Unit = + () => f(Box(x)) + + val leaked = usingAsync[() ->{f} Unit](boom) + + leaked() // scope violation \ No newline at end of file diff --git a/tests/pos-custom-args/captures/dep-reach.scala b/tests/pos-custom-args/captures/dep-reach.scala index 56343fbf8e53..177422565736 100644 --- a/tests/pos-custom-args/captures/dep-reach.scala +++ b/tests/pos-custom-args/captures/dep-reach.scala @@ -1,9 +1,10 @@ +import caps.unboxed object Test: class C type Proc = () => Unit def f(c: C^, d: C^): () ->{c, d} Unit = - def foo(xs: Proc*): () ->{xs*} Unit = + def foo(@unboxed xs: Proc*): () ->{xs*} Unit = xs.head val a: () ->{c} Unit = () => () val b: () ->{d} Unit = () => () @@ -12,7 +13,7 @@ object Test: def g(c: C^, d: C^): () ->{c, d} Unit = - def foo(xs: Seq[() => Unit]): () ->{xs*} Unit = + def foo(@unboxed xs: Seq[() => Unit]): () ->{xs*} Unit = xs.head val a: () ->{c} Unit = () => () diff --git a/tests/pos-custom-args/captures/reaches.scala b/tests/pos-custom-args/captures/reaches.scala index b1045e3c999a..976fadc4b649 100644 --- a/tests/pos-custom-args/captures/reaches.scala +++ b/tests/pos-custom-args/captures/reaches.scala @@ -1,3 +1,5 @@ +import caps.unboxed + class C def f(xs: List[C^]) = val y = xs @@ -20,7 +22,7 @@ extension [A](x: A) def :: (xs: List[A]): List[A] = ??? object Nil extends List[Nothing] -def runAll(xs: List[Proc]): Unit = +def runAll(@unboxed xs: List[Proc]): Unit = var cur: List[() ->{xs*} Unit] = xs // OK, by revised VAR while cur.nonEmpty do val next: () ->{xs*} Unit = cur.head diff --git a/tests/pos/cc-poly-source-capability.scala b/tests/pos/cc-poly-source-capability.scala index 9a21b2d5b802..0f61e98e5068 100644 --- a/tests/pos/cc-poly-source-capability.scala +++ b/tests/pos/cc-poly-source-capability.scala @@ -1,6 +1,7 @@ import language.experimental.captureChecking import annotation.experimental import caps.{CapSet, Capability} +import caps.unboxed @experimental object Test: @@ -17,7 +18,7 @@ import caps.{CapSet, Capability} def allListeners: Set[Listener^{X^}] = listeners - def test1(async1: Async, others: List[Async]) = + def test1(async1: Async, @unboxed others: List[Async]) = val src = Source[CapSet^{async1, others*}] val lst1 = listener(async1) val lsts = others.map(listener) diff --git a/tests/pos/cc-poly-source.scala b/tests/pos/cc-poly-source.scala index 939f1f682dc8..09b4a3024e3c 100644 --- a/tests/pos/cc-poly-source.scala +++ b/tests/pos/cc-poly-source.scala @@ -1,6 +1,7 @@ import language.experimental.captureChecking import annotation.experimental import caps.{CapSet, Capability} +import caps.unboxed @experimental object Test: @@ -24,7 +25,7 @@ import caps.{CapSet, Capability} val ls = src.allListeners val _: Set[Listener^{lbl1, lbl2}] = ls - def test2(lbls: List[Label^]) = + def test2(@unboxed lbls: List[Label^]) = def makeListener(lbl: Label^): Listener^{lbl} = ??? val listeners = lbls.map(makeListener) val src = Source[CapSet^{lbls*}] diff --git a/tests/pos/gears-probem-1.scala b/tests/pos/gears-probem-1.scala index f5c7fdfd0a3c..c683db9ce01d 100644 --- a/tests/pos/gears-probem-1.scala +++ b/tests/pos/gears-probem-1.scala @@ -1,4 +1,5 @@ import language.experimental.captureChecking +import caps.unboxed trait Future[+T]: def await: T @@ -16,7 +17,7 @@ class Result[+T, +E]: case class Err[+E](e: E) extends Result[Nothing, E] case class Ok[+T](x: T) extends Result[T, Nothing] -extension [T](fs: Seq[Future[T]^]) +extension [T](@unboxed fs: Seq[Future[T]^]) def awaitAll = val collector//: Collector[T]{val futures: Seq[Future[T]^{fs*}]} = Collector(fs) diff --git a/tests/pos/i18699.scala b/tests/pos/i18699.scala index 4bd3fbaad890..54390f6bdd71 100644 --- a/tests/pos/i18699.scala +++ b/tests/pos/i18699.scala @@ -1,7 +1,9 @@ import language.experimental.captureChecking +import caps.unboxed + trait Cap: def use: Int = 42 -def test2(cs: List[Cap^]): Unit = +def test2(@unboxed cs: List[Cap^]): Unit = val t0: Cap^{cs*} = cs.head // error - var t1: Cap^{cs*} = cs.head // error \ No newline at end of file + var t1: Cap^{cs*} = cs.head // error diff --git a/tests/pos/reach-capability.scala b/tests/pos/reach-capability.scala index d551113eb05b..5ad7534061b1 100644 --- a/tests/pos/reach-capability.scala +++ b/tests/pos/reach-capability.scala @@ -1,6 +1,7 @@ import language.experimental.captureChecking import annotation.experimental -import caps.{Capability} +import caps.Capability +import caps.unboxed @experimental object Test2: @@ -11,7 +12,7 @@ import caps.{Capability} class Listener - def test2(lbls: List[Label]) = + def test2(@unboxed lbls: List[Label]) = def makeListener(lbl: Label): Listener^{lbl} = ??? val listeners = lbls.map(makeListener) // should work diff --git a/tests/pos/reach-problem.scala b/tests/pos/reach-problem.scala index 60dd1d4667a7..29d46687a219 100644 --- a/tests/pos/reach-problem.scala +++ b/tests/pos/reach-problem.scala @@ -1,9 +1,22 @@ import language.experimental.captureChecking +import caps.unboxed class Box[T](items: Seq[T^]): def getOne: T^{items*} = ??? object Box: - def getOne[T](items: Seq[T^]): T^{items*} = + def getOne[T](@unboxed items: Seq[T^]): T^{items*} = val bx = Box(items) - bx.getOne \ No newline at end of file + bx.getOne +/* + def head[T](items: Seq[T^]): Unit = + val is = items + val x = is.head + () + + def head2[X^, T](items: Seq[T^{X^}]): T^{X^} = + items.head + + def head3[T](items: Seq[T^]): Unit = + head2[caps.CapSet^{items*}, T](items) +*/ \ No newline at end of file From 4333d3ed1cb9c8e2f56c75a972f01bdd789dcf25 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Jul 2024 09:54:30 +0200 Subject: [PATCH 43/61] Fix isAlwaysPure The condition on capturing types did not make sense. In a type T^{} with an empty capture set `T` can still be a type variable that's instantiated to a type with a capture set. Instead, T^cs is always pure if T is always pure. For instance `List[T]^{p}` is always pure. That's important in the context of the standard library, where such a type usually results from an instantiation of a type variable such as `C[T]^{p}`. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index b52d53d1ac99..7fe027b6623d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -282,8 +282,6 @@ extension (tp: Type) val sym = tp.typeSymbol if sym.isClass then sym.isPureClass else tp.superType.isAlwaysPure - case CapturingType(parent, refs) => - parent.isAlwaysPure || refs.isAlwaysEmpty case tp: TypeProxy => tp.superType.isAlwaysPure case tp: AndType => From 90749ea2d6c47b05ff2932ecd216f2cc5cb50103 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Jul 2024 11:08:24 +0200 Subject: [PATCH 44/61] Fix special capture set handling in recheckApply, Step 1 Step1: refactor The logic was querying the original types of trees, but we want the rechecked types instead. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 65 ++++++++++--------- .../dotty/tools/dotc/transform/Recheck.scala | 25 +++++-- .../captures/caseclass/Test_2.scala | 2 +- tests/neg-custom-args/captures/reaches.check | 10 +++ tests/neg-custom-args/captures/reaches.scala | 2 +- .../captures/unsound-reach-2.scala | 2 +- .../captures/unsound-reach-3.scala | 4 +- .../captures/unsound-reach-4.check | 11 ++-- .../captures/unsound-reach-4.scala | 4 +- .../pos-custom-args/captures/caseclass.scala | 2 +- .../dotc/transform/Recheck.scala | 32 +++++---- 12 files changed, 101 insertions(+), 62 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 7fe027b6623d..34dc5fc395d6 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -274,8 +274,8 @@ extension (tp: Type) case _ => tp - /** Is type known to be always pure by its class structure, - * so that adding a capture set to it would not make sense? + /** Is type known to be always pure by its class structure? + * In that case, adding a capture set to it would not make sense. */ def isAlwaysPure(using Context): Boolean = tp.dealias match case tp: (TypeRef | AppliedType) => diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 80da90e1b62f..c32028a22471 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -463,8 +463,8 @@ class CheckCaptures extends Recheck, SymTransformer: /** A specialized implementation of the selection rule. * - * E |- f: f{ m: Cr R }^Cf - * ----------------------- + * E |- f: T{ m: R^Cr }^{f} + * ------------------------ * E |- f.m: R^C * * The implementation picks as `C` one of `{f}` or `Cr`, depending on the @@ -507,17 +507,6 @@ class CheckCaptures extends Recheck, SymTransformer: selType }//.showing(i"recheck sel $tree, $qualType = $result") - /** A specialized implementation of the apply rule. - * - * E |- f: Ra ->Cf Rr^Cr - * E |- a: Ra^Ca - * --------------------- - * E |- f a: Rr^C - * - * The implementation picks as `C` one of `{f, a}` or `Cr`, depending on the - * outcome of a `mightSubcapture` test. It picks `{f, a}` if this might subcapture Cr - * and Cr otherwise. - */ override def recheckApply(tree: Apply, pt: Type)(using Context): Type = val meth = tree.fun.symbol @@ -552,31 +541,47 @@ class CheckCaptures extends Recheck, SymTransformer: tp.derivedCapturingType(forceBox(parent), refs) mapArgUsing(forceBox) else - handleCall(meth, tree, () => Existential.toCap(super.recheckApply(tree, pt))) match - case appType @ CapturingType(appType1, refs) => - tree.fun match - case Select(qual, _) - if !tree.fun.symbol.isConstructor - && !qual.tpe.isBoxedCapturing - && !tree.args.exists(_.tpe.isBoxedCapturing) - && qual.tpe.captureSet.mightSubcapture(refs) - && tree.args.forall(_.tpe.captureSet.mightSubcapture(refs)) - => - val callCaptures = tree.args.foldLeft(qual.tpe.captureSet): (cs, arg) => - cs ++ arg.tpe.captureSet - appType.derivedCapturingType(appType1, callCaptures) - .showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qual.tpe.captureSet} = $result", capt) - case _ => appType - case appType => appType + handleCall(meth, tree, () => super.recheckApply(tree, pt)) end recheckApply - override def recheckArg(arg: Tree, formal: Type)(using Context): Type = + protected override + def recheckArg(arg: Tree, formal: Type)(using Context): Type = val argType = recheck(arg, formal) if unboxedArgs.remove(arg) && ccConfig.useUnboxedParams then capt.println(i"charging deep capture set of $arg: ${argType} = ${CaptureSet.deepCaptureSet(argType)}") markFree(CaptureSet.deepCaptureSet(argType), arg.srcPos) argType + /** A specialized implementation of the apply rule. + * + * E |- f: Ra ->Cf Rr^Cr + * E |- a: Ra^Ca + * --------------------- + * E |- f a: Rr^C + * + * The implementation picks as `C` one of `{f, a}` or `Cr`, depending on the + * outcome of a `mightSubcapture` test. It picks `{f, a}` if this might subcapture Cr + * and Cr otherwise. + */ + protected override + def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = + Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) match + case appType @ CapturingType(appType1, refs) + if qualType.exists + && !tree.fun.symbol.isConstructor + && !qualType.isBoxedCapturing // TODO: This is not strng enough, we also have + // to exclude existentials in function results + && !argTypes.exists(_.isBoxedCapturing) + && qualType.captureSet.mightSubcapture(refs) + && argTypes.forall(_.captureSet.mightSubcapture(refs)) + => + val callCaptures = tree.args.foldLeft(qualType.captureSet): (cs, arg) => + cs ++ arg.tpe.captureSet + appType.derivedCapturingType(appType1, callCaptures) + .showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qualType.captureSet} = $result", capt) + case appType => + appType + private def isDistinct(xs: List[Type]): Boolean = xs match case x :: xs1 => xs1.isEmpty || !xs1.contains(x) && isDistinct(xs1) case Nil => true diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 93d41f06063d..4b8a8f072774 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -202,11 +202,13 @@ abstract class Recheck extends Phase, SymTransformer: tree.tpe def recheckSelect(tree: Select, pt: Type)(using Context): Type = - val Select(qual, name) = tree + recheckSelection(tree, recheckSelectQualifier(tree), tree.name, pt) + + def recheckSelectQualifier(tree: Select)(using Context): Type = val proto = if tree.symbol == defn.Any_asInstanceOf then WildcardType else AnySelectionProto - recheckSelection(tree, recheck(qual, proto).widenIfUnstable, name, pt) + recheck(tree.qualifier, proto).widenIfUnstable def recheckSelection(tree: Select, qualType: Type, name: Name, sharpen: Denotation => Denotation)(using Context): Type = @@ -292,8 +294,23 @@ abstract class Recheck extends Phase, SymTransformer: protected def recheckArg(arg: Tree, formal: Type)(using Context): Type = recheck(arg, formal) + /** A hook to check all the parts of an application: + * @param tree the application `fn(args)` + * @param qualType if the `fn` is a select `q.m`, the type of the qualifier `q`, + * otherwise NoType + * @param funType the method type of `fn` + * @param argTypes the types of the arguments + */ + protected def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = + constFold(tree, instantiate(funType, argTypes, tree.fun.symbol)) + def recheckApply(tree: Apply, pt: Type)(using Context): Type = - val funtpe0 = recheck(tree.fun) + val (funtpe0, qualType) = tree.fun match + case fun: Select => + val qualType = recheckSelectQualifier(fun) + (recheckSelection(fun, qualType, fun.name, WildcardType), qualType) + case _ => + (recheck(tree.fun), NoType) // reuse the tree's type on signature polymorphic methods, instead of using the (wrong) rechecked one val funtpe1 = if tree.fun.symbol.originalSignaturePolymorphic.exists then tree.fun.tpe else funtpe0 funtpe1.widen match @@ -316,7 +333,7 @@ abstract class Recheck extends Phase, SymTransformer: assert(formals.isEmpty) Nil val argTypes = recheckArgs(tree.args, formals, fntpe.paramRefs) - constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol)) + recheckApplication(tree, qualType, fntpe1, argTypes) //.showing(i"typed app $tree : $fntpe with ${tree.args}%, % : $argTypes%, % = $result") case tp => assert(false, i"unexpected type of ${tree.fun}: $tp") diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala index 9d97d5537c72..e54ab1774202 100644 --- a/tests/neg-custom-args/captures/caseclass/Test_2.scala +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -22,4 +22,4 @@ def test(c: C) = val y4 = y3 match case Ref(xx) => xx - val y4c: () ->{x3} Unit = y4 + val y4c: () ->{y3} Unit = y4 diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 6fdbdefea206..615e3901a437 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -34,6 +34,16 @@ | 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:57:6 --------------------------------------- +57 | id(() => f.write()) // error + | ^^^^^^^^^^^^^^^^^^^ + | Found: () => Unit + | Required: () ->? Unit + | + | Note that the universal capability `cap` + | cannot be included in capture set ? + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:64:27 -------------------------------------- 64 | val f1: File^{id*} = id(f) // error, since now id(f): File^ | ^^^^^ diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index f3b4e532e1a2..e602d4b34493 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -54,7 +54,7 @@ class Id[-A, +B >: A](): def test = val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error usingFile: f => - id(() => f.write()) // escape, if it was not for the error above + id(() => f.write()) // error def attack2 = val id: File^ -> File^ = x => x diff --git a/tests/neg-custom-args/captures/unsound-reach-2.scala b/tests/neg-custom-args/captures/unsound-reach-2.scala index 384af31ee1fc..5bea18bdccba 100644 --- a/tests/neg-custom-args/captures/unsound-reach-2.scala +++ b/tests/neg-custom-args/captures/unsound-reach-2.scala @@ -20,7 +20,7 @@ def bad(): Unit = var escaped: File^{backdoor*} = null withFile("hello.txt"): f => - boom.use(f): // error + boom.use(f): new Consumer[File^{backdoor*}]: // error def apply(f1: File^{backdoor*}) = escaped = f1 diff --git a/tests/neg-custom-args/captures/unsound-reach-3.scala b/tests/neg-custom-args/captures/unsound-reach-3.scala index 985beb7ae55d..0063216e957e 100644 --- a/tests/neg-custom-args/captures/unsound-reach-3.scala +++ b/tests/neg-custom-args/captures/unsound-reach-3.scala @@ -16,8 +16,8 @@ def bad(): Unit = val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null - withFile("hello.txt"): f => - escaped = boom.use(f) // error + withFile("hello.txt"): f => // error + escaped = boom.use(f) // boom.use: (x: File^) -> File^{backdoor*}, it is a selection so reach capabilities are allowed // f: File^, so there is no reach capabilities diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index 9abf86c772d5..d359b298555e 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -1,5 +1,6 @@ --- 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 +-- Error: tests/neg-custom-args/captures/unsound-reach-4.scala:21:25 --------------------------------------------------- +21 | withFile("hello.txt"): f => // error + | ^ + | Reach capability backdoor* and universal capability cap cannot both + | appear in the type (f: File^) ->{backdoor*} Unit of this expression +22 | escaped = boom.use(f) diff --git a/tests/neg-custom-args/captures/unsound-reach-4.scala b/tests/neg-custom-args/captures/unsound-reach-4.scala index 14050b4afff2..bc66085614f2 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.scala +++ b/tests/neg-custom-args/captures/unsound-reach-4.scala @@ -18,5 +18,5 @@ def bad(): Unit = val boom: Foo[File^{backdoor*}] = backdoor var escaped: File^{backdoor*} = null - withFile("hello.txt"): f => - escaped = boom.use(f) // error + withFile("hello.txt"): f => // error + escaped = boom.use(f) diff --git a/tests/pos-custom-args/captures/caseclass.scala b/tests/pos-custom-args/captures/caseclass.scala index 0aa656eaf9cb..fe7e02b1b6c2 100644 --- a/tests/pos-custom-args/captures/caseclass.scala +++ b/tests/pos-custom-args/captures/caseclass.scala @@ -31,4 +31,4 @@ object test2: val y4 = y3 match case Ref(xx) => xx - val y4c: () ->{x3} Unit = y4 + val y4c: () ->{y3} Unit = y4 diff --git a/tests/pos-with-compiler-cc/dotc/transform/Recheck.scala b/tests/pos-with-compiler-cc/dotc/transform/Recheck.scala index c524bbb7702f..e566a6d7482a 100644 --- a/tests/pos-with-compiler-cc/dotc/transform/Recheck.scala +++ b/tests/pos-with-compiler-cc/dotc/transform/Recheck.scala @@ -170,23 +170,15 @@ abstract class Recheck extends Phase, SymTransformer: def recheckIdent(tree: Ident)(using Context): Type = tree.tpe - def recheckSelect(tree: Select, pt: Type)(using Context): Type = + def recheckSelectQualifier(tree: Select)(using Conext): Type = val Select(qual, name) = tree val proto = if tree.symbol == defn.Any_asInstanceOf then WildcardType else AnySelectionProto - recheckSelection(tree, recheck(qual, proto).widenIfUnstable, name, pt) + recheck(qual, proto).widenIfUnstable - /** When we select the `apply` of a function with type such as `(=> A) => B`, - * we need to convert the parameter type `=> A` to `() ?=> A`. See doc comment - * of `mapExprType`. - */ - def normalizeByName(mbr: SingleDenotation)(using Context): SingleDenotation = mbr.info match - case mt: MethodType if mt.paramInfos.exists(_.isInstanceOf[ExprType]) => - mbr.derivedSingleDenotation(mbr.symbol, - mt.derivedLambdaType(paramInfos = mt.paramInfos.map(_.mapExprType))) - case _ => - mbr + def recheckSelect(tree: Select, pt: Type)(using Context): Type = + recheckSelection(tree, recheckSelectQualifier(tree), name, pt) def recheckSelection(tree: Select, qualType: Type, name: Name, sharpen: Denotation => Denotation)(using Context): Type = @@ -210,11 +202,21 @@ abstract class Recheck extends Phase, SymTransformer: constFold(tree, newType) //.showing(i"recheck select $qualType . $name : ${mbr.info} = $result") - /** Keep the symbol of the `select` but re-infer its type */ def recheckSelection(tree: Select, qualType: Type, name: Name, pt: Type)(using Context): Type = recheckSelection(tree, qualType, name, sharpen = identity[Denotation]) + /** When we select the `apply` of a function with type such as `(=> A) => B`, + * we need to convert the parameter type `=> A` to `() ?=> A`. See doc comment + * of `mapExprType`. + */ + def normalizeByName(mbr: SingleDenotation)(using Context): SingleDenotation = mbr.info match + case mt: MethodType if mt.paramInfos.exists(_.isInstanceOf[ExprType]) => + mbr.derivedSingleDenotation(mbr.symbol, + mt.derivedLambdaType(paramInfos = mt.paramInfos.map(_.mapExprType))) + case _ => + mbr + def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match case Bind(name, body) => recheck(body, pt) @@ -260,7 +262,11 @@ abstract class Recheck extends Phase, SymTransformer: protected def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = mt.instantiate(argTypes) + def recheckApplication(tree: Tree, qualType: Type, argTypes: List[Type])(using Context): Type = + constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol)) + def recheckApply(tree: Apply, pt: Type)(using Context): Type = + fun val funTp = recheck(tree.fun) // reuse the tree's type on signature polymorphic methods, instead of using the (wrong) rechecked one val funtpe = if tree.fun.symbol.originalSignaturePolymorphic.exists then tree.fun.tpe else funTp From bc818a911374f4f0e278880714b3cc0d501e2d8c Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Jul 2024 11:58:38 +0200 Subject: [PATCH 45/61] Fix special capture set handling in recheckApply, Step 2 Step 2: Change the logic. The previous one was unsound. The new logic is a bot too conservative. I left comments in tests where it could be improved. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 22 ++++++++----------- .../src/scala/collection/Iterator.scala | 5 +++-- .../immutable/LazyListIterable.scala | 4 +++- .../src/scala/collection/mutable/Buffer.scala | 6 ++++- tests/neg-custom-args/captures/reaches.check | 2 +- .../captures/nested-classes-2.scala | 2 +- .../colltest5/CollectionStrawManCC5_1.scala | 12 +++++++++- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c32028a22471..283df3254ab5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -554,14 +554,15 @@ class CheckCaptures extends Recheck, SymTransformer: /** A specialized implementation of the apply rule. * - * E |- f: Ra ->Cf Rr^Cr - * E |- a: Ra^Ca + * E |- q: Tq^Cq + * E |- q.f: Ta ->Cf Tr^Cr + * E |- a: Ta * --------------------- - * E |- f a: Rr^C + * E |- f(a): Tr^C * - * The implementation picks as `C` one of `{f, a}` or `Cr`, depending on the - * outcome of a `mightSubcapture` test. It picks `{f, a}` if this might subcapture Cr - * and Cr otherwise. + * The implementation picks `C` as `Cq` instead of `Cr`, if + * 1. The argument(s) Ta are always pure + * 2. `Cq` might subcapture `Cr`. */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = @@ -569,15 +570,10 @@ class CheckCaptures extends Recheck, SymTransformer: case appType @ CapturingType(appType1, refs) if qualType.exists && !tree.fun.symbol.isConstructor - && !qualType.isBoxedCapturing // TODO: This is not strng enough, we also have - // to exclude existentials in function results - && !argTypes.exists(_.isBoxedCapturing) + && argTypes.forall(_.isAlwaysPure) && qualType.captureSet.mightSubcapture(refs) - && argTypes.forall(_.captureSet.mightSubcapture(refs)) => - val callCaptures = tree.args.foldLeft(qualType.captureSet): (cs, arg) => - cs ++ arg.tpe.captureSet - appType.derivedCapturingType(appType1, callCaptures) + appType.derivedCapturingType(appType1, qualType.captureSet) .showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qualType.captureSet} = $result", capt) case appType => appType diff --git a/scala2-library-cc/src/scala/collection/Iterator.scala b/scala2-library-cc/src/scala/collection/Iterator.scala index 4d1b0ed4ff95..d75f136e0253 100644 --- a/scala2-library-cc/src/scala/collection/Iterator.scala +++ b/scala2-library-cc/src/scala/collection/Iterator.scala @@ -17,7 +17,7 @@ import scala.annotation.tailrec import scala.annotation.unchecked.uncheckedVariance import scala.runtime.Statics import language.experimental.captureChecking - +import caps.unsafe.unsafeAssumePure /** Iterators are data structures that allow to iterate over a sequence * of elements. They have a `hasNext` method for checking @@ -595,7 +595,8 @@ trait Iterator[+A] extends IterableOnce[A] with IterableOnceOps[A, Iterator, Ite private[this] def nextCur(): Unit = { cur = null - cur = f(self.next()).iterator + cur = f(self.next()).iterator.unsafeAssumePure + // !!! see explanation in colltest5.CollectionStrawManCC5_1.flatMap why the unsafeAssumePure is needed _hasNext = -1 } diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index 2f7b017a6729..baf89ef54aab 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -25,6 +25,7 @@ import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures import caps.untrackedCaptures +import caps.unsafe.unsafeAssumePure /** This class implements an immutable linked list. We call it "lazy" * because it computes its elements only when they are needed. @@ -1041,7 +1042,8 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { var itHasNext = false var rest = restRef // var rest = restRef.elem while (!itHasNext && !rest.isEmpty) { - it = f(rest.head).iterator + it = f(rest.head).iterator.unsafeAssumePure + // !!! see explanation in colltest5.CollectionStrawManCC5_1.flatMap why the unsafeAssumePure is needed itHasNext = it.hasNext if (!itHasNext) { // wait to advance `rest` because `it.next()` can throw rest = rest.tail diff --git a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala index 3ff614bfc556..eebc2fbc1417 100644 --- a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala +++ b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala @@ -16,6 +16,7 @@ package mutable import scala.annotation.nowarn import language.experimental.captureChecking import caps.unboxed +import caps.unsafe.unsafeAssumePure /** A `Buffer` is a growable and shrinkable `Seq`. */ trait Buffer[A] @@ -185,7 +186,10 @@ trait IndexedBuffer[A] extends IndexedSeq[A] var i = 0 val s = size val newElems = new Array[(IterableOnce[A]^{f*})](s) - while (i < s) { newElems(i) = f(this(i)); i += 1 } + while i < s do + newElems(i) = f(this(i)).unsafeAssumePure + // !!! see explanation in colltest5.CollectionStrawManCC5_1.flatMap why the unsafeAssumePure is needed + i += 1 clear() i = 0 while (i < s) { ++=(newElems(i)); i += 1 } diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 615e3901a437..96859290075a 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -47,7 +47,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:64:27 -------------------------------------- 64 | val f1: File^{id*} = id(f) // error, since now id(f): File^ | ^^^^^ - | Found: File^{id, f} + | Found: File^ | Required: File^{id*} | | longer explanation available when compiling with `-explain` diff --git a/tests/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala index 744635ee949b..17ee3b8f3cb0 100644 --- a/tests/pos-custom-args/captures/nested-classes-2.scala +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -20,5 +20,5 @@ def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = def test3(y1: (() => Unit), y2: (() => Unit) => Unit) = val cc1: C1^{y1, y2} = C1(y1, y2) val cc2 = cc1.c2(x1, x2) - val cc3: cc1.C2^{cc1, x1, x2} = cc2 + val cc3: cc1.C2^{cc2} = cc2 diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index 5443758afa72..da15fee9849b 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -5,6 +5,7 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec +import caps.unsafe.unsafeAssumePure /** A strawman architecture for new collections. It contains some * example collection classes and methods with the intent to expose @@ -555,7 +556,16 @@ object CollectionStrawMan5 { private var myCurrent: Iterator[B]^{this, f} = Iterator.empty private def current = { while (!myCurrent.hasNext && self.hasNext) - myCurrent = f(self.next()).iterator + myCurrent = f(self.next()).iterator.unsafeAssumePure + // !!! This is unsafe since the iterator's result could return a capability + // depending on the argument self.next() of type A. To exclude that we'd have + // to define f to be of type EX c. A ->{c} IterableOnce[B]^{c}, i.e. `c` may + // not depend on A. But to get there we have to + // - improve the way we express existentials using `^` + // - rework the recheckApplication code to cater for this. Right now + // we cannot do anything since `A` is not always pure. But if we took + // the existential scope of the result into account, this could provide + // a solution. myCurrent } def hasNext = current.hasNext From bb94805775ca9e3d7b1b2df24d78b2751640cd17 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 19:08:42 +0200 Subject: [PATCH 46/61] 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 d6868e569a05..e63aab484605 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2834,18 +2834,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 71de9ef0e0f9..9ed9fc079975 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -518,9 +518,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) = @@ -1657,9 +1691,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 @@ -2275,31 +2306,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 @@ -2312,17 +2366,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 @@ -3015,26 +3061,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, @@ -3089,8 +3115,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) { @@ -3192,8 +3216,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) @@ -5834,30 +5851,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 cc95088671374772d0923dc93621e0b1b074af32 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 28 Jun 2024 21:34:09 +0200 Subject: [PATCH 47/61] 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 34dc5fc395d6..889cb41e481b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -183,6 +183,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 9ed9fc079975..036795a3a57a 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 @@ -517,45 +518,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) @@ -2081,20 +2043,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) @@ -2293,86 +2241,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. */ From 3667ab84cae22269ad0ae757d1e4986338f6d583 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Jul 2024 18:34:25 +0200 Subject: [PATCH 48/61] Don't box arguments of any form of type cast or test Previously, only asInstanceOf was excluded. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 1 + compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 11 +++++++++++ compiler/src/dotty/tools/dotc/cc/Setup.scala | 9 +++------ compiler/src/dotty/tools/dotc/core/Definitions.scala | 6 ++++++ .../captures/opaque-inline-problem.scala | 12 ++++++++++++ 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 tests/pos-custom-args/captures/opaque-inline-problem.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 889cb41e481b..af17cdc6b11a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -573,6 +573,7 @@ extension (sym: Symbol) && sym != defn.Caps_unsafeBox && sym != defn.Caps_unsafeUnbox && !defn.isPolymorphicAfterErasure(sym) + && !defn.isTypeTestOrCast(sym) def isRefiningParamAccessor(using Context): Boolean = sym.is(ParamAccessor) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 283df3254ab5..f330421a2647 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -563,6 +563,17 @@ class CheckCaptures extends Recheck, SymTransformer: * The implementation picks `C` as `Cq` instead of `Cr`, if * 1. The argument(s) Ta are always pure * 2. `Cq` might subcapture `Cr`. + * TODO: We could generalize this as follows: + * + * If the function `f` does not have an `@unboxed` parameter, then + * any unboxing it does would be charged to the environment of the function + * so they have to appear in Cq. So another approximation of the + * result capability set is `Cq u Ca` where `Ca` is the capture set of the + * argument. + * If the function `f` does have an `@unboxed` parameter, then it could in addition + * unbox reach capabilities over its formal parameter. Therefore, the approximation + * would be `Cq u dcs(Ca)` instead. + * If the approximation is known to subcapture the declared result Cr, we pick it. */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index cb74e2c71e73..26817cb838c6 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -439,12 +439,9 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree @ TypeApply(fn, args) => traverse(fn) - fn match - case Select(qual, nme.asInstanceOf_) => - // No need to box type arguments of an asInstanceOf call. See #20224. - case _ => - for case arg: TypeTree <- args do - transformTT(arg, boxed = true, exact = false) // type arguments in type applications are boxed + if !defn.isTypeTestOrCast(fn.symbol) then + for case arg: TypeTree <- args do + transformTT(arg, boxed = true, exact = false) // type arguments in type applications are boxed case tree: TypeDef if tree.symbol.isClass => val sym = tree.symbol diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b7aa74ea2a92..8c2f448a4a5e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1768,6 +1768,12 @@ class Definitions { def isPolymorphicAfterErasure(sym: Symbol): Boolean = (sym eq Any_isInstanceOf) || (sym eq Any_asInstanceOf) || (sym eq Object_synchronized) + def isTypeTestOrCast(sym: Symbol): Boolean = + (sym eq Any_isInstanceOf) + || (sym eq Any_asInstanceOf) + || (sym eq Any_typeTest) + || (sym eq Any_typeCast) + /** Is this type a `TupleN` type? * * @return true if the dealiased type of `tp` is `TupleN[T1, T2, ..., Tn]` diff --git a/tests/pos-custom-args/captures/opaque-inline-problem.scala b/tests/pos-custom-args/captures/opaque-inline-problem.scala new file mode 100644 index 000000000000..de9a4437becd --- /dev/null +++ b/tests/pos-custom-args/captures/opaque-inline-problem.scala @@ -0,0 +1,12 @@ +trait Async extends caps.Capability: + def group: Int + +object Async: + inline def current(using async: Async): async.type = async + opaque type Spawn <: Async = Async + def blocking[T](f: Spawn => T): T = ??? + +def main() = + Async.blocking: spawn => + val c = Async.current(using spawn) + val a = c.group From d8ace30d171f4d9b2e1864611e86d97c5e343942 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jul 2024 10:20:57 +0200 Subject: [PATCH 49/61] Revert "Fix special capture set handling in recheckApply, Step 2" This reverts commit f1f5a05d3d9461fd8437d0469583fe860d0116ba. # Conflicts: # compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 33 ++++++++----------- .../src/scala/collection/Iterator.scala | 5 ++- .../immutable/LazyListIterable.scala | 4 +-- .../src/scala/collection/mutable/Buffer.scala | 6 +--- tests/neg-custom-args/captures/reaches.check | 2 +- .../captures/nested-classes-2.scala | 2 +- .../colltest5/CollectionStrawManCC5_1.scala | 12 +------ 7 files changed, 20 insertions(+), 44 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f330421a2647..c32028a22471 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -554,26 +554,14 @@ class CheckCaptures extends Recheck, SymTransformer: /** A specialized implementation of the apply rule. * - * E |- q: Tq^Cq - * E |- q.f: Ta ->Cf Tr^Cr - * E |- a: Ta + * E |- f: Ra ->Cf Rr^Cr + * E |- a: Ra^Ca * --------------------- - * E |- f(a): Tr^C + * E |- f a: Rr^C * - * The implementation picks `C` as `Cq` instead of `Cr`, if - * 1. The argument(s) Ta are always pure - * 2. `Cq` might subcapture `Cr`. - * TODO: We could generalize this as follows: - * - * If the function `f` does not have an `@unboxed` parameter, then - * any unboxing it does would be charged to the environment of the function - * so they have to appear in Cq. So another approximation of the - * result capability set is `Cq u Ca` where `Ca` is the capture set of the - * argument. - * If the function `f` does have an `@unboxed` parameter, then it could in addition - * unbox reach capabilities over its formal parameter. Therefore, the approximation - * would be `Cq u dcs(Ca)` instead. - * If the approximation is known to subcapture the declared result Cr, we pick it. + * The implementation picks as `C` one of `{f, a}` or `Cr`, depending on the + * outcome of a `mightSubcapture` test. It picks `{f, a}` if this might subcapture Cr + * and Cr otherwise. */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = @@ -581,10 +569,15 @@ class CheckCaptures extends Recheck, SymTransformer: case appType @ CapturingType(appType1, refs) if qualType.exists && !tree.fun.symbol.isConstructor - && argTypes.forall(_.isAlwaysPure) + && !qualType.isBoxedCapturing // TODO: This is not strng enough, we also have + // to exclude existentials in function results + && !argTypes.exists(_.isBoxedCapturing) && qualType.captureSet.mightSubcapture(refs) + && argTypes.forall(_.captureSet.mightSubcapture(refs)) => - appType.derivedCapturingType(appType1, qualType.captureSet) + val callCaptures = tree.args.foldLeft(qualType.captureSet): (cs, arg) => + cs ++ arg.tpe.captureSet + appType.derivedCapturingType(appType1, callCaptures) .showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qualType.captureSet} = $result", capt) case appType => appType diff --git a/scala2-library-cc/src/scala/collection/Iterator.scala b/scala2-library-cc/src/scala/collection/Iterator.scala index d75f136e0253..4d1b0ed4ff95 100644 --- a/scala2-library-cc/src/scala/collection/Iterator.scala +++ b/scala2-library-cc/src/scala/collection/Iterator.scala @@ -17,7 +17,7 @@ import scala.annotation.tailrec import scala.annotation.unchecked.uncheckedVariance import scala.runtime.Statics import language.experimental.captureChecking -import caps.unsafe.unsafeAssumePure + /** Iterators are data structures that allow to iterate over a sequence * of elements. They have a `hasNext` method for checking @@ -595,8 +595,7 @@ trait Iterator[+A] extends IterableOnce[A] with IterableOnceOps[A, Iterator, Ite private[this] def nextCur(): Unit = { cur = null - cur = f(self.next()).iterator.unsafeAssumePure - // !!! see explanation in colltest5.CollectionStrawManCC5_1.flatMap why the unsafeAssumePure is needed + cur = f(self.next()).iterator _hasNext = -1 } diff --git a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala index baf89ef54aab..2f7b017a6729 100644 --- a/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala +++ b/scala2-library-cc/src/scala/collection/immutable/LazyListIterable.scala @@ -25,7 +25,6 @@ import scala.runtime.Statics import language.experimental.captureChecking import annotation.unchecked.uncheckedCaptures import caps.untrackedCaptures -import caps.unsafe.unsafeAssumePure /** This class implements an immutable linked list. We call it "lazy" * because it computes its elements only when they are needed. @@ -1042,8 +1041,7 @@ object LazyListIterable extends IterableFactory[LazyListIterable] { var itHasNext = false var rest = restRef // var rest = restRef.elem while (!itHasNext && !rest.isEmpty) { - it = f(rest.head).iterator.unsafeAssumePure - // !!! see explanation in colltest5.CollectionStrawManCC5_1.flatMap why the unsafeAssumePure is needed + it = f(rest.head).iterator itHasNext = it.hasNext if (!itHasNext) { // wait to advance `rest` because `it.next()` can throw rest = rest.tail diff --git a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala index eebc2fbc1417..3ff614bfc556 100644 --- a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala +++ b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala @@ -16,7 +16,6 @@ package mutable import scala.annotation.nowarn import language.experimental.captureChecking import caps.unboxed -import caps.unsafe.unsafeAssumePure /** A `Buffer` is a growable and shrinkable `Seq`. */ trait Buffer[A] @@ -186,10 +185,7 @@ trait IndexedBuffer[A] extends IndexedSeq[A] var i = 0 val s = size val newElems = new Array[(IterableOnce[A]^{f*})](s) - while i < s do - newElems(i) = f(this(i)).unsafeAssumePure - // !!! see explanation in colltest5.CollectionStrawManCC5_1.flatMap why the unsafeAssumePure is needed - i += 1 + while (i < s) { newElems(i) = f(this(i)); i += 1 } clear() i = 0 while (i < s) { ++=(newElems(i)); i += 1 } diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 96859290075a..615e3901a437 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -47,7 +47,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:64:27 -------------------------------------- 64 | val f1: File^{id*} = id(f) // error, since now id(f): File^ | ^^^^^ - | Found: File^ + | Found: File^{id, f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` diff --git a/tests/pos-custom-args/captures/nested-classes-2.scala b/tests/pos-custom-args/captures/nested-classes-2.scala index 17ee3b8f3cb0..744635ee949b 100644 --- a/tests/pos-custom-args/captures/nested-classes-2.scala +++ b/tests/pos-custom-args/captures/nested-classes-2.scala @@ -20,5 +20,5 @@ def test2(x1: (() => Unit), x2: (() => Unit) => Unit) = def test3(y1: (() => Unit), y2: (() => Unit) => Unit) = val cc1: C1^{y1, y2} = C1(y1, y2) val cc2 = cc1.c2(x1, x2) - val cc3: cc1.C2^{cc2} = cc2 + val cc3: cc1.C2^{cc1, x1, x2} = cc2 diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index da15fee9849b..5443758afa72 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -5,7 +5,6 @@ import Predef.{augmentString as _, wrapString as _, *} import scala.reflect.ClassTag import annotation.unchecked.{uncheckedVariance, uncheckedCaptures} import annotation.tailrec -import caps.unsafe.unsafeAssumePure /** A strawman architecture for new collections. It contains some * example collection classes and methods with the intent to expose @@ -556,16 +555,7 @@ object CollectionStrawMan5 { private var myCurrent: Iterator[B]^{this, f} = Iterator.empty private def current = { while (!myCurrent.hasNext && self.hasNext) - myCurrent = f(self.next()).iterator.unsafeAssumePure - // !!! This is unsafe since the iterator's result could return a capability - // depending on the argument self.next() of type A. To exclude that we'd have - // to define f to be of type EX c. A ->{c} IterableOnce[B]^{c}, i.e. `c` may - // not depend on A. But to get there we have to - // - improve the way we express existentials using `^` - // - rework the recheckApplication code to cater for this. Right now - // we cannot do anything since `A` is not always pure. But if we took - // the existential scope of the result into account, this could provide - // a solution. + myCurrent = f(self.next()).iterator myCurrent } def hasNext = current.hasNext From 7db1d43518f6bbb23bbb2d00d7e9a910658d86d3 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jul 2024 10:31:02 +0200 Subject: [PATCH 50/61] Fix special capture set handling in recheckApply, Step 2 revised Step 2: Change the logic. The previous one was unsound. The new logic is makes use of the distinction between regular and unboxed parameters. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 43 ++++++----- tests/neg-custom-args/captures/reaches.check | 72 +++++++------------ tests/neg-custom-args/captures/reaches.scala | 23 +++--- tests/pos-custom-args/captures/Buffer.scala | 22 ++++++ .../captures/opaque-inline-problem.scala | 7 +- 5 files changed, 91 insertions(+), 76 deletions(-) create mode 100644 tests/pos-custom-args/captures/Buffer.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c32028a22471..8df958d7a977 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -547,36 +547,47 @@ class CheckCaptures extends Recheck, SymTransformer: protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = val argType = recheck(arg, formal) - if unboxedArgs.remove(arg) && ccConfig.useUnboxedParams then + if unboxedArgs.contains(arg) && ccConfig.useUnboxedParams then capt.println(i"charging deep capture set of $arg: ${argType} = ${CaptureSet.deepCaptureSet(argType)}") markFree(CaptureSet.deepCaptureSet(argType), arg.srcPos) argType /** A specialized implementation of the apply rule. * - * E |- f: Ra ->Cf Rr^Cr - * E |- a: Ra^Ca + * E |- q: Tq^Cq + * E |- q.f: Ta^Ca ->Cf Tr^Cr + * E |- a: Ta * --------------------- - * E |- f a: Rr^C + * E |- f(a): Tr^C * - * The implementation picks as `C` one of `{f, a}` or `Cr`, depending on the - * outcome of a `mightSubcapture` test. It picks `{f, a}` if this might subcapture Cr - * and Cr otherwise. + * If the function `f` does not have an `@unboxed` parameter, then + * any unboxing it does would be charged to the environment of the function + * so they have to appear in Cq. Since any capabilities of the result of the + * application must already be present in the application, an upper + * approximation of the result capture set is Cq \union Ca, where `Ca` + * is the capture set of the argument. + * If the function `f` does have an `@unboxed` parameter, then it could in addition + * unbox reach capabilities over its formal parameter. Therefore, the approximation + * would be `Cq \union dcs(Ca)` instead. + * If the approximation is known to subcapture the declared result Cr, we pick it for C + * otherwise we pick Cr. */ protected override def recheckApplication(tree: Apply, qualType: Type, funType: MethodType, argTypes: List[Type])(using Context): Type = - Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) match + val appType = Existential.toCap(super.recheckApplication(tree, qualType, funType, argTypes)) + val qualCaptures = qualType.captureSet + val argCaptures = + for (arg, argType) <- tree.args.lazyZip(argTypes) yield + if unboxedArgs.remove(arg) // need to ensure the remove happens, that's why argCaptures is computed even if not needed. + then CaptureSet.deepCaptureSet(argType) + else argType.captureSet + appType match case appType @ CapturingType(appType1, refs) if qualType.exists && !tree.fun.symbol.isConstructor - && !qualType.isBoxedCapturing // TODO: This is not strng enough, we also have - // to exclude existentials in function results - && !argTypes.exists(_.isBoxedCapturing) - && qualType.captureSet.mightSubcapture(refs) - && argTypes.forall(_.captureSet.mightSubcapture(refs)) - => - val callCaptures = tree.args.foldLeft(qualType.captureSet): (cs, arg) => - cs ++ arg.tpe.captureSet + && qualCaptures.mightSubcapture(refs) + && argCaptures.forall(_.mightSubcapture(refs)) => + val callCaptures = argCaptures.foldLeft(qualCaptures)(_ ++ _) appType.derivedCapturingType(appType1, callCaptures) .showing(i"narrow $tree: $appType, refs = $refs, qual-cs = ${qualType.captureSet} = $result", capt) case appType => diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index 615e3901a437..a0797a6ba0a9 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:24:11 -------------------------------------- -24 | cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:22:11 -------------------------------------- +22 | cur = (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | 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:35:7 --------------------------------------- -35 | (() => f.write()) :: Nil // error since {f*} !<: {xs*} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:33:7 --------------------------------------- +33 | (() => f.write()) :: Nil // error | ^^^^^^^^^^^^^^^^^^^^^^^ | Found: List[box () ->{f} Unit] | Required: box List[box () ->{xs*} Unit]^? @@ -15,52 +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:38:6 ------------------------------------------------------------ -38 | 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:45:15 ----------------------------------------------------------- -45 | 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:55:31 ----------------------------------------------------------- -55 | 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:57:6 --------------------------------------- -57 | id(() => f.write()) // error - | ^^^^^^^^^^^^^^^^^^^ - | Found: () => Unit - | Required: () ->? Unit - | - | Note that the universal capability `cap` - | cannot be included in capture set ? - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:64:27 -------------------------------------- -64 | val f1: File^{id*} = id(f) // error, since now id(f): File^ +-- Error: tests/neg-custom-args/captures/reaches.scala:38:31 ----------------------------------------------------------- +38 | val next: () => Unit = cur.head // error + | ^^^^^^^^ + | The expression's type box () => Unit is not allowed to capture the root capability `cap`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/reaches.scala:45:35 ----------------------------------------------------------- +45 | val next: () => Unit = cur.get.head // error + | ^^^^^^^^^^^^ + | The expression's type box () => Unit is not allowed to capture the root capability `cap`. + | This usually means that a capability persists longer than its allowed lifetime. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 -------------------------------------- +62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ | ^^^^^ - | Found: File^{id, f} + | Found: File^{f} | Required: File^{id*} | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/reaches.scala:81:5 ------------------------------------------------------------ -81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error - | ^^^^^^ - | 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 --- Error: tests/neg-custom-args/captures/reaches.scala:81:10 ----------------------------------------------------------- -81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error +-- Error: tests/neg-custom-args/captures/reaches.scala:79:10 ----------------------------------------------------------- +79 | ps.map((x, y) => compose1(x, y)) // error // error | ^ | Local reach capability ps* leaks into capture scope of method mapCompose --- Error: tests/neg-custom-args/captures/reaches.scala:81:13 ----------------------------------------------------------- -81 | ps.map((x, y) => compose1(x, y)) // error: cannot mix cap and * (should work now) // error // error +-- Error: tests/neg-custom-args/captures/reaches.scala:79:13 ----------------------------------------------------------- +79 | ps.map((x, y) => compose1(x, y)) // error // error | ^ | Local reach capability ps* leaks into capture scope of method mapCompose +-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:53:51 -------------------------------------- +53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error + | ^ + | Type argument () -> Unit does not conform to lower bound () => Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index e602d4b34493..08b2feed1bfe 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,5 +1,3 @@ -//> using options -source 3.4 -// (to make sure we use the sealed policy) import caps.unboxed class File: def write(): Unit = ??? @@ -14,14 +12,14 @@ class Ref[T](init: T): def set(y: T) = { x = y } def runAll0(@unboxed xs: List[Proc]): Unit = - var cur: List[() ->{xs*} Unit] = xs // OK, by revised VAR + var cur: List[() ->{xs*} Unit] = xs while cur.nonEmpty do val next: () ->{xs*} Unit = cur.head next() cur = cur.tail: List[() ->{xs*} Unit] usingFile: f => - cur = (() => f.write()) :: Nil // error since {f*} !<: {xs*} + cur = (() => f.write()) :: Nil // error def runAll1(@unboxed xs: List[Proc]): Unit = val cur = Ref[List[() ->{xs*} Unit]](xs) // OK, by revised VAR @@ -32,19 +30,19 @@ def runAll1(@unboxed xs: List[Proc]): Unit = usingFile: f => cur.set: - (() => f.write()) :: Nil // error since {f*} !<: {xs*} + (() => f.write()) :: Nil // error def runAll2(xs: List[Proc]): Unit = - var cur: List[Proc] = xs // error: Illegal type for var + var cur: List[Proc] = xs while cur.nonEmpty do - val next: () => Unit = cur.head + val next: () => Unit = cur.head // error next() cur = cur.tail def runAll3(xs: List[Proc]): Unit = - val cur = Ref[List[Proc]](xs) // error: illegal type for type argument to Ref + val cur = Ref[List[Proc]](xs) while cur.get.nonEmpty do - val next: () => Unit = cur.get.head + val next: () => Unit = cur.get.head // error next() cur.set(cur.get.tail: List[Proc]) @@ -54,7 +52,7 @@ class Id[-A, +B >: A](): def test = val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error usingFile: f => - id(() => f.write()) // error + id(() => f.write()) def attack2 = val id: File^ -> File^ = x => x @@ -78,4 +76,7 @@ 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 * (should work now) // error // error + ps.map((x, y) => compose1(x, y)) // error // error + +def mapCompose2[A](@unboxed ps: List[(A => A, A => A)]): List[A ->{ps*} A] = + ps.map((x, y) => compose1(x, y)) diff --git a/tests/pos-custom-args/captures/Buffer.scala b/tests/pos-custom-args/captures/Buffer.scala new file mode 100644 index 000000000000..17164dccc370 --- /dev/null +++ b/tests/pos-custom-args/captures/Buffer.scala @@ -0,0 +1,22 @@ +import language.experimental.captureChecking + +// Extract of the problem in collection.mutable.Buffers +trait Buffer[A]: + + def apply(i: Int): A = ??? + + def flatMapInPlace(f: A => IterableOnce[A]^): this.type = { + val g = f + val s = 10 + // capture checking: we need the copy since we box/unbox on g* on the next line + // TODO: This looks fishy, need to investigate + // Alternative would be to mark `f` as @unboxed. It's not inferred + // since `^ appears in a function result, not under a box. + val newElems = new Array[(IterableOnce[A]^{f})](s) + var i = 0 + while i < s do + val x = g(this(i)) + newElems(i) = x + i += 1 + this + } \ No newline at end of file diff --git a/tests/pos-custom-args/captures/opaque-inline-problem.scala b/tests/pos-custom-args/captures/opaque-inline-problem.scala index de9a4437becd..ed482e3fc164 100644 --- a/tests/pos-custom-args/captures/opaque-inline-problem.scala +++ b/tests/pos-custom-args/captures/opaque-inline-problem.scala @@ -4,9 +4,8 @@ trait Async extends caps.Capability: object Async: inline def current(using async: Async): async.type = async opaque type Spawn <: Async = Async - def blocking[T](f: Spawn => T): T = ??? + def blocking[T](f: Spawn ?=> T): T = ??? def main() = - Async.blocking: spawn => - val c = Async.current(using spawn) - val a = c.group + Async.blocking: + val a = Async.current.group \ No newline at end of file From c1036bf469f91dfabd8247c08c82bdf0dc6b075f Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jul 2024 16:17:58 +0200 Subject: [PATCH 51/61] Fix captureSet computations for false reach capabilities Fix the capture set computation of a type T @reachCapability where T is not a singleton captureRef. Such types can be the results of typemaps. The capture set in this case should be the deep capture set of T. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 3 ++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 10 ++++++++-- .../tools/dotc/printing/RefinedPrinter.scala | 4 ++-- .../captures/leak-problem-2.check | 7 +++++++ .../captures/leak-problem-2.scala | 9 +++++++++ tests/neg-custom-args/captures/reaches.check | 18 ++++++++++++------ 6 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 tests/neg-custom-args/captures/leak-problem-2.check create mode 100644 tests/neg-custom-args/captures/leak-problem-2.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index af17cdc6b11a..dad063f43c87 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -204,8 +204,9 @@ extension (tp: Type) case tp: TypeParamRef => tp.derivesFrom(defn.Caps_CapSet) case AnnotatedType(parent, annot) => - annot.symbol == defn.ReachCapabilityAnnot + (annot.symbol == defn.ReachCapabilityAnnot || annot.symbol == defn.MaybeCapabilityAnnot + ) && parent.isTrackableRef case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index aa65db2375e8..0aab33d31fdb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1057,7 +1057,7 @@ object CaptureSet: /** Capture set of a type */ def ofType(tp: Type, followResult: Boolean)(using Context): CaptureSet = def recur(tp: Type): CaptureSet = trace(i"ofType $tp, ${tp.getClass} $followResult", show = true): - tp.dealias match + tp.dealiasKeepAnnots match case tp: TermRef => tp.captureSet case tp: TermParamRef => @@ -1068,6 +1068,12 @@ object CaptureSet: empty case CapturingType(parent, refs) => recur(parent) ++ refs + case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) => + parent match + case parent: SingletonCaptureRef if parent.isTrackableRef => + tp.singletonCaptureSet + case _ => + CaptureSet.deepCaptureSet(parent) case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult => ofType(tpd.parent, followResult = false) // pick up capture set from parent type ++ (recur(rinfo.resType) // add capture set of result @@ -1083,7 +1089,7 @@ object CaptureSet: case tparams @ (LambdaParam(tl, _) :: _) => cs.substParams(tl, args) case _ => cs case tp: TypeProxy => - recur(tp.underlying) + recur(tp.superType) case AndType(tp1, tp2) => recur(tp1) ** recur(tp2) case OrType(tp1, tp2) => diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 18a1647572ef..3bdc67cb91ff 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -27,7 +27,7 @@ import config.{Config, Feature} import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} -import cc.{CaptureSet, CapturingType, toCaptureSet, IllegalCaptureRef, isRetains} +import cc.{CaptureSet, CapturingType, toCaptureSet, IllegalCaptureRef, isRetains, ReachCapability, MaybeCapability} import dotty.tools.dotc.parsing.JavaParsers class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { @@ -330,7 +330,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { "?" ~ (("(ignored: " ~ toText(ignored) ~ ")") provided printDebug) case tp @ PolyProto(targs, resType) => "[applied to [" ~ toTextGlobal(targs, ", ") ~ "] returning " ~ toText(resType) - case tp: AnnotatedType if tp.isReach || tp.isMaybe => + case ReachCapability(_) | MaybeCapability(_) => toTextCaptureRef(tp) case _ => super.toText(tp) diff --git a/tests/neg-custom-args/captures/leak-problem-2.check b/tests/neg-custom-args/captures/leak-problem-2.check new file mode 100644 index 000000000000..42282ff7f9f4 --- /dev/null +++ b/tests/neg-custom-args/captures/leak-problem-2.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/leak-problem-2.scala:8:8 --------------------------------- +8 | = race(Seq(src1, src2)) // error + | ^^^^^^^^^^^^^^^^^^^^^ + | Found: Source[box T^?]^{src1, src2} + | Required: Source[T] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/leak-problem-2.scala b/tests/neg-custom-args/captures/leak-problem-2.scala new file mode 100644 index 000000000000..82465924b852 --- /dev/null +++ b/tests/neg-custom-args/captures/leak-problem-2.scala @@ -0,0 +1,9 @@ +import language.experimental.captureChecking + +trait Source[+T] + +def race[T](@caps.unboxed sources: Seq[Source[T]^]): Source[T]^{sources*} = ??? + +def raceTwo[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} + = race(Seq(src1, src2)) // error + // this compiled and returned a Source that does not capture src1 and src2. \ No newline at end of file diff --git a/tests/neg-custom-args/captures/reaches.check b/tests/neg-custom-args/captures/reaches.check index a0797a6ba0a9..aa45c738dcc5 100644 --- a/tests/neg-custom-args/captures/reaches.check +++ b/tests/neg-custom-args/captures/reaches.check @@ -25,6 +25,18 @@ | ^^^^^^^^^^^^ | The expression's type box () => Unit is not allowed to capture the root capability `cap`. | This usually means that a capability persists longer than its allowed lifetime. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:53:2 --------------------------------------- +53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error + | ^ + | Found: box () => Unit + | Required: () => Unit + | + | Note that box () => Unit cannot be box-converted to () => Unit + | since at least one of their capture sets contains the root capability `cap` +54 | usingFile: f => +55 | id(() => f.write()) + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:62:27 -------------------------------------- 62 | val f1: File^{id*} = id(f) // error, since now id(f): File^ | ^^^^^ @@ -40,9 +52,3 @@ 79 | ps.map((x, y) => compose1(x, y)) // error // error | ^ | Local reach capability ps* leaks into capture scope of method mapCompose --- [E057] Type Mismatch Error: tests/neg-custom-args/captures/reaches.scala:53:51 -------------------------------------- -53 | val id: Id[Proc, Proc] = new Id[Proc, () -> Unit] // error - | ^ - | Type argument () -> Unit does not conform to lower bound () => Unit - | - | longer explanation available when compiling with `-explain` From 5a351600b854306ac7038b50e8e66aa21d8270b1 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jul 2024 16:54:06 +0200 Subject: [PATCH 52/61] Tweak deep capture set computations --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 16 +++++++++++++++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 9 +++++++-- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 6 +++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index dad063f43c87..46c346011c49 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -223,6 +223,20 @@ extension (tp: Type) case tp: SingletonCaptureRef => tp.captureSetOfInfo case _ => CaptureSet.ofType(tp, followResult = false) + /** The deep capture set of a type. + * For singleton capabilities `x` and reach capabilities `x*`, this is `{x*}`, provided + * the underlying capture set resulting from traversing the type is non-empty. + * For other types this is the union of all covariant capture sets embedded + * in the type, as computed by `CaptureSet.ofTypeDeeply`. + */ + def deepCaptureSet(using Context): CaptureSet = + val dcs = CaptureSet.ofTypeDeeply(tp) + if dcs.isAlwaysEmpty then dcs + else tp match + case tp @ ReachCapability(_) => tp.singletonCaptureSet + case tp: SingletonCaptureRef => tp.reach.singletonCaptureSet + case _ => dcs + /** A type capturing `ref` */ def capturing(ref: CaptureRef)(using Context): Type = if tp.captureSet.accountsFor(ref) then tp @@ -587,7 +601,7 @@ extension (sym: Symbol) } def hasTrackedParts(using Context): Boolean = - !CaptureSet.deepCaptureSet(sym.info).isAlwaysEmpty + !CaptureSet.ofTypeDeeply(sym.info).isAlwaysEmpty extension (tp: AnnotatedType) /** Is this a boxed capturing type? */ diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 0aab33d31fdb..87638b078040 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1073,7 +1073,7 @@ object CaptureSet: case parent: SingletonCaptureRef if parent.isTrackableRef => tp.singletonCaptureSet case _ => - CaptureSet.deepCaptureSet(parent) + CaptureSet.ofTypeDeeply(parent) case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult => ofType(tpd.parent, followResult = false) // pick up capture set from parent type ++ (recur(rinfo.resType) // add capture set of result @@ -1099,12 +1099,17 @@ object CaptureSet: recur(tp) //.showing(i"capture set of $tp = $result", captDebug) - def deepCaptureSet(tp: Type)(using Context): CaptureSet = + /** The deep capture set of a type is the union of all covariant occurrences of + * capture sets. Nested existential sets are approximated with `cap`. + */ + def ofTypeDeeply(tp: Type)(using Context): CaptureSet = val collect = new TypeAccumulator[CaptureSet]: def apply(cs: CaptureSet, t: Type) = t.dealias match case t @ CapturingType(p, cs1) => val cs2 = apply(cs, p) if variance > 0 then cs2 ++ cs1 else cs2 + case t @ Existential(_, _) => + apply(cs, Existential.toCap(t)) case _ => foldOver(cs, t) collect(CaptureSet.empty, tp) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 8df958d7a977..10668a6fbee9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -548,8 +548,8 @@ class CheckCaptures extends Recheck, SymTransformer: def recheckArg(arg: Tree, formal: Type)(using Context): Type = val argType = recheck(arg, formal) if unboxedArgs.contains(arg) && ccConfig.useUnboxedParams then - capt.println(i"charging deep capture set of $arg: ${argType} = ${CaptureSet.deepCaptureSet(argType)}") - markFree(CaptureSet.deepCaptureSet(argType), arg.srcPos) + capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") + markFree(argType.deepCaptureSet, arg.srcPos) argType /** A specialized implementation of the apply rule. @@ -579,7 +579,7 @@ class CheckCaptures extends Recheck, SymTransformer: val argCaptures = for (arg, argType) <- tree.args.lazyZip(argTypes) yield if unboxedArgs.remove(arg) // need to ensure the remove happens, that's why argCaptures is computed even if not needed. - then CaptureSet.deepCaptureSet(argType) + then argType.deepCaptureSet else argType.captureSet appType match case appType @ CapturingType(appType1, refs) From 082214e3a5473c2966d6f478afa6abfb73bc40ed Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jul 2024 17:02:10 +0200 Subject: [PATCH 53/61] Make the unboxed parameter scheme standard Drop the config option that enables it. --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 2 -- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 46c346011c49..116b35e34aea 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -40,8 +40,6 @@ object ccConfig: */ inline val handleEtaExpansionsSpecially = false - val useUnboxedParams = true - /** If true, use existential capture set variables */ def useExistentials(using Context) = Feature.sourceVersion.stable.isAtLeast(SourceVersion.`3.5`) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 10668a6fbee9..c53f53fa43ba 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -402,9 +402,8 @@ class CheckCaptures extends Recheck, SymTransformer: // by their underlying capture set, which cannot be universal. // Reach capabilities of @unboxed parameters are exempted. val cs = CaptureSet.ofInfo(c) - if ccConfig.useUnboxedParams then - cs.disallowRootCapability: () => - report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) + cs.disallowRootCapability: () => + report.error(em"Local reach capability $c leaks into capture scope of ${env.ownerString}", pos) checkSubset(cs, env.captured, pos, provenance(env)) isVisible case ref: ThisType => isVisibleFromEnv(ref.cls) @@ -547,7 +546,7 @@ class CheckCaptures extends Recheck, SymTransformer: protected override def recheckArg(arg: Tree, formal: Type)(using Context): Type = val argType = recheck(arg, formal) - if unboxedArgs.contains(arg) && ccConfig.useUnboxedParams then + if unboxedArgs.contains(arg) then capt.println(i"charging deep capture set of $arg: ${argType} = ${argType.deepCaptureSet}") markFree(argType.deepCaptureSet, arg.srcPos) argType From afcbdcbba3e259b0a6101772155a933c01b46f91 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jul 2024 17:07:44 +0200 Subject: [PATCH 54/61] Rename @unboxed --> @unbox --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 4 ++-- compiler/src/dotty/tools/dotc/core/Definitions.scala | 2 +- library/src/scala/caps.scala | 5 ++++- .../src/scala/collection/mutable/Buffer.scala | 5 ++--- tests/neg-custom-args/captures/i15749a.scala | 4 ++-- tests/neg-custom-args/captures/leak-problem-2.scala | 2 +- tests/neg-custom-args/captures/reaches.scala | 8 ++++---- tests/neg/i20503.scala | 4 ++-- tests/neg/leak-problem-unboxed.scala | 6 +++--- tests/pos-custom-args/captures/Buffer.scala | 2 +- tests/pos-custom-args/captures/dep-reach.scala | 6 +++--- tests/pos-custom-args/captures/reaches.scala | 4 ++-- tests/pos/cc-poly-source-capability.scala | 4 ++-- tests/pos/cc-poly-source.scala | 4 ++-- tests/pos/gears-probem-1.scala | 4 ++-- tests/pos/i18699.scala | 4 ++-- tests/pos/reach-capability.scala | 4 ++-- tests/pos/reach-problem.scala | 4 ++-- 18 files changed, 39 insertions(+), 37 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c53f53fa43ba..a83c32eb1284 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -395,7 +395,7 @@ class CheckCaptures extends Recheck, SymTransformer: val refOwner = refSym.owner val isVisible = isVisibleFromEnv(refOwner) if !isVisible && c.isReach && refSym.is(Param) && refOwner == env.owner then - if refSym.hasAnnotation(defn.UnboxedAnnot) then + if refSym.hasAnnotation(defn.UnboxAnnot) then capt.println(i"exempt: $ref in $refOwner") else // Reach capabilities that go out of scope have to be approximated @@ -425,7 +425,7 @@ class CheckCaptures extends Recheck, SymTransformer: val unboxedParamNames = meth.rawParamss.flatMap: params => params.collect: - case param if param.hasAnnotation(defn.UnboxedAnnot) => + case param if param.hasAnnotation(defn.UnboxAnnot) => param.name .toSet diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 8c2f448a4a5e..78a9ea360279 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1050,7 +1050,7 @@ class Definitions { @tu lazy val ExperimentalAnnot: ClassSymbol = requiredClass("scala.annotation.experimental") @tu lazy val ThrowsAnnot: ClassSymbol = requiredClass("scala.throws") @tu lazy val TransientAnnot: ClassSymbol = requiredClass("scala.transient") - @tu lazy val UnboxedAnnot: ClassSymbol = requiredClass("scala.caps.unboxed") + @tu lazy val UnboxAnnot: ClassSymbol = requiredClass("scala.caps.unbox") @tu lazy val UncheckedAnnot: ClassSymbol = requiredClass("scala.unchecked") @tu lazy val UncheckedStableAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedStable") @tu lazy val UncheckedVarianceAnnot: ClassSymbol = requiredClass("scala.annotation.unchecked.uncheckedVariance") diff --git a/library/src/scala/caps.scala b/library/src/scala/caps.scala index 5e675f7d4341..1416a7b35f83 100644 --- a/library/src/scala/caps.scala +++ b/library/src/scala/caps.scala @@ -39,7 +39,10 @@ import annotation.{experimental, compileTimeOnly} */ final class untrackedCaptures extends annotation.StaticAnnotation - final class unboxed extends annotation.StaticAnnotation + /** This should go into annotations. For now it is here, so that we + * can experiment with it quickly between minor releases + */ + final class unbox extends annotation.StaticAnnotation object unsafe: diff --git a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala index 3ff614bfc556..27e5a8997d48 100644 --- a/scala2-library-cc/src/scala/collection/mutable/Buffer.scala +++ b/scala2-library-cc/src/scala/collection/mutable/Buffer.scala @@ -15,7 +15,6 @@ package mutable import scala.annotation.nowarn import language.experimental.captureChecking -import caps.unboxed /** A `Buffer` is a growable and shrinkable `Seq`. */ trait Buffer[A] @@ -180,11 +179,11 @@ trait IndexedBuffer[A] extends IndexedSeq[A] override def iterableFactory: SeqFactory[IndexedBuffer] = IndexedBuffer - def flatMapInPlace(@unboxed f: A => IterableOnce[A]^): this.type = { + def flatMapInPlace(f: A => IterableOnce[A]^): this.type = { // There's scope for a better implementation which copies elements in place. var i = 0 val s = size - val newElems = new Array[(IterableOnce[A]^{f*})](s) + val newElems = new Array[(IterableOnce[A]^{f})](s) while (i < s) { newElems(i) = f(this(i)); i += 1 } clear() i = 0 diff --git a/tests/neg-custom-args/captures/i15749a.scala b/tests/neg-custom-args/captures/i15749a.scala index 109a73b2b130..57fca27fae66 100644 --- a/tests/neg-custom-args/captures/i15749a.scala +++ b/tests/neg-custom-args/captures/i15749a.scala @@ -1,5 +1,5 @@ import caps.cap -import caps.unboxed +import caps.unbox class Unit object u extends Unit @@ -18,7 +18,7 @@ def test = def force[A](thunk: Unit ->{cap} A): A = thunk(u) - def forceWrapper[A](@unboxed mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = + def forceWrapper[A](@unbox mx: Wrapper[Unit ->{cap} A]): Wrapper[A] = // Γ ⊢ mx: Wrapper[□ {cap} Unit => A] // `force` should be typed as ∀(□ {cap} Unit -> A) A, but it can not strictMap[Unit ->{mx*} A, A](mx)(t => force[A](t)) // error // should work diff --git a/tests/neg-custom-args/captures/leak-problem-2.scala b/tests/neg-custom-args/captures/leak-problem-2.scala index 82465924b852..08a3a6c2d9ca 100644 --- a/tests/neg-custom-args/captures/leak-problem-2.scala +++ b/tests/neg-custom-args/captures/leak-problem-2.scala @@ -2,7 +2,7 @@ import language.experimental.captureChecking trait Source[+T] -def race[T](@caps.unboxed sources: Seq[Source[T]^]): Source[T]^{sources*} = ??? +def race[T](@caps.unbox sources: Seq[Source[T]^]): Source[T]^{sources*} = ??? def raceTwo[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} = race(Seq(src1, src2)) // error diff --git a/tests/neg-custom-args/captures/reaches.scala b/tests/neg-custom-args/captures/reaches.scala index 08b2feed1bfe..c2d8001e2a7c 100644 --- a/tests/neg-custom-args/captures/reaches.scala +++ b/tests/neg-custom-args/captures/reaches.scala @@ -1,4 +1,4 @@ -import caps.unboxed +import caps.unbox class File: def write(): Unit = ??? @@ -11,7 +11,7 @@ class Ref[T](init: T): def get: T = x def set(y: T) = { x = y } -def runAll0(@unboxed xs: List[Proc]): Unit = +def runAll0(@unbox xs: List[Proc]): Unit = var cur: List[() ->{xs*} Unit] = xs while cur.nonEmpty do val next: () ->{xs*} Unit = cur.head @@ -21,7 +21,7 @@ def runAll0(@unboxed xs: List[Proc]): Unit = usingFile: f => cur = (() => f.write()) :: Nil // error -def runAll1(@unboxed xs: List[Proc]): Unit = +def runAll1(@unbox xs: List[Proc]): Unit = val cur = Ref[List[() ->{xs*} Unit]](xs) // OK, by revised VAR while cur.get.nonEmpty do val next: () ->{xs*} Unit = cur.get.head @@ -78,5 +78,5 @@ def compose1[A, B, C](f: A => B, g: B => C): A ->{f, g} C = def mapCompose[A](ps: List[(A => A, A => A)]): List[A ->{ps*} A] = ps.map((x, y) => compose1(x, y)) // error // error -def mapCompose2[A](@unboxed ps: List[(A => A, A => A)]): List[A ->{ps*} A] = +def mapCompose2[A](@unbox ps: List[(A => A, A => A)]): List[A ->{ps*} A] = ps.map((x, y) => compose1(x, y)) diff --git a/tests/neg/i20503.scala b/tests/neg/i20503.scala index 463e4e3f9686..3fb0573f6c2f 100644 --- a/tests/neg/i20503.scala +++ b/tests/neg/i20503.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking -import caps.unboxed +import caps.unbox class List[+A]: def head: A = ??? @@ -8,7 +8,7 @@ class List[+A]: def foreach[U](f: A => U): Unit = ??? def nonEmpty: Boolean = ??? -def runOps(@unboxed ops: List[() => Unit]): Unit = +def runOps(@unbox ops: List[() => Unit]): Unit = // See i20156, due to limitation in expressiveness of current system, // we could map over the list of impure elements. OK with existentials. ops.foreach(op => op()) diff --git a/tests/neg/leak-problem-unboxed.scala b/tests/neg/leak-problem-unboxed.scala index 8591145583e2..7de3d84bfcca 100644 --- a/tests/neg/leak-problem-unboxed.scala +++ b/tests/neg/leak-problem-unboxed.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking -import caps.unboxed +import caps.unbox // Some capabilities that should be used locally trait Async: @@ -9,12 +9,12 @@ def usingAsync[X](op: Async^ => X): X = ??? case class Box[+T](get: T) -def useBoxedAsync(@unboxed x: Box[Async^]): Unit = +def useBoxedAsync(@unbox x: Box[Async^]): Unit = val t0 = x val t1 = t0.get // ok t1.read() -def useBoxedAsync1(@unboxed x: Box[Async^]): Unit = x.get.read() // ok +def useBoxedAsync1(@unbox x: Box[Async^]): Unit = x.get.read() // ok def test(): Unit = diff --git a/tests/pos-custom-args/captures/Buffer.scala b/tests/pos-custom-args/captures/Buffer.scala index 17164dccc370..2412e5b388ca 100644 --- a/tests/pos-custom-args/captures/Buffer.scala +++ b/tests/pos-custom-args/captures/Buffer.scala @@ -10,7 +10,7 @@ trait Buffer[A]: val s = 10 // capture checking: we need the copy since we box/unbox on g* on the next line // TODO: This looks fishy, need to investigate - // Alternative would be to mark `f` as @unboxed. It's not inferred + // Alternative would be to mark `f` as @unbox. It's not inferred // since `^ appears in a function result, not under a box. val newElems = new Array[(IterableOnce[A]^{f})](s) var i = 0 diff --git a/tests/pos-custom-args/captures/dep-reach.scala b/tests/pos-custom-args/captures/dep-reach.scala index 177422565736..c81197aa738d 100644 --- a/tests/pos-custom-args/captures/dep-reach.scala +++ b/tests/pos-custom-args/captures/dep-reach.scala @@ -1,10 +1,10 @@ -import caps.unboxed +import caps.unbox object Test: class C type Proc = () => Unit def f(c: C^, d: C^): () ->{c, d} Unit = - def foo(@unboxed xs: Proc*): () ->{xs*} Unit = + def foo(@unbox xs: Proc*): () ->{xs*} Unit = xs.head val a: () ->{c} Unit = () => () val b: () ->{d} Unit = () => () @@ -13,7 +13,7 @@ object Test: def g(c: C^, d: C^): () ->{c, d} Unit = - def foo(@unboxed xs: Seq[() => Unit]): () ->{xs*} Unit = + def foo(@unbox xs: Seq[() => Unit]): () ->{xs*} Unit = xs.head val a: () ->{c} Unit = () => () diff --git a/tests/pos-custom-args/captures/reaches.scala b/tests/pos-custom-args/captures/reaches.scala index 976fadc4b649..ab0da9b67d18 100644 --- a/tests/pos-custom-args/captures/reaches.scala +++ b/tests/pos-custom-args/captures/reaches.scala @@ -1,4 +1,4 @@ -import caps.unboxed +import caps.unbox class C def f(xs: List[C^]) = @@ -22,7 +22,7 @@ extension [A](x: A) def :: (xs: List[A]): List[A] = ??? object Nil extends List[Nothing] -def runAll(@unboxed xs: List[Proc]): Unit = +def runAll(@unbox xs: List[Proc]): Unit = var cur: List[() ->{xs*} Unit] = xs // OK, by revised VAR while cur.nonEmpty do val next: () ->{xs*} Unit = cur.head diff --git a/tests/pos/cc-poly-source-capability.scala b/tests/pos/cc-poly-source-capability.scala index 0f61e98e5068..3b6c0bde1398 100644 --- a/tests/pos/cc-poly-source-capability.scala +++ b/tests/pos/cc-poly-source-capability.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import annotation.experimental import caps.{CapSet, Capability} -import caps.unboxed +import caps.unbox @experimental object Test: @@ -18,7 +18,7 @@ import caps.unboxed def allListeners: Set[Listener^{X^}] = listeners - def test1(async1: Async, @unboxed others: List[Async]) = + def test1(async1: Async, @unbox others: List[Async]) = val src = Source[CapSet^{async1, others*}] val lst1 = listener(async1) val lsts = others.map(listener) diff --git a/tests/pos/cc-poly-source.scala b/tests/pos/cc-poly-source.scala index 09b4a3024e3c..4cfbbaa06936 100644 --- a/tests/pos/cc-poly-source.scala +++ b/tests/pos/cc-poly-source.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import annotation.experimental import caps.{CapSet, Capability} -import caps.unboxed +import caps.unbox @experimental object Test: @@ -25,7 +25,7 @@ import caps.unboxed val ls = src.allListeners val _: Set[Listener^{lbl1, lbl2}] = ls - def test2(@unboxed lbls: List[Label^]) = + def test2(@unbox lbls: List[Label^]) = def makeListener(lbl: Label^): Listener^{lbl} = ??? val listeners = lbls.map(makeListener) val src = Source[CapSet^{lbls*}] diff --git a/tests/pos/gears-probem-1.scala b/tests/pos/gears-probem-1.scala index c683db9ce01d..ab71616b72fc 100644 --- a/tests/pos/gears-probem-1.scala +++ b/tests/pos/gears-probem-1.scala @@ -1,5 +1,5 @@ import language.experimental.captureChecking -import caps.unboxed +import caps.unbox trait Future[+T]: def await: T @@ -17,7 +17,7 @@ class Result[+T, +E]: case class Err[+E](e: E) extends Result[Nothing, E] case class Ok[+T](x: T) extends Result[T, Nothing] -extension [T](@unboxed fs: Seq[Future[T]^]) +extension [T](@unbox fs: Seq[Future[T]^]) def awaitAll = val collector//: Collector[T]{val futures: Seq[Future[T]^{fs*}]} = Collector(fs) diff --git a/tests/pos/i18699.scala b/tests/pos/i18699.scala index 54390f6bdd71..1937d7dca8c5 100644 --- a/tests/pos/i18699.scala +++ b/tests/pos/i18699.scala @@ -1,9 +1,9 @@ import language.experimental.captureChecking -import caps.unboxed +import caps.unbox trait Cap: def use: Int = 42 -def test2(@unboxed cs: List[Cap^]): Unit = +def test2(@unbox cs: List[Cap^]): Unit = val t0: Cap^{cs*} = cs.head // error var t1: Cap^{cs*} = cs.head // error diff --git a/tests/pos/reach-capability.scala b/tests/pos/reach-capability.scala index 5ad7534061b1..50ea479ec3c1 100644 --- a/tests/pos/reach-capability.scala +++ b/tests/pos/reach-capability.scala @@ -1,7 +1,7 @@ import language.experimental.captureChecking import annotation.experimental import caps.Capability -import caps.unboxed +import caps.unbox @experimental object Test2: @@ -12,7 +12,7 @@ import caps.unboxed class Listener - def test2(@unboxed lbls: List[Label]) = + def test2(@unbox lbls: List[Label]) = def makeListener(lbl: Label): Listener^{lbl} = ??? val listeners = lbls.map(makeListener) // should work diff --git a/tests/pos/reach-problem.scala b/tests/pos/reach-problem.scala index 29d46687a219..d6b7b79011a6 100644 --- a/tests/pos/reach-problem.scala +++ b/tests/pos/reach-problem.scala @@ -1,11 +1,11 @@ import language.experimental.captureChecking -import caps.unboxed +import caps.unbox class Box[T](items: Seq[T^]): def getOne: T^{items*} = ??? object Box: - def getOne[T](@unboxed items: Seq[T^]): T^{items*} = + def getOne[T](@unbox items: Seq[T^]): T^{items*} = val bx = Box(items) bx.getOne /* From 0b22948b91aa6b8e0651aa576cf7e40af6a02483 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Jul 2024 17:15:38 +0200 Subject: [PATCH 55/61] Another test --- tests/pos/Buffer.scala | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/pos/Buffer.scala diff --git a/tests/pos/Buffer.scala b/tests/pos/Buffer.scala new file mode 100644 index 000000000000..2412e5b388ca --- /dev/null +++ b/tests/pos/Buffer.scala @@ -0,0 +1,22 @@ +import language.experimental.captureChecking + +// Extract of the problem in collection.mutable.Buffers +trait Buffer[A]: + + def apply(i: Int): A = ??? + + def flatMapInPlace(f: A => IterableOnce[A]^): this.type = { + val g = f + val s = 10 + // capture checking: we need the copy since we box/unbox on g* on the next line + // TODO: This looks fishy, need to investigate + // Alternative would be to mark `f` as @unbox. It's not inferred + // since `^ appears in a function result, not under a box. + val newElems = new Array[(IterableOnce[A]^{f})](s) + var i = 0 + while i < s do + val x = g(this(i)) + newElems(i) = x + i += 1 + this + } \ No newline at end of file From 91cbca8375fa3a9b09088862535fd9ae10f2d529 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Jul 2024 12:01:55 +0200 Subject: [PATCH 56/61] Check that overrides don't change the @unbox status of their parameters --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 17 ++++++++++++++- .../dotty/tools/dotc/typer/RefChecks.scala | 3 +++ .../captures/unbox-overrides.check | 21 +++++++++++++++++++ .../captures/unbox-overrides.scala | 15 +++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 tests/neg-custom-args/captures/unbox-overrides.check create mode 100644 tests/neg-custom-args/captures/unbox-overrides.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a83c32eb1284..30dfe8f8881c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -22,7 +22,7 @@ import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult import CCState.* import StdNames.nme import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind} -import reporting.{trace, Message} +import reporting.{trace, Message, OverrideError} /** The capture checker */ object CheckCaptures: @@ -1271,6 +1271,21 @@ class CheckCaptures extends Recheck, SymTransformer: !setup.isPreCC(overriding) && !setup.isPreCC(overridden) override def checkInheritedTraitParameters: Boolean = false + + /** Check that overrides don't change the @unbox status of their parameters */ + override def additionalChecks(member: Symbol, other: Symbol)(using Context): Unit = + for + (params1, params2) <- member.rawParamss.lazyZip(other.rawParamss) + (param1, param2) <- params1.lazyZip(params2) + do + if param1.hasAnnotation(defn.UnboxAnnot) != param2.hasAnnotation(defn.UnboxAnnot) then + report.error( + OverrideError( + i"has a parameter ${param1.name} with different @unbox status than the corresponding parameter in the overridden definition", + self, member, other, self.memberInfo(member), self.memberInfo(other) + ), + if member.owner == clazz then member.srcPos else clazz.srcPos + ) end OverridingPairsCheckerCC def traverse(t: Tree)(using Context) = diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index cb1aea27c444..2601bfb42074 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -250,12 +250,15 @@ object RefChecks { */ def needsCheck(overriding: Symbol, overridden: Symbol)(using Context): Boolean = true + protected def additionalChecks(overriding: Symbol, overridden: Symbol)(using Context): Unit = () + private val subtypeChecker: (Type, Type) => Context ?=> Boolean = this.checkSubType def checkAll(checkOverride: ((Type, Type) => Context ?=> Boolean, Symbol, Symbol) => Unit) = while hasNext do if needsCheck(overriding, overridden) then checkOverride(subtypeChecker, overriding, overridden) + additionalChecks(overriding, overridden) next() // The OverridingPairs cursor does assume that concrete overrides abstract diff --git a/tests/neg-custom-args/captures/unbox-overrides.check b/tests/neg-custom-args/captures/unbox-overrides.check new file mode 100644 index 000000000000..b9a3be7bffbc --- /dev/null +++ b/tests/neg-custom-args/captures/unbox-overrides.check @@ -0,0 +1,21 @@ +-- [E164] Declaration Error: tests/neg-custom-args/captures/unbox-overrides.scala:8:6 ---------------------------------- +8 | def foo(x: C): C // error + | ^ + |error overriding method foo in trait A of type (x: C): C; + | method foo of type (x: C): C has a parameter x with different @unbox status than the corresponding parameter in the overridden definition + | + | longer explanation available when compiling with `-explain` +-- [E164] Declaration Error: tests/neg-custom-args/captures/unbox-overrides.scala:9:6 ---------------------------------- +9 | def bar(@unbox x: C): C // error + | ^ + |error overriding method bar in trait A of type (x: C): C; + | method bar of type (x: C): C has a parameter x with different @unbox status than the corresponding parameter in the overridden definition + | + | longer explanation available when compiling with `-explain` +-- [E164] Declaration Error: tests/neg-custom-args/captures/unbox-overrides.scala:15:15 -------------------------------- +15 |abstract class C extends A[C], B2 // error + | ^ + |error overriding method foo in trait A of type (x: C): C; + | method foo in trait B2 of type (x: C): C has a parameter x with different @unbox status than the corresponding parameter in the overridden definition + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/unbox-overrides.scala b/tests/neg-custom-args/captures/unbox-overrides.scala new file mode 100644 index 000000000000..5abb5013bfbe --- /dev/null +++ b/tests/neg-custom-args/captures/unbox-overrides.scala @@ -0,0 +1,15 @@ +import caps.unbox + +trait A[X]: + def foo(@unbox x: X): X + def bar(x: X): X + +trait B extends A[C]: + def foo(x: C): C // error + def bar(@unbox x: C): C // error + +trait B2: + def foo(x: C): C + def bar(@unbox x: C): C + +abstract class C extends A[C], B2 // error From 54828c7a2d8b7ffda1a12155a89e3bb6be9f26be Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 13 Jul 2024 23:01:54 +0200 Subject: [PATCH 57/61] Fix lubs over capturing types Also, fix Seq rechecking so that elements are always box adapted --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 3 +++ .../dotty/tools/dotc/core/TypeComparer.scala | 5 +++++ .../dotty/tools/dotc/transform/Recheck.scala | 13 ++++++++---- tests/neg-custom-args/captures/lazylist.check | 10 ++++----- tests/neg-custom-args/captures/lazylist.scala | 2 +- tests/neg-custom-args/captures/lubs.check | 21 +++++++++++++++++++ tests/neg-custom-args/captures/lubs.scala | 20 ++++++++++++++++++ .../captures/spread-problem.check | 14 +++++++++++++ .../captures/spread-problem.scala | 11 ++++++++++ 9 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 tests/neg-custom-args/captures/lubs.check create mode 100644 tests/neg-custom-args/captures/lubs.scala create mode 100644 tests/neg-custom-args/captures/spread-problem.check create mode 100644 tests/neg-custom-args/captures/spread-problem.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 30dfe8f8881c..667d91a10330 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -719,6 +719,9 @@ class CheckCaptures extends Recheck, SymTransformer: openClosures = openClosures.tail end recheckClosureBlock + override def seqLiteralElemProto(tree: SeqLiteral, pt: Type, declared: Type)(using Context) = + super.seqLiteralElemProto(tree, pt, declared).boxed + /** Maps mutable variables to the symbols that capture them (in the * CheckCaptures sense, i.e. symbol is referred to from a different method * than the one it is defined in). diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e63aab484605..c8e00686e62b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2751,6 +2751,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } case tp1: TypeVar if tp1.isInstantiated => lub(tp1.underlying, tp2, isSoft = isSoft) + case CapturingType(parent1, refs1) => + if tp1.isBoxCompatibleWith(tp2) then + tp1.derivedCapturingType(lub(parent1, tp2, isSoft = isSoft), refs1) + else // TODO: Analyze cases where they are not box compatible + NoType case tp1: AnnotatedType if !tp1.isRefining => lub(tp1.underlying, tp2, isSoft = isSoft) case _ => diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 4b8a8f072774..03f0001110d3 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -23,6 +23,7 @@ import reporting.trace import annotation.constructorOnly import cc.CaptureSet.IdempotentCaptRefMap import annotation.tailrec +import dotty.tools.dotc.cc.boxed object Recheck: import tpd.* @@ -438,12 +439,16 @@ abstract class Recheck extends Phase, SymTransformer: val finalizerType = recheck(tree.finalizer, defn.UnitType) TypeComparer.lub(bodyType :: casesTypes) + def seqLiteralElemProto(tree: SeqLiteral, pt: Type, declared: Type)(using Context): Type = + declared.orElse: + pt.stripNull().elemType match + case NoType => WildcardType + case bounds: TypeBounds => WildcardType(bounds) + case elemtp => elemtp + def recheckSeqLiteral(tree: SeqLiteral, pt: Type)(using Context): Type = - val elemProto = pt.stripNull().elemType match - case NoType => WildcardType - case bounds: TypeBounds => WildcardType(bounds) - case elemtp => elemtp val declaredElemType = recheck(tree.elemtpt) + val elemProto = seqLiteralElemProto(tree, pt, declaredElemType) val elemTypes = tree.elems.map(recheck(_, elemProto)) seqLitType(tree, TypeComparer.lub(declaredElemType :: elemTypes)) diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 643ef78841f0..f0fbd1a025b5 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -26,11 +26,11 @@ | Required: lazylists.LazyList[Int]^{cap2} | | longer explanation available when compiling with `-explain` --- [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}) - | Required: lazylists.LazyList[Int]^{cap1, ref3, cap3} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:41:42 ------------------------------------- +41 | val ref4c: LazyList[Int]^{cap1, ref3} = ref4 // error + | ^^^^ + | Found: (ref4 : lazylists.LazyList[Int]^{cap3, ref2, ref1}) + | Required: lazylists.LazyList[Int]^{cap1, ref3} | | longer explanation available when compiling with `-explain` -- [E164] Declaration Error: tests/neg-custom-args/captures/lazylist.scala:22:6 ---------------------------------------- diff --git a/tests/neg-custom-args/captures/lazylist.scala b/tests/neg-custom-args/captures/lazylist.scala index e6e4d003f7ae..f3cd0fd31e7a 100644 --- a/tests/neg-custom-args/captures/lazylist.scala +++ b/tests/neg-custom-args/captures/lazylist.scala @@ -38,4 +38,4 @@ def test(cap1: Cap, cap2: Cap, cap3: Cap) = val ref3 = ref1.map(g) val ref3c: LazyList[Int]^{cap2} = ref3 // error val ref4 = (if cap1 == cap2 then ref1 else ref2).map(h) - val ref4c: LazyList[Int]^{cap1, ref3, cap3} = ref4 // error + val ref4c: LazyList[Int]^{cap1, ref3} = ref4 // error diff --git a/tests/neg-custom-args/captures/lubs.check b/tests/neg-custom-args/captures/lubs.check new file mode 100644 index 000000000000..b2eaf6ae6f4e --- /dev/null +++ b/tests/neg-custom-args/captures/lubs.check @@ -0,0 +1,21 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lubs.scala:17:13 ----------------------------------------- +17 | val _: D = x1 // error + | ^^ + | Found: (x1 : D^{d1}) + | Required: D + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lubs.scala:18:13 ----------------------------------------- +18 | val _: D = x2 // error + | ^^ + | Found: (x2 : D^{d1}) + | Required: D + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lubs.scala:19:13 ----------------------------------------- +19 | val _: D = x3 // error + | ^^ + | Found: (x3 : D^{d1, d2}) + | Required: D + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lubs.scala b/tests/neg-custom-args/captures/lubs.scala new file mode 100644 index 000000000000..3a2eb59b48b5 --- /dev/null +++ b/tests/neg-custom-args/captures/lubs.scala @@ -0,0 +1,20 @@ +import java.sql.Date + +class C extends caps.Capability +class D + +def Test(c1: C, c2: C) = + val d: D = ??? + val d1: D^{c1} = ??? + val d2: D^{c2} = ??? + val x1 = if ??? then d else d1 + val _: D^{c1} = x1 + val x2 = if ??? then d1 else d + val _: D^{c1} = x2 + val x3 = if ??? then d1 else d2 + val _: D^{c1, c2} = x3 + + val _: D = x1 // error + val _: D = x2 // error + val _: D = x3 // error + diff --git a/tests/neg-custom-args/captures/spread-problem.check b/tests/neg-custom-args/captures/spread-problem.check new file mode 100644 index 000000000000..31cf38a51727 --- /dev/null +++ b/tests/neg-custom-args/captures/spread-problem.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/spread-problem.scala:8:6 --------------------------------- +8 | race(Seq(src1, src2)*) // error + | ^^^^^^^^^^^^^^^^^^^^^^ + | Found: Source[box T^?]^{src1, src2} + | Required: Source[T] + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/spread-problem.scala:11:6 -------------------------------- +11 | race(src1, src2) // error + | ^^^^^^^^^^^^^^^^ + | Found: Source[box T^?]^{src1, src2} + | Required: Source[T] + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/spread-problem.scala b/tests/neg-custom-args/captures/spread-problem.scala new file mode 100644 index 000000000000..579c7817b9c1 --- /dev/null +++ b/tests/neg-custom-args/captures/spread-problem.scala @@ -0,0 +1,11 @@ +import language.experimental.captureChecking + +trait Source[+T] + +def race[T](@caps.unbox sources: (Source[T]^)*): Source[T]^{sources*} = ??? + +def raceTwo[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} = + race(Seq(src1, src2)*) // error + +def raceThree[T](src1: Source[T]^, src2: Source[T]^): Source[T]^{} = + race(src1, src2) // error \ No newline at end of file From c8d418a1dab011b001dde8d30aff75df52215286 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Jul 2024 11:35:50 +0200 Subject: [PATCH 58/61] Don't do post checks in inlined code Capability references in inlined code might end up not being tracked or being redundant. Don't flag this as an error. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- tests/pos-custom-args/captures/inline-problem.scala | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/pos-custom-args/captures/inline-problem.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 26817cb838c6..c048edfb2102 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -753,7 +753,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: /** Check well formed at post check time */ private def checkWellformedLater(parent: Type, ann: Tree, tpt: Tree)(using Context): Unit = - if !tpt.span.isZeroExtent then + if !tpt.span.isZeroExtent && enclosingInlineds.isEmpty then todoAtPostCheck += (ctx1 => checkWellformedPost(parent, ann, tpt)(using ctx1.withOwner(ctx.owner))) diff --git a/tests/pos-custom-args/captures/inline-problem.scala b/tests/pos-custom-args/captures/inline-problem.scala new file mode 100644 index 000000000000..78034c20050a --- /dev/null +++ b/tests/pos-custom-args/captures/inline-problem.scala @@ -0,0 +1,5 @@ +trait Listener[+T] + +inline def consume[T](f: T => Unit): Listener[T]^{f} = ??? + +val consumePure = consume(_ => ()) \ No newline at end of file From 04dba38662388aa493c525b1fb4c30388865e500 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 18 Jul 2024 11:03:27 +0200 Subject: [PATCH 59/61] Widen non-trackable singleton types before computing their dcs --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 87638b078040..7b639ee64cdf 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -1073,7 +1073,7 @@ object CaptureSet: case parent: SingletonCaptureRef if parent.isTrackableRef => tp.singletonCaptureSet case _ => - CaptureSet.ofTypeDeeply(parent) + CaptureSet.ofTypeDeeply(parent.widen) case tpd @ defn.RefinedFunctionOf(rinfo: MethodType) if followResult => ofType(tpd.parent, followResult = false) // pick up capture set from parent type ++ (recur(rinfo.resType) // add capture set of result From 9391b9a38e876d2dcb10ba99da1f22bec24c60b1 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 18 Jul 2024 12:24:14 +0200 Subject: [PATCH 60/61] Give some explanation if a capture set was under-approximated Give some explanation if an empty capture set was the result of an under-approximation. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 3 ++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 15 +++++++++--- .../dotty/tools/dotc/cc/CapturingType.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 23 ++++++++++++++++++- .../captures/class-contra.check | 7 ++++-- .../captures/explain-under-approx.check | 20 ++++++++++++++++ .../captures/explain-under-approx.scala | 17 ++++++++++++++ 7 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 tests/neg-custom-args/captures/explain-under-approx.check create mode 100644 tests/neg-custom-args/captures/explain-under-approx.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 116b35e34aea..1f19641e3b08 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -244,7 +244,8 @@ extension (tp: 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 + if (cs.isAlwaysEmpty || cs.isConst && cs.subCaptures(tp.captureSet, frozen = true).isOK) + && !cs.keepAlways then tp else tp match case CapturingType(parent, cs1) => parent.capturing(cs1 ++ cs) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7b639ee64cdf..690d9b4411aa 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -88,6 +88,8 @@ sealed abstract class CaptureSet extends Showable: final def isUnboxable(using Context) = elems.exists(elem => elem.isRootCapability || Existential.isExistentialVar(elem)) + final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance] + /** 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. @@ -219,7 +221,8 @@ sealed abstract class CaptureSet extends Showable: * `this` and `that` */ def ++ (that: CaptureSet)(using Context): CaptureSet = - if this.subCaptures(that, frozen = true).isOK then that + if this.subCaptures(that, frozen = true).isOK then + if that.isAlwaysEmpty && this.keepAlways then this else that else if that.subCaptures(this, frozen = true).isOK then this else if this.isConst && that.isConst then Const(this.elems ++ that.elems) else Union(this, that) @@ -294,7 +297,7 @@ sealed abstract class CaptureSet extends Showable: case _ => val mapped = mapRefs(elems, tm, tm.variance) if isConst then - if mapped.isConst && mapped.elems == elems then this + if mapped.isConst && mapped.elems == elems && !mapped.keepAlways then this else mapped else Mapped(asVar, tm, tm.variance, mapped) @@ -398,6 +401,12 @@ object CaptureSet: override def toString = elems.toString end Const + case class EmptyWithProvenance(ref: CaptureRef, mapped: Type) extends Const(SimpleIdentitySet.empty): + override def optionalInfo(using Context): String = + if ctx.settings.YccDebug.value + then i" under-approximating the result of mapping $ref to $mapped" + else "" + /** A special capture set that gets added to the types of symbols that were not * themselves capture checked, in order to admit arbitrary corresponding capture * sets in subcapturing comparisons. Similar to platform types for explicit @@ -863,7 +872,7 @@ object CaptureSet: || 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 if variance < 0 then CaptureSet.EmptyWithProvenance(r, r1) else upper.maybe /** Apply `f` to each element in `xs`, and join result sets with `++` */ diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index f859b0d110aa..bb79e52f1060 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -33,7 +33,7 @@ object CapturingType: * boxing status is the same or if A is boxed. */ def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type = - if refs.isAlwaysEmpty then parent + if refs.isAlwaysEmpty && !refs.keepAlways then parent else parent match case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed => apply(parent1, refs ++ refs1, boxed) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 667d91a10330..511c978a8e32 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -990,6 +990,25 @@ class CheckCaptures extends Recheck, SymTransformer: | |Note that ${msg.toString}""" + private def addApproxAddenda(using Context) = + new TypeAccumulator[Addenda]: + def apply(add: Addenda, t: Type) = t match + case CapturingType(t, CaptureSet.EmptyWithProvenance(ref, mapped)) => + /* val (origCore, kind) = original match + case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) => + (parent, " deep") + case _ => + (original, "")*/ + add ++ new Addenda: + override def toAdd(using Context): List[String] = + i""" + | + |Note that a capability $ref in a capture set appearing in contravariant position + |was mapped to $mapped which is not a capability. Therefore, it was under-approximated to the empty set.""" + :: Nil + case _ => + foldOver(add, t) + /** 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 @@ -1015,7 +1034,9 @@ class CheckCaptures extends Recheck, SymTransformer: else capt.println(i"conforms failed for ${tree}: $actual vs $expected") err.typeMismatch(tree.withType(actualBoxed), expected1, - addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors)) + addApproxAddenda( + addenda ++ CaptureSet.levelErrors ++ boxErrorAddenda(boxErrors), + expected1)) actual end checkConformsExpr diff --git a/tests/neg-custom-args/captures/class-contra.check b/tests/neg-custom-args/captures/class-contra.check index 6d4c89f872ad..9fc009ac3d48 100644 --- a/tests/neg-custom-args/captures/class-contra.check +++ b/tests/neg-custom-args/captures/class-contra.check @@ -1,7 +1,10 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-contra.scala:12:39 --------------------------------- 12 | def fun(x: K{val f: T^{a}}) = x.setf(a) // error | ^ - | Found: (a : T^{x, y}) - | Required: T + | Found: (a : T^{x, y}) + | Required: T^{} + | + | Note that a capability (K.this.f : T^) in a capture set appearing in contravariant position + | was mapped to (x.f : T^{a}) which is not a capability. Therefore, it was under-approximated to the empty set. | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/explain-under-approx.check b/tests/neg-custom-args/captures/explain-under-approx.check new file mode 100644 index 000000000000..2d2b05b4b95a --- /dev/null +++ b/tests/neg-custom-args/captures/explain-under-approx.check @@ -0,0 +1,20 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:12:10 ------------------------- +12 | col.add(Future(() => 25)) // error + | ^^^^^^^^^^^^^^^^ + | Found: Future[Int]{val a: (async : Async^)}^{async} + | Required: Future[Int]^{} + | + | Note that a capability Collector.this.futs* in a capture set appearing in contravariant position + | was mapped to col.futs* which is not a capability. Therefore, it was under-approximated to the empty set. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/explain-under-approx.scala:15:11 ------------------------- +15 | col1.add(Future(() => 25)) // error + | ^^^^^^^^^^^^^^^^ + | Found: Future[Int]{val a: (async : Async^)}^{async} + | Required: Future[Int]^{} + | + | Note that a capability Collector.this.futs* in a capture set appearing in contravariant position + | was mapped to col1.futs* which is not a capability. Therefore, it was under-approximated to the empty set. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/explain-under-approx.scala b/tests/neg-custom-args/captures/explain-under-approx.scala new file mode 100644 index 000000000000..816465e4af34 --- /dev/null +++ b/tests/neg-custom-args/captures/explain-under-approx.scala @@ -0,0 +1,17 @@ +trait Async extends caps.Capability + +class Future[+T](x: () => T)(using val a: Async) + +class Collector[T](val futs: Seq[Future[T]^]): + def add(fut: Future[T]^{futs*}) = ??? + +def main() = + given async: Async = ??? + val futs = (1 to 20).map(x => Future(() => x)) + val col = Collector(futs) + col.add(Future(() => 25)) // error + val col1: Collector[Int] { val futs: Seq[Future[Int]^{async}] } + = Collector(futs) + col1.add(Future(() => 25)) // error + + From fac643a3ffd062c9d76593c0bba260f99f897a50 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 18 Jul 2024 14:14:57 +0200 Subject: [PATCH 61/61] Split postcheck phase in two Perform bounds checking before checking self types. Checking self types interpolates them, which may give an upper approximation solution that failes subsequent bounds checks. On the other hand, well-formedness checkimng should come after self types checking since otherwise we get spurious "has empty capture set, cannot be tracked" messages. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +++- .../dotty/tools/dotc/cc/CheckCaptures.scala | 18 ++++++++++----- .../captures/checkbounds.scala | 22 +++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 tests/pos-custom-args/captures/checkbounds.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 690d9b4411aa..1d09b9dc5f20 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -594,7 +594,9 @@ object CaptureSet: override def optionalInfo(using Context): String = for vars <- ctx.property(ShownVars) do vars += this val debugInfo = - if !isConst && ctx.settings.YccDebug.value then ids else "" + if !ctx.settings.YccDebug.value then "" + else if isConst then ids ++ "(solved)" + else ids val limitInfo = if ctx.settings.YprintLevel.value && level.isDefined then i"" diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 511c978a8e32..9f6cb278f012 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1363,8 +1363,9 @@ class CheckCaptures extends Recheck, SymTransformer: withCaptureSetsExplained: super.checkUnit(unit) checkOverrides.traverse(unit.tpdTree) - checkSelfTypes(unit.tpdTree) postCheck(unit.tpdTree) + checkSelfTypes(unit.tpdTree) + postCheckWF(unit.tpdTree) if ctx.settings.YccDebug.value then show(unit.tpdTree) // this does not print tree, but makes its variables visible for dependency printing @@ -1514,7 +1515,6 @@ class CheckCaptures extends Recheck, SymTransformer: check.traverse(tp) /** Perform the following kinds of checks - * - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`. * - Check that arguments of TypeApplys and AppliedTypes conform to their bounds. * - Heal ill-formed capture sets of type parameters. See `healTypeParam`. */ @@ -1542,10 +1542,8 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => end check end checker - checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) - for chk <- todoAtPostCheck do chk() - setup.postCheck() + checker.traverse(unit)(using ctx.withOwner(defn.RootClass)) if !ctx.reporter.errorsReported then // We dont report errors here if previous errors were reported, because other // errors often result in bad applied types, but flagging these bad types gives @@ -1557,5 +1555,15 @@ class CheckCaptures extends Recheck, SymTransformer: case tree: TypeTree => checkAppliedTypesIn(tree.withKnownType) case _ => traverseChildren(t) checkApplied.traverse(unit) + end postCheck + + /** Perform the following kinds of checks: + * - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`. + * - Check that publicly visible inferred types correspond to the type + * they have without capture checking. + */ + def postCheckWF(unit: tpd.Tree)(using Context): Unit = + for chk <- todoAtPostCheck do chk() + setup.postCheck() end CaptureChecker end CheckCaptures diff --git a/tests/pos-custom-args/captures/checkbounds.scala b/tests/pos-custom-args/captures/checkbounds.scala new file mode 100644 index 000000000000..f9cd76ce8b1a --- /dev/null +++ b/tests/pos-custom-args/captures/checkbounds.scala @@ -0,0 +1,22 @@ +trait Dsl: + + sealed trait Nat + case object Zero extends Nat + case class Succ[N <: Nat](n: N) extends Nat + + type Stable[+l <: Nat, +b <: Nat, +A] + type Now[+l <: Nat, +b <: Nat, +A] + type Box[+A] + def stable[l <: Nat, b <: Nat, A](e: Stable[l, b, A]): Now[l, b, Box[A]] + + def program[A](prog: Now[Zero.type, Zero.type, A]): Now[Zero.type, Zero.type, A] + + //val conforms: Zero.type <:< Nat = summon + // ^ need to uncomment this line to compile with captureChecking enabled + + def test: Any = + program[Box[Int]]: + val v : Stable[Zero.type, Zero.type, Int] = ??? + stable[Zero.type, Zero.type, Int](v) +// ^ +// Type argument Dsl.this.Zero.type does not conform to upper bound Dsl.this.Nat \ No newline at end of file