From 12131be6327b976d6bc955acfd56431378b5fc0b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 17 Aug 2021 12:38:12 +0200 Subject: [PATCH 01/99] Merge pull request #12971 from dotty-staging/add-rechecker Add recheck phase --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 + .../dotty/tools/dotc/config/Printers.scala | 1 + .../tools/dotc/config/ScalaSettings.scala | 1 + .../src/dotty/tools/dotc/core/NamerOps.scala | 20 ++ .../src/dotty/tools/dotc/core/Phases.scala | 3 + .../dotty/tools/dotc/core/TypeComparer.scala | 2 + .../tools/dotc/transform/PreRecheck.scala | 21 ++ .../dotty/tools/dotc/transform/Recheck.scala | 334 ++++++++++++++++++ .../tools/dotc/typer/RefineTypes.overflow | 0 .../dotty/tools/dotc/typer/TypeAssigner.scala | 18 +- compiler/test/dotc/pos-test-recheck.excludes | 3 + compiler/test/dotc/run-test-recheck.excludes | 0 compiler/test/dotty/tools/TestSources.scala | 4 + .../dotty/tools/dotc/CompilationTests.scala | 8 + .../tools/vulpix/TestConfiguration.scala | 1 + tests/neg/i6635a.scala | 19 + tests/pos/i6635.scala | 7 +- tests/pos/i6635a.scala | 14 + 18 files changed, 446 insertions(+), 12 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/transform/PreRecheck.scala create mode 100644 compiler/src/dotty/tools/dotc/transform/Recheck.scala create mode 100644 compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow create mode 100644 compiler/test/dotc/pos-test-recheck.excludes create mode 100644 compiler/test/dotc/run-test-recheck.excludes create mode 100644 tests/neg/i6635a.scala create mode 100644 tests/pos/i6635a.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index ce4ed2d4e4e8..544abc3211d3 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -101,6 +101,8 @@ class Compiler { new TupleOptimizations, // Optimize generic operations on tuples new LetOverApply, // Lift blocks from receivers of applications new ArrayConstructors) :: // Intercept creation of (non-generic) arrays and intrinsify. + List(new PreRecheck) :: + List(new TestRecheck) :: List(new Erasure) :: // Rewrite types to JVM model, erasing all type parameters, abstract types and refinements. List(new ElimErasedValueType, // Expand erased value types to their underlying implmementation types new PureStats, // Remove pure stats from blocks diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 458c149723b9..9772f1c98cdf 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -38,6 +38,7 @@ object Printers { val pickling = noPrinter val quotePickling = noPrinter val plugins = noPrinter + val recheckr = noPrinter val refcheck = noPrinter val simplify = noPrinter val staging = noPrinter diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 8a66b5abca8a..85d75a971e11 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -325,6 +325,7 @@ private sealed trait YSettings: val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") + val Yrecheck: Setting[Boolean] = BooleanSetting("-Yrecheck", "Run type rechecks (test only)") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 1a878c9547b1..9d826f293689 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -191,4 +191,24 @@ object NamerOps: cls.registeredCompanion = modcls modcls.registeredCompanion = cls + /** For secondary constructors, make it known in the context that their type parameters + * are aliases of the class type parameters. This is done by (ab?)-using GADT constraints. + * See pos/i941.scala + */ + def linkConstructorParams(sym: Symbol)(using Context): Context = + if sym.isConstructor && !sym.isPrimaryConstructor then + sym.rawParamss match + case (tparams @ (tparam :: _)) :: _ if tparam.isType => + val rhsCtx = ctx.fresh.setFreshGADTBounds + rhsCtx.gadt.addToConstraint(tparams) + tparams.lazyZip(sym.owner.typeParams).foreach { (psym, tparam) => + val tr = tparam.typeRef + rhsCtx.gadt.addBound(psym, tr, isUpper = false) + rhsCtx.gadt.addBound(psym, tr, isUpper = true) + } + rhsCtx + case _ => + ctx + else ctx + end NamerOps diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index b5ad289554c9..34c51f43bbd7 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -298,6 +298,9 @@ object Phases { /** If set, implicit search is enabled */ def allowsImplicitSearch: Boolean = false + /** If set equate Skolem types with underlying types */ + def widenSkolems: Boolean = false + /** List of names of phases that should precede this phase */ def runsAfter: Set[String] = Set.empty diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index f53426022e44..087a28a7323e 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -776,6 +776,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } compareClassInfo + case tp2: SkolemType => + ctx.phase.widenSkolems && recur(tp1, tp2.info) || fourthTry case _ => fourthTry } diff --git a/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala new file mode 100644 index 000000000000..ab27bb3bb306 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala @@ -0,0 +1,21 @@ +package dotty.tools.dotc +package transform + +import core.Phases.Phase +import core.DenotTransformers.IdentityDenotTransformer +import core.Contexts.{Context, ctx} + +/** A phase that precedes the rechecker and that allows installing + * new types for local symbols. + */ +class PreRecheck extends Phase, IdentityDenotTransformer: + + def phaseName: String = "preRecheck" + + override def isEnabled(using Context) = next.isEnabled + + override def changesBaseTypes: Boolean = true + + def run(using Context): Unit = () + + override def isCheckable = false diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala new file mode 100644 index 000000000000..76f89cb65757 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -0,0 +1,334 @@ +package dotty.tools +package dotc +package transform + +import core.* +import Symbols.*, Contexts.*, Types.*, ContextOps.*, Decorators.*, SymDenotations.* +import Flags.*, SymUtils.*, NameKinds.* +import ast.* +import Phases.Phase +import DenotTransformers.IdentityDenotTransformer +import NamerOps.{methodType, linkConstructorParams} +import NullOpsDecorator.stripNull +import typer.ErrorReporting.err +import typer.ProtoTypes.* +import typer.TypeAssigner.seqLitType +import typer.ConstFold +import config.Printers.recheckr +import util.Property +import StdNames.nme +import reporting.trace + +abstract class Recheck extends Phase, IdentityDenotTransformer: + thisPhase => + + import ast.tpd.* + + def preRecheckPhase = this.prev.asInstanceOf[PreRecheck] + + override def isEnabled(using Context) = ctx.settings.Yrecheck.value + override def changesBaseTypes: Boolean = true + + override def isCheckable = false + // TODO: investigate what goes wrong we Ycheck directly after rechecking. + // One failing test is pos/i583a.scala + + override def widenSkolems = true + + def run(using Context): Unit = + newRechecker().checkUnit(ctx.compilationUnit) + + def newRechecker()(using Context): Rechecker + + class Rechecker(ictx: Context): + val ta = ictx.typeAssigner + + extension (sym: Symbol) def updateInfo(newInfo: Type)(using Context): Unit = + if sym.info ne newInfo then + sym.copySymDenotation().installAfter(thisPhase) // reset + sym.copySymDenotation( + info = newInfo, + initFlags = + if newInfo.isInstanceOf[LazyType] then sym.flags &~ Touched + else sym.flags + ).installAfter(preRecheckPhase) + + /** Hook to be overridden */ + protected def reinfer(tp: Type)(using Context): Type = tp + + def reinferResult(info: Type)(using Context): Type = info match + case info: MethodOrPoly => + info.derivedLambdaType(resType = reinferResult(info.resultType)) + case _ => + reinfer(info) + + def enterDef(stat: Tree)(using Context): Unit = + val sym = stat.symbol + stat match + case stat: ValOrDefDef if stat.tpt.isInstanceOf[InferredTypeTree] => + sym.updateInfo(reinferResult(sym.info)) + case stat: Bind => + sym.updateInfo(reinferResult(sym.info)) + case _ => + + def constFold(tree: Tree, tp: Type)(using Context): Type = + val tree1 = tree.withType(tp) + val tree2 = ConstFold(tree1) + if tree2 ne tree1 then tree2.tpe else tp + + def recheckIdent(tree: Ident)(using Context): Type = + tree.tpe + + /** Keep the symbol of the `select` but re-infer its type */ + def recheckSelect(tree: Select)(using Context): Type = tree match + case Select(qual, name) => + val qualType = recheck(qual).widenIfUnstable + if name.is(OuterSelectName) then tree.tpe + else + //val pre = ta.maybeSkolemizePrefix(qualType, name) + val mbr = qualType.findMember(name, qualType, + excluded = if tree.symbol.is(Private) then EmptyFlags else Private + ).suchThat(tree.symbol ==) + constFold(tree, qualType.select(name, mbr)) + + def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match + case Bind(name, body) => + enterDef(tree) + recheck(body, pt) + val sym = tree.symbol + if sym.isType then sym.typeRef else sym.info + + def recheckLabeled(tree: Labeled, pt: Type)(using Context): Type = tree match + case Labeled(bind, expr) => + val bindType = recheck(bind, pt) + val exprType = recheck(expr, defn.UnitType) + bindType + + def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = + if !tree.rhs.isEmpty then recheck(tree.rhs, tree.symbol.info) + sym.termRef + + def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = + tree.paramss.foreach(_.foreach(enterDef)) + val rhsCtx = linkConstructorParams(sym) + if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then + recheck(tree.rhs, tree.symbol.localReturnType)(using rhsCtx) + sym.termRef + + def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type = + recheck(tree.rhs) + sym.typeRef + + def recheckClassDef(tree: TypeDef, impl: Template, sym: ClassSymbol)(using Context): Type = + recheck(impl.constr) + impl.parentsOrDerived.foreach(recheck(_)) + recheck(impl.self) + recheckStats(impl.body) + sym.typeRef + + // Need to remap Object to FromJavaObject since it got lost in ElimRepeated + private def mapJavaArgs(formals: List[Type])(using Context): List[Type] = + val tm = new TypeMap: + def apply(t: Type) = t match + case t: TypeRef if t.symbol == defn.ObjectClass => defn.FromJavaObjectType + case _ => mapOver(t) + formals.mapConserve(tm) + + def recheckApply(tree: Apply, pt: Type)(using Context): Type = + recheck(tree.fun).widen match + case fntpe: MethodType => + assert(sameLength(fntpe.paramInfos, tree.args)) + val formals = + if tree.symbol.is(JavaDefined) then mapJavaArgs(fntpe.paramInfos) + else fntpe.paramInfos + def recheckArgs(args: List[Tree], formals: List[Type], prefs: List[ParamRef]): List[Type] = args match + case arg :: args1 => + val argType = recheck(arg, formals.head) + val formals1 = + if fntpe.isParamDependent + then formals.tail.map(_.substParam(prefs.head, argType)) + else formals.tail + argType :: recheckArgs(args1, formals1, prefs.tail) + case Nil => + assert(formals.isEmpty) + Nil + val argTypes = recheckArgs(tree.args, formals, fntpe.paramRefs) + constFold(tree, fntpe.instantiate(argTypes)) + + def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = + recheck(tree.fun).widen match + case fntpe: PolyType => + assert(sameLength(fntpe.paramInfos, tree.args)) + val argTypes = tree.args.map(recheck(_)) + constFold(tree, fntpe.instantiate(argTypes)) + + def recheckTyped(tree: Typed)(using Context): Type = + val tptType = recheck(tree.tpt) + recheck(tree.expr, tptType) + tptType + + def recheckAssign(tree: Assign)(using Context): Type = + val lhsType = recheck(tree.lhs) + recheck(tree.rhs, lhsType.widen) + defn.UnitType + + def recheckBlock(stats: List[Tree], expr: Tree, pt: Type)(using Context): Type = + recheckStats(stats) + val exprType = recheck(expr, pt.dropIfProto) + TypeOps.avoid(exprType, localSyms(stats).filterConserve(_.isTerm)) + + def recheckBlock(tree: Block, pt: Type)(using Context): Type = + recheckBlock(tree.stats, tree.expr, pt) + + def recheckInlined(tree: Inlined, pt: Type)(using Context): Type = + recheckBlock(tree.bindings, tree.expansion, pt) + + def recheckIf(tree: If, pt: Type)(using Context): Type = + recheck(tree.cond, defn.BooleanType) + recheck(tree.thenp, pt) | recheck(tree.elsep, pt) + + def recheckClosure(tree: Closure, pt: Type)(using Context): Type = + if tree.tpt.isEmpty then + tree.meth.tpe.widen.toFunctionType(tree.meth.symbol.is(JavaDefined)) + else + recheck(tree.tpt) + + def recheckMatch(tree: Match, pt: Type)(using Context): Type = + val selectorType = recheck(tree.selector) + val casesTypes = tree.cases.map(recheck(_, selectorType.widen, pt)) + TypeComparer.lub(casesTypes) + + def recheck(tree: CaseDef, selType: Type, pt: Type)(using Context): Type = + recheck(tree.pat, selType) + recheck(tree.guard, defn.BooleanType) + recheck(tree.body, pt) + + def recheckReturn(tree: Return)(using Context): Type = + recheck(tree.expr, tree.from.symbol.returnProto) + defn.NothingType + + def recheckWhileDo(tree: WhileDo)(using Context): Type = + recheck(tree.cond, defn.BooleanType) + recheck(tree.body, defn.UnitType) + defn.UnitType + + def recheckTry(tree: Try, pt: Type)(using Context): Type = + val bodyType = recheck(tree.expr, pt) + val casesTypes = tree.cases.map(recheck(_, defn.ThrowableType, pt)) + val finalizerType = recheck(tree.finalizer, defn.UnitType) + TypeComparer.lub(bodyType :: casesTypes) + + 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 elemTypes = tree.elems.map(recheck(_, elemProto)) + seqLitType(tree, TypeComparer.lub(declaredElemType :: elemTypes)) + + def recheckTypeTree(tree: TypeTree)(using Context): Type = tree match + case tree: InferredTypeTree => reinfer(tree.tpe) + case _ => tree.tpe + + def recheckAnnotated(tree: Annotated)(using Context): Type = + tree.tpe match + case tp: AnnotatedType => + val argType = recheck(tree.arg) + tp.derivedAnnotatedType(argType, tp.annot) + + def recheckAlternative(tree: Alternative, pt: Type)(using Context): Type = + val altTypes = tree.trees.map(recheck(_, pt)) + TypeComparer.lub(altTypes) + + def recheckPackageDef(tree: PackageDef)(using Context): Type = + recheckStats(tree.stats) + NoType + + def recheckStats(stats: List[Tree])(using Context): Unit = + stats.foreach(enterDef) + stats.foreach(recheck(_)) + + /** Recheck tree without adapting it, returning its new type. + * @param tree the original tree + * @param pt the expected result type + */ + def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = trace(i"rechecking $tree with pt = $pt", recheckr, show = true) { + + def recheckNamed(tree: NameTree, pt: Type)(using Context): Type = + val sym = tree.symbol + tree match + case tree: Ident => recheckIdent(tree) + case tree: Select => recheckSelect(tree) + case tree: Bind => recheckBind(tree, pt) + case tree: ValDef => + if tree.isEmpty then NoType + else recheckValDef(tree, sym)(using ctx.localContext(tree, sym)) + case tree: DefDef => + recheckDefDef(tree, sym)(using ctx.localContext(tree, sym)) + case tree: TypeDef => + tree.rhs match + case impl: Template => + recheckClassDef(tree, impl, sym.asClass)(using ctx.localContext(tree, sym)) + case _ => + recheckTypeDef(tree, sym)(using ctx.localContext(tree, sym)) + case tree: Labeled => recheckLabeled(tree, pt) + + def recheckUnnamed(tree: Tree, pt: Type): Type = tree match + case tree: Apply => recheckApply(tree, pt) + case tree: TypeApply => recheckTypeApply(tree, pt) + case _: New | _: This | _: Super | _: Literal => tree.tpe + case tree: Typed => recheckTyped(tree) + case tree: Assign => recheckAssign(tree) + case tree: Block => recheckBlock(tree, pt) + case tree: If => recheckIf(tree, pt) + case tree: Closure => recheckClosure(tree, pt) + case tree: Match => recheckMatch(tree, pt) + case tree: Return => recheckReturn(tree) + case tree: WhileDo => recheckWhileDo(tree) + case tree: Try => recheckTry(tree, pt) + case tree: SeqLiteral => recheckSeqLiteral(tree, pt) + case tree: Inlined => recheckInlined(tree, pt) + case tree: TypeTree => recheckTypeTree(tree) + case tree: Annotated => recheckAnnotated(tree) + case tree: Alternative => recheckAlternative(tree, pt) + case tree: PackageDef => recheckPackageDef(tree) + case tree: Thicket => defn.NothingType + + try + val result = tree match + case tree: NameTree => recheckNamed(tree, pt) + case tree => recheckUnnamed(tree, pt) + checkConforms(result, pt, tree) + result + catch case ex: Exception => + println(i"error while rechecking $tree") + throw ex + } + end recheck + + def checkConforms(tpe: Type, pt: Type, tree: Tree)(using Context): Unit = tree match + case _: DefTree | EmptyTree | _: TypeTree => + case _ => + val actual = tpe.widenExpr + val expected = pt.widenExpr + val isCompatible = + actual <:< expected + || expected.isRepeatedParam + && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) + if !isCompatible then + println(i"err at ${ctx.phase}") + err.typeMismatch(tree.withType(tpe), pt) + + def checkUnit(unit: CompilationUnit)(using Context): Unit = + recheck(unit.tpdTree) + + end Rechecker +end Recheck + +class TestRecheck extends Recheck: + def phaseName: String = "recheck" + //override def isEnabled(using Context) = ctx.settings.YrefineTypes.value + def newRechecker()(using Context): Rechecker = Rechecker(ctx) + + diff --git a/compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow b/compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 24794011bf5b..9d31e29c7e2b 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -13,7 +13,8 @@ import reporting._ import Checking.checkNoPrivateLeaks trait TypeAssigner { - import tpd._ + import tpd.* + import TypeAssigner.* /** The qualifying class of a this or super with prefix `qual` (which might be empty). * @param packageOk The qualifier may refer to a package. @@ -444,13 +445,8 @@ trait TypeAssigner { if (cases.isEmpty) tree.withType(expr.tpe) else tree.withType(TypeComparer.lub(expr.tpe :: cases.tpes)) - def assignType(tree: untpd.SeqLiteral, elems: List[Tree], elemtpt: Tree)(using Context): SeqLiteral = { - val ownType = tree match { - case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemtpt.tpe) - case _ => if (ctx.erasedTypes) defn.SeqType else defn.SeqType.appliedTo(elemtpt.tpe) - } - tree.withType(ownType) - } + def assignType(tree: untpd.SeqLiteral, elems: List[Tree], elemtpt: Tree)(using Context): SeqLiteral = + tree.withType(seqLitType(tree, elemtpt.tpe)) def assignType(tree: untpd.SingletonTypeTree, ref: Tree)(using Context): SingletonTypeTree = tree.withType(ref.tpe) @@ -545,5 +541,9 @@ trait TypeAssigner { } +object TypeAssigner extends TypeAssigner: + def seqLitType(tree: untpd.SeqLiteral, elemType: Type)(using Context) = tree match + case tree: untpd.JavaSeqLiteral => defn.ArrayOf(elemType) + case _ => if ctx.erasedTypes then defn.SeqType else defn.SeqType.appliedTo(elemType) + -object TypeAssigner extends TypeAssigner diff --git a/compiler/test/dotc/pos-test-recheck.excludes b/compiler/test/dotc/pos-test-recheck.excludes new file mode 100644 index 000000000000..e973b2cd529f --- /dev/null +++ b/compiler/test/dotc/pos-test-recheck.excludes @@ -0,0 +1,3 @@ +# Cannot compensate dealiasing due to false result dependency +i6635a.scala +i6682a.scala diff --git a/compiler/test/dotc/run-test-recheck.excludes b/compiler/test/dotc/run-test-recheck.excludes new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/compiler/test/dotty/tools/TestSources.scala b/compiler/test/dotty/tools/TestSources.scala index b4be973147e3..c4d36b16c90b 100644 --- a/compiler/test/dotty/tools/TestSources.scala +++ b/compiler/test/dotty/tools/TestSources.scala @@ -13,17 +13,21 @@ object TestSources { def posFromTastyBlacklistFile: String = "compiler/test/dotc/pos-from-tasty.blacklist" def posTestPicklingBlacklistFile: String = "compiler/test/dotc/pos-test-pickling.blacklist" + def posTestRecheckExcludesFile = "compiler/test/dotc/pos-test-recheck.excludes" def posFromTastyBlacklisted: List[String] = loadList(posFromTastyBlacklistFile) def posTestPicklingBlacklisted: List[String] = loadList(posTestPicklingBlacklistFile) + def posTestRecheckExcluded = loadList(posTestRecheckExcludesFile) // run tests lists def runFromTastyBlacklistFile: String = "compiler/test/dotc/run-from-tasty.blacklist" def runTestPicklingBlacklistFile: String = "compiler/test/dotc/run-test-pickling.blacklist" + def runTestRecheckExcludesFile = "compiler/test/dotc/run-test-recheck.excludes" def runFromTastyBlacklisted: List[String] = loadList(runFromTastyBlacklistFile) def runTestPicklingBlacklisted: List[String] = loadList(runTestPicklingBlacklistFile) + def runTestRecheckExcluded = loadList(runTestRecheckExcludesFile) // load lists diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 1abb1ffd9ea3..fb84a01d8daf 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -233,6 +233,14 @@ class CompilationTests { ).checkCompile() } + @Test def recheck: Unit = + given TestGroup = TestGroup("recheck") + aggregateTests( + compileFilesInDir("tests/new", recheckOptions), + compileFilesInDir("tests/pos", recheckOptions, FileFilter.exclude(TestSources.posTestRecheckExcluded)), + compileFilesInDir("tests/run", recheckOptions, FileFilter.exclude(TestSources.runTestRecheckExcluded)) + ).checkCompile() + // Explicit nulls tests @Test def explicitNullsNeg: Unit = { implicit val testGroup: TestGroup = TestGroup("explicitNullsNeg") diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 1f53add86667..1e9d0d4697ea 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -83,6 +83,7 @@ object TestConfiguration { ) val picklingWithCompilerOptions = picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) + val recheckOptions = defaultOptions.and("-Yrecheck") val scala2CompatMode = defaultOptions.and("-source", "3.0-migration") val explicitUTF8 = defaultOptions and ("-encoding", "UTF8") val explicitUTF16 = defaultOptions and ("-encoding", "UTF16") diff --git a/tests/neg/i6635a.scala b/tests/neg/i6635a.scala new file mode 100644 index 000000000000..a79ea4e7c818 --- /dev/null +++ b/tests/neg/i6635a.scala @@ -0,0 +1,19 @@ +object Test { + abstract class ExprBase { s => + type A + } + + abstract class Lit extends ExprBase { s => + type A = Int + val n: A + } + + // It would be nice if the following could typecheck. We'd need to apply + // a reasoning like this: + // + // Since there is an argument `e2` of type `Lit & e1.type`, it follows that + // e1.type == e2.type Hence, e1.A == e2.A == Int. This looks similar + // to techniques used in GADTs. + // + def castTestFail2a(e1: ExprBase)(e2: Lit & e1.type)(x: e1.A): Int = x // error: Found: (x : e1.A) Required: Int +} diff --git a/tests/pos/i6635.scala b/tests/pos/i6635.scala index dacd1ef5cd8b..406eee6251e6 100644 --- a/tests/pos/i6635.scala +++ b/tests/pos/i6635.scala @@ -27,11 +27,12 @@ object Test { def castTest5a(e1: ExprBase)(e2: LitU with e1.type)(x: e2.A): e1.A = x def castTest5b(e1: ExprBase)(e2: LitL with e1.type)(x: e2.A): e1.A = x - //fail: def castTestFail1(e1: ExprBase)(e2: Lit with e1.type)(x: e2.A): e1.A = x // this is like castTest5a/b, but with Lit instead of LitU/LitL - // the other direction never works: - def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x + + // The next example fails rechecking. It is repeated in i6635a.scala + // def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x def castTestFail2b(e1: ExprBase)(e2: LitL with e1.type)(x: e1.A): e2.A = x + def castTestFail2c(e1: ExprBase)(e2: LitU with e1.type)(x: e1.A): e2.A = x // the problem isn't about order of intersections. diff --git a/tests/pos/i6635a.scala b/tests/pos/i6635a.scala new file mode 100644 index 000000000000..9454e03e3a4a --- /dev/null +++ b/tests/pos/i6635a.scala @@ -0,0 +1,14 @@ +object Test { + abstract class ExprBase { s => + type A + } + + abstract class Lit extends ExprBase { s => + type A = Int + val n: A + } + + // Fails recheck since the result type e2.A is converted to Int to avoid + // a false dependency on e2. + def castTestFail2a(e1: ExprBase)(e2: Lit with e1.type)(x: e1.A): e2.A = x +} From b64aa7edd171953557c996cfedcb0c533e1cf445 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 17 Aug 2021 13:02:09 +0200 Subject: [PATCH 02/99] Cleanups --- compiler/src/dotty/tools/dotc/Compiler.scala | 4 ++-- compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow | 0 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 544abc3211d3..57ede3c621f2 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -101,8 +101,8 @@ class Compiler { new TupleOptimizations, // Optimize generic operations on tuples new LetOverApply, // Lift blocks from receivers of applications new ArrayConstructors) :: // Intercept creation of (non-generic) arrays and intrinsify. - List(new PreRecheck) :: - List(new TestRecheck) :: + List(new PreRecheck) :: // Preparations for recheck phase, enabled under -Yrecheck + List(new TestRecheck) :: // Test rechecking, enabled under -Yrecheck List(new Erasure) :: // Rewrite types to JVM model, erasing all type parameters, abstract types and refinements. List(new ElimErasedValueType, // Expand erased value types to their underlying implmementation types new PureStats, // Remove pure stats from blocks diff --git a/compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow b/compiler/src/dotty/tools/dotc/typer/RefineTypes.overflow deleted file mode 100644 index e69de29bb2d1..000000000000 From cbdb6103680bf6d3983bcf98aa399e5ccfd5c166 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 29 Sep 2021 17:46:49 +0200 Subject: [PATCH 03/99] First version of capture checker. A squashed version of the following commits: Handle byname parameters Don't force symbol completion when printing flags or annotations Check overrides and disallow non-local inferred capture types Handle `this` in capture sets Print capture variable dependencies under -Ydebug-cc Avoid spurious error message Avoid spurious error message "cannot be tracked since its capture set is empty". This arose in lazyref.scala for a DependentTypeTree in an anaonymois function. Dependent type trees map to normal TypeTrees, not InferredTypeTrees (and things go wrong if we try to change that). Drop TopType Consider bounds of type variables to be boxed More tests Avoid multiple maps when creating symbol infos Use a single BiTypeMap to map from inferred result and parameters to method info. This improves efficiency and debuggability by reducing the frequence of multiple stacked maps capture sets. Refactor with CompareResult#andAlso Refactoring: use isOK on CompareResult Reflect inferred parameter types in enclosing method type The variables in the inferred parameter type of an anonymous function need to also show up in the closure type itself, so that they can be constrained. Don't interpolate parameters of anonymous functions Here, we should wait until we get the info from the outside, which can be arbitrarily much later. Compute upper approximation of bimapped sets from both sides Fail when trying to add new elements to mapped sets It's the safe option. Print full origin trail of derived capture sets under -Ycc-debug Fix isEmpty condition in well-formedness check Make printing capture sets dependent on -Ycc-debug Recursion brake for upperApprox Fixes to upperApprox Make instantiteRT a BiTypeMap Otherwise we will not be able to do upper approximations of parameters. Interpolate only variables at negative polarity Interpolating covariant variables risks restricting capture sets to early. For instance, when a variable has the capture set of a called function in its capture set. When we have indirectly recursive calls it could be that the capture set of a called function is not yet fully formed. Interpolate type variables when symbols are completed Allow for possibility that variables are constant Only recomplete symbols if their info changes Add completions to Rechecker Complete val and def definitions lazily on first access. Now, recheckDefDef and recheckValDef are called the first time the new info of the defined symbol is needed, or, if the info is never needed, when the typer gets to the definitions. This only applied to definitions with inferred types. The others are handled in typer sequence, as before. The motivation of the change is that some modifications to inferred types of symbols can be made in subclasses without running into ordering problems. More fixes for subCapture New setting -Ycc-debug for more info on capture variables Fix subCapture in frozen state Previously, we still OKed two empty variables to be compared with subcapture in the frozen state. This should give an error. Direct comparisons of dependent function types Revert: Special treatment of dependent functions in TypeComparer change test Also treat explicit capturing type arguments as boxed Print subcapturing steps in -explain traces Don't decorate type variables with additional capture sets Boxed CapturingTypes Drop unsound capture suppression if expected type is boxed If expected type is boxed, the expression still contributes to the captured variables of its environment. Re-infer result types of anonymous functions Keep erased implicit args Special treatment of dependent functions in TypeComparer Fix addFunctionRefinements Always print refined function types as dependent functions. Makes it easier to see what goes on. Make CaptureSet ++ and ** simplify more Refine function types when reinferring so that they can be dependent Fix avoidance problem when typing blocks We should not pass en expected type when rechecking the expression of a block since that can add local references to global capture set variables. Also: tests for lists and pairs Print empty variables with "?" Fix printing untyped annotations Fix printing annotations in trees Drop redundant code Refactor map operations on capture sets Intoduce Bi-Mapped CaptureSets Report an error is a simply mapped capture set gets new elements that do not come from the original souurce. Introduce a new abstraction of bi-mapped sets that accept new elements and propagate them to the original source. Add map operation to SimpleIdentitySet Restrict tracked class parameters to vals Handle local classes and secondary constructors Fix CapturingType precedence when printing First stab at handling classes Bug fixes 1. Fix canBeTracked for TermRefs only TermRefs where prefix is NoPrefix or `this` can be tracked. The others have to be widened. 2. Fix rule for comparing capture refs on the left 3. Be more careful where comparisons are frozen Capture checker for functions --- compiler/src/dotty/tools/dotc/Compiler.scala | 5 +- compiler/src/dotty/tools/dotc/Run.scala | 3 +- .../src/dotty/tools/dotc/ast/Desugar.scala | 5 + compiler/src/dotty/tools/dotc/ast/Trees.scala | 8 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 11 + .../tools/dotc/cc/CaptureAnnotation.scala | 63 ++ .../src/dotty/tools/dotc/cc/CaptureOps.scala | 82 +++ .../src/dotty/tools/dotc/cc/CaptureSet.scala | 577 ++++++++++++++++++ .../dotty/tools/dotc/cc/CapturingType.scala | 21 + .../src/dotty/tools/dotc/config/Config.scala | 4 + .../dotty/tools/dotc/config/Printers.scala | 1 + .../tools/dotc/config/ScalaSettings.scala | 2 + .../dotty/tools/dotc/core/Annotations.scala | 6 +- .../dotty/tools/dotc/core/Definitions.scala | 18 +- .../tools/dotc/core/OrderingConstraint.scala | 4 + .../src/dotty/tools/dotc/core/Phases.scala | 14 +- .../src/dotty/tools/dotc/core/StdNames.scala | 5 +- .../dotty/tools/dotc/core/Substituters.scala | 6 +- .../tools/dotc/core/SymDenotations.scala | 10 +- .../dotty/tools/dotc/core/TypeComparer.scala | 108 +++- .../dotty/tools/dotc/core/TypeErrors.scala | 1 + .../src/dotty/tools/dotc/core/TypeOps.scala | 28 +- .../src/dotty/tools/dotc/core/Types.scala | 223 ++++++- .../src/dotty/tools/dotc/core/Variances.scala | 1 + .../tools/dotc/core/tasty/TreeUnpickler.scala | 10 +- .../dotty/tools/dotc/parsing/Parsers.scala | 34 +- .../src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- .../tools/dotc/printing/PlainPrinter.scala | 28 +- .../dotty/tools/dotc/printing/Printer.scala | 8 +- .../tools/dotc/printing/RefinedPrinter.scala | 23 +- .../dotty/tools/dotc/reporting/messages.scala | 1 - .../src/dotty/tools/dotc/sbt/ExtractAPI.scala | 1 + .../tools/dotc/transform/EmptyPhase.scala | 19 + .../dotty/tools/dotc/transform/Recheck.scala | 238 ++++++-- .../tools/dotc/transform/TreeChecker.scala | 4 +- .../dotc/transform/TryCatchPatterns.scala | 2 +- .../tools/dotc/transform/TypeTestsCasts.scala | 2 +- .../tools/dotc/typer/CheckCaptures.scala | 468 ++++++++++++++ .../src/dotty/tools/dotc/typer/Checking.scala | 6 +- .../dotty/tools/dotc/typer/Inferencing.scala | 9 +- .../dotty/tools/dotc/typer/RefChecks.scala | 2 +- .../dotty/tools/dotc/typer/TypeAssigner.scala | 25 +- .../src/dotty/tools/dotc/typer/Typer.scala | 6 +- .../tools/dotc/util/SimpleIdentitySet.scala | 13 + .../dotty/tools/dotc/CompilationTests.scala | 3 + library/src-bootstrapped/scala/Retains.scala | 6 + .../scala/annotation/ability.scala | 9 + .../neg-custom-args/captures/capt-wf.scala | 19 + .../neg-custom-args/captures/try2.check | 38 ++ .../neg-custom-args/captures/try2.scala | 55 ++ tests/disabled/pos/lazylist.scala | 51 ++ .../allow-deep-subtypes}/i9325.scala | 0 tests/neg-custom-args/capt-wf.scala | 35 ++ tests/neg-custom-args/captures/bounded.scala | 14 + tests/neg-custom-args/captures/boxmap.check | 7 + tests/neg-custom-args/captures/boxmap.scala | 14 + tests/neg-custom-args/captures/byname.scala | 10 + .../captures/capt-box-env.scala | 12 + tests/neg-custom-args/captures/capt-box.scala | 13 + .../captures/capt-depfun.scala | 7 + .../captures/capt-depfun2.scala | 10 + tests/neg-custom-args/captures/capt-env.scala | 13 + .../neg-custom-args/captures/capt-test.scala | 26 + .../captures/capt-wf-typer.scala | 10 + tests/neg-custom-args/captures/capt1.check | 46 ++ tests/neg-custom-args/captures/capt1.scala | 34 ++ tests/neg-custom-args/captures/capt2.scala | 9 + tests/neg-custom-args/captures/capt3.scala | 26 + tests/neg-custom-args/captures/cc1.scala | 4 + tests/neg-custom-args/captures/classes.scala | 12 + tests/neg-custom-args/captures/io.scala | 22 + tests/neg-custom-args/captures/lazylist.check | 42 ++ tests/neg-custom-args/captures/lazylist.scala | 41 ++ tests/neg-custom-args/captures/lazyref.check | 28 + tests/neg-custom-args/captures/lazyref.scala | 25 + tests/neg-custom-args/captures/try.check | 25 + tests/neg-custom-args/captures/try.scala | 53 ++ tests/neg-custom-args/captures/try3.scala | 27 + tests/neg/multiLineOps.scala | 2 +- tests/neg/polymorphic-functions1.check | 7 + tests/neg/polymorphic-functions1.scala | 1 + tests/pos-custom-args/captures/bounded.scala | 14 + .../captures/boxmap-paper.scala | 38 ++ tests/pos-custom-args/captures/boxmap.scala | 20 + tests/pos-custom-args/captures/byname.scala | 10 + .../captures/capt-depfun.scala | 18 + .../captures/capt-depfun2.scala | 8 + .../pos-custom-args/captures/capt-test.scala | 35 ++ tests/pos-custom-args/captures/capt0.scala | 7 + tests/pos-custom-args/captures/capt1.scala | 27 + tests/pos-custom-args/captures/capt2.scala | 20 + .../pos-custom-args/captures/cc-expand.scala | 21 + tests/pos-custom-args/captures/classes.scala | 34 ++ .../pos-custom-args/captures/iterators.scala | 23 + tests/pos-custom-args/captures/lazyref.scala | 25 + .../captures/list-encoding.scala | 23 + tests/pos-custom-args/captures/lists.scala | 91 +++ tests/pos-custom-args/captures/pairs.scala | 33 + tests/pos-custom-args/captures/try.scala | 26 + tests/pos-custom-args/captures/try3.scala | 51 ++ 100 files changed, 3219 insertions(+), 168 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala create mode 100644 compiler/src/dotty/tools/dotc/cc/CaptureOps.scala create mode 100644 compiler/src/dotty/tools/dotc/cc/CaptureSet.scala create mode 100644 compiler/src/dotty/tools/dotc/cc/CapturingType.scala create mode 100644 compiler/src/dotty/tools/dotc/transform/EmptyPhase.scala create mode 100644 compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala create mode 100644 library/src-bootstrapped/scala/Retains.scala create mode 100644 library/src-bootstrapped/scala/annotation/ability.scala create mode 100644 tests/disabled/neg-custom-args/captures/capt-wf.scala create mode 100644 tests/disabled/neg-custom-args/captures/try2.check create mode 100644 tests/disabled/neg-custom-args/captures/try2.scala create mode 100644 tests/disabled/pos/lazylist.scala rename tests/{neg => neg-custom-args/allow-deep-subtypes}/i9325.scala (100%) create mode 100644 tests/neg-custom-args/capt-wf.scala create mode 100644 tests/neg-custom-args/captures/bounded.scala create mode 100644 tests/neg-custom-args/captures/boxmap.check create mode 100644 tests/neg-custom-args/captures/boxmap.scala create mode 100644 tests/neg-custom-args/captures/byname.scala create mode 100644 tests/neg-custom-args/captures/capt-box-env.scala create mode 100644 tests/neg-custom-args/captures/capt-box.scala create mode 100644 tests/neg-custom-args/captures/capt-depfun.scala create mode 100644 tests/neg-custom-args/captures/capt-depfun2.scala create mode 100644 tests/neg-custom-args/captures/capt-env.scala create mode 100644 tests/neg-custom-args/captures/capt-test.scala create mode 100644 tests/neg-custom-args/captures/capt-wf-typer.scala create mode 100644 tests/neg-custom-args/captures/capt1.check create mode 100644 tests/neg-custom-args/captures/capt1.scala create mode 100644 tests/neg-custom-args/captures/capt2.scala create mode 100644 tests/neg-custom-args/captures/capt3.scala create mode 100644 tests/neg-custom-args/captures/cc1.scala create mode 100644 tests/neg-custom-args/captures/classes.scala create mode 100644 tests/neg-custom-args/captures/io.scala create mode 100644 tests/neg-custom-args/captures/lazylist.check create mode 100644 tests/neg-custom-args/captures/lazylist.scala create mode 100644 tests/neg-custom-args/captures/lazyref.check create mode 100644 tests/neg-custom-args/captures/lazyref.scala create mode 100644 tests/neg-custom-args/captures/try.check create mode 100644 tests/neg-custom-args/captures/try.scala create mode 100644 tests/neg-custom-args/captures/try3.scala create mode 100644 tests/neg/polymorphic-functions1.check create mode 100644 tests/neg/polymorphic-functions1.scala create mode 100644 tests/pos-custom-args/captures/bounded.scala create mode 100644 tests/pos-custom-args/captures/boxmap-paper.scala create mode 100644 tests/pos-custom-args/captures/boxmap.scala create mode 100644 tests/pos-custom-args/captures/byname.scala create mode 100644 tests/pos-custom-args/captures/capt-depfun.scala create mode 100644 tests/pos-custom-args/captures/capt-depfun2.scala create mode 100644 tests/pos-custom-args/captures/capt-test.scala create mode 100644 tests/pos-custom-args/captures/capt0.scala create mode 100644 tests/pos-custom-args/captures/capt1.scala create mode 100644 tests/pos-custom-args/captures/capt2.scala create mode 100644 tests/pos-custom-args/captures/cc-expand.scala create mode 100644 tests/pos-custom-args/captures/classes.scala create mode 100644 tests/pos-custom-args/captures/iterators.scala create mode 100644 tests/pos-custom-args/captures/lazyref.scala create mode 100644 tests/pos-custom-args/captures/list-encoding.scala create mode 100644 tests/pos-custom-args/captures/lists.scala create mode 100644 tests/pos-custom-args/captures/pairs.scala create mode 100644 tests/pos-custom-args/captures/try.scala create mode 100644 tests/pos-custom-args/captures/try3.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 57ede3c621f2..94a5ef473fc8 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -4,6 +4,7 @@ package dotc import core._ import Contexts._ import typer.{TyperPhase, RefChecks} +import cc.CheckCaptures import parsing.Parser import Phases.Phase import transform._ @@ -78,6 +79,8 @@ class Compiler { new SpecializeApplyMethods, // Adds specialized methods to FunctionN new TryCatchPatterns, // Compile cases in try/catch new PatternMatcher) :: // Compile pattern matches + List(new PreRecheck) :: // Preparations for check captures phase, enabled under -Ycc + List(new CheckCaptures) :: // Check captures, enabled under -Ycc List(new ElimOpaque, // Turn opaque into normal aliases new sjs.ExplicitJSClasses, // Make all JS classes explicit (Scala.js only) new ExplicitOuter, // Add accessors to outer classes from nested ones. @@ -101,8 +104,6 @@ class Compiler { new TupleOptimizations, // Optimize generic operations on tuples new LetOverApply, // Lift blocks from receivers of applications new ArrayConstructors) :: // Intercept creation of (non-generic) arrays and intrinsify. - List(new PreRecheck) :: // Preparations for recheck phase, enabled under -Yrecheck - List(new TestRecheck) :: // Test rechecking, enabled under -Yrecheck List(new Erasure) :: // Rewrite types to JVM model, erasing all type parameters, abstract types and refinements. List(new ElimErasedValueType, // Expand erased value types to their underlying implmementation types new PureStats, // Remove pure stats from blocks diff --git a/compiler/src/dotty/tools/dotc/Run.scala b/compiler/src/dotty/tools/dotc/Run.scala index 8f0bc395879e..f9152e8294c6 100644 --- a/compiler/src/dotty/tools/dotc/Run.scala +++ b/compiler/src/dotty/tools/dotc/Run.scala @@ -19,7 +19,6 @@ import reporting.{Suppression, Action, Profile, ActiveProfile, NoProfile} import reporting.Diagnostic import reporting.Diagnostic.Warning import rewrites.Rewrites - import profile.Profiler import printing.XprintMode import typer.ImplicitRunInfo @@ -294,7 +293,7 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint val fusedPhase = ctx.phase.prevMega val echoHeader = f"[[syntax trees at end of $fusedPhase%25s]] // ${unit.source}" val tree = if ctx.isAfterTyper then unit.tpdTree else unit.untpdTree - val treeString = tree.show(using ctx.withProperty(XprintMode, Some(()))) + val treeString = fusedPhase.show(tree) last match { case SomePrintedTree(phase, lastTreeString) if lastTreeString == treeString => diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 10d4fed7f058..bc3baa2c6c54 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1810,6 +1810,9 @@ object desugar { flatTree(pats1 map (makePatDef(tree, mods, _, rhs))) case ext: ExtMethods => Block(List(ext), Literal(Constant(())).withSpan(ext.span)) + case CapturingTypeTree(refs, parent) => + val annot = New(scalaDot(tpnme.retains), List(refs)) + Annotated(parent, annot) } desugared.withSpan(tree.span) } @@ -1946,6 +1949,8 @@ object desugar { case _ => traverseChildren(tree) } }.traverse(expr) + case CapturingTypeTree(refs, parent) => + collect(parent) case _ => } collect(tree) diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 41077637d92e..1159d13d5aef 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -253,16 +253,10 @@ object Trees { /** Tree's denotation can be derived from its type */ abstract class DenotingTree[-T >: Untyped](implicit @constructorOnly src: SourceFile) extends Tree[T] { type ThisTree[-T >: Untyped] <: DenotingTree[T] - override def denot(using Context): Denotation = typeOpt match { + override def denot(using Context): Denotation = typeOpt.stripped match case tpe: NamedType => tpe.denot case tpe: ThisType => tpe.cls.denot - case tpe: AnnotatedType => tpe.stripAnnots match { - case tpe: NamedType => tpe.denot - case tpe: ThisType => tpe.cls.denot - case _ => NoDenotation - } case _ => NoDenotation - } } /** Tree's denot/isType/isTerm properties come from a subtree diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index a769df537f1a..6435ffd4b3c4 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -145,6 +145,9 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case Floating } + /** {x1, ..., xN} T (only relevant under -Ycc) */ + case class CapturingTypeTree(refs: List[Tree], parent: Tree)(implicit @constructorOnly src: SourceFile) extends TypTree + /** Short-lived usage in typer, does not need copy/transform/fold infrastructure */ case class DependentTypeTree(tp: List[Symbol] => Type)(implicit @constructorOnly src: SourceFile) extends Tree @@ -647,6 +650,10 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case tree: Number if (digits == tree.digits) && (kind == tree.kind) => tree case _ => finalize(tree, untpd.Number(digits, kind)) } + def CapturingTypeTree(tree: Tree)(refs: List[Tree], parent: Tree)(using Context): Tree = tree match + case tree: CapturingTypeTree if (refs eq tree.refs) && (parent eq tree.parent) => tree + case _ => finalize(tree, untpd.CapturingTypeTree(refs, parent)) + def TypedSplice(tree: Tree)(splice: tpd.Tree)(using Context): ProxyTree = tree match { case tree: TypedSplice if splice `eq` tree.splice => tree case _ => finalize(tree, untpd.TypedSplice(splice)(using ctx)) @@ -710,6 +717,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { tree case MacroTree(expr) => cpy.MacroTree(tree)(transform(expr)) + case CapturingTypeTree(refs, parent) => + cpy.CapturingTypeTree(tree)(transform(refs), transform(parent)) case _ => super.transformMoreCases(tree) } @@ -769,6 +778,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { this(x, splice) case MacroTree(expr) => this(x, expr) + case CapturingTypeTree(refs, parent) => + this(this(x, refs), parent) case _ => super.foldMoreCases(x, tree) } diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala new file mode 100644 index 000000000000..5f73b50a6bbe --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -0,0 +1,63 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.* +import ast.Trees.* +import ast.{tpd, untpd} +import Decorators.* +import config.Printers.capt +import printing.Printer +import printing.Texts.Text + + +case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean) extends Annotation: + import CaptureAnnotation.* + import tpd.* + + override def tree(using Context) = + val elems = refs.elems.toList.map { + case cr: TermRef => ref(cr) + case cr: TermParamRef => untpd.Ident(cr.paramName).withType(cr) + case cr: ThisType => This(cr.cls) + } + val arg = repeated(elems, TypeTree(defn.AnyType)) + New(symbol.typeRef, arg :: Nil) + + override def symbol(using Context) = defn.RetainsAnnot + + override def derivedAnnotation(tree: Tree)(using Context): Annotation = + unsupported("derivedAnnotation(Tree)") + + def derivedAnnotation(refs: CaptureSet, boxed: Boolean)(using Context): Annotation = + if (this.refs eq refs) && (this.boxed == boxed) then this + else CaptureAnnotation(refs, boxed) + + override def sameAnnotation(that: Annotation)(using Context): Boolean = that match + case CaptureAnnotation(refs2, boxed2) => refs == refs2 && boxed == boxed2 + case _ => false + + override def mapWith(tp: TypeMap)(using Context) = + val elems = refs.elems.toList + val elems1 = elems.mapConserve(tp) + if elems1 eq elems then this + else if elems1.forall(_.isInstanceOf[CaptureRef]) + then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), boxed) + else EmptyAnnotation + + override def refersToParamOf(tl: TermLambda)(using Context): Boolean = + refs.elems.exists { + case TermParamRef(tl1, _) => tl eq tl1 + case _ => false + } + + override def toText(printer: Printer): Text = refs.toText(printer) + + override def hash: Int = (refs.hashCode << 1) | (if boxed then 1 else 0) + + override def eql(that: Annotation) = that match + case that: CaptureAnnotation => (this.refs eq that.refs) && (this.boxed == boxed) + case _ => false + +end CaptureAnnotation diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala new file mode 100644 index 000000000000..09064314b1bf --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -0,0 +1,82 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.*, Annotations.* +import ast.{tpd, untpd} +import Decorators.* +import config.Printers.capt +import util.Property.Key +import tpd.* + +private val Captures: Key[CaptureSet] = Key() +private val IsBoxed: Key[Unit] = Key() + +def retainedElems(tree: Tree)(using Context): List[Tree] = tree match + case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems + case _ => Nil + +extension (tree: Tree) + + def toCaptureRef(using Context): CaptureRef = tree.tpe.asInstanceOf[CaptureRef] + + def toCaptureSet(using Context): CaptureSet = + tree.getAttachment(Captures) match + case Some(refs) => refs + case None => + val refs = CaptureSet(retainedElems(tree).map(_.toCaptureRef)*) + .showing(i"toCaptureSet $tree --> $result", capt) + tree.putAttachment(Captures, refs) + refs + + def isBoxedCapturing(using Context): Boolean = + tree.hasAttachment(IsBoxed) + + def setBoxedCapturing()(using Context): Unit = + tree.putAttachment(IsBoxed, ()) + +extension (tp: Type) + + def derivedCapturingType(parent: Type, refs: CaptureSet)(using Context): Type = tp match + case CapturingType(p, r, b) => + if (parent eq p) && (refs eq r) then tp + else CapturingType(parent, refs, b) + + /** If this is type variable instantiated or upper bounded with a capturing type, + * the capture set associated with that type. Extended to and-or types and + * type proxies in the obvious way. If a term has a type with a boxed captureset, + * that captureset counts towards the capture variables of the envirionment. + */ + def boxedCaptured(using Context): CaptureSet = + def getBoxed(tp: Type): CaptureSet = tp match + case CapturingType(_, refs, boxed) => if boxed then refs else CaptureSet.empty + case tp: TypeProxy => getBoxed(tp.superType) + case tp: AndType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) + case tp: OrType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) + case _ => CaptureSet.empty + getBoxed(tp) + + def isBoxedCapturing(using Context) = !tp.boxedCaptured.isAlwaysEmpty + + def canHaveInferredCapture(using Context): Boolean = tp match + case tp: TypeRef if tp.symbol.isClass => + !tp.symbol.isValueClass && tp.symbol != defn.AnyClass + case _: TypeVar | _: TypeParamRef => + false + case tp: TypeProxy => + tp.superType.canHaveInferredCapture + case tp: AndType => + tp.tp1.canHaveInferredCapture && tp.tp2.canHaveInferredCapture + case tp: OrType => + tp.tp1.canHaveInferredCapture || tp.tp2.canHaveInferredCapture + case _ => + false + + def stripCapturing(using Context): Type = tp.dealiasKeepAnnots match + case CapturingType(parent, _, _) => + parent.stripCapturing + case atd @ AnnotatedType(parent, annot) => + atd.derivedAnnotatedType(parent.stripCapturing, annot) + case _ => + tp diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala new file mode 100644 index 000000000000..f8ca2f87e3c5 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -0,0 +1,577 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Flags.*, Contexts.*, Decorators.* +import config.Printers.capt +import Annotations.Annotation +import annotation.threadUnsafe +import annotation.constructorOnly +import annotation.internal.sharable +import reporting.trace +import printing.{Showable, Printer} +import printing.Texts.* +import util.{SimpleIdentitySet, Property} +import util.common.alwaysTrue +import scala.collection.mutable + +/** A class for capture sets. Capture sets can be constants or variables. + * Capture sets support inclusion constraints <:< where <:< is subcapturing. + * They also allow mapping with arbitrary functions from elements to capture sets, + * by supporting a monadic flatMap operation. That is, constraints can be + * of one of the following forms + * + * cs1 <:< cs2 + * cs1 = ∪ {f(x) | x ∈ cs2} + * + * where the `f`s are arbitrary functions from capture references to capture sets. + * We call the resulting constraint system "monadic set constraints". + */ +sealed abstract class CaptureSet extends Showable: + import CaptureSet.* + + /** The elements of this capture set. For capture variables, + * the elements known so far. + */ + def elems: Refs + + /** Is this capture set constant (i.e. not an unsolved capture variable)? + * Solved capture variables count as constant. + */ + def isConst: Boolean + + /** Is this capture set always empty? For capture veraiables, returns + * always false + */ + def isAlwaysEmpty: Boolean + + /** Is this capture set definitely non-empty? */ + final def isNotEmpty: Boolean = !elems.isEmpty + + /** Cast to variable. @pre: @isConst */ + def asVar: Var = + assert(!isConst) + asInstanceOf[Var] + + /** Add new elements to this capture set if allowed. + * @pre `newElems` is not empty and does not overlap with `this.elems`. + * Constant capture sets never allow to add new elements. + * Variables allow it if and only if the new elements can be included + * in all their supersets. + * @param origin The set where the elements come from, or `empty` if not known. + * @return CompareResult.OK if elements were added, or a conflicting + * capture set that prevents addition otherwise. + */ + protected def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult + + /** If this is a variable, add `cs` as a super set */ + protected def addSuper(cs: CaptureSet)(using Context, VarState): CompareResult + + /** If `cs` is a variable, add this capture set as one of its super sets */ + protected def addSub(cs: CaptureSet)(using Context): this.type = + cs.addSuper(this)(using ctx, UnrecordedState) + this + + /** Try to include all references of `elems` that are not yet accounted by this + * capture set. Inclusion is via `addNewElems`. + * @param origin The set where the elements come from, or `empty` if not known. + * @return CompareResult.OK if all unaccounted elements could be added, + * capture set that prevents addition otherwise. + */ + protected final def tryInclude(elems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + val unaccounted = elems.filter(!accountsFor(_)) + if unaccounted.isEmpty then CompareResult.OK + else addNewElems(unaccounted, origin) + + protected final def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = + if accountsFor(elem) then CompareResult.OK + else addNewElems(elem.singletonCaptureSet.elems, origin) + + extension (x: CaptureRef) private def subsumes(y: CaptureRef) = + (x eq y) + || y.match + case y: TermRef => y.prefix eq x // ^^^ y.prefix.subsumes(x) ? + case _ => false + + /** {x} <:< this where <:< is subcapturing, but treating all variables + * as frozen. + */ + def accountsFor(x: CaptureRef)(using ctx: Context): Boolean = + reporting.trace(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true) { + elems.exists(_.subsumes(x)) + || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK + } + + /** The subcapturing test */ + final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = + subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + + private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = + def recur(elems: List[CaptureRef]): CompareResult = elems match + case elem :: elems1 => + var result = that.tryInclude(elem, this) + if !result.isOK && !elem.isRootCapability && summon[VarState] != FrozenState then + result = elem.captureSetOfInfo.subCaptures(that) + if result.isOK then + recur(elems1) + else + varState.abort() + result + case Nil => + addSuper(that) + recur(elems.toList) + .showing(i"subcaptures $this <:< $that = ${result.show}", capt) + + def =:= (that: CaptureSet)(using Context): Boolean = + this.subCaptures(that, frozen = true).isOK + && that.subCaptures(this, frozen = true).isOK + + /** The smallest capture set (via <:<) that is a superset of both + * `this` and `that` + */ + def ++ (that: CaptureSet)(using Context): CaptureSet = + if this.subCaptures(that, frozen = true).isOK then that + else if that.subCaptures(this, frozen = true).isOK then this + else if this.isConst && that.isConst then Const(this.elems ++ that.elems) + else Var(this.elems ++ that.elems).addSub(this).addSub(that) + + /** The smallest superset (via <:<) of this capture set that also contains `ref`. + */ + def + (ref: CaptureRef)(using Context): CaptureSet = + this ++ ref.singletonCaptureSet + + /** The largest capture set (via <:<) that is a subset of both `this` and `that` + */ + def **(that: CaptureSet)(using Context): CaptureSet = + 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(elems.intersect(that.elems)) + else if that.isConst then Intersected(this.asVar, that) + else Intersected(that.asVar, this) + + def -- (that: CaptureSet.Const)(using Context): CaptureSet = + val elems1 = elems.filter(!that.accountsFor(_)) + if elems1.size == elems.size then this + else if this.isConst then Const(elems1) + else Diff(asVar, that) + + def - (ref: CaptureRef)(using Context): CaptureSet = + this -- ref.singletonCaptureSet + + def filter(p: CaptureRef => Boolean)(using Context): CaptureSet = + if this.isConst then Const(elems.filter(p)) + else Filtered(asVar, p) + + /** capture set obtained by applying `f` to all elements of the current capture set + * and joining the results. If the current capture set is a variable, the same + * transformation is applied to all future additions of new elements. + */ + def map(tm: TypeMap)(using Context): CaptureSet = tm match + case tm: BiTypeMap => + val mappedElems = elems.map(tm.forward) + if isConst then Const(mappedElems) + else BiMapped(asVar, tm, mappedElems) + case _ => + val mapped = mapRefs(elems, tm, tm.variance) + if isConst then mapped + else Mapped(asVar, tm, tm.variance, mapped) + + def substParams(tl: BindingType, to: List[Type])(using Context) = + map(Substituters.SubstParamsMap(tl, to)) + + /** An upper approximation of this capture set. This is the set itself + * except for real (non-mapped, non-filtered) capture set variables, where + * it is the intersection of all upper approximations of known supersets + * of the variable. + * The upper approximation is meaningful only if it is constant. If not, + * `upperApprox` can return an arbitrary capture set variable. + */ + protected def upperApprox(origin: CaptureSet)(using Context): CaptureSet + + protected def propagateSolved()(using Context): Unit = () + + def toRetainsTypeArg(using Context): Type = + assert(isConst) + ((NoType: Type) /: elems) ((tp, ref) => + if tp.exists then OrType(tp, ref, soft = false) else ref) + + def toRegularAnnotation(using Context): Annotation = + Annotation(CaptureAnnotation(this, boxed = false).tree) + + override def toText(printer: Printer): Text = + Str("{") ~ Text(elems.toList.map(printer.toTextCaptureRef), ", ") ~ Str("}") + +object CaptureSet: + type Refs = SimpleIdentitySet[CaptureRef] + type Vars = SimpleIdentitySet[Var] + type Deps = SimpleIdentitySet[CaptureSet] + + /** If set to `true`, capture stack traces that tell us where sets are created */ + private final val debugSets = false + + private val emptySet = SimpleIdentitySet.empty + @sharable private var varId = 0 + + val empty: CaptureSet.Const = Const(emptySet) + + /** The universal capture set `{*}` */ + def universal(using Context): CaptureSet = + defn.captureRoot.termRef.singletonCaptureSet + + /** Used as a recursion brake */ + @sharable private[dotc] val Pending = Const(SimpleIdentitySet.empty) + + def apply(elems: CaptureRef*)(using Context): CaptureSet.Const = + if elems.isEmpty then empty + else Const(SimpleIdentitySet(elems.map(_.normalizedRef)*)) + + def apply(elems: Refs)(using Context): CaptureSet.Const = + if elems.isEmpty then empty else Const(elems) + + class Const private[CaptureSet] (val elems: Refs) extends CaptureSet: + assert(elems != null) + def isConst = true + def isAlwaysEmpty = elems.isEmpty + + def addNewElems(elems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + CompareResult.fail(this) + + def addSuper(cs: CaptureSet)(using Context, VarState) = CompareResult.OK + + def upperApprox(origin: CaptureSet)(using Context): CaptureSet = this + + override def toString = elems.toString + end Const + + class Var(initialElems: Refs = emptySet) extends CaptureSet: + val id = + varId += 1 + varId + + private var isSolved: Boolean = false + + var elems: Refs = initialElems + var deps: Deps = emptySet + def isConst = isSolved + def isAlwaysEmpty = false + + private def recordElemsState()(using VarState): Boolean = + varState.getElems(this) match + case None => varState.putElems(this, elems) + case _ => true + + private[CaptureSet] def recordDepsState()(using VarState): Boolean = + varState.getDeps(this) match + case None => varState.putDeps(this, deps) + case _ => true + + def resetElems()(using state: VarState): Unit = + elems = state.elems(this) + + def resetDeps()(using state: VarState): Unit = + deps = state.deps(this) + + def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + if !isConst && recordElemsState() then + elems ++= newElems + // assert(id != 2 || elems.size != 2, this) + (CompareResult.OK /: deps) { (r, dep) => + r.andAlso(dep.tryInclude(newElems, this)) + } + else + CompareResult.fail(this) + + def addSuper(cs: CaptureSet)(using Context, VarState): CompareResult = + if (cs eq this) || cs.elems.contains(defn.captureRoot.termRef) || isConst then + CompareResult.OK + else if recordDepsState() then + deps += cs + CompareResult.OK + else + CompareResult.fail(this) + + private var computingApprox = false + + final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = + if computingApprox then universal + else if isConst then this + else + computingApprox = true + try computeApprox(origin).ensuring(_.isConst) + finally computingApprox = false + + protected def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + (universal /: deps) { (acc, sup) => acc ** sup.upperApprox(this) } + + def solve(variance: Int)(using Context): Unit = + if variance < 0 && !isConst then + val approx = upperApprox(empty) + //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") + if approx.isConst then + val newElems = approx.elems -- elems + if newElems.isEmpty || addNewElems(newElems, empty)(using ctx, VarState()).isOK then + markSolved() + + def markSolved()(using Context): Unit = + isSolved = true + deps.foreach(_.propagateSolved()) + + protected def ids(using Context): String = + val trail = this.match + case dv: DerivedVar => dv.source.ids + case _ => "" + s"$id${getClass.getSimpleName.take(1)}$trail" + + override def toText(printer: Printer): Text = inContext(printer.printerContext) { + for vars <- ctx.property(ShownVars) do vars += this + super.toText(printer) ~ (Str(ids) provided !isConst && ctx.settings.YccDebug.value) + } + + override def toString = s"Var$id$elems" + end Var + + abstract class DerivedVar(initialElems: Refs)(using @constructorOnly ctx: Context) + extends Var(initialElems): + def source: Var + + addSub(source) + + override def propagateSolved()(using Context) = + if source.isConst && !isConst then markSolved() + end DerivedVar + + /** A variable that changes when `source` changes, where all additional new elements are mapped + * using ∪ { f(x) | x <- elems } + */ + class Mapped private[CaptureSet] + (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) + extends DerivedVar(initial.elems): + addSub(initial) + val stack = if debugSets then (new Throwable).getStackTrace().take(20) else null + + private def whereCreated(using Context): String = + if stack == null then "" + else i""" + |Stack trace of variable creation:" + |${stack.mkString("\n")}""" + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + val added = + if origin eq source then + mapRefs(newElems, tm, variance) + else + if variance <= 0 && !origin.isConst && (origin ne initial) then + report.warning(i"trying to add elems $newElems from unrecognized source $origin of mapped set $this$whereCreated") + return CompareResult.fail(this) + Const(newElems) + super.addNewElems(added.elems, origin) + .andAlso { + if added.isConst then CompareResult.OK + else if added.asVar.recordDepsState() then { addSub(added); CompareResult.OK } + else CompareResult.fail(this) + } + + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + if source eq origin then universal + else source.upperApprox(this).map(tm) + + override def propagateSolved()(using Context) = + if initial.isConst then super.propagateSolved() + + override def toString = s"Mapped$id($source, elems = $elems)" + end Mapped + + class BiMapped private[CaptureSet] + (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) + extends DerivedVar(initialElems): + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + if origin eq source then + super.addNewElems(newElems.map(bimap.forward), origin) + else + super.addNewElems(newElems, origin) + .andAlso { + source.tryInclude(newElems.map(bimap.backward), this) + .showing(i"propagating new elems $newElems backward from $this to $source", capt) + } + + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + val supApprox = super.computeApprox(this) + if source eq origin then supApprox.map(bimap.inverseTypeMap) + else source.upperApprox(this).map(bimap) ** supApprox + + override def toString = s"BiMapped$id($source, elems = $elems)" + end BiMapped + + /** A variable with elements given at any time as { x <- source.elems | p(x) } */ + class Filtered private[CaptureSet] + (val source: Var, p: CaptureRef => Boolean)(using @constructorOnly ctx: Context) + extends DerivedVar(source.elems.filter(p)): + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + super.addNewElems(newElems.filter(p), origin) + + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + if source eq origin then universal + else source.upperApprox(this).filter(p) + + override def toString = s"${getClass.getSimpleName}$id($source, elems = $elems)" + end Filtered + + /** A variable with elements given at any time as { x <- source.elems | !other.accountsFor(x) } */ + class Diff(source: Var, other: Const)(using Context) + extends Filtered(source, !other.accountsFor(_)) + + /** A variable with elements given at any time as { x <- source.elems | other.accountsFor(x) } */ + class Intersected(source: Var, other: CaptureSet)(using Context) + extends Filtered(source, other.accountsFor(_)): + addSub(other) + + def extrapolateCaptureRef(r: CaptureRef, tm: TypeMap, variance: Int)(using Context): CaptureSet = + val r1 = tm(r) + val upper = r1.captureSet + def isExact = + upper.isAlwaysEmpty || upper.isConst && upper.elems.size == 1 && upper.elems.contains(r1) + if variance > 0 || isExact then upper + else if variance < 0 then CaptureSet.empty + else assert(false, i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting") + + def mapRefs(xs: Refs, f: CaptureRef => CaptureSet)(using Context): CaptureSet = + ((empty: CaptureSet) /: xs)((cs, x) => cs ++ f(x)) + + def mapRefs(xs: Refs, tm: TypeMap, variance: Int)(using Context): CaptureSet = + mapRefs(xs, extrapolateCaptureRef(_, tm, variance)) + + type CompareResult = CompareResult.Type + + /** None = ok, Some(cs) = failure since not a subset of cs */ + object CompareResult: + opaque type Type = CaptureSet + val OK: Type = Const(emptySet) + def fail(cs: CaptureSet): Type = cs + extension (result: Type) + def isOK: Boolean = result eq OK + def blocking: CaptureSet = result + def show: String = if result.isOK then "OK" else result.toString + def andAlso(op: Context ?=> Type)(using Context): Type = if result.isOK then op else result + + class VarState: + private val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap + private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap + + def elems(v: Var): Refs = elemsMap(v) + def getElems(v: Var): Option[Refs] = elemsMap.get(v) + def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } + + def deps(v: Var): Deps = depsMap(v) + def getDeps(v: Var): Option[Deps] = depsMap.get(v) + def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } + + def abort(): Unit = + elemsMap.keysIterator.foreach(_.resetElems()(using this)) + depsMap.keysIterator.foreach(_.resetDeps()(using this)) + end VarState + + @sharable + object FrozenState extends VarState: + override def putElems(v: Var, refs: Refs) = false + override def putDeps(v: Var, deps: Deps) = false + override def abort(): Unit = () + + @sharable + object UnrecordedState extends VarState: + override def putElems(v: Var, refs: Refs) = true + override def putDeps(v: Var, deps: Deps) = true + override def abort(): Unit = () + + def varState(using state: VarState): VarState = state + + def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = + CaptureSet.empty + /* + def captureSetOf(tp: Type): CaptureSet = tp match + case tp: TypeRef if tp.symbol.is(ParamAccessor) => + def mapArg(accs: List[Symbol], tps: List[Type]): CaptureSet = accs match + case acc :: accs1 if tps.nonEmpty => + if acc == tp.symbol then tps.head.captureSet + else mapArg(accs1, tps.tail) + case _ => + empty + mapArg(cinfo.cls.paramAccessors, argTypes) + case _ => + tp.captureSet + val css = + for + parent <- cinfo.parents if parent.classSymbol == defn.RetainingClass + arg <- parent.argInfos + yield captureSetOf(arg) + css.foldLeft(empty)(_ ++ _) + */ + def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match + case ref: ThisType => + val declaredCaptures = ref.cls.givenSelfType.captureSet + ref.cls.paramAccessors.foldLeft(declaredCaptures) ((cs, acc) => + cs ++ acc.termRef.captureSetOfInfo) // ^^^ need to also include outer references of inner classes + .showing(i"cc info $ref with ${ref.cls.paramAccessors.map(_.termRef)}%, % = $result", capt) + case ref: TermRef if ref.isRootCapability => ref.singletonCaptureSet + case _ => ofType(ref.underlying) + + def ofType(tp: Type)(using Context): CaptureSet = + def recur(tp: Type): CaptureSet = tp.dealias match + case tp: TermRef => + tp.captureSet + case tp: TermParamRef => + tp.captureSet + case _: TypeRef | _: TypeParamRef => + empty + case CapturingType(parent, refs, _) => + recur(parent) ++ refs + case AppliedType(tycon, args) => + val cs = recur(tycon) + tycon.typeParams match + case tparams @ (LambdaParam(tl, _) :: _) => cs.substParams(tl, args) + case _ => cs + case tp: TypeProxy => + recur(tp.underlying) + case AndType(tp1, tp2) => + recur(tp1) ** recur(tp2) + case OrType(tp1, tp2) => + recur(tp1) ++ recur(tp2) + case tp: ClassInfo => + ofClass(tp, Nil) + case _ => + empty + recur(tp) + .showing(i"capture set of $tp = $result", capt) + + private val ShownVars: Property.Key[mutable.Set[Var]] = Property.Key() + + def withCaptureSetsExplained[T](op: Context ?=> T)(using ctx: Context): T = + if ctx.settings.YccDebug.value then + val shownVars = mutable.Set[Var]() + inContext(ctx.withProperty(ShownVars, Some(shownVars))) { + try op + finally + val reachable = mutable.Set[Var]() + val todo = mutable.Queue[Var]() ++= shownVars + def incl(cv: Var): Unit = + if !reachable.contains(cv) then todo += cv + while todo.nonEmpty do + val cv = todo.dequeue() + if !reachable.contains(cv) then + reachable += cv + cv.deps.foreach { + case cv: Var => incl(cv) + case _ => + } + cv match + case cv: DerivedVar => incl(cv.source) + case _ => + val allVars = reachable.toArray.sortBy(_.id) + println(i"Capture set dependencies:") + for cv <- allVars do + println(i" ${cv.show.padTo(20, ' ')} :: ${cv.deps.toList}%, %") + } + else op +end CaptureSet diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala new file mode 100644 index 000000000000..2eeb1ff41b72 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -0,0 +1,21 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.* + +object CapturingType: + + def apply(parent: Type, refs: CaptureSet, boxed: Boolean)(using Context): Type = + if refs.isAlwaysEmpty then parent + else AnnotatedType(parent, CaptureAnnotation(refs, boxed)) + + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, Boolean)] = + if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then + tp.annot match + case ann: CaptureAnnotation => Some((tp.parent, ann.refs, ann.boxed)) + case ann => Some((tp.parent, ann.tree.toCaptureSet, ann.tree.isBoxedCapturing)) + else None + +end CapturingType diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index 67545233136d..f000cfae6649 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -235,4 +235,8 @@ object Config { inline val checkLevelsOnConstraints = false inline val checkLevelsOnInstantiation = true + /** If true, print capturing types in the form `{c} T`. + * If false, print them in the form `T @retains(c)`. + */ + inline val printCaptureSetsAsPrefix = true } diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 9772f1c98cdf..ecb189de9bb3 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -12,6 +12,7 @@ object Printers { val default = new Printer + val capt = noPrinter val constr = noPrinter val core = noPrinter val checks = noPrinter diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 85d75a971e11..da53ea18da82 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -326,6 +326,8 @@ private sealed trait YSettings: val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") val Yrecheck: Setting[Boolean] = BooleanSetting("-Yrecheck", "Run type rechecks (test only)") + val Ycc: Setting[Boolean] = BooleanSetting("-Ycc", "Check captured references") + val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Debug info for captured references") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/core/Annotations.scala b/compiler/src/dotty/tools/dotc/core/Annotations.scala index 8eca364e4621..05210ec60811 100644 --- a/compiler/src/dotty/tools/dotc/core/Annotations.scala +++ b/compiler/src/dotty/tools/dotc/core/Annotations.scala @@ -45,7 +45,7 @@ object Annotations { /** The tree evaluation has finished. */ def isEvaluated: Boolean = true - /** Normally, type map over all tree nodes of this annotation, but can + /** Normally, applies a type map to all tree nodes of this annotation, but can * be overridden. Returns EmptyAnnotation if type type map produces a range * type, since ranges cannot be types of trees. */ @@ -83,6 +83,10 @@ object Annotations { def sameAnnotation(that: Annotation)(using Context): Boolean = symbol == that.symbol && tree.sameTree(that.tree) + + /** Operations for hash-consing, can be overridden */ + def hash: Int = System.identityHashCode(this) + def eql(that: Annotation) = this eq that } case class ConcreteAnnotation(t: Tree) extends Annotation: diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3e2373d3bd4b..62c4ae389028 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -14,6 +14,7 @@ import Comments.CommentsContext import Comments.Comment import util.Spans.NoSpan import Symbols.requiredModuleRef +import cc.{CapturingType, CaptureSet} import scala.annotation.tailrec @@ -146,11 +147,13 @@ class Definitions { private def enterMethod(cls: ClassSymbol, name: TermName, info: Type, flags: FlagSet = EmptyFlags): TermSymbol = newMethod(cls, name, info, flags).entered - private def enterAliasType(name: TypeName, tpe: Type, flags: FlagSet = EmptyFlags): TypeSymbol = { - val sym = newPermanentSymbol(ScalaPackageClass, name, flags, TypeAlias(tpe)) + private def enterPermanentSymbol(name: Name, info: Type, flags: FlagSet = EmptyFlags): Symbol = + val sym = newPermanentSymbol(ScalaPackageClass, name, flags, info) ScalaPackageClass.currentPackageDecls.enter(sym) sym - } + + private def enterAliasType(name: TypeName, tpe: Type, flags: FlagSet = EmptyFlags): TypeSymbol = + enterPermanentSymbol(name, TypeAlias(tpe), flags).asType private def enterBinaryAlias(name: TypeName, op: (Type, Type) => Type): TypeSymbol = enterAliasType(name, @@ -446,6 +449,7 @@ class Definitions { @tu lazy val andType: TypeSymbol = enterBinaryAlias(tpnme.AND, AndType(_, _)) @tu lazy val orType: TypeSymbol = enterBinaryAlias(tpnme.OR, OrType(_, _, soft = false)) + @tu lazy val captureRoot: TermSymbol = enterPermanentSymbol(nme.CAPTURE_ROOT, AnyType).asTerm /** Method representing a throw */ @tu lazy val throwMethod: TermSymbol = enterMethod(OpsPackageClass, nme.THROWkw, @@ -989,6 +993,9 @@ class Definitions { @tu lazy val FunctionalInterfaceAnnot: ClassSymbol = requiredClass("java.lang.FunctionalInterface") @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") + @tu lazy val SinceAnnot: ClassSymbol = requiredClass("scala.annotation.since") + @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.retains") + @tu lazy val AbilityAnnot: ClassSymbol = requiredClass("scala.annotation.ability") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") @@ -1593,6 +1600,9 @@ class Definitions { def isFunctionType(tp: Type)(using Context): Boolean = isNonRefinedFunction(tp.dropDependentRefinement) + def isFunctionOrPolyType(tp: RefinedType)(using Context): Boolean = + isFunctionType(tp) || (tp.parent.typeSymbol eq defn.PolyFunctionClass) + private def withSpecMethods(cls: ClassSymbol, bases: List[Name], paramTypes: Set[TypeRef]) = for base <- bases; tp <- paramTypes do cls.enter(newSymbol(cls, base.specializedName(List(tp)), Method, ExprType(tp))) @@ -1917,7 +1927,7 @@ class Definitions { this.initCtx = ctx if (!isInitialized) { // force initialization of every symbol that is synthesized or hijacked by the compiler - val forced = syntheticCoreClasses ++ syntheticCoreMethods ++ ScalaValueClasses() :+ JavaEnumClass + val forced = syntheticCoreClasses ++ syntheticCoreMethods ++ ScalaValueClasses() ++ List(JavaEnumClass, captureRoot) isInitialized = true } diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 54df9cdd9535..633ff03389d5 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -12,6 +12,7 @@ import config.Printers.constr import reflect.ClassTag import annotation.tailrec import annotation.internal.sharable +import cc.{CapturingType, derivedCapturingType} object OrderingConstraint { @@ -333,6 +334,9 @@ class OrderingConstraint(private val boundsMap: ParamBounds, case tp: TypeVar => val underlying1 = recur(tp.underlying, fromBelow) if underlying1 ne tp.underlying then underlying1 else tp + case CapturingType(parent, refs, _) => + val parent1 = recur(parent, fromBelow) + if parent1 ne parent then tp.derivedCapturingType(parent1, refs) else tp case tp: AnnotatedType => val parent1 = recur(tp.parent, fromBelow) if parent1 ne tp.parent then tp.derivedAnnotatedType(parent1, tp.annot) else tp diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 34c51f43bbd7..75c13835bc02 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -14,9 +14,11 @@ import dotty.tools.dotc.transform.MegaPhase._ import dotty.tools.dotc.transform._ import Periods._ import parsing.Parser +import printing.XprintMode import typer.{TyperPhase, RefChecks} +import cc.CheckCaptures import typer.ImportInfo.withRootImports -import ast.tpd +import ast.{tpd, untpd} import scala.annotation.internal.sharable import scala.util.control.NonFatal @@ -217,6 +219,7 @@ object Phases { private var myCountOuterAccessesPhase: Phase = _ private var myFlattenPhase: Phase = _ private var myGenBCodePhase: Phase = _ + private var myCheckCapturesPhase: Phase = _ final def parserPhase: Phase = myParserPhase final def typerPhase: Phase = myTyperPhase @@ -240,6 +243,7 @@ object Phases { final def countOuterAccessesPhase = myCountOuterAccessesPhase final def flattenPhase: Phase = myFlattenPhase final def genBCodePhase: Phase = myGenBCodePhase + final def checkCapturesPhase: Phase = myCheckCapturesPhase private def setSpecificPhases() = { def phaseOfClass(pclass: Class[?]) = phases.find(pclass.isInstance).getOrElse(NoPhase) @@ -265,7 +269,8 @@ object Phases { myFlattenPhase = phaseOfClass(classOf[Flatten]) myExplicitOuterPhase = phaseOfClass(classOf[ExplicitOuter]) myGettersPhase = phaseOfClass(classOf[Getters]) - myGenBCodePhase = phaseOfClass(classOf[GenBCode]) + myGenBCodePhase = phaseOfClass(classOf[GenBCode]) + myCheckCapturesPhase = phaseOfClass(classOf[CheckCaptures]) } final def isAfterTyper(phase: Phase): Boolean = phase.id > typerPhase.id @@ -318,6 +323,10 @@ object Phases { unitCtx.compilationUnit } + /** Convert a compilation unit's tree to a string; can be overridden */ + def show(tree: untpd.Tree)(using Context): String = + tree.show(using ctx.withProperty(XprintMode, Some(()))) + def description: String = phaseName /** Output should be checkable by TreeChecker */ @@ -448,6 +457,7 @@ object Phases { def lambdaLiftPhase(using Context): Phase = ctx.base.lambdaLiftPhase def flattenPhase(using Context): Phase = ctx.base.flattenPhase def genBCodePhase(using Context): Phase = ctx.base.genBCodePhase + def checkCapturesPhase(using Context): Phase = ctx.base.checkCapturesPhase def unfusedPhases(using Context): Array[Phase] = ctx.base.phases diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 9f128a71be7b..6a355d09f3cf 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -282,6 +282,8 @@ object StdNames { // ----- Term names ----------------------------------------- // Compiler-internal + val ANYname: N = "" + val CAPTURE_ROOT: N = "*" val CONSTRUCTOR: N = "" val STATIC_CONSTRUCTOR: N = "" val EVT2U: N = "evt2u$" @@ -348,6 +350,7 @@ object StdNames { val AppliedTypeTree: N = "AppliedTypeTree" val ArrayAnnotArg: N = "ArrayAnnotArg" val CAP: N = "CAP" + val ClassManifestFactory: N = "ClassManifestFactory" val Constant: N = "Constant" val ConstantType: N = "ConstantType" val Eql: N = "Eql" @@ -425,7 +428,6 @@ object StdNames { val canEqual_ : N = "canEqual" val canEqualAny : N = "canEqualAny" val checkInitialized: N = "checkInitialized" - val ClassManifestFactory: N = "ClassManifestFactory" val classOf: N = "classOf" val classType: N = "classType" val clone_ : N = "clone" @@ -563,6 +565,7 @@ object StdNames { val reflectiveSelectable: N = "reflectiveSelectable" val reify : N = "reify" val releaseFence : N = "releaseFence" + val retains: N = "retains" val rootMirror : N = "rootMirror" val run: N = "run" val runOrElse: N = "runOrElse" diff --git a/compiler/src/dotty/tools/dotc/core/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index 0db49855bfda..953893826039 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -161,8 +161,9 @@ object Substituters: .mapOver(tp) } - final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap { + final class SubstBindingMap(from: BindingType, to: BindingType)(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) + def inverse(tp: Type): Type = tp.subst(to, from) } final class Subst1Map(from: Symbol, to: Type)(using Context) extends DeepTypeMap { @@ -177,8 +178,9 @@ object Substituters: def apply(tp: Type): Type = subst(tp, from, to, this)(using mapCtx) } - final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap { + final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = substSym(tp, from, to, this)(using mapCtx) + def inverse(tp: Type) = tp.substSym(to, from) } final class SubstThisMap(from: ClassSymbol, to: Type)(using Context) extends DeepTypeMap { diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index a1752ccc0976..19609a20c553 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -23,6 +23,8 @@ import scala.util.control.NonFatal import config.Config import reporting._ import collection.mutable +import transform.TypeUtils._ +import cc.{CapturingType, derivedCapturingType} import scala.annotation.internal.sharable @@ -228,6 +230,8 @@ object SymDenotations { ensureCompleted(); myAnnotations } + final def annotationsUNSAFE(using Context): List[Annotation] = myAnnotations + /** Update the annotations of this denotation */ final def annotations_=(annots: List[Annotation]): Unit = myAnnotations = annots @@ -1513,8 +1517,7 @@ object SymDenotations { case tp: ExprType => hasSkolems(tp.resType) case tp: AppliedType => hasSkolems(tp.tycon) || tp.args.exists(hasSkolems) case tp: LambdaType => tp.paramInfos.exists(hasSkolems) || hasSkolems(tp.resType) - case tp: AndType => hasSkolems(tp.tp1) || hasSkolems(tp.tp2) - case tp: OrType => hasSkolems(tp.tp1) || hasSkolems(tp.tp2) + case tp: AndOrType => hasSkolems(tp.tp1) || hasSkolems(tp.tp2) case tp: AnnotatedType => hasSkolems(tp.parent) case _ => false } @@ -2170,6 +2173,9 @@ object SymDenotations { case tp: TypeParamRef => // uncachable, since baseType depends on context bounds recur(TypeComparer.bounds(tp).hi) + case CapturingType(parent, refs, _) => + tp.derivedCapturingType(recur(parent), refs) + case tp: TypeProxy => def computeTypeProxy = { val superTp = tp.superType diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 087a28a7323e..d43bb27a420c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,6 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly +import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing} /** Provides methods to compare types. */ @@ -339,6 +340,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling compareWild case tp2: LazyRef => isBottom(tp1) || !tp2.evaluating && recur(tp1, tp2.ref) + case CapturingType(_, _, _) => + secondTry case tp2: AnnotatedType if !tp2.isRefining => recur(tp1, tp2.parent) case tp2: ThisType => @@ -458,8 +461,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // See i859.scala for an example where we hit this case. tp2.isRef(AnyClass, skipRefined = false) || !tp1.evaluating && recur(tp1.ref, tp2) - case tp1: AnnotatedType if !tp1.isRefining => - recur(tp1.parent, tp2) case AndType(tp11, tp12) => if (tp11.stripTypeVar eq tp12.stripTypeVar) recur(tp11, tp2) else thirdTry @@ -512,7 +513,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // recursively, so we do it only once. See i14870.scala as a test case, which would // loop for a very long time without the recursion brake. - case tp1: MatchType => + case CapturingType(parent1, refs1, _) => + if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK then + recur(parent1, tp2) + else + thirdTry + case tp1: AnnotatedType if !tp1.isRefining => + recur(tp1.parent, tp2) + case tp1: MatchType => val reduced = tp1.reduced if reduced.exists then recur(reduced, tp2) && recordGadtUsageIf { MatchType.thatReducesUsingGadt(tp1) } @@ -555,8 +563,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // Note: We would like to replace this by `if (tp1.hasHigherKind)` // but right now we cannot since some parts of the standard library rely on the // idiom that e.g. `List <: Any`. We have to bootstrap without scalac first. - if (cls2 eq AnyClass) return true - if (cls2 == defn.SingletonClass && tp1.isStable) return true + if cls2 eq AnyClass then return true + if cls2 == defn.SingletonClass && tp1.isStable then return true return tryBaseType(cls2) } else if (cls2.is(JavaDefined)) { @@ -625,6 +633,28 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling isSubRefinements(tp1w.asInstanceOf[RefinedType], tp2, skipped2) && recur(tp1, skipped2) + def isSubInfo(info1: Type, info2: Type): Boolean = (info1, info2) match + case (info1: PolyType, info2: PolyType) => + sameLength(info1.paramNames, info2.paramNames) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) + case (info1: MethodType, info2: MethodType) => + matchingMethodParams(info1, info2, precise = false) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) + case _ => + isSubType(info1, info2) + + if ctx.phase == Phases.checkCapturesPhase then + if defn.isFunctionType(tp2) then + tp1.widenDealias match + case tp1: RefinedType => + return isSubInfo(tp1.refinedInfo, tp2.refinedInfo) + case _ => + else if tp2.parent.typeSymbol == defn.PolyFunctionClass then + tp1.member(nme.apply).info match + case info1: PolyType => + return isSubInfo(info1, tp2.refinedInfo) + case _ => + compareRefined case tp2: RecType => def compareRec = tp1.safeDealias match { @@ -758,13 +788,17 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def compareTypeBounds = tp1 match { case tp1 @ TypeBounds(lo1, hi1) => ((lo2 eq NothingType) || isSubType(lo2, lo1)) && - ((hi2 eq AnyType) && !hi1.isLambdaSub || (hi2 eq AnyKindType) || isSubType(hi1, hi2)) + ((hi2 eq AnyType) && !hi1.isLambdaSub + || (hi2 eq AnyKindType) + || isSubType(hi1, hi2)) case tp1: ClassInfo => tp2 contains tp1 case _ => false } compareTypeBounds + case CapturingType(parent2, _, _) => + recur(tp1, parent2) || fourthTry case tp2: AnnotatedType if tp2.isRefining => (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) && recur(tp1, tp2.parent) @@ -827,6 +861,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp: AppliedType => isNullable(tp.tycon) case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2) case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2) + case CapturingType(tp1, _, _) => isNullable(tp1) case _ => false val sym1 = tp1.symbol (sym1 eq NothingClass) && tp2.isValueTypeOrLambda || @@ -844,7 +879,15 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => false } case _ => false - comparePaths || isSubType(tp1.underlying.widenExpr, tp2, approx.addLow) + comparePaths || { + var tp1w = tp1.underlying.widenExpr + tp1 match + case tp1: CaptureRef if tp1.isTracked => + val stripped = tp1w.stripCapturing + tp1w = CapturingType(stripped, tp1.singletonCaptureSet, boxed = false) + case _ => + isSubType(tp1w, tp2, approx.addLow) + } case tp1: RefinedType => isNewSubType(tp1.parent) case tp1: RecType => @@ -1816,6 +1859,31 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def hasMatchingMember(name: Name, tp1: Type, tp2: RefinedType): Boolean = trace(i"hasMatchingMember($tp1 . $name :? ${tp2.refinedInfo}), mbr: ${tp1.member(name).info}", subtyping) { + // If the member is an abstract type and the prefix is a path, compare the member itself + // instead of its bounds. This case is needed situations like: + // + // class C { type T } + // val foo: C + // foo.type <: C { type T {= , <: , >:} foo.T } + // + // or like: + // + // class C[T] + // C[?] <: C[TV] + // + // where TV is a type variable. See i2397.scala for an example of the latter. + def matchAbstractTypeMember(info1: Type): Boolean = info1 match { + case TypeBounds(lo, hi) if lo ne hi => + tp2.refinedInfo match { + case rinfo2: TypeBounds if tp1.isStable => + val ref1 = tp1.widenExpr.select(name) + isSubType(rinfo2.lo, ref1) && isSubType(ref1, rinfo2.hi) + case _ => + false + } + case _ => false + } + def qualifies(m: SingleDenotation): Boolean = // If the member is an abstract type and the prefix is a path, compare the member itself // instead of its bounds. This case is needed situations like: @@ -1876,11 +1944,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && sigsOK(symInfo1, info2) case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } - + + def qualifies(m: SingleDenotation): Boolean = val info1 = m.info.widenExpr isSubInfo(info1, tp2.refinedInfo.widenExpr, m.symbol.info.orElse(info1)) || matchAbstractTypeMember(m.info) - end qualifies tp1.member(name) match // inlined hasAltWith for performance case mbr: SingleDenotation => qualifies(mbr) @@ -2008,8 +2076,12 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case formal2 :: rest2 => val formal2a = if (tp2.isParamDependent) formal2.subst(tp2, tp1) else formal2 val paramsMatch = - if precise then isSameTypeWhenFrozen(formal1, formal2a) - else isSubTypeWhenFrozen(formal2a, formal1) + if precise then + isSameTypeWhenFrozen(formal1, formal2a) + else if ctx.phase == Phases.checkCapturesPhase then + isSubType(formal2a, formal1) + else + isSubTypeWhenFrozen(formal2a, formal1) paramsMatch && loop(rest1, rest2) case nil => false @@ -2429,6 +2501,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } case tp1: TypeVar if tp1.isInstantiated => tp1.underlying & tp2 + case CapturingType(parent1, refs1, _) => + if subCaptures(tp2.captureSet, refs1, frozenConstraint).isOK then + parent1 & tp2 + else + tp1.derivedCapturingType(parent1 & tp2, refs1) case tp1: AnnotatedType if !tp1.isRefining => tp1.underlying & tp2 case _ => @@ -2491,6 +2568,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } + protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult.Type = + refs1.subCaptures(refs2, frozen) + // ----------- Diagnostics -------------------------------------------------- /** A hook for showing subtype traces. Overridden in ExplainingTypeComparer */ @@ -2766,6 +2846,7 @@ object TypeComparer { else res match case ClassInfo(_, cls, _, _, _) => cls.showLocated case bounds: TypeBounds => i"type bounds [$bounds]" + case CaptureSet.CompareResult.OK => "OK" case res: printing.Showable => res.show case _ => String.valueOf(res).nn @@ -3126,5 +3207,10 @@ class ExplainingTypeComparer(initctx: Context) extends TypeComparer(initctx) { super.gadtAddBound(sym, b, isUpper) } + override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult.Type = + traceIndented(i"subcaptures $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { + super.subCaptures(refs1, refs2, frozen) + } + def lastTrace(header: String): String = header + { try b.toString finally b.clear() } } diff --git a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala index 622c30a22644..5816e1254873 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErrors.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErrors.scala @@ -74,6 +74,7 @@ extends TypeError { s"""Recursion limit exceeded. |Maybe there is an illegal cyclic reference? |If that's not the case, you could also try to increase the stacksize using the -Xss JVM option. + |For the unprocessed stack trace, compile with -Yno-decode-stacktraces. |A recurring operation is (inner to outer): |${opsString(mostCommon)}""".stripMargin } diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 2ff2570dfa02..a1eac2b724fa 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -16,6 +16,10 @@ import config.Feature import typer.ProtoTypes._ import typer.ForceDegree import typer.Inferencing._ +import typer.IfBottom +import reporting.TestingReporter +import cc.{CapturingType, derivedCapturingType, CaptureSet} +import CaptureSet.CompareResult import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -164,6 +168,12 @@ object TypeOps: // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take // corrective steps, so no widening is wanted. simplify(l, theMap) | simplify(r, theMap) + case CapturingType(parent, refs, _) => + if !ctx.mode.is(Mode.Type) + && refs.subCaptures(parent.captureSet, frozen = true).isOK then + simplify(parent, theMap) + else + mapOver case tp @ AnnotatedType(parent, annot) => val parent1 = simplify(parent, theMap) if annot.symbol == defn.UncheckedVarianceAnnot @@ -279,15 +289,23 @@ object TypeOps: case _ => false } - // Step 1: Get RecTypes and ErrorTypes out of the way, + // Step 1: Get RecTypes and ErrorTypes and CapturingTypes out of the way, tp1 match { - case tp1: RecType => return tp1.rebind(approximateOr(tp1.parent, tp2)) - case err: ErrorType => return err + case tp1: RecType => + return tp1.rebind(approximateOr(tp1.parent, tp2)) + case CapturingType(parent1, refs1, _) => + return tp1.derivedCapturingType(approximateOr(parent1, tp2), refs1) + case err: ErrorType => + return err case _ => } tp2 match { - case tp2: RecType => return tp2.rebind(approximateOr(tp1, tp2.parent)) - case err: ErrorType => return err + case tp2: RecType => + return tp2.rebind(approximateOr(tp1, tp2.parent)) + case CapturingType(parent2, refs2, _) => + return tp2.derivedCapturingType(approximateOr(tp1, parent2), refs2) + case err: ErrorType => + return err case _ => } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 65352aaae219..3487561d9155 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,6 +36,8 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized +import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing} +import CaptureSet.CompareResult import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -65,7 +67,7 @@ object Types { * | | +--- SkolemType * | +- TypeParamRef * | +- RefinedOrRecType -+-- RefinedType - * | | -+-- RecType + * | | +-- RecType * | +- AppliedType * | +- TypeBounds * | +- ExprType @@ -187,7 +189,7 @@ object Types { * It makes no sense for it to be an alias type because isRef would always * return false in that case. */ - def isRef(sym: Symbol, skipRefined: Boolean = true)(using Context): Boolean = stripped match { + def isRef(sym: Symbol, skipRefined: Boolean = true)(using Context): Boolean = this match { case this1: TypeRef => this1.info match { // see comment in Namer#typeDefSig case TypeAlias(tp) => tp.isRef(sym, skipRefined) @@ -199,6 +201,12 @@ object Types { val this2 = this1.dealias if (this2 ne this1) this2.isRef(sym, skipRefined) else this1.underlying.isRef(sym, skipRefined) + case this1: TypeVar => + this1.instanceOpt.isRef(sym, skipRefined) + case this1: AnnotatedType => + this1 match + case CapturingType(_, _, _) => false + case _ => this1.parent.isRef(sym, skipRefined) case _ => false } @@ -360,6 +368,7 @@ object Types { case tp: AndOrType => tp.tp1.unusableForInference || tp.tp2.unusableForInference case tp: LambdaType => tp.resultType.unusableForInference || tp.paramInfos.exists(_.unusableForInference) case WildcardType(optBounds) => optBounds.unusableForInference + case CapturingType(parent, refs, _) => parent.unusableForInference || refs.elems.exists(_.unusableForInference) case _: ErrorType => true case _ => false @@ -1179,9 +1188,13 @@ object Types { */ def stripAnnots(using Context): Type = this - /** Strip TypeVars and Annotation wrappers */ + /** Strip TypeVars and Annotation and CapturingType wrappers */ def stripped(using Context): Type = this + def strippedDealias(using Context): Type = + val tp1 = stripped.dealias + if tp1 ne this then tp1.strippedDealias else this + def rewrapAnnots(tp: Type)(using Context): Type = tp.stripTypeVar match { case AnnotatedType(tp1, annot) => AnnotatedType(rewrapAnnots(tp1), annot) case _ => this @@ -1378,8 +1391,13 @@ object Types { val tp1 = tp.instanceOpt if (tp1.exists) tp1.dealias1(keep, keepOpaques) else tp case tp: AnnotatedType => - val tp1 = tp.parent.dealias1(keep, keepOpaques) - if keep(tp) then tp.derivedAnnotatedType(tp1, tp.annot) else tp1 + val parent1 = tp.parent.dealias1(keep, keepOpaques) + tp match + case tp @ CapturingType(parent, refs, _) => + tp.derivedCapturingType(parent1, refs) + case _ => + if keep(tp) then tp.derivedAnnotatedType(parent1, tp.annot) + else parent1 case tp: LazyRef => tp.ref.dealias1(keep, keepOpaques) case _ => this @@ -1478,7 +1496,7 @@ object Types { if (tp.tycon.isLambdaSub) NoType else tp.superType.underlyingClassRef(refinementOK) case tp: AnnotatedType => - tp.underlying.underlyingClassRef(refinementOK) + tp.parent.underlyingClassRef(refinementOK) case tp: RefinedType => if (refinementOK) tp.underlying.underlyingClassRef(refinementOK) else NoType case tp: RecType => @@ -1521,6 +1539,8 @@ object Types { case _ => if (isRepeatedParam) this.argTypesHi.head else this } + def captureSet(using Context): CaptureSet = CaptureSet.ofType(this) + // ----- Normalizing typerefs over refined types ---------------------------- /** If this normalizes* to a refinement type that has a refinement for `name` (which might be followed @@ -1816,7 +1836,7 @@ object Types { * @param dropLast The number of trailing parameters that should be dropped * when forming the function type. */ - def toFunctionType(isJava: Boolean, dropLast: Int = 0)(using Context): Type = this match { + def toFunctionType(isJava: Boolean, dropLast: Int = 0, alwaysDependent: Boolean = false)(using Context): Type = this match { case mt: MethodType if !mt.isParamDependent => val formals1 = if (dropLast == 0) mt.paramInfos else mt.paramInfos dropRight dropLast val isContextual = mt.isContextualMethod && !ctx.erasedTypes @@ -1828,7 +1848,7 @@ object Types { val funType = defn.FunctionOf( formals1 mapConserve (_.translateFromRepeated(toArray = isJava)), result1, isContextual, isErased) - if (mt.isResultDependent) RefinedType(funType, nme.apply, mt) + if alwaysDependent || mt.isResultDependent then RefinedType(funType, nme.apply, mt) else funType } @@ -1860,6 +1880,16 @@ object Types { case _ => this } + def capturing(ref: CaptureRef)(using Context): Type = + if captureSet.accountsFor(ref) then this + else CapturingType(this, ref.singletonCaptureSet, this.isBoxedCapturing) + + def capturing(cs: CaptureSet)(using Context): Type = + if cs.isConst && cs.subCaptures(captureSet, frozen = true).isOK then this + else this match + case CapturingType(parent, cs1, boxed) => parent.capturing(cs1 ++ cs) + case _ => CapturingType(this, cs, this.isBoxedCapturing) + /** 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) @@ -2043,6 +2073,40 @@ object Types { def isOverloaded(using Context): Boolean = false } + /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs */ + trait CaptureRef extends SingletonType: + private var myCaptureSet: CaptureSet = _ + private var myCaptureSetRunId: Int = NoRunId + private var mySingletonCaptureSet: CaptureSet.Const = null + + def canBeTracked(using Context): Boolean + final def isTracked(using Context): Boolean = canBeTracked && !captureSetOfInfo.isAlwaysEmpty + def isRootCapability(using Context): Boolean = false + def normalizedRef(using Context): CaptureRef = this + + def singletonCaptureSet(using Context): CaptureSet.Const = + if mySingletonCaptureSet == null then + mySingletonCaptureSet = CaptureSet(this.normalizedRef) + mySingletonCaptureSet + + def captureSetOfInfo(using Context): CaptureSet = + if ctx.runId == myCaptureSetRunId then myCaptureSet + else if myCaptureSet eq CaptureSet.Pending then CaptureSet.empty + else + myCaptureSet = CaptureSet.Pending + val computed = CaptureSet.ofInfo(this) + if ctx.phase != Phases.checkCapturesPhase || underlying.isProvisional then + myCaptureSet = null + else + myCaptureSet = computed + myCaptureSetRunId = ctx.runId + computed + + override def captureSet(using Context): CaptureSet = + val cs = captureSetOfInfo + if canBeTracked && !cs.isAlwaysEmpty then singletonCaptureSet else cs + end CaptureRef + /** A trait for types that bind other types that refer to them. * Instances are: LambdaType, RecType. */ @@ -2090,7 +2154,7 @@ object Types { // --- NamedTypes ------------------------------------------------------------------ - abstract class NamedType extends CachedProxyType with ValueType { self => + abstract class NamedType extends CachedProxyType, ValueType { self => type ThisType >: this.type <: NamedType type ThisName <: Name @@ -2109,6 +2173,9 @@ object Types { private var mySignature: Signature = _ private var mySignatureRunId: Int = NoRunId + private var myCaptureSet: CaptureSet = _ + private var myCaptureSetRunId: Int = NoRunId + // Invariants: // (1) checkedPeriod != Nowhere => lastDenotation != null // (2) lastDenotation != null => lastSymbol != null @@ -2469,7 +2536,7 @@ object Types { val tparam = symbol val cls = tparam.owner val base = pre.baseType(cls) - base match { + base.stripped match { case AppliedType(_, allArgs) => var tparams = cls.typeParams var args = allArgs @@ -2666,7 +2733,7 @@ object Types { */ abstract case class TermRef(override val prefix: Type, private var myDesignator: Designator) - extends NamedType with SingletonType with ImplicitRef { + extends NamedType, ImplicitRef, CaptureRef { type ThisType = TermRef type ThisName = TermName @@ -2690,6 +2757,25 @@ object Types { 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 avout call-by-name? + */ + def canBeTracked(using Context) = + ((prefix eq NoPrefix) + || symbol.is(ParamAccessor) && (prefix eq symbol.owner.thisType) + || symbol.hasAnnotation(defn.AbilityAnnot) + || isRootCapability + ) && !symbol.is(Method) + + override def isRootCapability(using Context): Boolean = + name == nme.CAPTURE_ROOT && symbol == defn.captureRoot + + override def normalizedRef(using Context): CaptureRef = + if canBeTracked then symbol.termRef else this } abstract case class TypeRef(override val prefix: Type, @@ -2825,7 +2911,7 @@ object Types { * Note: we do not pass a class symbol directly, because symbols * do not survive runs whereas typerefs do. */ - abstract case class ThisType(tref: TypeRef) extends CachedProxyType with SingletonType { + abstract case class ThisType(tref: TypeRef) extends CachedProxyType, CaptureRef { def cls(using Context): ClassSymbol = tref.stableInRunSymbol match { case cls: ClassSymbol => cls case _ if ctx.mode.is(Mode.Interactive) => defn.AnyClass // was observed to happen in IDE mode @@ -2839,6 +2925,8 @@ object Types { // can happen in IDE if `cls` is stale } + def canBeTracked(using Context) = true + override def computeHash(bs: Binders): Int = doHash(bs, tref) override def eql(that: Type): Boolean = that match { @@ -3667,9 +3755,17 @@ object Types { case tp: AppliedType => tp.fold(status, compute(_, _, theAcc)) case tp: TypeVar if !tp.isInstantiated => combine(status, Provisional) case tp: TermParamRef if tp.binder eq thisLambdaType => TrueDeps - case AnnotatedType(parent, ann) => - if ann.refersToParamOf(thisLambdaType) then TrueDeps - else compute(status, parent, theAcc) + case tp: AnnotatedType => + tp match + case CapturingType(parent, refs, _) => + (compute(status, parent, theAcc) /: refs.elems) { + (s, ref) => ref match + case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, CaptureDeps) + case _ => s + } + case _ => + if tp.annot.refersToParamOf(thisLambdaType) then TrueDeps + else compute(status, tp.parent, theAcc) case _: ThisType | _: BoundType | NoPrefix => status case t: LazyRef => if t.completed then compute(status, t.ref, theAcc) @@ -3711,29 +3807,52 @@ object Types { /** Does result type contain references to parameters of this method type, * which cannot be eliminated by de-aliasing? */ - def isResultDependent(using Context): Boolean = dependencyStatus == TrueDeps + def isResultDependent(using Context): Boolean = + dependencyStatus == TrueDeps || dependencyStatus == CaptureDeps /** Does one of the parameter types contain references to earlier parameters * of this method type which cannot be eliminated by de-aliasing? */ def isParamDependent(using Context): Boolean = paramDependencyStatus == TrueDeps + /** Is there either a true or false type dependency, or does the result + * type capture a parameter? + */ + def isCaptureDependent(using Context) = dependencyStatus == CaptureDeps + def newParamRef(n: Int): TermParamRef = new TermParamRefImpl(this, n) /** The least supertype of `resultType` that does not contain parameter dependencies */ def nonDependentResultApprox(using Context): Type = - if (isResultDependent) { + if isResultDependent then val dropDependencies = new ApproximatingTypeMap { def apply(tp: Type) = tp match { case tp @ TermParamRef(`thisLambdaType`, _) => range(defn.NothingType, atVariance(1)(apply(tp.underlying))) + case CapturingType(parent, refs, boxed) => + val parent1 = this(parent) + val elems1 = refs.elems.filter { + case tp @ TermParamRef(`thisLambdaType`, _) => false + case _ => true + } + if elems1.size == refs.elems.size then + derivedCapturingType(tp, parent1, refs) + else + range( + CapturingType(parent1, CaptureSet(elems1), boxed), + CapturingType(parent1, CaptureSet.universal, boxed)) case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => - mapOver(parent) + val parent1 = mapOver(parent) + if ann.symbol == defn.RetainsAnnot then + range( + AnnotatedType(parent1, CaptureSet.empty.toRegularAnnotation), + AnnotatedType(parent1, CaptureSet.universal.toRegularAnnotation)) + else + parent1 case _ => mapOver(tp) } } dropDependencies(resultType) - } else resultType } @@ -4104,9 +4223,10 @@ object Types { final val Unknown: DependencyStatus = 0 // not yet computed final val NoDeps: DependencyStatus = 1 // no dependent parameters found final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types - final val TrueDeps: DependencyStatus = 3 // some truly dependent parameters exist - final val StatusMask: DependencyStatus = 3 // the bits indicating actual dependency status - final val Provisional: DependencyStatus = 4 // set if dependency status can still change due to type variable instantiations + final val CaptureDeps: DependencyStatus = 3 + final val TrueDeps: DependencyStatus = 4 // some truly dependent parameters exist + final val StatusMask: DependencyStatus = 7 // the bits indicating actual dependency status + final val Provisional: DependencyStatus = 8 // set if dependency status can still change due to type variable instantiations } // ----- Type application: LambdaParam, AppliedType --------------------- @@ -4373,8 +4493,9 @@ object Types { /** Only created in `binder.paramRefs`. Use `binder.paramRefs(paramNum)` to * refer to `TermParamRef(binder, paramNum)`. */ - abstract case class TermParamRef(binder: TermLambda, paramNum: Int) extends ParamRef with SingletonType { + abstract case class TermParamRef(binder: TermLambda, paramNum: Int) extends ParamRef, CaptureRef { type BT = TermLambda + def canBeTracked(using Context) = true def kindString: String = "Term" def copyBoundType(bt: BT): Type = bt.paramRefs(paramNum) } @@ -5058,7 +5179,7 @@ object Types { // ----- Annotated and Import types ----------------------------------------------- /** An annotated type tpe @ annot */ - abstract case class AnnotatedType(parent: Type, annot: Annotation) extends CachedProxyType with ValueType { + abstract case class AnnotatedType(parent: Type, annot: Annotation) extends CachedProxyType, ValueType { override def underlying(using Context): Type = parent @@ -5087,16 +5208,16 @@ object Types { // equals comes from case class; no matching override is needed override def computeHash(bs: Binders): Int = - doHash(bs, System.identityHashCode(annot), parent) + doHash(bs, annot.hash, parent) override def hashIsStable: Boolean = parent.hashIsStable override def eql(that: Type): Boolean = that match - case that: AnnotatedType => (parent eq that.parent) && (annot eq that.annot) + case that: AnnotatedType => (parent eq that.parent) && (annot eql that.annot) case _ => false override def iso(that: Any, bs: BinderPairs): Boolean = that match - case that: AnnotatedType => parent.equals(that.parent, bs) && (annot eq that.annot) + case that: AnnotatedType => parent.equals(that.parent, bs) && (annot eql that.annot) case _ => false } @@ -5107,6 +5228,7 @@ object Types { annots.foldLeft(underlying)(apply(_, _)) def apply(parent: Type, annot: Annotation)(using Context): AnnotatedType = unique(CachedAnnotatedType(parent, annot)) + end AnnotatedType // Special type objects and classes ----------------------------------------------------- @@ -5327,7 +5449,7 @@ object Types { /** Common base class of TypeMap and TypeAccumulator */ abstract class VariantTraversal: - protected[core] var variance: Int = 1 + protected[dotc] var variance: Int = 1 inline protected def atVariance[T](v: Int)(op: => T): T = { val saved = variance @@ -5353,6 +5475,24 @@ object Types { } end VariantTraversal + /** A supertrait for some typemaps that are bijections. Used for capture checking + * BiTypeMaps should map capture references to capture references. + */ + trait BiTypeMap extends TypeMap: + thisMap => + def inverse(tp: Type): Type + + def inverseTypeMap(using Context) = new BiTypeMap: + def apply(tp: Type) = thisMap.inverse(tp) + def inverse(tp: Type) = thisMap.apply(tp) + + def forward(ref: CaptureRef): CaptureRef = this(ref) match + case result: CaptureRef if result.canBeTracked => result + + def backward(ref: CaptureRef): CaptureRef = inverse(ref) match + case result: CaptureRef if result.canBeTracked => result + end BiTypeMap + abstract class TypeMap(implicit protected var mapCtx: Context) extends VariantTraversal with (Type => Type) { thisMap => @@ -5380,6 +5520,8 @@ object Types { tp.derivedMatchType(bound, scrutinee, cases) protected def derivedAnnotatedType(tp: AnnotatedType, underlying: Type, annot: Annotation): Type = tp.derivedAnnotatedType(underlying, annot) + protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type = + tp.derivedCapturingType(parent, refs) protected def derivedWildcardType(tp: WildcardType, bounds: Type): Type = tp.derivedWildcardType(bounds) protected def derivedSkolemType(tp: SkolemType, info: Type): Type = @@ -5415,6 +5557,12 @@ object Types { def isRange(tp: Type): Boolean = tp.isInstanceOf[Range] + protected def mapCapturingType(tp: Type, parent: Type, refs: CaptureSet, v: Int): Type = + val saved = variance + variance = v + try derivedCapturingType(tp, this(parent), refs.map(this)) + finally variance = saved + /** Map this function over given type */ def mapOver(tp: Type): Type = { record(s"TypeMap mapOver ${getClass}") @@ -5456,6 +5604,9 @@ object Types { case tp: ExprType => derivedExprType(tp, this(tp.resultType)) + case CapturingType(parent, refs, _) => + mapCapturingType(tp, parent, refs, variance) + case tp @ AnnotatedType(underlying, annot) => val underlying1 = this(underlying) val annot1 = annot.mapWith(this) @@ -5783,6 +5934,13 @@ object Types { if (underlying.isExactlyNothing) underlying else tp.derivedAnnotatedType(underlying, annot) } + override protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type = + parent match // ^^^ handle ranges in capture sets as well + case Range(lo, hi) => + range(derivedCapturingType(tp, lo, refs), derivedCapturingType(tp, hi, refs)) + case _ => + tp.derivedCapturingType(parent, refs) + override protected def derivedWildcardType(tp: WildcardType, bounds: Type): WildcardType = tp.derivedWildcardType(rangeToBounds(bounds)) @@ -5829,6 +5987,12 @@ object Types { tp.derivedLambdaType(tp.paramNames, formals, restpe) } + override def mapCapturingType(tp: Type, parent: Type, refs: CaptureSet, v: Int): Type = + if v == 0 then + range(mapCapturingType(tp, parent, refs, -1), mapCapturingType(tp, parent, refs, 1)) + else + super.mapCapturingType(tp, parent, refs, v) + protected def reapply(tp: Type): Type = apply(tp) } @@ -5926,6 +6090,9 @@ object Types { val x2 = atVariance(0)(this(x1, tp.scrutinee)) foldOver(x2, tp.cases) + case CapturingType(parent, refs, _) => + (this(x, parent) /: refs.elems)(this) + case AnnotatedType(underlying, annot) => this(applyToAnnot(x, annot), underlying) diff --git a/compiler/src/dotty/tools/dotc/core/Variances.scala b/compiler/src/dotty/tools/dotc/core/Variances.scala index 2401b43c8e17..0304825891ac 100644 --- a/compiler/src/dotty/tools/dotc/core/Variances.scala +++ b/compiler/src/dotty/tools/dotc/core/Variances.scala @@ -4,6 +4,7 @@ package core import Types._, Contexts._, Flags._, Symbols._, Annotations._ import TypeApplications.TypeParamInfo import Decorators._ +import cc.CapturingType object Variances { diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 440871481114..caf4deb2738c 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -847,7 +847,7 @@ class TreeUnpickler(reader: TastyReader, def TypeDef(rhs: Tree) = ta.assignType(untpd.TypeDef(sym.name.asTypeName, rhs), sym) - def ta = ctx.typeAssigner + def ta = ctx.typeAssigner val name = readName() pickling.println(s"reading def of $name at $start") @@ -1337,11 +1337,9 @@ class TreeUnpickler(reader: TastyReader, // types. This came up in #137 of collection strawman. val tycon = readTpt() val args = until(end)(readTpt()) - val ownType = - if (tycon.symbol == defn.andType) AndType(args(0).tpe, args(1).tpe) - else if (tycon.symbol == defn.orType) OrType(args(0).tpe, args(1).tpe, soft = false) - else tycon.tpe.safeAppliedTo(args.tpes) - untpd.AppliedTypeTree(tycon, args).withType(ownType) + val tree = untpd.AppliedTypeTree(tycon, args) + val ownType = ctx.typeAssigner.processAppliedType(tree, tycon.tpe.safeAppliedTo(args.tpes)) + tree.withType(ownType) case ANNOTATEDtpt => Annotated(readTpt(), readTerm()) case LAMBDAtpt => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 28f02b7db2a0..41cb62c72ee6 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -955,6 +955,24 @@ object Parsers { isArrowIndent() else false + def followingIsCaptureSet(): Boolean = + val lookahead = in.LookaheadScanner() + def recur(): Boolean = + (lookahead.isIdent || lookahead.token == THIS) && { + lookahead.nextToken() + if lookahead.token == COMMA then + lookahead.nextToken() + recur() + else + lookahead.token == RBRACE && { + lookahead.nextToken() + canStartInfixTypeTokens.contains(lookahead.token) + || lookahead.token == LBRACKET + } + } + lookahead.nextToken() + recur() + /* --------- OPERAND/OPERATOR STACK --------------------------------------- */ var opStack: List[OpInfo] = Nil @@ -1405,17 +1423,25 @@ object Parsers { case _ => false } + /** CaptureRef ::= ident | `this` + */ + def captureRef(): Tree = + if in.token == THIS then simpleRef() else termIdent() + /** Type ::= FunType * | HkTypeParamClause ‘=>>’ Type * | FunParamClause ‘=>>’ Type * | MatchType * | InfixType + * | CaptureSet Type * FunType ::= (MonoFunType | PolyFunType) * MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type * PolyFunType ::= HKTypeParamClause '=>' Type * FunTypeArgs ::= InfixType * | `(' [ [ ‘[using]’ ‘['erased'] FunArgType {`,' FunArgType } ] `)' * | '(' [ ‘[using]’ ‘['erased'] TypedFunParam {',' TypedFunParam } ')' + * CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` + * CaptureRef ::= Ident */ def typ(): Tree = { val start = in.offset @@ -1515,6 +1541,10 @@ object Parsers { } else { accept(TLARROW); typ() } } + else if in.token == LBRACE && followingIsCaptureSet() then + val refs = inBraces { commaSeparated(captureRef) } + val t = typ() + CapturingTypeTree(refs, t) else if (in.token == INDENT) enclosed(INDENT, typ()) else infixType() @@ -1583,7 +1613,7 @@ object Parsers { def infixType(): Tree = infixTypeRest(refinedType()) def infixTypeRest(t: Tree): Tree = - infixOps(t, canStartTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type, + infixOps(t, canStartInfixTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type, isOperator = !followingIsVararg()) /** RefinedType ::= WithType {[nl] Refinement} @@ -3256,7 +3286,7 @@ object Parsers { ImportSelector( atSpan(in.skipToken()) { Ident(nme.EMPTY) }, bound = - if canStartTypeTokens.contains(in.token) then rejectWildcardType(infixType()) + if canStartInfixTypeTokens.contains(in.token) then rejectWildcardType(infixType()) else EmptyTree) /** id [‘as’ (id | ‘_’) */ diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index 82a5297faf97..13bcfcb511df 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -228,7 +228,7 @@ object Tokens extends TokensCommon { final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO) - final val canStartTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet( + final val canStartInfixTypeTokens: TokenSet = literalTokens | identifierTokens | BitSet( THIS, SUPER, USCORE, LPAREN, LBRACE, AT) final val templateIntroTokens: TokenSet = BitSet(CLASS, TRAIT, OBJECT, ENUM, CASECLASS, CASEOBJECT) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index d3545f09b0e7..d75d160a2574 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -14,13 +14,16 @@ import Variances.varianceSign import util.SourcePosition import scala.util.control.NonFatal import scala.annotation.switch +import config.Config +import cc.{CapturingType, CaptureSet} class PlainPrinter(_ctx: Context) extends Printer { + /** The context of all public methods in Printer and subclasses. * Overridden in RefinedPrinter. */ - protected def curCtx: Context = _ctx.addMode(Mode.Printing) - protected given [DummyToEnforceDef]: Context = curCtx + def printerContext: Context = _ctx.addMode(Mode.Printing) + protected given [DummyToEnforceDef]: Context = printerContext protected def printDebug = ctx.settings.YprintDebug.value @@ -194,6 +197,22 @@ class PlainPrinter(_ctx: Context) extends Printer { keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~ (" <: " ~ toText(bound) provided !bound.isAny) }.close + case CapturingType(parent, refs, boxed) => + def box = Str("box ") provided boxed + if printDebug && !refs.isConst then + changePrec(GlobalPrec)(box ~ s"$refs " ~ toText(parent)) + else if ctx.settings.YccDebug.value then + changePrec(GlobalPrec)(box ~ refs.toText(this) ~ " " ~ toText(parent)) + else if !refs.isConst && refs.elems.isEmpty then + changePrec(GlobalPrec)("?" ~ " " ~ toText(parent)) + else if Config.printCaptureSetsAsPrefix then + changePrec(GlobalPrec)( + box ~ "{" + ~ Text(refs.elems.toList.map(toTextCaptureRef), ", ") + ~ "} " + ~ toText(parent)) + else + changePrec(InfixPrec)(toText(parent) ~ " retains " ~ box ~ toText(refs.toRetainsTypeArg)) case tp: PreviousErrorType if ctx.settings.XprintTypes.value => "" // do not print previously reported error message because they may try to print this error type again recuresevely case tp: ErrorType => @@ -346,6 +365,11 @@ class PlainPrinter(_ctx: Context) extends Printer { } } + def toTextCaptureRef(tp: Type): Text = + homogenize(tp) match + case tp: SingletonType => toTextRef(tp) + case _ => toText(tp) + protected def isOmittablePrefix(sym: Symbol): Boolean = defn.unqualifiedOwnerTypes.exists(_.symbol == sym) || isEmptyPrefix(sym) diff --git a/compiler/src/dotty/tools/dotc/printing/Printer.scala b/compiler/src/dotty/tools/dotc/printing/Printer.scala index 550bdb94af4f..b883b6be805b 100644 --- a/compiler/src/dotty/tools/dotc/printing/Printer.scala +++ b/compiler/src/dotty/tools/dotc/printing/Printer.scala @@ -6,7 +6,7 @@ import core._ import Texts._, ast.Trees._ import Types.{Type, SingletonType, LambdaParam}, Symbols.Symbol, Scopes.Scope, Constants.Constant, - Names.Name, Denotations._, Annotations.Annotation + Names.Name, Denotations._, Annotations.Annotation, Contexts.Context import typer.Implicits.SearchResult import util.SourcePosition import typer.ImportInfo @@ -104,6 +104,9 @@ abstract class Printer { /** Textual representation of a prefix of some reference, ending in `.` or `#` */ def toTextPrefix(tp: Type): Text + /** Textual representation of a reference in a capture set */ + def toTextCaptureRef(tp: Type): Text + /** Textual representation of symbol's declaration */ def dclText(sym: Symbol): Text @@ -182,6 +185,9 @@ abstract class Printer { /** A plain printer without any embellishments */ def plain: Printer + + /** The context in which this printer operates */ + def printerContext: Context } object Printer { diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index b74e78ba591b..9230ec146647 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -32,11 +32,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { /** A stack of enclosing DefDef, TypeDef, or ClassDef, or ModuleDefs nodes */ private var enclosingDef: untpd.Tree = untpd.EmptyTree - private var myCtx: Context = super.curCtx + private var myCtx: Context = super.printerContext private var printPos = ctx.settings.YprintPos.value private val printLines = ctx.settings.printLines.value - override protected def curCtx: Context = myCtx + override def printerContext: Context = myCtx def withEnclosingDef(enclDef: Tree[? >: Untyped])(op: => Text): Text = { val savedCtx = myCtx @@ -162,10 +162,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(GlobalPrec) { "(" ~ keywordText("erased ").provided(info.isErasedMethod) - ~ ( if info.isParamDependent || info.isResultDependent - then paramsText(info) - else argsText(info.paramInfos) - ) + ~ paramsText(info) ~ ") " ~ arrow(info.isImplicitMethod) ~ " " @@ -245,9 +242,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { if !printDebug && appliedText(tp.asInstanceOf[HKLambda].resType).isEmpty => // don't eta contract if the application would be printed specially toText(tycon) - case tp: RefinedType - if (defn.isFunctionType(tp) || (tp.parent.typeSymbol eq defn.PolyFunctionClass)) - && !printDebug => + case tp: RefinedType if defn.isFunctionOrPolyType(tp) && !printDebug => toTextMethodAsFunction(tp.refinedInfo) case tp: TypeRef => if (tp.symbol.isAnonymousClass && !showUniqueIds) @@ -703,6 +698,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val contentText = toTextGlobal(content) val tptText = toTextGlobal(tpt) prefix ~~ idx.toString ~~ "|" ~~ tptText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix + case CapturingTypeTree(refs, parent) => + changePrec(GlobalPrec)("{" ~ Text(refs.map(toText), ", ") ~ "} " ~ toText(parent)) case _ => tree.fallbackToText(this) } @@ -789,9 +786,9 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { if mdef.hasType then Modifiers(mdef.symbol) else mdef.rawMods private def Modifiers(sym: Symbol): Modifiers = untpd.Modifiers( - sym.flags & (if (sym.isType) ModifierFlags | VarianceFlags else ModifierFlags), + sym.flagsUNSAFE & (if (sym.isType) ModifierFlags | VarianceFlags else ModifierFlags), if (sym.privateWithin.exists) sym.privateWithin.asType.name else tpnme.EMPTY, - sym.annotations.filterNot(ann => dropAnnotForModText(ann.symbol)).map(_.tree)) + sym.annotationsUNSAFE.filterNot(ann => dropAnnotForModText(ann.symbol)).map(_.tree)) protected def dropAnnotForModText(sym: Symbol): Boolean = sym == defn.BodyAnnot @@ -995,13 +992,13 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { else if (suppressKw) PrintableFlags(isType) &~ Private else PrintableFlags(isType) if (homogenizedView && mods.flags.isTypeFlags) flagMask &~= GivenOrImplicit // drop implicit/given from classes - val rawFlags = if (sym.exists) sym.flags else mods.flags + val rawFlags = if (sym.exists) sym.flagsUNSAFE else mods.flags if (rawFlags.is(Param)) flagMask = flagMask &~ Given &~ Erased val flags = rawFlags & flagMask var flagsText = toTextFlags(sym, flags) val annotTexts = if sym.exists then - sym.annotations.filterNot(ann => dropAnnotForModText(ann.symbol)).map(toText) + sym.annotationsUNSAFE.filterNot(ann => dropAnnotForModText(ann.symbol)).map(toText) else mods.annotations.filterNot(tree => dropAnnotForModText(tree.symbol)).map(annotText(NoSymbol, _)) Text(annotTexts, " ") ~~ flagsText ~~ (Str(kw) provided !suppressKw) diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 0a01e4ef4911..de920fee4625 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -295,7 +295,6 @@ import transform.SymUtils._ val treeStr = inTree.map(x => s"\nTree: ${x.show}").getOrElse("") treeStr + "\n" + super.explain - end TypeMismatch class NotAMember(site: Type, val name: Name, selected: String, addendum: => String = "")(using Context) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index e561b26abf6d..6de6ac02cf7d 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -196,6 +196,7 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { private val byNameMarker = marker("ByName") private val matchMarker = marker("Match") private val superMarker = marker("Super") + private val retainsMarker = marker("Retains") /** Extract the API representation of a source file */ def apiSource(tree: Tree): Seq[api.ClassLike] = { diff --git a/compiler/src/dotty/tools/dotc/transform/EmptyPhase.scala b/compiler/src/dotty/tools/dotc/transform/EmptyPhase.scala new file mode 100644 index 000000000000..9a287b2dd1d9 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/transform/EmptyPhase.scala @@ -0,0 +1,19 @@ +package dotty.tools.dotc +package transform + +import core.* +import Contexts.Context +import Phases.Phase + +/** A phase that can be inserted directly after a phase that cannot + * be checked, to enable a -Ycheck as soon as possible afterwards + */ +class EmptyPhase extends Phase: + + def phaseName: String = "dummy" + + override def isEnabled(using Context) = prev.isEnabled + + override def run(using Context) = () + +end EmptyPhase \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 76f89cb65757..a61b736a9cc1 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -14,15 +14,22 @@ import typer.ErrorReporting.err import typer.ProtoTypes.* import typer.TypeAssigner.seqLitType import typer.ConstFold +import NamerOps.methodType import config.Printers.recheckr import util.Property import StdNames.nme import reporting.trace +object Recheck: + + /** Attachment key for rechecked types of TypeTrees */ + private val RecheckedType = Property.Key[Type] + abstract class Recheck extends Phase, IdentityDenotTransformer: thisPhase => import ast.tpd.* + import Recheck.* def preRecheckPhase = this.prev.asInstanceOf[PreRecheck] @@ -36,12 +43,17 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: override def widenSkolems = true def run(using Context): Unit = - newRechecker().checkUnit(ctx.compilationUnit) + val rechecker = newRechecker() + rechecker.transformTypes.traverse(ctx.compilationUnit.tpdTree) + rechecker.checkUnit(ctx.compilationUnit) def newRechecker()(using Context): Rechecker class Rechecker(ictx: Context): - val ta = ictx.typeAssigner + private val ta = ictx.typeAssigner + private val keepTypes = inContext(ictx) { + ictx.settings.Xprint.value.containsPhase(thisPhase) + } extension (sym: Symbol) def updateInfo(newInfo: Type)(using Context): Unit = if sym.info ne newInfo then @@ -53,23 +65,102 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: else sym.flags ).installAfter(preRecheckPhase) - /** Hook to be overridden */ - protected def reinfer(tp: Type)(using Context): Type = tp - - def reinferResult(info: Type)(using Context): Type = info match - case info: MethodOrPoly => - info.derivedLambdaType(resType = reinferResult(info.resultType)) - case _ => - reinfer(info) + extension (tpe: Type) def rememberFor(tree: Tree)(using Context): Unit = + if (tpe ne tree.tpe) && !tree.hasAttachment(RecheckedType) then + tree.putAttachment(RecheckedType, tpe) + + def knownType(tree: Tree) = + tree.attachmentOrElse(RecheckedType, tree.tpe) + + def isUpdated(sym: Symbol)(using Context) = + val symd = sym.denot + symd.validFor.firstPhaseId == thisPhase.id && (sym.originDenotation ne symd) + + def transformType(tp: Type, inferred: Boolean)(using Context): Type = tp + + object transformTypes extends TreeTraverser: + + // Substitute parameter symbols in `from` to paramRefs in corresponding + // method or poly types `to`. We use a single BiTypeMap to do everything. + class SubstParams(from: List[List[Symbol]], to: List[LambdaType])(using Context) + extends DeepTypeMap, BiTypeMap: + + def apply(t: Type): Type = t match + case t: NamedType => + val sym = t.symbol + def outer(froms: List[List[Symbol]], tos: List[LambdaType]): Type = + def inner(from: List[Symbol], to: List[ParamRef]): Type = + if from.isEmpty then outer(froms.tail, tos.tail) + else if sym eq from.head then to.head + else inner(from.tail, to.tail) + if tos.isEmpty then t + else inner(froms.head, tos.head.paramRefs) + outer(from, to) + case _ => + mapOver(t) + + def inverse(t: Type): Type = t match + case t: ParamRef => + def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = + if from.isEmpty then t + else if t.binder eq from.head then to.head(t.paramNum).namedType + else recur(from.tail, to.tail) + recur(to, from) + case _ => + mapOver(t) + end SubstParams + + def traverse(tree: Tree)(using Context) = + traverseChildren(tree) + tree match - def enterDef(stat: Tree)(using Context): Unit = - val sym = stat.symbol - stat match - case stat: ValOrDefDef if stat.tpt.isInstanceOf[InferredTypeTree] => - sym.updateInfo(reinferResult(sym.info)) - case stat: Bind => - sym.updateInfo(reinferResult(sym.info)) - case _ => + case tree: TypeTree => + transformType(tree.tpe, tree.isInstanceOf[InferredTypeTree]).rememberFor(tree) + case tree: ValOrDefDef => + val sym = tree.symbol + + // replace an existing symbol info with inferred types + def integrateRT( + info: Type, // symbol info to replace + psymss: List[List[Symbol]], // the local (type and trem) parameter symbols corresponding to `info` + prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order + prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order + ): Type = + info match + case mt: MethodOrPoly => + val psyms = psymss.head + mt.companion(mt.paramNames)( + mt1 => + if !psyms.exists(isUpdated) && !mt.isParamDependent && prevLambdas.isEmpty then + mt.paramInfos + else + val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) + psyms.map(psym => subst(psym.info).asInstanceOf[mt.PInfo]), + mt1 => + integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) + ) + case info: ExprType => + info.derivedExprType(resType = + integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) + case _ => + val restp = knownType(tree.tpt) + if prevLambdas.isEmpty then restp + else SubstParams(prevPsymss, prevLambdas)(restp) + + if tree.tpt.hasAttachment(RecheckedType) && !sym.isConstructor then + val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) + .showing(i"update info $sym: ${sym.info} --> $result", recheckr) + if newInfo ne sym.info then + val completer = new LazyType: + def complete(denot: SymDenotation)(using Context) = + denot.info = newInfo + recheckDef(tree, sym) + sym.updateInfo(completer) + case tree: Bind => + val sym = tree.symbol + sym.updateInfo(transformType(sym.info, inferred = true)) + case _ => + end transformTypes def constFold(tree: Tree, tp: Type)(using Context): Type = val tree1 = tree.withType(tp) @@ -90,10 +181,10 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: excluded = if tree.symbol.is(Private) then EmptyFlags else Private ).suchThat(tree.symbol ==) constFold(tree, qualType.select(name, mbr)) + //.showing(i"recheck select $qualType . $name : ${mbr.symbol.info} = $result") def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match case Bind(name, body) => - enterDef(tree) recheck(body, pt) val sym = tree.symbol if sym.isType then sym.typeRef else sym.info @@ -104,16 +195,13 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: val exprType = recheck(expr, defn.UnitType) bindType - def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = - if !tree.rhs.isEmpty then recheck(tree.rhs, tree.symbol.info) - sym.termRef + def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = + if !tree.rhs.isEmpty then recheck(tree.rhs, sym.info) - def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Type = - tree.paramss.foreach(_.foreach(enterDef)) - val rhsCtx = linkConstructorParams(sym) + def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = + val rhsCtx = linkConstructorParams(sym).withOwner(sym) if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then - recheck(tree.rhs, tree.symbol.localReturnType)(using rhsCtx) - sym.termRef + inContext(rhsCtx) { recheck(tree.rhs, recheck(tree.tpt)) } def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type = recheck(tree.rhs) @@ -134,6 +222,11 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: case _ => mapOver(t) formals.mapConserve(tm) + /** Hook for method type instantiation + */ + protected def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = + mt.instantiate(argTypes) + def recheckApply(tree: Apply, pt: Type)(using Context): Type = recheck(tree.fun).widen match case fntpe: MethodType => @@ -153,7 +246,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: assert(formals.isEmpty) Nil val argTypes = recheckArgs(tree.args, formals, fntpe.paramRefs) - constFold(tree, fntpe.instantiate(argTypes)) + constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol)) def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = recheck(tree.fun).widen match @@ -174,7 +267,10 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def recheckBlock(stats: List[Tree], expr: Tree, pt: Type)(using Context): Type = recheckStats(stats) - val exprType = recheck(expr, pt.dropIfProto) + val exprType = recheck(expr) + // The expected type `pt` is not propagated. Doing so would allow variables in the + // expected type to contain references to local symbols of the block, so the + // local symbols could escape that way. TypeOps.avoid(exprType, localSyms(stats).filterConserve(_.isTerm)) def recheckBlock(tree: Block, pt: Type)(using Context): Type = @@ -195,10 +291,10 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def recheckMatch(tree: Match, pt: Type)(using Context): Type = val selectorType = recheck(tree.selector) - val casesTypes = tree.cases.map(recheck(_, selectorType.widen, pt)) + val casesTypes = tree.cases.map(recheckCase(_, selectorType.widen, pt)) TypeComparer.lub(casesTypes) - def recheck(tree: CaseDef, selType: Type, pt: Type)(using Context): Type = + def recheckCase(tree: CaseDef, selType: Type, pt: Type)(using Context): Type = recheck(tree.pat, selType) recheck(tree.guard, defn.BooleanType) recheck(tree.body, pt) @@ -214,7 +310,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def recheckTry(tree: Try, pt: Type)(using Context): Type = val bodyType = recheck(tree.expr, pt) - val casesTypes = tree.cases.map(recheck(_, defn.ThrowableType, pt)) + val casesTypes = tree.cases.map(recheckCase(_, defn.ThrowableType, pt)) val finalizerType = recheck(tree.finalizer, defn.UnitType) TypeComparer.lub(bodyType :: casesTypes) @@ -227,9 +323,8 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: val elemTypes = tree.elems.map(recheck(_, elemProto)) seqLitType(tree, TypeComparer.lub(declaredElemType :: elemTypes)) - def recheckTypeTree(tree: TypeTree)(using Context): Type = tree match - case tree: InferredTypeTree => reinfer(tree.tpe) - case _ => tree.tpe + def recheckTypeTree(tree: TypeTree)(using Context): Type = + knownType(tree) def recheckAnnotated(tree: Annotated)(using Context): Type = tree.tpe match @@ -246,14 +341,20 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: NoType def recheckStats(stats: List[Tree])(using Context): Unit = - stats.foreach(enterDef) stats.foreach(recheck(_)) + def recheckDef(tree: ValOrDefDef, sym: Symbol)(using Context): Unit = + 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 * @param pt the expected result type */ - def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = trace(i"rechecking $tree with pt = $pt", recheckr, show = true) { + def recheckStart(tree: Tree, pt: Type = WildcardType)(using Context): Type = def recheckNamed(tree: NameTree, pt: Type)(using Context): Type = val sym = tree.symbol @@ -261,11 +362,12 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: case tree: Ident => recheckIdent(tree) case tree: Select => recheckSelect(tree) case tree: Bind => recheckBind(tree, pt) - case tree: ValDef => + case tree: ValOrDefDef => if tree.isEmpty then NoType - else recheckValDef(tree, sym)(using ctx.localContext(tree, sym)) - case tree: DefDef => - recheckDefDef(tree, sym)(using ctx.localContext(tree, sym)) + else + if isUpdated(sym) then sym.ensureCompleted() + else recheckDef(tree, sym) + sym.termRef case tree: TypeDef => tree.rhs match case impl: Template => @@ -295,35 +397,61 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: case tree: PackageDef => recheckPackageDef(tree) case tree: Thicket => defn.NothingType - try - val result = tree match - case tree: NameTree => recheckNamed(tree, pt) - case tree => recheckUnnamed(tree, pt) - checkConforms(result, pt, tree) - result - catch case ex: Exception => - println(i"error while rechecking $tree") - throw ex - } - end recheck + tree match + case tree: NameTree => recheckNamed(tree, pt) + case tree => recheckUnnamed(tree, pt) + end recheckStart + + def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = + checkConforms(tpe, pt, tree) + if keepTypes then tpe.rememberFor(tree) + tpe + + def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = + trace(i"rechecking $tree with pt = $pt", recheckr, show = true) { + try recheckFinish(recheckStart(tree, pt), tree, pt) + catch case ex: Exception => + println(i"error while rechecking $tree") + throw ex + } + + private val debugSuccesses = false def checkConforms(tpe: Type, pt: Type, tree: Tree)(using Context): Unit = tree match - case _: DefTree | EmptyTree | _: TypeTree => + case _: DefTree | EmptyTree | _: TypeTree | _: Closure => + // Don't report closure nodes, since their span is a point; wait instead + // for enclosing block to preduce an error case _ => val actual = tpe.widenExpr val expected = pt.widenExpr + //println(i"check conforms $actual <:< $expected") val isCompatible = actual <:< expected || expected.isRepeatedParam && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) if !isCompatible then - println(i"err at ${ctx.phase}") - err.typeMismatch(tree.withType(tpe), pt) + err.typeMismatch(tree.withType(tpe), expected) + else if debugSuccesses then + tree match + case _: Ident => + println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") + case _ => def checkUnit(unit: CompilationUnit)(using Context): Unit = recheck(unit.tpdTree) end Rechecker + + override def show(tree: untpd.Tree)(using Context): String = + val addRecheckedTypes = new TreeMap: + override def transform(tree: Tree)(using Context): Tree = + val tree1 = super.transform(tree) + tree.getAttachment(RecheckedType) match + case Some(tpe) => tree1.withType(tpe) + case None => tree1 + atPhase(thisPhase) { + super.show(addRecheckedTypes.transform(tree.asInstanceOf[tpd.Tree])) + } end Recheck class TestRecheck extends Recheck: diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index aa5ccce6ad2e..5791d0c7d119 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -374,14 +374,14 @@ class TreeChecker extends Phase with SymTransformer { val tpe = tree.typeOpt // Polymorphic apply methods stay structural until Erasure - val isPolyFunctionApply = (tree.name eq nme.apply) && (tree.qualifier.typeOpt <:< defn.PolyFunctionType) + val isPolyFunctionApply = (tree.name eq nme.apply) && tree.qualifier.typeOpt.derivesFrom(defn.PolyFunctionClass) // Outer selects are pickled specially so don't require a symbol val isOuterSelect = tree.name.is(OuterSelectName) val isPrimitiveArrayOp = ctx.erasedTypes && nme.isPrimitiveName(tree.name) if !(tree.isType || isPolyFunctionApply || isOuterSelect || isPrimitiveArrayOp) then val denot = tree.denot assert(denot.exists, i"Selection $tree with type $tpe does not have a denotation") - assert(denot.symbol.exists, i"Denotation $denot of selection $tree with type $tpe does not have a symbol") + assert(denot.symbol.exists, i"Denotation $denot of selection $tree with type $tpe does not have a symbol, qualifier type = ${tree.qualifier.typeOpt}") val sym = tree.symbol val symIsFixed = tpe match { diff --git a/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala b/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala index 91f63a033c76..92d22b1cc57e 100644 --- a/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala +++ b/compiler/src/dotty/tools/dotc/transform/TryCatchPatterns.scala @@ -71,7 +71,7 @@ class TryCatchPatterns extends MiniPhase { case _ => isDefaultCase(cdef) } - private def isSimpleThrowable(tp: Type)(using Context): Boolean = tp.stripAnnots match { + private def isSimpleThrowable(tp: Type)(using Context): Boolean = tp.stripped match { case tp @ TypeRef(pre, _) => (pre == NoPrefix || pre.typeSymbol.isStatic) && // Does not require outer class check !tp.symbol.is(Flags.Trait) && // Traits not supported by JVM diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index ca977a6799f8..fc914e9b03bf 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -214,7 +214,7 @@ object TypeTestsCasts { * can be true in some cases. Issues a warning or an error otherwise. */ def checkSensical(foundClasses: List[Symbol])(using Context): Boolean = - def exprType = i"type ${expr.tpe.widen.stripAnnots}" + def exprType = i"type ${expr.tpe.widen.stripped}" def check(foundCls: Symbol): Boolean = if (!isCheckable(foundCls)) true else if (!foundCls.derivesFrom(testCls)) { diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala new file mode 100644 index 000000000000..1415016fea26 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -0,0 +1,468 @@ +package dotty.tools +package dotc +package cc + +import core._ +import Phases.*, DenotTransformers.*, SymDenotations.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import Types._ +import Symbols._ +import StdNames._ +import Decorators._ +import config.Printers.{capt, recheckr} +import ast.{tpd, untpd, Trees} +import NameKinds.{DocArtifactName, OuterSelectName, DefaultGetterName} +import Trees._ +import scala.util.control.NonFatal +import typer.ErrorReporting._ +import typer.RefChecks +import util.Spans.Span +import util.{SimpleIdentitySet, EqHashMap, SrcPos} +import util.Chars.* +import transform.* +import transform.SymUtils.* +import scala.collection.mutable +import reporting._ +import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions +import CaptureSet.{CompareResult, withCaptureSetsExplained} + +object CheckCaptures: + import ast.tpd.* + + case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer: Env): + def isOpen = !captured.isAlwaysEmpty && !isBoxed + + final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) + extends ApproximatingTypeMap: + def apply(tp: Type): Type = tp match + case tp: ParamRef => + if tp.binder == from then to(tp.paramNum) else tp + case tp: NamedType => + if tp.prefix `eq` NoPrefix then tp + else tp.derivedSelect(apply(tp.prefix)) + case _: ThisType => + tp + case _ => + mapOver(tp) + + /** Check that a @retains annotation only mentions references that can be tracked + * This check is performed at Typer. + */ + def checkWellformed(ann: Tree)(using Context): Unit = + for elem <- retainedElems(ann) do + elem.tpe match + case ref: CaptureRef => + if !ref.canBeTracked then + report.error(em"$elem cannot be tracked since it is not a parameter or a local variable", elem.srcPos) + case tpe => + report.error(em"$tpe is not a legal type for a capture set", elem.srcPos) + + /** If `tp` is a capturing type, check that all references it mentions have non-empty + * capture sets. + * This check is performed after capture sets are computed in phase cc. + */ + def checkWellformedPost(tp: Type, pos: SrcPos)(using Context): Unit = tp match + case CapturingType(parent, refs, _) => + for ref <- refs.elems do + if ref.captureSetOfInfo.elems.isEmpty then + report.error(em"$ref cannot be tracked since its capture set is empty", pos) + else if parent.captureSet.accountsFor(ref) then + report.warning(em"redundant capture: $parent already accounts for $ref", pos) + case _ => + + def checkWellformedPost(ann: Tree)(using Context): Unit = + /** The lists `elems(i) :: prev.reerse :: elems(0),...,elems(i-1),elems(i+1),elems(n)` + * where `n == elems.length-1`, i <- 0..n`. + */ + def choices(prev: List[Tree], elems: List[Tree]): List[List[Tree]] = elems match + case Nil => Nil + case elem :: elems => + List(elem :: (prev reverse_::: elems)) ++ choices(elem :: prev, elems) + for case first :: others <- choices(Nil, retainedElems(ann)) do + val firstRef = first.toCaptureRef + val remaining = CaptureSet(others.map(_.toCaptureRef)*) + if remaining.accountsFor(firstRef) then + report.warning(em"redundant capture: $remaining already accounts for $firstRef", ann.srcPos) + + private inline val disallowGlobal = true + +class CheckCaptures extends Recheck: + thisPhase => + + import ast.tpd.* + import CheckCaptures.* + + def phaseName: String = "cc" + override def isEnabled(using Context) = ctx.settings.Ycc.value + + def newRechecker()(using Context) = CaptureChecker(ctx) + + override def run(using Context): Unit = + checkOverrides.traverse(ctx.compilationUnit.tpdTree) + super.run + + def checkOverrides = new TreeTraverser: + def traverse(t: Tree)(using Context) = + t match + case t: Template => + // ^^^ TODO: Can we avoid doing overrides checks twice? + // We need to do them here since only at this phase CaptureTypes are relevant + // But maybe we can then elide the check during the RefChecks phase if -Ycc is set? + RefChecks.checkAllOverrides(ctx.owner.asClass) + case _ => + traverseChildren(t) + + class CaptureChecker(ictx: Context) extends Rechecker(ictx): + import ast.tpd.* + + override def transformType(tp: Type, inferred: Boolean)(using Context): Type = + + def addInnerVars(tp: Type): Type = tp match + case tp @ AppliedType(tycon, args) => + tp.derivedAppliedType(tycon, args.map(addVars(_, boxed = true))) + case tp @ RefinedType(core, rname, rinfo) => + val rinfo1 = addVars(rinfo) + if defn.isFunctionType(tp) then + rinfo1.toFunctionType(isJava = false, alwaysDependent = true) + else + tp.derivedRefinedType(addInnerVars(core), rname, rinfo1) + case tp: MethodType => + tp.derivedLambdaType( + paramInfos = tp.paramInfos.mapConserve(addVars(_)), + resType = addVars(tp.resType)) + case tp: PolyType => + tp.derivedLambdaType( + resType = addVars(tp.resType)) + case tp: ExprType => + tp.derivedExprType(resType = addVars(tp.resType)) + case _ => + tp + + def addFunctionRefinements(tp: Type): Type = tp match + case tp @ AppliedType(tycon, args) => + if defn.isNonRefinedFunction(tp) then + MethodType.companion( + isContextual = defn.isContextFunctionClass(tycon.classSymbol), + isErased = defn.isErasedFunctionClass(tycon.classSymbol) + )(args.init, addFunctionRefinements(args.last)) + .toFunctionType(isJava = false, alwaysDependent = true) + .showing(i"add function refinement $tp --> $result", capt) + else + tp.derivedAppliedType(tycon, args.map(addFunctionRefinements(_))) + case tp @ RefinedType(core, rname, rinfo) if !defn.isFunctionType(tp) => + tp.derivedRefinedType( + addFunctionRefinements(core), rname, addFunctionRefinements(rinfo)) + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = addFunctionRefinements(tp.resType)) + case tp: ExprType => + tp.derivedExprType(resType = addFunctionRefinements(tp.resType)) + case _ => + tp + + /** Refine a possibly applied class type C where the class has tracked parameters + * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } + * where CV_1, ..., CV_n are fresh capture sets. + */ + def addCaptureRefinements(tp: Type): Type = tp.stripped match + case _: TypeRef | _: AppliedType if tp.typeSymbol.isClass => + val cls = tp.typeSymbol.asClass + cls.paramGetters.foldLeft(tp) { (core, getter) => + if getter.termRef.isTracked then + val getterType = tp.memberInfo(getter).strippedDealias + RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), boxed = false)) + .showing(i"add capture refinement $tp --> $result", capt) + else + core + } + case _ => + tp + + def addVars(tp: Type, boxed: Boolean = false): Type = + var tp1 = addInnerVars(tp) + val tp2 = addCaptureRefinements(tp1) + if tp1.canHaveInferredCapture + then CapturingType(tp2, CaptureSet.Var(), boxed) + else tp2 + + if inferred then + val cleanup = new TypeMap: + def apply(t: Type) = t match + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + apply(parent) + case _ => + mapOver(t) + addVars(addFunctionRefinements(cleanup(tp))) + .showing(i"reinfer $tp --> $result", capt) + else + val addBoxes = new TypeTraverser: + def setBoxed(t: Type) = t match + case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => + annot.tree.setBoxedCapturing() + case _ => + + def traverse(t: Type) = + t match + case AppliedType(tycon, args) if !defn.isNonRefinedFunction(t) => + args.foreach(setBoxed) + case TypeBounds(lo, hi) => + setBoxed(lo); setBoxed(hi) + case _ => + traverseChildren(t) + end addBoxes + + addBoxes.traverse(tp) + tp + end transformType + + private def interpolator(using Context) = new TypeTraverser: + override def traverse(t: Type) = + t match + case CapturingType(parent, refs: CaptureSet.Var, _) => + if variance < 0 then capt.println(i"solving $t") + refs.solve(variance) + traverse(parent) + case t @ RefinedType(_, nme.apply, rinfo) if defn.isFunctionOrPolyType(t) => + traverse(rinfo) + case tp: TypeVar => + case tp: TypeRef => + traverse(tp.prefix) + case _ => + traverseChildren(t) + + private def interpolateVarsIn(tpt: Tree)(using Context): Unit = + if tpt.isInstanceOf[InferredTypeTree] then + interpolator.traverse(knownType(tpt)) + .showing(i"solved vars in ${knownType(tpt)}", capt) + + private var curEnv: Env = Env(NoSymbol, CaptureSet.empty, false, null) + + private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap() + def capturedVars(sym: Symbol)(using Context) = + myCapturedVars.getOrElseUpdate(sym, + if sym.ownersIterator.exists(_.isTerm) then CaptureSet.Var() + else CaptureSet.empty) + + def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = + if sym.exists then + val ref = sym.termRef + def recur(env: Env): Unit = + if env.isOpen && env.owner != sym.enclosure then + capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") + checkElem(ref, env.captured, pos) + if env.owner.isConstructor then + if env.outer.owner != sym.enclosure then recur(env.outer.outer) + else recur(env.outer) + if ref.isTracked then recur(curEnv) + + def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = + if curEnv.isOpen then + val ownEnclosure = ctx.owner.enclosingMethodOrClass + var targetSet = capturedVars(sym) + if !targetSet.isAlwaysEmpty && sym.enclosure == ownEnclosure then + targetSet = targetSet.filter { + case ref: TermRef => ref.symbol.enclosure != ownEnclosure + case _ => true + } + checkSubset(targetSet, curEnv.captured, pos) + + def includeBoxedCaptures(tp: Type, pos: SrcPos)(using Context): Unit = + if curEnv.isOpen then + val ownEnclosure = ctx.owner.enclosingMethodOrClass + val targetSet = tp.boxedCaptured.filter { + case ref: TermRef => ref.symbol.enclosure != ownEnclosure + case _ => true + } + checkSubset(targetSet, curEnv.captured, pos) + + def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = + assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + + def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos)(using Context) = + val res = elem.singletonCaptureSet.subCaptures(cs, frozen = false) + if !res.isOK then + report.error(i"$elem cannot be referenced here; it is not included in allowed capture set ${res.blocking}", pos) + + def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos)(using Context) = + val res = cs1.subCaptures(cs2, frozen = false) + if !res.isOK then + report.error(i"references $cs1 are not all included in allowed capture set ${res.blocking}", pos) + + override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = + val cs = capturedVars(tree.meth.symbol) + recheckr.println(i"typing closure $tree with cvs $cs") + super.recheckClosure(tree, pt).capturing(cs) + .showing(i"rechecked $tree, $result", capt) + + override def recheckIdent(tree: Ident)(using Context): Type = + markFree(tree.symbol, tree.srcPos) + if tree.symbol.is(Method) then includeCallCaptures(tree.symbol, tree.srcPos) + super.recheckIdent(tree) + + override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = + try super.recheckValDef(tree, sym) + finally + if !sym.is(Param) then + // parameters with inferred types belong to anonymous methods. We need to wait + // for more info from the context, so we cannot interpolate. Note that we cannot + // expect to have all necessary info available at the point where the anonymous + // function is compiled since we do not propagate expected types into blocks. + interpolateVarsIn(tree.tpt) + + override def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = + val saved = curEnv + val localSet = capturedVars(sym) + if !localSet.isAlwaysEmpty then curEnv = Env(sym, localSet, false, curEnv) + try super.recheckDefDef(tree, sym) + finally + interpolateVarsIn(tree.tpt) + curEnv = saved + + override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = + for param <- cls.paramGetters do + if param.is(Private) && !param.info.captureSet.isAlwaysEmpty then + report.error( + "Implementation restriction: Class parameter with non-empty capture set must be a `val`", + param.srcPos) + val saved = curEnv + val localSet = capturedVars(cls) + if !localSet.isAlwaysEmpty then curEnv = Env(cls, localSet, false, curEnv) + try super.recheckClassDef(tree, impl, cls) + finally curEnv = saved + + /** First half: Refine the type of a constructor call `new C(t_1, ..., t_n)` + * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked + * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. + * + * Second half: union of all capture sets of arguments to tracked parameters. + */ + private def addParamArgRefinements(core: Type, argTypes: List[Type], cls: ClassSymbol)(using Context): (Type, CaptureSet) = + cls.paramGetters.lazyZip(argTypes).foldLeft((core, CaptureSet.empty: CaptureSet)) { (acc, refine) => + val (core, allCaptures) = acc + val (getter, argType) = refine + if getter.termRef.isTracked then + (RefinedType(core, getter.name, argType), allCaptures ++ argType.captureSet) + else + (core, allCaptures) + } + + /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. + * This means: + * - Instantiate result type with actual arguments + * - If call is to a constructor: + * - remember types of arguments corresponding to tracked + * parameters in refinements. + * - add capture set of instantiated class to capture set of result type. + */ + override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = + val ownType = + if mt.isResultDependent then SubstParamsMap(mt, argTypes)(mt.resType) + else mt.resType + if sym.isConstructor then + val cls = sym.owner.asClass + val (refined, cs) = addParamArgRefinements(ownType, argTypes, cls) + refined.capturing(cs ++ capturedVars(cls) ++ capturedVars(sym)) + .showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt) + else ownType + + def recheckByNameArg(tree: Tree, pt: Type)(using Context): Type = + val closureDef(mdef) = tree + val arg = mdef.rhs + val localSet = CaptureSet.Var() + curEnv = Env(mdef.symbol, localSet, isBoxed = false, curEnv) + val result = + try + inContext(ctx.withOwner(mdef.symbol)) { + recheckStart(arg, pt).capturing(localSet) + } + finally curEnv = curEnv.outer + recheckFinish(result, arg, pt) + + override def recheckApply(tree: Apply, pt: Type)(using Context): Type = + if tree.symbol == defn.cbnArg then + recheckByNameArg(tree.args(0), pt) + else + includeCallCaptures(tree.symbol, tree.srcPos) + super.recheckApply(tree, pt) + + override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = + val res = super.recheck(tree, pt) + if tree.isTerm then + includeBoxedCaptures(res, tree.srcPos) + res + + override def checkUnit(unit: CompilationUnit)(using Context): Unit = + withCaptureSetsExplained { + super.checkUnit(unit) + PostRefinerCheck.traverse(unit.tpdTree) + if ctx.settings.YccDebug.value then + show(unit.tpdTree) // this dows not print tree, but makes its variables visible for dependency printing + } + + def checkNotGlobal(tree: Tree, allArgs: Tree*)(using Context): Unit = + if disallowGlobal then + tree match + case LambdaTypeTree(_, restpt) => + checkNotGlobal(restpt, allArgs*) + case _ => + for ref <- knownType(tree).captureSet.elems do + val isGlobal = ref match + case ref: TermRef => + ref.isRootCapability || ref.prefix != NoPrefix && ref.symbol.hasAnnotation(defn.AbilityAnnot) + case _ => false + val what = if ref.isRootCapability then "universal" else "global" + if isGlobal then + val notAllowed = i" is not allowed to capture the $what capability $ref" + def msg = tree match + case tree: InferredTypeTree => + i"""inferred type argument ${knownType(tree)}$notAllowed + | + |The inferred arguments are: [${allArgs.map(knownType)}%, %]""" + case _ => s"type argument$notAllowed" + report.error(msg, tree.srcPos) + + object PostRefinerCheck extends TreeTraverser: + def traverse(tree: Tree)(using Context) = + tree match + case _: InferredTypeTree => + case tree: TypeTree if !tree.span.isZeroExtent => + knownType(tree).foreachPart( + checkWellformedPost(_, tree.srcPos)) + knownType(tree).foreachPart { + case AnnotatedType(_, annot) => + checkWellformedPost(annot.tree) + case _ => + } + case tree1 @ TypeApply(fn, args) if disallowGlobal => + for arg <- args do + //println(i"checking $arg in $tree: ${knownType(tree).captureSet}") + checkNotGlobal(arg, args*) + case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] => + val sym = t.symbol + val isLocal = + sym.ownersIterator.exists(_.isTerm) + || sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass) + + // The following classes of definitions need explicit capture types ... + if !isLocal // ... since external capture types are not inferred + || sym.owner.is(Trait) // ... since we do OverridingPairs checking before capture inference + || sym.allOverriddenSymbols.nonEmpty // ... since we do override checking before capture inference + then + val inferred = knownType(t.tpt) + def checkPure(tp: Type) = tp match + case CapturingType(_, refs, _) if !refs.elems.isEmpty => + val resultStr = if t.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Non-local $sym cannot have an inferred$resultStr type + |$inferred + |with non-empty capture set $refs. + |The type needs to be declared explicitly.""", t.srcPos) + case _ => + inferred.foreachPart(checkPure, StopAt.Static) + case _ => + traverseChildren(tree) + + def postRefinerCheck(tree: tpd.Tree)(using Context): Unit = + PostRefinerCheck.traverse(tree) + + end CaptureChecker +end CheckCaptures diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 7629776525a0..6de903972c1f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -74,9 +74,8 @@ object Checking { } for (arg, which, bound) <- TypeOps.boundsViolations(args, boundss, instantiate, app) do report.error( - showInferred(DoesNotConformToBound(arg.tpe, which, bound), - app, tpt), - arg.srcPos.focus) + showInferred(DoesNotConformToBound(arg.tpe, which, bound), app, tpt), + arg.srcPos.focus) /** Check that type arguments `args` conform to corresponding bounds in `tl` * Note: This does not check the bounds of AppliedTypeTrees. These @@ -325,6 +324,7 @@ object Checking { case AndType(tp1, tp2) => isInteresting(tp1) || isInteresting(tp2) case OrType(tp1, tp2) => isInteresting(tp1) && isInteresting(tp2) case _: RefinedOrRecType | _: AppliedType => true + case tp: AnnotatedType => isInteresting(tp.parent) case _ => false } diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 1d075902c816..5bb85c1ee67d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -12,6 +12,8 @@ import util.{Stats, SimpleIdentityMap, SrcPos} import Decorators._ import config.Printers.{gadts, typr} import annotation.tailrec +import reporting._ +import cc.{CapturingType, derivedCapturingType} import collection.mutable import scala.annotation.internal.sharable @@ -126,8 +128,8 @@ object Inferencing { couldInstantiateTypeVar(parent, applied) case tp: AndOrType => couldInstantiateTypeVar(tp.tp1, applied) || couldInstantiateTypeVar(tp.tp2, applied) - case AnnotatedType(tp, _) => - couldInstantiateTypeVar(tp, applied) + case tp: AnnotatedType => + couldInstantiateTypeVar(tp.parent, applied) case _ => false @@ -543,6 +545,7 @@ object Inferencing { case tp: RefinedType => tp.derivedRefinedType(captureWildcards(tp.parent), tp.refinedName, tp.refinedInfo) case tp: RecType => tp.derivedRecType(captureWildcards(tp.parent)) case tp: LazyRef => captureWildcards(tp.ref) + case CapturingType(parent, refs, _) => tp.derivedCapturingType(captureWildcards(parent), refs) case tp: AnnotatedType => tp.derivedAnnotatedType(captureWildcards(tp.parent), tp.annot) case _ => tp } @@ -733,6 +736,7 @@ trait Inferencing { this: Typer => if !argType.isSingleton then argType = SkolemType(argType) argType <:< tvar case _ => + () // scala-meta complains if this is missing, but I could not mimimize further end constrainIfDependentParamRef } @@ -747,4 +751,3 @@ trait Inferencing { this: Typer => enum IfBottom: case ok, fail, flip - diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index b416b028efe8..9f1a40080cb2 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -272,7 +272,7 @@ object RefChecks { * TODO This still needs to be cleaned up; the current version is a straight port of what was there * before, but it looks too complicated and method bodies are far too large. */ - private def checkAllOverrides(clazz: ClassSymbol)(using Context): Unit = { + def checkAllOverrides(clazz: ClassSymbol)(using Context): Unit = { val self = clazz.thisType val upwardsSelf = upwardsThisType(clazz) var hasErrors = false diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 9d31e29c7e2b..c2f928a7d6c7 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -10,7 +10,8 @@ import util.SrcPos import NameOps._ import collection.mutable import reporting._ -import Checking.checkNoPrivateLeaks +import Checking.{checkNoPrivateLeaks, checkNoWildcard} +import cc.CaptureSet trait TypeAssigner { import tpd.* @@ -187,6 +188,14 @@ trait TypeAssigner { if tpe.isError then tpe else errorType(ex"$whatCanNot be accessed as a member of $pre$where.$whyNot", pos) + def processAppliedType(tree: untpd.Tree, tp: Type)(using Context): Type = tp match + case AppliedType(tycon, args) => + val constr = tycon.typeSymbol + if constr == defn.andType then AndType(args(0), args(1)) + else if constr == defn.orType then OrType(args(0), args(1), soft = false) + else tp + case _ => tp + /** Type assignment method. Each method takes as parameters * - an untpd.Tree to which it assigns a type, * - typed child trees it needs to access to cpmpute that type, @@ -288,7 +297,10 @@ trait TypeAssigner { val ownType = fn.tpe.widen match { case fntpe: MethodType => if (fntpe.paramInfos.hasSameLengthAs(args) || ctx.phase.prev.relaxedTyping) - safeSubstMethodParams(fntpe, args.tpes) + if fntpe.isCaptureDependent then + fntpe.resultType.substParams(fntpe, args.tpes) + else + safeSubstMethodParams(fntpe, args.tpes) else errorType(i"wrong number of arguments at ${ctx.phase.prev} for $fntpe: ${fn.tpe}, expected: ${fntpe.paramInfos.length}, found: ${args.length}", tree.srcPos) case t => @@ -470,11 +482,10 @@ trait TypeAssigner { assert(!hasNamedArg(args) || ctx.reporter.errorsReported, tree) val tparams = tycon.tpe.typeParams val ownType = - if (tparams.hasSameLengthAs(args)) - if (tycon.symbol == defn.andType) AndType(args(0).tpe, args(1).tpe) - else if (tycon.symbol == defn.orType) OrType(args(0).tpe, args(1).tpe, soft = false) - else tycon.tpe.appliedTo(args.tpes) - else wrongNumberOfTypeArgs(tycon.tpe, tparams, args, tree.srcPos) + if tparams.hasSameLengthAs(args) then + processAppliedType(tree, tycon.tpe.appliedTo(args.tpes)) + else + wrongNumberOfTypeArgs(tycon.tpe, tparams, args, tree.srcPos) tree.withType(ownType) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index ef0c3a49a02c..796fe0dcf91b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -49,6 +49,8 @@ import transform.TypeUtils._ import reporting._ import Nullables._ import NullOpsDecorator._ +import cc.CheckCaptures +import config.Config import scala.annotation.constructorOnly @@ -1200,7 +1202,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer case _ => mapOver(t) } - val pt1 = pt.stripTypeVar.dealias.normalized + val pt1 = pt.strippedDealias.normalized if (pt1 ne pt1.dropDependentRefinement) && defn.isContextFunctionType(pt1.nonPrivateMember(nme.apply).info.finalResultType) then @@ -2700,6 +2702,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer registerNowarn(annot1, tree) val arg1 = typed(tree.arg, pt) if (ctx.mode is Mode.Type) { + if annot1.symbol.maybeOwner == defn.RetainsAnnot then + CheckCaptures.checkWellformed(annot1) if arg1.isType then assignType(cpy.Annotated(tree)(arg1, annot1), arg1, annot1) else diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index ed49699b633a..98d74f03920c 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -17,6 +17,7 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { def map[B <: AnyRef](f: Elem => B): SimpleIdentitySet[B] def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A def toList: List[Elem] + def iterator: Iterator[Elem] final def isEmpty: Boolean = size == 0 @@ -61,6 +62,7 @@ object SimpleIdentitySet { def map[B <: AnyRef](f: Nothing => B): SimpleIdentitySet[B] = empty def /: [A, E <: AnyRef](z: A)(f: (A, E) => A): A = z def toList = Nil + def iterator = Iterator.empty } private class Set1[+Elem <: AnyRef](x0: AnyRef) extends SimpleIdentitySet[Elem] { @@ -78,6 +80,7 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(z, x0.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: Nil + def iterator = Iterator.single(x0.asInstanceOf[Elem]) } private class Set2[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef) extends SimpleIdentitySet[Elem] { @@ -97,6 +100,10 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: Nil + def iterator = Iterator.tabulate(2) { + case 0 => x0.asInstanceOf[Elem] + case 1 => x1.asInstanceOf[Elem] + } } private class Set3[+Elem <: AnyRef](x0: AnyRef, x1: AnyRef, x2: AnyRef) extends SimpleIdentitySet[Elem] { @@ -127,6 +134,11 @@ object SimpleIdentitySet { def /: [A, E >: Elem <: AnyRef](z: A)(f: (A, E) => A): A = f(f(f(z, x0.asInstanceOf[E]), x1.asInstanceOf[E]), x2.asInstanceOf[E]) def toList = x0.asInstanceOf[Elem] :: x1.asInstanceOf[Elem] :: x2.asInstanceOf[Elem] :: Nil + def iterator = Iterator.tabulate(3) { + case 0 => x0.asInstanceOf[Elem] + case 1 => x1.asInstanceOf[Elem] + case 2 => x2.asInstanceOf[Elem] + } } private class SetN[+Elem <: AnyRef](val xs: Array[AnyRef]) extends SimpleIdentitySet[Elem] { @@ -175,6 +187,7 @@ object SimpleIdentitySet { foreach(buf += _) buf.toList } + def iterator = xs.iterator.asInstanceOf[Iterator[Elem]] override def ++ [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): SimpleIdentitySet[E] = that match { case that: SetN[?] => diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index fb84a01d8daf..f61ac453a741 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -40,6 +40,7 @@ class CompilationTests { compileFilesInDir("tests/pos-special/isInstanceOf", allowDeepSubtypes.and("-Xfatal-warnings")), compileFilesInDir("tests/new", defaultOptions.and("-source", "3.2")), // just to see whether 3.2 works compileFilesInDir("tests/pos-scala2", scala2CompatMode), + compileFilesInDir("tests/pos-custom-args/captures", defaultOptions.and("-Ycc")), compileFilesInDir("tests/pos-custom-args/erased", defaultOptions.and("-language:experimental.erasedDefinitions")), compileFilesInDir("tests/pos", defaultOptions.and("-Ysafe-init")), compileFilesInDir("tests/pos-deep-subtype", allowDeepSubtypes), @@ -139,6 +140,7 @@ class CompilationTests { compileFilesInDir("tests/neg-custom-args/allow-double-bindings", allowDoubleBindings), compileFilesInDir("tests/neg-custom-args/allow-deep-subtypes", allowDeepSubtypes), compileFilesInDir("tests/neg-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")), + compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-Ycc")), compileDir("tests/neg-custom-args/impl-conv", defaultOptions.and("-Xfatal-warnings", "-feature")), compileDir("tests/neg-custom-args/i13946", defaultOptions.and("-Xfatal-warnings", "-feature")), compileFile("tests/neg-custom-args/avoid-warn-deprecation.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), @@ -183,6 +185,7 @@ class CompilationTests { compileFile("tests/neg-custom-args/deptypes.scala", defaultOptions.and("-language:experimental.dependent")), compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")), + compileFile("tests/neg-custom-args/capt-wf.scala", defaultOptions.and("-Ycc", "-Xfatal-warnings")), compileFile("tests/neg-custom-args/feature-shadowing.scala", defaultOptions.and("-Xfatal-warnings", "-feature")), compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")), compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")), diff --git a/library/src-bootstrapped/scala/Retains.scala b/library/src-bootstrapped/scala/Retains.scala new file mode 100644 index 000000000000..f3bfa282a012 --- /dev/null +++ b/library/src-bootstrapped/scala/Retains.scala @@ -0,0 +1,6 @@ +package scala + +/** An annotation that indicates capture + */ +class retains(xs: Any*) extends annotation.StaticAnnotation + diff --git a/library/src-bootstrapped/scala/annotation/ability.scala b/library/src-bootstrapped/scala/annotation/ability.scala new file mode 100644 index 000000000000..8b327a2f8b02 --- /dev/null +++ b/library/src-bootstrapped/scala/annotation/ability.scala @@ -0,0 +1,9 @@ +package scala.annotation + +/** An annotation inidcating that a val should be tracked as its own ability. + * Example: + * + * @ability erased val canThrow: * = ??? + * ^^^ rename to capability + */ +class ability extends StaticAnnotation \ No newline at end of file diff --git a/tests/disabled/neg-custom-args/captures/capt-wf.scala b/tests/disabled/neg-custom-args/captures/capt-wf.scala new file mode 100644 index 000000000000..54fe545f443b --- /dev/null +++ b/tests/disabled/neg-custom-args/captures/capt-wf.scala @@ -0,0 +1,19 @@ +// No longer valid +class C +type Cap = C @retains(*) +type Top = Any @retains(*) + +type T = (x: Cap) => List[String @retains(x)] => Unit // error +val x: (x: Cap) => Array[String @retains(x)] = ??? // error +val y = x + +def test: Unit = + def f(x: Cap) = // ok + val g = (xs: List[String @retains(x)]) => () + g + def f2(x: Cap)(xs: List[String @retains(x)]) = () + val x = f // error + val x2 = f2 // error + val y = f(C()) // ok + val y2 = f2(C()) // ok + () diff --git a/tests/disabled/neg-custom-args/captures/try2.check b/tests/disabled/neg-custom-args/captures/try2.check new file mode 100644 index 000000000000..c7b20d0f7c5e --- /dev/null +++ b/tests/disabled/neg-custom-args/captures/try2.check @@ -0,0 +1,38 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try2.scala:31:32 ----------------------------------------- +31 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {x} () => Nothing + | Required: () => Nothing + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try2.scala:45:2 ------------------------------------------ +45 | yy // error + | ^^ + | Found: (yy : List[(xx : (() => Int) retains canThrow)]) + | Required: List[() => Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try2.scala:52:2 ------------------------------------------ +47 |val global = handle { +48 | (x: CanThrow[Exception]) => +49 | () => +50 | raise(new Exception)(using x) +51 | 22 +52 |} { // error + | ^ + | Found: (() => Int) retains canThrow + | Required: () => Int +53 | (ex: Exception) => () => 22 +54 |} + +longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/try2.scala:24:28 -------------------------------------------------------------- +24 | val a = handle[Exception, CanThrow[Exception]] { // error + | ^^^^^^^^^^^^^^^^^^^ + | type argument is not allowed to capture the global capability (canThrow : *) +-- Error: tests/neg-custom-args/captures/try2.scala:36:11 -------------------------------------------------------------- +36 | val xx = handle { // error + | ^^^^^^ + |inferred type argument ((() => Int) retains canThrow) is not allowed to capture the global capability (canThrow : *) + | + |The inferred arguments are: [Exception, ((() => Int) retains canThrow)] diff --git a/tests/disabled/neg-custom-args/captures/try2.scala b/tests/disabled/neg-custom-args/captures/try2.scala new file mode 100644 index 000000000000..dd3cc890a197 --- /dev/null +++ b/tests/disabled/neg-custom-args/captures/try2.scala @@ -0,0 +1,55 @@ +// Retains syntax for classes not (yet?) supported +import language.experimental.erasedDefinitions +import annotation.ability + +@ability erased val canThrow: * = ??? + +class CanThrow[E <: Exception] extends Retains[canThrow.type] +type Top = Any @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R <: Top](op: CanThrow[E] => R)(handler: E => R): R = + val x: CanThrow[E] = ??? + try op(x) + catch case ex: E => handler(ex) + +def test: List[() => Int] = + val a = handle[Exception, CanThrow[Exception]] { // error + (x: CanThrow[Exception]) => x + }{ + (ex: Exception) => ??? + } + + val b = handle[Exception, () => Nothing] { + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) // error + } { + (ex: Exception) => ??? + } + + val xx = handle { // error + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 + } { + (ex: Exception) => () => 22 + } + val yy = xx :: Nil + yy // error + +val global = handle { + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 +} { // error + (ex: Exception) => () => 22 +} diff --git a/tests/disabled/pos/lazylist.scala b/tests/disabled/pos/lazylist.scala new file mode 100644 index 000000000000..be628113d2d8 --- /dev/null +++ b/tests/disabled/pos/lazylist.scala @@ -0,0 +1,51 @@ +package lazylists + +abstract class LazyList[+T]: + this: ({*} LazyList[T]) => + + def isEmpty: Boolean + def head: T + def tail: LazyList[T] + + def map[U](f: {*} T => U): {f, this} LazyList[U] = + if isEmpty then LazyNil + else LazyCons(f(head), () => tail.map(f)) + + def concat[U >: T](that: {*} LazyList[U]): {this, that} LazyList[U] + +// def flatMap[U](f: {*} T => LazyList[U]): {f, this} LazyList[U] + +class LazyCons[+T](val x: T, val xs: {*} () => {*} LazyList[T]) extends LazyList[T]: + def isEmpty = false + def head = x + def tail: {*} LazyList[T] = xs() + def concat[U >: T](that: {*} LazyList[U]): {this, that} LazyList[U] = + LazyCons(x, () => xs().concat(that)) +// def flatMap[U](f: {*} T => LazyList[U]): {f, this} LazyList[U] = +// f(x).concat(xs().flatMap(f)) + +object LazyNil extends LazyList[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + def concat[U](that: {*} LazyList[U]): {that} LazyList[U] = that +// def flatMap[U](f: {*} Nothing => LazyList[U]): LazyList[U] = LazyNil + +def map[A, B](xs: {*} LazyList[A], f: {*} A => B): {f, xs} LazyList[B] = + xs.map(f) + +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap, cap3: Cap) = + def f[T](x: LazyList[T]): LazyList[T] = if cap1 == cap1 then x else LazyNil + def g(x: Int) = if cap2 == cap2 then x else 0 + def h(x: Int) = if cap3 == cap3 then x else 0 + val ref1 = LazyCons(1, () => f(LazyNil)) + val ref1c: {cap1} LazyList[Int] = ref1 + val ref2 = map(ref1, g) + val ref2c: {cap2, ref1} LazyList[Int] = ref2 + val ref3 = ref1.map(g) + val ref3c: {cap2, ref1} LazyList[Int] = ref3 + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(h) + val ref4c: {cap1, cap2, cap3} LazyList[Int] = ref4 \ No newline at end of file diff --git a/tests/neg/i9325.scala b/tests/neg-custom-args/allow-deep-subtypes/i9325.scala similarity index 100% rename from tests/neg/i9325.scala rename to tests/neg-custom-args/allow-deep-subtypes/i9325.scala diff --git a/tests/neg-custom-args/capt-wf.scala b/tests/neg-custom-args/capt-wf.scala new file mode 100644 index 000000000000..dc4d6a0d4bff --- /dev/null +++ b/tests/neg-custom-args/capt-wf.scala @@ -0,0 +1,35 @@ +class C +type Cap = {*} C + +object foo + +def test(c: Cap, other: String): Unit = + val x1: {*} C = ??? // OK + val x2: {other} C = ??? // error: cs is empty + val s1 = () => "abc" + val x3: {s1} C = ??? // error: cs is empty + val x3a: () => String = s1 + val s2 = () => if x1 == null then "" else "abc" + val x4: {s2} C = ??? // OK + val x5: {c, c} C = ??? // error: redundant + val x6: {c} {c} C = ??? // error: redundant + val x7: {c} Cap = ??? // error: redundant + val x8: {*} {c} C = ??? // OK + val x9: {c, *} C = ??? // error: redundant + val x10: {*, c} C = ??? // error: redundant + + def even(n: Int): Boolean = if n == 0 then true else odd(n - 1) + def odd(n: Int): Boolean = if n == 1 then true else even(n - 1) + val e1 = even + val o1 = odd + + val y1: {e1} String = ??? // error cs is empty + val y2: {o1} String = ??? // error cs is empty + + lazy val ev: (Int => Boolean) = (n: Int) => + lazy val od: (Int => Boolean) = (n: Int) => + if n == 1 then true else ev(n - 1) + if n == 0 then true else od(n - 1) + val y3: {ev} String = ??? // error cs is empty + + () \ No newline at end of file diff --git a/tests/neg-custom-args/captures/bounded.scala b/tests/neg-custom-args/captures/bounded.scala new file mode 100644 index 000000000000..dc2621e95a65 --- /dev/null +++ b/tests/neg-custom-args/captures/bounded.scala @@ -0,0 +1,14 @@ +class CC +type Cap = {*} CC + +def test(c: Cap) = + class B[X <: {c} Object](x: X): + def elem = x + def lateElem = () => x + + def f(x: Int): Int = if c == c then x else 0 + val b = new B(f) + val r1 = b.elem + val r1c: {c} Int => Int = r1 + val r2 = b.lateElem + val r2c: () => {c} Int => Int = r2 // error \ No newline at end of file diff --git a/tests/neg-custom-args/captures/boxmap.check b/tests/neg-custom-args/captures/boxmap.check new file mode 100644 index 000000000000..406077077af5 --- /dev/null +++ b/tests/neg-custom-args/captures/boxmap.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boxmap.scala:14:2 ---------------------------------------- +14 | () => b[Box[B]]((x: A) => box(f(x))) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {f} () => ? Box[B] + | Required: () => Box[B] + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/boxmap.scala b/tests/neg-custom-args/captures/boxmap.scala new file mode 100644 index 000000000000..e335320ef9d4 --- /dev/null +++ b/tests/neg-custom-args/captures/boxmap.scala @@ -0,0 +1,14 @@ +type Top = Any @retains(*) + +infix type ==> [A, B] = (A => B) @retains(*) + +type Box[+T <: Top] = ([K <: Top] => (T ==> K) => K) + +def box[T <: Top](x: T): Box[T] = + [K <: Top] => (k: T ==> K) => k(x) + +def map[A <: Top, B <: Top](b: Box[A])(f: A ==> B): Box[B] = + b[Box[B]]((x: A) => box(f(x))) + +def lazymap[A <: Top, B <: Top](b: Box[A])(f: A ==> B): () => Box[B] = + () => b[Box[B]]((x: A) => box(f(x))) // error diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala new file mode 100644 index 000000000000..526cdc50952f --- /dev/null +++ b/tests/neg-custom-args/captures/byname.scala @@ -0,0 +1,10 @@ +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap) = + def f() = if cap1 == cap1 then g else g + def g(x: Int) = if cap2 == cap2 then 1 else x + def h(ff: => {cap2} Int => Int) = ff + h(f()) // error + + diff --git a/tests/neg-custom-args/captures/capt-box-env.scala b/tests/neg-custom-args/captures/capt-box-env.scala new file mode 100644 index 000000000000..e9743054076e --- /dev/null +++ b/tests/neg-custom-args/captures/capt-box-env.scala @@ -0,0 +1,12 @@ +class C +type Cap = {*} C + +class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + +def test(c: Cap) = + def f(x: Cap): Unit = if c == x then () + val p = Pair(f, f) + val g = () => p.fst == p.snd + val gc: () => Boolean = g // error diff --git a/tests/neg-custom-args/captures/capt-box.scala b/tests/neg-custom-args/captures/capt-box.scala new file mode 100644 index 000000000000..317fc064ec0b --- /dev/null +++ b/tests/neg-custom-args/captures/capt-box.scala @@ -0,0 +1,13 @@ +//import scala.retains +class C +type Cap = {*} C + +def test(x: Cap) = + + def foo(y: Cap) = if x == y then println() + + val x1 = foo + + val x2 = identity(x1) + + val x3: Cap => Unit = x2 // error \ No newline at end of file diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala new file mode 100644 index 000000000000..6b0beb92b313 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -0,0 +1,7 @@ +class C +type Cap = C @retains(*) + +def f(y: Cap, z: Cap) = + def g(): C @retains(y, z) = ??? + val ac: ((x: Cap) => String @retains(x) => String @retains(x)) = ??? + val dc: (({y, z} String) => {y, z} String) = ac(g()) // error diff --git a/tests/neg-custom-args/captures/capt-depfun2.scala b/tests/neg-custom-args/captures/capt-depfun2.scala new file mode 100644 index 000000000000..874d753b048d --- /dev/null +++ b/tests/neg-custom-args/captures/capt-depfun2.scala @@ -0,0 +1,10 @@ +class C +type Cap = C @retains(*) + +def f(y: Cap, z: Cap) = + def g(): C @retains(y, z) = ??? + val ac: ((x: Cap) => Array[String @retains(x)]) = ??? + val dc = ac(g()) // error: Needs explicit type Array[? >: String <: {y, z} String] + // This is a shortcoming of rechecking since the originally inferred + // type is `Array[String]` and the actual type after rechecking + // cannot be expressed as `Array[C String]` for any capture set C \ No newline at end of file diff --git a/tests/neg-custom-args/captures/capt-env.scala b/tests/neg-custom-args/captures/capt-env.scala new file mode 100644 index 000000000000..84b4b57a7930 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-env.scala @@ -0,0 +1,13 @@ +class C +type Cap = {*} C + +class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + +def test(c: Cap) = + def f(x: Cap): Unit = if c == x then () + val p = Pair(f, f) + val g = () => p.fst == p.snd + val gc: () => Boolean = g // error + diff --git a/tests/neg-custom-args/captures/capt-test.scala b/tests/neg-custom-args/captures/capt-test.scala new file mode 100644 index 000000000000..0c536a280f5c --- /dev/null +++ b/tests/neg-custom-args/captures/capt-test.scala @@ -0,0 +1,26 @@ +import language.experimental.erasedDefinitions + +class CT[E <: Exception] +type CanThrow[E <: Exception] = CT[E] @retains(*) +type Top = Any @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R <: Top](op: (CanThrow[E]) => R)(handler: E => R): R = + val x: CanThrow[E] = ??? + try op(x) + catch case ex: E => handler(ex) + +def test: Unit = + val b = handle[Exception, () => Nothing] { // error + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) + } { + (ex: Exception) => ??? + } diff --git a/tests/neg-custom-args/captures/capt-wf-typer.scala b/tests/neg-custom-args/captures/capt-wf-typer.scala new file mode 100644 index 000000000000..5120e2b288d5 --- /dev/null +++ b/tests/neg-custom-args/captures/capt-wf-typer.scala @@ -0,0 +1,10 @@ +class C +type Cap = {*} C + +object foo + +def test(c: Cap, other: String): Unit = + val x7: {c} String = ??? // OK + val x8: String @retains(x7 + x7) = ??? // error + val x9: String @retains(foo) = ??? // error + () \ No newline at end of file diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check new file mode 100644 index 000000000000..ce7c4833bf9c --- /dev/null +++ b/tests/neg-custom-args/captures/capt1.check @@ -0,0 +1,46 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:3:2 ------------------------------------------ +3 | () => if x == null then y else y // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {x} () => ? C + | Required: () => C + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:6:2 ------------------------------------------ +6 | () => if x == null then y else y // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {x} () => ? C + | Required: Matchable + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:13:2 ----------------------------------------- +13 | def f(y: Int) = if x == null then y else y // error + | ^ + | Found: {x} Int => Int + | Required: Matchable +14 | f + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:20:2 ----------------------------------------- +20 | class F(y: Int) extends A: // error + | ^ + | Found: {x} A + | Required: A +21 | def m() = if x == null then y else y +22 | F(22) + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:25:2 ----------------------------------------- +25 | new A: // error + | ^ + | Found: {x} A + | Required: A +26 | def m() = if x == null then y else y + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:31:24 ---------------------------------------- +31 | val z2 = h[() => Cap](() => x)(() => C()) // error + | ^^^^^^^ + | Found: {x} () => ? Cap + | Required: () => Cap + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala new file mode 100644 index 000000000000..4da49c5f4f1e --- /dev/null +++ b/tests/neg-custom-args/captures/capt1.scala @@ -0,0 +1,34 @@ +class C +def f(x: C @retains(*), y: C): () => C = + () => if x == null then y else y // error + +def g(x: C @retains(*), y: C): Matchable = + () => if x == null then y else y // error + +def h1(x: C @retains(*), y: C): Any = + def f() = if x == null then y else y + () => f() // ok + +def h2(x: C @retains(*)): Matchable = + def f(y: Int) = if x == null then y else y // error + f + +class A +type Cap = C @retains(*) + +def h3(x: Cap): A = + class F(y: Int) extends A: // error + def m() = if x == null then y else y + F(22) + +def h4(x: Cap, y: Int): A = + new A: // error + def m() = if x == null then y else y + +def foo() = + val x: C @retains(*) = ??? + def h[X](a: X)(b: X) = a + val z2 = h[() => Cap](() => x)(() => C()) // error + val z3 = h[(() => Cap) @retains(x)](() => x)(() => C()) // ok + val z4 = h[(() => Cap) @retains(x)](() => x)(() => C()) // what was inferred for z3 + diff --git a/tests/neg-custom-args/captures/capt2.scala b/tests/neg-custom-args/captures/capt2.scala new file mode 100644 index 000000000000..1eee53463f6d --- /dev/null +++ b/tests/neg-custom-args/captures/capt2.scala @@ -0,0 +1,9 @@ +//import scala.retains +class C +type Cap = {*} C + +def f1(c: Cap): (() => {c} C) = () => c // error, but would be OK under capture abbreciations for funciton types +def f2(c: Cap): ({c} () => C) = () => c // error + +def h5(x: Cap): () => C = + f1(x) // error diff --git a/tests/neg-custom-args/captures/capt3.scala b/tests/neg-custom-args/captures/capt3.scala new file mode 100644 index 000000000000..80b937276f73 --- /dev/null +++ b/tests/neg-custom-args/captures/capt3.scala @@ -0,0 +1,26 @@ +class C +type Cap = C @retains(*) + +def test1() = + val x: Cap = C() + val y = () => { x; () } + val z = y + z: (() => Unit) // error + +def test2() = + val x: Cap = C() + def y = () => { x; () } + def z = y + z: (() => Unit) // error + +def test3() = + val x: Cap = C() + def y = () => { x; () } + val z = y + z: (() => Unit) // error + +def test4() = + val x: Cap = C() + val y = () => { x; () } + def z = y + z: (() => Unit) // error diff --git a/tests/neg-custom-args/captures/cc1.scala b/tests/neg-custom-args/captures/cc1.scala new file mode 100644 index 000000000000..ebd983c58fe9 --- /dev/null +++ b/tests/neg-custom-args/captures/cc1.scala @@ -0,0 +1,4 @@ +object Test: + + def f[A <: Matchable @retains(*)](x: A): Matchable = x // error + diff --git a/tests/neg-custom-args/captures/classes.scala b/tests/neg-custom-args/captures/classes.scala new file mode 100644 index 000000000000..b87d21913d4e --- /dev/null +++ b/tests/neg-custom-args/captures/classes.scala @@ -0,0 +1,12 @@ +class B +type Cap = {*} B +class C0(n: Cap) // error: class parameter must be a `val`. + +class C(val n: Cap): + def foo(): {n} B = n + +def test(x: Cap, y: Cap) = + val c0 = C(x) + val c1: C = c0 // error + val c2 = if ??? then C(x) else /*identity*/(C(y)) // TODO: uncomment + val c3: {x} C { val n: {x, y} B } = c2 // error diff --git a/tests/neg-custom-args/captures/io.scala b/tests/neg-custom-args/captures/io.scala new file mode 100644 index 000000000000..17c22a2111e4 --- /dev/null +++ b/tests/neg-custom-args/captures/io.scala @@ -0,0 +1,22 @@ +sealed trait IO: + def puts(msg: Any): Unit = println(msg) + +def test1 = + val IO : IO @retains(*) = new IO {} + def foo = {IO; IO.puts("hello") } + val x : () => Unit = () => foo // error: Found: (() => Unit) retains IO; Required: () => Unit + +def test2 = + val IO : IO @retains(*) = new IO {} + def puts(msg: Any, io: IO @retains(*)) = println(msg) + def foo() = puts("hello", IO) + val x : () => Unit = () => foo() // error: Found: (() => Unit) retains IO; Required: () => Unit + +type Capability[T] = T @retains(*) + +def test3 = + val IO : Capability[IO] = new IO {} + def puts(msg: Any, io: Capability[IO]) = println(msg) + def foo() = puts("hello", IO) + val x : () => Unit = () => foo() // error: Found: (() => Unit) retains IO; Required: () => Unit + diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check new file mode 100644 index 000000000000..3a80de9bdf16 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylist.check @@ -0,0 +1,42 @@ +-- [E163] Declaration Error: tests/neg-custom-args/captures/lazylist.scala:22:6 ---------------------------------------- +22 | def tail: {*} LazyList[Nothing] = ??? // error overriding + | ^ + | error overriding method tail in class LazyList of type => lazylists.LazyList[Nothing]; + | method tail of type => {*} lazylists.LazyList[Nothing] has incompatible type + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:35:29 ------------------------------------- +35 | val ref1c: LazyList[Int] = ref1 // error + | ^^^^ + | Found: (ref1 : {cap1} lazylists.LazyCons[Int]{xs: {cap1} () => {*} lazylists.LazyList[Int]}) + | Required: lazylists.LazyList[Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:37:36 ------------------------------------- +37 | val ref2c: {ref1} LazyList[Int] = ref2 // error + | ^^^^ + | Found: (ref2 : {cap2, ref1} lazylists.LazyList[Int]) + | Required: {ref1} lazylists.LazyList[Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:39:36 ------------------------------------- +39 | val ref3c: {cap2} LazyList[Int] = ref3 // error + | ^^^^ + | Found: (ref3 : {cap2, ref1} lazylists.LazyList[Int]) + | Required: {cap2} lazylists.LazyList[Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:41:48 ------------------------------------- +41 | val ref4c: {cap1, ref3, cap3} LazyList[Int] = ref4 // error + | ^^^^ + | Found: (ref4 : {cap3, cap2, ref1, cap1} lazylists.LazyList[Int]) + | Required: {cap1, ref3, cap3} lazylists.LazyList[Int] + +longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/lazylist.scala:17:6 ----------------------------------------------------------- +17 | def tail = xs() // error: cannot have an inferred type + | ^^^^^^^^^^^^^^^ + | Non-local method tail cannot have an inferred result type + | {*} lazylists.LazyList[T] + | with non-empty capture set {*}. + | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/lazylist.scala b/tests/neg-custom-args/captures/lazylist.scala new file mode 100644 index 000000000000..f7be43e8dc27 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylist.scala @@ -0,0 +1,41 @@ +package lazylists + +abstract class LazyList[+T]: + this: ({*} LazyList[T]) => + + def isEmpty: Boolean + def head: T + def tail: LazyList[T] + + def map[U](f: {*} T => U): {f, this} LazyList[U] = + if isEmpty then LazyNil + else LazyCons(f(head), () => tail.map(f)) + +class LazyCons[+T](val x: T, val xs: {*} () => {*} LazyList[T]) extends LazyList[T]: + def isEmpty = false + def head = x + def tail = xs() // error: cannot have an inferred type + +object LazyNil extends LazyList[Nothing]: + def isEmpty = true + def head = ??? + def tail: {*} LazyList[Nothing] = ??? // error overriding + +def map[A, B](xs: {*} LazyList[A], f: {*} A => B): {f, xs} LazyList[B] = + xs.map(f) + +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap, cap3: Cap) = + def f[T](x: LazyList[T]): LazyList[T] = if cap1 == cap1 then x else LazyNil + def g(x: Int) = if cap2 == cap2 then x else 0 + def h(x: Int) = if cap3 == cap3 then x else 0 + val ref1 = LazyCons(1, () => f(LazyNil)) + val ref1c: LazyList[Int] = ref1 // error + val ref2 = map(ref1, g) + val ref2c: {ref1} LazyList[Int] = ref2 // error + val ref3 = ref1.map(g) + val ref3c: {cap2} LazyList[Int] = ref3 // error + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(h) + val ref4c: {cap1, ref3, cap3} LazyList[Int] = ref4 // error diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check new file mode 100644 index 000000000000..2affed020dec --- /dev/null +++ b/tests/neg-custom-args/captures/lazyref.check @@ -0,0 +1,28 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:19:28 -------------------------------------- +19 | val ref1c: LazyRef[Int] = ref1 // error + | ^^^^ + | Found: (ref1 : {cap1} LazyRef[Int]{elem: {cap1} () => Int}) + | Required: LazyRef[Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- +21 | val ref2c: {cap2} LazyRef[Int] = ref2 // error + | ^^^^ + | Found: (ref2 : {cap2, ref1} LazyRef[Int]{elem: {*} () => Int}) + | Required: {cap2} LazyRef[Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- +23 | val ref3c: {ref1} LazyRef[Int] = ref3 // error + | ^^^^ + | Found: (ref3 : {cap2, ref1} LazyRef[Int]{elem: {*} () => Int}) + | Required: {ref1} LazyRef[Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- +25 | val ref4c: {cap1} LazyRef[Int] = ref4 // error + | ^^^^ + | Found: (ref4 : {cap2, cap1} LazyRef[Int]{elem: {*} () => Int}) + | Required: {cap1} LazyRef[Int] + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala new file mode 100644 index 000000000000..1002f9685675 --- /dev/null +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -0,0 +1,25 @@ +class CC +type Cap = {*} CC + +class LazyRef[T](val elem: {*} () => T): + val get = elem + def map[U](f: {*} T => U): {f, this} LazyRef[U] = + new LazyRef(() => f(elem())) + +def map[A, B](ref: {*} LazyRef[A], f: {*} A => B): {f, ref} LazyRef[B] = + new LazyRef(() => f(ref.elem())) + +def mapc[A, B]: (ref: {*} LazyRef[A], f: {*} A => B) => {f, ref} LazyRef[B] = + (ref1, f1) => map[A, B](ref1, f1) + +def test(cap1: Cap, cap2: Cap) = + def f(x: Int) = if cap1 == cap1 then x else 0 + def g(x: Int) = if cap2 == cap2 then x else 0 + val ref1 = LazyRef(() => f(0)) + val ref1c: LazyRef[Int] = ref1 // error + val ref2 = map(ref1, g) + val ref2c: {cap2} LazyRef[Int] = ref2 // error + val ref3 = ref1.map(g) + val ref3c: {ref1} LazyRef[Int] = ref3 // error + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4c: {cap1} LazyRef[Int] = ref4 // error diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check new file mode 100644 index 000000000000..bd95835c6525 --- /dev/null +++ b/tests/neg-custom-args/captures/try.check @@ -0,0 +1,25 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:28:43 ------------------------------------------ +28 | val b = handle[Exception, () => Nothing] { // error + | ^ + | Found: ? (x: CanThrow[Exception]) => {x} () => ? Nothing + | Required: CanThrow[Exception] => () => Nothing +29 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) +30 | } { + +longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/try.scala:22:28 --------------------------------------------------------------- +22 | val a = handle[Exception, CanThrow[Exception]] { // error + | ^^^^^^^^^^^^^^^^^^^ + | type argument is not allowed to capture the universal capability (* : Any) +-- Error: tests/neg-custom-args/captures/try.scala:34:11 --------------------------------------------------------------- +34 | val xx = handle { // error + | ^^^^^^ + | inferred type argument {*} () => Int is not allowed to capture the universal capability (* : Any) + | + | The inferred arguments are: [? Exception, {*} () => Int] +-- Error: tests/neg-custom-args/captures/try.scala:46:13 --------------------------------------------------------------- +46 |val global = handle { // error + | ^^^^^^ + | inferred type argument {*} () => Int is not allowed to capture the universal capability (* : Any) + | + | The inferred arguments are: [? Exception, {*} () => Int] diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala new file mode 100644 index 000000000000..804a16192be0 --- /dev/null +++ b/tests/neg-custom-args/captures/try.scala @@ -0,0 +1,53 @@ +import language.experimental.erasedDefinitions + +class CT[E <: Exception] +type CanThrow[E <: Exception] = CT[E] @retains(*) +type Top = Any @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R <: Top](op: CanThrow[E] => R)(handler: E => R): R = + val x: CanThrow[E] = ??? + try op(x) + catch case ex: E => handler(ex) + +def test = + val a = handle[Exception, CanThrow[Exception]] { // error + (x: CanThrow[Exception]) => x + }{ + (ex: Exception) => ??? + } + + val b = handle[Exception, () => Nothing] { // error + (x: CanThrow[Exception]) => () => raise(new Exception)(using x) + } { + (ex: Exception) => ??? + } + + val xx = handle { // error + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 + } { + (ex: Exception) => () => 22 + } + val yy = xx :: Nil + yy // OK + + +val global = handle { // error + (x: CanThrow[Exception]) => + () => + raise(new Exception)(using x) + 22 +} { + (ex: Exception) => () => 22 +} \ No newline at end of file diff --git a/tests/neg-custom-args/captures/try3.scala b/tests/neg-custom-args/captures/try3.scala new file mode 100644 index 000000000000..4fbb980b9e03 --- /dev/null +++ b/tests/neg-custom-args/captures/try3.scala @@ -0,0 +1,27 @@ +import java.io.IOException + +class CT[E] +type CanThrow[E] = {*} CT[E] +type Top = {*} Any + +def handle[E <: Exception, T <: Top](op: CanThrow[E] ?=> T)(handler: E => T): T = + val x: CanThrow[E] = ??? + try op(using x) + catch case ex: E => handler(ex) + +def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = + throw ex + +@main def Test: Int = + def f(a: Boolean) = + handle { // error + if !a then raise(IOException()) + (b: Boolean) => + if !b then raise(IOException()) + 0 + } { + ex => (b: Boolean) => -1 + } + val g = f(true) + g(false) // would raise an uncaught exception + f(true)(false) // would raise an uncaught exception diff --git a/tests/neg/multiLineOps.scala b/tests/neg/multiLineOps.scala index 8499cc9fe710..08a0a3925fd1 100644 --- a/tests/neg/multiLineOps.scala +++ b/tests/neg/multiLineOps.scala @@ -5,7 +5,7 @@ val x = 1 val b1 = { 22 * 22 // ok - */*one more*/22 // error: end of statement expected // error: not found: * + */*one more*/22 // error: end of statement expected } val b2: Boolean = { diff --git a/tests/neg/polymorphic-functions1.check b/tests/neg/polymorphic-functions1.check new file mode 100644 index 000000000000..86492e96dab5 --- /dev/null +++ b/tests/neg/polymorphic-functions1.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/polymorphic-functions1.scala:1:53 --------------------------------------------- +1 |val f: [T] => (x: T) => x.type = [T] => (x: Int) => x // error + | ^ + | Found: [T] => (x: Int) => Int + | Required: [T] => (x: T) => x.type + +longer explanation available when compiling with `-explain` diff --git a/tests/neg/polymorphic-functions1.scala b/tests/neg/polymorphic-functions1.scala new file mode 100644 index 000000000000..de887f3b8c50 --- /dev/null +++ b/tests/neg/polymorphic-functions1.scala @@ -0,0 +1 @@ +val f: [T] => (x: T) => x.type = [T] => (x: Int) => x // error diff --git a/tests/pos-custom-args/captures/bounded.scala b/tests/pos-custom-args/captures/bounded.scala new file mode 100644 index 000000000000..fad0b50c2137 --- /dev/null +++ b/tests/pos-custom-args/captures/bounded.scala @@ -0,0 +1,14 @@ +class CC +type Cap = {*} CC + +def test(c: Cap) = + class B[X <: {c} Object](x: X): + def elem = x + def lateElem = () => x + + def f(x: Int): Int = if c == c then x else 0 + val b = new B(f) + val r1 = b.elem + val r1c: {c} Int => Int = r1 + val r2 = b.lateElem + val r2c: {c} () => {c} Int => Int = r2 \ No newline at end of file diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala new file mode 100644 index 000000000000..ed8c648526d1 --- /dev/null +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -0,0 +1,38 @@ +infix type ==> [A, B] = {*} (A => B) + +type Cell[+T] = [K] => (T ==> K) => K + +def cell[T](x: T): Cell[T] = + [K] => (k: T ==> K) => k(x) + +def get[T](c: Cell[T]): T = c[T](identity) + +def map[A, B](c: Cell[A])(f: A ==> B): Cell[B] + = c[Cell[B]]((x: A) => cell(f(x))) + +def pureMap[A, B](c: Cell[A])(f: A => B): Cell[B] + = c[Cell[B]]((x: A) => cell(f(x))) + +def lazyMap[A, B](c: Cell[A])(f: A ==> B): {f} () => Cell[B] + = () => c[Cell[B]]((x: A) => cell(f(x))) + +trait IO: + def print(s: String): Unit + +def test(io: {*} IO) = + + val loggedOne: {io} () => Int = () => { io.print("1"); 1 } + + val c: Cell[{io} () => Int] + = cell[{io} () => Int](loggedOne) + + val g = (f: {io} () => Int) => + val x = f(); io.print(" + ") + val y = f(); io.print(s" = ${x + y}") + + val r = lazyMap[{io} () => Int, Unit](c)(f => g(f)) + val r2 = lazyMap[{io} () => Int, Unit](c)(g) + val r3 = lazyMap(c)(g) + val _ = r() + val _ = r2() + val _ = r3() diff --git a/tests/pos-custom-args/captures/boxmap.scala b/tests/pos-custom-args/captures/boxmap.scala new file mode 100644 index 000000000000..a0dcade2b179 --- /dev/null +++ b/tests/pos-custom-args/captures/boxmap.scala @@ -0,0 +1,20 @@ +type Top = Any @retains(*) + +infix type ==> [A, B] = (A => B) @retains(*) + +type Box[+T <: Top] = ([K <: Top] => (T ==> K) => K) + +def box[T <: Top](x: T): Box[T] = + [K <: Top] => (k: T ==> K) => k(x) + +def map[A <: Top, B <: Top](b: Box[A])(f: A ==> B): Box[B] = + b[Box[B]]((x: A) => box(f(x))) + +def lazymap[A <: Top, B <: Top](b: Box[A])(f: A ==> B): (() => Box[B]) @retains(f) = + () => b[Box[B]]((x: A) => box(f(x))) + +def test[A <: Top, B <: Top] = + def lazymap[A <: Top, B <: Top](b: Box[A])(f: A ==> B) = + () => b[Box[B]]((x: A) => box(f(x))) + val x: (b: Box[A]) => (f: A ==> B) => (() => Box[B]) @retains(f) = lazymap[A, B] + () diff --git a/tests/pos-custom-args/captures/byname.scala b/tests/pos-custom-args/captures/byname.scala new file mode 100644 index 000000000000..917154079b36 --- /dev/null +++ b/tests/pos-custom-args/captures/byname.scala @@ -0,0 +1,10 @@ +class CC +type Cap = {*} CC + +class I + +def test(cap1: Cap, cap2: Cap): {cap1} I = + def f() = if cap1 == cap1 then I() else I() + def h(x: => {cap1} I) = x + h(f()) + diff --git a/tests/pos-custom-args/captures/capt-depfun.scala b/tests/pos-custom-args/captures/capt-depfun.scala new file mode 100644 index 000000000000..6b99eff32692 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-depfun.scala @@ -0,0 +1,18 @@ +class C +type Cap = C @retains(*) + +type T = (x: Cap) => String @retains(x) + +val aa: ((x: Cap) => String @retains(x)) = (x: Cap) => "" + +def f(y: Cap, z: Cap): String @retains(*) = + val a: ((x: Cap) => String @retains(x)) = (x: Cap) => "" + val b = a(y) + val c: String @retains(y) = b + def g(): C @retains(y, z) = ??? + val d = a(g()) + + val ac: ((x: Cap) => String @retains(x) => String @retains(x)) = ??? + val bc: (({y} String) => {y} String) = ac(y) + val dc: (String => {y, z} String) = ac(g()) + c diff --git a/tests/pos-custom-args/captures/capt-depfun2.scala b/tests/pos-custom-args/captures/capt-depfun2.scala new file mode 100644 index 000000000000..17f98b4a1554 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-depfun2.scala @@ -0,0 +1,8 @@ +class C +type Cap = C @retains(*) + +def f(y: Cap, z: Cap) = + def g(): C @retains(y, z) = ??? + val ac: ((x: Cap) => Array[String @retains(x)]) = ??? + val dc: Array[? >: String <: {y, z} String] = ac(g()) // needs to be inferred + val ec = ac(y) diff --git a/tests/pos-custom-args/captures/capt-test.scala b/tests/pos-custom-args/captures/capt-test.scala new file mode 100644 index 000000000000..f40bd2ff1746 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-test.scala @@ -0,0 +1,35 @@ +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: {*} T => U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +def map[A, B](f: {*} A => B)(xs: LIST[A]): LIST[B] = + xs.map(f) + +class C +type Cap = {*} C + +def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val y = f + val ys = CONS(y, NIL) + val zs = + val z = g + CONS(z, ys) + val zsc: LIST[{d, y} Cap => Unit] = zs + + val a4 = zs.map(identity) + val a4c: LIST[{d, y} Cap => Unit] = a4 diff --git a/tests/pos-custom-args/captures/capt0.scala b/tests/pos-custom-args/captures/capt0.scala new file mode 100644 index 000000000000..c8ff8a102856 --- /dev/null +++ b/tests/pos-custom-args/captures/capt0.scala @@ -0,0 +1,7 @@ +object Test: + + def test() = + val x: {*} Any = "abc" + val y: Object @scala.retains(x) = ??? + val z: Object @scala.retains(x, *) = y: Object @scala.retains(x) + diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala new file mode 100644 index 000000000000..14c0855544d4 --- /dev/null +++ b/tests/pos-custom-args/captures/capt1.scala @@ -0,0 +1,27 @@ +class C +type Cap = {*} C +def f1(c: Cap): {c} () => c.type = () => c // ok + +def f2: Int = + val g: {*} Boolean => Int = ??? + val x = g(true) + x + +def f3: Int = + def g: {*} Boolean => Int = ??? + def h = g + val x = g.apply(true) + x + +def foo() = + val x: {*} C = ??? + val y: {x} C = x + val x2: {x} () => C = ??? + val y2: {x} () => {x} C = x2 + + val z1: {*} () => Cap = f1(x) + def h[X](a: X)(b: X) = a + + val z2 = + if x == null then () => x else () => C() + x \ No newline at end of file diff --git a/tests/pos-custom-args/captures/capt2.scala b/tests/pos-custom-args/captures/capt2.scala new file mode 100644 index 000000000000..e3d4cd67b30c --- /dev/null +++ b/tests/pos-custom-args/captures/capt2.scala @@ -0,0 +1,20 @@ +import scala.retains +class C +type Cap = C @retains(*) + +def test1() = + val y: {*} String = "" + def x: Object @retains(y) = y + +def test2() = + val x: Cap = C() + val y = () => { x; () } + def z: (() => Unit) @retains(x) = y + z: (() => Unit) @retains(x) + def z2: (() => Unit) @retains(y) = y + z2: (() => Unit) @retains(y) + val p: {*} () => String = () => "abc" + val q: {p} C = ??? + p: ({p} () => String) + + diff --git a/tests/pos-custom-args/captures/cc-expand.scala b/tests/pos-custom-args/captures/cc-expand.scala new file mode 100644 index 000000000000..eedc95554b17 --- /dev/null +++ b/tests/pos-custom-args/captures/cc-expand.scala @@ -0,0 +1,21 @@ +object Test: + + class A + class B + class C + class CTC + type CT = CTC @retains(*) + + def test(ct: CT, dt: CT) = + + def x0: A => {ct} B = ??? + + def x1: A => B @retains(ct) = ??? + def x2: A => B => C @retains(ct) = ??? + def x3: A => () => B => C @retains(ct) = ??? + + def x4: (x: A @retains(ct)) => B => C = ??? + + def x5: A => (x: B @retains(ct)) => () => C @retains(dt) = ??? + def x6: A => (x: B @retains(ct)) => (() => C @retains(dt)) @retains(x, dt) = ??? + def x7: A => (x: B @retains(ct)) => (() => C @retains(dt)) @retains(x) = ??? \ No newline at end of file diff --git a/tests/pos-custom-args/captures/classes.scala b/tests/pos-custom-args/captures/classes.scala new file mode 100644 index 000000000000..f3d6e44b27ca --- /dev/null +++ b/tests/pos-custom-args/captures/classes.scala @@ -0,0 +1,34 @@ +class B +type Cap = {*} B +class C(val n: Cap): + this: ({n} C) => + def foo(): {n} B = n + + +def test(x: Cap, y: Cap, z: Cap) = + val c0 = C(x) + val c1: {x} C {val n: {x} B} = c0 + val d = c1.foo() + d: ({x} B) + + val c2 = if ??? then C(x) else C(y) + val c2a = identity(c2) + val c3: {x, y} C { val n: {x, y} B } = c2 + val d1 = c3.foo() + d1: B @retains(x, y) + + class Local: + + def this(a: Cap) = + this() + if a == z then println("?") + + val f = y + def foo = x + end Local + + val l = Local() + val l1: {x, y} Local = l + val l2 = Local(x) + val l3: {x, y, z} Local = l2 + diff --git a/tests/pos-custom-args/captures/iterators.scala b/tests/pos-custom-args/captures/iterators.scala new file mode 100644 index 000000000000..dd1067bcdc72 --- /dev/null +++ b/tests/pos-custom-args/captures/iterators.scala @@ -0,0 +1,23 @@ +package cctest + +abstract class Iterator[T]: + thisIterator => + + def hasNext: Boolean + def next: T + def map(f: {*} T => T): {f} Iterator[T] = new Iterator: + def hasNext = thisIterator.hasNext + def next = f(thisIterator.next) +end Iterator + +class C +type Cap = {*} C + +def test(c: Cap, d: Cap, e: Cap) = + val it = new Iterator[Int]: + private var ctr = 0 + def hasNext = ctr < 10 + def next = { ctr += 1; ctr } + + def f(x: Int): Int = if c == d then x else 10 + val it2 = it.map(f) diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala new file mode 100644 index 000000000000..39748b00506b --- /dev/null +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -0,0 +1,25 @@ +class CC +type Cap = {*} CC + +class LazyRef[T](val elem: {*} () => T): + val get = elem + def map[U](f: {*} T => U): {f, this} LazyRef[U] = + new LazyRef(() => f(elem())) + +def map[A, B](ref: {*} LazyRef[A], f: {*} A => B): {f, ref} LazyRef[B] = + new LazyRef(() => f(ref.elem())) + +def mapc[A, B]: (ref: {*} LazyRef[A], f: {*} A => B) => {f, ref} LazyRef[B] = + (ref1, f1) => map[A, B](ref1, f1) + +def test(cap1: Cap, cap2: Cap) = + def f(x: Int) = if cap1 == cap1 then x else 0 + def g(x: Int) = if cap2 == cap2 then x else 0 + val ref1 = LazyRef(() => f(0)) + val ref1c: {cap1} LazyRef[Int] = ref1 + val ref2 = map(ref1, g) + val ref2c: {cap2, ref1} LazyRef[Int] = ref2 + val ref3 = ref1.map(g) + val ref3c: {cap2, ref1} LazyRef[Int] = ref3 + val ref4 = (if cap1 == cap2 then ref1 else ref2).map(g) + val ref4c: {cap1, cap2} LazyRef[Int] = ref4 diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala new file mode 100644 index 000000000000..74bc8bd2b099 --- /dev/null +++ b/tests/pos-custom-args/captures/list-encoding.scala @@ -0,0 +1,23 @@ +package listEncoding + +class Cap + +type Op[T, C] = + {*} (v: T) => {*} (s: C) => C + +type List[T] = + [C] => (op: Op[T, C]) => {op} (s: C) => C + +def nil[T]: List[T] = + [C] => (op: Op[T, C]) => (s: C) => s + +def cons[T](hd: T, tl: List[T]): List[T] = + [C] => (op: Op[T, C]) => (s: C) => op(hd)(tl(op)(s)) + +def foo(c: {*} Cap) = + def f(x: String @retains(c), y: String @retains(c)) = + cons(x, cons(y, nil)) + def g(x: String @retains(c), y: Any) = + cons(x, cons(y, nil)) + def h(x: String, y: Any @retains(c)) = + cons(x, cons(y, nil)) diff --git a/tests/pos-custom-args/captures/lists.scala b/tests/pos-custom-args/captures/lists.scala new file mode 100644 index 000000000000..139f885ec87a --- /dev/null +++ b/tests/pos-custom-args/captures/lists.scala @@ -0,0 +1,91 @@ +abstract class LIST[+T]: + def isEmpty: Boolean + def head: T + def tail: LIST[T] + def map[U](f: {*} T => U): LIST[U] = + if isEmpty then NIL + else CONS(f(head), tail.map(f)) + +class CONS[+T](x: T, xs: LIST[T]) extends LIST[T]: + def isEmpty = false + def head = x + def tail = xs +object NIL extends LIST[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +def map[A, B](f: {*} A => B)(xs: LIST[A]): LIST[B] = + xs.map(f) + +class C +type Cap = {*} C + +def test(c: Cap, d: Cap, e: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val y = f + val ys = CONS(y, NIL) + val zs = + val z = g + CONS(z, ys) + val zsc: LIST[{d, y} Cap => Unit] = zs + val z1 = zs.head + val z1c: {y, d} Cap => Unit = z1 + val ys1 = zs.tail + val y1 = ys1.head + + + def m1[A, B] = + (f: {*} A => B) => (xs: LIST[A]) => xs.map(f) + + def m1c: (f: {*} String => Int) => {f} LIST[String] => LIST[Int] = m1[String, Int] + + def m2 = [A, B] => + (f: {*} A => B) => (xs: LIST[A]) => xs.map(f) + + def m2c: [A, B] => (f: {*} A => B) => {f} LIST[A] => LIST[B] = m2 + + def eff[A](x: A) = if x == e then x else x + + val eff2 = [A] => (x: A) => if x == e then x else x + + val a0 = identity[{d, y} Cap => Unit] + val a0c: ({d, y} Cap => Unit) => {d, y} Cap => Unit = a0 + val a1 = zs.map[{d, y} Cap => Unit](a0) + val a1c: LIST[{d, y} Cap => Unit] = a1 + val a2 = zs.map[{d, y} Cap => Unit](identity[{d, y} Cap => Unit]) + val a2c: LIST[{d, y} Cap => Unit] = a2 + val a3 = zs.map(identity[{d, y} Cap => Unit]) + val a3c: LIST[{d, y} Cap => Unit] = a3 + val a4 = zs.map(identity) + val a4c: LIST[{d, c} Cap => Unit] = a4 + val a5 = map[{d, y} Cap => Unit, {d, y} Cap => Unit](identity)(zs) + val a5c: LIST[{d, c} Cap => Unit] = a5 + val a6 = m1[{d, y} Cap => Unit, {d, y} Cap => Unit](identity)(zs) + val a6c: LIST[{d, c} Cap => Unit] = a6 + + val b0 = eff[{d, y} Cap => Unit] + val b0c: {e} ({d, y} Cap => Unit) => {d, y} Cap => Unit = b0 + val b1 = zs.map[{d, y} Cap => Unit](a0) + val b1c: {e} LIST[{d, y} Cap => Unit] = b1 + val b2 = zs.map[{d, y} Cap => Unit](eff[{d, y} Cap => Unit]) + val b2c: {e} LIST[{d, y} Cap => Unit] = b2 + val b3 = zs.map(eff[{d, y} Cap => Unit]) + val b3c: {e} LIST[{d, y} Cap => Unit] = b3 + val b4 = zs.map(eff) + val b4c: {e} LIST[{d, c} Cap => Unit] = b4 + val b5 = map[{d, y} Cap => Unit, {d, y} Cap => Unit](eff)(zs) + val b5c: {e} LIST[{d, c} Cap => Unit] = b5 + val b6 = m1[{d, y} Cap => Unit, {d, y} Cap => Unit](eff)(zs) + val b6c: {e} LIST[{d, c} Cap => Unit] = b6 + + val c0 = eff2[{d, y} Cap => Unit] + val c0c: {e} ({d, y} Cap => Unit) => {d, y} Cap => Unit = c0 + val c1 = zs.map[{d, y} Cap => Unit](a0) + val c1c: {e} LIST[{d, y} Cap => Unit] = c1 + val c2 = zs.map[{d, y} Cap => Unit](eff2[{d, y} Cap => Unit]) + val c2c: {e} LIST[{d, y} Cap => Unit] = c2 + val c3 = zs.map(eff2[{d, y} Cap => Unit]) + val c3c: {e} LIST[{d, y} Cap => Unit] = c3 + diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala new file mode 100644 index 000000000000..4f23a086a075 --- /dev/null +++ b/tests/pos-custom-args/captures/pairs.scala @@ -0,0 +1,33 @@ + +class C +type Cap = {*} C + +object Generic: + + class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + + def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val p = Pair(f, g) + val x1 = p.fst + val x1c: {c} Cap => Unit = x1 + val y1 = p.snd + val y1c: {d} Cap => Unit = y1 + +object Monomorphic: + + class Pair(val x: {*} Cap => Unit, val y: {*} Cap => Unit): + def fst = x + def snd = y + + def test(c: Cap, d: Cap) = + def f(x: Cap): Unit = if c == x then () + def g(x: Cap): Unit = if d == x then () + val p = Pair(f, g) + val x1 = p.fst + val x1c: {c} Cap => Unit = x1 + val y1 = p.snd + val y1c: {d} Cap => Unit = y1 diff --git a/tests/pos-custom-args/captures/try.scala b/tests/pos-custom-args/captures/try.scala new file mode 100644 index 000000000000..a50eeabfb3a3 --- /dev/null +++ b/tests/pos-custom-args/captures/try.scala @@ -0,0 +1,26 @@ +import language.experimental.erasedDefinitions + +class CT[E <: Exception] +type CanThrow[E <: Exception] = CT[E] @retains(*) + +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R + +class Fail extends Exception + +def raise[E <: Exception](e: E): Nothing throws E = throw e + +def foo(x: Boolean): Int throws Fail = + if x then 1 else raise(Fail()) + +def handle[E <: Exception, R](op: (erased CanThrow[E]) => R)(handler: E => R): R = + erased val x: CanThrow[E] = ??? + try op(x) + catch case ex: E => handler(ex) + +val _ = handle { (erased x) => + if true then + raise(new Exception)(using x) + 22 + else + 11 + } \ No newline at end of file diff --git a/tests/pos-custom-args/captures/try3.scala b/tests/pos-custom-args/captures/try3.scala new file mode 100644 index 000000000000..074517d8a9e5 --- /dev/null +++ b/tests/pos-custom-args/captures/try3.scala @@ -0,0 +1,51 @@ +import language.experimental.erasedDefinitions +import annotation.ability +import java.io.IOException + +class CT[-E] // variance is needed for correct rechecking inference +type CanThrow[E] = {*} CT[E] + +def handle[E <: Exception, T](op: CanThrow[E] ?=> T)(handler: E => T): T = + val x: CanThrow[E] = ??? + try op(using x) + catch case ex: E => handler(ex) + +def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = + throw ex + +def test1: Int = + def f(a: Boolean): Boolean => CanThrow[IOException] ?=> Int = + handle { + if !a then raise(IOException()) + (b: Boolean) => (_: CanThrow[IOException]) ?=> + if !b then raise(IOException()) + 0 + } { + ex => (b: Boolean) => (_: CanThrow[IOException]) ?=> -1 + } + handle { + val g = f(true) + g(false) // can raise an exception + f(true)(false) // can raise an exception + } { + ex => -1 + } +/* +def test2: Int = + def f(a: Boolean): Boolean => CanThrow[IOException] ?=> Int = + handle { // error + if !a then raise(IOException()) + (b: Boolean) => + if !b then raise(IOException()) + 0 + } { + ex => (b: Boolean) => -1 + } + handle { + val g = f(true) + g(false) // would raise an uncaught exception + f(true)(false) // would raise an uncaught exception + } { + ex => -1 + } +*/ \ No newline at end of file From b3474f2932ebca414f3d891d9601456f15eea306 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 2 Oct 2021 12:19:30 +0200 Subject: [PATCH 04/99] Include capture sets of methods in enclosing class --- .../dotty/tools/dotc/typer/CheckCaptures.scala | 7 ++++++- tests/neg-custom-args/captures/nestedclass.check | 7 +++++++ tests/neg-custom-args/captures/nestedclass.scala | 15 +++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/neg-custom-args/captures/nestedclass.check create mode 100644 tests/neg-custom-args/captures/nestedclass.scala diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 1415016fea26..69f184bd79ef 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -263,7 +263,12 @@ class CheckCaptures extends Recheck: case ref: TermRef => ref.symbol.enclosure != ownEnclosure case _ => true } - checkSubset(targetSet, curEnv.captured, pos) + def includeIn(env: Env) = + capt.println(i"Include call capture $targetSet in ${env.owner}") + checkSubset(targetSet, env.captured, pos) + includeIn(curEnv) + if curEnv.owner.isTerm && curEnv.outer.owner.isClass then + includeIn(curEnv.outer) def includeBoxedCaptures(tp: Type, pos: SrcPos)(using Context): Unit = if curEnv.isOpen then diff --git a/tests/neg-custom-args/captures/nestedclass.check b/tests/neg-custom-args/captures/nestedclass.check new file mode 100644 index 000000000000..d3912d417a4c --- /dev/null +++ b/tests/neg-custom-args/captures/nestedclass.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/nestedclass.scala:15:15 ---------------------------------- +15 | val xsc: C = xs // error + | ^^ + | Found: (xs : {cap1} C) + | Required: C + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/nestedclass.scala b/tests/neg-custom-args/captures/nestedclass.scala new file mode 100644 index 000000000000..38adf7998868 --- /dev/null +++ b/tests/neg-custom-args/captures/nestedclass.scala @@ -0,0 +1,15 @@ +class CC +type Cap = {*} CC + +abstract class C: + def head: String + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + def g(x: String): String = if cap2 == cap2 then "" else "a" + + val xs = + class Cimpl extends C: + def head = f("") + new Cimpl + val xsc: C = xs // error From e12648b7e3268ee1244b01f31075fbb5179ced8f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 19 Nov 2021 13:39:21 +0100 Subject: [PATCH 05/99] Update iterators example --- tests/pos-custom-args/captures/iterators.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/pos-custom-args/captures/iterators.scala b/tests/pos-custom-args/captures/iterators.scala index dd1067bcdc72..1ac1bd96f6d7 100644 --- a/tests/pos-custom-args/captures/iterators.scala +++ b/tests/pos-custom-args/captures/iterators.scala @@ -1,11 +1,11 @@ package cctest abstract class Iterator[T]: - thisIterator => + thisIterator: ({*} Iterator[T]) => def hasNext: Boolean def next: T - def map(f: {*} T => T): {f} Iterator[T] = new Iterator: + def map(f: {*} T => T): {f, this} Iterator[T] = new Iterator: def hasNext = thisIterator.hasNext def next = f(thisIterator.next) end Iterator @@ -13,6 +13,10 @@ end Iterator class C type Cap = {*} C +def map[T, U](it: {*} Iterator[T], f: {*} T => U): {it, f} Iterator[U] = new Iterator: + def hasNext = it.hasNext + def next = f(it.next) + def test(c: Cap, d: Cap, e: Cap) = val it = new Iterator[Int]: private var ctr = 0 @@ -21,3 +25,4 @@ def test(c: Cap, d: Cap, e: Cap) = def f(x: Int): Int = if c == d then x else 10 val it2 = it.map(f) + val it3 = map(it, f) \ No newline at end of file From 8f5ef6653e9e2f7e88df7447026efe1a91eebed1 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 5 Oct 2021 10:43:47 +0200 Subject: [PATCH 06/99] Add capture checks for mutable variables - Mutable variables have boxed types, so that we do not need to track them when computing capture sets of classes. - Mutable variable types cannot capture `*` in order to prevent scope extrusion. --- .../dotty/tools/dotc/transform/Recheck.scala | 15 ++++++-- .../tools/dotc/typer/CheckCaptures.scala | 33 ++++++++++------- tests/neg-custom-args/captures/vars.check | 17 +++++++++ tests/neg-custom-args/captures/vars.scala | 37 +++++++++++++++++++ tests/pos-custom-args/captures/vars.scala | 18 +++++++++ 5 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 tests/neg-custom-args/captures/vars.check create mode 100644 tests/neg-custom-args/captures/vars.scala create mode 100644 tests/pos-custom-args/captures/vars.scala diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index a61b736a9cc1..55a57ede2e0a 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -76,7 +76,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: val symd = sym.denot symd.validFor.firstPhaseId == thisPhase.id && (sym.originDenotation ne symd) - def transformType(tp: Type, inferred: Boolean)(using Context): Type = tp + def transformType(tp: Type, inferred: Boolean, boxed: Boolean = false)(using Context): Type = tp object transformTypes extends TreeTraverser: @@ -110,12 +110,19 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: mapOver(t) end SubstParams + private def transformTT(tree: TypeTree, boxed: Boolean)(using Context) = + transformType(tree.tpe, tree.isInstanceOf[InferredTypeTree], boxed).rememberFor(tree) + def traverse(tree: Tree)(using Context) = - traverseChildren(tree) tree match - + case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) => + transformTT(tpt, boxed = true) + traverse(tree.rhs) + case _ => + traverseChildren(tree) + tree match case tree: TypeTree => - transformType(tree.tpe, tree.isInstanceOf[InferredTypeTree]).rememberFor(tree) + transformTT(tree, boxed = false) case tree: ValOrDefDef => val sym = tree.symbol diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 69f184bd79ef..c550b514e3ff 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -115,7 +115,7 @@ class CheckCaptures extends Recheck: class CaptureChecker(ictx: Context) extends Rechecker(ictx): import ast.tpd.* - override def transformType(tp: Type, inferred: Boolean)(using Context): Type = + override def transformType(tp: Type, inferred: Boolean, boxed: Boolean)(using Context): Type = def addInnerVars(tp: Type): Type = tp match case tp @ AppliedType(tycon, args) => @@ -191,15 +191,15 @@ class CheckCaptures extends Recheck: apply(parent) case _ => mapOver(t) - addVars(addFunctionRefinements(cleanup(tp))) + addVars(addFunctionRefinements(cleanup(tp)), boxed) .showing(i"reinfer $tp --> $result", capt) else - val addBoxes = new TypeTraverser: - def setBoxed(t: Type) = t match - case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => - annot.tree.setBoxedCapturing() - case _ => + def setBoxed(t: Type) = t match + case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => + annot.tree.setBoxedCapturing() + case _ => + val addBoxes = new TypeTraverser: def traverse(t: Type) = t match case AppliedType(tycon, args) if !defn.isNonRefinedFunction(t) => @@ -208,8 +208,8 @@ class CheckCaptures extends Recheck: setBoxed(lo); setBoxed(hi) case _ => traverseChildren(t) - end addBoxes + if boxed then setBoxed(tp) addBoxes.traverse(tp) tp end transformType @@ -417,12 +417,15 @@ class CheckCaptures extends Recheck: val what = if ref.isRootCapability then "universal" else "global" if isGlobal then val notAllowed = i" is not allowed to capture the $what capability $ref" - def msg = tree match - case tree: InferredTypeTree => - i"""inferred type argument ${knownType(tree)}$notAllowed - | - |The inferred arguments are: [${allArgs.map(knownType)}%, %]""" - case _ => s"type argument$notAllowed" + def msg = + if allArgs.isEmpty then + i"type of mutable variable ${knownType(tree)}$notAllowed" + else tree match + case tree: InferredTypeTree => + i"""inferred type argument ${knownType(tree)}$notAllowed + | + |The inferred arguments are: [${allArgs.map(knownType)}%, %]""" + case _ => s"type argument$notAllowed" report.error(msg, tree.srcPos) object PostRefinerCheck extends TreeTraverser: @@ -463,6 +466,8 @@ class CheckCaptures extends Recheck: |The type needs to be declared explicitly.""", t.srcPos) case _ => inferred.foreachPart(checkPure, StopAt.Static) + case t: ValDef if t.symbol.is(Mutable) => + checkNotGlobal(t.tpt) case _ => traverseChildren(tree) diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check new file mode 100644 index 000000000000..ceba6f5fb422 --- /dev/null +++ b/tests/neg-custom-args/captures/vars.check @@ -0,0 +1,17 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:11:24 ----------------------------------------- +11 | val z2c: () => Unit = z2 // error + | ^^ + | Found: (z2 : {x, cap1} () => Unit) + | Required: () => Unit + +longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/vars.scala:13:10 -------------------------------------------------------------- +13 | var a: {*} String => String = f // error + | ^^^^^^^^^^^^^^^^^^^ + | type of mutable variable box {*} String => String is not allowed to capture the universal capability (* : Any) +-- Error: tests/neg-custom-args/captures/vars.scala:27:2 --------------------------------------------------------------- +27 | local { cap3 => // error + | ^^^^^ + |inferred type argument {*} (x$0: ? String) => ? String is not allowed to capture the universal capability (* : Any) + | + |The inferred arguments are: [{*} (x$0: ? String) => ? String] diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala new file mode 100644 index 000000000000..8c80eec75783 --- /dev/null +++ b/tests/neg-custom-args/captures/vars.scala @@ -0,0 +1,37 @@ +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + var x = f + val y = x + val z = () => if x("") == "" then "a" else "b" + val zc: {cap1} () => String = z + val z2 = () => { x = identity } + val z2c: () => Unit = z2 // error + + var a: {*} String => String = f // error + + def scope = + val cap3: Cap = CC() + def g(x: String): String = if cap3 == cap3 then "" else "a" + a = g + val gc = g + g + + val s = scope + val sc: {*} String => String = scope + + def local[T](op: Cap => T): T = op(CC()) + + local { cap3 => // error + def g(x: String): String = if cap3 == cap3 then "" else "a" + g + } + + class Ref: + var elem: {cap1} String => String = null + + val r = Ref() + r.elem = f + val fc = r.elem diff --git a/tests/pos-custom-args/captures/vars.scala b/tests/pos-custom-args/captures/vars.scala new file mode 100644 index 000000000000..aca56c55f386 --- /dev/null +++ b/tests/pos-custom-args/captures/vars.scala @@ -0,0 +1,18 @@ +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + var x = f + val y = x + val z = () => if x("") == "" then "a" else "b" + val zc: {cap1} () => String = z + val z2 = () => { x = identity } + val z2c: {cap1} () => Unit = z2 + + class Ref: + var elem: {cap1} String => String = null + + val r = Ref() + r.elem = f + val fc: {cap1} String => String = r.elem From cdaee68f63a0753a282a30b06fc5e8f5d8f435c9 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 5 Oct 2021 12:23:41 +0200 Subject: [PATCH 07/99] Implement deep check for variables Scope extrusion can also happen for nested types, so we need to prevent {*} capturesets anywhere in the type of a mutable variable. --- .../tools/dotc/typer/CheckCaptures.scala | 54 ++++++++++++------- tests/neg-custom-args/captures/vars.check | 8 ++- tests/neg-custom-args/captures/vars.scala | 2 + 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index c550b514e3ff..f7a14f259f24 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -403,30 +403,46 @@ class CheckCaptures extends Recheck: show(unit.tpdTree) // this dows not print tree, but makes its variables visible for dependency printing } + def checkNotGlobal(tree: Tree, tp: Type, allArgs: Tree*)(using Context): Unit = + for ref <-tp.captureSet.elems do + val isGlobal = ref match + case ref: TermRef => + ref.isRootCapability || ref.prefix != NoPrefix && ref.symbol.hasAnnotation(defn.AbilityAnnot) + case _ => false + if isGlobal then + val what = if ref.isRootCapability then "universal" else "global" + val notAllowed = i" is not allowed to capture the $what capability $ref" + def msg = + if allArgs.isEmpty then + i"type of mutable variable ${knownType(tree)}$notAllowed" + else tree match + case tree: InferredTypeTree => + i"""inferred type argument ${knownType(tree)}$notAllowed + | + |The inferred arguments are: [${allArgs.map(knownType)}%, %]""" + case _ => s"type argument$notAllowed" + report.error(msg, tree.srcPos) + def checkNotGlobal(tree: Tree, allArgs: Tree*)(using Context): Unit = if disallowGlobal then tree match case LambdaTypeTree(_, restpt) => checkNotGlobal(restpt, allArgs*) case _ => - for ref <- knownType(tree).captureSet.elems do - val isGlobal = ref match - case ref: TermRef => - ref.isRootCapability || ref.prefix != NoPrefix && ref.symbol.hasAnnotation(defn.AbilityAnnot) - case _ => false - val what = if ref.isRootCapability then "universal" else "global" - if isGlobal then - val notAllowed = i" is not allowed to capture the $what capability $ref" - def msg = - if allArgs.isEmpty then - i"type of mutable variable ${knownType(tree)}$notAllowed" - else tree match - case tree: InferredTypeTree => - i"""inferred type argument ${knownType(tree)}$notAllowed - | - |The inferred arguments are: [${allArgs.map(knownType)}%, %]""" - case _ => s"type argument$notAllowed" - report.error(msg, tree.srcPos) + checkNotGlobal(tree, knownType(tree), allArgs*) + + def checkNotGlobalDeep(tree: Tree)(using Context): Unit = + val checker = new TypeTraverser: + def traverse(tp: Type): Unit = tp match + case tp: TypeRef => + tp.info match + case TypeBounds(_, hi) => traverse(hi) + case _ => + case tp: TermRef => + case _ => + checkNotGlobal(tree, tp) + traverseChildren(tp) + checker.traverse(knownType(tree)) object PostRefinerCheck extends TreeTraverser: def traverse(tree: Tree)(using Context) = @@ -467,7 +483,7 @@ class CheckCaptures extends Recheck: case _ => inferred.foreachPart(checkPure, StopAt.Static) case t: ValDef if t.symbol.is(Mutable) => - checkNotGlobal(t.tpt) + checkNotGlobalDeep(t.tpt) case _ => traverseChildren(tree) diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index ceba6f5fb422..4eab5b6b2b3a 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -9,8 +9,12 @@ longer explanation available when compiling with `-explain` 13 | var a: {*} String => String = f // error | ^^^^^^^^^^^^^^^^^^^ | type of mutable variable box {*} String => String is not allowed to capture the universal capability (* : Any) --- Error: tests/neg-custom-args/captures/vars.scala:27:2 --------------------------------------------------------------- -27 | local { cap3 => // error +-- Error: tests/neg-custom-args/captures/vars.scala:14:9 --------------------------------------------------------------- +14 | var b: List[{*} String => String] = Nil // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + |type of mutable variable List[box {*} String => String] is not allowed to capture the universal capability (* : Any) +-- Error: tests/neg-custom-args/captures/vars.scala:29:2 --------------------------------------------------------------- +29 | local { cap3 => // error | ^^^^^ |inferred type argument {*} (x$0: ? String) => ? String is not allowed to capture the universal capability (* : Any) | diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index 8c80eec75783..4a58f79932b3 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -11,11 +11,13 @@ def test(cap1: Cap, cap2: Cap) = val z2c: () => Unit = z2 // error var a: {*} String => String = f // error + var b: List[{*} String => String] = Nil // error def scope = val cap3: Cap = CC() def g(x: String): String = if cap3 == cap3 then "" else "a" a = g + b = List(g) val gc = g g From 5b045537a04efb171f185429276d393589eaf9e8 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 12 Dec 2021 19:15:29 +0100 Subject: [PATCH 08/99] Introduce @capability annotations This replaces the earlier @ability annotation. The mechanisms are different, though. @ability was an annotation on `val`s whereas `capability` is an annotation on classes. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 ++- .../dotty/tools/dotc/core/Definitions.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 1 - .../tools/dotc/typer/CheckCaptures.scala | 3 +- .../scala/annotation/ability.scala | 9 ------ library/src/scala/annotation/capability.scala | 13 +++++++++ tests/disabled/pos/lazylist.scala | 3 +- tests/neg-custom-args/captures/byname.scala | 3 +- .../captures/capt-capability.scala | 28 +++++++++++++++++++ tests/pos-custom-args/captures/try3.scala | 2 +- tests/pos-deep-subtype/i4036.scala | 12 -------- 11 files changed, 49 insertions(+), 31 deletions(-) delete mode 100644 library/src-bootstrapped/scala/annotation/ability.scala create mode 100644 library/src/scala/annotation/capability.scala create mode 100644 tests/pos-custom-args/captures/capt-capability.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index f8ca2f87e3c5..f81c7bbc7b4c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -523,7 +523,9 @@ object CaptureSet: tp.captureSet case tp: TermParamRef => tp.captureSet - case _: TypeRef | _: TypeParamRef => + case _: TypeRef => + if tp.classSymbol.hasAnnotation(defn.CapabilityAnnot) then universal else empty + case _: TypeParamRef => empty case CapturingType(parent, refs, _) => recur(parent) ++ refs diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 62c4ae389028..287638b076d5 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -951,6 +951,7 @@ class Definitions { @tu lazy val BeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BeanProperty") @tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty") @tu lazy val BodyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Body") + @tu lazy val CapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.capability") @tu lazy val ChildAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Child") @tu lazy val ContextResultCountAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ContextResultCount") @tu lazy val ProvisionalSuperClassAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ProvisionalSuperClass") @@ -995,7 +996,6 @@ class Definitions { @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val SinceAnnot: ClassSymbol = requiredClass("scala.annotation.since") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.retains") - @tu lazy val AbilityAnnot: ClassSymbol = requiredClass("scala.annotation.ability") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 3487561d9155..8c62c056c6b9 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2767,7 +2767,6 @@ object Types { def canBeTracked(using Context) = ((prefix eq NoPrefix) || symbol.is(ParamAccessor) && (prefix eq symbol.owner.thisType) - || symbol.hasAnnotation(defn.AbilityAnnot) || isRootCapability ) && !symbol.is(Method) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index f7a14f259f24..b842b76f84a3 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -406,8 +406,7 @@ class CheckCaptures extends Recheck: def checkNotGlobal(tree: Tree, tp: Type, allArgs: Tree*)(using Context): Unit = for ref <-tp.captureSet.elems do val isGlobal = ref match - case ref: TermRef => - ref.isRootCapability || ref.prefix != NoPrefix && ref.symbol.hasAnnotation(defn.AbilityAnnot) + case ref: TermRef => ref.isRootCapability case _ => false if isGlobal then val what = if ref.isRootCapability then "universal" else "global" diff --git a/library/src-bootstrapped/scala/annotation/ability.scala b/library/src-bootstrapped/scala/annotation/ability.scala deleted file mode 100644 index 8b327a2f8b02..000000000000 --- a/library/src-bootstrapped/scala/annotation/ability.scala +++ /dev/null @@ -1,9 +0,0 @@ -package scala.annotation - -/** An annotation inidcating that a val should be tracked as its own ability. - * Example: - * - * @ability erased val canThrow: * = ??? - * ^^^ rename to capability - */ -class ability extends StaticAnnotation \ No newline at end of file diff --git a/library/src/scala/annotation/capability.scala b/library/src/scala/annotation/capability.scala new file mode 100644 index 000000000000..15504acc3258 --- /dev/null +++ b/library/src/scala/annotation/capability.scala @@ -0,0 +1,13 @@ +package scala.annotation + +/** Marks an annotated class as a capabulity. + * If the annotation is present and -Ycc is set, any (possibly aliased + * or refined) instance of the class type is implicitly augmented with + * the universal capture set. Example + * + * @capability class CanThrow[T] + * + * THere, the capture set of any instance of `CanThrow` is assumed to be + * `{*}`. + */ +final class capability extends StaticAnnotation diff --git a/tests/disabled/pos/lazylist.scala b/tests/disabled/pos/lazylist.scala index be628113d2d8..958f4c35aaf0 100644 --- a/tests/disabled/pos/lazylist.scala +++ b/tests/disabled/pos/lazylist.scala @@ -34,8 +34,7 @@ object LazyNil extends LazyList[Nothing]: def map[A, B](xs: {*} LazyList[A], f: {*} A => B): {f, xs} LazyList[B] = xs.map(f) -class CC -type Cap = {*} CC +@annotation.capability class Cap def test(cap1: Cap, cap2: Cap, cap3: Cap) = def f[T](x: LazyList[T]): LazyList[T] = if cap1 == cap1 then x else LazyNil diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala index 526cdc50952f..ef5876be2c11 100644 --- a/tests/neg-custom-args/captures/byname.scala +++ b/tests/neg-custom-args/captures/byname.scala @@ -1,5 +1,4 @@ -class CC -type Cap = {*} CC +@annotation.capability class Cap def test(cap1: Cap, cap2: Cap) = def f() = if cap1 == cap1 then g else g diff --git a/tests/pos-custom-args/captures/capt-capability.scala b/tests/pos-custom-args/captures/capt-capability.scala new file mode 100644 index 000000000000..41da15d288f1 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-capability.scala @@ -0,0 +1,28 @@ +import annotation.capability + +@capability class Cap +def f1(c: Cap): {c} () => c.type = () => c // ok + +def f2: Int = + val g: {*} Boolean => Int = ??? + val x = g(true) + x + +def f3: Int = + def g: {*} Boolean => Int = ??? + def h = g + val x = g.apply(true) + x + +def foo() = + val x: Cap = ??? + val y: Cap = x + val x2: {x} () => Cap = ??? + val y2: {x} () => Cap = x2 + + val z1: {*} () => Cap = f1(x) + def h[X](a: X)(b: X) = a + + val z2 = + if x == null then () => x else () => Cap() + x diff --git a/tests/pos-custom-args/captures/try3.scala b/tests/pos-custom-args/captures/try3.scala index 074517d8a9e5..b29ad2d4b352 100644 --- a/tests/pos-custom-args/captures/try3.scala +++ b/tests/pos-custom-args/captures/try3.scala @@ -1,5 +1,5 @@ import language.experimental.erasedDefinitions -import annotation.ability +import annotation.capability import java.io.IOException class CT[-E] // variance is needed for correct rechecking inference diff --git a/tests/pos-deep-subtype/i4036.scala b/tests/pos-deep-subtype/i4036.scala index 1784a9189fab..08ff248caf9d 100644 --- a/tests/pos-deep-subtype/i4036.scala +++ b/tests/pos-deep-subtype/i4036.scala @@ -11,9 +11,6 @@ object A { x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.type 1: v. @@ -25,16 +22,10 @@ object A { x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.T val u = new B { type T = Int } u: u. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. - x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x.x. @@ -55,8 +46,5 @@ object A { T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# - T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# - T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# - T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T# T#T#T#T#T#T#T#T#T#T#T#T#T#T#T#T } From e835dc0a6fb3de39ea4f072fda51207652e0048b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 12 Dec 2021 21:12:16 +0100 Subject: [PATCH 09/99] Fix typo and update semanticdb.expect --- library/src/scala/annotation/capability.scala | 2 +- tests/semanticdb/metac.expect | 80 ++----------------- 2 files changed, 9 insertions(+), 73 deletions(-) diff --git a/library/src/scala/annotation/capability.scala b/library/src/scala/annotation/capability.scala index 15504acc3258..98c6e15e023a 100644 --- a/library/src/scala/annotation/capability.scala +++ b/library/src/scala/annotation/capability.scala @@ -1,6 +1,6 @@ package scala.annotation -/** Marks an annotated class as a capabulity. +/** Marks an annotated class as a capability. * If the annotation is present and -Ycc is set, any (possibly aliased * or refined) instance of the class type is implicitly augmented with * the universal capture set. Example diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index 5e206505137b..cad281110d9c 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -359,7 +359,6 @@ Text => empty Language => Scala Symbols => 14 entries Occurrences => 30 entries -Synthetics => 2 entries Symbols: example/Anonymous# => class Anonymous extends Object { self: Anonymous & Anonymous => +6 decls } @@ -409,10 +408,6 @@ Occurrences: [18:6..18:9): foo <- example/Anonymous#foo. [18:16..18:19): Foo -> example/Anonymous#Foo# -Synthetics: -[10:2..10:9):locally => *[Unit] -[13:2..13:9):locally => *[Unit] - expect/AnonymousGiven.scala --------------------------- @@ -447,7 +442,6 @@ Text => empty Language => Scala Symbols => 109 entries Occurrences => 113 entries -Synthetics => 2 entries Symbols: classes/C1# => final class C1 extends AnyVal { self: C1 => +2 decls } @@ -675,10 +669,6 @@ Occurrences: [53:4..53:9): local -> local4 [53:10..53:11): + -> scala/Int#`+`(+4). -Synthetics: -[51:16..51:27):List(1).map => *[Int] -[51:16..51:20):List => *.apply[Int] - expect/Empty.scala ------------------ @@ -896,7 +886,7 @@ Text => empty Language => Scala Symbols => 181 entries Occurrences => 148 entries -Synthetics => 10 entries +Synthetics => 8 entries Symbols: _empty_/Enums. => final object Enums extends Object { self: Enums.type => +30 decls } @@ -1239,8 +1229,6 @@ Synthetics: [52:31..52:50):identity[Option[B]] => *[Function1[A, Option[B]]] [54:14..54:18):Some => *.apply[Some[Int]] [54:14..54:34):Some(Some(1)).unwrap => *(given_<:<_T_T[Option[Int]]) -[54:19..54:23):Some => *.apply[Int] -[54:28..54:34):unwrap => *[Some[Int], Int] [56:52..56:64):Enum[Planet] => *[Planet] expect/EtaExpansion.scala @@ -1253,7 +1241,7 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 8 entries -Synthetics => 5 entries +Synthetics => 1 entries Symbols: example/EtaExpansion# => class EtaExpansion extends Object { self: EtaExpansion => +1 decls } @@ -1271,11 +1259,7 @@ Occurrences: [4:25..4:26): + -> java/lang/String#`+`(). Synthetics: -[3:2..3:13):Some(1).map => *[Int] -[3:2..3:6):Some => *.apply[Int] -[3:14..3:22):identity => *[Int] [4:2..4:18):List(1).foldLeft => *[String] -[4:2..4:6):List => *.apply[Int] expect/Example.scala -------------------- @@ -1425,7 +1409,7 @@ Text => empty Language => Scala Symbols => 13 entries Occurrences => 52 entries -Synthetics => 6 entries +Synthetics => 2 entries Symbols: example/ForComprehension# => class ForComprehension extends Object { self: ForComprehension => +1 decls } @@ -1497,10 +1481,6 @@ Occurrences: [41:6..41:7): f -> local10 Synthetics: -[4:9..4:13):List => *.apply[Int] -[5:9..5:13):List => *.apply[Int] -[10:9..10:13):List => *.apply[Int] -[11:9..11:13):List => *.apply[Int] [19:9..19:13):List => *.apply[Tuple2[Int, Int]] [33:9..33:13):List => *.apply[Tuple4[Int, Int, Int, Int]] @@ -1514,7 +1494,6 @@ Text => empty Language => Scala Symbols => 29 entries Occurrences => 65 entries -Synthetics => 3 entries Symbols: a/b/Givens. => final object Givens extends Object { self: Givens.type => +12 decls } @@ -1614,11 +1593,6 @@ Occurrences: [26:57..26:58): A -> a/b/Givens.foo().(A) [26:59..26:64): empty -> a/b/Givens.Monoid#empty(). -Synthetics: -[12:17..12:25):sayHello => *[Int] -[13:19..13:29):sayGoodbye => *[Int] -[14:18..14:27):saySoLong => *[Int] - expect/ImplicitConversion.scala ------------------------------- @@ -2018,7 +1992,6 @@ Text => empty Language => Scala Symbols => 6 entries Occurrences => 10 entries -Synthetics => 1 entries Symbols: example/Local# => class Local extends Object { self: Local => +2 decls } @@ -2040,9 +2013,6 @@ Occurrences: [4:25..4:26): a -> local1 [5:4..5:6): id -> local2 -Synthetics: -[5:4..5:6):id => *[Int] - expect/Locals.scala ------------------- @@ -2053,7 +2023,6 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 6 entries -Synthetics => 1 entries Symbols: local0 => val local x: Int @@ -2222,7 +2191,7 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 80 entries -Synthetics => 2 entries +Synthetics => 1 entries Symbols: example/MethodUsages# => class MethodUsages extends Object { self: MethodUsages => +2 decls } @@ -2312,7 +2281,6 @@ Occurrences: [34:8..34:9): m -> example/Methods#m17.m(). Synthetics: -[13:2..13:6):m.m7 => *[Int] [13:2..13:26):m.m7(m, new m.List[Int]) => *(Int) expect/Methods.scala @@ -3165,7 +3133,7 @@ Text => empty Language => Scala Symbols => 52 entries Occurrences => 132 entries -Synthetics => 36 entries +Synthetics => 23 entries Symbols: example/Synthetic# => class Synthetic extends Object { self: Synthetic => +23 decls } @@ -3356,26 +3324,17 @@ Occurrences: [58:6..58:9): foo -> example/Synthetic#Contexts.foo(). Synthetics: -[5:2..5:13):List(1).map => *[Int] -[5:2..5:6):List => *.apply[Int] [6:2..6:18):Array.empty[Int] => intArrayOps(*) [7:2..7:8):"fooo" => augmentString(*) [10:13..10:24):"name:(.*)" => augmentString(*) -[11:17..11:25):LazyList => *.apply[Int] -[13:4..13:28):#:: 2 #:: LazyList.empty => *[Int] [13:8..13:28):2 #:: LazyList.empty => toDeferrer[Int](*) -[13:10..13:28):#:: LazyList.empty => *[Int] [13:14..13:28):LazyList.empty => toDeferrer[Nothing](*) [13:14..13:28):LazyList.empty => *[Nothing] -[15:25..15:33):LazyList => *.apply[Int] -[17:14..17:38):#:: 2 #:: LazyList.empty => *[Int] [17:18..17:38):2 #:: LazyList.empty => toDeferrer[Int](*) -[17:20..17:38):#:: LazyList.empty => *[Int] [17:24..17:38):LazyList.empty => toDeferrer[Nothing](*) [17:24..17:38):LazyList.empty => *[Nothing] [19:12..19:13):1 => intWrapper(*) [19:26..19:27):0 => intWrapper(*) -[19:46..19:50):x -> => *[Int] [19:46..19:47):x => ArrowAssoc[Int](*) [20:12..20:13):1 => intWrapper(*) [20:26..20:27):0 => intWrapper(*) @@ -3384,10 +3343,6 @@ Synthetics: [32:35..32:49):Array.empty[T] => *(evidence$1) [36:22..36:27):new F => orderingToOrdered[F](*) [36:22..36:27):new F => *(ordering) -[40:9..40:43):scala.concurrent.Future.successful => *[Int] -[41:9..41:43):scala.concurrent.Future.successful => *[Int] -[44:9..44:43):scala.concurrent.Future.successful => *[Int] -[45:9..45:43):scala.concurrent.Future.successful => *[Int] [51:24..51:30):foo(0) => *(x$1) [52:27..52:33):foo(0) => *(x) [55:6..55:12):foo(x) => *(x) @@ -3471,7 +3426,7 @@ Text => empty Language => Scala Symbols => 22 entries Occurrences => 46 entries -Synthetics => 7 entries +Synthetics => 2 entries Symbols: example/ValPattern# => class ValPattern extends Object { self: ValPattern => +14 decls } @@ -3546,13 +3501,8 @@ Occurrences: [40:10..40:18): rightVar -> local4 Synthetics: -[6:4..6:8):Some => *.apply[Int] [8:6..8:10):List => *.unapplySeq[Nothing] [8:11..8:15):Some => *.unapply[Nothing] -[12:4..12:8):Some => *.apply[Int] -[25:4..25:11):locally => *[Unit] -[28:8..28:12):Some => *.apply[Int] -[32:8..32:12):Some => *.apply[Int] expect/Vals.scala ----------------- @@ -4171,7 +4121,6 @@ Text => empty Language => Scala Symbols => 3 entries Occurrences => 6 entries -Synthetics => 1 entries Symbols: example/`local-file`# => class local-file extends Object { self: local-file => +1 decls } @@ -4186,9 +4135,6 @@ Occurrences: [5:4..5:9): local -> local0 [5:10..5:11): + -> scala/Int#`+`(+4). -Synthetics: -[3:2..3:9):locally => *[Int] - expect/nullary.scala -------------------- @@ -4199,7 +4145,6 @@ Text => empty Language => Scala Symbols => 17 entries Occurrences => 29 entries -Synthetics => 1 entries Symbols: _empty_/Concrete# => class Concrete extends NullaryTest[Int, List] { self: Concrete => +3 decls } @@ -4251,9 +4196,6 @@ Occurrences: [18:7..18:15): Concrete -> _empty_/Concrete# [18:17..18:25): nullary3 -> _empty_/Concrete#nullary3(). -Synthetics: -[13:17..13:21):List => *.apply[Int] - expect/recursion.scala ---------------------- @@ -4542,7 +4484,6 @@ Text => empty Language => Scala Symbols => 144 entries Occurrences => 225 entries -Synthetics => 1 entries Symbols: local0 => abstract method k => Int @@ -4917,9 +4858,6 @@ Occurrences: [119:32..119:38): Option -> scala/Option# [119:39..119:42): Int -> scala/Int# -Synthetics: -[68:20..68:24):@ann => *[Int] - expect/semanticdb-extract.scala ------------------------------- @@ -4930,7 +4868,7 @@ Text => empty Language => Scala Symbols => 18 entries Occurrences => 20 entries -Synthetics => 3 entries +Synthetics => 2 entries Symbols: _empty_/AnObject. => final object AnObject extends Object { self: AnObject.type => +6 decls } @@ -4975,7 +4913,6 @@ Occurrences: [16:20..16:23): Int -> scala/Int# Synthetics: -[11:2..11:6):List => *.apply[Int] [12:2..12:12):List.apply => *[Nothing] [13:2..13:14):List.`apply` => *[Nothing] @@ -4989,7 +4926,7 @@ Text => empty Language => Scala Symbols => 18 entries Occurrences => 43 entries -Synthetics => 2 entries +Synthetics => 1 entries Symbols: _empty_/MyProgram# => final class MyProgram extends Object { self: MyProgram => +2 decls } @@ -5057,6 +4994,5 @@ Occurrences: [7:30..7:33): foo -> _empty_/toplevel$package.foo(). Synthetics: -[5:40..5:60):(1 to times) foreach => *[Unit] [5:41..5:42):1 => intWrapper(*) From 8cac9db182838a7f4b31d1a272005edcc4310ef8 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Dec 2021 19:36:53 +0100 Subject: [PATCH 10/99] Special rule for {this} in capture sets of class members Consider the lazylists.scala test in pos-custom-args/captures: ```scala class CC type Cap = {*} CC trait LazyList[+A]: this: ({*} LazyList[A]) => def isEmpty: Boolean def head: A def tail: {this} LazyList[A] object LazyNil extends LazyList[Nothing]: def isEmpty: Boolean = true def head = ??? def tail = ??? extension [A](xs: {*} LazyList[A]) def map[B](f: {*} A => B): {xs, f} LazyList[B] = class Mapped extends LazyList[B]: this: ({xs, f} Mapped) => def isEmpty = false def head: B = f(xs.head) def tail: {this} LazyList[B] = xs.tail.map(f) // OK new Mapped ``` Without this commit, the second to last line is an error since the right hand side has capture set `{xs, f}` but the required capture set is `this`. To fix this, we widen the expected type of the rhs `xs.tail.map(f)` from `{this}` to `{this, f, xs}`. That is, we add the declared captures of the self type to the expected type. The soundness argument for doing this is as follows: Since `tail` does not have parameters, the only thing it could capture are references that the receiver `this` captures as well. So `xs` and `f` must come via `this`. For instance, if the receiver `xs` of `xs.tail` happens to be pure, then `xs.tail` is pure as well. On the other hand, in the neg test `lazylists1.scala` we add the following line to `Mapped`: ```scala def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error ``` Here, we cannot widen the expected type from `{this}` to `{this, xs, f}` since the result of concat refers to `f` independently of `this`, namely through its parameter `other`. Hence, if `ys: {f} LazyList[String]` then ``` LazyNil.concat(ys) ``` still refers to `f` even though `LazyNil` is pure. But if we would accept the definition of `concat` above, the type of `LazyNil.concat(ys)` would be `LazyList[String]`, which is unsound. The current implementation widens the expected type of class members if the class member does not have tracked parameters. We could potentially refine this to say we widen with all references in the expected type that are not subsumed by one of the parameter types. ## Changes: ### Refine rule for this widening We now widen the expected type of the right hand side of a class member as follows: Add all references of the declared type of this that are not subsumed by a capture set of a parameter type. ### Do expected type widening only in final classes Alex found a counter-example why this is required. See map5 in neg-customargs/captures/lazylists2.scala --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 9 ++- .../dotty/tools/dotc/transform/Recheck.scala | 7 +- .../tools/dotc/typer/CheckCaptures.scala | 16 +++++ .../neg-custom-args/captures/lazylists1.check | 7 ++ .../neg-custom-args/captures/lazylists1.scala | 27 ++++++++ .../neg-custom-args/captures/lazylists2.check | 45 +++++++++++++ .../neg-custom-args/captures/lazylists2.scala | 64 +++++++++++++++++++ .../captures/lazylists-mono.scala | 27 ++++++++ .../pos-custom-args/captures/lazylists.scala | 42 ++++++++++++ .../pos-custom-args/captures/lazylists1.scala | 35 ++++++++++ 10 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 tests/neg-custom-args/captures/lazylists1.check create mode 100644 tests/neg-custom-args/captures/lazylists1.scala create mode 100644 tests/neg-custom-args/captures/lazylists2.check create mode 100644 tests/neg-custom-args/captures/lazylists2.scala create mode 100644 tests/pos-custom-args/captures/lazylists-mono.scala create mode 100644 tests/pos-custom-args/captures/lazylists.scala create mode 100644 tests/pos-custom-args/captures/lazylists1.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index f81c7bbc7b4c..cd8d67399d8d 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -49,7 +49,14 @@ sealed abstract class CaptureSet extends Showable: /** Is this capture set definitely non-empty? */ final def isNotEmpty: Boolean = !elems.isEmpty - /** Cast to variable. @pre: @isConst */ + /** Cast to Const. @pre: isConst */ + def asConst: Const = this match + case c: Const => c + case v: Var => + assert(v.isConst) + Const(v.elems) + + /** Cast to variable. @pre: !isConst */ def asVar: Var = assert(!isConst) asInstanceOf[Var] diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 55a57ede2e0a..924a444aeff4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -203,12 +203,15 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: bindType def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = - if !tree.rhs.isEmpty then recheck(tree.rhs, sym.info) + if !tree.rhs.isEmpty then recheckRHS(tree.rhs, sym.info, sym) def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = val rhsCtx = linkConstructorParams(sym).withOwner(sym) if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then - inContext(rhsCtx) { recheck(tree.rhs, recheck(tree.tpt)) } + inContext(rhsCtx) { recheckRHS(tree.rhs, recheck(tree.tpt), sym) } + + def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type = + recheck(tree, pt) def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type = recheck(tree.rhs) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index b842b76f84a3..1f30dd989f3a 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -322,6 +322,22 @@ class CheckCaptures extends Recheck: interpolateVarsIn(tree.tpt) curEnv = saved + override def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type = + val pt1 = pt match + case CapturingType(core, refs, _) + if sym.owner.isClass && !sym.owner.isExtensibleClass + && refs.elems.contains(sym.owner.thisType) => + val paramCaptures = + sym.paramSymss.flatten.foldLeft(CaptureSet.empty) { (cs, p) => + val pcs = p.info.captureSet + (cs ++ (if pcs.isConst then pcs else CaptureSet.universal)).asConst + } + val declaredCaptures = sym.owner.asClass.givenSelfType.captureSet + pt.derivedCapturingType(core, refs ++ (declaredCaptures -- paramCaptures)) + case _ => + pt + recheck(tree, pt1) + override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = for param <- cls.paramGetters do if param.is(Private) && !param.info.captureSet.isAlwaysEmpty then diff --git a/tests/neg-custom-args/captures/lazylists1.check b/tests/neg-custom-args/captures/lazylists1.check new file mode 100644 index 000000000000..29291c8044c0 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists1.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists1.scala:25:63 ----------------------------------- +25 | def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {xs, f} LazyList[A] + | Required: {Mapped.this, xs} LazyList[A] + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists1.scala b/tests/neg-custom-args/captures/lazylists1.scala new file mode 100644 index 000000000000..02c7cb4ff3e5 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists1.scala @@ -0,0 +1,27 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + this: ({*} LazyList[A]) => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +extension [A](xs: {*} LazyList[A]) + def map[B](f: {*} A => B): {xs, f} LazyList[B] = + final class Mapped extends LazyList[B]: + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) // OK + def drop(n: Int): {this} LazyList[B] = ??? : ({xs, f} LazyList[B]) // OK + def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error + new Mapped + diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check new file mode 100644 index 000000000000..8e09dd26cccf --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -0,0 +1,45 @@ +-- [E163] Declaration Error: tests/neg-custom-args/captures/lazylists2.scala:50:10 ------------------------------------- +50 | def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error + | ^ + | error overriding method tail in trait LazyList of type => {Mapped.this} LazyList[B]; + | method tail of type => {xs, f} LazyList[B] has incompatible type + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ +18 | final class Mapped extends LazyList[B]: // error + | ^ + | Found: {f, xs} LazyList[B] + | Required: {f} LazyList[B] +19 | this: ({xs, f} Mapped) => +20 | def isEmpty = false +21 | def head: B = f(xs.head) +22 | def tail: {this} LazyList[B] = xs.tail.map(f) +23 | new Mapped + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------ +27 | final class Mapped extends LazyList[B]: // error + | ^ + | Found: {f, xs} LazyList[B] + | Required: {xs} LazyList[B] +28 | this: ({xs, f} Mapped) => +29 | def isEmpty = false +30 | def head: B = f(xs.head) +31 | def tail: {this} LazyList[B] = xs.tail.map(f) +32 | new Mapped + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:41:48 ----------------------------------- +41 | def tail: {this} LazyList[B] = xs.tail.map(f) // error + | ^^^^^^^^^^^^^^ + | Found: {f} LazyList[B] + | Required: {xs} LazyList[B] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:59:48 ----------------------------------- +59 | def tail: {this} LazyList[B] = xs.tail.map(f) // error + | ^^^^^^^^^^^^^^ + | Found: {f} LazyList[B] + | Required: {Mapped.this} LazyList[B] + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists2.scala b/tests/neg-custom-args/captures/lazylists2.scala new file mode 100644 index 000000000000..c31a1ae5d04f --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists2.scala @@ -0,0 +1,64 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + this: ({*} LazyList[A]) => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +extension [A](xs: {*} LazyList[A]) + def map[B](f: {*} A => B): {f} LazyList[B] = + final class Mapped extends LazyList[B]: // error + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) + new Mapped + + def map2[B](f: {*} A => B): {xs} LazyList[B] = + final class Mapped extends LazyList[B]: // error + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) + new Mapped + + def map3[B](f: {*} A => B): {xs} LazyList[B] = + final class Mapped extends LazyList[B]: + this: ({xs} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) // error + new Mapped + + def map4[B](f: {*} A => B): {xs} LazyList[B] = + final class Mapped extends LazyList[B]: + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error + new Mapped + + def map5[B](f: {*} A => B): LazyList[B] = + class Mapped extends LazyList[B]: + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) // error + class Mapped2 extends Mapped: + this: Mapped => + new Mapped2 + + diff --git a/tests/pos-custom-args/captures/lazylists-mono.scala b/tests/pos-custom-args/captures/lazylists-mono.scala new file mode 100644 index 000000000000..82c44abf703a --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists-mono.scala @@ -0,0 +1,27 @@ +class CC +type Cap = {*} CC + +//------------------------------------------------- + +def test(E: Cap) = + + trait LazyList[+A]: + protected def contents: {E} () => (A, {E} LazyList[A]) + def isEmpty: Boolean + def head: A = contents()._1 + def tail: {E} LazyList[A] = contents()._2 + + class LazyCons[+A](override val contents: {E} () => (A, {E} LazyList[A])) + extends LazyList[A]: + def isEmpty: Boolean = false + + object LazyNil extends LazyList[Nothing]: + def contents: {E} () => (Nothing, LazyList[Nothing]) = ??? + def isEmpty: Boolean = true + + extension [A](xs: {E} LazyList[A]) + def map[B](f: {E} A => B): {E} LazyList[B] = + if xs.isEmpty then LazyNil + else + val cons = () => (f(xs.head), xs.tail.map(f)) + LazyCons(cons) diff --git a/tests/pos-custom-args/captures/lazylists.scala b/tests/pos-custom-args/captures/lazylists.scala new file mode 100644 index 000000000000..17d5f8546edc --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists.scala @@ -0,0 +1,42 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + this: ({*} LazyList[A]) => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +extension [A](xs: {*} LazyList[A]) + def map[B](f: {*} A => B): {xs, f} LazyList[B] = + final class Mapped extends LazyList[B]: + this: ({xs, f} Mapped) => + + def isEmpty = false + def head: B = f(xs.head) + def tail: {this} LazyList[B] = xs.tail.map(f) // OK + def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : ({xs, f} LazyList[A]) // OK + if xs.isEmpty then LazyNil + else new Mapped + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + def g(x: String): String = if cap2 == cap2 then "" else "a" + + val xs = + class Initial extends LazyList[String]: + this: ({cap1} Initial) => + + def isEmpty = false + def head = f("") + def tail = LazyNil + new Initial + val xsc: {cap1} LazyList[String] = xs + val ys = xs.map(g) + val ysc: {cap1, cap2} LazyList[String] = ys diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala new file mode 100644 index 000000000000..4c8006fb0e29 --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -0,0 +1,35 @@ +class CC +type Cap = {*} CC + +trait LazyList[+A]: + this: ({*} LazyList[A]) => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +final class LazyCons[+T](val x: T, val xs: {*} () => {*} LazyList[T]) extends LazyList[T]: + this: ({*} LazyList[T]) => + + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = xs() + +extension [A](xs: {*} LazyList[A]) + def map[B](f: {*} A => B): {xs, f} LazyList[B] = + if xs.isEmpty then LazyNil + else LazyCons(f(xs.head), () => xs.tail.map(f)) + +def test(cap1: Cap, cap2: Cap) = + def f(x: String): String = if cap1 == cap1 then "" else "a" + def g(x: String): String = if cap2 == cap2 then "" else "a" + + val xs = LazyCons("", () => if f("") == f("") then LazyNil else LazyNil) + val xsc: {cap1} LazyList[String] = xs + val ys = xs.map(g) + val ysc: {cap1, cap2} LazyList[String] = ys From 965635c8cdc80ae0698958337f84dbd2a3697d51 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 25 Dec 2021 11:53:08 +0100 Subject: [PATCH 11/99] Pure function types -> and ?-> 1. Allow `->` and `?->` and function operators, treated like `=>` and `?=>`. 2. under -Ycc treat `->` and `?->` as immutable function types, whereas `A => B` is an alias of `{*} A -> B` and `A ?=> B` is an alias of `{*} A ?-> B`. Closures are unaffected, we still use `=>` for all closures where they are pure or not. Improve printing of capturing types Avoid explicit retains annotations also outside phase cc Generate "Impure" function aliases For every (possibly erased and/or context) function class XFunctionN, generate an alias ImpureXFunctionN in the Scala package defined as type ImpureXFunctionN[...] = {*} XFunctionN[...] Also: - Fix a bug in TypeComparer: glb has to test subCapture in a frozen state - Harden EventuallyCapturingType extractor to not crash on illegal capture sets - Cleanup transformation of inferred types --- compiler/src/dotty/tools/dotc/ast/untpd.scala | 6 +- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 20 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 6 + .../dotty/tools/dotc/cc/CapturingType.scala | 18 +- .../dotty/tools/dotc/core/Definitions.scala | 125 +++++++---- .../src/dotty/tools/dotc/core/Flags.scala | 4 +- .../src/dotty/tools/dotc/core/NameOps.scala | 40 ++-- .../src/dotty/tools/dotc/core/StdNames.scala | 2 + .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 6 +- .../dotty/tools/dotc/parsing/Parsers.scala | 63 +++--- .../tools/dotc/printing/PlainPrinter.scala | 13 +- .../tools/dotc/printing/RefinedPrinter.scala | 54 +++-- .../tools/dotc/typer/CheckCaptures.scala | 206 ++++++++++++------ .../dotty/tools/dotc/typer/Implicits.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 9 +- .../quoted/runtime/impl/QuotesImpl.scala | 2 +- tests/neg-custom-args/capt-wf.scala | 6 +- tests/neg-custom-args/captures/bounded.scala | 4 +- tests/neg-custom-args/captures/boxmap.check | 8 +- tests/neg-custom-args/captures/boxmap.scala | 10 +- tests/neg-custom-args/captures/byname.scala | 2 +- .../captures/capt-box-env.scala | 5 +- tests/neg-custom-args/captures/capt-box.scala | 6 +- tests/neg-custom-args/captures/capt-env.scala | 2 +- tests/neg-custom-args/captures/capt1.check | 14 +- tests/neg-custom-args/captures/capt1.scala | 8 +- tests/neg-custom-args/captures/capt2.scala | 6 +- tests/neg-custom-args/captures/capt3.scala | 8 +- tests/neg-custom-args/captures/io.scala | 6 +- tests/neg-custom-args/captures/lazylist.check | 2 +- tests/neg-custom-args/captures/lazylist.scala | 6 +- .../neg-custom-args/captures/lazylists1.scala | 2 +- .../neg-custom-args/captures/lazylists2.scala | 10 +- tests/neg-custom-args/captures/lazyref.check | 8 +- tests/neg-custom-args/captures/lazyref.scala | 8 +- tests/neg-custom-args/captures/try.check | 14 +- tests/neg-custom-args/captures/try.scala | 2 +- tests/neg-custom-args/captures/vars.check | 24 +- tests/neg-custom-args/captures/vars.scala | 14 +- tests/pos-custom-args/captures/bounded.scala | 4 +- .../captures/boxmap-paper.scala | 23 +- tests/pos-custom-args/captures/boxmap.scala | 14 +- .../captures/capt-capability.scala | 12 +- .../captures/capt-depfun.scala | 12 +- .../captures/capt-depfun2.scala | 2 +- .../pos-custom-args/captures/capt-test.scala | 8 +- tests/pos-custom-args/captures/capt1.scala | 12 +- tests/pos-custom-args/captures/capt2.scala | 12 +- .../pos-custom-args/captures/cc-expand.scala | 16 +- .../pos-custom-args/captures/impurefun.scala | 8 + .../captures/lazylists-mono.scala | 8 +- .../pos-custom-args/captures/lazylists.scala | 2 +- .../pos-custom-args/captures/lazylists1.scala | 10 +- tests/pos-custom-args/captures/lazyref.scala | 11 +- .../captures/list-encoding.scala | 4 +- tests/pos-custom-args/captures/lists.scala | 87 ++++---- tests/pos-custom-args/captures/pairs.scala | 13 +- tests/pos-custom-args/captures/try.scala | 4 +- tests/pos-custom-args/captures/try3.scala | 5 +- tests/pos-custom-args/captures/vars.scala | 11 +- tests/pos/i12723.scala | 8 +- tests/pos/impurefun.scala | 4 + 63 files changed, 595 insertions(+), 448 deletions(-) create mode 100644 tests/pos-custom-args/captures/impurefun.scala create mode 100644 tests/pos/impurefun.scala diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 6435ffd4b3c4..3b74f5c1e121 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -69,13 +69,13 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class InterpolatedString(id: TermName, segments: List[Tree])(implicit @constructorOnly src: SourceFile) extends TermTree - /** A function type */ + /** A function type or closure */ case class Function(args: List[Tree], body: Tree)(implicit @constructorOnly src: SourceFile) extends Tree { override def isTerm: Boolean = body.isTerm override def isType: Boolean = body.isType } - /** A function type with `implicit`, `erased`, or `given` modifiers */ + /** A function type or closure with `implicit`, `erased`, or `given` modifiers */ class FunctionWithMods(args: List[Tree], body: Tree, val mods: Modifiers)(implicit @constructorOnly src: SourceFile) extends Function(args, body) @@ -216,6 +216,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Transparent()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Transparent) case class Infix()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Infix) + + case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure) } /** Modifiers and annotations for definitions diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 09064314b1bf..4c201d7edf54 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -17,9 +17,13 @@ def retainedElems(tree: Tree)(using Context): List[Tree] = tree match case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems case _ => Nil +class IllegalCaptureRef(tpe: Type) extends Exception + extension (tree: Tree) - def toCaptureRef(using Context): CaptureRef = tree.tpe.asInstanceOf[CaptureRef] + def toCaptureRef(using Context): CaptureRef = tree.tpe match + case ref: CaptureRef => ref + case tpe => throw IllegalCaptureRef(tpe) def toCaptureSet(using Context): CaptureSet = tree.getAttachment(Captures) match @@ -59,20 +63,6 @@ extension (tp: Type) def isBoxedCapturing(using Context) = !tp.boxedCaptured.isAlwaysEmpty - def canHaveInferredCapture(using Context): Boolean = tp match - case tp: TypeRef if tp.symbol.isClass => - !tp.symbol.isValueClass && tp.symbol != defn.AnyClass - case _: TypeVar | _: TypeParamRef => - false - case tp: TypeProxy => - tp.superType.canHaveInferredCapture - case tp: AndType => - tp.tp1.canHaveInferredCapture && tp.tp2.canHaveInferredCapture - case tp: OrType => - tp.tp1.canHaveInferredCapture || tp.tp2.canHaveInferredCapture - case _ => - false - def stripCapturing(using Context): Type = tp.dealiasKeepAnnots match case CapturingType(parent, _, _) => parent.stripCapturing diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index cd8d67399d8d..82e5e6e14a4b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -56,6 +56,12 @@ sealed abstract class CaptureSet extends Showable: assert(v.isConst) Const(v.elems) + final def isUniversal(using Context) = + elems.exists { + case ref: TermRef => ref.symbol == defn.captureRoot + case _ => false + } + /** Cast to variable. @pre: !isConst */ def asVar: Var = assert(!isConst) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index 2eeb1ff41b72..738e746d0178 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -12,10 +12,22 @@ object CapturingType: else AnnotatedType(parent, CaptureAnnotation(refs, boxed)) def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, Boolean)] = - if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then + if ctx.phase == Phases.checkCapturesPhase then EventuallyCapturingType.unapply(tp) + else None + +end CapturingType + +object EventuallyCapturingType: + + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, Boolean)] = + if tp.annot.symbol == defn.RetainsAnnot then tp.annot match case ann: CaptureAnnotation => Some((tp.parent, ann.refs, ann.boxed)) - case ann => Some((tp.parent, ann.tree.toCaptureSet, ann.tree.isBoxedCapturing)) + case ann => + try Some((tp.parent, ann.tree.toCaptureSet, ann.tree.isBoxedCapturing)) + catch case ex: IllegalCaptureRef => None else None -end CapturingType +end EventuallyCapturingType + + diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 287638b076d5..d6abcf693bf1 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -8,6 +8,7 @@ import Flags._, Scopes._, Decorators._, NameOps._, Periods._, NullOpsDecorator._ import unpickleScala2.Scala2Unpickler.ensureConstructor import scala.collection.mutable import collection.mutable +import Denotations.{SingleDenotation, staticRef} import util.{SimpleIdentityMap, SourceFile, NoSource} import typer.ImportInfo.RootRef import Comments.CommentsContext @@ -89,7 +90,7 @@ class Definitions { * * FunctionN traits follow this template: * - * trait FunctionN[T0,...T{N-1}, R] extends Object { + * trait FunctionN[-T0,...-T{N-1}, +R] extends Object { * def apply($x0: T0, ..., $x{N_1}: T{N-1}): R * } * @@ -99,46 +100,65 @@ class Definitions { * * ContextFunctionN traits follow this template: * - * trait ContextFunctionN[T0,...,T{N-1}, R] extends Object { + * trait ContextFunctionN[-T0,...,-T{N-1}, +R] extends Object { * def apply(using $x0: T0, ..., $x{N_1}: T{N-1}): R * } * * ErasedFunctionN traits follow this template: * - * trait ErasedFunctionN[T0,...,T{N-1}, R] extends Object { + * trait ErasedFunctionN[-T0,...,-T{N-1}, +R] extends Object { * def apply(erased $x0: T0, ..., $x{N_1}: T{N-1}): R * } * * ErasedContextFunctionN traits follow this template: * - * trait ErasedContextFunctionN[T0,...,T{N-1}, R] extends Object { + * trait ErasedContextFunctionN[-T0,...,-T{N-1}, +R] extends Object { * def apply(using erased $x0: T0, ..., $x{N_1}: T{N-1}): R * } * * ErasedFunctionN and ErasedContextFunctionN erase to Function0. + * + * EffXYZFunctionN afollow this template: + * + * type EffXYZFunctionN[-T0,...,-T{N-1}, +R] = {*} XYZFunctionN[T0,...,T{N-1}, R] */ - def newFunctionNTrait(name: TypeName): ClassSymbol = { + private def newFunctionNType(name: TypeName): Symbol = { + val impure = name.startsWith("Impure") val completer = new LazyType { def complete(denot: SymDenotation)(using Context): Unit = { - val cls = denot.asClass.classSymbol - val decls = newScope val arity = name.functionArity - val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR - val argParamRefs = List.tabulate(arity) { i => - enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef - } - val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef - val methodType = MethodType.companion( - isContextual = name.isContextFunction, - isImplicit = false, - isErased = name.isErasedFunction) - decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred)) - denot.info = - ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls) + if impure then + val argParamNames = List.tabulate(arity)(tpnme.syntheticTypeParamName) + val argVariances = List.fill(arity)(Contravariant) + val underlyingName = name.asSimpleName.drop(6) + val underlyingClass = ScalaPackageVal.requiredClass(underlyingName) + denot.info = TypeAlias( + HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)( + tl => List.fill(arity + 1)(TypeBounds.empty), + tl => CapturingType(underlyingClass.typeRef.appliedTo(tl.paramRefs), + CaptureSet.universal, boxed = false) + )) + else + val cls = denot.asClass.classSymbol + val decls = newScope + val paramNamePrefix = tpnme.scala ++ str.NAME_JOIN ++ name ++ str.EXPAND_SEPARATOR + val argParamRefs = List.tabulate(arity) { i => + enterTypeParam(cls, paramNamePrefix ++ "T" ++ (i + 1).toString, Contravariant, decls).typeRef + } + val resParamRef = enterTypeParam(cls, paramNamePrefix ++ "R", Covariant, decls).typeRef + val methodType = MethodType.companion( + isContextual = name.isContextFunction, + isImplicit = false, + isErased = name.isErasedFunction) + decls.enter(newMethod(cls, nme.apply, methodType(argParamRefs, resParamRef), Deferred)) + denot.info = + ClassInfo(ScalaPackageClass.thisType, cls, ObjectType :: Nil, decls) } } - val flags = Trait | NoInits - newPermanentClassSymbol(ScalaPackageClass, name, flags, completer) + if impure then + newPermanentSymbol(ScalaPackageClass, name, EmptyFlags, completer) + else + newPermanentClassSymbol(ScalaPackageClass, name, Trait | NoInits, completer) } private def newMethod(cls: ClassSymbol, name: TermName, info: Type, flags: FlagSet = EmptyFlags): TermSymbol = @@ -212,7 +232,7 @@ class Definitions { val cls = ScalaPackageVal.moduleClass.asClass cls.info.decls.openForMutations.useSynthesizer( name => - if (name.isTypeName && name.isSyntheticFunction) newFunctionNTrait(name.asTypeName) + if (name.isTypeName && name.isSyntheticFunction) newFunctionNType(name.asTypeName) else NoSymbol) cls } @@ -1342,6 +1362,10 @@ class Definitions { def SpecializedTuple(base: Symbol, args: List[Type])(using Context): Symbol = base.owner.requiredClass(base.name.specializedName(args)) + /** Cached function types of arbitary arities. + * Function types are created on demand with newFunctionNTrait, which is + * called from a synthesizer installed in ScalaPackageClass. + */ private class FunType(prefix: String): private var classRefs: Array[TypeRef | Null] = new Array(22) def apply(n: Int): TypeRef = @@ -1349,32 +1373,43 @@ class Definitions { val classRefs1 = new Array[TypeRef | Null](classRefs.length * 2) Array.copy(classRefs, 0, classRefs1, 0, classRefs.length) classRefs = classRefs1 + val funName = s"scala.$prefix$n" if classRefs(n) == null then - classRefs(n) = requiredClassRef(prefix + n.toString) + classRefs(n) = + if prefix.startsWith("Impure") + then staticRef(funName.toTypeName).symbol.typeRef + else requiredClassRef(funName) classRefs(n).nn - - private val erasedContextFunType = FunType("scala.ErasedContextFunction") - private val contextFunType = FunType("scala.ContextFunction") - private val erasedFunType = FunType("scala.ErasedFunction") - private val funType = FunType("scala.Function") - - def FunctionClass(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): Symbol = - ( if isContextual && isErased then erasedContextFunType(n) - else if isContextual then contextFunType(n) - else if isErased then erasedFunType(n) - else funType(n) - ).symbol.asClass + end FunType + + private def funTypeIdx(isContextual: Boolean, isErased: Boolean, isImpure: Boolean): Int = + (if isContextual then 1 else 0) + + (if isErased then 2 else 0) + + (if isImpure then 4 else 0) + + private val funTypeArray: IArray[FunType] = + val arr = Array.ofDim[FunType](8) + val choices = List(false, true) + for contxt <- choices; erasd <- choices; impure <- choices do + var str = "Function" + if contxt then str = "Context" + str + if erasd then str = "Erased" + str + if impure then str = "Impure" + str + arr(funTypeIdx(contxt, erasd, impure)) = FunType(str) + IArray.unsafeFromArray(arr) + + def FunctionSymbol(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): Symbol = + funTypeArray(funTypeIdx(isContextual, isErased, isImpure))(n).symbol @tu lazy val Function0_apply: Symbol = Function0.requiredMethod(nme.apply) - @tu lazy val ContextFunction0_apply: Symbol = ContextFunction0.requiredMethod(nme.apply) - @tu lazy val Function0: Symbol = FunctionClass(0) - @tu lazy val Function1: Symbol = FunctionClass(1) - @tu lazy val Function2: Symbol = FunctionClass(2) - @tu lazy val ContextFunction0: Symbol = FunctionClass(0, isContextual = true) + @tu lazy val Function0: Symbol = FunctionSymbol(0) + @tu lazy val Function1: Symbol = FunctionSymbol(1) + @tu lazy val Function2: Symbol = FunctionSymbol(2) + @tu lazy val ContextFunction0: Symbol = FunctionSymbol(0, isContextual = true) - def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false)(using Context): TypeRef = - FunctionClass(n, isContextual && !ctx.erasedTypes, isErased).typeRef + def FunctionType(n: Int, isContextual: Boolean = false, isErased: Boolean = false, isImpure: Boolean = false)(using Context): TypeRef = + FunctionSymbol(n, isContextual && !ctx.erasedTypes, isErased, isImpure).typeRef lazy val PolyFunctionClass = requiredClass("scala.PolyFunction") def PolyFunctionType = PolyFunctionClass.typeRef @@ -1416,6 +1451,10 @@ class Definitions { */ def isFunctionClass(cls: Symbol): Boolean = scalaClassName(cls).isFunction + /** Is a function class, or an impure function type alias */ + def isFunctionSymbol(sym: Symbol): Boolean = + sym.isType && (sym.owner eq ScalaPackageClass) && sym.name.isFunction + /** Is a function class where * - FunctionN for N >= 0 and N != XXL */ @@ -1649,7 +1688,7 @@ class Definitions { def isSpecializableFunction(cls: ClassSymbol, paramTypes: List[Type], retType: Type)(using Context): Boolean = paramTypes.length <= 2 - && (cls.derivesFrom(FunctionClass(paramTypes.length)) || isByNameFunctionClass(cls)) + && (cls.derivesFrom(FunctionSymbol(paramTypes.length)) || isByNameFunctionClass(cls)) && isSpecializableFunctionSAM(paramTypes, retType) /** If the Single Abstract Method of a Function class has this type, is it specializable? */ diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index cb590e2384a0..f2682621a7bd 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -314,8 +314,8 @@ object Flags { /** A Scala 2x super accessor / an unpickled Scala 2.x class */ val (SuperParamAliasOrScala2x @ _, SuperParamAlias @ _, Scala2x @ _) = newFlags(26, "", "") - /** A parameter with a default value */ - val (_, HasDefault @ _, _) = newFlags(27, "") + /** A parameter with a default value / an impure untpd.Function type */ + val (_, HasDefault @ _, Impure @ _) = newFlags(27, "", "<{*}>") /** An extension method, or a collective extension instance */ val (Extension @ _, ExtensionMethod @ _, _) = newFlags(28, "") diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index 98fcf0d9e303..1cf6009e5b8d 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -199,20 +199,25 @@ object NameOps { else collectDigits(acc * 10 + d, idx + 1) collectDigits(0, suffixStart + 8) - /** name[0..suffixStart) == `str` */ - private def isPreceded(str: String, suffixStart: Int) = - str.length == suffixStart && name.firstPart.startsWith(str) + private def isFunctionPrefix(suffixStart: Int, mustHave: String = ""): Boolean = + suffixStart >= 0 + && { + val first = name.firstPart + var found = mustHave.isEmpty + def skip(idx: Int, str: String) = + if first.startsWith(str, idx) then + if str == mustHave then found = true + idx + str.length + else idx + skip(skip(skip(0, "Impure"), "Erased"), "Context") == suffixStart + && found + } /** Same as `funArity`, except that it returns -1 if the prefix * is not one of "", "Context", "Erased", "ErasedContext" */ private def checkedFunArity(suffixStart: Int): Int = - if suffixStart == 0 - || isPreceded("Context", suffixStart) - || isPreceded("Erased", suffixStart) - || isPreceded("ErasedContext", suffixStart) - then funArity(suffixStart) - else -1 + if isFunctionPrefix(suffixStart) then funArity(suffixStart) else -1 /** Is a function name, i.e one of FunctionXXL, FunctionN, ContextFunctionN, ErasedFunctionN, ErasedContextFunctionN for N >= 0 */ @@ -224,19 +229,14 @@ object NameOps { */ def isPlainFunction: Boolean = functionArity >= 0 - /** Is an context function name, i.e one of ContextFunctionN or ErasedContextFunctionN for N >= 0 - */ - def isContextFunction: Boolean = + /** Is a function name that contains `mustHave` as a substring */ + private def isSpecificFunction(mustHave: String): Boolean = val suffixStart = functionSuffixStart - (isPreceded("Context", suffixStart) || isPreceded("ErasedContext", suffixStart)) - && funArity(suffixStart) >= 0 + isFunctionPrefix(suffixStart, mustHave) && funArity(suffixStart) >= 0 - /** Is an erased function name, i.e. one of ErasedFunctionN, ErasedContextFunctionN for N >= 0 - */ - def isErasedFunction: Boolean = - val suffixStart = functionSuffixStart - (isPreceded("Erased", suffixStart) || isPreceded("ErasedContext", suffixStart)) - && funArity(suffixStart) >= 0 + def isContextFunction: Boolean = isSpecificFunction("Context") + def isErasedFunction: Boolean = isSpecificFunction("Erased") + def isImpureFunction: Boolean = isSpecificFunction("Impure") /** Is a synthetic function name, i.e. one of * - FunctionN for N > 22 diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 6a355d09f3cf..3bfb3a734083 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -726,6 +726,8 @@ object StdNames { val XOR : N = "^" val ZAND : N = "&&" val ZOR : N = "||" + val PUREARROW: N = "->" + val PURECTXARROW: N = "?->" // unary operators val UNARY_PREFIX: N = "unary_" diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index d43bb27a420c..320c2202f53b 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2502,7 +2502,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: TypeVar if tp1.isInstantiated => tp1.underlying & tp2 case CapturingType(parent1, refs1, _) => - if subCaptures(tp2.captureSet, refs1, frozenConstraint).isOK then + if subCaptures(tp2.captureSet, refs1, frozen = true).isOK then parent1 & tp2 else tp1.derivedCapturingType(parent1 & tp2, refs1) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 8c62c056c6b9..53a8196d927b 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -183,7 +183,7 @@ object Types { case _ => false } - /** Is this type a (possibly refined or applied or aliased) type reference + /** Is this type a (possibly refined, applied, aliased or annotated) type reference * to the given type symbol? * @sym The symbol to compare to. It must be a class symbol or abstract type. * It makes no sense for it to be an alias type because isRef would always @@ -204,9 +204,7 @@ object Types { case this1: TypeVar => this1.instanceOpt.isRef(sym, skipRefined) case this1: AnnotatedType => - this1 match - case CapturingType(_, _, _) => false - case _ => this1.parent.isRef(sym, skipRefined) + this1.parent.isRef(sym, skipRefined) case _ => false } diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 41cb62c72ee6..b884c8ffb93c 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1435,47 +1435,56 @@ object Parsers { * | InfixType * | CaptureSet Type * FunType ::= (MonoFunType | PolyFunType) - * MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type - * PolyFunType ::= HKTypeParamClause '=>' Type + * MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’ | ‘->’ | ‘?->’ ) Type + * PolyFunType ::= HKTypeParamClause ('=>' | ‘->’_) Type * FunTypeArgs ::= InfixType * | `(' [ [ ‘[using]’ ‘['erased'] FunArgType {`,' FunArgType } ] `)' * | '(' [ ‘[using]’ ‘['erased'] TypedFunParam {',' TypedFunParam } ')' * CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` * CaptureRef ::= Ident */ - def typ(): Tree = { + def typ(): Tree = val start = in.offset var imods = Modifiers() def functionRest(params: List[Tree]): Tree = val paramSpan = Span(start, in.lastOffset) atSpan(start, in.offset) { - if in.token == TLARROW then + var token = in.token + if in.isIdent(nme.PUREARROW) then + token = ARROW + else if in.isIdent(nme.PURECTXARROW) then + token = CTXARROW + else if token == TLARROW then if !imods.flags.isEmpty || params.isEmpty then syntaxError(em"illegal parameter list for type lambda", start) - in.token = ARROW - else - for case ValDef(_, tpt: ByNameTypeTree, _) <- params do - syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span) - in.nextToken() - return TermLambdaTypeTree(params.asInstanceOf[List[ValDef]], typ()) + token = ARROW + else if ctx.settings.Ycc.value then + // `=>` means impure function under -Ycc whereas `->` is a regular function. + // Without -Ycc they both mean regular function. + imods |= Impure - if in.token == CTXARROW then + if token == CTXARROW then in.nextToken() imods |= Given + else if token == ARROW || token == TLARROW then + in.nextToken() else accept(ARROW) - val t = typ() - if imods.isOneOf(Given | Erased) then + val resultType = typ() + if token == TLARROW then + for case ValDef(_, tpt: ByNameTypeTree, _) <- params do + syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span) + TermLambdaTypeTree(params.asInstanceOf[List[ValDef]], resultType) + else if imods.isOneOf(Given | Erased | Impure) then if imods.is(Given) && params.isEmpty then syntaxError("context function types require at least one parameter", paramSpan) - new FunctionWithMods(params, t, imods) + FunctionWithMods(params, resultType, imods) else if !ctx.settings.YkindProjector.isDefault then - val (newParams :+ newT, tparams) = replaceKindProjectorPlaceholders(params :+ t): @unchecked - - lambdaAbstract(tparams, Function(newParams, newT)) + val (newParams :+ newResultType, tparams) = replaceKindProjectorPlaceholders(params :+ resultType): @unchecked + lambdaAbstract(tparams, Function(newParams, newResultType)) else - Function(params, t) + Function(params, resultType) } var isValParamList = false @@ -1527,7 +1536,7 @@ object Parsers { val tparams = typeParamClause(ParamOwner.TypeParam) if (in.token == TLARROW) atSpan(start, in.skipToken())(LambdaTypeTree(tparams, toplevelTyp())) - else if (in.token == ARROW) { + else if (in.token == ARROW || in.isIdent(nme.PUREARROW)) { val arrowOffset = in.skipToken() val body = toplevelTyp() atSpan(start, arrowOffset) { @@ -1548,16 +1557,18 @@ object Parsers { else if (in.token == INDENT) enclosed(INDENT, typ()) else infixType() - in.token match { + in.token match case ARROW | CTXARROW => functionRest(t :: Nil) case MATCH => matchType(t) case FORSOME => syntaxError(ExistentialTypesNoLongerSupported()); t case _ => - if (imods.is(Erased) && !t.isInstanceOf[FunctionWithMods]) - syntaxError(ErasedTypesCanOnlyBeFunctionTypes(), implicitKwPos(start)) - t - } - } + if isIdent(nme.PUREARROW) || isIdent(nme.PURECTXARROW) then + functionRest(t :: Nil) + else + if (imods.is(Erased) && !t.isInstanceOf[FunctionWithMods]) + syntaxError(ErasedTypesCanOnlyBeFunctionTypes(), implicitKwPos(start)) + t + end typ private def makeKindProjectorTypeDef(name: TypeName): TypeDef = { val isVarianceAnnotated = name.startsWith("+") || name.startsWith("-") @@ -1614,7 +1625,7 @@ object Parsers { def infixTypeRest(t: Tree): Tree = infixOps(t, canStartInfixTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type, - isOperator = !followingIsVararg()) + isOperator = !followingIsVararg() && !isIdent(nme.PUREARROW) && !isIdent(nme.PURECTXARROW)) /** RefinedType ::= WithType {[nl] Refinement} */ diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index d75d160a2574..42fa8edd2ade 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 -import cc.{CapturingType, CaptureSet} +import cc.{EventuallyCapturingType, CaptureSet} class PlainPrinter(_ctx: Context) extends Printer { @@ -143,6 +143,9 @@ class PlainPrinter(_ctx: Context) extends Printer { + defn.ObjectClass + defn.FromJavaObjectSymbol + def toText(cs: CaptureSet): Text = + "{" ~ Text(cs.elems.toList.map(toTextCaptureRef), ", ") ~ "}" + def toText(tp: Type): Text = controlled { homogenize(tp) match { case tp: TypeType => @@ -197,7 +200,7 @@ class PlainPrinter(_ctx: Context) extends Printer { keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~ (" <: " ~ toText(bound) provided !bound.isAny) }.close - case CapturingType(parent, refs, boxed) => + case EventuallyCapturingType(parent, refs, boxed) => def box = Str("box ") provided boxed if printDebug && !refs.isConst then changePrec(GlobalPrec)(box ~ s"$refs " ~ toText(parent)) @@ -206,11 +209,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if !refs.isConst && refs.elems.isEmpty then changePrec(GlobalPrec)("?" ~ " " ~ toText(parent)) else if Config.printCaptureSetsAsPrefix then - changePrec(GlobalPrec)( - box ~ "{" - ~ Text(refs.elems.toList.map(toTextCaptureRef), ", ") - ~ "} " - ~ toText(parent)) + changePrec(GlobalPrec)(box ~ toText(refs) ~ " " ~ toText(parent)) else changePrec(InfixPrec)(toText(parent) ~ " retains " ~ box ~ toText(refs.toRetainsTypeArg)) case tp: PreviousErrorType if ctx.settings.XprintTypes.value => diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 9230ec146647..4f1eaa9eaf9f 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -15,8 +15,8 @@ import Annotations.Annotation import Denotations._ import SymDenotations._ import StdNames.{nme, tpnme} -import ast.{Trees, untpd} -import typer.{Implicits, Namer} +import ast.{Trees, tpd, untpd} +import typer.{Implicits, Namer, Applications} import typer.ProtoTypes._ import Trees._ import TypeApplications._ @@ -24,9 +24,11 @@ import NameKinds.{WildcardParamName, DefaultGetterName} import util.Chars.isOperatorPart import transform.TypeUtils._ import transform.SymUtils._ +import config.Config import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} +import cc.{EventuallyCapturingType, CaptureSet, toCaptureSet, IllegalCaptureRef} class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { @@ -134,14 +136,14 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { else simpleNameString(tsym) } - private def arrow(isGiven: Boolean): String = - if isGiven then "?=>" else "=>" + private def arrow(isGiven: Boolean, isPure: Boolean): String = + (if isGiven then "?" else "") + (if isPure then "->" else "=>") override def toText(tp: Type): Text = controlled { def toTextTuple(args: List[Type]): Text = "(" ~ argsText(args) ~ ")" - def toTextFunction(args: List[Type], isGiven: Boolean, isErased: Boolean): Text = + def toTextFunction(args: List[Type], isGiven: Boolean, isErased: Boolean, isPure: Boolean): Text = changePrec(GlobalPrec) { val argStr: Text = if args.length == 2 @@ -154,26 +156,26 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ keywordText("erased ").provided(isErased) ~ argsText(args.init) ~ ")" - argStr ~ " " ~ arrow(isGiven) ~ " " ~ argText(args.last) + argStr ~ " " ~ arrow(isGiven, isPure) ~ " " ~ argText(args.last) } - def toTextMethodAsFunction(info: Type): Text = info match + def toTextMethodAsFunction(info: Type, isPure: Boolean): Text = info match case info: MethodType => changePrec(GlobalPrec) { "(" ~ keywordText("erased ").provided(info.isErasedMethod) ~ paramsText(info) ~ ") " - ~ arrow(info.isImplicitMethod) + ~ arrow(info.isImplicitMethod, isPure) ~ " " - ~ toTextMethodAsFunction(info.resultType) + ~ toTextMethodAsFunction(info.resultType, isPure) } case info: PolyType => changePrec(GlobalPrec) { "[" ~ paramsText(info) ~ "] => " - ~ toTextMethodAsFunction(info.resultType) + ~ toTextMethodAsFunction(info.resultType, isPure) } case _ => toText(info) @@ -215,9 +217,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { tp.tupleElementTypes match case Some(types) if types.size >= 2 && !printDebug => toTextTuple(types) case _ => - val cls = tycon.typeSymbol + val tsym = tycon.typeSymbol if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*" - else if defn.isFunctionClass(cls) then toTextFunction(args, cls.name.isContextFunction, cls.name.isErasedFunction) + else if defn.isFunctionSymbol(tsym) then + toTextFunction(args, tsym.name.isContextFunction, tsym.name.isErasedFunction, + isPure = ctx.settings.Ycc.value && !tsym.name.isImpureFunction) else if isInfixType(tp) then val l :: r :: Nil = args: @unchecked val opName = tyconName(tycon) @@ -243,7 +247,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { // don't eta contract if the application would be printed specially toText(tycon) case tp: RefinedType if defn.isFunctionOrPolyType(tp) && !printDebug => - toTextMethodAsFunction(tp.refinedInfo) + toTextMethodAsFunction(tp.refinedInfo, + isPure = ctx.settings.Ycc.value && !tp.typeSymbol.name.isImpureFunction) case tp: TypeRef => if (tp.symbol.isAnonymousClass && !showUniqueIds) toText(tp.info) @@ -259,7 +264,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case tp: ClassInfo => if tp.cls.derivesFrom(defn.PolyFunctionClass) then tp.member(nme.apply).info match - case info: PolyType => toTextMethodAsFunction(info) + case info: PolyType => toTextMethodAsFunction(info, isPure = false) case _ => toTextParents(tp.parents) ~~ "{...}" else toTextParents(tp.parents) ~~ "{...}" case JavaArrayType(elemtp) => @@ -527,7 +532,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { changePrec(OrTypePrec) { toText(args(0)) ~ " | " ~ atPrec(OrTypePrec + 1) { toText(args(1)) } } else if (tpt.symbol == defn.andType && args.length == 2) changePrec(AndTypePrec) { toText(args(0)) ~ " & " ~ atPrec(AndTypePrec + 1) { toText(args(1)) } } - else if defn.isFunctionClass(tpt.symbol) + else if defn.isFunctionSymbol(tpt.symbol) && tpt.isInstanceOf[TypeTree] && tree.hasType && !printDebug then changePrec(GlobalPrec) { toText(tree.typeOpt) } else args match @@ -602,7 +607,17 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { case tree: Template => toTextTemplate(tree) case Annotated(arg, annot) => - toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot) + def captureSet = + annot.asInstanceOf[tpd.Tree].toCaptureSet + def toTextAnnot = + toTextLocal(arg) ~~ annotText(annot.symbol.enclosingClass, annot) + def toTextRetainsAnnot = + try changePrec(GlobalPrec)(toText(captureSet) ~ " " ~ toText(arg)) + catch case ex: IllegalCaptureRef => toTextAnnot + if annot.symbol.maybeOwner == defn.RetainsAnnot + && ctx.settings.Ycc.value && Config.printCaptureSetsAsPrefix && !printDebug + then toTextRetainsAnnot + else toTextAnnot case EmptyTree => "" case TypedSplice(t) => @@ -645,7 +660,12 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { ~ Text(args.map(argToText), ", ") ~ ")" } - argsText ~ " " ~ arrow(isGiven) ~ " " ~ toText(body) + val isPure = + ctx.settings.Ycc.value + && tree.match + case tree: FunctionWithMods => !tree.mods.is(Impure) + case _ => true + argsText ~ " " ~ arrow(isGiven, isPure) ~ " " ~ toText(body) case PolyFunction(targs, body) => val targsText = "[" ~ Text(targs.map((arg: Tree) => toText(arg)), ", ") ~ "]" changePrec(GlobalPrec) { diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 1f30dd989f3a..e78776cb0158 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -117,82 +117,150 @@ class CheckCaptures extends Recheck: override def transformType(tp: Type, inferred: Boolean, boxed: Boolean)(using Context): Type = - def addInnerVars(tp: Type): Type = tp match - case tp @ AppliedType(tycon, args) => - tp.derivedAppliedType(tycon, args.map(addVars(_, boxed = true))) - case tp @ RefinedType(core, rname, rinfo) => - val rinfo1 = addVars(rinfo) - if defn.isFunctionType(tp) then - rinfo1.toFunctionType(isJava = false, alwaysDependent = true) - else - tp.derivedRefinedType(addInnerVars(core), rname, rinfo1) - case tp: MethodType => - tp.derivedLambdaType( - paramInfos = tp.paramInfos.mapConserve(addVars(_)), - resType = addVars(tp.resType)) - case tp: PolyType => - tp.derivedLambdaType( - resType = addVars(tp.resType)) - case tp: ExprType => - tp.derivedExprType(resType = addVars(tp.resType)) - case _ => - tp - - def addFunctionRefinements(tp: Type): Type = tp match - case tp @ AppliedType(tycon, args) => - if defn.isNonRefinedFunction(tp) then - MethodType.companion( - isContextual = defn.isContextFunctionClass(tycon.classSymbol), - isErased = defn.isErasedFunctionClass(tycon.classSymbol) - )(args.init, addFunctionRefinements(args.last)) - .toFunctionType(isJava = false, alwaysDependent = true) - .showing(i"add function refinement $tp --> $result", capt) - else - tp.derivedAppliedType(tycon, args.map(addFunctionRefinements(_))) - case tp @ RefinedType(core, rname, rinfo) if !defn.isFunctionType(tp) => - tp.derivedRefinedType( - addFunctionRefinements(core), rname, addFunctionRefinements(rinfo)) - case tp: MethodOrPoly => - tp.derivedLambdaType(resType = addFunctionRefinements(tp.resType)) - case tp: ExprType => - tp.derivedExprType(resType = addFunctionRefinements(tp.resType)) - case _ => - tp - - /** Refine a possibly applied class type C where the class has tracked parameters - * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } - * where CV_1, ..., CV_n are fresh capture sets. + def depFun(tycon: Type, argTypes: List[Type], resType: Type): Type = + MethodType.companion( + isContextual = defn.isContextFunctionClass(tycon.classSymbol), + isErased = defn.isErasedFunctionClass(tycon.classSymbol) + )(argTypes, resType) + .toFunctionType(isJava = false, alwaysDependent = true) + + def box(tp: Type): Type = tp match + case CapturingType(parent, refs, false) => CapturingType(parent, refs, true) + case _ => tp + + /** Perform the following transformation steps everywhere in a type: + * 1. Drop retains annotations + * 2. Turn plain function types into dependent function types, so that + * we can refer to their parameter in capture sets. Currently this is + * only done at the toplevel, i.e. for function types that are not + * themselves argument types of other function types. Without this restriction + * boxmap-paper.scala fails. Need to figure out why. + * 3. Refine other class types C by adding capture set variables to their parameter getters + * (see addCaptureRefinements) + * 4. Add capture set variables to all types that can be tracked + * + * Polytype bounds are only cleaned using step 1, but not otherwise transformed. */ - def addCaptureRefinements(tp: Type): Type = tp.stripped match - case _: TypeRef | _: AppliedType if tp.typeSymbol.isClass => - val cls = tp.typeSymbol.asClass - cls.paramGetters.foldLeft(tp) { (core, getter) => - if getter.termRef.isTracked then - val getterType = tp.memberInfo(getter).strippedDealias - RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), boxed = false)) - .showing(i"add capture refinement $tp --> $result", capt) - else - core - } - case _ => - tp + def mapInferred = new TypeMap: - def addVars(tp: Type, boxed: Boolean = false): Type = - var tp1 = addInnerVars(tp) - val tp2 = addCaptureRefinements(tp1) - if tp1.canHaveInferredCapture - then CapturingType(tp2, CaptureSet.Var(), boxed) - else tp2 - - if inferred then - val cleanup = new TypeMap: + /** Drop @retains annotations everywhere */ + object cleanup extends TypeMap: def apply(t: Type) = t match case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => apply(parent) case _ => mapOver(t) - addVars(addFunctionRefinements(cleanup(tp)), boxed) - .showing(i"reinfer $tp --> $result", capt) + + /** Refine a possibly applied class type C where the class has tracked parameters + * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } + * where CV_1, ..., CV_n are fresh capture sets. + */ + def addCaptureRefinements(tp: Type): Type = tp match + case _: TypeRef | _: AppliedType if tp.typeParams.isEmpty => + tp.typeSymbol match + case cls: ClassSymbol if !defn.isFunctionClass(cls) => + cls.paramGetters.foldLeft(tp) { (core, getter) => + if getter.termRef.isTracked then + val getterType = tp.memberInfo(getter).strippedDealias + RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), boxed = false)) + .showing(i"add capture refinement $tp --> $result", capt) + else + core + } + case _ => tp + case _ => tp + + /** Should a capture set variable be added on type `tp`? */ + def canHaveInferredCapture(tp: Type): Boolean = + tp.typeParams.isEmpty && tp.match + case tp: (TypeRef | AppliedType) => + val sym = tp.typeSymbol + if sym.isClass then !sym.isValueClass && sym != defn.AnyClass + else canHaveInferredCapture(tp.superType.dealias) + case tp: (RefinedOrRecType | MatchType) => + canHaveInferredCapture(tp.underlying) + case tp: AndType => + canHaveInferredCapture(tp.tp1) && canHaveInferredCapture(tp.tp2) + case tp: OrType => + canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2) + case _ => + false + + /** Add a capture set variable to `tp` if necessary, or maybe pull out + * an embedded capture set variables from a part of `tp`. + */ + def addVar(tp: Type) = tp match + case tp @ RefinedType(parent @ CapturingType(parent1, refs, boxed), rname, rinfo) => + CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, boxed) + case tp: RecType => + tp.parent match + case CapturingType(parent1, refs, boxed) => + CapturingType(tp.derivedRecType(parent1), refs, boxed) + case _ => + tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created + // by `mapInferred`. Hence if the underlying type admits capture variables + // a variable was already added, and the first case above would apply. + case AndType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => + assert(refs1.asVar.elems.isEmpty) + assert(refs2.asVar.elems.isEmpty) + assert(boxed1 == boxed2) + CapturingType(AndType(parent1, parent2), refs1, boxed1) + case tp @ OrType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => + assert(refs1.asVar.elems.isEmpty) + assert(refs2.asVar.elems.isEmpty) + assert(boxed1 == boxed2) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1, boxed1) + case tp @ OrType(CapturingType(parent1, refs1, boxed1), tp2) => + CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, boxed1) + case tp @ OrType(tp1, CapturingType(parent2, refs2, boxed2)) => + CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, boxed2) + case _ if canHaveInferredCapture(tp) => + CapturingType(tp, CaptureSet.Var(), boxed = false) + case _ => + tp + + var isTopLevel = true + + def mapNested(ts: List[Type]): List[Type] = + val saved = isTopLevel + isTopLevel = false + try ts.mapConserve(this) finally isTopLevel = saved + + def apply(t: Type) = + val t1 = t match + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + apply(parent) + case tp @ AppliedType(tycon, args) => + val tycon1 = this(tycon) + if defn.isNonRefinedFunction(tp) then + val args1 = mapNested(args.init) + val res1 = this(args.last) + if isTopLevel then + depFun(tycon1, args1, res1) + .showing(i"add function refinement $tp --> $result", capt) + else + tp.derivedAppliedType(tycon1, args1 :+ res1) + else + tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) + case tp @ RefinedType(core, rname, rinfo) if defn.isFunctionType(tp) => + apply(rinfo).toFunctionType(isJava = false, alwaysDependent = true) + case tp: MethodType => + tp.derivedLambdaType( + paramInfos = mapNested(tp.paramInfos), + resType = this(tp.resType)) + case tp: TypeLambda => + // Don't recurse into parameter bounds, just cleanup any stray retains annotations + tp.derivedLambdaType( + paramInfos = tp.paramInfos.mapConserve(cleanup(_).bounds), + resType = this(tp.resType)) + case _ => + mapOver(t) + addVar(addCaptureRefinements(t1)) + end mapInferred + + if inferred then + val tp1 = mapInferred(tp) + if boxed then box(tp1) else tp1 else def setBoxed(t: Type) = t match case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index e8e49071bf2b..ac86702ac588 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -836,7 +836,7 @@ trait Implicits: def isOldStyleFunctionConversion(tpe: Type): Boolean = tpe match { case PolyType(_, resType) => isOldStyleFunctionConversion(resType) - case _ => tpe.derivesFrom(defn.FunctionClass(1)) && !tpe.derivesFrom(defn.ConversionClass) && !tpe.derivesFrom(defn.SubTypeClass) + case _ => tpe.derivesFrom(defn.FunctionSymbol(1)) && !tpe.derivesFrom(defn.ConversionClass) && !tpe.derivesFrom(defn.SubTypeClass) } try diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 796fe0dcf91b..0f54e887415a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1284,7 +1284,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val numArgs = args.length val isContextual = funFlags.is(Given) val isErased = funFlags.is(Erased) - val funCls = defn.FunctionClass(numArgs, isContextual, isErased) + val isImpure = funFlags.is(Impure) + val funSym = defn.FunctionSymbol(numArgs, isContextual, isErased, isImpure) /** If `app` is a function type with arguments that are all erased classes, * turn it into an erased function type. @@ -1294,7 +1295,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if !isErased && numArgs > 0 && args.indexWhere(!_.tpe.isErasedClass) == numArgs => - val tycon1 = TypeTree(defn.FunctionClass(numArgs, isContextual, isErased = true).typeRef) + val tycon1 = TypeTree(defn.FunctionSymbol(numArgs, isContextual, true, isImpure).typeRef) .withSpan(tycon.span) assignType(cpy.AppliedTypeTree(app)(tycon1, args), tycon1, args) case _ => @@ -1321,7 +1322,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer report.error(i"$mt is an illegal function type because it has inter-parameter dependencies", tree.srcPos) val resTpt = TypeTree(mt.nonDependentResultApprox).withSpan(body.span) val typeArgs = appDef.termParamss.head.map(_.tpt) :+ resTpt - val tycon = TypeTree(funCls.typeRef) + val tycon = TypeTree(funSym.typeRef) val core = propagateErased(AppliedTypeTree(tycon, typeArgs)) RefinedTypeTree(core, List(appDef), ctx.owner.asClass) end typedDependent @@ -1332,7 +1333,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer using ctx.fresh.setOwner(newRefinedClassSymbol(tree.span)).setNewScope) case _ => propagateErased( - typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funCls.typeRef), args :+ body), pt)) + typed(cpy.AppliedTypeTree(tree)(untpd.TypeTree(funSym.typeRef), args :+ body), pt)) } } diff --git a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala index 30f4dc29aeea..f8e439baeb0e 100644 --- a/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala +++ b/compiler/src/scala/quoted/runtime/impl/QuotesImpl.scala @@ -2729,7 +2729,7 @@ class QuotesImpl private (using val ctx: Context) extends Quotes, QuoteUnpickler def SomeModule: Symbol = dotc.core.Symbols.defn.SomeClass.companionModule def ProductClass: Symbol = dotc.core.Symbols.defn.ProductClass def FunctionClass(arity: Int, isImplicit: Boolean = false, isErased: Boolean = false): Symbol = - dotc.core.Symbols.defn.FunctionClass(arity, isImplicit, isErased) + dotc.core.Symbols.defn.FunctionSymbol(arity, isImplicit, isErased) def TupleClass(arity: Int): Symbol = dotc.core.Symbols.defn.TupleType(arity).nn.classSymbol.asClass def isTupleClass(sym: Symbol): Boolean = diff --git a/tests/neg-custom-args/capt-wf.scala b/tests/neg-custom-args/capt-wf.scala index dc4d6a0d4bff..3bd80e0d0f68 100644 --- a/tests/neg-custom-args/capt-wf.scala +++ b/tests/neg-custom-args/capt-wf.scala @@ -8,7 +8,7 @@ def test(c: Cap, other: String): Unit = val x2: {other} C = ??? // error: cs is empty val s1 = () => "abc" val x3: {s1} C = ??? // error: cs is empty - val x3a: () => String = s1 + val x3a: () -> String = s1 val s2 = () => if x1 == null then "" else "abc" val x4: {s2} C = ??? // OK val x5: {c, c} C = ??? // error: redundant @@ -26,8 +26,8 @@ def test(c: Cap, other: String): Unit = val y1: {e1} String = ??? // error cs is empty val y2: {o1} String = ??? // error cs is empty - lazy val ev: (Int => Boolean) = (n: Int) => - lazy val od: (Int => Boolean) = (n: Int) => + lazy val ev: (Int -> Boolean) = (n: Int) => + lazy val od: (Int -> Boolean) = (n: Int) => if n == 1 then true else ev(n - 1) if n == 0 then true else od(n - 1) val y3: {ev} String = ??? // error cs is empty diff --git a/tests/neg-custom-args/captures/bounded.scala b/tests/neg-custom-args/captures/bounded.scala index dc2621e95a65..fb6b198fb3a3 100644 --- a/tests/neg-custom-args/captures/bounded.scala +++ b/tests/neg-custom-args/captures/bounded.scala @@ -9,6 +9,6 @@ def test(c: Cap) = def f(x: Int): Int = if c == c then x else 0 val b = new B(f) val r1 = b.elem - val r1c: {c} Int => Int = r1 + val r1c: {c} Int -> Int = r1 val r2 = b.lateElem - val r2c: () => {c} Int => Int = r2 // error \ No newline at end of file + val r2c: () -> {c} Int -> Int = r2 // error \ No newline at end of file diff --git a/tests/neg-custom-args/captures/boxmap.check b/tests/neg-custom-args/captures/boxmap.check index 406077077af5..b3d6605989bf 100644 --- a/tests/neg-custom-args/captures/boxmap.check +++ b/tests/neg-custom-args/captures/boxmap.check @@ -1,7 +1,7 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boxmap.scala:14:2 ---------------------------------------- -14 | () => b[Box[B]]((x: A) => box(f(x))) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boxmap.scala:12:2 ---------------------------------------- +12 | () => b[Box[B]]((x: A) => box(f(x))) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: {f} () => ? Box[B] - | Required: () => Box[B] + | Found: {f} () -> ? Box[B] + | Required: () -> Box[B] longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/boxmap.scala b/tests/neg-custom-args/captures/boxmap.scala index e335320ef9d4..114aaccb6bb5 100644 --- a/tests/neg-custom-args/captures/boxmap.scala +++ b/tests/neg-custom-args/captures/boxmap.scala @@ -1,14 +1,12 @@ type Top = Any @retains(*) -infix type ==> [A, B] = (A => B) @retains(*) - -type Box[+T <: Top] = ([K <: Top] => (T ==> K) => K) +type Box[+T <: Top] = ([K <: Top] -> (T => K) -> K) def box[T <: Top](x: T): Box[T] = - [K <: Top] => (k: T ==> K) => k(x) + [K <: Top] => (k: T => K) => k(x) -def map[A <: Top, B <: Top](b: Box[A])(f: A ==> B): Box[B] = +def map[A <: Top, B <: Top](b: Box[A])(f: A => B): Box[B] = b[Box[B]]((x: A) => box(f(x))) -def lazymap[A <: Top, B <: Top](b: Box[A])(f: A ==> B): () => Box[B] = +def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B): () -> Box[B] = () => b[Box[B]]((x: A) => box(f(x))) // error diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala index ef5876be2c11..feb9461dc4c7 100644 --- a/tests/neg-custom-args/captures/byname.scala +++ b/tests/neg-custom-args/captures/byname.scala @@ -3,7 +3,7 @@ def test(cap1: Cap, cap2: Cap) = def f() = if cap1 == cap1 then g else g def g(x: Int) = if cap2 == cap2 then 1 else x - def h(ff: => {cap2} Int => Int) = ff + def h(ff: => {cap2} Int -> Int) = ff h(f()) // error diff --git a/tests/neg-custom-args/captures/capt-box-env.scala b/tests/neg-custom-args/captures/capt-box-env.scala index e9743054076e..605b446d5262 100644 --- a/tests/neg-custom-args/captures/capt-box-env.scala +++ b/tests/neg-custom-args/captures/capt-box-env.scala @@ -1,5 +1,4 @@ -class C -type Cap = {*} C +@annotation.capability class Cap class Pair[+A, +B](x: A, y: B): def fst: A = x @@ -9,4 +8,4 @@ def test(c: Cap) = def f(x: Cap): Unit = if c == x then () val p = Pair(f, f) val g = () => p.fst == p.snd - val gc: () => Boolean = g // error + val gc: () -> Boolean = g // error diff --git a/tests/neg-custom-args/captures/capt-box.scala b/tests/neg-custom-args/captures/capt-box.scala index 317fc064ec0b..634470704fc5 100644 --- a/tests/neg-custom-args/captures/capt-box.scala +++ b/tests/neg-custom-args/captures/capt-box.scala @@ -1,6 +1,4 @@ -//import scala.retains -class C -type Cap = {*} C +@annotation.capability class Cap def test(x: Cap) = @@ -10,4 +8,4 @@ def test(x: Cap) = val x2 = identity(x1) - val x3: Cap => Unit = x2 // error \ No newline at end of file + val x3: Cap -> Unit = x2 // error \ No newline at end of file diff --git a/tests/neg-custom-args/captures/capt-env.scala b/tests/neg-custom-args/captures/capt-env.scala index 84b4b57a7930..52fa4abfdaa8 100644 --- a/tests/neg-custom-args/captures/capt-env.scala +++ b/tests/neg-custom-args/captures/capt-env.scala @@ -9,5 +9,5 @@ def test(c: Cap) = def f(x: Cap): Unit = if c == x then () val p = Pair(f, f) val g = () => p.fst == p.snd - val gc: () => Boolean = g // error + val gc: () -> Boolean = g // error diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index ce7c4833bf9c..0b99f1bac09e 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,21 +1,21 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:3:2 ------------------------------------------ 3 | () => if x == null then y else y // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: {x} () => ? C - | Required: () => C + | Found: {x} () -> ? C + | Required: () -> C longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:6:2 ------------------------------------------ 6 | () => if x == null then y else y // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: {x} () => ? C + | Found: {x} () -> ? C | Required: Matchable longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:13:2 ----------------------------------------- 13 | def f(y: Int) = if x == null then y else y // error | ^ - | Found: {x} Int => Int + | Found: {x} Int -> Int | Required: Matchable 14 | f @@ -38,9 +38,9 @@ longer explanation available when compiling with `-explain` longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:31:24 ---------------------------------------- -31 | val z2 = h[() => Cap](() => x)(() => C()) // error +31 | val z2 = h[() -> Cap](() => x)(() => C()) // error | ^^^^^^^ - | Found: {x} () => ? Cap - | Required: () => Cap + | Found: {x} () -> Cap + | Required: () -> Cap longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index 4da49c5f4f1e..e230defda170 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -1,5 +1,5 @@ class C -def f(x: C @retains(*), y: C): () => C = +def f(x: C @retains(*), y: C): () -> C = () => if x == null then y else y // error def g(x: C @retains(*), y: C): Matchable = @@ -28,7 +28,7 @@ def h4(x: Cap, y: Int): A = def foo() = val x: C @retains(*) = ??? def h[X](a: X)(b: X) = a - val z2 = h[() => Cap](() => x)(() => C()) // error - val z3 = h[(() => Cap) @retains(x)](() => x)(() => C()) // ok - val z4 = h[(() => Cap) @retains(x)](() => x)(() => C()) // what was inferred for z3 + val z2 = h[() -> Cap](() => x)(() => C()) // error + val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // ok + val z4 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // what was inferred for z3 diff --git a/tests/neg-custom-args/captures/capt2.scala b/tests/neg-custom-args/captures/capt2.scala index 1eee53463f6d..8b08832dfdb9 100644 --- a/tests/neg-custom-args/captures/capt2.scala +++ b/tests/neg-custom-args/captures/capt2.scala @@ -2,8 +2,8 @@ class C type Cap = {*} C -def f1(c: Cap): (() => {c} C) = () => c // error, but would be OK under capture abbreciations for funciton types -def f2(c: Cap): ({c} () => C) = () => c // error +def f1(c: Cap): (() -> {c} C) = () => c // error, but would be OK under capture abbreciations for funciton types +def f2(c: Cap): ({c} () -> C) = () => c // error -def h5(x: Cap): () => C = +def h5(x: Cap): () -> C = f1(x) // error diff --git a/tests/neg-custom-args/captures/capt3.scala b/tests/neg-custom-args/captures/capt3.scala index 80b937276f73..6e9ea02fe8e3 100644 --- a/tests/neg-custom-args/captures/capt3.scala +++ b/tests/neg-custom-args/captures/capt3.scala @@ -5,22 +5,22 @@ def test1() = val x: Cap = C() val y = () => { x; () } val z = y - z: (() => Unit) // error + z: (() -> Unit) // error def test2() = val x: Cap = C() def y = () => { x; () } def z = y - z: (() => Unit) // error + z: (() -> Unit) // error def test3() = val x: Cap = C() def y = () => { x; () } val z = y - z: (() => Unit) // error + z: (() -> Unit) // error def test4() = val x: Cap = C() val y = () => { x; () } def z = y - z: (() => Unit) // error + z: (() -> Unit) // error diff --git a/tests/neg-custom-args/captures/io.scala b/tests/neg-custom-args/captures/io.scala index 17c22a2111e4..c0cb11686b32 100644 --- a/tests/neg-custom-args/captures/io.scala +++ b/tests/neg-custom-args/captures/io.scala @@ -4,13 +4,13 @@ sealed trait IO: def test1 = val IO : IO @retains(*) = new IO {} def foo = {IO; IO.puts("hello") } - val x : () => Unit = () => foo // error: Found: (() => Unit) retains IO; Required: () => Unit + val x : () -> Unit = () => foo // error: Found: (() -> Unit) retains IO; Required: () -> Unit def test2 = val IO : IO @retains(*) = new IO {} def puts(msg: Any, io: IO @retains(*)) = println(msg) def foo() = puts("hello", IO) - val x : () => Unit = () => foo() // error: Found: (() => Unit) retains IO; Required: () => Unit + val x : () -> Unit = () => foo() // error: Found: (() -> Unit) retains IO; Required: () -> Unit type Capability[T] = T @retains(*) @@ -18,5 +18,5 @@ def test3 = val IO : Capability[IO] = new IO {} def puts(msg: Any, io: Capability[IO]) = println(msg) def foo() = puts("hello", IO) - val x : () => Unit = () => foo() // error: Found: (() => Unit) retains IO; Required: () => Unit + val x : () -> Unit = () => foo() // error: Found: (() -> Unit) retains IO; Required: () -> Unit diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 3a80de9bdf16..0de190df8f11 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -8,7 +8,7 @@ longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:35:29 ------------------------------------- 35 | val ref1c: LazyList[Int] = ref1 // error | ^^^^ - | Found: (ref1 : {cap1} lazylists.LazyCons[Int]{xs: {cap1} () => {*} lazylists.LazyList[Int]}) + | Found: (ref1 : {cap1} lazylists.LazyCons[Int]{xs: {cap1} () -> {*} lazylists.LazyList[Int]}) | Required: lazylists.LazyList[Int] longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylist.scala b/tests/neg-custom-args/captures/lazylist.scala index f7be43e8dc27..56bfc3ea6da2 100644 --- a/tests/neg-custom-args/captures/lazylist.scala +++ b/tests/neg-custom-args/captures/lazylist.scala @@ -7,11 +7,11 @@ abstract class LazyList[+T]: def head: T def tail: LazyList[T] - def map[U](f: {*} T => U): {f, this} LazyList[U] = + def map[U](f: T => U): {f, this} LazyList[U] = if isEmpty then LazyNil else LazyCons(f(head), () => tail.map(f)) -class LazyCons[+T](val x: T, val xs: {*} () => {*} LazyList[T]) extends LazyList[T]: +class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: def isEmpty = false def head = x def tail = xs() // error: cannot have an inferred type @@ -21,7 +21,7 @@ object LazyNil extends LazyList[Nothing]: def head = ??? def tail: {*} LazyList[Nothing] = ??? // error overriding -def map[A, B](xs: {*} LazyList[A], f: {*} A => B): {f, xs} LazyList[B] = +def map[A, B](xs: {*} LazyList[A], f: A => B): {f, xs} LazyList[B] = xs.map(f) class CC diff --git a/tests/neg-custom-args/captures/lazylists1.scala b/tests/neg-custom-args/captures/lazylists1.scala index 02c7cb4ff3e5..4091ee2c62ae 100644 --- a/tests/neg-custom-args/captures/lazylists1.scala +++ b/tests/neg-custom-args/captures/lazylists1.scala @@ -14,7 +14,7 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? extension [A](xs: {*} LazyList[A]) - def map[B](f: {*} A => B): {xs, f} LazyList[B] = + def map[B](f: A => B): {xs, f} LazyList[B] = final class Mapped extends LazyList[B]: this: ({xs, f} Mapped) => diff --git a/tests/neg-custom-args/captures/lazylists2.scala b/tests/neg-custom-args/captures/lazylists2.scala index c31a1ae5d04f..b9ebb0a7a9f0 100644 --- a/tests/neg-custom-args/captures/lazylists2.scala +++ b/tests/neg-custom-args/captures/lazylists2.scala @@ -14,7 +14,7 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? extension [A](xs: {*} LazyList[A]) - def map[B](f: {*} A => B): {f} LazyList[B] = + def map[B](f: A => B): {f} LazyList[B] = final class Mapped extends LazyList[B]: // error this: ({xs, f} Mapped) => @@ -23,7 +23,7 @@ extension [A](xs: {*} LazyList[A]) def tail: {this} LazyList[B] = xs.tail.map(f) new Mapped - def map2[B](f: {*} A => B): {xs} LazyList[B] = + def map2[B](f: A => B): {xs} LazyList[B] = final class Mapped extends LazyList[B]: // error this: ({xs, f} Mapped) => @@ -32,7 +32,7 @@ extension [A](xs: {*} LazyList[A]) def tail: {this} LazyList[B] = xs.tail.map(f) new Mapped - def map3[B](f: {*} A => B): {xs} LazyList[B] = + def map3[B](f: A => B): {xs} LazyList[B] = final class Mapped extends LazyList[B]: this: ({xs} Mapped) => @@ -41,7 +41,7 @@ extension [A](xs: {*} LazyList[A]) def tail: {this} LazyList[B] = xs.tail.map(f) // error new Mapped - def map4[B](f: {*} A => B): {xs} LazyList[B] = + def map4[B](f: A => B): {xs} LazyList[B] = final class Mapped extends LazyList[B]: this: ({xs, f} Mapped) => @@ -50,7 +50,7 @@ extension [A](xs: {*} LazyList[A]) def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error new Mapped - def map5[B](f: {*} A => B): LazyList[B] = + def map5[B](f: A => B): LazyList[B] = class Mapped extends LazyList[B]: this: ({xs, f} Mapped) => diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index 2affed020dec..e4e06f8c52cb 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -1,28 +1,28 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:19:28 -------------------------------------- 19 | val ref1c: LazyRef[Int] = ref1 // error | ^^^^ - | Found: (ref1 : {cap1} LazyRef[Int]{elem: {cap1} () => Int}) + | Found: (ref1 : {cap1} LazyRef[Int]{elem: {cap1} () -> Int}) | Required: LazyRef[Int] longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- 21 | val ref2c: {cap2} LazyRef[Int] = ref2 // error | ^^^^ - | Found: (ref2 : {cap2, ref1} LazyRef[Int]{elem: {*} () => Int}) + | Found: (ref2 : {cap2, ref1} LazyRef[Int]{elem: {*} () -> Int}) | Required: {cap2} LazyRef[Int] longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- 23 | val ref3c: {ref1} LazyRef[Int] = ref3 // error | ^^^^ - | Found: (ref3 : {cap2, ref1} LazyRef[Int]{elem: {*} () => Int}) + | Found: (ref3 : {cap2, ref1} LazyRef[Int]{elem: {*} () -> Int}) | Required: {ref1} LazyRef[Int] longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- 25 | val ref4c: {cap1} LazyRef[Int] = ref4 // error | ^^^^ - | Found: (ref4 : {cap2, cap1} LazyRef[Int]{elem: {*} () => Int}) + | Found: (ref4 : {cap2, cap1} LazyRef[Int]{elem: {*} () -> Int}) | Required: {cap1} LazyRef[Int] longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 1002f9685675..2b278fd51a43 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -1,15 +1,15 @@ class CC type Cap = {*} CC -class LazyRef[T](val elem: {*} () => T): +class LazyRef[T](val elem: () => T): val get = elem - def map[U](f: {*} T => U): {f, this} LazyRef[U] = + def map[U](f: T => U): {f, this} LazyRef[U] = new LazyRef(() => f(elem())) -def map[A, B](ref: {*} LazyRef[A], f: {*} A => B): {f, ref} LazyRef[B] = +def map[A, B](ref: {*} LazyRef[A], f: A => B): {f, ref} LazyRef[B] = new LazyRef(() => f(ref.elem())) -def mapc[A, B]: (ref: {*} LazyRef[A], f: {*} A => B) => {f, ref} LazyRef[B] = +def mapc[A, B]: (ref: {*} LazyRef[A], f: A => B) -> {f, ref} LazyRef[B] = (ref1, f1) => map[A, B](ref1, f1) def test(cap1: Cap, cap2: Cap) = diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index bd95835c6525..a2fe96016b80 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,8 +1,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:28:43 ------------------------------------------ -28 | val b = handle[Exception, () => Nothing] { // error +28 | val b = handle[Exception, () -> Nothing] { // error | ^ - | Found: ? (x: CanThrow[Exception]) => {x} () => ? Nothing - | Required: CanThrow[Exception] => () => Nothing + | Found: ? (x: CanThrow[Exception]) -> {x} () -> ? Nothing + | Required: CanThrow[Exception] => () -> Nothing 29 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) 30 | } { @@ -14,12 +14,12 @@ longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/try.scala:34:11 --------------------------------------------------------------- 34 | val xx = handle { // error | ^^^^^^ - | inferred type argument {*} () => Int is not allowed to capture the universal capability (* : Any) + | inferred type argument {*} () -> Int is not allowed to capture the universal capability (* : Any) | - | The inferred arguments are: [? Exception, {*} () => Int] + | The inferred arguments are: [? Exception, {*} () -> Int] -- Error: tests/neg-custom-args/captures/try.scala:46:13 --------------------------------------------------------------- 46 |val global = handle { // error | ^^^^^^ - | inferred type argument {*} () => Int is not allowed to capture the universal capability (* : Any) + | inferred type argument {*} () -> Int is not allowed to capture the universal capability (* : Any) | - | The inferred arguments are: [? Exception, {*} () => Int] + | The inferred arguments are: [? Exception, {*} () -> Int] diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 804a16192be0..b128f82a2a3c 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -25,7 +25,7 @@ def test = (ex: Exception) => ??? } - val b = handle[Exception, () => Nothing] { // error + val b = handle[Exception, () -> Nothing] { // error (x: CanThrow[Exception]) => () => raise(new Exception)(using x) } { (ex: Exception) => ??? diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 4eab5b6b2b3a..0df38b918862 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -1,21 +1,21 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:11:24 ----------------------------------------- -11 | val z2c: () => Unit = z2 // error +11 | val z2c: () -> Unit = z2 // error | ^^ - | Found: (z2 : {x, cap1} () => Unit) - | Required: () => Unit + | Found: (z2 : {x, cap1} () -> Unit) + | Required: () -> Unit longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/vars.scala:13:10 -------------------------------------------------------------- -13 | var a: {*} String => String = f // error - | ^^^^^^^^^^^^^^^^^^^ - | type of mutable variable box {*} String => String is not allowed to capture the universal capability (* : Any) +-- Error: tests/neg-custom-args/captures/vars.scala:13:16 -------------------------------------------------------------- +13 | var a: String => String = f // error + | ^^^^^^^^^^^^^^^^ + | type of mutable variable String => String is not allowed to capture the universal capability (* : Any) -- Error: tests/neg-custom-args/captures/vars.scala:14:9 --------------------------------------------------------------- -14 | var b: List[{*} String => String] = Nil // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ - |type of mutable variable List[box {*} String => String] is not allowed to capture the universal capability (* : Any) +14 | var b: List[String => String] = Nil // error + | ^^^^^^^^^^^^^^^^^^^^^^ + | type of mutable variable List[String => String] is not allowed to capture the universal capability (* : Any) -- Error: tests/neg-custom-args/captures/vars.scala:29:2 --------------------------------------------------------------- 29 | local { cap3 => // error | ^^^^^ - |inferred type argument {*} (x$0: ? String) => ? String is not allowed to capture the universal capability (* : Any) + |inferred type argument {*} (x$0: ? String) -> ? String is not allowed to capture the universal capability (* : Any) | - |The inferred arguments are: [{*} (x$0: ? String) => ? String] + |The inferred arguments are: [{*} (x$0: ? String) -> ? String] diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index 4a58f79932b3..e85bcbe2db04 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -6,12 +6,12 @@ def test(cap1: Cap, cap2: Cap) = var x = f val y = x val z = () => if x("") == "" then "a" else "b" - val zc: {cap1} () => String = z + val zc: {cap1} () -> String = z val z2 = () => { x = identity } - val z2c: () => Unit = z2 // error + val z2c: () -> Unit = z2 // error - var a: {*} String => String = f // error - var b: List[{*} String => String] = Nil // error + var a: String => String = f // error + var b: List[String => String] = Nil // error def scope = val cap3: Cap = CC() @@ -22,9 +22,9 @@ def test(cap1: Cap, cap2: Cap) = g val s = scope - val sc: {*} String => String = scope + val sc: String => String = scope - def local[T](op: Cap => T): T = op(CC()) + def local[T](op: Cap -> T): T = op(CC()) local { cap3 => // error def g(x: String): String = if cap3 == cap3 then "" else "a" @@ -32,7 +32,7 @@ def test(cap1: Cap, cap2: Cap) = } class Ref: - var elem: {cap1} String => String = null + var elem: {cap1} String -> String = null val r = Ref() r.elem = f diff --git a/tests/pos-custom-args/captures/bounded.scala b/tests/pos-custom-args/captures/bounded.scala index fad0b50c2137..85c1a67387b5 100644 --- a/tests/pos-custom-args/captures/bounded.scala +++ b/tests/pos-custom-args/captures/bounded.scala @@ -9,6 +9,6 @@ def test(c: Cap) = def f(x: Int): Int = if c == c then x else 0 val b = new B(f) val r1 = b.elem - val r1c: {c} Int => Int = r1 + val r1c: {c} Int -> Int = r1 val r2 = b.lateElem - val r2c: {c} () => {c} Int => Int = r2 \ No newline at end of file + val r2c: {c} () -> {c} Int -> Int = r2 \ No newline at end of file diff --git a/tests/pos-custom-args/captures/boxmap-paper.scala b/tests/pos-custom-args/captures/boxmap-paper.scala index ed8c648526d1..aff4c38e1b9d 100644 --- a/tests/pos-custom-args/captures/boxmap-paper.scala +++ b/tests/pos-custom-args/captures/boxmap-paper.scala @@ -1,19 +1,18 @@ -infix type ==> [A, B] = {*} (A => B) -type Cell[+T] = [K] => (T ==> K) => K +type Cell[+T] = [K] -> (T => K) -> K def cell[T](x: T): Cell[T] = - [K] => (k: T ==> K) => k(x) + [K] => (k: T => K) => k(x) def get[T](c: Cell[T]): T = c[T](identity) -def map[A, B](c: Cell[A])(f: A ==> B): Cell[B] +def map[A, B](c: Cell[A])(f: A => B): Cell[B] = c[Cell[B]]((x: A) => cell(f(x))) -def pureMap[A, B](c: Cell[A])(f: A => B): Cell[B] +def pureMap[A, B](c: Cell[A])(f: A -> B): Cell[B] = c[Cell[B]]((x: A) => cell(f(x))) -def lazyMap[A, B](c: Cell[A])(f: A ==> B): {f} () => Cell[B] +def lazyMap[A, B](c: Cell[A])(f: A => B): {f} () -> Cell[B] = () => c[Cell[B]]((x: A) => cell(f(x))) trait IO: @@ -21,17 +20,17 @@ trait IO: def test(io: {*} IO) = - val loggedOne: {io} () => Int = () => { io.print("1"); 1 } + val loggedOne: {io} () -> Int = () => { io.print("1"); 1 } - val c: Cell[{io} () => Int] - = cell[{io} () => Int](loggedOne) + val c: Cell[{io} () -> Int] + = cell[{io} () -> Int](loggedOne) - val g = (f: {io} () => Int) => + val g = (f: {io} () -> Int) => val x = f(); io.print(" + ") val y = f(); io.print(s" = ${x + y}") - val r = lazyMap[{io} () => Int, Unit](c)(f => g(f)) - val r2 = lazyMap[{io} () => Int, Unit](c)(g) + val r = lazyMap[{io} () -> Int, Unit](c)(f => g(f)) + val r2 = lazyMap[{io} () -> Int, Unit](c)(g) val r3 = lazyMap(c)(g) val _ = r() val _ = r2() diff --git a/tests/pos-custom-args/captures/boxmap.scala b/tests/pos-custom-args/captures/boxmap.scala index a0dcade2b179..003e46804a9d 100644 --- a/tests/pos-custom-args/captures/boxmap.scala +++ b/tests/pos-custom-args/captures/boxmap.scala @@ -1,20 +1,18 @@ type Top = Any @retains(*) -infix type ==> [A, B] = (A => B) @retains(*) - -type Box[+T <: Top] = ([K <: Top] => (T ==> K) => K) +type Box[+T <: Top] = ([K <: Top] -> (T => K) -> K) def box[T <: Top](x: T): Box[T] = - [K <: Top] => (k: T ==> K) => k(x) + [K <: Top] => (k: T => K) => k(x) -def map[A <: Top, B <: Top](b: Box[A])(f: A ==> B): Box[B] = +def map[A <: Top, B <: Top](b: Box[A])(f: A => B): Box[B] = b[Box[B]]((x: A) => box(f(x))) -def lazymap[A <: Top, B <: Top](b: Box[A])(f: A ==> B): (() => Box[B]) @retains(f) = +def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B): (() -> Box[B]) @retains(f) = () => b[Box[B]]((x: A) => box(f(x))) def test[A <: Top, B <: Top] = - def lazymap[A <: Top, B <: Top](b: Box[A])(f: A ==> B) = + def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B) = () => b[Box[B]]((x: A) => box(f(x))) - val x: (b: Box[A]) => (f: A ==> B) => (() => Box[B]) @retains(f) = lazymap[A, B] + val x: (b: Box[A]) -> (f: A => B) -> (() -> Box[B]) @retains(f) = lazymap[A, B] () diff --git a/tests/pos-custom-args/captures/capt-capability.scala b/tests/pos-custom-args/captures/capt-capability.scala index 41da15d288f1..9990542a199d 100644 --- a/tests/pos-custom-args/captures/capt-capability.scala +++ b/tests/pos-custom-args/captures/capt-capability.scala @@ -1,15 +1,15 @@ import annotation.capability @capability class Cap -def f1(c: Cap): {c} () => c.type = () => c // ok +def f1(c: Cap): {c} () -> c.type = () => c // ok def f2: Int = - val g: {*} Boolean => Int = ??? + val g: Boolean => Int = ??? val x = g(true) x def f3: Int = - def g: {*} Boolean => Int = ??? + def g: Boolean => Int = ??? def h = g val x = g.apply(true) x @@ -17,10 +17,10 @@ def f3: Int = def foo() = val x: Cap = ??? val y: Cap = x - val x2: {x} () => Cap = ??? - val y2: {x} () => Cap = x2 + val x2: {x} () -> Cap = ??? + val y2: {x} () -> Cap = x2 - val z1: {*} () => Cap = f1(x) + val z1: () => Cap = f1(x) def h[X](a: X)(b: X) = a val z2 = diff --git a/tests/pos-custom-args/captures/capt-depfun.scala b/tests/pos-custom-args/captures/capt-depfun.scala index 6b99eff32692..072eaefd3e78 100644 --- a/tests/pos-custom-args/captures/capt-depfun.scala +++ b/tests/pos-custom-args/captures/capt-depfun.scala @@ -1,18 +1,18 @@ class C type Cap = C @retains(*) -type T = (x: Cap) => String @retains(x) +type T = (x: Cap) -> String @retains(x) -val aa: ((x: Cap) => String @retains(x)) = (x: Cap) => "" +val aa: ((x: Cap) -> String @retains(x)) = (x: Cap) => "" def f(y: Cap, z: Cap): String @retains(*) = - val a: ((x: Cap) => String @retains(x)) = (x: Cap) => "" + val a: ((x: Cap) -> String @retains(x)) = (x: Cap) => "" val b = a(y) val c: String @retains(y) = b def g(): C @retains(y, z) = ??? val d = a(g()) - val ac: ((x: Cap) => String @retains(x) => String @retains(x)) = ??? - val bc: (({y} String) => {y} String) = ac(y) - val dc: (String => {y, z} String) = ac(g()) + val ac: ((x: Cap) -> String @retains(x) -> String @retains(x)) = ??? + val bc: (({y} String) -> {y} String) = ac(y) + val dc: (String -> {y, z} String) = ac(g()) c diff --git a/tests/pos-custom-args/captures/capt-depfun2.scala b/tests/pos-custom-args/captures/capt-depfun2.scala index 17f98b4a1554..98ee9dbfdc6b 100644 --- a/tests/pos-custom-args/captures/capt-depfun2.scala +++ b/tests/pos-custom-args/captures/capt-depfun2.scala @@ -3,6 +3,6 @@ type Cap = C @retains(*) def f(y: Cap, z: Cap) = def g(): C @retains(y, z) = ??? - val ac: ((x: Cap) => Array[String @retains(x)]) = ??? + val ac: ((x: Cap) -> Array[String @retains(x)]) = ??? val dc: Array[? >: String <: {y, z} String] = ac(g()) // needs to be inferred val ec = ac(y) diff --git a/tests/pos-custom-args/captures/capt-test.scala b/tests/pos-custom-args/captures/capt-test.scala index f40bd2ff1746..6ee0d2a4d9f4 100644 --- a/tests/pos-custom-args/captures/capt-test.scala +++ b/tests/pos-custom-args/captures/capt-test.scala @@ -2,7 +2,7 @@ abstract class LIST[+T]: def isEmpty: Boolean def head: T def tail: LIST[T] - def map[U](f: {*} T => U): LIST[U] = + def map[U](f: T => U): LIST[U] = if isEmpty then NIL else CONS(f(head), tail.map(f)) @@ -15,7 +15,7 @@ object NIL extends LIST[Nothing]: def head = ??? def tail = ??? -def map[A, B](f: {*} A => B)(xs: LIST[A]): LIST[B] = +def map[A, B](f: A => B)(xs: LIST[A]): LIST[B] = xs.map(f) class C @@ -29,7 +29,7 @@ def test(c: Cap, d: Cap) = val zs = val z = g CONS(z, ys) - val zsc: LIST[{d, y} Cap => Unit] = zs + val zsc: LIST[{d, y} Cap -> Unit] = zs val a4 = zs.map(identity) - val a4c: LIST[{d, y} Cap => Unit] = a4 + val a4c: LIST[{d, y} Cap -> Unit] = a4 diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala index 14c0855544d4..e8e217435f96 100644 --- a/tests/pos-custom-args/captures/capt1.scala +++ b/tests/pos-custom-args/captures/capt1.scala @@ -1,14 +1,14 @@ class C type Cap = {*} C -def f1(c: Cap): {c} () => c.type = () => c // ok +def f1(c: Cap): {c} () -> c.type = () => c // ok def f2: Int = - val g: {*} Boolean => Int = ??? + val g: {*} Boolean -> Int = ??? val x = g(true) x def f3: Int = - def g: {*} Boolean => Int = ??? + def g: Boolean => Int = ??? def h = g val x = g.apply(true) x @@ -16,10 +16,10 @@ def f3: Int = def foo() = val x: {*} C = ??? val y: {x} C = x - val x2: {x} () => C = ??? - val y2: {x} () => {x} C = x2 + val x2: {x} () -> C = ??? + val y2: {x} () -> {x} C = x2 - val z1: {*} () => Cap = f1(x) + val z1: () => Cap = f1(x) def h[X](a: X)(b: X) = a val z2 = diff --git a/tests/pos-custom-args/captures/capt2.scala b/tests/pos-custom-args/captures/capt2.scala index e3d4cd67b30c..11bb2d5eb7b5 100644 --- a/tests/pos-custom-args/captures/capt2.scala +++ b/tests/pos-custom-args/captures/capt2.scala @@ -9,12 +9,12 @@ def test1() = def test2() = val x: Cap = C() val y = () => { x; () } - def z: (() => Unit) @retains(x) = y - z: (() => Unit) @retains(x) - def z2: (() => Unit) @retains(y) = y - z2: (() => Unit) @retains(y) - val p: {*} () => String = () => "abc" + def z: (() -> Unit) @retains(x) = y + z: (() -> Unit) @retains(x) + def z2: (() -> Unit) @retains(y) = y + z2: (() -> Unit) @retains(y) + val p: {*} () -> String = () => "abc" val q: {p} C = ??? - p: ({p} () => String) + p: ({p} () -> String) diff --git a/tests/pos-custom-args/captures/cc-expand.scala b/tests/pos-custom-args/captures/cc-expand.scala index eedc95554b17..7bce1ea8387e 100644 --- a/tests/pos-custom-args/captures/cc-expand.scala +++ b/tests/pos-custom-args/captures/cc-expand.scala @@ -8,14 +8,14 @@ object Test: def test(ct: CT, dt: CT) = - def x0: A => {ct} B = ??? + def x0: A -> {ct} B = ??? - def x1: A => B @retains(ct) = ??? - def x2: A => B => C @retains(ct) = ??? - def x3: A => () => B => C @retains(ct) = ??? + def x1: A -> B @retains(ct) = ??? + def x2: A -> B -> C @retains(ct) = ??? + def x3: A -> () -> B -> C @retains(ct) = ??? - def x4: (x: A @retains(ct)) => B => C = ??? + def x4: (x: A @retains(ct)) -> B -> C = ??? - def x5: A => (x: B @retains(ct)) => () => C @retains(dt) = ??? - def x6: A => (x: B @retains(ct)) => (() => C @retains(dt)) @retains(x, dt) = ??? - def x7: A => (x: B @retains(ct)) => (() => C @retains(dt)) @retains(x) = ??? \ No newline at end of file + def x5: A -> (x: B @retains(ct)) -> () -> C @retains(dt) = ??? + def x6: A -> (x: B @retains(ct)) -> (() -> C @retains(dt)) @retains(x, dt) = ??? + def x7: A -> (x: B @retains(ct)) -> (() -> C @retains(dt)) @retains(x) = ??? \ No newline at end of file diff --git a/tests/pos-custom-args/captures/impurefun.scala b/tests/pos-custom-args/captures/impurefun.scala new file mode 100644 index 000000000000..6e31008fe54a --- /dev/null +++ b/tests/pos-custom-args/captures/impurefun.scala @@ -0,0 +1,8 @@ +object Test: + + val f: ImpureFunction1[Int, Int] = (x: Int) => x + 1 + + val g: Int -> Int = (x: Int) => x + 1 + + val h: Int => Int = (x: Int) => x + 1 + diff --git a/tests/pos-custom-args/captures/lazylists-mono.scala b/tests/pos-custom-args/captures/lazylists-mono.scala index 82c44abf703a..44ab36ded6a2 100644 --- a/tests/pos-custom-args/captures/lazylists-mono.scala +++ b/tests/pos-custom-args/captures/lazylists-mono.scala @@ -6,21 +6,21 @@ type Cap = {*} CC def test(E: Cap) = trait LazyList[+A]: - protected def contents: {E} () => (A, {E} LazyList[A]) + protected def contents: {E} () -> (A, {E} LazyList[A]) def isEmpty: Boolean def head: A = contents()._1 def tail: {E} LazyList[A] = contents()._2 - class LazyCons[+A](override val contents: {E} () => (A, {E} LazyList[A])) + class LazyCons[+A](override val contents: {E} () -> (A, {E} LazyList[A])) extends LazyList[A]: def isEmpty: Boolean = false object LazyNil extends LazyList[Nothing]: - def contents: {E} () => (Nothing, LazyList[Nothing]) = ??? + def contents: {E} () -> (Nothing, LazyList[Nothing]) = ??? def isEmpty: Boolean = true extension [A](xs: {E} LazyList[A]) - def map[B](f: {E} A => B): {E} LazyList[B] = + def map[B](f: {E} A -> B): {E} LazyList[B] = if xs.isEmpty then LazyNil else val cons = () => (f(xs.head), xs.tail.map(f)) diff --git a/tests/pos-custom-args/captures/lazylists.scala b/tests/pos-custom-args/captures/lazylists.scala index 17d5f8546edc..bf3e0300b5b5 100644 --- a/tests/pos-custom-args/captures/lazylists.scala +++ b/tests/pos-custom-args/captures/lazylists.scala @@ -14,7 +14,7 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? extension [A](xs: {*} LazyList[A]) - def map[B](f: {*} A => B): {xs, f} LazyList[B] = + def map[B](f: A => B): {xs, f} LazyList[B] = final class Mapped extends LazyList[B]: this: ({xs, f} Mapped) => diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala index 4c8006fb0e29..7fbdde87ad9b 100644 --- a/tests/pos-custom-args/captures/lazylists1.scala +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -13,23 +13,23 @@ object LazyNil extends LazyList[Nothing]: def head = ??? def tail = ??? -final class LazyCons[+T](val x: T, val xs: {*} () => {*} LazyList[T]) extends LazyList[T]: +final class LazyCons[+T](val x: T, val xs: Int => {*} LazyList[T]) extends LazyList[T]: this: ({*} LazyList[T]) => def isEmpty = false def head = x - def tail: {this} LazyList[T] = xs() + def tail: {this} LazyList[T] = xs(0) extension [A](xs: {*} LazyList[A]) - def map[B](f: {*} A => B): {xs, f} LazyList[B] = + def map[B](f: A => B): {xs, f} LazyList[B] = if xs.isEmpty then LazyNil - else LazyCons(f(xs.head), () => xs.tail.map(f)) + else LazyCons(f(xs.head), x => xs.tail.map(f)) def test(cap1: Cap, cap2: Cap) = def f(x: String): String = if cap1 == cap1 then "" else "a" def g(x: String): String = if cap2 == cap2 then "" else "a" - val xs = LazyCons("", () => if f("") == f("") then LazyNil else LazyNil) + val xs = new LazyCons("", x => if f("") == f("") then LazyNil else LazyNil) val xsc: {cap1} LazyList[String] = xs val ys = xs.map(g) val ysc: {cap1, cap2} LazyList[String] = ys diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala index 39748b00506b..2ab770178a16 100644 --- a/tests/pos-custom-args/captures/lazyref.scala +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -1,15 +1,14 @@ -class CC -type Cap = {*} CC +@annotation.capability class Cap -class LazyRef[T](val elem: {*} () => T): +class LazyRef[T](val elem: () => T): val get = elem - def map[U](f: {*} T => U): {f, this} LazyRef[U] = + def map[U](f: T => U): {f, this} LazyRef[U] = new LazyRef(() => f(elem())) -def map[A, B](ref: {*} LazyRef[A], f: {*} A => B): {f, ref} LazyRef[B] = +def map[A, B](ref: {*} LazyRef[A], f: A => B): {f, ref} LazyRef[B] = new LazyRef(() => f(ref.elem())) -def mapc[A, B]: (ref: {*} LazyRef[A], f: {*} A => B) => {f, ref} LazyRef[B] = +def mapc[A, B]: (ref: {*} LazyRef[A], f: A => B) => {f, ref} LazyRef[B] = (ref1, f1) => map[A, B](ref1, f1) def test(cap1: Cap, cap2: Cap) = diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala index 74bc8bd2b099..59ae61273af7 100644 --- a/tests/pos-custom-args/captures/list-encoding.scala +++ b/tests/pos-custom-args/captures/list-encoding.scala @@ -3,10 +3,10 @@ package listEncoding class Cap type Op[T, C] = - {*} (v: T) => {*} (s: C) => C + (v: T) => (s: C) => C type List[T] = - [C] => (op: Op[T, C]) => {op} (s: C) => C + [C] -> (op: Op[T, C]) -> {op} (s: C) -> C def nil[T]: List[T] = [C] => (op: Op[T, C]) => (s: C) => s diff --git a/tests/pos-custom-args/captures/lists.scala b/tests/pos-custom-args/captures/lists.scala index 139f885ec87a..f52727af7b94 100644 --- a/tests/pos-custom-args/captures/lists.scala +++ b/tests/pos-custom-args/captures/lists.scala @@ -2,7 +2,7 @@ abstract class LIST[+T]: def isEmpty: Boolean def head: T def tail: LIST[T] - def map[U](f: {*} T => U): LIST[U] = + def map[U](f: {*} T -> U): LIST[U] = if isEmpty then NIL else CONS(f(head), tail.map(f)) @@ -15,11 +15,10 @@ object NIL extends LIST[Nothing]: def head = ??? def tail = ??? -def map[A, B](f: {*} A => B)(xs: LIST[A]): LIST[B] = +def map[A, B](f: A => B)(xs: LIST[A]): LIST[B] = xs.map(f) -class C -type Cap = {*} C +@annotation.capability class Cap def test(c: Cap, d: Cap, e: Cap) = def f(x: Cap): Unit = if c == x then () @@ -29,63 +28,63 @@ def test(c: Cap, d: Cap, e: Cap) = val zs = val z = g CONS(z, ys) - val zsc: LIST[{d, y} Cap => Unit] = zs + val zsc: LIST[{d, y} Cap -> Unit] = zs val z1 = zs.head - val z1c: {y, d} Cap => Unit = z1 + val z1c: {y, d} Cap -> Unit = z1 val ys1 = zs.tail val y1 = ys1.head def m1[A, B] = - (f: {*} A => B) => (xs: LIST[A]) => xs.map(f) + (f: A => B) => (xs: LIST[A]) => xs.map(f) - def m1c: (f: {*} String => Int) => {f} LIST[String] => LIST[Int] = m1[String, Int] + def m1c: (f: String => Int) -> {f} LIST[String] -> LIST[Int] = m1[String, Int] def m2 = [A, B] => - (f: {*} A => B) => (xs: LIST[A]) => xs.map(f) + (f: A => B) => (xs: LIST[A]) => xs.map(f) - def m2c: [A, B] => (f: {*} A => B) => {f} LIST[A] => LIST[B] = m2 + def m2c: [A, B] -> (f: A => B) -> {f} LIST[A] -> LIST[B] = m2 def eff[A](x: A) = if x == e then x else x val eff2 = [A] => (x: A) => if x == e then x else x - val a0 = identity[{d, y} Cap => Unit] - val a0c: ({d, y} Cap => Unit) => {d, y} Cap => Unit = a0 - val a1 = zs.map[{d, y} Cap => Unit](a0) - val a1c: LIST[{d, y} Cap => Unit] = a1 - val a2 = zs.map[{d, y} Cap => Unit](identity[{d, y} Cap => Unit]) - val a2c: LIST[{d, y} Cap => Unit] = a2 - val a3 = zs.map(identity[{d, y} Cap => Unit]) - val a3c: LIST[{d, y} Cap => Unit] = a3 + val a0 = identity[{d, y} Cap -> Unit] + val a0c: ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = a0 + val a1 = zs.map[{d, y} Cap -> Unit](a0) + val a1c: LIST[{d, y} Cap -> Unit] = a1 + val a2 = zs.map[{d, y} Cap -> Unit](identity[{d, y} Cap -> Unit]) + val a2c: LIST[{d, y} Cap -> Unit] = a2 + val a3 = zs.map(identity[{d, y} Cap -> Unit]) + val a3c: LIST[{d, y} Cap -> Unit] = a3 val a4 = zs.map(identity) - val a4c: LIST[{d, c} Cap => Unit] = a4 - val a5 = map[{d, y} Cap => Unit, {d, y} Cap => Unit](identity)(zs) - val a5c: LIST[{d, c} Cap => Unit] = a5 - val a6 = m1[{d, y} Cap => Unit, {d, y} Cap => Unit](identity)(zs) - val a6c: LIST[{d, c} Cap => Unit] = a6 + val a4c: LIST[{d, c} Cap -> Unit] = a4 + val a5 = map[{d, y} Cap -> Unit, {d, y} Cap -> Unit](identity)(zs) + val a5c: LIST[{d, c} Cap -> Unit] = a5 + val a6 = m1[{d, y} Cap -> Unit, {d, y} Cap -> Unit](identity)(zs) + val a6c: LIST[{d, c} Cap -> Unit] = a6 - val b0 = eff[{d, y} Cap => Unit] - val b0c: {e} ({d, y} Cap => Unit) => {d, y} Cap => Unit = b0 - val b1 = zs.map[{d, y} Cap => Unit](a0) - val b1c: {e} LIST[{d, y} Cap => Unit] = b1 - val b2 = zs.map[{d, y} Cap => Unit](eff[{d, y} Cap => Unit]) - val b2c: {e} LIST[{d, y} Cap => Unit] = b2 - val b3 = zs.map(eff[{d, y} Cap => Unit]) - val b3c: {e} LIST[{d, y} Cap => Unit] = b3 + val b0 = eff[{d, y} Cap -> Unit] + val b0c: {e} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = b0 + val b1 = zs.map[{d, y} Cap -> Unit](a0) + val b1c: {e} LIST[{d, y} Cap -> Unit] = b1 + val b2 = zs.map[{d, y} Cap -> Unit](eff[{d, y} Cap -> Unit]) + val b2c: {e} LIST[{d, y} Cap -> Unit] = b2 + val b3 = zs.map(eff[{d, y} Cap -> Unit]) + val b3c: {e} LIST[{d, y} Cap -> Unit] = b3 val b4 = zs.map(eff) - val b4c: {e} LIST[{d, c} Cap => Unit] = b4 - val b5 = map[{d, y} Cap => Unit, {d, y} Cap => Unit](eff)(zs) - val b5c: {e} LIST[{d, c} Cap => Unit] = b5 - val b6 = m1[{d, y} Cap => Unit, {d, y} Cap => Unit](eff)(zs) - val b6c: {e} LIST[{d, c} Cap => Unit] = b6 + val b4c: {e} LIST[{d, c} Cap -> Unit] = b4 + val b5 = map[{d, y} Cap -> Unit, {d, y} Cap -> Unit](eff)(zs) + val b5c: {e} LIST[{d, c} Cap -> Unit] = b5 + val b6 = m1[{d, y} Cap -> Unit, {d, y} Cap -> Unit](eff)(zs) + val b6c: {e} LIST[{d, c} Cap -> Unit] = b6 - val c0 = eff2[{d, y} Cap => Unit] - val c0c: {e} ({d, y} Cap => Unit) => {d, y} Cap => Unit = c0 - val c1 = zs.map[{d, y} Cap => Unit](a0) - val c1c: {e} LIST[{d, y} Cap => Unit] = c1 - val c2 = zs.map[{d, y} Cap => Unit](eff2[{d, y} Cap => Unit]) - val c2c: {e} LIST[{d, y} Cap => Unit] = c2 - val c3 = zs.map(eff2[{d, y} Cap => Unit]) - val c3c: {e} LIST[{d, y} Cap => Unit] = c3 + val c0 = eff2[{d, y} Cap -> Unit] + val c0c: {e} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = c0 + val c1 = zs.map[{d, y} Cap -> Unit](a0) + val c1c: {e} LIST[{d, y} Cap -> Unit] = c1 + val c2 = zs.map[{d, y} Cap -> Unit](eff2[{d, y} Cap -> Unit]) + val c2c: {e} LIST[{d, y} Cap -> Unit] = c2 + val c3 = zs.map(eff2[{d, y} Cap -> Unit]) + val c3c: {e} LIST[{d, y} Cap -> Unit] = c3 diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index 4f23a086a075..14d484ff21b1 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -1,6 +1,5 @@ -class C -type Cap = {*} C +@annotation.capability class Cap object Generic: @@ -13,13 +12,13 @@ object Generic: def g(x: Cap): Unit = if d == x then () val p = Pair(f, g) val x1 = p.fst - val x1c: {c} Cap => Unit = x1 + val x1c: {c} Cap -> Unit = x1 val y1 = p.snd - val y1c: {d} Cap => Unit = y1 + val y1c: {d} Cap -> Unit = y1 object Monomorphic: - class Pair(val x: {*} Cap => Unit, val y: {*} Cap => Unit): + class Pair(val x: Cap => Unit, val y: {*} Cap -> Unit): def fst = x def snd = y @@ -28,6 +27,6 @@ object Monomorphic: def g(x: Cap): Unit = if d == x then () val p = Pair(f, g) val x1 = p.fst - val x1c: {c} Cap => Unit = x1 + val x1c: {c} Cap -> Unit = x1 val y1 = p.snd - val y1c: {d} Cap => Unit = y1 + val y1c: {d} Cap -> Unit = y1 diff --git a/tests/pos-custom-args/captures/try.scala b/tests/pos-custom-args/captures/try.scala index a50eeabfb3a3..14773cd5be0f 100644 --- a/tests/pos-custom-args/captures/try.scala +++ b/tests/pos-custom-args/captures/try.scala @@ -3,7 +3,7 @@ import language.experimental.erasedDefinitions class CT[E <: Exception] type CanThrow[E <: Exception] = CT[E] @retains(*) -infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?=> R +infix type throws[R, E <: Exception] = (erased CanThrow[E]) ?-> R class Fail extends Exception @@ -12,7 +12,7 @@ def raise[E <: Exception](e: E): Nothing throws E = throw e def foo(x: Boolean): Int throws Fail = if x then 1 else raise(Fail()) -def handle[E <: Exception, R](op: (erased CanThrow[E]) => R)(handler: E => R): R = +def handle[E <: Exception, R](op: (erased CanThrow[E]) -> R)(handler: E -> R): R = erased val x: CanThrow[E] = ??? try op(x) catch case ex: E => handler(ex) diff --git a/tests/pos-custom-args/captures/try3.scala b/tests/pos-custom-args/captures/try3.scala index b29ad2d4b352..b8937bec00f3 100644 --- a/tests/pos-custom-args/captures/try3.scala +++ b/tests/pos-custom-args/captures/try3.scala @@ -2,8 +2,7 @@ import language.experimental.erasedDefinitions import annotation.capability import java.io.IOException -class CT[-E] // variance is needed for correct rechecking inference -type CanThrow[E] = {*} CT[E] +@annotation.capability class CanThrow[-E] def handle[E <: Exception, T](op: CanThrow[E] ?=> T)(handler: E => T): T = val x: CanThrow[E] = ??? @@ -14,7 +13,7 @@ def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = throw ex def test1: Int = - def f(a: Boolean): Boolean => CanThrow[IOException] ?=> Int = + def f(a: Boolean): Boolean -> CanThrow[IOException] ?-> Int = handle { if !a then raise(IOException()) (b: Boolean) => (_: CanThrow[IOException]) ?=> diff --git a/tests/pos-custom-args/captures/vars.scala b/tests/pos-custom-args/captures/vars.scala index aca56c55f386..12721158a2bb 100644 --- a/tests/pos-custom-args/captures/vars.scala +++ b/tests/pos-custom-args/captures/vars.scala @@ -1,18 +1,17 @@ -class CC -type Cap = {*} CC +@annotation.capability class Cap def test(cap1: Cap, cap2: Cap) = def f(x: String): String = if cap1 == cap1 then "" else "a" var x = f val y = x val z = () => if x("") == "" then "a" else "b" - val zc: {cap1} () => String = z + val zc: {cap1} () -> String = z val z2 = () => { x = identity } - val z2c: {cap1} () => Unit = z2 + val z2c: {cap1} () -> Unit = z2 class Ref: - var elem: {cap1} String => String = null + var elem: {cap1} String -> String = null val r = Ref() r.elem = f - val fc: {cap1} String => String = r.elem + val fc: {cap1} String -> String = r.elem diff --git a/tests/pos/i12723.scala b/tests/pos/i12723.scala index d1cab3ede638..022a3a458f04 100644 --- a/tests/pos/i12723.scala +++ b/tests/pos/i12723.scala @@ -1,10 +1,10 @@ class Fun[|*|[_, _]] { - enum ->[A, B] { - case BiId[X, Y]() extends ((X |*| Y) -> (X |*| Y)) + enum -->[A, B] { + case BiId[X, Y]() extends ((X |*| Y) --> (X |*| Y)) } - def go[A, B](f: A -> B): Unit = + def go[A, B](f: A --> B): Unit = f match { - case ->.BiId() => () + case -->.BiId() => () } } diff --git a/tests/pos/impurefun.scala b/tests/pos/impurefun.scala new file mode 100644 index 000000000000..c9f4c54a0b90 --- /dev/null +++ b/tests/pos/impurefun.scala @@ -0,0 +1,4 @@ +object Test: + + val f: ImpureFunction1[Int, Int] = (x: Int) => x + 1 + From bfeacfe6c4702b85aafbef2de490853cfb67ee33 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 21 Jan 2022 14:27:43 +0100 Subject: [PATCH 12/99] Updates for latest master - Fix rebase breakage - weaken test in TreePickler that was introduced in the meantime since the last rebase (this one needs follow up) - adapt to latest restrictions on rhs of erased definitions --- compiler/src/dotty/tools/dotc/core/Definitions.scala | 1 + compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala | 7 ++----- tests/pos-custom-args/captures/byname.scala | 2 +- tests/pos-custom-args/captures/try.scala | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index d6abcf693bf1..5c1fa371da75 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1402,6 +1402,7 @@ class Definitions { funTypeArray(funTypeIdx(isContextual, isErased, isImpure))(n).symbol @tu lazy val Function0_apply: Symbol = Function0.requiredMethod(nme.apply) + @tu lazy val ContextFunction0_apply: Symbol = ContextFunction0.requiredMethod(nme.apply) @tu lazy val Function0: Symbol = FunctionSymbol(0) @tu lazy val Function1: Symbol = FunctionSymbol(1) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index e78776cb0158..3cdf39cb56e6 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -467,11 +467,8 @@ class CheckCaptures extends Recheck: recheckFinish(result, arg, pt) override def recheckApply(tree: Apply, pt: Type)(using Context): Type = - if tree.symbol == defn.cbnArg then - recheckByNameArg(tree.args(0), pt) - else - includeCallCaptures(tree.symbol, tree.srcPos) - super.recheckApply(tree, pt) + includeCallCaptures(tree.symbol, tree.srcPos) + super.recheckApply(tree, pt) override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = val res = super.recheck(tree, pt) diff --git a/tests/pos-custom-args/captures/byname.scala b/tests/pos-custom-args/captures/byname.scala index 917154079b36..a3d80f31d579 100644 --- a/tests/pos-custom-args/captures/byname.scala +++ b/tests/pos-custom-args/captures/byname.scala @@ -5,6 +5,6 @@ class I def test(cap1: Cap, cap2: Cap): {cap1} I = def f() = if cap1 == cap1 then I() else I() - def h(x: => {cap1} I) = x + def h(x: /*=>*/ {cap1} I) = x // TODO: enable cbn h(f()) diff --git a/tests/pos-custom-args/captures/try.scala b/tests/pos-custom-args/captures/try.scala index 14773cd5be0f..73e8a1d27ec7 100644 --- a/tests/pos-custom-args/captures/try.scala +++ b/tests/pos-custom-args/captures/try.scala @@ -13,7 +13,7 @@ def foo(x: Boolean): Int throws Fail = if x then 1 else raise(Fail()) def handle[E <: Exception, R](op: (erased CanThrow[E]) -> R)(handler: E -> R): R = - erased val x: CanThrow[E] = ??? + erased val x: CanThrow[E] = ??? : CanThrow[E] try op(x) catch case ex: E => handler(ex) From 0ae79116f96b74fb5606a6a103c5a847dc313e08 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 23 Dec 2021 18:47:33 +0100 Subject: [PATCH 13/99] Shorthand for curried functions Propagate capture sets to the right in curried functions. Example: {x} A -> B -> C is a shorthand for {x} A -> {x} B -> C or: (x: {*} A) -> B -> C is a shorthand for (x: {*} A) -> {x} B -> C or: ({*} A) -> B -> C is a shorthand for (x$0: {*} A) -> {x$0} B -> C Also: allow empty capture sets in types This gives a more convenient override to disable capture set propagation in curried types than wrapping in a type alias. E.g. compare {x} A -> {} B -> C with {x} A -> Protect[B -> C] where type Protect[X] = X Also: refactoring to move setup code from Rechecker and CheckCaptures into a joint class cc.Setup. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 328 ++++++++++++++++++ .../tools/dotc/config/ScalaSettings.scala | 1 + .../dotty/tools/dotc/parsing/Parsers.scala | 16 +- .../tools/dotc/printing/RefinedPrinter.scala | 2 +- .../dotty/tools/dotc/transform/Recheck.scala | 163 +++------ .../tools/dotc/typer/CheckCaptures.scala | 214 ++---------- .../captures/curried-simplified.check | 42 +++ .../captures/curried-simplified.scala | 21 ++ .../captures/capt-depfun.scala | 4 +- 9 files changed, 468 insertions(+), 323 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/Setup.scala create mode 100644 tests/neg-custom-args/captures/curried-simplified.check create mode 100644 tests/neg-custom-args/captures/curried-simplified.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala new file mode 100644 index 000000000000..b5ccb002e209 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -0,0 +1,328 @@ +package dotty.tools +package dotc +package cc + +import core._ +import Phases.*, DenotTransformers.*, SymDenotations.* +import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* +import Types.*, StdNames.* +import config.Printers.capt +import ast.tpd +import transform.Recheck.* + +class Setup( + preRecheckPhase: DenotTransformer, + thisPhase: DenotTransformer, + recheckDef: (tpd.ValOrDefDef, Symbol) => Context ?=> Unit) +extends tpd.TreeTraverser: + import tpd.* + + private def depFun(tycon: Type, argTypes: List[Type], resType: Type)(using Context): Type = + MethodType.companion( + isContextual = defn.isContextFunctionClass(tycon.classSymbol), + isErased = defn.isErasedFunctionClass(tycon.classSymbol) + )(argTypes, resType) + .toFunctionType(isJava = false, alwaysDependent = true) + + private def box(tp: Type)(using Context): Type = tp match + case CapturingType(parent, refs, false) => CapturingType(parent, refs, true) + case _ => tp + + private def setBoxed(tp: Type)(using Context) = tp match + case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => + annot.tree.setBoxedCapturing() + case _ => + + private def addBoxes(using Context) = new TypeTraverser: + def traverse(t: Type) = + t match + case AppliedType(tycon, args) if !defn.isNonRefinedFunction(t) => + args.foreach(setBoxed) + case TypeBounds(lo, hi) => + setBoxed(lo); setBoxed(hi) + case _ => + traverseChildren(t) + + /** Perform the following transformation steps everywhere in a type: + * 1. Drop retains annotations + * 2. Turn plain function types into dependent function types, so that + * we can refer to their parameter in capture sets. Currently this is + * only done at the toplevel, i.e. for function types that are not + * themselves argument types of other function types. Without this restriction + * boxmap-paper.scala fails. Need to figure out why. + * 3. Refine other class types C by adding capture set variables to their parameter getters + * (see addCaptureRefinements) + * 4. Add capture set variables to all types that can be tracked + * + * Polytype bounds are only cleaned using step 1, but not otherwise transformed. + */ + private def mapInferred(using Context) = new TypeMap: + + /** Drop @retains annotations everywhere */ + object cleanup extends TypeMap: + def apply(t: Type) = t match + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + apply(parent) + case _ => + mapOver(t) + + /** Refine a possibly applied class type C where the class has tracked parameters + * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } + * where CV_1, ..., CV_n are fresh capture sets. + */ + def addCaptureRefinements(tp: Type): Type = tp match + case _: TypeRef | _: AppliedType if tp.typeParams.isEmpty => + tp.typeSymbol match + case cls: ClassSymbol if !defn.isFunctionClass(cls) => + cls.paramGetters.foldLeft(tp) { (core, getter) => + if getter.termRef.isTracked then + val getterType = tp.memberInfo(getter).strippedDealias + RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), boxed = false)) + .showing(i"add capture refinement $tp --> $result", capt) + else + core + } + case _ => tp + case _ => tp + + /** Should a capture set variable be added on type `tp`? */ + def canHaveInferredCapture(tp: Type): Boolean = + tp.typeParams.isEmpty && tp.match + case tp: (TypeRef | AppliedType) => + val sym = tp.typeSymbol + if sym.isClass then !sym.isValueClass && sym != defn.AnyClass + else canHaveInferredCapture(tp.superType.dealias) + case tp: (RefinedOrRecType | MatchType) => + canHaveInferredCapture(tp.underlying) + case tp: AndType => + canHaveInferredCapture(tp.tp1) && canHaveInferredCapture(tp.tp2) + case tp: OrType => + canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2) + case _ => + false + + /** Add a capture set variable to `tp` if necessary, or maybe pull out + * an embedded capture set variables from a part of `tp`. + */ + def addVar(tp: Type) = tp match + case tp @ RefinedType(parent @ CapturingType(parent1, refs, boxed), rname, rinfo) => + CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, boxed) + case tp: RecType => + tp.parent match + case CapturingType(parent1, refs, boxed) => + CapturingType(tp.derivedRecType(parent1), refs, boxed) + case _ => + tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created + // by `mapInferred`. Hence if the underlying type admits capture variables + // a variable was already added, and the first case above would apply. + case AndType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => + assert(refs1.asVar.elems.isEmpty) + assert(refs2.asVar.elems.isEmpty) + assert(boxed1 == boxed2) + CapturingType(AndType(parent1, parent2), refs1, boxed1) + case tp @ OrType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => + assert(refs1.asVar.elems.isEmpty) + assert(refs2.asVar.elems.isEmpty) + assert(boxed1 == boxed2) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1, boxed1) + case tp @ OrType(CapturingType(parent1, refs1, boxed1), tp2) => + CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, boxed1) + case tp @ OrType(tp1, CapturingType(parent2, refs2, boxed2)) => + CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, boxed2) + case _ if canHaveInferredCapture(tp) => + CapturingType(tp, CaptureSet.Var(), boxed = false) + case _ => + tp + + var isTopLevel = true + + def mapNested(ts: List[Type]): List[Type] = + val saved = isTopLevel + isTopLevel = false + try ts.mapConserve(this) finally isTopLevel = saved + + def apply(t: Type) = + val t1 = t match + case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + apply(parent) + case tp @ AppliedType(tycon, args) => + val tycon1 = this(tycon) + if defn.isNonRefinedFunction(tp) then + val args1 = mapNested(args.init) + val res1 = this(args.last) + if isTopLevel then + depFun(tycon1, args1, res1) + .showing(i"add function refinement $tp --> $result", capt) + else + tp.derivedAppliedType(tycon1, args1 :+ res1) + else + tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) + case tp @ RefinedType(core, rname, rinfo) if defn.isFunctionType(tp) => + val rinfo1 = apply(rinfo) + if rinfo1 ne rinfo then rinfo1.toFunctionType(isJava = false, alwaysDependent = true) + else tp + case tp: MethodType => + tp.derivedLambdaType( + paramInfos = mapNested(tp.paramInfos), + resType = this(tp.resType)) + case tp: TypeLambda => + // Don't recurse into parameter bounds, just cleanup any stray retains annotations + tp.derivedLambdaType( + paramInfos = tp.paramInfos.mapConserve(cleanup(_).bounds), + resType = this(tp.resType)) + case _ => + mapOver(t) + addVar(addCaptureRefinements(t1)) + end mapInferred + + private def expandAbbreviations(using Context) = new TypeMap: + + def propagateMethodResult(tp: Type, outerCs: CaptureSet, deep: Boolean): Type = tp match + case tp: MethodType => + if deep then + val tp1 = tp.derivedLambdaType(paramInfos = tp.paramInfos.mapConserve(this)) + propagateMethodResult(tp1, outerCs, deep = false) + else + val localCs = CaptureSet(tp.paramRefs.filter(_.isTracked)*) + tp.derivedLambdaType( + resType = propagateEnclosing(tp.resType, CaptureSet.empty, outerCs ++ localCs)) + + def propagateDepFunctionResult(tp: Type, outerCs: CaptureSet, deep: Boolean): Type = tp match + case tp @ RefinedType(parent, nme.apply, rinfo: MethodType) => + val rinfo1 = propagateMethodResult(rinfo, outerCs, deep) + if rinfo1 ne rinfo then rinfo1.toFunctionType(isJava = false, alwaysDependent = true) + else tp + + def propagateEnclosing(tp: Type, currentCs: CaptureSet, outerCs: CaptureSet): Type = tp match + case tp @ AppliedType(tycon, args) if defn.isFunctionClass(tycon.typeSymbol) => + val tycon1 = this(tycon) + val args1 = args.init.mapConserve(this) + val tp1 = + if args1.exists(!_.captureSet.isAlwaysEmpty) then + val propagated = propagateDepFunctionResult( + depFun(tycon, args1, args.last), currentCs ++ outerCs, deep = false) + propagated match + case RefinedType(_, _, mt: MethodType) => + val following = mt.resType.captureSet.elems + if mt.paramRefs.exists(following.contains(_)) then propagated + else tp.derivedAppliedType(tycon1, args1 :+ mt.resType) + else + val resType1 = propagateEnclosing( + args.last, CaptureSet.empty, currentCs ++ outerCs) + tp.derivedAppliedType(tycon1, args1 :+ resType1) + tp1.capturing(outerCs) + case tp @ RefinedType(parent, nme.apply, rinfo: MethodType) if defn.isFunctionType(tp) => + propagateDepFunctionResult(tp, currentCs ++ outerCs, deep = true) + .capturing(outerCs) + case _ => + mapOver(tp) + + def apply(tp: Type): Type = tp match + case CapturingType(parent, cs, boxed) => + tp.derivedCapturingType(propagateEnclosing(parent, cs, CaptureSet.empty), cs) + case _ => + propagateEnclosing(tp, CaptureSet.empty, CaptureSet.empty) + end expandAbbreviations + + private def transformInferredType(tp: Type, boxed: Boolean)(using Context): Type = + val tp1 = mapInferred(tp) + if boxed then box(tp1) else tp1 + + private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = + addBoxes.traverse(tp) + if boxed then setBoxed(tp) + if ctx.settings.YccNoAbbrev.value then tp + else expandAbbreviations(tp) + + // Substitute parameter symbols in `from` to paramRefs in corresponding + // method or poly types `to`. We use a single BiTypeMap to do everything. + private class SubstParams(from: List[List[Symbol]], to: List[LambdaType])(using Context) + extends DeepTypeMap, BiTypeMap: + + def apply(t: Type): Type = t match + case t: NamedType => + val sym = t.symbol + def outer(froms: List[List[Symbol]], tos: List[LambdaType]): Type = + def inner(from: List[Symbol], to: List[ParamRef]): Type = + if from.isEmpty then outer(froms.tail, tos.tail) + else if sym eq from.head then to.head + else inner(from.tail, to.tail) + if tos.isEmpty then t + else inner(froms.head, tos.head.paramRefs) + outer(from, to) + case _ => + mapOver(t) + + def inverse(t: Type): Type = t match + case t: ParamRef => + def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = + if from.isEmpty then t + else if t.binder eq from.head then to.head(t.paramNum).namedType + else recur(from.tail, to.tail) + recur(to, from) + case _ => + mapOver(t) + end SubstParams + + private def transformTT(tree: TypeTree, boxed: Boolean)(using Context) = + tree.rememberType( + if tree.isInstanceOf[InferredTypeTree] + then transformInferredType(tree.tpe, boxed) + else transformExplicitType(tree.tpe, boxed)) + + def traverse(tree: Tree)(using Context) = + tree match + case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) => + transformTT(tpt, boxed = true) + traverse(tree.rhs) + case _ => + traverseChildren(tree) + tree match + case tree: TypeTree => + transformTT(tree, boxed = false) + case tree: ValOrDefDef => + val sym = tree.symbol + + // replace an existing symbol info with inferred types + def integrateRT( + info: Type, // symbol info to replace + psymss: List[List[Symbol]], // the local (type and trem) parameter symbols corresponding to `info` + prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order + prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order + ): Type = + info match + case mt: MethodOrPoly => + val psyms = psymss.head + mt.companion(mt.paramNames)( + mt1 => + if !psyms.exists(_.isUpdatedAfter(preRecheckPhase)) && !mt.isParamDependent && prevLambdas.isEmpty then + mt.paramInfos + else + val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) + psyms.map(psym => subst(psym.info).asInstanceOf[mt.PInfo]), + mt1 => + integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) + ) + case info: ExprType => + info.derivedExprType(resType = + integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) + case _ => + val restp = tree.tpt.knownType + if prevLambdas.isEmpty then restp + else SubstParams(prevPsymss, prevLambdas)(restp) + + if tree.tpt.hasRememberedType && !sym.isConstructor then + val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) + .showing(i"update info $sym: ${sym.info} --> $result", capt) + if newInfo ne sym.info then + val completer = new LazyType: + def complete(denot: SymDenotation)(using Context) = + denot.info = newInfo + recheckDef(tree, sym) + sym.updateInfoBetween(preRecheckPhase, thisPhase, completer) + case tree: Bind => + val sym = tree.symbol + sym.updateInfoBetween(preRecheckPhase, thisPhase, + transformInferredType(sym.info, boxed = false)) + case _ => +end Setup diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index da53ea18da82..b8f08e09fa6d 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -328,6 +328,7 @@ private sealed trait YSettings: val Yrecheck: Setting[Boolean] = BooleanSetting("-Yrecheck", "Run type rechecks (test only)") val Ycc: Setting[Boolean] = BooleanSetting("-Ycc", "Check captured references") val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Debug info for captured references") + val YccNoAbbrev: Setting[Boolean] = BooleanSetting("-Ycc-no-abbrev", "Used in conjunction with -Ycc, suppress type abbreviations") /** Area-specific debug output */ val YexplainLowlevel: Setting[Boolean] = BooleanSetting("-Yexplain-lowlevel", "When explaining type errors, show types at a lower level.") diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index b884c8ffb93c..c3d5f6cea973 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -957,6 +957,10 @@ object Parsers { def followingIsCaptureSet(): Boolean = val lookahead = in.LookaheadScanner() + def followingIsTypeStart() = + lookahead.nextToken() + canStartInfixTypeTokens.contains(lookahead.token) + || lookahead.token == LBRACKET def recur(): Boolean = (lookahead.isIdent || lookahead.token == THIS) && { lookahead.nextToken() @@ -964,14 +968,10 @@ object Parsers { lookahead.nextToken() recur() else - lookahead.token == RBRACE && { - lookahead.nextToken() - canStartInfixTypeTokens.contains(lookahead.token) - || lookahead.token == LBRACKET - } + lookahead.token == RBRACE && followingIsTypeStart() } lookahead.nextToken() - recur() + if lookahead.token == RBRACE then followingIsTypeStart() else recur() /* --------- OPERAND/OPERATOR STACK --------------------------------------- */ @@ -1551,7 +1551,9 @@ object Parsers { else { accept(TLARROW); typ() } } else if in.token == LBRACE && followingIsCaptureSet() then - val refs = inBraces { commaSeparated(captureRef) } + val refs = inBraces { + if in.token == RBRACE then Nil else commaSeparated(captureRef) + } val t = typ() CapturingTypeTree(refs, t) else if (in.token == INDENT) enclosed(INDENT, typ()) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index 4f1eaa9eaf9f..b68cc0f8b8d0 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -28,7 +28,7 @@ import config.Config import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.ast.untpd.{MemberDef, Modifiers, PackageDef, RefTree, Template, TypeDef, ValOrDefDef} -import cc.{EventuallyCapturingType, CaptureSet, toCaptureSet, IllegalCaptureRef} +import cc.{CaptureSet, toCaptureSet, IllegalCaptureRef} class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 924a444aeff4..bdb49c5a8edb 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -7,7 +7,7 @@ import Symbols.*, Contexts.*, Types.*, ContextOps.*, Decorators.*, SymDenotation import Flags.*, SymUtils.*, NameKinds.* import ast.* import Phases.Phase -import DenotTransformers.IdentityDenotTransformer +import DenotTransformers.{IdentityDenotTransformer, DenotTransformer} import NamerOps.{methodType, linkConstructorParams} import NullOpsDecorator.stripNull import typer.ErrorReporting.err @@ -22,9 +22,48 @@ import reporting.trace object Recheck: + import tpd.Tree + /** Attachment key for rechecked types of TypeTrees */ private val RecheckedType = Property.Key[Type] + extension (sym: Symbol) + + /** Update symbol's info to newInfo from prevPhase.next to lastPhase. + * Reset to previous info for phases after lastPhase. + */ + def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: DenotTransformer, newInfo: Type)(using Context): Unit = + if sym.info ne newInfo then + sym.copySymDenotation().installAfter(lastPhase) // reset + sym.copySymDenotation( + info = newInfo, + initFlags = + if newInfo.isInstanceOf[LazyType] then sym.flags &~ Touched + else sym.flags + ).installAfter(prevPhase) + + /** Does symbol have a new denotation valid from phase.next that is different + * from the denotation it had before? + */ + def isUpdatedAfter(phase: Phase)(using Context) = + val symd = sym.denot + symd.validFor.firstPhaseId == phase.id + 1 && (sym.originDenotation ne symd) + + extension (tree: Tree) + + /** Remember `tpe` as the type of `tree`, which might be different from the + * type stored in the tree itself. + */ + def rememberType(tpe: Type)(using Context): Unit = + if (tpe ne tree.tpe) && !tree.hasAttachment(RecheckedType) then + tree.putAttachment(RecheckedType, tpe) + + /** The remembered type of the tree, or if none was installed, the original type */ + def knownType = + tree.attachmentOrElse(RecheckedType, tree.tpe) + + def hasRememberedType: Boolean = tree.hasAttachment(RecheckedType) + abstract class Recheck extends Phase, IdentityDenotTransformer: thisPhase => @@ -43,9 +82,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: override def widenSkolems = true def run(using Context): Unit = - val rechecker = newRechecker() - rechecker.transformTypes.traverse(ctx.compilationUnit.tpdTree) - rechecker.checkUnit(ctx.compilationUnit) + newRechecker().checkUnit(ctx.compilationUnit) def newRechecker()(using Context): Rechecker @@ -55,120 +92,6 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: ictx.settings.Xprint.value.containsPhase(thisPhase) } - extension (sym: Symbol) def updateInfo(newInfo: Type)(using Context): Unit = - if sym.info ne newInfo then - sym.copySymDenotation().installAfter(thisPhase) // reset - sym.copySymDenotation( - info = newInfo, - initFlags = - if newInfo.isInstanceOf[LazyType] then sym.flags &~ Touched - else sym.flags - ).installAfter(preRecheckPhase) - - extension (tpe: Type) def rememberFor(tree: Tree)(using Context): Unit = - if (tpe ne tree.tpe) && !tree.hasAttachment(RecheckedType) then - tree.putAttachment(RecheckedType, tpe) - - def knownType(tree: Tree) = - tree.attachmentOrElse(RecheckedType, tree.tpe) - - def isUpdated(sym: Symbol)(using Context) = - val symd = sym.denot - symd.validFor.firstPhaseId == thisPhase.id && (sym.originDenotation ne symd) - - def transformType(tp: Type, inferred: Boolean, boxed: Boolean = false)(using Context): Type = tp - - object transformTypes extends TreeTraverser: - - // Substitute parameter symbols in `from` to paramRefs in corresponding - // method or poly types `to`. We use a single BiTypeMap to do everything. - class SubstParams(from: List[List[Symbol]], to: List[LambdaType])(using Context) - extends DeepTypeMap, BiTypeMap: - - def apply(t: Type): Type = t match - case t: NamedType => - val sym = t.symbol - def outer(froms: List[List[Symbol]], tos: List[LambdaType]): Type = - def inner(from: List[Symbol], to: List[ParamRef]): Type = - if from.isEmpty then outer(froms.tail, tos.tail) - else if sym eq from.head then to.head - else inner(from.tail, to.tail) - if tos.isEmpty then t - else inner(froms.head, tos.head.paramRefs) - outer(from, to) - case _ => - mapOver(t) - - def inverse(t: Type): Type = t match - case t: ParamRef => - def recur(from: List[LambdaType], to: List[List[Symbol]]): Type = - if from.isEmpty then t - else if t.binder eq from.head then to.head(t.paramNum).namedType - else recur(from.tail, to.tail) - recur(to, from) - case _ => - mapOver(t) - end SubstParams - - private def transformTT(tree: TypeTree, boxed: Boolean)(using Context) = - transformType(tree.tpe, tree.isInstanceOf[InferredTypeTree], boxed).rememberFor(tree) - - def traverse(tree: Tree)(using Context) = - tree match - case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) => - transformTT(tpt, boxed = true) - traverse(tree.rhs) - case _ => - traverseChildren(tree) - tree match - case tree: TypeTree => - transformTT(tree, boxed = false) - case tree: ValOrDefDef => - val sym = tree.symbol - - // replace an existing symbol info with inferred types - def integrateRT( - info: Type, // symbol info to replace - psymss: List[List[Symbol]], // the local (type and trem) parameter symbols corresponding to `info` - prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order - prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order - ): Type = - info match - case mt: MethodOrPoly => - val psyms = psymss.head - mt.companion(mt.paramNames)( - mt1 => - if !psyms.exists(isUpdated) && !mt.isParamDependent && prevLambdas.isEmpty then - mt.paramInfos - else - val subst = SubstParams(psyms :: prevPsymss, mt1 :: prevLambdas) - psyms.map(psym => subst(psym.info).asInstanceOf[mt.PInfo]), - mt1 => - integrateRT(mt.resType, psymss.tail, psyms :: prevPsymss, mt1 :: prevLambdas) - ) - case info: ExprType => - info.derivedExprType(resType = - integrateRT(info.resType, psymss, prevPsymss, prevLambdas)) - case _ => - val restp = knownType(tree.tpt) - if prevLambdas.isEmpty then restp - else SubstParams(prevPsymss, prevLambdas)(restp) - - if tree.tpt.hasAttachment(RecheckedType) && !sym.isConstructor then - val newInfo = integrateRT(sym.info, sym.paramSymss, Nil, Nil) - .showing(i"update info $sym: ${sym.info} --> $result", recheckr) - if newInfo ne sym.info then - val completer = new LazyType: - def complete(denot: SymDenotation)(using Context) = - denot.info = newInfo - recheckDef(tree, sym) - sym.updateInfo(completer) - case tree: Bind => - val sym = tree.symbol - sym.updateInfo(transformType(sym.info, inferred = true)) - case _ => - end transformTypes - def constFold(tree: Tree, tp: Type)(using Context): Type = val tree1 = tree.withType(tp) val tree2 = ConstFold(tree1) @@ -375,7 +298,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: case tree: ValOrDefDef => if tree.isEmpty then NoType else - if isUpdated(sym) then sym.ensureCompleted() + if sym.isUpdatedAfter(preRecheckPhase) then sym.ensureCompleted() else recheckDef(tree, sym) sym.termRef case tree: TypeDef => @@ -414,7 +337,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = checkConforms(tpe, pt, tree) - if keepTypes then tpe.rememberFor(tree) + if keepTypes then tree.rememberType(tpe) tpe def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 3cdf39cb56e6..bfe93514548e 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -5,26 +5,17 @@ package cc import core._ import Phases.*, DenotTransformers.*, SymDenotations.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* -import Types._ -import Symbols._ -import StdNames._ -import Decorators._ +import Types.*, StdNames.* import config.Printers.{capt, recheckr} import ast.{tpd, untpd, Trees} -import NameKinds.{DocArtifactName, OuterSelectName, DefaultGetterName} -import Trees._ -import scala.util.control.NonFatal -import typer.ErrorReporting._ -import typer.RefChecks -import util.Spans.Span +import Trees.* +import typer.RefChecks.checkAllOverrides import util.{SimpleIdentitySet, EqHashMap, SrcPos} -import util.Chars.* -import transform.* import transform.SymUtils.* +import transform.Recheck +import Recheck.* import scala.collection.mutable -import reporting._ -import dotty.tools.backend.jvm.DottyBackendInterface.symExtensions -import CaptureSet.{CompareResult, withCaptureSetsExplained} +import CaptureSet.withCaptureSetsExplained object CheckCaptures: import ast.tpd.* @@ -108,180 +99,13 @@ class CheckCaptures extends Recheck: // ^^^ TODO: Can we avoid doing overrides checks twice? // We need to do them here since only at this phase CaptureTypes are relevant // But maybe we can then elide the check during the RefChecks phase if -Ycc is set? - RefChecks.checkAllOverrides(ctx.owner.asClass) + checkAllOverrides(ctx.owner.asClass) case _ => traverseChildren(t) class CaptureChecker(ictx: Context) extends Rechecker(ictx): import ast.tpd.* - override def transformType(tp: Type, inferred: Boolean, boxed: Boolean)(using Context): Type = - - def depFun(tycon: Type, argTypes: List[Type], resType: Type): Type = - MethodType.companion( - isContextual = defn.isContextFunctionClass(tycon.classSymbol), - isErased = defn.isErasedFunctionClass(tycon.classSymbol) - )(argTypes, resType) - .toFunctionType(isJava = false, alwaysDependent = true) - - def box(tp: Type): Type = tp match - case CapturingType(parent, refs, false) => CapturingType(parent, refs, true) - case _ => tp - - /** Perform the following transformation steps everywhere in a type: - * 1. Drop retains annotations - * 2. Turn plain function types into dependent function types, so that - * we can refer to their parameter in capture sets. Currently this is - * only done at the toplevel, i.e. for function types that are not - * themselves argument types of other function types. Without this restriction - * boxmap-paper.scala fails. Need to figure out why. - * 3. Refine other class types C by adding capture set variables to their parameter getters - * (see addCaptureRefinements) - * 4. Add capture set variables to all types that can be tracked - * - * Polytype bounds are only cleaned using step 1, but not otherwise transformed. - */ - def mapInferred = new TypeMap: - - /** Drop @retains annotations everywhere */ - object cleanup extends TypeMap: - def apply(t: Type) = t match - case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => - apply(parent) - case _ => - mapOver(t) - - /** Refine a possibly applied class type C where the class has tracked parameters - * x_1: T_1, ..., x_n: T_n to C { val x_1: CV_1 T_1, ..., val x_n: CV_n T_n } - * where CV_1, ..., CV_n are fresh capture sets. - */ - def addCaptureRefinements(tp: Type): Type = tp match - case _: TypeRef | _: AppliedType if tp.typeParams.isEmpty => - tp.typeSymbol match - case cls: ClassSymbol if !defn.isFunctionClass(cls) => - cls.paramGetters.foldLeft(tp) { (core, getter) => - if getter.termRef.isTracked then - val getterType = tp.memberInfo(getter).strippedDealias - RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), boxed = false)) - .showing(i"add capture refinement $tp --> $result", capt) - else - core - } - case _ => tp - case _ => tp - - /** Should a capture set variable be added on type `tp`? */ - def canHaveInferredCapture(tp: Type): Boolean = - tp.typeParams.isEmpty && tp.match - case tp: (TypeRef | AppliedType) => - val sym = tp.typeSymbol - if sym.isClass then !sym.isValueClass && sym != defn.AnyClass - else canHaveInferredCapture(tp.superType.dealias) - case tp: (RefinedOrRecType | MatchType) => - canHaveInferredCapture(tp.underlying) - case tp: AndType => - canHaveInferredCapture(tp.tp1) && canHaveInferredCapture(tp.tp2) - case tp: OrType => - canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2) - case _ => - false - - /** Add a capture set variable to `tp` if necessary, or maybe pull out - * an embedded capture set variables from a part of `tp`. - */ - def addVar(tp: Type) = tp match - case tp @ RefinedType(parent @ CapturingType(parent1, refs, boxed), rname, rinfo) => - CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, boxed) - case tp: RecType => - tp.parent match - case CapturingType(parent1, refs, boxed) => - CapturingType(tp.derivedRecType(parent1), refs, boxed) - case _ => - tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created - // by `mapInferred`. Hence if the underlying type admits capture variables - // a variable was already added, and the first case above would apply. - case AndType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => - assert(refs1.asVar.elems.isEmpty) - assert(refs2.asVar.elems.isEmpty) - assert(boxed1 == boxed2) - CapturingType(AndType(parent1, parent2), refs1, boxed1) - case tp @ OrType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => - assert(refs1.asVar.elems.isEmpty) - assert(refs2.asVar.elems.isEmpty) - assert(boxed1 == boxed2) - CapturingType(OrType(parent1, parent2, tp.isSoft), refs1, boxed1) - case tp @ OrType(CapturingType(parent1, refs1, boxed1), tp2) => - CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, boxed1) - case tp @ OrType(tp1, CapturingType(parent2, refs2, boxed2)) => - CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, boxed2) - case _ if canHaveInferredCapture(tp) => - CapturingType(tp, CaptureSet.Var(), boxed = false) - case _ => - tp - - var isTopLevel = true - - def mapNested(ts: List[Type]): List[Type] = - val saved = isTopLevel - isTopLevel = false - try ts.mapConserve(this) finally isTopLevel = saved - - def apply(t: Type) = - val t1 = t match - case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => - apply(parent) - case tp @ AppliedType(tycon, args) => - val tycon1 = this(tycon) - if defn.isNonRefinedFunction(tp) then - val args1 = mapNested(args.init) - val res1 = this(args.last) - if isTopLevel then - depFun(tycon1, args1, res1) - .showing(i"add function refinement $tp --> $result", capt) - else - tp.derivedAppliedType(tycon1, args1 :+ res1) - else - tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) - case tp @ RefinedType(core, rname, rinfo) if defn.isFunctionType(tp) => - apply(rinfo).toFunctionType(isJava = false, alwaysDependent = true) - case tp: MethodType => - tp.derivedLambdaType( - paramInfos = mapNested(tp.paramInfos), - resType = this(tp.resType)) - case tp: TypeLambda => - // Don't recurse into parameter bounds, just cleanup any stray retains annotations - tp.derivedLambdaType( - paramInfos = tp.paramInfos.mapConserve(cleanup(_).bounds), - resType = this(tp.resType)) - case _ => - mapOver(t) - addVar(addCaptureRefinements(t1)) - end mapInferred - - if inferred then - val tp1 = mapInferred(tp) - if boxed then box(tp1) else tp1 - else - def setBoxed(t: Type) = t match - case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => - annot.tree.setBoxedCapturing() - case _ => - - val addBoxes = new TypeTraverser: - def traverse(t: Type) = - t match - case AppliedType(tycon, args) if !defn.isNonRefinedFunction(t) => - args.foreach(setBoxed) - case TypeBounds(lo, hi) => - setBoxed(lo); setBoxed(hi) - case _ => - traverseChildren(t) - - if boxed then setBoxed(tp) - addBoxes.traverse(tp) - tp - end transformType - private def interpolator(using Context) = new TypeTraverser: override def traverse(t: Type) = t match @@ -299,8 +123,8 @@ class CheckCaptures extends Recheck: private def interpolateVarsIn(tpt: Tree)(using Context): Unit = if tpt.isInstanceOf[InferredTypeTree] then - interpolator.traverse(knownType(tpt)) - .showing(i"solved vars in ${knownType(tpt)}", capt) + interpolator.traverse(tpt.knownType) + .showing(i"solved vars in ${tpt.knownType}", capt) private var curEnv: Env = Env(NoSymbol, CaptureSet.empty, false, null) @@ -477,6 +301,8 @@ class CheckCaptures extends Recheck: res override def checkUnit(unit: CompilationUnit)(using Context): Unit = + Setup(preRecheckPhase, thisPhase, recheckDef) + .traverse(ctx.compilationUnit.tpdTree) withCaptureSetsExplained { super.checkUnit(unit) PostRefinerCheck.traverse(unit.tpdTree) @@ -494,12 +320,12 @@ class CheckCaptures extends Recheck: val notAllowed = i" is not allowed to capture the $what capability $ref" def msg = if allArgs.isEmpty then - i"type of mutable variable ${knownType(tree)}$notAllowed" + i"type of mutable variable ${tree.knownType}$notAllowed" else tree match case tree: InferredTypeTree => - i"""inferred type argument ${knownType(tree)}$notAllowed + i"""inferred type argument ${tree.knownType}$notAllowed | - |The inferred arguments are: [${allArgs.map(knownType)}%, %]""" + |The inferred arguments are: [${allArgs.map(_.knownType)}%, %]""" case _ => s"type argument$notAllowed" report.error(msg, tree.srcPos) @@ -509,7 +335,7 @@ class CheckCaptures extends Recheck: case LambdaTypeTree(_, restpt) => checkNotGlobal(restpt, allArgs*) case _ => - checkNotGlobal(tree, knownType(tree), allArgs*) + checkNotGlobal(tree, tree.knownType, allArgs*) def checkNotGlobalDeep(tree: Tree)(using Context): Unit = val checker = new TypeTraverser: @@ -522,23 +348,23 @@ class CheckCaptures extends Recheck: case _ => checkNotGlobal(tree, tp) traverseChildren(tp) - checker.traverse(knownType(tree)) + checker.traverse(tree.knownType) object PostRefinerCheck extends TreeTraverser: def traverse(tree: Tree)(using Context) = tree match case _: InferredTypeTree => case tree: TypeTree if !tree.span.isZeroExtent => - knownType(tree).foreachPart( + tree.knownType.foreachPart( checkWellformedPost(_, tree.srcPos)) - knownType(tree).foreachPart { + tree.knownType.foreachPart { case AnnotatedType(_, annot) => checkWellformedPost(annot.tree) case _ => } case tree1 @ TypeApply(fn, args) if disallowGlobal => for arg <- args do - //println(i"checking $arg in $tree: ${knownType(tree).captureSet}") + //println(i"checking $arg in $tree: ${tree.knownType.captureSet}") checkNotGlobal(arg, args*) case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] => val sym = t.symbol @@ -551,7 +377,7 @@ class CheckCaptures extends Recheck: || sym.owner.is(Trait) // ... since we do OverridingPairs checking before capture inference || sym.allOverriddenSymbols.nonEmpty // ... since we do override checking before capture inference then - val inferred = knownType(t.tpt) + val inferred = t.tpt.knownType def checkPure(tp: Type) = tp match case CapturingType(_, refs, _) if !refs.elems.isEmpty => val resultStr = if t.isInstanceOf[DefDef] then " result" else "" diff --git a/tests/neg-custom-args/captures/curried-simplified.check b/tests/neg-custom-args/captures/curried-simplified.check new file mode 100644 index 000000000000..055558530a76 --- /dev/null +++ b/tests/neg-custom-args/captures/curried-simplified.check @@ -0,0 +1,42 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:7:28 ---------------------------- +7 | def y1: () -> () -> Int = x1 // error + | ^^ + | Found: {x} () -> {x} () -> Int + | Required: () -> () -> Int + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:9:28 ---------------------------- +9 | def y2: () -> () => Int = x2 // error + | ^^ + | Found: {x} () -> () => Int + | Required: () -> () => Int + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:11:39 --------------------------- +11 | def y3: Cap -> Protect[Int -> Int] = x3 // error + | ^^ + | Found: (x$0: Cap) -> {x$0} Int -> Int + | Required: Cap -> Protect[Int -> Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:15:33 --------------------------- +15 | def y5: Cap -> {} Int -> Int = x5 // error + | ^^ + | Found: Cap -> {x} Int -> Int + | Required: Cap -> {} Int -> Int + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:17:49 --------------------------- +17 | def y6: Cap -> {} Cap -> Protect[Int -> Int] = x6 // error + | ^^ + | Found: (x$0: Cap) -> {x$0} (x$0: Cap) -> {x$0, x$0} Int -> Int + | Required: Cap -> {} Cap -> Protect[Int -> Int] + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:19:49 --------------------------- +19 | def y7: Cap -> Protect[Cap -> {} Int -> Int] = x7 // error + | ^^ + | Found: (x$0: Cap) -> {x$0} (x: Cap) -> {x$0, x} Int -> Int + | Required: Cap -> Protect[Cap -> {} Int -> Int] + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/curried-simplified.scala b/tests/neg-custom-args/captures/curried-simplified.scala new file mode 100644 index 000000000000..25b23370d154 --- /dev/null +++ b/tests/neg-custom-args/captures/curried-simplified.scala @@ -0,0 +1,21 @@ +@annotation.capability class Cap + +type Protect[T] = T + +def test(x: Cap, y: Cap) = + def x1: {x} () -> () -> Int = ??? + def y1: () -> () -> Int = x1 // error + def x2: {x} () -> () => Int = ??? + def y2: () -> () => Int = x2 // error + def x3: Cap -> Int -> Int = ??? + def y3: Cap -> Protect[Int -> Int] = x3 // error + def x4: Cap -> Protect[Int -> Int] = ??? + def y4: Cap -> {} Int -> Int = x4 // ok + def x5: Cap -> {x} Int -> Int = ??? + def y5: Cap -> {} Int -> Int = x5 // error + def x6: Cap -> Cap -> Int -> Int = ??? + def y6: Cap -> {} Cap -> Protect[Int -> Int] = x6 // error + def x7: Cap -> (x: Cap) -> Int -> Int = ??? + def y7: Cap -> Protect[Cap -> {} Int -> Int] = x7 // error + + diff --git a/tests/pos-custom-args/captures/capt-depfun.scala b/tests/pos-custom-args/captures/capt-depfun.scala index 072eaefd3e78..808b1b5e85f3 100644 --- a/tests/pos-custom-args/captures/capt-depfun.scala +++ b/tests/pos-custom-args/captures/capt-depfun.scala @@ -3,6 +3,8 @@ type Cap = C @retains(*) type T = (x: Cap) -> String @retains(x) +type ID[X] = X + val aa: ((x: Cap) -> String @retains(x)) = (x: Cap) => "" def f(y: Cap, z: Cap): String @retains(*) = @@ -12,7 +14,7 @@ def f(y: Cap, z: Cap): String @retains(*) = def g(): C @retains(y, z) = ??? val d = a(g()) - val ac: ((x: Cap) -> String @retains(x) -> String @retains(x)) = ??? + val ac: ((x: Cap) -> ID[String @retains(x) -> String @retains(x)]) = ??? val bc: (({y} String) -> {y} String) = ac(y) val dc: (String -> {y, z} String) = ac(g()) c From 62095a6d531910ad0fbb585c254a76a24e93c95f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 26 Jan 2022 16:26:19 +0100 Subject: [PATCH 14/99] Syntax change: allow capture sets in infix types --- .../src/dotty/tools/dotc/parsing/Parsers.scala | 16 ++++++++++------ tests/disabled/pos/lazylist.scala | 2 +- tests/pos-custom-args/captures/classes.scala | 4 ++-- tests/pos-custom-args/captures/iterators.scala | 2 +- tests/pos-custom-args/captures/lazylists.scala | 8 ++++---- tests/pos-custom-args/captures/lazylists1.scala | 4 ++-- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c3d5f6cea973..342f3db9ecc5 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1551,11 +1551,7 @@ object Parsers { else { accept(TLARROW); typ() } } else if in.token == LBRACE && followingIsCaptureSet() then - val refs = inBraces { - if in.token == RBRACE then Nil else commaSeparated(captureRef) - } - val t = typ() - CapturingTypeTree(refs, t) + CapturingTypeTree(captureSet(), typ()) else if (in.token == INDENT) enclosed(INDENT, typ()) else infixType() @@ -1941,8 +1937,14 @@ object Parsers { def typeDependingOn(location: Location): Tree = if location.inParens then typ() else if location.inPattern then rejectWildcardType(refinedType()) + else if in.token == LBRACE && followingIsCaptureSet() then + CapturingTypeTree(captureSet(), infixType()) else infixType() + def captureSet(): List[Tree] = inBraces { + if in.token == RBRACE then Nil else commaSeparated(captureRef) + } + /* ----------- EXPRESSIONS ------------------------------------------------ */ /** Does the current conditional expression continue after @@ -2012,7 +2014,7 @@ object Parsers { * | ‘inline’ InfixExpr MatchClause * Bindings ::= `(' [Binding {`,' Binding}] `)' * Binding ::= (id | `_') [`:' Type] - * Ascription ::= `:' InfixType + * Ascription ::= `:' [CaptureSet] InfixType * | `:' Annotation {Annotation} * | `:' `_' `*' * Catches ::= ‘catch’ (Expr | ExprCaseClause) @@ -4035,6 +4037,8 @@ object Parsers { * | * EnumStat ::= TemplateStat * | Annotations Modifiers EnumCase + * SelfType ::= id [‘:’ [CaptureSet] InfixType] ‘=>’ + * | ‘this’ ‘:’ [CaptureSet] InfixType ‘=>’ */ def templateStatSeq(): (ValDef, List[Tree]) = checkNoEscapingPlaceholders { val stats = new ListBuffer[Tree] diff --git a/tests/disabled/pos/lazylist.scala b/tests/disabled/pos/lazylist.scala index 958f4c35aaf0..c24f8677b91f 100644 --- a/tests/disabled/pos/lazylist.scala +++ b/tests/disabled/pos/lazylist.scala @@ -1,7 +1,7 @@ package lazylists abstract class LazyList[+T]: - this: ({*} LazyList[T]) => + this: {*} LazyList[T] => def isEmpty: Boolean def head: T diff --git a/tests/pos-custom-args/captures/classes.scala b/tests/pos-custom-args/captures/classes.scala index f3d6e44b27ca..243f70e02899 100644 --- a/tests/pos-custom-args/captures/classes.scala +++ b/tests/pos-custom-args/captures/classes.scala @@ -1,7 +1,7 @@ class B type Cap = {*} B class C(val n: Cap): - this: ({n} C) => + this: {n} C => def foo(): {n} B = n @@ -9,7 +9,7 @@ def test(x: Cap, y: Cap, z: Cap) = val c0 = C(x) val c1: {x} C {val n: {x} B} = c0 val d = c1.foo() - d: ({x} B) + d: {x} B val c2 = if ??? then C(x) else C(y) val c2a = identity(c2) diff --git a/tests/pos-custom-args/captures/iterators.scala b/tests/pos-custom-args/captures/iterators.scala index 1ac1bd96f6d7..50be2012e25c 100644 --- a/tests/pos-custom-args/captures/iterators.scala +++ b/tests/pos-custom-args/captures/iterators.scala @@ -1,7 +1,7 @@ package cctest abstract class Iterator[T]: - thisIterator: ({*} Iterator[T]) => + thisIterator: {*} Iterator[T] => def hasNext: Boolean def next: T diff --git a/tests/pos-custom-args/captures/lazylists.scala b/tests/pos-custom-args/captures/lazylists.scala index bf3e0300b5b5..c566bea8dd64 100644 --- a/tests/pos-custom-args/captures/lazylists.scala +++ b/tests/pos-custom-args/captures/lazylists.scala @@ -2,7 +2,7 @@ class CC type Cap = {*} CC trait LazyList[+A]: - this: ({*} LazyList[A]) => + this: {*} LazyList[A] => def isEmpty: Boolean def head: A @@ -16,12 +16,12 @@ object LazyNil extends LazyList[Nothing]: extension [A](xs: {*} LazyList[A]) def map[B](f: A => B): {xs, f} LazyList[B] = final class Mapped extends LazyList[B]: - this: ({xs, f} Mapped) => + this: {xs, f} Mapped => def isEmpty = false def head: B = f(xs.head) def tail: {this} LazyList[B] = xs.tail.map(f) // OK - def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : ({xs, f} LazyList[A]) // OK + def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : {xs, f} LazyList[A] // OK if xs.isEmpty then LazyNil else new Mapped @@ -31,7 +31,7 @@ def test(cap1: Cap, cap2: Cap) = val xs = class Initial extends LazyList[String]: - this: ({cap1} Initial) => + this: {cap1} Initial => def isEmpty = false def head = f("") diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala index 7fbdde87ad9b..2dbb5ac232e2 100644 --- a/tests/pos-custom-args/captures/lazylists1.scala +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -2,7 +2,7 @@ class CC type Cap = {*} CC trait LazyList[+A]: - this: ({*} LazyList[A]) => + this: {*} LazyList[A] => def isEmpty: Boolean def head: A @@ -14,7 +14,7 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? final class LazyCons[+T](val x: T, val xs: Int => {*} LazyList[T]) extends LazyList[T]: - this: ({*} LazyList[T]) => + this: {*} LazyList[T] => def isEmpty = false def head = x From 9c485e55b4db2979db3c47f6502194350a607096 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 26 Jan 2022 16:26:44 +0100 Subject: [PATCH 15/99] Test for contravariantly used class fields As discussed in the CC meeting on 21 Jan 2022 --- .../dotty/tools/dotc/core/tasty/TreePickler.scala | 1 + tests/neg-custom-args/captures/class-contra.check | 7 +++++++ tests/neg-custom-args/captures/class-contra.scala | 13 +++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 tests/neg-custom-args/captures/class-contra.check create mode 100644 tests/neg-custom-args/captures/class-contra.scala diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 475a258e8330..27fb9f68da32 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -208,6 +208,7 @@ class TreePickler(pickler: TastyPickler) { writeByte(if (tpe.isType) TYPEREFdirect else TERMREFdirect) if Config.checkLevelsOnConstraints && !symRefs.contains(sym) && !sym.isPatternBound && !sym.hasAnnotation(defn.QuotedRuntimePatterns_patternTypeAnnot) then report.error(i"pickling reference to as yet undefined $tpe with symbol ${sym}", sym.srcPos) + // todo: find out why this happens for pos-customargs/captures/capt2 pickleSymRef(sym) } else tpe.designator match { diff --git a/tests/neg-custom-args/captures/class-contra.check b/tests/neg-custom-args/captures/class-contra.check new file mode 100644 index 000000000000..3825d57b602e --- /dev/null +++ b/tests/neg-custom-args/captures/class-contra.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-contra.scala:12:39 --------------------------------- +12 | def fun(x: K{val f: {a} T}) = x.setf(a) // error + | ^ + | Found: (a : {x, y} T) + | Required: T + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/class-contra.scala b/tests/neg-custom-args/captures/class-contra.scala new file mode 100644 index 000000000000..270aaf9309a9 --- /dev/null +++ b/tests/neg-custom-args/captures/class-contra.scala @@ -0,0 +1,13 @@ + +class C +type Cap = {*} C + +class K(val f: {*} T): + def setf(x: {f} T) = ??? + +class T + +def test(x: Cap, y: Cap) = + val a: {x, y} T = ??? + def fun(x: K{val f: {a} T}) = x.setf(a) // error + () \ No newline at end of file From 4074217fa363c30953344f3731274d6ebca2b863 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 23 Jan 2022 16:21:24 +0100 Subject: [PATCH 16/99] Handle captures in by-name parameters 1. Infrastructure to deal with capturesets in byname parameters 2. Handle retainsByName annotations in ElimByName Convert them to regular annotations on the generated function types. This enables capture checking on by-name parameters. 3. Add a style warning for misleading by-name parameter type formatting. By-name types should be formatted `{...}-> T`. `{...} -> T` looks too much like a function type. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 14 ++-- .../src/dotty/tools/dotc/ast/TreeInfo.scala | 23 ++++++- .../tools/dotc/cc/CaptureAnnotation.scala | 20 +++--- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 7 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 5 +- .../dotty/tools/dotc/cc/CapturingKind.scala | 9 +++ .../dotty/tools/dotc/cc/CapturingType.scala | 38 +++++++--- compiler/src/dotty/tools/dotc/cc/Setup.scala | 7 +- .../dotty/tools/dotc/core/Definitions.scala | 20 ++++-- .../src/dotty/tools/dotc/core/StdNames.scala | 1 + .../dotty/tools/dotc/core/TypeComparer.scala | 4 +- .../src/dotty/tools/dotc/core/Types.scala | 15 ++-- .../dotty/tools/dotc/parsing/Parsers.scala | 69 ++++++++++++++----- .../tools/dotc/printing/PlainPrinter.scala | 15 ++-- .../tools/dotc/printing/RefinedPrinter.scala | 9 ++- .../dotty/tools/dotc/reporting/messages.scala | 2 +- .../tools/dotc/typer/ErrorReporting.scala | 8 ++- .../src/dotty/tools/dotc/typer/Typer.scala | 3 +- .../scala/retainsByName.scala | 6 ++ tests/neg-custom-args/captures/byname.check | 20 ++++++ tests/neg-custom-args/captures/byname.scala | 10 +++ tests/neg-custom-args/captures/lazylist.check | 4 +- .../neg-custom-args/captures/lazylists2.check | 4 +- tests/pos-custom-args/captures/byname.scala | 4 +- 24 files changed, 233 insertions(+), 84 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/CapturingKind.scala create mode 100644 library/src-bootstrapped/scala/retainsByName.scala create mode 100644 tests/neg-custom-args/captures/byname.check diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index bc3baa2c6c54..b8b81b565e6c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -468,7 +468,7 @@ object desugar { if mods.is(Trait) then for vparams <- originalVparamss; vparam <- vparams do - if vparam.tpt.isInstanceOf[ByNameTypeTree] then + if isByNameType(vparam.tpt) then report.error(em"implementation restriction: traits cannot have by name parameters", vparam.srcPos) // Annotations on class _type_ parameters are set on the derived parameters @@ -576,9 +576,8 @@ object desugar { appliedTypeTree(tycon, targs) } - def isRepeated(tree: Tree): Boolean = tree match { + def isRepeated(tree: Tree): Boolean = stripByNameType(tree) match { case PostfixOp(_, Ident(tpnme.raw.STAR)) => true - case ByNameTypeTree(tree1) => isRepeated(tree1) case _ => false } @@ -1811,8 +1810,13 @@ object desugar { case ext: ExtMethods => Block(List(ext), Literal(Constant(())).withSpan(ext.span)) case CapturingTypeTree(refs, parent) => - val annot = New(scalaDot(tpnme.retains), List(refs)) - Annotated(parent, annot) + def annotate(annotName: TypeName, tp: Tree) = + Annotated(tp, New(scalaDot(annotName), List(refs))) + parent match + case ByNameTypeTree(restpt) => + cpy.ByNameTypeTree(parent)(annotate(tpnme.retainsByName, restpt)) + case _ => + annotate(tpnme.retains, parent) } desugared.withSpan(tree.span) } diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 553a73a6f4c6..43d40779982a 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -178,8 +178,7 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => } /** Is tpt a vararg type of the form T* or => T*? */ - def isRepeatedParamType(tpt: Tree)(using Context): Boolean = tpt match { - case ByNameTypeTree(tpt1) => isRepeatedParamType(tpt1) + def isRepeatedParamType(tpt: Tree)(using Context): Boolean = stripByNameType(tpt) match { case tpt: TypeTree => tpt.typeOpt.isRepeatedParam case AppliedTypeTree(Select(_, tpnme.REPEATED_PARAM_CLASS), _) => true case _ => false @@ -196,6 +195,16 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => case arg => arg.typeOpt.widen.isRepeatedParam } + def isByNameType(tree: Tree)(using Context): Boolean = + stripByNameType(tree) ne tree + + def stripByNameType(tree: Tree)(using Context): Tree = unsplice(tree) match + case ByNameTypeTree(t1) => t1 + case untpd.CapturingTypeTree(_, parent) => + val parent1 = stripByNameType(parent) + if parent1 eq parent then tree else parent1 + case _ => tree + /** All type and value parameter symbols of this DefDef */ def allParamSyms(ddef: DefDef)(using Context): List[Symbol] = ddef.paramss.flatten.map(_.symbol) @@ -388,6 +397,16 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] case _ => None } } + + object ImpureByNameTypeTree: + def apply(tp: ByNameTypeTree)(using Context): untpd.CapturingTypeTree = + untpd.CapturingTypeTree( + Ident(nme.CAPTURE_ROOT).withSpan(tp.span.startPos) :: Nil, tp) + def unapply(tp: Tree)(using Context): Option[ByNameTypeTree] = tp match + case untpd.CapturingTypeTree(id @ Ident(nme.CAPTURE_ROOT) :: Nil, bntp: ByNameTypeTree) + if id.span == bntp.span.startPos => Some(bntp) + case _ => None + end ImpureByNameTypeTree } trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] => diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index 5f73b50a6bbe..9f4f99ad52f3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -12,7 +12,7 @@ import printing.Printer import printing.Texts.Text -case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean) extends Annotation: +case class CaptureAnnotation(refs: CaptureSet, kind: CapturingKind) extends Annotation: import CaptureAnnotation.* import tpd.* @@ -25,17 +25,18 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean) extends Annotatio val arg = repeated(elems, TypeTree(defn.AnyType)) New(symbol.typeRef, arg :: Nil) - override def symbol(using Context) = defn.RetainsAnnot + override def symbol(using Context) = + if kind == CapturingKind.ByName then defn.RetainsByNameAnnot else defn.RetainsAnnot override def derivedAnnotation(tree: Tree)(using Context): Annotation = unsupported("derivedAnnotation(Tree)") - def derivedAnnotation(refs: CaptureSet, boxed: Boolean)(using Context): Annotation = - if (this.refs eq refs) && (this.boxed == boxed) then this - else CaptureAnnotation(refs, boxed) + def derivedAnnotation(refs: CaptureSet, kind: CapturingKind)(using Context): Annotation = + if (this.refs eq refs) && (this.kind == kind) then this + else CaptureAnnotation(refs, kind) override def sameAnnotation(that: Annotation)(using Context): Boolean = that match - case CaptureAnnotation(refs2, boxed2) => refs == refs2 && boxed == boxed2 + case CaptureAnnotation(refs2, kind2) => refs == refs2 && kind == kind2 case _ => false override def mapWith(tp: TypeMap)(using Context) = @@ -43,7 +44,7 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean) extends Annotatio val elems1 = elems.mapConserve(tp) if elems1 eq elems then this else if elems1.forall(_.isInstanceOf[CaptureRef]) - then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), boxed) + then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), kind) else EmptyAnnotation override def refersToParamOf(tl: TermLambda)(using Context): Boolean = @@ -54,10 +55,11 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean) extends Annotatio override def toText(printer: Printer): Text = refs.toText(printer) - override def hash: Int = (refs.hashCode << 1) | (if boxed then 1 else 0) + override def hash: Int = + (refs.hashCode << 1) | (if kind == CapturingKind.Regular then 0 else 1) override def eql(that: Annotation) = that match - case that: CaptureAnnotation => (this.refs eq that.refs) && (this.boxed == boxed) + case that: CaptureAnnotation => (this.refs eq that.refs) && (this.kind == kind) case _ => false end CaptureAnnotation diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 4c201d7edf54..117b6e528e62 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -43,9 +43,9 @@ extension (tree: Tree) extension (tp: Type) def derivedCapturingType(parent: Type, refs: CaptureSet)(using Context): Type = tp match - case CapturingType(p, r, b) => + case CapturingType(p, r, k) => if (parent eq p) && (refs eq r) then tp - else CapturingType(parent, refs, b) + else CapturingType(parent, refs, k) /** If this is type variable instantiated or upper bounded with a capturing type, * the capture set associated with that type. Extended to and-or types and @@ -54,7 +54,8 @@ extension (tp: Type) */ def boxedCaptured(using Context): CaptureSet = def getBoxed(tp: Type): CaptureSet = tp match - case CapturingType(_, refs, boxed) => if boxed then refs else CaptureSet.empty + case CapturingType(_, refs, CapturingKind.Boxed) => refs + case CapturingType(_, _, _) => CaptureSet.empty case tp: TypeProxy => getBoxed(tp.superType) case tp: AndType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) case tp: OrType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 82e5e6e14a4b..6118e54174cd 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -209,8 +209,9 @@ sealed abstract class CaptureSet extends Showable: ((NoType: Type) /: elems) ((tp, ref) => if tp.exists then OrType(tp, ref, soft = false) else ref) - def toRegularAnnotation(using Context): Annotation = - Annotation(CaptureAnnotation(this, boxed = false).tree) + def toRegularAnnotation(byName: Boolean)(using Context): Annotation = + val kind = if byName then CapturingKind.ByName else CapturingKind.Regular + Annotation(CaptureAnnotation(this, kind).tree) override def toText(printer: Printer): Text = Str("{") ~ Text(elems.toList.map(printer.toTextCaptureRef), ", ") ~ Str("}") diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingKind.scala b/compiler/src/dotty/tools/dotc/cc/CapturingKind.scala new file mode 100644 index 000000000000..3bb00b110b21 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/CapturingKind.scala @@ -0,0 +1,9 @@ +package dotty.tools +package dotc +package cc + +/** Possible kinds of captures */ +enum CapturingKind: + case Regular // normal capture + case Boxed // capture under box + case ByName // capture applies to enclosing by-name type (only possible before ElimByName) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index 738e746d0178..bca791e46205 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -5,26 +5,46 @@ package cc import core.* import Types.*, Symbols.*, Contexts.* +/** A capturing type. This is internally represented as an annotated type with a `retains` + * annotation, but the extractor will succeed only at phase CheckCaptures. + * Annotated types with `@retainsByName` annotation can also be created that way, by + * giving a `CapturingKind.ByName` as `kind` argument, but they are never extracted, + * since they have already been converted to regular capturing types before CheckCaptures. + */ object CapturingType: - def apply(parent: Type, refs: CaptureSet, boxed: Boolean)(using Context): Type = + def apply(parent: Type, refs: CaptureSet, kind: CapturingKind)(using Context): Type = if refs.isAlwaysEmpty then parent - else AnnotatedType(parent, CaptureAnnotation(refs, boxed)) - - def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, Boolean)] = - if ctx.phase == Phases.checkCapturesPhase then EventuallyCapturingType.unapply(tp) + else AnnotatedType(parent, CaptureAnnotation(refs, kind)) + + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, CapturingKind)] = + if ctx.phase == Phases.checkCapturesPhase then + val r = EventuallyCapturingType.unapply(tp) + r match + case Some((_, _, CapturingKind.ByName)) => None + case _ => r else None end CapturingType +/** An extractor for types that will be capturing types at phase CheckCaptures. Also + * included are types that indicate captures on enclosing call-by-name parameters + * before phase ElimByName + */ object EventuallyCapturingType: - def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, Boolean)] = - if tp.annot.symbol == defn.RetainsAnnot then + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, CapturingKind)] = + val sym = tp.annot.symbol + if sym == defn.RetainsAnnot || sym == defn.RetainsByNameAnnot then tp.annot match - case ann: CaptureAnnotation => Some((tp.parent, ann.refs, ann.boxed)) + case ann: CaptureAnnotation => + Some((tp.parent, ann.refs, ann.kind)) case ann => - try Some((tp.parent, ann.tree.toCaptureSet, ann.tree.isBoxedCapturing)) + val kind = + if ann.tree.isBoxedCapturing then CapturingKind.Boxed + else if sym == defn.RetainsByNameAnnot then CapturingKind.ByName + else CapturingKind.Regular + try Some((tp.parent, ann.tree.toCaptureSet, kind)) catch case ex: IllegalCaptureRef => None else None diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index b5ccb002e209..622baff6f2a3 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -25,7 +25,8 @@ extends tpd.TreeTraverser: .toFunctionType(isJava = false, alwaysDependent = true) private def box(tp: Type)(using Context): Type = tp match - case CapturingType(parent, refs, false) => CapturingType(parent, refs, true) + case CapturingType(parent, refs, CapturingKind.Regular) => + CapturingType(parent, refs, CapturingKind.Boxed) case _ => tp private def setBoxed(tp: Type)(using Context) = tp match @@ -77,7 +78,7 @@ extends tpd.TreeTraverser: cls.paramGetters.foldLeft(tp) { (core, getter) => if getter.termRef.isTracked then val getterType = tp.memberInfo(getter).strippedDealias - RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), boxed = false)) + RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), CapturingKind.Regular)) .showing(i"add capture refinement $tp --> $result", capt) else core @@ -130,7 +131,7 @@ extends tpd.TreeTraverser: case tp @ OrType(tp1, CapturingType(parent2, refs2, boxed2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, boxed2) case _ if canHaveInferredCapture(tp) => - CapturingType(tp, CaptureSet.Var(), boxed = false) + CapturingType(tp, CaptureSet.Var(), CapturingKind.Regular) case _ => tp diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 5c1fa371da75..cc2dc830ab94 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.CommentsContext import Comments.Comment import util.Spans.NoSpan import Symbols.requiredModuleRef -import cc.{CapturingType, CaptureSet} +import cc.{CapturingType, CaptureSet, CapturingKind, EventuallyCapturingType} import scala.annotation.tailrec @@ -118,9 +118,9 @@ class Definitions { * * ErasedFunctionN and ErasedContextFunctionN erase to Function0. * - * EffXYZFunctionN afollow this template: + * ImpureXYZFunctionN follow this template: * - * type EffXYZFunctionN[-T0,...,-T{N-1}, +R] = {*} XYZFunctionN[T0,...,T{N-1}, R] + * type ImpureXYZFunctionN[-T0,...,-T{N-1}, +R] = {*} XYZFunctionN[T0,...,T{N-1}, R] */ private def newFunctionNType(name: TypeName): Symbol = { val impure = name.startsWith("Impure") @@ -136,7 +136,7 @@ class Definitions { HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)( tl => List.fill(arity + 1)(TypeBounds.empty), tl => CapturingType(underlyingClass.typeRef.appliedTo(tl.paramRefs), - CaptureSet.universal, boxed = false) + CaptureSet.universal, CapturingKind.Regular) )) else val cls = denot.asClass.classSymbol @@ -1016,6 +1016,7 @@ class Definitions { @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val SinceAnnot: ClassSymbol = requiredClass("scala.annotation.since") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.retains") + @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.retainsByName") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") @@ -1149,9 +1150,16 @@ class Definitions { } } + /** Extractor for function types representing by-name parameters, of the form + * `() ?=> T`. + * Under -Ycc, this becomes `() ?-> T` or `{r1, ..., rN} () ?-> T`. + */ object ByNameFunction: - def apply(tp: Type)(using Context): Type = - defn.ContextFunction0.typeRef.appliedTo(tp :: Nil) + def apply(tp: Type)(using Context): Type = tp match + case EventuallyCapturingType(tp1, refs, CapturingKind.ByName) => + CapturingType(apply(tp1), refs, CapturingKind.Regular) + case _ => + defn.ContextFunction0.typeRef.appliedTo(tp :: Nil) def unapply(tp: Type)(using Context): Option[Type] = tp match case tp @ AppliedType(tycon, arg :: Nil) if defn.isByNameFunctionClass(tycon.typeSymbol) => Some(arg) diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 3bfb3a734083..81e130779912 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -566,6 +566,7 @@ object StdNames { val reify : N = "reify" val releaseFence : N = "releaseFence" val retains: N = "retains" + val retainsByName: N = "retainsByName" val rootMirror : N = "rootMirror" val run: N = "run" val runOrElse: N = "runOrElse" diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 320c2202f53b..65a3d0b106d0 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,7 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly -import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing} +import cc.{CapturingType, derivedCapturingType, CaptureSet, CapturingKind, stripCapturing} /** Provides methods to compare types. */ @@ -884,7 +884,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling tp1 match case tp1: CaptureRef if tp1.isTracked => val stripped = tp1w.stripCapturing - tp1w = CapturingType(stripped, tp1.singletonCaptureSet, boxed = false) + tp1w = CapturingType(stripped, tp1.singletonCaptureSet, CapturingKind.Regular) case _ => isSubType(tp1w, tp2, approx.addLow) } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 53a8196d927b..864e6ec8a020 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,7 +36,7 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized -import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing} +import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, CapturingKind} import CaptureSet.CompareResult import scala.annotation.internal.sharable @@ -1880,13 +1880,15 @@ object Types { def capturing(ref: CaptureRef)(using Context): Type = if captureSet.accountsFor(ref) then this - else CapturingType(this, ref.singletonCaptureSet, this.isBoxedCapturing) + else CapturingType(this, ref.singletonCaptureSet, + if this.isBoxedCapturing then CapturingKind.Boxed else CapturingKind.Regular) def capturing(cs: CaptureSet)(using Context): Type = if cs.isConst && cs.subCaptures(captureSet, frozen = true).isOK then this else this match case CapturingType(parent, cs1, boxed) => parent.capturing(cs1 ++ cs) - case _ => CapturingType(this, cs, this.isBoxedCapturing) + case _ => CapturingType(this, cs, + if this.isBoxedCapturing then CapturingKind.Boxed else CapturingKind.Regular) /** The set of distinct symbols referred to by this type, after all aliases are expanded */ def coveringSet(using Context): Set[Symbol] = @@ -3840,10 +3842,11 @@ object Types { CapturingType(parent1, CaptureSet.universal, boxed)) case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => val parent1 = mapOver(parent) - if ann.symbol == defn.RetainsAnnot then + if ann.symbol == defn.RetainsAnnot || ann.symbol == defn.RetainsByNameAnnot then + val byName = ann.symbol == defn.RetainsByNameAnnot range( - AnnotatedType(parent1, CaptureSet.empty.toRegularAnnotation), - AnnotatedType(parent1, CaptureSet.universal.toRegularAnnotation)) + AnnotatedType(parent1, CaptureSet.empty.toRegularAnnotation(byName)), + AnnotatedType(parent1, CaptureSet.universal.toRegularAnnotation(byName))) else parent1 case _ => mapOver(tp) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 342f3db9ecc5..8b99b3c1d239 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -1473,8 +1473,9 @@ object Parsers { val resultType = typ() if token == TLARROW then - for case ValDef(_, tpt: ByNameTypeTree, _) <- params do - syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span) + for case ValDef(_, tpt, _) <- params do + if isByNameType(tpt) then + syntaxError(em"parameter of type lambda may not be call-by-name", tpt.span) TermLambdaTypeTree(params.asInstanceOf[List[ValDef]], resultType) else if imods.isOneOf(Given | Erased | Impure) then if imods.is(Given) && params.isEmpty then @@ -1513,15 +1514,13 @@ object Parsers { if isValParamList || in.isArrow then functionRest(ts) else { - val ts1 = - for (t <- ts) yield - t match { - case t@ByNameTypeTree(t1) => - syntaxError(ByNameParameterNotSupported(t), t.span) - t1 - case _ => - t - } + val ts1 = ts.mapConserve { t => + if isByNameType(t) then + syntaxError(ByNameParameterNotSupported(t), t.span) + stripByNameType(t) + else + t + } val tuple = atSpan(start) { makeTupleOrParens(ts1) } infixTypeRest( refinedTypeRest( @@ -1861,17 +1860,48 @@ object Parsers { else commaSeparated(() => argType()) } - /** FunArgType ::= Type | `=>' Type + def paramTypeOf(core: () => Tree): Tree = + if in.token == ARROW || isIdent(nme.PUREARROW) then + val isImpure = in.token == ARROW + val tp = atSpan(in.skipToken()) { ByNameTypeTree(core()) } + if isImpure && ctx.settings.Ycc.value then ImpureByNameTypeTree(tp) else tp + else if in.token == LBRACE && followingIsCaptureSet() then + val start = in.offset + val cs = captureSet() + val endCsOffset = in.lastOffset + val startTpOffset = in.offset + val tp = paramTypeOf(core) + val tp1 = tp match + case ImpureByNameTypeTree(tp1) => + syntaxError("explicit captureSet is superfluous for impure call-by-name type", start) + tp1 + case CapturingTypeTree(_, tp1: ByNameTypeTree) => + syntaxError("only one captureSet is allowed here", start) + tp1 + case _: ByNameTypeTree if startTpOffset > endCsOffset => + report.warning( + i"""Style: by-name `->` should immediately follow closing `}` of capture set + |to avoid confusion with function type. + |That is, `{c}-> T` instead of `{c} -> T`.""", + source.atSpan(Span(startTpOffset, startTpOffset))) + tp + case _ => + tp + CapturingTypeTree(cs, tp1) + else + core() + + /** FunArgType ::= Type + * | `=>' Type + * | [CaptureSet] `->' Type */ - val funArgType: () => Tree = () => - if (in.token == ARROW) atSpan(in.skipToken()) { ByNameTypeTree(typ()) } - else typ() + val funArgType: () => Tree = () => paramTypeOf(typ) - /** ParamType ::= [`=>'] ParamValueType + /** ParamType ::= ParamValueType + * | `=>' ParamValueType + * | [CaptureSet] `->' ParamValueType */ - def paramType(): Tree = - if (in.token == ARROW) atSpan(in.skipToken()) { ByNameTypeTree(paramValueType()) } - else paramValueType() + def paramType(): Tree = paramTypeOf(paramValueType) /** ParamValueType ::= Type [`*'] */ @@ -3144,6 +3174,7 @@ object Parsers { acceptColon() if (in.token == ARROW && ofClass && !mods.is(Local)) syntaxError(VarValParametersMayNotBeCallByName(name, mods.is(Mutable))) + // needed?, it's checked later anyway val tpt = paramType() val default = if (in.token == EQUALS) { in.nextToken(); subExpr() } diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 42fa8edd2ade..680c309c32c1 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 -import cc.{EventuallyCapturingType, CaptureSet} +import cc.{CapturingType, EventuallyCapturingType, CaptureSet, CapturingKind} class PlainPrinter(_ctx: Context) extends Printer { @@ -200,8 +200,8 @@ class PlainPrinter(_ctx: Context) extends Printer { keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~ (" <: " ~ toText(bound) provided !bound.isAny) }.close - case EventuallyCapturingType(parent, refs, boxed) => - def box = Str("box ") provided boxed + case EventuallyCapturingType(parent, refs, kind) => + def box = Str("box ") provided kind == CapturingKind.Boxed if printDebug && !refs.isConst then changePrec(GlobalPrec)(box ~ s"$refs " ~ toText(parent)) else if ctx.settings.YccDebug.value then @@ -232,8 +232,13 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ (if tp.resultType.isInstanceOf[MethodType] then ")" else "): ") ~ toText(tp.resultType) } - case tp: ExprType => - changePrec(GlobalPrec) { "=> " ~ toText(tp.resultType) } + case ExprType(ct @ EventuallyCapturingType(parent, refs, CapturingKind.ByName)) => + if refs.isUniversal then changePrec(GlobalPrec) { "=> " ~ toText(parent) } + else toText(CapturingType(ExprType(parent), refs, CapturingKind.Regular)) + case ExprType(restp) => + changePrec(GlobalPrec) { + (if ctx.settings.Ycc.value then "-> " else "=> ") ~ toText(restp) + } case tp: HKTypeLambda => changePrec(GlobalPrec) { "[" ~ paramsText(tp) ~ "]" ~ lambdaHash(tp) ~ Str(" =>> ") ~ toTextGlobal(tp.resultType) diff --git a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala index b68cc0f8b8d0..619bfafeb775 100644 --- a/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala @@ -554,7 +554,8 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { (" <: " ~ toText(bound) provided !bound.isEmpty) } case ByNameTypeTree(tpt) => - "=> " ~ toTextLocal(tpt) + (if ctx.settings.Ycc.value then "-> " else "=> ") + ~ toTextLocal(tpt) case TypeBoundsTree(lo, hi, alias) => if (lo eq hi) && alias.isEmpty then optText(lo)(" = " ~ _) else optText(lo)(" >: " ~ _) ~ optText(hi)(" <: " ~ _) ~ optText(alias)(" = " ~ _) @@ -719,7 +720,11 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) { val tptText = toTextGlobal(tpt) prefix ~~ idx.toString ~~ "|" ~~ tptText ~~ "|" ~~ argsText ~~ "|" ~~ contentText ~~ postfix case CapturingTypeTree(refs, parent) => - changePrec(GlobalPrec)("{" ~ Text(refs.map(toText), ", ") ~ "} " ~ toText(parent)) + parent match + case ImpureByNameTypeTree(bntpt) => + "=> " ~ toTextLocal(bntpt) + case _ => + changePrec(GlobalPrec)("{" ~ Text(refs.map(toText), ", ") ~ "} " ~ toText(parent)) case _ => tree.fallbackToText(this) } diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index de920fee4625..9d921bc9aa37 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -673,7 +673,7 @@ import transform.SymUtils._ } } - class ByNameParameterNotSupported(tpe: untpd.TypTree)(using Context) + class ByNameParameterNotSupported(tpe: untpd.Tree)(using Context) extends SyntaxMsg(ByNameParameterNotSupportedID) { def msg = em"By-name parameter type ${tpe} not allowed here." diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index 215fe9bfa687..ecc85eb4fd43 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -124,13 +124,15 @@ object ErrorReporting { def typeMismatch(tree: Tree, pt: Type, implicitFailure: SearchFailureType = NoMatchingImplicits): Tree = { val normTp = normalize(tree.tpe, pt) - val treeTp = if (normTp <:< pt) tree.tpe else normTp - // use normalized type if that also shows an error, original type otherwise + val normPt = normalize(pt, pt) + val (treeTp, expectedTp) = + if (normTp <:< normPt) (tree.tpe, pt) else (normTp, normPt) + // use normalized types if that also shows an error, original types otherwise def missingElse = tree match case If(_, _, elsep @ Literal(Constant(()))) if elsep.span.isSynthetic => "\nMaybe you are missing an else part for the conditional?" case _ => "" - errorTree(tree, TypeMismatch(treeTp, pt, Some(tree), implicitFailure.whyNoConversion, missingElse)) + errorTree(tree, TypeMismatch(treeTp, expectedTp, Some(tree), implicitFailure.whyNoConversion, missingElse)) } /** A subtype log explaining why `found` does not conform to `expected` */ diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0f54e887415a..1fb691fce8aa 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2703,7 +2703,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer registerNowarn(annot1, tree) val arg1 = typed(tree.arg, pt) if (ctx.mode is Mode.Type) { - if annot1.symbol.maybeOwner == defn.RetainsAnnot then + val cls = annot1.symbol.maybeOwner + if cls == defn.RetainsAnnot || cls == defn.RetainsByNameAnnot then CheckCaptures.checkWellformed(annot1) if arg1.isType then assignType(cpy.Annotated(tree)(arg1, annot1), arg1, annot1) diff --git a/library/src-bootstrapped/scala/retainsByName.scala b/library/src-bootstrapped/scala/retainsByName.scala new file mode 100644 index 000000000000..c530f35ec0e4 --- /dev/null +++ b/library/src-bootstrapped/scala/retainsByName.scala @@ -0,0 +1,6 @@ +package scala + +/** An annotation that indicates capture of an enclosing by-name type + */ +class retainsByName(xs: Any*) extends annotation.StaticAnnotation + diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check new file mode 100644 index 000000000000..d8d5d689efae --- /dev/null +++ b/tests/neg-custom-args/captures/byname.check @@ -0,0 +1,20 @@ +-- Warning: tests/neg-custom-args/captures/byname.scala:14:18 ---------------------------------------------------------- +14 | def h(x: {cap1} -> I) = x // warning + | ^ + | Style: by-name `->` should immediately follow closing `}` of capture set + | to avoid confusion with function type. + | That is, `{c}-> T` instead of `{c} -> T`. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:7:5 ----------------------------------------- +7 | h(f()) // error + | ^^^ + | Found: {cap2} (x$0: Int) -> Int + | Required: Int -> Int + +longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:16:5 ---------------------------------------- +16 | h(g()) // error + | ^^^ + | Found: {cap2} () ?-> I + | Required: {cap1} () ?-> I + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala index feb9461dc4c7..0afca7e261e2 100644 --- a/tests/neg-custom-args/captures/byname.scala +++ b/tests/neg-custom-args/captures/byname.scala @@ -6,4 +6,14 @@ def test(cap1: Cap, cap2: Cap) = def h(ff: => {cap2} Int -> Int) = ff h(f()) // error +class I + +def test2(cap1: Cap, cap2: Cap): {cap1} I = + def f() = if cap1 == cap1 then I() else I() + def g() = if cap2 == cap2 then I() else I() + def h(x: {cap1} -> I) = x // warning + h(f()) // OK + h(g()) // error + + diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 0de190df8f11..31624e437928 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -1,8 +1,8 @@ -- [E163] Declaration Error: tests/neg-custom-args/captures/lazylist.scala:22:6 ---------------------------------------- 22 | def tail: {*} LazyList[Nothing] = ??? // error overriding | ^ - | error overriding method tail in class LazyList of type => lazylists.LazyList[Nothing]; - | method tail of type => {*} lazylists.LazyList[Nothing] has incompatible type + | error overriding method tail in class LazyList of type -> lazylists.LazyList[Nothing]; + | method tail of type -> {*} lazylists.LazyList[Nothing] has incompatible type longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:35:29 ------------------------------------- diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 8e09dd26cccf..a8f145ecd9d7 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -1,8 +1,8 @@ -- [E163] Declaration Error: tests/neg-custom-args/captures/lazylists2.scala:50:10 ------------------------------------- 50 | def tail: {xs, f} LazyList[B] = xs.tail.map(f) // error | ^ - | error overriding method tail in trait LazyList of type => {Mapped.this} LazyList[B]; - | method tail of type => {xs, f} LazyList[B] has incompatible type + | error overriding method tail in trait LazyList of type -> {Mapped.this} LazyList[B]; + | method tail of type -> {xs, f} LazyList[B] has incompatible type longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ diff --git a/tests/pos-custom-args/captures/byname.scala b/tests/pos-custom-args/captures/byname.scala index a3d80f31d579..5cb5255d4652 100644 --- a/tests/pos-custom-args/captures/byname.scala +++ b/tests/pos-custom-args/captures/byname.scala @@ -5,6 +5,6 @@ class I def test(cap1: Cap, cap2: Cap): {cap1} I = def f() = if cap1 == cap1 then I() else I() - def h(x: /*=>*/ {cap1} I) = x // TODO: enable cbn - h(f()) + def h(x: {cap1}-> I) = x + h(f()) // OK From fd6b376f2baf2382c60bbf3cc68f91e27069521f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 25 Jan 2022 19:40:04 +0100 Subject: [PATCH 17/99] Treat exceptions as capabilities 1. Make CanThrow a @capability class 2. Fix pure arrow handling in parser 3. Avoid misleading type mismatch message 4. Make map and filter conserve Const capturesets if there's no change 5. Expand $throws clauses to context function types 6. Exempt compiletime.erasedValue for "no '*'" checks 7. Capability escape checking for try --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 13 +++- .../dotty/tools/dotc/cc/CapturingType.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 36 +++++++++-- .../dotty/tools/dotc/parsing/Parsers.scala | 7 ++- .../dotty/tools/dotc/parsing/Scanners.scala | 3 + .../dotty/tools/dotc/transform/Recheck.scala | 5 +- .../tools/dotc/typer/CheckCaptures.scala | 45 ++++++++----- .../tools/dotc/typer/ErrorReporting.scala | 16 ++++- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../tools/dotc/util/SimpleIdentitySet.scala | 4 ++ library/src/scala/CanThrow.scala | 4 +- tests/neg-custom-args/captures/real-try.check | 8 +++ tests/neg-custom-args/captures/real-try.scala | 14 +++++ tests/pos-custom-args/captures/i13816.scala | 63 +++++++++++++++++++ 14 files changed, 188 insertions(+), 34 deletions(-) create mode 100644 tests/neg-custom-args/captures/real-try.check create mode 100644 tests/neg-custom-args/captures/real-try.scala create mode 100644 tests/pos-custom-args/captures/i13816.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 6118e54174cd..b8daef92beef 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -173,7 +173,10 @@ sealed abstract class CaptureSet extends Showable: this -- ref.singletonCaptureSet def filter(p: CaptureRef => Boolean)(using Context): CaptureSet = - if this.isConst then Const(elems.filter(p)) + if this.isConst then + val elems1 = elems.filter(p) + if elems1 == elems then this + else Const(elems.filter(p)) else Filtered(asVar, p) /** capture set obtained by applying `f` to all elements of the current capture set @@ -183,11 +186,15 @@ sealed abstract class CaptureSet extends Showable: def map(tm: TypeMap)(using Context): CaptureSet = tm match case tm: BiTypeMap => val mappedElems = elems.map(tm.forward) - if isConst then Const(mappedElems) + if isConst then + if mappedElems == elems then this + else Const(mappedElems) else BiMapped(asVar, tm, mappedElems) case _ => val mapped = mapRefs(elems, tm, tm.variance) - if isConst then mapped + if isConst then + if mapped.isConst && mapped.elems == elems then this + else mapped else Mapped(asVar, tm, tm.variance, mapped) def substParams(tl: BindingType, to: List[Type])(using Context) = diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index bca791e46205..d19850b72e4f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -29,7 +29,7 @@ end CapturingType /** An extractor for types that will be capturing types at phase CheckCaptures. Also * included are types that indicate captures on enclosing call-by-name parameters - * before phase ElimByName + * before phase ElimByName. */ object EventuallyCapturingType: diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 622baff6f2a3..4197d097d8aa 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -44,6 +44,29 @@ extends tpd.TreeTraverser: case _ => traverseChildren(t) + /** Expand some aliases of function types to the underlying functions. + * Right now, these are only $throws aliases, but this could be generalized. + */ + def expandInlineAlias(tp: Type)(using Context) = tp match + case AppliedType(tycon, res :: exc :: Nil) if tycon.typeSymbol == defn.throwsAlias => + // hard-coded expansion since $throws aliases in stdlib are defined with `?=>` rather than `?->` + defn.FunctionOf(defn.CanThrowClass.typeRef.appliedTo(exc) :: Nil, res, isContextual = true, isErased = true) + case _ => tp + + private def expandInlineAliases(using Context) = new TypeMap: + def apply(t: Type) = t match + case _: AppliedType => + val t1 = expandInlineAlias(t) + if t1 ne t then apply(t1) else mapOver(t) + case _: LazyRef => + t + case t @ AnnotatedType(t1, ann) => + // Don't map capture sets, since that would implicitly normalize sets that + // are not well-formed. + t.derivedAnnotatedType(apply(t1), ann) + case _ => + mapOver(t) + /** Perform the following transformation steps everywhere in a type: * 1. Drop retains annotations * 2. Turn plain function types into dependent function types, so that @@ -143,7 +166,8 @@ extends tpd.TreeTraverser: try ts.mapConserve(this) finally isTopLevel = saved def apply(t: Type) = - val t1 = t match + val tp = expandInlineAlias(t) + val tp1 = tp match case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => apply(parent) case tp @ AppliedType(tycon, args) => @@ -172,8 +196,8 @@ extends tpd.TreeTraverser: paramInfos = tp.paramInfos.mapConserve(cleanup(_).bounds), resType = this(tp.resType)) case _ => - mapOver(t) - addVar(addCaptureRefinements(t1)) + mapOver(tp) + addVar(addCaptureRefinements(tp1)) end mapInferred private def expandAbbreviations(using Context) = new TypeMap: @@ -232,8 +256,10 @@ extends tpd.TreeTraverser: private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = addBoxes.traverse(tp) if boxed then setBoxed(tp) - if ctx.settings.YccNoAbbrev.value then tp - else expandAbbreviations(tp) + val tp1 = expandInlineAliases(tp) + if tp1 ne tp then capt.println(i"expanded: $tp --> $tp1") + if ctx.settings.YccNoAbbrev.value then tp1 + else expandAbbreviations(tp1) // Substitute parameter symbols in `from` to paramRefs in corresponding // method or poly types `to`. We use a single BiTypeMap to do everything. diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 8b99b3c1d239..fc1c64611bbd 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -426,7 +426,10 @@ object Parsers { /** Convert tree to formal parameter list */ def convertToParams(tree: Tree): List[ValDef] = - val mods = if in.token == CTXARROW then Modifiers(Given) else EmptyModifiers + val mods = + if in.token == CTXARROW || in.isIdent(nme.PURECTXARROW) + then Modifiers(Given) + else EmptyModifiers tree match case Parens(t) => convertToParam(t, mods) :: Nil @@ -1511,7 +1514,7 @@ object Parsers { commaSeparatedRest(t, funArgType) } accept(RPAREN) - if isValParamList || in.isArrow then + if isValParamList || in.isArrow || in.isPureArrow then functionRest(ts) else { val ts1 = ts.mapConserve { t => diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 082112d800d9..726e6b7980ce 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -88,6 +88,9 @@ object Scanners { def isArrow = token == ARROW || token == CTXARROW + + def isPureArrow = + isIdent(nme.PUREARROW) || isIdent(nme.PURECTXARROW) } abstract class ScannerCommon(source: SourceFile)(using Context) extends CharArrayReader with TokenData { diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index bdb49c5a8edb..33de86d091ef 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -337,7 +337,9 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = checkConforms(tpe, pt, tree) - if keepTypes then tree.rememberType(tpe) + if keepTypes + || tree.isInstanceOf[Try] // type needs tp be checked for * escapes + then tree.rememberType(tpe) tpe def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = @@ -363,6 +365,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: || expected.isRepeatedParam && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) if !isCompatible then + recheckr.println(i"conforms failed for ${tree}: $tpe vs $expected") err.typeMismatch(tree.withType(tpe), expected) else if debugSuccesses then tree match diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index bfe93514548e..c4e62e66ac75 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -16,6 +16,7 @@ import transform.Recheck import Recheck.* import scala.collection.mutable import CaptureSet.withCaptureSetsExplained +import reporting.trace object CheckCaptures: import ast.tpd.* @@ -75,7 +76,15 @@ object CheckCaptures: if remaining.accountsFor(firstRef) then report.warning(em"redundant capture: $remaining already accounts for $firstRef", ann.srcPos) - private inline val disallowGlobal = true + /** Does this function allow type arguments carrying the universal capability? + * Currently this is true only for `erasedValue` since this function is magic in + * that is allows to conjure global capabilies from nothing (aside: can we find a + * more controlled way to achieve this?). + * But it could be generalized to other functions that so that they can take capability + * classes as arguments. + */ + private def allowUniversalArguments(fn: Tree)(using Context): Boolean = + fn.symbol == defn.Compiletime_erasedValue class CheckCaptures extends Recheck: thisPhase => @@ -305,13 +314,13 @@ class CheckCaptures extends Recheck: .traverse(ctx.compilationUnit.tpdTree) withCaptureSetsExplained { super.checkUnit(unit) - PostRefinerCheck.traverse(unit.tpdTree) + PostCheck.traverse(unit.tpdTree) if ctx.settings.YccDebug.value then show(unit.tpdTree) // this dows not print tree, but makes its variables visible for dependency printing } - def checkNotGlobal(tree: Tree, tp: Type, allArgs: Tree*)(using Context): Unit = - for ref <-tp.captureSet.elems do + def checkNotGlobal(tree: Tree, tp: Type, isVar: Boolean, allArgs: Tree*)(using Context): Unit = + for ref <- tp.captureSet.elems do val isGlobal = ref match case ref: TermRef => ref.isRootCapability case _ => false @@ -320,7 +329,7 @@ class CheckCaptures extends Recheck: val notAllowed = i" is not allowed to capture the $what capability $ref" def msg = if allArgs.isEmpty then - i"type of mutable variable ${tree.knownType}$notAllowed" + i"${if isVar then "type of mutable variable" else "result type"} ${tree.knownType}$notAllowed" else tree match case tree: InferredTypeTree => i"""inferred type argument ${tree.knownType}$notAllowed @@ -330,12 +339,11 @@ class CheckCaptures extends Recheck: report.error(msg, tree.srcPos) def checkNotGlobal(tree: Tree, allArgs: Tree*)(using Context): Unit = - if disallowGlobal then - tree match - case LambdaTypeTree(_, restpt) => - checkNotGlobal(restpt, allArgs*) - case _ => - checkNotGlobal(tree, tree.knownType, allArgs*) + tree match + case LambdaTypeTree(_, restpt) => + checkNotGlobal(restpt, allArgs*) + case _ => + checkNotGlobal(tree, tree.knownType, isVar = false, allArgs*) def checkNotGlobalDeep(tree: Tree)(using Context): Unit = val checker = new TypeTraverser: @@ -346,12 +354,12 @@ class CheckCaptures extends Recheck: case _ => case tp: TermRef => case _ => - checkNotGlobal(tree, tp) + checkNotGlobal(tree, tp, isVar = true) traverseChildren(tp) checker.traverse(tree.knownType) - object PostRefinerCheck extends TreeTraverser: - def traverse(tree: Tree)(using Context) = + object PostCheck extends TreeTraverser: + def traverse(tree: Tree)(using Context) = trace{i"post check $tree"} { tree match case _: InferredTypeTree => case tree: TypeTree if !tree.span.isZeroExtent => @@ -362,7 +370,7 @@ class CheckCaptures extends Recheck: checkWellformedPost(annot.tree) case _ => } - case tree1 @ TypeApply(fn, args) if disallowGlobal => + case tree1 @ TypeApply(fn, args) if !allowUniversalArguments(fn) => for arg <- args do //println(i"checking $arg in $tree: ${tree.knownType.captureSet}") checkNotGlobal(arg, args*) @@ -390,11 +398,14 @@ class CheckCaptures extends Recheck: inferred.foreachPart(checkPure, StopAt.Static) case t: ValDef if t.symbol.is(Mutable) => checkNotGlobalDeep(t.tpt) + case t: Try => + checkNotGlobal(t) case _ => traverseChildren(tree) + } - def postRefinerCheck(tree: tpd.Tree)(using Context): Unit = - PostRefinerCheck.traverse(tree) + def postCheck(tree: tpd.Tree)(using Context): Unit = + PostCheck.traverse(tree) end CaptureChecker end CheckCaptures diff --git a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala index ecc85eb4fd43..3c92a217206d 100644 --- a/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala +++ b/compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala @@ -125,13 +125,25 @@ object ErrorReporting { def typeMismatch(tree: Tree, pt: Type, implicitFailure: SearchFailureType = NoMatchingImplicits): Tree = { val normTp = normalize(tree.tpe, pt) val normPt = normalize(pt, pt) + + def contextFunctionCount(tp: Type): Int = tp.stripped match + case defn.ContextFunctionType(_, restp, _) => 1 + contextFunctionCount(restp) + case _ => 0 + def strippedTpCount = contextFunctionCount(tree.tpe) - contextFunctionCount(normTp) + def strippedPtCount = contextFunctionCount(pt) - contextFunctionCount(normPt) + val (treeTp, expectedTp) = - if (normTp <:< normPt) (tree.tpe, pt) else (normTp, normPt) - // use normalized types if that also shows an error, original types otherwise + if normTp <:< normPt || strippedTpCount != strippedPtCount + then (tree.tpe, pt) + else (normTp, normPt) + // use normalized types if that also shows an error, and both sides stripped + // the same number of context functions. Use original types otherwise. + def missingElse = tree match case If(_, _, elsep @ Literal(Constant(()))) if elsep.span.isSynthetic => "\nMaybe you are missing an else part for the conditional?" case _ => "" + errorTree(tree, TypeMismatch(treeTp, expectedTp, Some(tree), implicitFailure.whyNoConversion, missingElse)) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 1fb691fce8aa..557eea853f81 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2400,7 +2400,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer //todo: make sure dependent method types do not depend on implicits or by-name params } - /** (1) Check that the signature of the class mamber does not return a repeated parameter type + /** (1) Check that the signature of the class member does not return a repeated parameter type * (2) If info is an erased class, set erased flag of member */ private def postProcessInfo(sym: Symbol)(using Context): Unit = diff --git a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala index 98d74f03920c..dd766dc99c7e 100644 --- a/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala +++ b/compiler/src/dotty/tools/dotc/util/SimpleIdentitySet.scala @@ -38,6 +38,10 @@ abstract class SimpleIdentitySet[+Elem <: AnyRef] { ((SimpleIdentitySet.empty: SimpleIdentitySet[E]) /: this) { (s, x) => if (that.contains(x)) s else s + x } + + def == [E >: Elem <: AnyRef](that: SimpleIdentitySet[E]): Boolean = + this.size == that.size && forall(that.contains) + override def toString: String = toList.mkString("{", ", ", "}") } diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index fcfd11fc9197..c7f23a393715 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -1,12 +1,12 @@ package scala import language.experimental.erasedDefinitions -import annotation.{implicitNotFound, experimental} +import annotation.{implicitNotFound, experimental, capability} /** A capability class that allows to throw exception `E`. When used with the * experimental.saferExceptions feature, a `throw Ex()` expression will require * a given of class `CanThrow[Ex]` to be available. */ -@experimental +@experimental @capability @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") erased class CanThrow[-E <: Exception] diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check new file mode 100644 index 000000000000..11a6fdfd50dd --- /dev/null +++ b/tests/neg-custom-args/captures/real-try.check @@ -0,0 +1,8 @@ +-- Error: tests/neg-custom-args/captures/real-try.scala:10:2 ----------------------------------------------------------- +10 | try // error + | ^ + | result type {*} () -> Unit is not allowed to capture the universal capability *.type +11 | () => foo(1) +12 | catch +13 | case _: Ex1 => ??? +14 | case _: Ex2 => ??? diff --git a/tests/neg-custom-args/captures/real-try.scala b/tests/neg-custom-args/captures/real-try.scala new file mode 100644 index 000000000000..9a8ccd694dc9 --- /dev/null +++ b/tests/neg-custom-args/captures/real-try.scala @@ -0,0 +1,14 @@ +import language.experimental.saferExceptions + +class Ex1 extends Exception("Ex1") +class Ex2 extends Exception("Ex2") + +def foo(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def test() = + try // error + () => foo(1) + catch + case _: Ex1 => ??? + case _: Ex2 => ??? diff --git a/tests/pos-custom-args/captures/i13816.scala b/tests/pos-custom-args/captures/i13816.scala new file mode 100644 index 000000000000..b8f9db405188 --- /dev/null +++ b/tests/pos-custom-args/captures/i13816.scala @@ -0,0 +1,63 @@ +import language.experimental.saferExceptions + +class Ex1 extends Exception("Ex1") +class Ex2 extends Exception("Ex2") + +def foo0(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo01(i: Int): CanThrow[Ex1] ?-> CanThrow[Ex2] ?-> Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo1(i: Int): Unit throws Ex1 throws Ex2 = + if i > 0 then throw new Ex1 else throw new Ex1 + +def foo2(i: Int): Unit throws Ex1 | Ex2 = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo3(i: Int): Unit throws (Ex1 | Ex2) = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo4(i: Int)(using CanThrow[Ex1], CanThrow[Ex2]): Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo5(i: Int)(using CanThrow[Ex1])(using CanThrow[Ex2]): Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo6(i: Int)(using CanThrow[Ex1 | Ex2]): Unit = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo7(i: Int)(using CanThrow[Ex1]): Unit throws Ex2 = + if i > 0 then throw new Ex1 else throw new Ex2 + +def foo8(i: Int)(using CanThrow[Ex2]): Unit throws Ex1 = + if i > 0 then throw new Ex1 else throw new Ex2 + +def test(): Unit = + try + foo1(1) + foo2(1) + foo3(1) + foo4(1) + foo5(1) + foo6(1) + foo7(1) + foo8(1) + catch + case _: Ex1 => + case _: Ex2 => + + try + try + foo1(1) + foo2(1) + foo3(1) + foo4(1) + foo5(1) + // foo6(1) // As explained in the docs this won't work until we find a way to aggregate capabilities + foo7(1) + foo8(1) + catch + case _: Ex1 => + catch + case _: Ex2 => From 39b14f6fee7b9823f77fd27d3494cae23cb5899a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 26 Jan 2022 12:52:33 +0100 Subject: [PATCH 18/99] Mark classes compiled under -Ycc with a CaptureChecked annotation --- .../src/dotty/tools/dotc/core/Definitions.scala | 1 + .../src/dotty/tools/dotc/transform/PostTyper.scala | 14 +++++++------- .../scala/annotation/internal/CaptureChecked.scala | 8 ++++++++ 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 library/src/scala/annotation/internal/CaptureChecked.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index cc2dc830ab94..3ca61ab1a372 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -972,6 +972,7 @@ class Definitions { @tu lazy val BooleanBeanPropertyAnnot: ClassSymbol = requiredClass("scala.beans.BooleanBeanProperty") @tu lazy val BodyAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Body") @tu lazy val CapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.capability") + @tu lazy val CaptureCheckedAnnot: ClassSymbol = requiredClass("scala.annotation.internal.CaptureChecked") @tu lazy val ChildAnnot: ClassSymbol = requiredClass("scala.annotation.internal.Child") @tu lazy val ContextResultCountAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ContextResultCount") @tu lazy val ProvisionalSuperClassAnnot: ClassSymbol = requiredClass("scala.annotation.internal.ProvisionalSuperClass") diff --git a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala index 8b7ac94675a1..3db751df4145 100644 --- a/compiler/src/dotty/tools/dotc/transform/PostTyper.scala +++ b/compiler/src/dotty/tools/dotc/transform/PostTyper.scala @@ -391,13 +391,13 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase for parent <- impl.parents do Checking.checkTraitInheritance(parent.tpe.classSymbol, sym.asClass, parent.srcPos) // Add SourceFile annotation to top-level classes - if sym.owner.is(Package) - && ctx.compilationUnit.source.exists - && sym != defn.SourceFileAnnot - then - val reference = ctx.settings.sourceroot.value - val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference) - sym.addAnnotation(Annotation.makeSourceFile(relativePath)) + if sym.owner.is(Package) then + if ctx.compilationUnit.source.exists && sym != defn.SourceFileAnnot then + val reference = ctx.settings.sourceroot.value + val relativePath = util.SourceFile.relativePath(ctx.compilationUnit.source, reference) + sym.addAnnotation(Annotation.makeSourceFile(relativePath)) + if ctx.settings.Ycc.value && sym != defn.CaptureCheckedAnnot then + sym.addAnnotation(Annotation(defn.CaptureCheckedAnnot)) else if !sym.is(Param) && !sym.owner.isOneOf(AbstractOrTrait) then Checking.checkGoodBounds(tree.symbol) diff --git a/library/src/scala/annotation/internal/CaptureChecked.scala b/library/src/scala/annotation/internal/CaptureChecked.scala new file mode 100644 index 000000000000..3ffea31b898c --- /dev/null +++ b/library/src/scala/annotation/internal/CaptureChecked.scala @@ -0,0 +1,8 @@ +package scala.annotation +package internal + +/** A marker annotation on a toplevel class that indicates + * that the class was checked under -Ycc + */ +class CaptureChecked extends StaticAnnotation + From d4ffebbfafc011c4f86f57df757a4376cc472bb8 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 26 Jan 2022 15:09:46 +0100 Subject: [PATCH 19/99] Map regular function types to impure function types when unpickling Map regular function types to impure function types when unpickling a class under -Ycc that was not itself compiled with -Ycc. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 16 +++++++++++++++- .../tools/dotc/core/tasty/TreeUnpickler.scala | 17 +++++++++++++++-- .../core/unpickleScala2/Scala2Unpickler.scala | 5 ++++- .../captures/capt-separate/Lib_1.scala | 6 ++++++ .../captures/capt-separate/Test_2.scala | 15 +++++++++++++++ .../captures/saferExceptions.scala | 18 ++++++++++++++++++ 6 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 tests/pos-custom-args/captures/capt-separate/Lib_1.scala create mode 100644 tests/pos-custom-args/captures/capt-separate/Test_2.scala create mode 100644 tests/pos-custom-args/captures/saferExceptions.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 117b6e528e62..aa7efe2a04cf 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -5,7 +5,7 @@ package cc import core.* import Types.*, Symbols.*, Contexts.*, Annotations.* import ast.{tpd, untpd} -import Decorators.* +import Decorators.*, NameOps.* import config.Printers.capt import util.Property.Key import tpd.* @@ -71,3 +71,17 @@ extension (tp: Type) atd.derivedAnnotatedType(parent.stripCapturing, annot) case _ => tp + + /** Under -Ycc, map regular function type to impure function type + */ + def adaptFunctionType(using Context): Type = tp match + case AppliedType(fn, args) + if ctx.settings.Ycc.value && defn.isFunctionClass(fn.typeSymbol) => + val fname = fn.typeSymbol.name + defn.FunctionType( + fname.functionArity, + isContextual = fname.isContextFunction, + isErased = fname.isErasedFunction, + isImpure = true).appliedTo(args) + case _ => + tp diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index caf4deb2738c..7becaff1386b 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -31,6 +31,8 @@ import util.{SourceFile, Property} import ast.{Trees, tpd, untpd} import Trees._ import Decorators._ +import transform.SymUtils._ +import cc.adaptFunctionType import dotty.tools.tasty.{TastyBuffer, TastyReader} import TastyBuffer._ @@ -85,6 +87,9 @@ class TreeUnpickler(reader: TastyReader, /** The root owner tree. See `OwnerTree` class definition. Set by `enterTopLevel`. */ private var ownerTree: OwnerTree = _ + /** Was unpickled class compiled with -Ycc? */ + private var wasCaptureChecked: Boolean = false + private def registerSym(addr: Addr, sym: Symbol) = symAtAddr(addr) = sym @@ -371,7 +376,7 @@ class TreeUnpickler(reader: TastyReader, // Note that the lambda "rt => ..." is not equivalent to a wildcard closure! // Eta expansion of the latter puts readType() out of the expression. case APPLIEDtype => - readType().appliedTo(until(end)(readType())) + postProcessFunction(readType().appliedTo(until(end)(readType()))) case TYPEBOUNDS => val lo = readType() if nothingButMods(end) then @@ -484,6 +489,12 @@ class TreeUnpickler(reader: TastyReader, def readTermRef()(using Context): TermRef = readType().asInstanceOf[TermRef] + /** Under -Ycc, map all function types to impure function types, + * unless the unpickled class was also compiled with -Ycc. + */ + private def postProcessFunction(tp: Type)(using Context): Type = + if wasCaptureChecked then tp else tp.adaptFunctionType + // ------ Reading definitions ----------------------------------------------------- private def nothingButMods(end: Addr): Boolean = @@ -631,6 +642,8 @@ class TreeUnpickler(reader: TastyReader, } registerSym(start, sym) if (isClass) { + if sym.owner.is(Package) && annots.exists(_.symbol == defn.CaptureCheckedAnnot) then + wasCaptureChecked = true sym.completer.withDecls(newScope) forkAt(templateStart).indexTemplateParams()(using localContext(sym)) } @@ -1339,7 +1352,7 @@ class TreeUnpickler(reader: TastyReader, val args = until(end)(readTpt()) val tree = untpd.AppliedTypeTree(tycon, args) val ownType = ctx.typeAssigner.processAppliedType(tree, tycon.tpe.safeAppliedTo(args.tpes)) - tree.withType(ownType) + tree.withType(postProcessFunction(ownType)) case ANNOTATEDtpt => Annotated(readTpt(), readTerm()) case LAMBDAtpt => diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index 535159409c69..0c088abc0b2b 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -32,6 +32,7 @@ import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.annotation.switch import reporting._ +import cc.adaptFunctionType object Scala2Unpickler { @@ -822,7 +823,9 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas // special-case in erasure, see TypeErasure#eraseInfo. OrType(args(0), args(1), soft = false) } - else if (args.nonEmpty) tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) + else if args.nonEmpty then + tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) + .adaptFunctionType else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams) else tycon case TYPEBOUNDStpe => diff --git a/tests/pos-custom-args/captures/capt-separate/Lib_1.scala b/tests/pos-custom-args/captures/capt-separate/Lib_1.scala new file mode 100644 index 000000000000..c620ebca3631 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-separate/Lib_1.scala @@ -0,0 +1,6 @@ +object Lib: + extension [A](xs: Seq[A]) + def mapp[B](f: A => B): Seq[B] = + xs.map(f.asInstanceOf[A -> B]) + + diff --git a/tests/pos-custom-args/captures/capt-separate/Test_2.scala b/tests/pos-custom-args/captures/capt-separate/Test_2.scala new file mode 100644 index 000000000000..1a3b5bbd8571 --- /dev/null +++ b/tests/pos-custom-args/captures/capt-separate/Test_2.scala @@ -0,0 +1,15 @@ +import language.experimental.saferExceptions +import Lib.* + +class LimitExceeded extends Exception + +val limit = 10e9 + +def f(x: Double): Double throws LimitExceeded = + if x < limit then x * x else throw LimitExceeded() + +@main def test(xs: Double*) = + try println(xs.mapp(f).sum) + catch case ex: LimitExceeded => println("too large") + + diff --git a/tests/pos-custom-args/captures/saferExceptions.scala b/tests/pos-custom-args/captures/saferExceptions.scala new file mode 100644 index 000000000000..47793c7450c8 --- /dev/null +++ b/tests/pos-custom-args/captures/saferExceptions.scala @@ -0,0 +1,18 @@ +import language.experimental.saferExceptions + +class LimitExceeded extends Exception + +val limit = 10e9 + +extension [A](xs: Seq[A]) + def mapp[B](f: A => B): Seq[B] = + xs.map(f.asInstanceOf[A -> B]) + +def f(x: Double): Double throws LimitExceeded = + if x < limit then x * x else throw LimitExceeded() + +@main def test(xs: Double*) = + try println(xs.mapp(f).sum + xs.map(f).sum) + catch case ex: LimitExceeded => println("too large") + + From 57a6bc0d7da8771ee2da7f3f81ecaa43ae15daae Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 28 Jan 2022 23:00:32 +0100 Subject: [PATCH 20/99] New scheme to reject root captures Reject root captures by considering unbox operations. This allows us to ignore root captures buried under type applications. --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 26 ++++++- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 10 +++ .../dotty/tools/dotc/transform/Recheck.scala | 1 + .../tools/dotc/typer/CheckCaptures.scala | 77 +++++-------------- .../neg-custom-args/captures/capt-test.scala | 4 +- tests/neg-custom-args/captures/real-try.check | 26 +++++-- tests/neg-custom-args/captures/real-try.scala | 16 ++++ tests/neg-custom-args/captures/try.check | 46 +++++++---- tests/neg-custom-args/captures/try.scala | 12 +-- tests/neg-custom-args/captures/try3.scala | 4 +- tests/neg-custom-args/captures/vars.check | 27 +++---- tests/neg-custom-args/captures/vars.scala | 3 +- 12 files changed, 147 insertions(+), 105 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index aa7efe2a04cf..23cb802356fc 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -3,7 +3,7 @@ package dotc package cc import core.* -import Types.*, Symbols.*, Contexts.*, Annotations.* +import Types.*, Symbols.*, Contexts.*, Annotations.*, Flags.* import ast.{tpd, untpd} import Decorators.*, NameOps.* import config.Printers.capt @@ -85,3 +85,27 @@ extension (tp: Type) isImpure = true).appliedTo(args) case _ => tp + +extension (sym: Symbol) + + /** Does this symbol allow results carrying the universal capability? + * Currently this is true only for function type applies (since their + * results are unboxed) and `erasedValue` since this function is magic in + * that is allows to conjure global capabilies from nothing (aside: can we find a + * more controlled way to achieve this?). + * But it could be generalized to other functions that so that they can take capability + * classes as arguments. + */ + def allowsRootCapture(using Context): Boolean = + sym == defn.Compiletime_erasedValue + || defn.isFunctionClass(sym.maybeOwner) + + def unboxesResult(using Context): Boolean = + def containsEnclTypeParam(tp: Type): Boolean = tp.strippedDealias match + case tp @ TypeRef(pre: ThisType, _) => tp.symbol.is(Param) + case tp: TypeParamRef => true + case tp: AndOrType => containsEnclTypeParam(tp.tp1) || containsEnclTypeParam(tp.tp2) + case tp: RefinedType => containsEnclTypeParam(tp.parent) || containsEnclTypeParam(tp.refinedInfo) + case _ => false + containsEnclTypeParam(sym.info.finalResultType) + && !sym.allowsRootCapture diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index b8daef92beef..a987b8788dd1 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -172,6 +172,10 @@ sealed abstract class CaptureSet extends Showable: def - (ref: CaptureRef)(using Context): CaptureSet = this -- ref.singletonCaptureSet + def disallowRootCapability(handler: () => Unit)(using Context): this.type = + if isUniversal then handler() + this + def filter(p: CaptureRef => Boolean)(using Context): CaptureSet = if this.isConst then val elems1 = elems.filter(p) @@ -276,6 +280,7 @@ object CaptureSet: var deps: Deps = emptySet def isConst = isSolved def isAlwaysEmpty = false + var addRootHandler: () => Unit = () => () private def recordElemsState()(using VarState): Boolean = varState.getElems(this) match @@ -296,6 +301,7 @@ object CaptureSet: def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = if !isConst && recordElemsState() then elems ++= newElems + if isUniversal then addRootHandler() // assert(id != 2 || elems.size != 2, this) (CompareResult.OK /: deps) { (r, dep) => r.andAlso(dep.tryInclude(newElems, this)) @@ -312,6 +318,10 @@ object CaptureSet: else CompareResult.fail(this) + override def disallowRootCapability(handler: () => Unit)(using Context): this.type = + addRootHandler = handler + super.disallowRootCapability(handler) + private var computingApprox = false final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 33de86d091ef..f9af3c38de8a 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -329,6 +329,7 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: case tree: Alternative => recheckAlternative(tree, pt) case tree: PackageDef => recheckPackageDef(tree) case tree: Thicket => defn.NothingType + case tree: Import => defn.NothingType tree match case tree: NameTree => recheckNamed(tree, pt) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index c4e62e66ac75..994df9a382c4 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -76,16 +76,6 @@ object CheckCaptures: if remaining.accountsFor(firstRef) then report.warning(em"redundant capture: $remaining already accounts for $firstRef", ann.srcPos) - /** Does this function allow type arguments carrying the universal capability? - * Currently this is true only for `erasedValue` since this function is magic in - * that is allows to conjure global capabilies from nothing (aside: can we find a - * more controlled way to achieve this?). - * But it could be generalized to other functions that so that they can take capability - * classes as arguments. - */ - private def allowUniversalArguments(fn: Tree)(using Context): Boolean = - fn.symbol == defn.Compiletime_erasedValue - class CheckCaptures extends Recheck: thisPhase => @@ -309,6 +299,26 @@ class CheckCaptures extends Recheck: includeBoxedCaptures(res, tree.srcPos) res + override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = + val typeToCheck = tree match + case _: Ident | _: Select | _: Apply | _: TypeApply if tree.symbol.unboxesResult => + tpe + case _: Try => + tpe + case ValDef(_, tpt, _) if tree.symbol.is(Mutable) => + tree.symbol.info + case _ => + NoType + if typeToCheck.exists then + typeToCheck.widenDealias match + case wtp @ CapturingType(parent, refs, _) => + refs.disallowRootCapability { () => + val kind = if tree.isInstanceOf[ValDef] then "mutable variable" else "expression" + report.error(em"the $kind's type $wtp is not allowed to capture the root capability `*`", tree.srcPos) + } + case _ => + super.recheckFinish(tpe, tree, pt) + override def checkUnit(unit: CompilationUnit)(using Context): Unit = Setup(preRecheckPhase, thisPhase, recheckDef) .traverse(ctx.compilationUnit.tpdTree) @@ -319,45 +329,6 @@ class CheckCaptures extends Recheck: show(unit.tpdTree) // this dows not print tree, but makes its variables visible for dependency printing } - def checkNotGlobal(tree: Tree, tp: Type, isVar: Boolean, allArgs: Tree*)(using Context): Unit = - for ref <- tp.captureSet.elems do - val isGlobal = ref match - case ref: TermRef => ref.isRootCapability - case _ => false - if isGlobal then - val what = if ref.isRootCapability then "universal" else "global" - val notAllowed = i" is not allowed to capture the $what capability $ref" - def msg = - if allArgs.isEmpty then - i"${if isVar then "type of mutable variable" else "result type"} ${tree.knownType}$notAllowed" - else tree match - case tree: InferredTypeTree => - i"""inferred type argument ${tree.knownType}$notAllowed - | - |The inferred arguments are: [${allArgs.map(_.knownType)}%, %]""" - case _ => s"type argument$notAllowed" - report.error(msg, tree.srcPos) - - def checkNotGlobal(tree: Tree, allArgs: Tree*)(using Context): Unit = - tree match - case LambdaTypeTree(_, restpt) => - checkNotGlobal(restpt, allArgs*) - case _ => - checkNotGlobal(tree, tree.knownType, isVar = false, allArgs*) - - def checkNotGlobalDeep(tree: Tree)(using Context): Unit = - val checker = new TypeTraverser: - def traverse(tp: Type): Unit = tp match - case tp: TypeRef => - tp.info match - case TypeBounds(_, hi) => traverse(hi) - case _ => - case tp: TermRef => - case _ => - checkNotGlobal(tree, tp, isVar = true) - traverseChildren(tp) - checker.traverse(tree.knownType) - object PostCheck extends TreeTraverser: def traverse(tree: Tree)(using Context) = trace{i"post check $tree"} { tree match @@ -370,10 +341,6 @@ class CheckCaptures extends Recheck: checkWellformedPost(annot.tree) case _ => } - case tree1 @ TypeApply(fn, args) if !allowUniversalArguments(fn) => - for arg <- args do - //println(i"checking $arg in $tree: ${tree.knownType.captureSet}") - checkNotGlobal(arg, args*) case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] => val sym = t.symbol val isLocal = @@ -396,10 +363,6 @@ class CheckCaptures extends Recheck: |The type needs to be declared explicitly.""", t.srcPos) case _ => inferred.foreachPart(checkPure, StopAt.Static) - case t: ValDef if t.symbol.is(Mutable) => - checkNotGlobalDeep(t.tpt) - case t: Try => - checkNotGlobal(t) case _ => traverseChildren(tree) } diff --git a/tests/neg-custom-args/captures/capt-test.scala b/tests/neg-custom-args/captures/capt-test.scala index 0c536a280f5c..0face680a285 100644 --- a/tests/neg-custom-args/captures/capt-test.scala +++ b/tests/neg-custom-args/captures/capt-test.scala @@ -19,8 +19,8 @@ def handle[E <: Exception, R <: Top](op: (CanThrow[E]) => R)(handler: E => R): R 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/real-try.check b/tests/neg-custom-args/captures/real-try.check index 11a6fdfd50dd..95531857712e 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -1,8 +1,20 @@ --- Error: tests/neg-custom-args/captures/real-try.scala:10:2 ----------------------------------------------------------- -10 | try // error +-- Error: tests/neg-custom-args/captures/real-try.scala:12:2 ----------------------------------------------------------- +12 | try // error | ^ - | result type {*} () -> Unit is not allowed to capture the universal capability *.type -11 | () => foo(1) -12 | catch -13 | case _: Ex1 => ??? -14 | case _: Ex2 => ??? + | the expression's type {*} () -> Unit is not allowed to capture the root capability `*` +13 | () => foo(1) +14 | catch +15 | case _: Ex1 => ??? +16 | case _: Ex2 => ??? +-- Error: tests/neg-custom-args/captures/real-try.scala:18:2 ----------------------------------------------------------- +18 | try // error + | ^ + | the expression's type {*} () -> ? Cell[Unit] is not allowed to capture the root capability `*` +19 | () => Cell(foo(1)) +20 | catch +21 | case _: Ex1 => ??? +22 | case _: Ex2 => ??? +-- Error: tests/neg-custom-args/captures/real-try.scala:30:4 ----------------------------------------------------------- +30 | b.x // error + | ^^^ + | the expression's type box {*} () -> Unit is not allowed to capture the root capability `*` diff --git a/tests/neg-custom-args/captures/real-try.scala b/tests/neg-custom-args/captures/real-try.scala index 9a8ccd694dc9..94e1eafd9af2 100644 --- a/tests/neg-custom-args/captures/real-try.scala +++ b/tests/neg-custom-args/captures/real-try.scala @@ -6,9 +6,25 @@ class Ex2 extends Exception("Ex2") def foo(i: Int): (CanThrow[Ex1], CanThrow[Ex2]) ?-> Unit = if i > 0 then throw new Ex1 else throw new Ex2 +class Cell[+T](val x: T) + def test() = try // error () => foo(1) catch case _: Ex1 => ??? case _: Ex2 => ??? + + try // error + () => Cell(foo(1)) + catch + case _: Ex1 => ??? + case _: Ex2 => ??? + + val b = try // ok here, but error on use + Cell(() => foo(1))//: Cell[box {ev} () => Unit] <: Cell[box {*} () => Unit] + catch + case _: Ex1 => ??? + case _: Ex2 => ??? + + b.x // error diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index a2fe96016b80..7dbccc469089 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,3 +1,11 @@ +-- Error: tests/neg-custom-args/captures/try.scala:24:3 ---------------------------------------------------------------- +22 | val a = handle[Exception, CanThrow[Exception]] { +23 | (x: CanThrow[Exception]) => x +24 | }{ // error + | ^ + | the expression's type {*} CT[Exception] is not allowed to capture the root capability `*` +25 | (ex: Exception) => ??? +26 | } -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:28:43 ------------------------------------------ 28 | val b = handle[Exception, () -> Nothing] { // error | ^ @@ -7,19 +15,25 @@ 30 | } { longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/try.scala:22:28 --------------------------------------------------------------- -22 | val a = handle[Exception, CanThrow[Exception]] { // error - | ^^^^^^^^^^^^^^^^^^^ - | type argument is not allowed to capture the universal capability (* : Any) --- Error: tests/neg-custom-args/captures/try.scala:34:11 --------------------------------------------------------------- -34 | val xx = handle { // error - | ^^^^^^ - | inferred type argument {*} () -> Int is not allowed to capture the universal capability (* : Any) - | - | The inferred arguments are: [? Exception, {*} () -> Int] --- Error: tests/neg-custom-args/captures/try.scala:46:13 --------------------------------------------------------------- -46 |val global = handle { // error - | ^^^^^^ - | inferred type argument {*} () -> Int is not allowed to capture the universal capability (* : Any) - | - | The inferred arguments are: [? Exception, {*} () -> Int] +-- Error: tests/neg-custom-args/captures/try.scala:39:4 ---------------------------------------------------------------- +34 | val xx = handle { +35 | (x: CanThrow[Exception]) => +36 | () => +37 | raise(new Exception)(using x) +38 | 22 +39 | } { // error + | ^ + | the expression's type {*} () -> Int is not allowed to capture the root capability `*` +40 | (ex: Exception) => () => 22 +41 | } +-- Error: tests/neg-custom-args/captures/try.scala:51:2 ---------------------------------------------------------------- +46 |val global = handle { +47 | (x: CanThrow[Exception]) => +48 | () => +49 | raise(new Exception)(using x) +50 | 22 +51 |} { // error + | ^ + | the expression's type {*} () -> Int is not allowed to capture the root capability `*` +52 | (ex: Exception) => () => 22 +53 |} diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index b128f82a2a3c..c76da6641780 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -19,9 +19,9 @@ def handle[E <: Exception, R <: Top](op: CanThrow[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 (ex: Exception) => ??? } @@ -31,23 +31,23 @@ def test = (ex: Exception) => ??? } - val xx = handle { // error + val xx = handle { (x: CanThrow[Exception]) => () => raise(new Exception)(using x) 22 - } { + } { // error (ex: Exception) => () => 22 } val yy = xx :: Nil yy // OK -val global = handle { // error +val global = handle { (x: CanThrow[Exception]) => () => raise(new Exception)(using x) 22 -} { +} { // error (ex: Exception) => () => 22 } \ No newline at end of file diff --git a/tests/neg-custom-args/captures/try3.scala b/tests/neg-custom-args/captures/try3.scala index 4fbb980b9e03..8c5bc18bf3be 100644 --- a/tests/neg-custom-args/captures/try3.scala +++ b/tests/neg-custom-args/captures/try3.scala @@ -14,12 +14,12 @@ def raise[E <: Exception](ex: E)(using CanThrow[E]): Nothing = @main def Test: Int = def f(a: Boolean) = - handle { // error + handle { if !a then raise(IOException()) (b: Boolean) => if !b then raise(IOException()) 0 - } { + } { // error ex => (b: Boolean) => -1 } val g = f(true) diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 0df38b918862..6a036e49ede2 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -5,17 +5,18 @@ | Required: () -> Unit longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/vars.scala:13:16 -------------------------------------------------------------- +-- Error: tests/neg-custom-args/captures/vars.scala:13:6 --------------------------------------------------------------- 13 | var a: String => String = f // error - | ^^^^^^^^^^^^^^^^ - | type of mutable variable String => String is not allowed to capture the universal capability (* : Any) --- Error: tests/neg-custom-args/captures/vars.scala:14:9 --------------------------------------------------------------- -14 | var b: List[String => String] = Nil // error - | ^^^^^^^^^^^^^^^^^^^^^^ - | type of mutable variable List[String => String] is not allowed to capture the universal capability (* : Any) --- Error: tests/neg-custom-args/captures/vars.scala:29:2 --------------------------------------------------------------- -29 | local { cap3 => // error - | ^^^^^ - |inferred type argument {*} (x$0: ? String) -> ? String is not allowed to capture the universal capability (* : Any) - | - |The inferred arguments are: [{*} (x$0: ? String) -> ? String] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | the mutable variable's type {*} String -> String is not allowed to capture the root capability `*` +-- Error: tests/neg-custom-args/captures/vars.scala:15:4 --------------------------------------------------------------- +15 | b.head // error + | ^^^^^^ + | the expression's type {*} String -> String is not allowed to capture the root capability `*` +-- Error: tests/neg-custom-args/captures/vars.scala:30:8 --------------------------------------------------------------- +30 | local { cap3 => // error + | ^ + | the expression's type {*} (x$0: ? String) -> ? String is not allowed to capture the root capability `*` +31 | def g(x: String): String = if cap3 == cap3 then "" else "a" +32 | g +33 | } diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index e85bcbe2db04..5e413b7ea3fb 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -11,7 +11,8 @@ def test(cap1: Cap, cap2: Cap) = val z2c: () -> Unit = z2 // error var a: String => String = f // error - var b: List[String => String] = Nil // error + var b: List[String => String] = Nil // was error, now OK + b.head // error def scope = val cap3: Cap = CC() From 127a223d69fd39c55841861ca7353d0d3a962074 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 30 Jan 2022 18:44:48 +0100 Subject: [PATCH 21/99] Three changes to typing rules The following two rules replace #13657: 1. Exploit capture monotonicity in the apply rule, as discussed in #14387. 2. A rule to make typing nested classes more flexible as discussed in #14390. There's also a bug fix where we now enforce a previously missing subcapturing relationship between the capture set of parent of a class and the capture set of the class itself. Clearly a class captures all variables captured by one of its parent classes. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 2 +- .../dotty/tools/dotc/transform/Recheck.scala | 38 ++++----- .../tools/dotc/typer/CheckCaptures.scala | 82 +++++++++++++++---- tests/neg-custom-args/captures/lazylist.check | 4 +- .../neg-custom-args/captures/lazylists1.check | 10 +-- .../neg-custom-args/captures/lazylists1.scala | 2 +- .../neg-custom-args/captures/lazylists2.check | 32 +++++--- .../neg-custom-args/captures/lazylists2.scala | 8 +- .../captures/lazylists-exceptions.scala | 68 +++++++++++++++ .../pos-custom-args/captures/lazylists.scala | 1 - .../pos-custom-args/captures/lazylists1.scala | 17 ++-- 11 files changed, 198 insertions(+), 66 deletions(-) create mode 100644 tests/pos-custom-args/captures/lazylists-exceptions.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index a987b8788dd1..bf1306024e5f 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -104,7 +104,7 @@ sealed abstract class CaptureSet extends Showable: extension (x: CaptureRef) private def subsumes(y: CaptureRef) = (x eq y) || y.match - case y: TermRef => y.prefix eq x // ^^^ y.prefix.subsumes(x) ? + case y: TermRef => y.prefix eq x case _ => false /** {x} <:< this where <:< is subcapturing, but treating all variables diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index f9af3c38de8a..d100bdd36f21 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -126,15 +126,12 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: bindType def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = - if !tree.rhs.isEmpty then recheckRHS(tree.rhs, sym.info, sym) + if !tree.rhs.isEmpty then recheck(tree.rhs, sym.info) def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = val rhsCtx = linkConstructorParams(sym).withOwner(sym) if !tree.rhs.isEmpty && !sym.isInlineMethod && !sym.isEffectivelyErased then - inContext(rhsCtx) { recheckRHS(tree.rhs, recheck(tree.tpt), sym) } - - def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type = - recheck(tree, pt) + inContext(rhsCtx) { recheck(tree.rhs, recheck(tree.tpt)) } def recheckTypeDef(tree: TypeDef, sym: Symbol)(using Context): Type = recheck(tree.rhs) @@ -358,21 +355,22 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: // Don't report closure nodes, since their span is a point; wait instead // for enclosing block to preduce an error case _ => - val actual = tpe.widenExpr - val expected = pt.widenExpr - //println(i"check conforms $actual <:< $expected") - val isCompatible = - actual <:< expected - || expected.isRepeatedParam - && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) - if !isCompatible then - recheckr.println(i"conforms failed for ${tree}: $tpe vs $expected") - err.typeMismatch(tree.withType(tpe), expected) - else if debugSuccesses then - tree match - case _: Ident => - println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") - case _ => + checkConformsExpr(tpe, tpe.widenExpr, pt.widenExpr, tree) + + def checkConformsExpr(original: Type, actual: Type, expected: Type, tree: Tree)(using Context): Unit = + //println(i"check conforms $actual <:< $expected") + val isCompatible = + actual <:< expected + || expected.isRepeatedParam + && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) + if !isCompatible then + recheckr.println(i"conforms failed for ${tree}: $original vs $expected") + err.typeMismatch(tree.withType(original), expected) + else if debugSuccesses then + tree match + case _: Ident => + println(i"SUCCESS $tree:\n${TypeComparer.explained(_.isSubType(actual, expected))}") + case _ => def checkUnit(unit: CompilationUnit)(using Context): Unit = recheck(unit.tpdTree) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 994df9a382c4..2e81576eb317 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -16,6 +16,7 @@ import transform.Recheck import Recheck.* import scala.collection.mutable import CaptureSet.withCaptureSetsExplained +import StdNames.nme import reporting.trace object CheckCaptures: @@ -213,22 +214,6 @@ class CheckCaptures extends Recheck: interpolateVarsIn(tree.tpt) curEnv = saved - override def recheckRHS(tree: Tree, pt: Type, sym: Symbol)(using Context): Type = - val pt1 = pt match - case CapturingType(core, refs, _) - if sym.owner.isClass && !sym.owner.isExtensibleClass - && refs.elems.contains(sym.owner.thisType) => - val paramCaptures = - sym.paramSymss.flatten.foldLeft(CaptureSet.empty) { (cs, p) => - val pcs = p.info.captureSet - (cs ++ (if pcs.isConst then pcs else CaptureSet.universal)).asConst - } - val declaredCaptures = sym.owner.asClass.givenSelfType.captureSet - pt.derivedCapturingType(core, refs ++ (declaredCaptures -- paramCaptures)) - case _ => - pt - recheck(tree, pt1) - override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = for param <- cls.paramGetters do if param.is(Private) && !param.info.captureSet.isAlwaysEmpty then @@ -237,6 +222,8 @@ class CheckCaptures extends Recheck: param.srcPos) val saved = curEnv val localSet = capturedVars(cls) + for parent <- impl.parents do + checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos) if !localSet.isAlwaysEmpty then curEnv = Env(cls, localSet, false, curEnv) try super.recheckClassDef(tree, impl, cls) finally curEnv = saved @@ -289,9 +276,34 @@ class CheckCaptures extends Recheck: finally curEnv = curEnv.outer recheckFinish(result, arg, pt) + /** A specialized implementation of the apply rule from https://github.com/lampepfl/dotty/discussions/14387: + * + * E |- f: Cf (Ra -> Cr Rr) + * E |- a: Ra + * ------------------------ + * E |- f a: Cr /\ {f} Rr + * + * Specialized for the case where `f` is a tracked and the arguments are pure. + * This replaces the previous rule #13657 while still allowing the code in pos/lazylists1.scala. + * We could consider generalizing to the case where the function arguments have non-empty + * capture sets as suggested in #14387, but that would make capture set computations more complex, + * so we should also evaluate the performance impact. + */ override def recheckApply(tree: Apply, pt: Type)(using Context): Type = includeCallCaptures(tree.symbol, tree.srcPos) - super.recheckApply(tree, pt) + super.recheckApply(tree, pt) match + case tp @ CapturingType(tp1, refs, kind) => + tree.fun match + case Select(qual, nme.apply) + if defn.isFunctionType(qual.tpe.widen) => + qual.tpe match + case ref: CaptureRef + if ref.isTracked && tree.args.forall(_.tpe.captureSet.isAlwaysEmpty) => + tp.derivedCapturingType(tp1, refs ** ref.singletonCaptureSet) + .showing(i"narrow $tree: $tp --> $result", capt) + case _ => tp + case _ => tp + case tp => tp override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = val res = super.recheck(tree, pt) @@ -319,6 +331,42 @@ class CheckCaptures extends Recheck: case _ => super.recheckFinish(tpe, tree, pt) + /** This method implements the rule outlined in #14390: + * When checking an expression `e: Ca Ta` against an expected type `Cx Tx` + * where the capture set of `Cx` contains this and any method inside the class + * `Cls` of `this` that contains `e` has only pure parameters, drop from `Ca` + * all references to variables or this references outside `Cls`. These are all + * accessed through this, so are already accounted for by `Cx`. + */ + override def checkConformsExpr(original: Type, actual: Type, expected: Type, tree: Tree)(using Context): Unit = + def isPure(info: Type): Boolean = info match + case info: PolyType => isPure(info.resType) + case info: MethodType => info.paramInfos.forall(_.captureSet.isAlwaysEmpty) && isPure(info.resType) + case _ => true + def isPureContext(owner: Symbol, limit: Symbol): Boolean = + if owner == limit then true + else if !owner.exists then false + else isPure(owner.info) && isPureContext(owner.owner, limit) + val actual1 = (expected, actual.widen) match + case (CapturingType(ecore, erefs, _), actualw @ CapturingType(acore, arefs, _)) => + val arefs1 = (arefs /: erefs.elems) { (arefs1, eref) => + eref match + case eref: ThisType if isPureContext(ctx.owner, eref.cls) => + arefs1.filter { + case aref1: TermRef => !eref.cls.isContainedIn(aref1.symbol.owner) + case aref1: ThisType => !eref.cls.isContainedIn(aref1.cls) + case _ => true + } + case _ => + arefs1 + } + if arefs1 eq arefs then actual + else actualw.derivedCapturingType(acore, arefs1) + .showing(i"healing $actual --> $result", capt) + case _ => + actual + super.checkConformsExpr(original, actual1, expected, tree) + override def checkUnit(unit: CompilationUnit)(using Context): Unit = Setup(preRecheckPhase, thisPhase, recheckDef) .traverse(ctx.compilationUnit.tpdTree) diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 31624e437928..bdbef10de0d6 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -37,6 +37,6 @@ longer explanation available when compiling with `-explain` 17 | def tail = xs() // error: cannot have an inferred type | ^^^^^^^^^^^^^^^ | Non-local method tail cannot have an inferred result type - | {*} lazylists.LazyList[T] - | with non-empty capture set {*}. + | {LazyCons.this.xs} lazylists.LazyList[T] + | with non-empty capture set {LazyCons.this.xs}. | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/lazylists1.check b/tests/neg-custom-args/captures/lazylists1.check index 29291c8044c0..1d23de2ff134 100644 --- a/tests/neg-custom-args/captures/lazylists1.check +++ b/tests/neg-custom-args/captures/lazylists1.check @@ -1,7 +1,7 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists1.scala:25:63 ----------------------------------- -25 | def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: {xs, f} LazyList[A] - | Required: {Mapped.this, xs} LazyList[A] +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists1.scala:25:66 ----------------------------------- +25 | def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Found: {xs, f} LazyList[A] + | Required: {Mapped.this, f} LazyList[A] longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists1.scala b/tests/neg-custom-args/captures/lazylists1.scala index 4091ee2c62ae..c6475223b783 100644 --- a/tests/neg-custom-args/captures/lazylists1.scala +++ b/tests/neg-custom-args/captures/lazylists1.scala @@ -22,6 +22,6 @@ extension [A](xs: {*} LazyList[A]) def head: B = f(xs.head) def tail: {this} LazyList[B] = xs.tail.map(f) // OK def drop(n: Int): {this} LazyList[B] = ??? : ({xs, f} LazyList[B]) // OK - def concat(other: {f} LazyList[A]): {this} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error + def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : ({xs, f} LazyList[A]) // error new Mapped diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index a8f145ecd9d7..8cb825f02537 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -29,17 +29,29 @@ longer explanation available when compiling with `-explain` 32 | new Mapped longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:41:48 ----------------------------------- -41 | def tail: {this} LazyList[B] = xs.tail.map(f) // error - | ^^^^^^^^^^^^^^ - | Found: {f} LazyList[B] - | Required: {xs} LazyList[B] +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:36:4 ------------------------------------ +36 | final class Mapped extends LazyList[B]: // error + | ^ + | Found: {f, xs} LazyList[B] + | Required: {xs} LazyList[B] +37 | this: ({xs} Mapped) => +38 | def isEmpty = false +39 | def head: B = f(xs.head) +40 | def tail: {this} LazyList[B] = xs.tail.map(f) +41 | new Mapped longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:59:48 ----------------------------------- -59 | def tail: {this} LazyList[B] = xs.tail.map(f) // error - | ^^^^^^^^^^^^^^ - | Found: {f} LazyList[B] - | Required: {Mapped.this} LazyList[B] +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:54:4 ------------------------------------ +54 | class Mapped extends LazyList[B]: // error + | ^ + | Found: {f, xs} LazyList[B] + | Required: LazyList[B] +55 | this: ({xs, f} Mapped) => +56 | def isEmpty = false +57 | def head: B = f(xs.head) +58 | def tail: {this} LazyList[B] = xs.tail.map(f) +59 | class Mapped2 extends Mapped: +60 | this: Mapped => +61 | new Mapped2 longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists2.scala b/tests/neg-custom-args/captures/lazylists2.scala index b9ebb0a7a9f0..fc1ab768047a 100644 --- a/tests/neg-custom-args/captures/lazylists2.scala +++ b/tests/neg-custom-args/captures/lazylists2.scala @@ -33,12 +33,12 @@ extension [A](xs: {*} LazyList[A]) new Mapped def map3[B](f: A => B): {xs} LazyList[B] = - final class Mapped extends LazyList[B]: + final class Mapped extends LazyList[B]: // error this: ({xs} Mapped) => def isEmpty = false def head: B = f(xs.head) - def tail: {this} LazyList[B] = xs.tail.map(f) // error + def tail: {this} LazyList[B] = xs.tail.map(f) new Mapped def map4[B](f: A => B): {xs} LazyList[B] = @@ -51,12 +51,12 @@ extension [A](xs: {*} LazyList[A]) new Mapped def map5[B](f: A => B): LazyList[B] = - class Mapped extends LazyList[B]: + class Mapped extends LazyList[B]: // error this: ({xs, f} Mapped) => def isEmpty = false def head: B = f(xs.head) - def tail: {this} LazyList[B] = xs.tail.map(f) // error + def tail: {this} LazyList[B] = xs.tail.map(f) class Mapped2 extends Mapped: this: Mapped => new Mapped2 diff --git a/tests/pos-custom-args/captures/lazylists-exceptions.scala b/tests/pos-custom-args/captures/lazylists-exceptions.scala new file mode 100644 index 000000000000..6b2378906b8e --- /dev/null +++ b/tests/pos-custom-args/captures/lazylists-exceptions.scala @@ -0,0 +1,68 @@ +import language.experimental.saferExceptions +import annotation.unchecked.uncheckedVariance + +trait LazyList[+A]: + this: {*} LazyList[A] => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: + this: {*} LazyList[T] => + + var forced = false + var cache: {this} LazyList[T @uncheckedVariance] = compiletime.uninitialized + + private def force = + if !forced then + cache = xs() + forced = true + cache + + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = force +end LazyCons + +extension [A](xs: {*} LazyList[A]) + def map[B](f: A => B): {xs, f} LazyList[B] = + if xs.isEmpty then LazyNil + else LazyCons(f(xs.head), () => xs.tail.map(f)) + + def filter(p: A => Boolean): {xs, p} LazyList[A] = + if xs.isEmpty then LazyNil + else if p(xs.head) then LazyCons(xs.head, () => xs.tail.filter(p)) + else xs.tail.filter(p) + + def concat(ys: {*} LazyList[A]): {xs, ys} LazyList[A] = + if xs.isEmpty then ys + else LazyCons(xs.head, () => xs.tail.concat(ys)) +end extension + +class Ex1 extends Exception +class Ex2 extends Exception + +def test(using cap1: CanThrow[Ex1], cap2: CanThrow[Ex2]) = + val xs = LazyCons(1, () => LazyNil) + + def f(x: Int): Int throws Ex1 = + if x < 0 then throw Ex1() + x * x + + def g(x: Int): Int throws Ex1 = + if x < 0 then throw Ex1() + x * x + + def x1 = xs.map(f) + def x1c: {cap1} LazyList[Int] = x1 + + def x2 = x1.concat(xs.map(g).filter(_ > 0)) + def x2c: {cap1, cap2} LazyList[Int] = x2 + + diff --git a/tests/pos-custom-args/captures/lazylists.scala b/tests/pos-custom-args/captures/lazylists.scala index c566bea8dd64..fd130c87cdea 100644 --- a/tests/pos-custom-args/captures/lazylists.scala +++ b/tests/pos-custom-args/captures/lazylists.scala @@ -21,7 +21,6 @@ extension [A](xs: {*} LazyList[A]) def isEmpty = false def head: B = f(xs.head) def tail: {this} LazyList[B] = xs.tail.map(f) // OK - def concat(other: {f} LazyList[A]): {this, f} LazyList[A] = ??? : {xs, f} LazyList[A] // OK if xs.isEmpty then LazyNil else new Mapped diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala index 2dbb5ac232e2..c203450499e7 100644 --- a/tests/pos-custom-args/captures/lazylists1.scala +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -7,29 +7,36 @@ trait LazyList[+A]: def isEmpty: Boolean def head: A def tail: {this} LazyList[A] + def concat[B >: A](other: {*} LazyList[B]): {this, other} LazyList[B] object LazyNil extends LazyList[Nothing]: def isEmpty: Boolean = true def head = ??? def tail = ??? + def concat[B](other: {*} LazyList[B]): {other} LazyList[B] = other -final class LazyCons[+T](val x: T, val xs: Int => {*} LazyList[T]) extends LazyList[T]: - this: {*} LazyList[T] => +final class LazyCons[+A](val x: A, val xs: () => {*} LazyList[A]) extends LazyList[A]: + this: {*} LazyList[A] => def isEmpty = false def head = x - def tail: {this} LazyList[T] = xs(0) + def tail: {this} LazyList[A] = xs() + def concat[B >: A](other: {*} LazyList[B]): {this, other} LazyList[B] = + LazyCons(head, () => tail.concat(other)) extension [A](xs: {*} LazyList[A]) def map[B](f: A => B): {xs, f} LazyList[B] = if xs.isEmpty then LazyNil - else LazyCons(f(xs.head), x => xs.tail.map(f)) + else LazyCons(f(xs.head), () => xs.tail.map(f)) def test(cap1: Cap, cap2: Cap) = def f(x: String): String = if cap1 == cap1 then "" else "a" def g(x: String): String = if cap2 == cap2 then "" else "a" - val xs = new LazyCons("", x => if f("") == f("") then LazyNil else LazyNil) + val xs = new LazyCons("", () => if f("") == f("") then LazyNil else LazyNil) val xsc: {cap1} LazyList[String] = xs val ys = xs.map(g) val ysc: {cap1, cap2} LazyList[String] = ys + val zs = new LazyCons("", () => if g("") == g("") then LazyNil else LazyNil) + val as = xs.concat(zs) + val asc: {xs, zs} LazyList[String] = as From 1b4370bed97a0ba38ed13718ff0198f0353e7c07 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 1 Feb 2022 11:14:42 +0100 Subject: [PATCH 22/99] Change result type of by-name closures Make it the formal type rather than the actual one. This avoids messing up capture annotations. --- .../tools/dotc/transform/ElimByName.scala | 9 +++------ tests/neg-custom-args/captures/byname.check | 18 +++++++++--------- tests/neg-custom-args/captures/byname.scala | 5 ++++- .../captures/lazylists-exceptions.scala | 13 ++++++++++--- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/ElimByName.scala b/compiler/src/dotty/tools/dotc/transform/ElimByName.scala index efdd18291821..479a455b4aea 100644 --- a/compiler/src/dotty/tools/dotc/transform/ElimByName.scala +++ b/compiler/src/dotty/tools/dotc/transform/ElimByName.scala @@ -94,6 +94,7 @@ class ElimByName extends MiniPhase, InfoTransformer: sym.is(Method) || exprBecomesFunction(sym) def byNameClosure(arg: Tree, argType: Type)(using Context): Tree = + report.log(i"creating by name closure for $argType") val meth = newAnonFun(ctx.owner, MethodType(Nil, argType), coord = arg.span) Closure(meth, _ => arg.changeOwnerAfter(ctx.owner, meth, thisPhase), @@ -137,12 +138,8 @@ class ElimByName extends MiniPhase, InfoTransformer: if isByNameRef(qual) && (isPureExpr(qual) || qual.symbol.isAllOf(InlineParam)) => qual case _ => - if isByNameRef(arg) || arg.symbol.name.is(SuperArgName) - then arg - else - var argType = arg.tpe.widenIfUnstable - if argType.isBottomType then argType = formalResult - byNameClosure(arg, argType) + if isByNameRef(arg) || arg.symbol.name.is(SuperArgName) then arg + else byNameClosure(arg, formalResult) case _ => arg diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index d8d5d689efae..3321da3c17db 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -1,18 +1,18 @@ --- Warning: tests/neg-custom-args/captures/byname.scala:14:18 ---------------------------------------------------------- -14 | def h(x: {cap1} -> I) = x // warning +-- Warning: tests/neg-custom-args/captures/byname.scala:17:18 ---------------------------------------------------------- +17 | def h(x: {cap1} -> I) = x // warning | ^ | Style: by-name `->` should immediately follow closing `}` of capture set | to avoid confusion with function type. | That is, `{c}-> T` instead of `{c} -> T`. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:7:5 ----------------------------------------- -7 | h(f()) // error - | ^^^ - | Found: {cap2} (x$0: Int) -> Int - | Required: Int -> Int +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:10:6 ---------------------------------------- +10 | h(f2()) // error + | ^^^^ + | Found: {cap1} (x$0: Int) -> Int + | Required: {cap2} Int -> Int longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:16:5 ---------------------------------------- -16 | h(g()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:19:5 ---------------------------------------- +19 | h(g()) // error | ^^^ | Found: {cap2} () ?-> I | Required: {cap1} () ?-> I diff --git a/tests/neg-custom-args/captures/byname.scala b/tests/neg-custom-args/captures/byname.scala index 0afca7e261e2..1838647f2899 100644 --- a/tests/neg-custom-args/captures/byname.scala +++ b/tests/neg-custom-args/captures/byname.scala @@ -3,8 +3,11 @@ def test(cap1: Cap, cap2: Cap) = def f() = if cap1 == cap1 then g else g def g(x: Int) = if cap2 == cap2 then 1 else x + def g2(x: Int) = if cap1 == cap1 then 1 else x + def f2() = if cap1 == cap1 then g2 else g2 def h(ff: => {cap2} Int -> Int) = ff - h(f()) // error + h(f()) // ok + h(f2()) // error class I diff --git a/tests/pos-custom-args/captures/lazylists-exceptions.scala b/tests/pos-custom-args/captures/lazylists-exceptions.scala index 6b2378906b8e..b9b303118358 100644 --- a/tests/pos-custom-args/captures/lazylists-exceptions.scala +++ b/tests/pos-custom-args/captures/lazylists-exceptions.scala @@ -37,19 +37,26 @@ extension [A](xs: {*} LazyList[A]) def filter(p: A => Boolean): {xs, p} LazyList[A] = if xs.isEmpty then LazyNil - else if p(xs.head) then LazyCons(xs.head, () => xs.tail.filter(p)) + else if p(xs.head) then lazyCons(xs.head, xs.tail.filter(p)) else xs.tail.filter(p) def concat(ys: {*} LazyList[A]): {xs, ys} LazyList[A] = if xs.isEmpty then ys - else LazyCons(xs.head, () => xs.tail.concat(ys)) + else xs.head #: xs.tail.concat(ys) end extension +extension [A](x: A) + def #:(xs1: => {*} LazyList[A]): {xs1} LazyList[A] = + LazyCons(x, () => xs1) + +def lazyCons[A](x: A, xs1: => {*} LazyList[A]): {xs1} LazyList[A] = + LazyCons(x, () => xs1) + class Ex1 extends Exception class Ex2 extends Exception def test(using cap1: CanThrow[Ex1], cap2: CanThrow[Ex2]) = - val xs = LazyCons(1, () => LazyNil) + val xs = 1 #: LazyNil def f(x: Int): Int throws Ex1 = if x < 0 then throw Ex1() From 0949618578dd1ec4dfe9054d3152ded356282594 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 1 Feb 2022 14:58:19 +0100 Subject: [PATCH 23/99] Capture-check exception capabilties Required exeption capability references are now preserved beyond typer in type ascriptions, so that they can be checked for escapes. --- .../dotty/tools/dotc/core/Definitions.scala | 1 + .../tools/dotc/typer/CheckCaptures.scala | 10 +++++ .../src/dotty/tools/dotc/typer/Checking.scala | 6 ++- .../src/dotty/tools/dotc/typer/Typer.scala | 13 ++++-- .../scala/internal/requiresCapability.scala | 8 ++++ .../captures/lazylists-exceptions.check | 9 ++++ .../captures/lazylists-exceptions.scala | 45 +++++++++++++++++++ tests/pos-custom-args/captures/i13816.scala | 4 +- .../captures/lazylists-exceptions.scala | 11 +++++ 9 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 library/src-bootstrapped/scala/internal/requiresCapability.scala create mode 100644 tests/neg-custom-args/captures/lazylists-exceptions.check create mode 100644 tests/neg-custom-args/captures/lazylists-exceptions.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3ca61ab1a372..69709b9ce347 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1016,6 +1016,7 @@ class Definitions { @tu lazy val TargetNameAnnot: ClassSymbol = requiredClass("scala.annotation.targetName") @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val SinceAnnot: ClassSymbol = requiredClass("scala.annotation.since") + @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.retains") @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.retainsByName") diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 2e81576eb317..46ce6d037ff3 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -305,6 +305,16 @@ class CheckCaptures extends Recheck: case _ => tp case tp => tp + override def recheckTyped(tree: Typed)(using Context): Type = + tree.tpt.tpe match + case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => + annot.tree match + case Apply(_, cap :: Nil) => + markFree(cap.symbol, tree.srcPos) + case _ => + case _ => + super.recheckTyped(tree) + override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = val res = super.recheck(tree, pt) if tree.isTerm then diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 6de903972c1f..4edcaab9f5eb 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -1422,9 +1422,11 @@ trait Checking { val kind = if pattern then "pattern selector" else "value" report.warning(MatchableWarning(tp, pattern), pos) - def checkCanThrow(tp: Type, span: Span)(using Context): Unit = + def checkCanThrow(tp: Type, span: Span)(using Context): Tree = if Feature.enabled(Feature.saferExceptions) && tp.isCheckedException then ctx.typer.implicitArgTree(defn.CanThrowClass.typeRef.appliedTo(tp), span) + else + EmptyTree /** Check that catch can generate a good CanThrow exception */ def checkCatch(pat: Tree, guard: Tree)(using Context): Unit = pat match @@ -1474,7 +1476,7 @@ trait ReChecking extends Checking { override def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean = true override def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = () override def checkNoModuleClash(sym: Symbol)(using Context) = () - override def checkCanThrow(tp: Type, span: Span)(using Context): Unit = () + override def checkCanThrow(tp: Type, span: Span)(using Context): Tree = EmptyTree override def checkCatch(pat: Tree, guard: Tree)(using Context): Unit = () override def checkFeature(name: TermName, description: => String, featureUseSite: Symbol, pos: SrcPos)(using Context): Unit = () } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 557eea853f81..815c59c46056 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1870,11 +1870,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer desugar.makeTryCase(handler1) :: Nil typedTry(untpd.Try(tree.expr, cases, tree.finalizer).withSpan(tree.span), pt) - def typedThrow(tree: untpd.Throw)(using Context): Tree = { + def typedThrow(tree: untpd.Throw)(using Context): Tree = val expr1 = typed(tree.expr, defn.ThrowableType) - checkCanThrow(expr1.tpe.widen, tree.span) - Throw(expr1).withSpan(tree.span) - } + val cap = checkCanThrow(expr1.tpe.widen, tree.span) + val res = Throw(expr1).withSpan(tree.span) + if cap.isEmpty || !ctx.settings.Ycc.value || ctx.isAfterTyper then res + else + Typed(res, + TypeTree( + AnnotatedType(res.tpe, + Annotation(defn.RequiresCapabilityAnnot, cap)))) def typedSeqLiteral(tree: untpd.SeqLiteral, pt: Type)(using Context): SeqLiteral = { val elemProto = pt.stripNull.elemType match { diff --git a/library/src-bootstrapped/scala/internal/requiresCapability.scala b/library/src-bootstrapped/scala/internal/requiresCapability.scala new file mode 100644 index 000000000000..371c44173f0b --- /dev/null +++ b/library/src-bootstrapped/scala/internal/requiresCapability.scala @@ -0,0 +1,8 @@ +package scala.annotation.internal + +import scala.annotation.StaticAnnotation + +/** An annotation to record a required capaility in the type of a throws + */ +class requiresCapability(capability: Any) extends StaticAnnotation + diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check new file mode 100644 index 000000000000..cf55ff8ebd55 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -0,0 +1,9 @@ +-- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- +36 | try // error + | ^ + | the expression's type {*} LazyList[Int] is not allowed to capture the root capability `*` +37 | tabulate(10) { i => +38 | if i > 9 then throw Ex1() +39 | i * i +40 | } +41 | catch case ex: Ex1 => LazyNil diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.scala b/tests/neg-custom-args/captures/lazylists-exceptions.scala new file mode 100644 index 000000000000..6d325abcf936 --- /dev/null +++ b/tests/neg-custom-args/captures/lazylists-exceptions.scala @@ -0,0 +1,45 @@ +import language.experimental.saferExceptions + +trait LazyList[+A]: + this: {*} LazyList[A] => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: + this: {*} LazyList[T] => + + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = xs() +end LazyCons + +extension [A](x: A) + def #:(xs1: => {*} LazyList[A]): {xs1} LazyList[A] = + LazyCons(x, () => xs1) + +def tabulate[A](n: Int)(gen: Int => A) = + def recur(i: Int): {gen} LazyList[A] = + if i == n then LazyNil + else gen(i) #: recur(i + 1) + recur(0) + +class Ex1 extends Exception + +def problem = + try // error + tabulate(10) { i => + if i > 9 then throw Ex1() + i * i + } + catch case ex: Ex1 => LazyNil + + + + diff --git a/tests/pos-custom-args/captures/i13816.scala b/tests/pos-custom-args/captures/i13816.scala index b8f9db405188..235afef35f1c 100644 --- a/tests/pos-custom-args/captures/i13816.scala +++ b/tests/pos-custom-args/captures/i13816.scala @@ -27,10 +27,10 @@ def foo5(i: Int)(using CanThrow[Ex1])(using CanThrow[Ex2]): Unit = def foo6(i: Int)(using CanThrow[Ex1 | Ex2]): Unit = if i > 0 then throw new Ex1 else throw new Ex2 -def foo7(i: Int)(using CanThrow[Ex1]): Unit throws Ex2 = +def foo7(i: Int)(using CanThrow[Ex1]): Unit throws Ex1 | Ex2 = if i > 0 then throw new Ex1 else throw new Ex2 -def foo8(i: Int)(using CanThrow[Ex2]): Unit throws Ex1 = +def foo8(i: Int)(using CanThrow[Ex2]): Unit throws Ex2 | Ex1 = if i > 0 then throw new Ex1 else throw new Ex2 def test(): Unit = diff --git a/tests/pos-custom-args/captures/lazylists-exceptions.scala b/tests/pos-custom-args/captures/lazylists-exceptions.scala index b9b303118358..96b179e564c3 100644 --- a/tests/pos-custom-args/captures/lazylists-exceptions.scala +++ b/tests/pos-custom-args/captures/lazylists-exceptions.scala @@ -52,6 +52,12 @@ extension [A](x: A) def lazyCons[A](x: A, xs1: => {*} LazyList[A]): {xs1} LazyList[A] = LazyCons(x, () => xs1) +def tabulate[A](n: Int)(gen: Int => A) = + def recur(i: Int): {gen} LazyList[A] = + if i == n then LazyNil + else gen(i) #: recur(i + 1) + recur(0) + class Ex1 extends Exception class Ex2 extends Exception @@ -72,4 +78,9 @@ def test(using cap1: CanThrow[Ex1], cap2: CanThrow[Ex2]) = def x2 = x1.concat(xs.map(g).filter(_ > 0)) def x2c: {cap1, cap2} LazyList[Int] = x2 + val x3 = tabulate(10) { i => + if i > 9 then throw Ex1() + i * i + } + val x3c: {cap1} LazyList[Int] = x3 From c361123fca5a052b838dc7c11c9761be90bda6ef Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 2 Feb 2022 16:43:30 +0100 Subject: [PATCH 24/99] Drop restriction that tracked class parameters must be `val`s. To circumvent the restriction and to allow class refinements as inferred types we temporarily make class parameters public during phase CheckCaptures. --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 +- .../src/dotty/tools/dotc/core/Flags.scala | 4 +- .../tools/dotc/transform/PreRecheck.scala | 6 +-- .../dotty/tools/dotc/transform/Recheck.scala | 28 ++++++++-- .../tools/dotc/typer/CheckCaptures.scala | 22 +++++--- tests/neg-custom-args/captures/classes.scala | 4 +- .../pos-custom-args/captures/lazylists1.scala | 2 +- tests/pos-custom-args/captures/logger.scala | 54 +++++++++++++++++++ tests/pos-custom-args/captures/pairs.scala | 2 +- 9 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 tests/pos-custom-args/captures/logger.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index 94a5ef473fc8..fb972c5e7973 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -79,7 +79,7 @@ class Compiler { new SpecializeApplyMethods, // Adds specialized methods to FunctionN new TryCatchPatterns, // Compile cases in try/catch new PatternMatcher) :: // Compile pattern matches - List(new PreRecheck) :: // Preparations for check captures phase, enabled under -Ycc + List(new CheckCaptures.Pre) :: // Preparations for check captures phase, enabled under -Ycc List(new CheckCaptures) :: // Check captures, enabled under -Ycc List(new ElimOpaque, // Turn opaque into normal aliases new sjs.ExplicitJSClasses, // Make all JS classes explicit (Scala.js only) diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index f2682621a7bd..152b2075b514 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -404,7 +404,9 @@ object Flags { /** Children were queried on this class */ val (_, _, ChildrenQueried @ _) = newFlags(56, "") - /** A module variable (Scala 2.x only) */ + /** A module variable (Scala 2.x only) + * (re-used as a capture checking flag in CheckCaptures) + */ val (_, Scala2ModuleVar @ _, _) = newFlags(57, "") /** A macro */ diff --git a/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala index ab27bb3bb306..53a1548a4227 100644 --- a/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala @@ -2,18 +2,16 @@ package dotty.tools.dotc package transform import core.Phases.Phase -import core.DenotTransformers.IdentityDenotTransformer +import core.DenotTransformers.DenotTransformer import core.Contexts.{Context, ctx} /** A phase that precedes the rechecker and that allows installing * new types for local symbols. */ -class PreRecheck extends Phase, IdentityDenotTransformer: +abstract class PreRecheck extends Phase, DenotTransformer: def phaseName: String = "preRecheck" - override def isEnabled(using Context) = next.isEnabled - override def changesBaseTypes: Boolean = true def run(using Context): Unit = () diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index d100bdd36f21..ec767093373a 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -7,7 +7,7 @@ import Symbols.*, Contexts.*, Types.*, ContextOps.*, Decorators.*, SymDenotation import Flags.*, SymUtils.*, NameKinds.* import ast.* import Phases.Phase -import DenotTransformers.{IdentityDenotTransformer, DenotTransformer} +import DenotTransformers.{DenotTransformer, IdentityDenotTransformer, SymTransformer} import NamerOps.{methodType, linkConstructorParams} import NullOpsDecorator.stripNull import typer.ErrorReporting.err @@ -22,6 +22,14 @@ import reporting.trace object Recheck: + /** A flag used to indicate that a ParamAccessor has been temporily made not-private + * Only used at the start of the Recheck phase, reset at its end. + * The flag repurposes the Scala2ModuleVar flag. No confusion is possible since + * Scala2ModuleVar cannot be also ParamAccessors. + */ + val ResetPrivate = Scala2ModuleVar + val ResetPrivateParamAccessor = ResetPrivate | ParamAccessor + import tpd.Tree /** Attachment key for rechecked types of TypeTrees */ @@ -34,7 +42,12 @@ object Recheck: */ def updateInfoBetween(prevPhase: DenotTransformer, lastPhase: DenotTransformer, newInfo: Type)(using Context): Unit = if sym.info ne newInfo then - sym.copySymDenotation().installAfter(lastPhase) // reset + sym.copySymDenotation( + initFlags = + if sym.flags.isAllOf(ResetPrivateParamAccessor) + then sym.flags &~ ResetPrivate | Private + else sym.flags + ).installAfter(lastPhase) // reset sym.copySymDenotation( info = newInfo, initFlags = @@ -64,7 +77,7 @@ object Recheck: def hasRememberedType: Boolean = tree.hasAttachment(RecheckedType) -abstract class Recheck extends Phase, IdentityDenotTransformer: +abstract class Recheck extends Phase, SymTransformer: thisPhase => import ast.tpd.* @@ -81,6 +94,12 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: override def widenSkolems = true + /** Change any `ResetPrivate` flags back to `Private` */ + def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if sym.isAllOf(Recheck.ResetPrivateParamAccessor) then + sym.copySymDenotation(initFlags = sym.flags &~ Recheck.ResetPrivate | Private) + else sym + def run(using Context): Unit = newRechecker().checkUnit(ctx.compilationUnit) @@ -389,6 +408,9 @@ abstract class Recheck extends Phase, IdentityDenotTransformer: } end Recheck +object TestRecheck: + class Pre extends PreRecheck, IdentityDenotTransformer + class TestRecheck extends Recheck: def phaseName: String = "recheck" //override def isEnabled(using Context) = ctx.settings.YrefineTypes.value diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 46ce6d037ff3..19c01d607458 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -12,7 +12,7 @@ import Trees.* import typer.RefChecks.checkAllOverrides import util.{SimpleIdentitySet, EqHashMap, SrcPos} import transform.SymUtils.* -import transform.Recheck +import transform.{Recheck, PreRecheck} import Recheck.* import scala.collection.mutable import CaptureSet.withCaptureSetsExplained @@ -22,6 +22,19 @@ import reporting.trace object CheckCaptures: import ast.tpd.* + class Pre extends PreRecheck, SymTransformer: + + override def isEnabled(using Context) = ctx.settings.Ycc.value + + /** Reset `private` flags of parameter accessors so that we can refine them + * in Setup if they have non-empty capture sets + */ + def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if sym.isAllOf(PrivateParamAccessor) + then sym.copySymDenotation(initFlags = sym.flags &~ Private | Recheck.ResetPrivate) + else sym + end Pre + case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer: Env): def isOpen = !captured.isAlwaysEmpty && !isBoxed @@ -77,7 +90,7 @@ object CheckCaptures: if remaining.accountsFor(firstRef) then report.warning(em"redundant capture: $remaining already accounts for $firstRef", ann.srcPos) -class CheckCaptures extends Recheck: +class CheckCaptures extends Recheck, SymTransformer: thisPhase => import ast.tpd.* @@ -215,11 +228,6 @@ class CheckCaptures extends Recheck: curEnv = saved override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = - for param <- cls.paramGetters do - if param.is(Private) && !param.info.captureSet.isAlwaysEmpty then - report.error( - "Implementation restriction: Class parameter with non-empty capture set must be a `val`", - param.srcPos) val saved = curEnv val localSet = capturedVars(cls) for parent <- impl.parents do diff --git a/tests/neg-custom-args/captures/classes.scala b/tests/neg-custom-args/captures/classes.scala index b87d21913d4e..e4c141ea981b 100644 --- a/tests/neg-custom-args/captures/classes.scala +++ b/tests/neg-custom-args/captures/classes.scala @@ -1,6 +1,6 @@ class B type Cap = {*} B -class C0(n: Cap) // error: class parameter must be a `val`. +class C0(n: Cap) // was error: class parameter must be a `val`, now OK class C(val n: Cap): def foo(): {n} B = n @@ -8,5 +8,5 @@ class C(val n: Cap): def test(x: Cap, y: Cap) = val c0 = C(x) val c1: C = c0 // error - val c2 = if ??? then C(x) else /*identity*/(C(y)) // TODO: uncomment + val c2 = if ??? then C(x) else identity(C(y)) val c3: {x} C { val n: {x, y} B } = c2 // error diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala index c203450499e7..2e6f55d06451 100644 --- a/tests/pos-custom-args/captures/lazylists1.scala +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -15,7 +15,7 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? def concat[B](other: {*} LazyList[B]): {other} LazyList[B] = other -final class LazyCons[+A](val x: A, val xs: () => {*} LazyList[A]) extends LazyList[A]: +final class LazyCons[+A](x: A, xs: () => {*} LazyList[A]) extends LazyList[A]: this: {*} LazyList[A] => def isEmpty = false diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala new file mode 100644 index 000000000000..05b2732b11f1 --- /dev/null +++ b/tests/pos-custom-args/captures/logger.scala @@ -0,0 +1,54 @@ +class FileSystem + +class Logger(fs: {*} FileSystem): + def log(s: String): Unit = ??? + +def delayed(l: {*} Logger) = + () => + l.log("hi") + 22 + +def test(fs: {*} FileSystem) = + val l0 = Logger(fs) + val l: {fs} Logger = Logger(fs) + l.log("hello world!") + val xs: {l} LazyList[Int] = + LazyList.from(1) + .map { i => + l.log(s"computing elem # $i") + i * i + } + +trait LazyList[+A]: + this: {*} LazyList[A] => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + +object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + +final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: + this: {*} LazyList[T] => + + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = xs() +end LazyCons + +extension [A](x: A) + def #::(xs1: => {*} LazyList[A]): {xs1} LazyList[A] = + LazyCons(x, () => xs1) + +extension [A](xs: {*} LazyList[A]) + def map[B](f: A => B): {xs, f} LazyList[B] = + if xs.isEmpty then LazyNil + else f(xs.head) #:: xs.tail.map(f) + +object LazyList: + def from(start: Int): LazyList[Int] = + start #:: from(start + 1) + diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index 14d484ff21b1..c4ce3148d762 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -18,7 +18,7 @@ object Generic: object Monomorphic: - class Pair(val x: Cap => Unit, val y: {*} Cap -> Unit): + class Pair(x: Cap => Unit, y: {*} Cap -> Unit): def fst = x def snd = y From 5460a83514c342747e58ca86aab5f4075eeb7e46 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 2 Feb 2022 17:57:02 +0100 Subject: [PATCH 25/99] Handle constructors with multiple parameter lists --- .../tools/dotc/typer/CheckCaptures.scala | 52 ++++++++++++------- .../pos-custom-args/captures/lazylists1.scala | 10 ++-- tests/pos-custom-args/captures/logger.scala | 8 +-- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 19c01d607458..1f7d93e098ac 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -236,22 +236,6 @@ class CheckCaptures extends Recheck, SymTransformer: try super.recheckClassDef(tree, impl, cls) finally curEnv = saved - /** First half: Refine the type of a constructor call `new C(t_1, ..., t_n)` - * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked - * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. - * - * Second half: union of all capture sets of arguments to tracked parameters. - */ - private def addParamArgRefinements(core: Type, argTypes: List[Type], cls: ClassSymbol)(using Context): (Type, CaptureSet) = - cls.paramGetters.lazyZip(argTypes).foldLeft((core, CaptureSet.empty: CaptureSet)) { (acc, refine) => - val (core, allCaptures) = acc - val (getter, argType) = refine - if getter.termRef.isTracked then - (RefinedType(core, getter.name, argType), allCaptures ++ argType.captureSet) - else - (core, allCaptures) - } - /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. * This means: * - Instantiate result type with actual arguments @@ -264,12 +248,42 @@ class CheckCaptures extends Recheck, SymTransformer: val ownType = if mt.isResultDependent then SubstParamsMap(mt, argTypes)(mt.resType) else mt.resType + if sym.isConstructor then val cls = sym.owner.asClass - val (refined, cs) = addParamArgRefinements(ownType, argTypes, cls) - refined.capturing(cs ++ capturedVars(cls) ++ capturedVars(sym)) - .showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt) + + /** First half: Refine the type of a constructor call `new C(t_1, ..., t_n)` + * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked + * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. + * + * Second half: union of all capture sets of arguments to tracked parameters. + */ + def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = + mt.paramNames.lazyZip(argTypes).foldLeft((core, initCs)) { (acc, refine) => + val (core, allCaptures) = acc + val (getterName, argType) = refine + val getter = cls.info.member(getterName).suchThat(_.is(ParamAccessor)).symbol + if getter.termRef.isTracked then + (RefinedType(core, getterName, argType), allCaptures ++ argType.captureSet) + else + (core, allCaptures) + } + + def augmentConstructorType(core: Type, initCs: CaptureSet): Type = core match + case core: MethodType => + core.derivedLambdaType(resType = augmentConstructorType(core.resType, initCs)) + case CapturingType(parent, refs, _) => + augmentConstructorType(parent, initCs ++ refs) + case _ => + val (refined, cs) = addParamArgRefinements(core, initCs) + refined.capturing(cs) + + val augmented = augmentConstructorType(ownType, CaptureSet.empty) + { if augmented.isInstanceOf[MethodType] then augmented + else augmented.capturing(capturedVars(cls) ++ capturedVars(sym)) + }.showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt) else ownType + end instantiate def recheckByNameArg(tree: Tree, pt: Type)(using Context): Type = val closureDef(mdef) = tree diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala index 2e6f55d06451..df8090fee667 100644 --- a/tests/pos-custom-args/captures/lazylists1.scala +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -15,28 +15,28 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? def concat[B](other: {*} LazyList[B]): {other} LazyList[B] = other -final class LazyCons[+A](x: A, xs: () => {*} LazyList[A]) extends LazyList[A]: +final class LazyCons[+A](x: A)(xs: () => {*} LazyList[A]) extends LazyList[A]: this: {*} LazyList[A] => def isEmpty = false def head = x def tail: {this} LazyList[A] = xs() def concat[B >: A](other: {*} LazyList[B]): {this, other} LazyList[B] = - LazyCons(head, () => tail.concat(other)) + LazyCons(head)(() => tail.concat(other)) extension [A](xs: {*} LazyList[A]) def map[B](f: A => B): {xs, f} LazyList[B] = if xs.isEmpty then LazyNil - else LazyCons(f(xs.head), () => xs.tail.map(f)) + else LazyCons(f(xs.head))(() => xs.tail.map(f)) def test(cap1: Cap, cap2: Cap) = def f(x: String): String = if cap1 == cap1 then "" else "a" def g(x: String): String = if cap2 == cap2 then "" else "a" - val xs = new LazyCons("", () => if f("") == f("") then LazyNil else LazyNil) + val xs = new LazyCons("")(() => if f("") == f("") then LazyNil else LazyNil) val xsc: {cap1} LazyList[String] = xs val ys = xs.map(g) val ysc: {cap1, cap2} LazyList[String] = ys - val zs = new LazyCons("", () => if g("") == g("") then LazyNil else LazyNil) + val zs = new LazyCons("")(() => if g("") == g("") then LazyNil else LazyNil) val as = xs.concat(zs) val asc: {xs, zs} LazyList[String] = as diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala index 05b2732b11f1..1bda407bda79 100644 --- a/tests/pos-custom-args/captures/logger.scala +++ b/tests/pos-custom-args/captures/logger.scala @@ -1,6 +1,6 @@ class FileSystem -class Logger(fs: {*} FileSystem): +class Logger(using fs: {*} FileSystem): def log(s: String): Unit = ??? def delayed(l: {*} Logger) = @@ -8,9 +8,9 @@ def delayed(l: {*} Logger) = l.log("hi") 22 -def test(fs: {*} FileSystem) = - val l0 = Logger(fs) - val l: {fs} Logger = Logger(fs) +def test(using fs: {*} FileSystem) = + val l0 = Logger() + val l: {fs} Logger = Logger(using fs) l.log("hello world!") val xs: {l} LazyList[Int] = LazyList.from(1) From ba8ae0fc1ba50a5f6e3052663acd1e2f27e9b469 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Thu, 3 Feb 2022 14:48:09 +0100 Subject: [PATCH 26/99] Don't add class refinements for @constructorOnly parameters --- .../dotty/tools/dotc/typer/CheckCaptures.scala | 9 ++++----- .../src/scala/annotation/constructorOnly.scala | 5 ++++- tests/pos-custom-args/captures/logger.scala | 15 ++++++--------- tests/pos-custom-args/captures/null-logger.scala | 9 +++++++++ 4 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 tests/pos-custom-args/captures/null-logger.scala diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 1f7d93e098ac..9abc8c56c0f7 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -30,7 +30,7 @@ object CheckCaptures: * in Setup if they have non-empty capture sets */ def transformSym(sym: SymDenotation)(using Context): SymDenotation = - if sym.isAllOf(PrivateParamAccessor) + if sym.isAllOf(PrivateParamAccessor) && !sym.hasAnnotation(defn.ConstructorOnlyAnnot) then sym.copySymDenotation(initFlags = sym.flags &~ Private | Recheck.ResetPrivate) else sym end Pre @@ -263,10 +263,9 @@ class CheckCaptures extends Recheck, SymTransformer: val (core, allCaptures) = acc val (getterName, argType) = refine val getter = cls.info.member(getterName).suchThat(_.is(ParamAccessor)).symbol - if getter.termRef.isTracked then - (RefinedType(core, getterName, argType), allCaptures ++ argType.captureSet) - else - (core, allCaptures) + if getter.termRef.isTracked && !getter.is(Private) + then (RefinedType(core, getterName, argType), allCaptures ++ argType.captureSet) + else (core, allCaptures) } def augmentConstructorType(core: Type, initCs: CaptureSet): Type = core match diff --git a/library/src/scala/annotation/constructorOnly.scala b/library/src/scala/annotation/constructorOnly.scala index 580354e468a3..c78c16534183 100644 --- a/library/src/scala/annotation/constructorOnly.scala +++ b/library/src/scala/annotation/constructorOnly.scala @@ -16,5 +16,8 @@ import scala.annotation.meta._ * compile errors. The annotation is particularly useful for implicit * parameters since for these a textual scan is not sufficient to know * where they are used. + * Note: the annotation is copied from constructor parameters to corresponding + * class fields. But it is checked that the field is eliminated before code + * is generated. */ -@param class constructorOnly extends scala.annotation.StaticAnnotation +@param @field class constructorOnly extends scala.annotation.StaticAnnotation diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala index 1bda407bda79..bb5f3f44f22e 100644 --- a/tests/pos-custom-args/captures/logger.scala +++ b/tests/pos-custom-args/captures/logger.scala @@ -1,15 +1,11 @@ -class FileSystem +import annotation.capability -class Logger(using fs: {*} FileSystem): - def log(s: String): Unit = ??? +@capability class FileSystem -def delayed(l: {*} Logger) = - () => - l.log("hi") - 22 +class Logger(using fs: FileSystem): + def log(s: String): Unit = ??? -def test(using fs: {*} FileSystem) = - val l0 = Logger() +def test(using fs: FileSystem) = val l: {fs} Logger = Logger(using fs) l.log("hello world!") val xs: {l} LazyList[Int] = @@ -18,6 +14,7 @@ def test(using fs: {*} FileSystem) = l.log(s"computing elem # $i") i * i } + xs trait LazyList[+A]: this: {*} LazyList[A] => diff --git a/tests/pos-custom-args/captures/null-logger.scala b/tests/pos-custom-args/captures/null-logger.scala new file mode 100644 index 000000000000..0b32d045778c --- /dev/null +++ b/tests/pos-custom-args/captures/null-logger.scala @@ -0,0 +1,9 @@ +import annotation.capability +import annotation.constructorOnly + +@capability class FileSystem + +class NullLogger(using @constructorOnly fs: FileSystem) + +def test2(using fs: FileSystem): NullLogger = NullLogger() + From ebe081b694876e33c47e10fdbe210f47204676c6 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 4 Feb 2022 18:02:11 +0100 Subject: [PATCH 27/99] Check capture set conditions on self types The self type of a class must include: - any references to capabilities outside the class - the capture sets of its retained parameters We now infer the capture set of the self type if an explicit self type is missing. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 40 +++-- compiler/src/dotty/tools/dotc/cc/Setup.scala | 10 ++ .../src/dotty/tools/dotc/core/Types.scala | 11 +- .../tools/dotc/typer/CheckCaptures.scala | 156 ++++++++++++------ .../dotty/tools/dotc/typer/RefChecks.scala | 2 +- docs/_docs/reference/experimental/overview.md | 2 +- tests/neg-custom-args/captures/cc-this.check | 18 ++ tests/neg-custom-args/captures/cc-this.scala | 19 +++ tests/neg-custom-args/captures/cc-this2.check | 8 + .../captures/cc-this2/C_1.scala | 3 + .../captures/cc-this2/D_2.scala | 3 + tests/neg-custom-args/captures/cc-this3.check | 14 ++ tests/neg-custom-args/captures/cc-this3.scala | 18 ++ tests/neg-custom-args/captures/cc-this4.check | 6 + tests/neg-custom-args/captures/cc-this4.scala | 7 + .../captures/inner-classes.scala | 26 +++ .../neg-custom-args/captures/lazylists2.check | 37 ++--- .../neg-custom-args/captures/lazylists2.scala | 10 +- tests/pos-custom-args/captures/cc-this.scala | 17 ++ .../captures/lazylists-exceptions.scala | 4 - .../pos-custom-args/captures/lazylists1.scala | 3 - tests/pos-custom-args/captures/logger.scala | 24 ++- 22 files changed, 331 insertions(+), 107 deletions(-) create mode 100644 tests/neg-custom-args/captures/cc-this.check create mode 100644 tests/neg-custom-args/captures/cc-this.scala create mode 100644 tests/neg-custom-args/captures/cc-this2.check create mode 100644 tests/neg-custom-args/captures/cc-this2/C_1.scala create mode 100644 tests/neg-custom-args/captures/cc-this2/D_2.scala create mode 100644 tests/neg-custom-args/captures/cc-this3.check create mode 100644 tests/neg-custom-args/captures/cc-this3.scala create mode 100644 tests/neg-custom-args/captures/cc-this4.check create mode 100644 tests/neg-custom-args/captures/cc-this4.scala create mode 100644 tests/neg-custom-args/captures/inner-classes.scala create mode 100644 tests/pos-custom-args/captures/cc-this.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index bf1306024e5f..338e153809a0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -215,6 +215,12 @@ sealed abstract class CaptureSet extends Showable: protected def propagateSolved()(using Context): Unit = () + /** This capture set with a description that tells where it comes from */ + def withDescription(description: String): CaptureSet + + /** The provided description (using `withDescription`) for this capture set or else "" */ + def description: String + def toRetainsTypeArg(using Context): Type = assert(isConst) ((NoType: Type) /: elems) ((tp, ref) => @@ -225,7 +231,7 @@ sealed abstract class CaptureSet extends Showable: Annotation(CaptureAnnotation(this, kind).tree) override def toText(printer: Printer): Text = - Str("{") ~ Text(elems.toList.map(printer.toTextCaptureRef), ", ") ~ Str("}") + Str("{") ~ Text(elems.toList.map(printer.toTextCaptureRef), ", ") ~ Str("}") ~~ description object CaptureSet: type Refs = SimpleIdentitySet[CaptureRef] @@ -254,7 +260,7 @@ object CaptureSet: def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) - class Const private[CaptureSet] (val elems: Refs) extends CaptureSet: + class Const private[CaptureSet] (val elems: Refs, val description: String = "") extends CaptureSet: assert(elems != null) def isConst = true def isAlwaysEmpty = elems.isEmpty @@ -266,6 +272,8 @@ object CaptureSet: def upperApprox(origin: CaptureSet)(using Context): CaptureSet = this + def withDescription(description: String): Const = Const(elems, description) + override def toString = elems.toString end Const @@ -282,6 +290,8 @@ object CaptureSet: def isAlwaysEmpty = false var addRootHandler: () => Unit = () => () + var description: String = "" + private def recordElemsState()(using VarState): Boolean = varState.getElems(this) match case None => varState.putElems(this, elems) @@ -332,17 +342,25 @@ object CaptureSet: try computeApprox(origin).ensuring(_.isConst) finally computingApprox = false + def withDescription(description: String): this.type = + this.description = + if this.description.isEmpty then description + else s"${this.description} and $description" + this + protected def computeApprox(origin: CaptureSet)(using Context): CaptureSet = (universal /: deps) { (acc, sup) => acc ** sup.upperApprox(this) } - def solve(variance: Int)(using Context): Unit = - if variance < 0 && !isConst then + /** Widen the variable's elements to its upper approximation and + * mark it as constant from now on. + */ + def solve()(using Context): Unit = + if !isConst then val approx = upperApprox(empty) //println(i"solving var $this $approx ${approx.isConst} deps = ${deps.toList}") - if approx.isConst then - val newElems = approx.elems -- elems - if newElems.isEmpty || addNewElems(newElems, empty)(using ctx, VarState()).isOK then - markSolved() + val newElems = approx.elems -- elems + if newElems.isEmpty || addNewElems(newElems, empty)(using ctx, VarState()).isOK then + markSolved() def markSolved()(using Context): Unit = isSolved = true @@ -539,12 +557,8 @@ object CaptureSet: yield captureSetOf(arg) css.foldLeft(empty)(_ ++ _) */ + def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match - case ref: ThisType => - val declaredCaptures = ref.cls.givenSelfType.captureSet - ref.cls.paramAccessors.foldLeft(declaredCaptures) ((cs, acc) => - cs ++ acc.termRef.captureSetOfInfo) // ^^^ need to also include outer references of inner classes - .showing(i"cc info $ref with ${ref.cls.paramAccessors.map(_.termRef)}%, % = $result", capt) case ref: TermRef if ref.isRootCapability => ref.singletonCaptureSet case _ => ofType(ref.underlying) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 4197d097d8aa..aa6987cc333e 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -351,5 +351,15 @@ extends tpd.TreeTraverser: val sym = tree.symbol sym.updateInfoBetween(preRecheckPhase, thisPhase, transformInferredType(sym.info, boxed = false)) + case tree: TypeDef if tree.symbol.isClass && !tree.symbol.is(ModuleClass) => + // TODO handle modules + val cls = tree.symbol.asClass + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo + if selfInfo eq NoType then + val newInfo = ClassInfo(prefix, cls, ps, decls, + CapturingType(cinfo.selfType, CaptureSet.Var(), CapturingKind.Regular) + .showing(i"inferred self type for $cls: $result", capt)) + cls.updateInfoBetween(preRecheckPhase, thisPhase, newInfo) + cls.thisType.asInstanceOf[ThisType].invalidateCaches() case _ => end Setup diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 864e6ec8a020..b635b5824267 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,7 +36,7 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized -import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, CapturingKind} +import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, CapturingKind, EventuallyCapturingType} import CaptureSet.CompareResult import scala.annotation.internal.sharable @@ -2102,6 +2102,9 @@ object Types { myCaptureSetRunId = ctx.runId computed + def invalidateCaches() = + myCaptureSetRunId = NoRunId + override def captureSet(using Context): CaptureSet = val cs = captureSetOfInfo if canBeTracked && !cs.isAlwaysEmpty then singletonCaptureSet else cs @@ -4915,7 +4918,11 @@ object Types { if (!givenSelf.isValueType) appliedRef else if (clsd.is(Module)) givenSelf else if (ctx.erasedTypes) appliedRef - else AndType(givenSelf, appliedRef) + else givenSelf match + case givenSelf @ EventuallyCapturingType(tp, refs, kind) => + givenSelf.derivedAnnotatedType(tp & appliedRef, givenSelf.annot) + case _ => + AndType(givenSelf, appliedRef) } selfTypeCache.nn } diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 9abc8c56c0f7..9757f351ab46 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -9,7 +9,7 @@ import Types.*, StdNames.* import config.Printers.{capt, recheckr} import ast.{tpd, untpd, Trees} import Trees.* -import typer.RefChecks.checkAllOverrides +import typer.RefChecks.{checkAllOverrides, checkParents} import util.{SimpleIdentitySet, EqHashMap, SrcPos} import transform.SymUtils.* import transform.{Recheck, PreRecheck} @@ -119,12 +119,14 @@ class CheckCaptures extends Recheck, SymTransformer: class CaptureChecker(ictx: Context) extends Rechecker(ictx): import ast.tpd.* - private def interpolator(using Context) = new TypeTraverser: + private def interpolator(startingVariance: Int = 1)(using Context) = new TypeTraverser: + variance = startingVariance override def traverse(t: Type) = t match case CapturingType(parent, refs: CaptureSet.Var, _) => - if variance < 0 then capt.println(i"solving $t") - refs.solve(variance) + if variance < 0 then + capt.println(i"solving $t") + refs.solve() traverse(parent) case t @ RefinedType(_, nme.apply, rinfo) if defn.isFunctionOrPolyType(t) => traverse(rinfo) @@ -136,7 +138,7 @@ class CheckCaptures extends Recheck, SymTransformer: private def interpolateVarsIn(tpt: Tree)(using Context): Unit = if tpt.isInstanceOf[InferredTypeTree] then - interpolator.traverse(tpt.knownType) + interpolator().traverse(tpt.knownType) .showing(i"solved vars in ${tpt.knownType}", capt) private var curEnv: Env = Env(NoSymbol, CaptureSet.empty, false, null) @@ -190,12 +192,15 @@ class CheckCaptures extends Recheck, SymTransformer: def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos)(using Context) = val res = elem.singletonCaptureSet.subCaptures(cs, frozen = false) if !res.isOK then - report.error(i"$elem cannot be referenced here; it is not included in allowed capture set ${res.blocking}", pos) + report.error(i"$elem cannot be referenced here; it is not included in the allowed capture set ${res.blocking}", pos) def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos)(using Context) = val res = cs1.subCaptures(cs2, frozen = false) if !res.isOK then - report.error(i"references $cs1 are not all included in allowed capture set ${res.blocking}", pos) + def header = + if cs1.elems.size == 1 then i"reference ${cs1.elems.toList}%, % is not" + else i"references $cs1 are not all" + report.error(i"$header included in allowed capture set ${res.blocking}", pos) override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = val cs = capturedVars(tree.meth.symbol) @@ -233,8 +238,14 @@ class CheckCaptures extends Recheck, SymTransformer: for parent <- impl.parents do checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos) if !localSet.isAlwaysEmpty then curEnv = Env(cls, localSet, false, curEnv) - try super.recheckClassDef(tree, impl, cls) - finally curEnv = saved + try + val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") + checkSubset(localSet, thisSet, tree.srcPos) + for param <- cls.paramGetters do + checkSubset(param.termRef.captureSet, thisSet, param.srcPos) + super.recheckClassDef(tree, impl, cls) + finally + curEnv = saved /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. * This means: @@ -403,51 +414,98 @@ class CheckCaptures extends Recheck, SymTransformer: .traverse(ctx.compilationUnit.tpdTree) withCaptureSetsExplained { super.checkUnit(unit) - PostCheck.traverse(unit.tpdTree) + checkSelfTypes(unit.tpdTree) + postCheck(unit.tpdTree) if ctx.settings.YccDebug.value then - show(unit.tpdTree) // this dows not print tree, but makes its variables visible for dependency printing + show(unit.tpdTree) // this does not print tree, but makes its variables visible for dependency printing } - object PostCheck extends TreeTraverser: - def traverse(tree: Tree)(using Context) = trace{i"post check $tree"} { - tree match - case _: InferredTypeTree => - case tree: TypeTree if !tree.span.isZeroExtent => - tree.knownType.foreachPart( - checkWellformedPost(_, tree.srcPos)) - tree.knownType.foreachPart { - case AnnotatedType(_, annot) => - checkWellformedPost(annot.tree) + /** Check that self types of subclasses conform to self types of super classes. + * (See comment below how this is achieved). The check assumes that classes + * without an explicit self type have the universal capture set `{*}` on the + * self type. If a class without explicit self type is not `effectivelyFinal` + * it is checked that the inferred self type is universal, in order to assure + * that joint and separate compilation give the same result. + */ + def checkSelfTypes(unit: tpd.Tree)(using Context): Unit = + val parentTrees = mutable.HashMap[Symbol, List[Tree]]() + unit.foreachSubTree { + case cdef @ TypeDef(_, impl: Template) => parentTrees(cdef.symbol) = impl.parents + case _ => + } + // Perform self type checking. The problem here is that `checkParents` compares a + // self type of a subclass with the result of an asSeenFrom of the self type of the + // superclass. That's no good. We need to constrain the original superclass self type + // capture set, not the set mapped by asSeenFrom. + // + // Instead, we proceed from parent classes to child classes. For every class + // we first check its parents, and then interpolate the self type to an + // upper approximation that satisfies all constraints on its capture set. + // That means all capture sets of parent self types are constants, so mapping + // them with asSeenFrom is OK. + while parentTrees.nonEmpty do + val roots = parentTrees.keysIterator.filter { + cls => !parentTrees(cls).exists(ptree => parentTrees.contains(ptree.tpe.classSymbol)) + } + assert(roots.nonEmpty) + for root <- roots do + checkParents(root, parentTrees(root)) + val selfType = root.asClass.classInfo.selfType + interpolator(startingVariance = -1).traverse(selfType) + if !root.isEffectivelySealed then + selfType match + case CapturingType(_, refs: CaptureSet.Var, _) if !refs.isUniversal => + report.error( + i"""$root needs an explicitly declared self type since its + |inferred self type $selfType + |is not visible in other compilation units that define subclasses.""", + root.srcPos) case _ => - } - case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] => - val sym = t.symbol - val isLocal = - sym.ownersIterator.exists(_.isTerm) - || sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass) - - // The following classes of definitions need explicit capture types ... - if !isLocal // ... since external capture types are not inferred - || sym.owner.is(Trait) // ... since we do OverridingPairs checking before capture inference - || sym.allOverriddenSymbols.nonEmpty // ... since we do override checking before capture inference - then - val inferred = t.tpt.knownType - def checkPure(tp: Type) = tp match - case CapturingType(_, refs, _) if !refs.elems.isEmpty => - val resultStr = if t.isInstanceOf[DefDef] then " result" else "" - report.error( - em"""Non-local $sym cannot have an inferred$resultStr type - |$inferred - |with non-empty capture set $refs. - |The type needs to be declared explicitly.""", t.srcPos) - case _ => - inferred.foreachPart(checkPure, StopAt.Static) - case _ => - traverseChildren(tree) + parentTrees -= root + capt.println(i"checked $root with $selfType") + end checkSelfTypes + + /** Perform the following kinds of checks + * - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`. + * - Check that externally visible `val`s or `def`s have empty capture sets. If not + * suggest an explicit type. This is so that separate compilation (where external + * symbols have empty capture sets) gives the same results as jount compilation. + */ + def postCheck(unit: tpd.Tree)(using Context): Unit = + unit.foreachSubTree { + case _: InferredTypeTree => + case tree: TypeTree if !tree.span.isZeroExtent => + tree.knownType.foreachPart( + checkWellformedPost(_, tree.srcPos)) + tree.knownType.foreachPart { + case AnnotatedType(_, annot) => + checkWellformedPost(annot.tree) + case _ => + } + case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] => + val sym = t.symbol + val isLocal = + sym.ownersIterator.exists(_.isTerm) + || sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass) + + // The following classes of definitions need explicit capture types ... + if !isLocal // ... since external capture types are not inferred + || sym.owner.is(Trait) // ... since we do OverridingPairs checking before capture inference + || sym.allOverriddenSymbols.nonEmpty // ... since we do override checking before capture inference + then + val inferred = t.tpt.knownType + def checkPure(tp: Type) = tp match + case CapturingType(_, refs, _) if !refs.elems.isEmpty => + val resultStr = if t.isInstanceOf[DefDef] then " result" else "" + report.error( + em"""Non-local $sym cannot have an inferred$resultStr type + |$inferred + |with non-empty capture set $refs. + |The type needs to be declared explicitly.""", t.srcPos) + case _ => + inferred.foreachPart(checkPure, StopAt.Static) + case _ => } - def postCheck(tree: tpd.Tree)(using Context): Unit = - PostCheck.traverse(tree) - end CaptureChecker end CheckCaptures diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 9f1a40080cb2..539cdb2665e9 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -95,7 +95,7 @@ object RefChecks { * and required classes. Also check that only `enum` constructs extend * `java.lang.Enum` and no user-written class extends ContextFunctionN. */ - private def checkParents(cls: Symbol, parentTrees: List[Tree])(using Context): Unit = cls.info match { + def checkParents(cls: Symbol, parentTrees: List[Tree])(using Context): Unit = cls.info match { case cinfo: ClassInfo => def checkSelfConforms(other: ClassSymbol, category: String, relation: String) = { val otherSelf = other.declaredSelfTypeAsSeenFrom(cls.thisType) diff --git a/docs/_docs/reference/experimental/overview.md b/docs/_docs/reference/experimental/overview.md index 0f2aa543ebbb..62109837290b 100644 --- a/docs/_docs/reference/experimental/overview.md +++ b/docs/_docs/reference/experimental/overview.md @@ -26,4 +26,4 @@ They can be imported at the top-level if all top-level definitions are `@experim Some experimental language features that are still in research and development can be enabled with special compiler options. These include * [`-Yexplicit-nulls`](./explicit-nulls.md). Enable support for tracking null references in the type system. - +* [`-Ycc`](./cc.md). Enable support for capture checking. diff --git a/tests/neg-custom-args/captures/cc-this.check b/tests/neg-custom-args/captures/cc-this.check new file mode 100644 index 000000000000..b8aae19214ea --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this.check @@ -0,0 +1,18 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this.scala:8:15 --------------------------------------- +8 | val y: C = this // error + | ^^^^ + | Found: (C.this : {C.this.x} C) + | Required: C + +longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/cc-this.scala:10:15 ----------------------------------------------------------- +10 | class C2(val x: () => Int): // error + | ^ + | reference (C2.this.x : () => Int) is not included in allowed capture set {} of the self type of class C2 +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this.scala:17:8 --------------------------------------- +17 | class C4(val f: () => Int) extends C3 // error + | ^ + | illegal inheritance: self type {C4.this.f} C4 of class C4 does not conform to self type C3 + | of parent class C3 + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this.scala b/tests/neg-custom-args/captures/cc-this.scala new file mode 100644 index 000000000000..4c05be702c51 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this.scala @@ -0,0 +1,19 @@ +@annotation.capability class Cap + +def eff(using Cap): Unit = () + +def test(using Cap) = + + class C(val x: () => Int): + val y: C = this // error + + class C2(val x: () => Int): // error + this: C2 => + + class C3: + this: C3 => + val x: Object = this + + class C4(val f: () => Int) extends C3 // error + + diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check new file mode 100644 index 000000000000..8ede9caa7a83 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this2.check @@ -0,0 +1,8 @@ + +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this2/D_2.scala:2:6 ----------------------------------- +2 |class D extends C: // error + | ^ + | illegal inheritance: self type {*} D of class D does not conform to self type C + | of parent class C + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this2/C_1.scala b/tests/neg-custom-args/captures/cc-this2/C_1.scala new file mode 100644 index 000000000000..22718f301313 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this2/C_1.scala @@ -0,0 +1,3 @@ +open class C: + this: C => + val x: C = this diff --git a/tests/neg-custom-args/captures/cc-this2/D_2.scala b/tests/neg-custom-args/captures/cc-this2/D_2.scala new file mode 100644 index 000000000000..793f3f6353a9 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this2/D_2.scala @@ -0,0 +1,3 @@ + +class D extends C: // error + this: {*} D => diff --git a/tests/neg-custom-args/captures/cc-this3.check b/tests/neg-custom-args/captures/cc-this3.check new file mode 100644 index 000000000000..a10f25f7da3c --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this3.check @@ -0,0 +1,14 @@ +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this3.scala:8:6 --------------------------------------- +8 |class B extends A: // error + | ^ + | illegal inheritance: self type {*} B of class B does not conform to self type {} A + | of parent class A + +longer explanation available when compiling with `-explain` +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this3.scala:11:6 -------------------------------------- +11 |class C(val f: () => Int) extends A // error + | ^ + | illegal inheritance: self type {C.this.f} C of class C does not conform to self type {} A + | of parent class A + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this3.scala b/tests/neg-custom-args/captures/cc-this3.scala new file mode 100644 index 000000000000..eeb9606f0c81 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this3.scala @@ -0,0 +1,18 @@ +@annotation.capability class Cap + +def eff(using Cap): Unit = () + +class A: + val x: A = this + +class B extends A: // error + this: {*} B => + +class C(val f: () => Int) extends A // error + +class A2 + +class B2 extends A2: // ok + this: {*} B2 => + +class C2(val f: () => Int) extends A2 // ok diff --git a/tests/neg-custom-args/captures/cc-this4.check b/tests/neg-custom-args/captures/cc-this4.check new file mode 100644 index 000000000000..a54ca8d57f4e --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this4.check @@ -0,0 +1,6 @@ +-- Error: tests/neg-custom-args/captures/cc-this4.scala:1:11 ----------------------------------------------------------- +1 |open class C: // error + | ^ + | class C needs an explicitly declared self type since its + | inferred self type {} C + | is not visible in other compilation units that define subclasses. diff --git a/tests/neg-custom-args/captures/cc-this4.scala b/tests/neg-custom-args/captures/cc-this4.scala new file mode 100644 index 000000000000..7580067bfa62 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this4.scala @@ -0,0 +1,7 @@ +open class C: // error + val x: C = this + +open class D: + this: D => + val x: D = this + diff --git a/tests/neg-custom-args/captures/inner-classes.scala b/tests/neg-custom-args/captures/inner-classes.scala new file mode 100644 index 000000000000..cf4073b36f81 --- /dev/null +++ b/tests/neg-custom-args/captures/inner-classes.scala @@ -0,0 +1,26 @@ +object test: + + @annotation.capability class FileSystem + + def foo(fs: FileSystem) = + + trait LazyList[+A]: + this: {fs} LazyList[A] => + + def isEmpty: Boolean + def head: A + def tail: {this} LazyList[A] + + object LazyNil extends LazyList[Nothing]: + def isEmpty: Boolean = true + def head = ??? + def tail = ??? + + final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: // error + def isEmpty = false + def head = x + def tail: {this} LazyList[T] = xs() + end LazyCons + + new LazyCons(1, () => LazyNil) + diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 8cb825f02537..0daefdccd1f8 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -29,29 +29,16 @@ longer explanation available when compiling with `-explain` 32 | new Mapped longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:36:4 ------------------------------------ -36 | final class Mapped extends LazyList[B]: // error +-- Error: tests/neg-custom-args/captures/lazylists2.scala:40:20 -------------------------------------------------------- +40 | def head: B = f(xs.head) // error + | ^ + |(f : A => B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped +-- Error: tests/neg-custom-args/captures/lazylists2.scala:41:49 -------------------------------------------------------- +41 | def tail: {this} LazyList[B] = xs.tail.map(f) // error + | ^ + |(f : A => B) cannot be referenced here; it is not included in the allowed capture set {xs} of the self type of class Mapped +-- Error: tests/neg-custom-args/captures/lazylists2.scala:60:10 -------------------------------------------------------- +60 | class Mapped2 extends Mapped: // error | ^ - | Found: {f, xs} LazyList[B] - | Required: {xs} LazyList[B] -37 | this: ({xs} Mapped) => -38 | def isEmpty = false -39 | def head: B = f(xs.head) -40 | def tail: {this} LazyList[B] = xs.tail.map(f) -41 | new Mapped - -longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:54:4 ------------------------------------ -54 | class Mapped extends LazyList[B]: // error - | ^ - | Found: {f, xs} LazyList[B] - | Required: LazyList[B] -55 | this: ({xs, f} Mapped) => -56 | def isEmpty = false -57 | def head: B = f(xs.head) -58 | def tail: {this} LazyList[B] = xs.tail.map(f) -59 | class Mapped2 extends Mapped: -60 | this: Mapped => -61 | new Mapped2 - -longer explanation available when compiling with `-explain` + | references {f, xs} are not all included in allowed capture set {} of the self type of class Mapped2 +61 | this: Mapped => diff --git a/tests/neg-custom-args/captures/lazylists2.scala b/tests/neg-custom-args/captures/lazylists2.scala index fc1ab768047a..7b661e931441 100644 --- a/tests/neg-custom-args/captures/lazylists2.scala +++ b/tests/neg-custom-args/captures/lazylists2.scala @@ -33,12 +33,12 @@ extension [A](xs: {*} LazyList[A]) new Mapped def map3[B](f: A => B): {xs} LazyList[B] = - final class Mapped extends LazyList[B]: // error + final class Mapped extends LazyList[B]: this: ({xs} Mapped) => def isEmpty = false - def head: B = f(xs.head) - def tail: {this} LazyList[B] = xs.tail.map(f) + def head: B = f(xs.head) // error + def tail: {this} LazyList[B] = xs.tail.map(f) // error new Mapped def map4[B](f: A => B): {xs} LazyList[B] = @@ -51,13 +51,13 @@ extension [A](xs: {*} LazyList[A]) new Mapped def map5[B](f: A => B): LazyList[B] = - class Mapped extends LazyList[B]: // error + class Mapped extends LazyList[B]: this: ({xs, f} Mapped) => def isEmpty = false def head: B = f(xs.head) def tail: {this} LazyList[B] = xs.tail.map(f) - class Mapped2 extends Mapped: + class Mapped2 extends Mapped: // error this: Mapped => new Mapped2 diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala new file mode 100644 index 000000000000..a8e58e7ee4a8 --- /dev/null +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -0,0 +1,17 @@ +@annotation.capability class Cap + +def eff(using Cap): Unit = () + +def test(using Cap) = + + class C(val x: () => Int): + val y: {*} C = this + + def f = () => + eff + 1 + + def c1 = new C(f) + def c2 = c1 + def c3 = c2.y + c3: {*} C diff --git a/tests/pos-custom-args/captures/lazylists-exceptions.scala b/tests/pos-custom-args/captures/lazylists-exceptions.scala index 96b179e564c3..5c4645ed1b6f 100644 --- a/tests/pos-custom-args/captures/lazylists-exceptions.scala +++ b/tests/pos-custom-args/captures/lazylists-exceptions.scala @@ -2,8 +2,6 @@ import language.experimental.saferExceptions import annotation.unchecked.uncheckedVariance trait LazyList[+A]: - this: {*} LazyList[A] => - def isEmpty: Boolean def head: A def tail: {this} LazyList[A] @@ -14,8 +12,6 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: - this: {*} LazyList[T] => - var forced = false var cache: {this} LazyList[T @uncheckedVariance] = compiletime.uninitialized diff --git a/tests/pos-custom-args/captures/lazylists1.scala b/tests/pos-custom-args/captures/lazylists1.scala index df8090fee667..a59e7c0da12f 100644 --- a/tests/pos-custom-args/captures/lazylists1.scala +++ b/tests/pos-custom-args/captures/lazylists1.scala @@ -2,7 +2,6 @@ class CC type Cap = {*} CC trait LazyList[+A]: - this: {*} LazyList[A] => def isEmpty: Boolean def head: A @@ -16,8 +15,6 @@ object LazyNil extends LazyList[Nothing]: def concat[B](other: {*} LazyList[B]): {other} LazyList[B] = other final class LazyCons[+A](x: A)(xs: () => {*} LazyList[A]) extends LazyList[A]: - this: {*} LazyList[A] => - def isEmpty = false def head = x def tail: {this} LazyList[A] = xs() diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala index bb5f3f44f22e..42da5fbe26ef 100644 --- a/tests/pos-custom-args/captures/logger.scala +++ b/tests/pos-custom-args/captures/logger.scala @@ -1,4 +1,5 @@ import annotation.capability +import language.experimental.saferExceptions @capability class FileSystem @@ -17,8 +18,6 @@ def test(using fs: FileSystem) = xs trait LazyList[+A]: - this: {*} LazyList[A] => - def isEmpty: Boolean def head: A def tail: {this} LazyList[A] @@ -29,8 +28,6 @@ object LazyNil extends LazyList[Nothing]: def tail = ??? final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: - this: {*} LazyList[T] => - def isEmpty = false def head = x def tail: {this} LazyList[T] = xs() @@ -49,3 +46,22 @@ object LazyList: def from(start: Int): LazyList[Int] = start #:: from(start + 1) +class Pair[+A, +B](x: A, y: B): + def fst: A = x + def snd: B = y + +def test2(ct: CanThrow[Exception], fs: FileSystem) = + def x: {ct} Int -> String = ??? + def y: {fs} Logger = ??? + def p = Pair(x, y) + def f = () => p.fst + + +/* + val l1: {*} Int -> String = ??? + val l2: {c} Object = ??? + val pd = () => Pair(l1, l2) + val p2: Pair[{*} Int -> String, {c} Object] = pd() + val hd = () => p2.fst + +*/ \ No newline at end of file From 27128c8a755bf75fbb82a82bd06af254c06d40b0 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sat, 5 Feb 2022 19:34:04 +0100 Subject: [PATCH 28/99] Handle local objects --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 13 +++++++++---- .../src/dotty/tools/dotc/core/SymDenotations.scala | 1 + .../src/dotty/tools/dotc/typer/CheckCaptures.scala | 3 ++- compiler/src/dotty/tools/dotc/typer/RefChecks.scala | 5 +++-- tests/neg-custom-args/captures/cc-this5.check | 7 +++++++ tests/neg-custom-args/captures/cc-this5.scala | 12 ++++++++++++ 6 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 tests/neg-custom-args/captures/cc-this5.check create mode 100644 tests/neg-custom-args/captures/cc-this5.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index aa6987cc333e..7b47832104d8 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -351,15 +351,20 @@ extends tpd.TreeTraverser: val sym = tree.symbol sym.updateInfoBetween(preRecheckPhase, thisPhase, transformInferredType(sym.info, boxed = false)) - case tree: TypeDef if tree.symbol.isClass && !tree.symbol.is(ModuleClass) => - // TODO handle modules + case tree: TypeDef if tree.symbol.isClass => val cls = tree.symbol.asClass val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - if selfInfo eq NoType then + if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then + val localRefs = CaptureSet.Var() val newInfo = ClassInfo(prefix, cls, ps, decls, - CapturingType(cinfo.selfType, CaptureSet.Var(), CapturingKind.Regular) + CapturingType(cinfo.selfType, localRefs, CapturingKind.Regular) .showing(i"inferred self type for $cls: $result", capt)) cls.updateInfoBetween(preRecheckPhase, thisPhase, newInfo) cls.thisType.asInstanceOf[ThisType].invalidateCaches() + if cls.is(ModuleClass) then + val modul = cls.sourceModule + modul.updateInfoBetween(preRecheckPhase, thisPhase, + CapturingType(modul.info, localRefs, CapturingKind.Regular)) + modul.termRef.invalidateCaches() case _ => end Setup diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 19609a20c553..f9eb00b24ae4 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1049,6 +1049,7 @@ object SymDenotations { case tp: TermRef => tp.symbol case tp: Symbol => sourceOfSelf(tp.info) case tp: RefinedType => sourceOfSelf(tp.parent) + case tp: AnnotatedType => sourceOfSelf(tp.parent) } sourceOfSelf(selfType) case info: LazyType => diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 9757f351ab46..1dbca8f19054 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -214,7 +214,8 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckIdent(tree) override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = - try super.recheckValDef(tree, sym) + try + if !sym.is(Module) then super.recheckValDef(tree, sym) finally if !sym.is(Param) then // parameters with inferred types belong to anonymous methods. We need to wait diff --git a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala index 539cdb2665e9..94eacca5c7db 100644 --- a/compiler/src/dotty/tools/dotc/typer/RefChecks.scala +++ b/compiler/src/dotty/tools/dotc/typer/RefChecks.scala @@ -106,8 +106,9 @@ object RefChecks { val psyms = cls.asClass.parentSyms for (psym <- psyms) checkSelfConforms(psym.asClass, "illegal inheritance", "parent") - for (reqd <- cinfo.cls.givenSelfType.classSymbols) - checkSelfConforms(reqd, "missing requirement", "required") + for reqd <- cinfo.cls.givenSelfType.classSymbols do + if reqd != cls then + checkSelfConforms(reqd, "missing requirement", "required") def isClassExtendingJavaEnum = !cls.isOneOf(Enum | Trait) && psyms.contains(defn.JavaEnumClass) diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check new file mode 100644 index 000000000000..f35da5e09329 --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -0,0 +1,7 @@ +-- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:7:9 --------------------------------------- +7 | object D extends C: // error + | ^ + | illegal inheritance: self type {c} D.type of object D does not conform to self type {} C + | of parent class C + +longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this5.scala b/tests/neg-custom-args/captures/cc-this5.scala new file mode 100644 index 000000000000..833d2f4fc83e --- /dev/null +++ b/tests/neg-custom-args/captures/cc-this5.scala @@ -0,0 +1,12 @@ +class C: + val x: C = this + +@annotation.capability class Cap + +def foo(c: Cap) = + object D extends C: // error + def bar: Unit = println(c) + object E: + def bar: Unit = println(c) + D.bar + From 97b95194b52537709e0d7953024386a6cd342e4e Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 6 Feb 2022 18:19:30 +0100 Subject: [PATCH 29/99] Print boxes only under -Ydebug-cc --- .../tools/dotc/printing/PlainPrinter.scala | 5 ++-- .../captures/class-constr.scala | 24 +++++++++++++++++++ tests/neg-custom-args/captures/real-try.check | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/neg-custom-args/captures/class-constr.scala diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 680c309c32c1..d11467895437 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -201,11 +201,12 @@ class PlainPrinter(_ctx: Context) extends Printer { (" <: " ~ toText(bound) provided !bound.isAny) }.close case EventuallyCapturingType(parent, refs, kind) => - def box = Str("box ") provided kind == CapturingKind.Boxed + def box = + Str("box ") provided kind == CapturingKind.Boxed && ctx.settings.YccDebug.value if printDebug && !refs.isConst then changePrec(GlobalPrec)(box ~ s"$refs " ~ toText(parent)) else if ctx.settings.YccDebug.value then - changePrec(GlobalPrec)(box ~ refs.toText(this) ~ " " ~ toText(parent)) + changePrec(GlobalPrec)(box ~ refs.show ~ " " ~ toText(parent)) else if !refs.isConst && refs.elems.isEmpty then changePrec(GlobalPrec)("?" ~ " " ~ toText(parent)) else if Config.printCaptureSetsAsPrefix then diff --git a/tests/neg-custom-args/captures/class-constr.scala b/tests/neg-custom-args/captures/class-constr.scala new file mode 100644 index 000000000000..eeedf1043f37 --- /dev/null +++ b/tests/neg-custom-args/captures/class-constr.scala @@ -0,0 +1,24 @@ +import annotation.{capability, constructorOnly} + +@capability class Cap + +class C(x: Cap, @constructorOnly y: Cap) + +def test(a: Cap, b: Cap) = + val f = () => C(a, b) + val f_ok: {a, b} () -> {a} C = f + val f_no1: {a, b} () -> C = f // error + val f_no2: {a} () -> {a} C = f // error + val f_no3: {b} () -> {a} C = f // error + + class D: + val xz = + println(a) + 1 + def yz = + println(b) + 2 + val d = () => new D() + val d_ok1: {a, b} () -> {a, b} D = d + val d_ok2: () -> {a, b} D = d // because of function shorthand + val d_ok3: {a, b} () -> {b} D = d // error, but should work diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index 95531857712e..ae5053c283f7 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -17,4 +17,4 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:30:4 ----------------------------------------------------------- 30 | b.x // error | ^^^ - | the expression's type box {*} () -> Unit is not allowed to capture the root capability `*` + | the expression's type {*} () -> Unit is not allowed to capture the root capability `*` From c6a2e07d89b6cd28de0e419e0a2f71cfcab8e97d Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 7 Feb 2022 16:45:38 +0100 Subject: [PATCH 30/99] Refine setup of initial types for anonymous functions If the expected formals are pure, so should be the function parameters. --- .../dotty/tools/dotc/transform/Recheck.scala | 13 ++++-- .../tools/dotc/typer/CheckCaptures.scala | 40 ++++++++++++++++++- tests/neg-custom-args/captures/cc-this5.check | 4 ++ tests/neg-custom-args/captures/cc-this5.scala | 4 ++ .../captures/withLogFile.scala | 25 ++++++++++++ 5 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 tests/neg-custom-args/captures/withLogFile.scala diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index ec767093373a..a90a0c99de84 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -33,7 +33,7 @@ object Recheck: import tpd.Tree /** Attachment key for rechecked types of TypeTrees */ - private val RecheckedType = Property.Key[Type] + val RecheckedType = Property.Key[Type] extension (sym: Symbol) @@ -65,11 +65,16 @@ object Recheck: extension (tree: Tree) /** Remember `tpe` as the type of `tree`, which might be different from the - * type stored in the tree itself. + * type stored in the tree itself, unless a type was already remembered for `tree`. */ def rememberType(tpe: Type)(using Context): Unit = - if (tpe ne tree.tpe) && !tree.hasAttachment(RecheckedType) then - tree.putAttachment(RecheckedType, tpe) + if !tree.hasAttachment(RecheckedType) then rememberTypeAlways(tpe) + + /** Remember `tpe` as the type of `tree`, which might be different from the + * type stored in the tree itself + */ + def rememberTypeAlways(tpe: Type)(using Context): Unit = + if tpe ne tree.tpe then tree.putAttachment(RecheckedType, tpe) /** The remembered type of the tree, or if none was installed, the original type */ def knownType = diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 1dbca8f19054..7baf4856e4b1 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -204,9 +204,44 @@ class CheckCaptures extends Recheck, SymTransformer: override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = val cs = capturedVars(tree.meth.symbol) - recheckr.println(i"typing closure $tree with cvs $cs") + capt.println(i"typing closure $tree with cvs $cs") super.recheckClosure(tree, pt).capturing(cs) - .showing(i"rechecked $tree, $result", capt) + .showing(i"rechecked $tree / $pt = $result", capt) + + override def recheckBlock(block: Block, pt: Type)(using Context): Type = + block match + case closureDef(mdef) => + pt.dealias match + case defn.FunctionOf(ptformals, _, _, _) if ptformals.forall(_.captureSet.isAlwaysEmpty) => + // Redo setup of the anonymous function so that formal parameters don't + // get capture sets. This is important to avoid false widenings to `*` + // when taking the base type of the actual clsoures's dependent function + // type so that it conforms to the expected non-dependent function type. + // See withLogFile.scala for a test case. + val meth = mdef.symbol + // First, undo the previous setup which installed a completer for `meth`. + atPhase(preRecheckPhase.prev)(meth.denot.copySymDenotation()) + .installAfter(preRecheckPhase) + // Next, update all parameter symbols to match expected formals + meth.paramSymss.head.lazyZip(ptformals).foreach { (psym, pformal) => + psym.copySymDenotation(info = pformal).installAfter(preRecheckPhase) + } + // Next, update types of parameter ValDefs + mdef.paramss.head.lazyZip(ptformals).foreach { (param, pformal) => + val ValDef(_, tpt, _) = param: @unchecked + tpt.rememberTypeAlways(pformal) + } + // Next, install a new completer reflecting the new parameters for the anonymous method + val completer = new LazyType: + def complete(denot: SymDenotation)(using Context) = + denot.info = MethodType(ptformals, mdef.tpt.knownType) + .showing(i"simplify info of $meth to $result", capt) + recheckDef(mdef, meth) + meth.copySymDenotation(info = completer, initFlags = meth.flags &~ Touched) + .installAfter(preRecheckPhase) + case _ => + case _ => + super.recheckBlock(block, pt) override def recheckIdent(tree: Ident)(using Context): Type = markFree(tree.symbol, tree.srcPos) @@ -408,6 +443,7 @@ class CheckCaptures extends Recheck, SymTransformer: .showing(i"healing $actual --> $result", capt) case _ => actual + //println(i"check conforms $actual1 <<< $expected") super.checkConformsExpr(original, actual1, expected, tree) override def checkUnit(unit: CompilationUnit)(using Context): Unit = diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index f35da5e09329..0d5ddb65af3e 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -1,3 +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 {} -- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:7:9 --------------------------------------- 7 | object D extends C: // error | ^ diff --git a/tests/neg-custom-args/captures/cc-this5.scala b/tests/neg-custom-args/captures/cc-this5.scala index 833d2f4fc83e..096cd8414492 100644 --- a/tests/neg-custom-args/captures/cc-this5.scala +++ b/tests/neg-custom-args/captures/cc-this5.scala @@ -10,3 +10,7 @@ def foo(c: Cap) = def bar: Unit = println(c) D.bar +def test(c: Cap) = + class A: + val x: A = this + def f = println(c) // error diff --git a/tests/neg-custom-args/captures/withLogFile.scala b/tests/neg-custom-args/captures/withLogFile.scala new file mode 100644 index 000000000000..afeb79cd1fe6 --- /dev/null +++ b/tests/neg-custom-args/captures/withLogFile.scala @@ -0,0 +1,25 @@ +import java.io.FileOutputStream +import annotation.capability + +def withLogFile1[T](op: FileOutputStream => T): T = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + +def withLogFile2[T](op: ({*} FileOutputStream) => T): T = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + +def test = + val later1 = withLogFile1 { f => // ok + () => f.write(0) + } + later1() + + val later2 = withLogFile2 { f => // error + () => f.write(0) + } + later2() From 7db3a7cc0b54fa6af07d0c0da1cf2bb1415c256a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Wed, 9 Feb 2022 08:33:37 +0100 Subject: [PATCH 31/99] Test from doc page --- .../tools/dotc/typer/CheckCaptures.scala | 5 +- tests/neg-custom-args/captures/cc-this5.check | 7 ++ tests/neg-custom-args/captures/cc-this5.scala | 5 ++ .../neg-custom-args/captures/exceptions.scala | 16 +++++ .../captures/lazylists-exceptions.check | 3 +- tests/neg-custom-args/captures/real-try.check | 9 ++- tests/neg-custom-args/captures/try.check | 9 ++- .../captures/usingLogFile.check | 20 ++++++ .../captures/usingLogFile.scala | 38 ++++++++++ tests/neg-custom-args/captures/vars.check | 9 ++- .../captures/withLogFile.scala | 25 ------- .../captures/curried-shorthands.scala | 23 ++++++ .../captures/lazylists-exceptions.scala | 71 ++++++++++--------- .../captures/strictlists.scala | 41 +++++++++++ 14 files changed, 213 insertions(+), 68 deletions(-) create mode 100644 tests/neg-custom-args/captures/exceptions.scala create mode 100644 tests/neg-custom-args/captures/usingLogFile.check create mode 100644 tests/neg-custom-args/captures/usingLogFile.scala delete mode 100644 tests/neg-custom-args/captures/withLogFile.scala create mode 100644 tests/pos-custom-args/captures/curried-shorthands.scala create mode 100644 tests/pos-custom-args/captures/strictlists.scala diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 7baf4856e4b1..aaf5bcfdbddc 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -404,7 +404,10 @@ class CheckCaptures extends Recheck, SymTransformer: case wtp @ CapturingType(parent, refs, _) => refs.disallowRootCapability { () => val kind = if tree.isInstanceOf[ValDef] then "mutable variable" else "expression" - report.error(em"the $kind's type $wtp is not allowed to capture the root capability `*`", tree.srcPos) + report.error( + em"""The $kind's type $wtp is not allowed to capture the root capability `*`. + |This usually means that a capability persists longer than its allowed lifetime.""", + tree.srcPos) } case _ => super.recheckFinish(tpe, tree, pt) diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index 0d5ddb65af3e..bce02b0e9fe1 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -2,6 +2,13 @@ 16 | def f = println(c) // error | ^ | (c : Cap) cannot be referenced here; it is not included in the allowed capture set {} +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:21:15 ------------------------------------- +21 | val x: A = this // error + | ^^^^ + | Found: (A.this : {c} A) + | Required: A + +longer explanation available when compiling with `-explain` -- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:7:9 --------------------------------------- 7 | object D extends C: // error | ^ diff --git a/tests/neg-custom-args/captures/cc-this5.scala b/tests/neg-custom-args/captures/cc-this5.scala index 096cd8414492..e84c2a41f55c 100644 --- a/tests/neg-custom-args/captures/cc-this5.scala +++ b/tests/neg-custom-args/captures/cc-this5.scala @@ -14,3 +14,8 @@ def test(c: Cap) = class A: val x: A = this def f = println(c) // error + +def test2(c: Cap) = + class A: + def f = println(c) + val x: A = this // error diff --git a/tests/neg-custom-args/captures/exceptions.scala b/tests/neg-custom-args/captures/exceptions.scala new file mode 100644 index 000000000000..bcfef0930434 --- /dev/null +++ b/tests/neg-custom-args/captures/exceptions.scala @@ -0,0 +1,16 @@ +import language.experimental.saferExceptions + +class LimitExceeded extends Exception + +val limit = 10e+10 + +def f(x: Double): Double throws LimitExceeded = + if x < limit then x * x else throw LimitExceeded() + +def escaped(xs: Double*)(using CanThrow[LimitExceeded]): () => Double = + try () => xs.map(f).sum // error + catch case ex: LimitExceeded => () => -1 + +def escaped2(xs: Double*): (() => Double) throws LimitExceeded = + try () => xs.map(f).sum // error + catch case ex: LimitExceeded => () => -1 diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index cf55ff8ebd55..bd6fad047fe9 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,7 +1,8 @@ -- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- 36 | try // error | ^ - | the expression's type {*} LazyList[Int] is not allowed to capture the root capability `*` + | The expression's type {*} LazyList[Int] is not allowed to capture the root capability `*`. + | 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/real-try.check b/tests/neg-custom-args/captures/real-try.check index ae5053c283f7..8becde577e0f 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -1,7 +1,8 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:12:2 ----------------------------------------------------------- 12 | try // error | ^ - | the expression's type {*} () -> Unit is not allowed to capture the root capability `*` + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. 13 | () => foo(1) 14 | catch 15 | case _: Ex1 => ??? @@ -9,7 +10,8 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:18:2 ----------------------------------------------------------- 18 | try // error | ^ - | the expression's type {*} () -> ? Cell[Unit] is not allowed to capture the root capability `*` + | The expression's type {*} () -> ? Cell[Unit] is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. 19 | () => Cell(foo(1)) 20 | catch 21 | case _: Ex1 => ??? @@ -17,4 +19,5 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:30:4 ----------------------------------------------------------- 30 | b.x // error | ^^^ - | the expression's type {*} () -> Unit is not allowed to capture the root capability `*` + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 7dbccc469089..01f2cf88aba3 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -3,7 +3,8 @@ 23 | (x: CanThrow[Exception]) => x 24 | }{ // error | ^ - | the expression's type {*} CT[Exception] is not allowed to capture the root capability `*` + | The expression's type {*} CT[Exception] is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. 25 | (ex: Exception) => ??? 26 | } -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:28:43 ------------------------------------------ @@ -23,7 +24,8 @@ longer explanation available when compiling with `-explain` 38 | 22 39 | } { // error | ^ - | the expression's type {*} () -> Int is not allowed to capture the root capability `*` + | The expression's type {*} () -> Int is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. 40 | (ex: Exception) => () => 22 41 | } -- Error: tests/neg-custom-args/captures/try.scala:51:2 ---------------------------------------------------------------- @@ -34,6 +36,7 @@ longer explanation available when compiling with `-explain` 50 | 22 51 |} { // error | ^ - | the expression's type {*} () -> Int is not allowed to capture the root capability `*` + | The expression's type {*} () -> Int is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. 52 | (ex: Exception) => () => 22 53 |} diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check new file mode 100644 index 000000000000..b292b230b742 --- /dev/null +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -0,0 +1,20 @@ +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:27 ------------------------------------------------------ +23 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:29:9 ------------------------------------------------------- +29 | later2.x() // error + | ^^^^^^^^ + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:31:6 ------------------------------------------------------- +31 | var later3: () => Unit = () => () // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The mutable variable's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:37:9 ------------------------------------------------------- +37 | later4.x() // error + | ^^^^^^^^ + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala new file mode 100644 index 000000000000..206986e0bb22 --- /dev/null +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -0,0 +1,38 @@ +import java.io.FileOutputStream +import annotation.capability + +object Test1: + + def usingLogFile[T](op: FileOutputStream => T): T = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + + val later = usingLogFile { f => () => f.write(0) } + later() + +object Test2: + + def usingLogFile[T](op: ({*} FileOutputStream) => T): T = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + + val later = usingLogFile { f => () => f.write(0) } // error + later() + + class Cell[+T](val x: T) + + val later2 = usingLogFile { f => Cell(() => f.write(0)) } + later2.x() // error + + var later3: () => Unit = () => () // error + usingLogFile { f => later3 = () => f.write(0) } + later3() + + var later4: Cell[() => Unit] = Cell(() => ()) + usingLogFile { f => later4 = Cell(() => f.write(0)) } + later4.x() // error + diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 6a036e49ede2..1ec8a5bb0092 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -8,15 +8,18 @@ longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/vars.scala:13:6 --------------------------------------------------------------- 13 | var a: String => String = f // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | the mutable variable's type {*} String -> String is not allowed to capture the root capability `*` + | The mutable variable's type {*} String -> String is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/vars.scala:15:4 --------------------------------------------------------------- 15 | b.head // error | ^^^^^^ - | the expression's type {*} String -> String is not allowed to capture the root capability `*` + | The expression's type {*} String -> String is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/vars.scala:30:8 --------------------------------------------------------------- 30 | local { cap3 => // error | ^ - | the expression's type {*} (x$0: ? String) -> ? String is not allowed to capture the root capability `*` + | The expression's type {*} (x$0: ? String) -> ? String is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. 31 | def g(x: String): String = if cap3 == cap3 then "" else "a" 32 | g 33 | } diff --git a/tests/neg-custom-args/captures/withLogFile.scala b/tests/neg-custom-args/captures/withLogFile.scala deleted file mode 100644 index afeb79cd1fe6..000000000000 --- a/tests/neg-custom-args/captures/withLogFile.scala +++ /dev/null @@ -1,25 +0,0 @@ -import java.io.FileOutputStream -import annotation.capability - -def withLogFile1[T](op: FileOutputStream => T): T = - val logFile = FileOutputStream("log") - val result = op(logFile) - logFile.close() - result - -def withLogFile2[T](op: ({*} FileOutputStream) => T): T = - val logFile = FileOutputStream("log") - val result = op(logFile) - logFile.close() - result - -def test = - val later1 = withLogFile1 { f => // ok - () => f.write(0) - } - later1() - - val later2 = withLogFile2 { f => // error - () => f.write(0) - } - later2() diff --git a/tests/pos-custom-args/captures/curried-shorthands.scala b/tests/pos-custom-args/captures/curried-shorthands.scala new file mode 100644 index 000000000000..44559b13ae95 --- /dev/null +++ b/tests/pos-custom-args/captures/curried-shorthands.scala @@ -0,0 +1,23 @@ +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 + +def map3(f: Int => Int)(xs: List[Int]): List[Int] = xs.map(f) +val f2 = map3 +val fc2: (Int => Int) -> List[Int] -> List[Int] = f2 + +val f3 = (f: Int => Int) => + println(f(3)) + (xs: List[Int]) => xs.map(_ + 1) +val f3c: (Int => Int) -> {} List[Int] -> List[Int] = f3 + +class LL[A]: + def drop(n: Int): {this} LL[A] = ??? + +def test(ct: CanThrow[Exception]) = + def xs: {ct} LL[Int] = ??? + val ys = xs.drop(_) + val ysc: Int -> {ct} LL[Int] = ys + + + diff --git a/tests/pos-custom-args/captures/lazylists-exceptions.scala b/tests/pos-custom-args/captures/lazylists-exceptions.scala index 5c4645ed1b6f..a02c98b71bb8 100644 --- a/tests/pos-custom-args/captures/lazylists-exceptions.scala +++ b/tests/pos-custom-args/captures/lazylists-exceptions.scala @@ -1,56 +1,56 @@ import language.experimental.saferExceptions -import annotation.unchecked.uncheckedVariance +import scala.compiletime.uninitialized -trait LazyList[+A]: +trait LzyList[+A]: def isEmpty: Boolean def head: A - def tail: {this} LazyList[A] + def tail: {this} LzyList[A] -object LazyNil extends LazyList[Nothing]: - def isEmpty: Boolean = true +object LzyNil extends LzyList[Nothing]: + def isEmpty = true def head = ??? def tail = ??? -final class LazyCons[+T](val x: T, val xs: () => {*} LazyList[T]) extends LazyList[T]: - var forced = false - var cache: {this} LazyList[T @uncheckedVariance] = compiletime.uninitialized - +final class LzyCons[+A](hd: A, tl: () => {*} LzyList[A]) extends LzyList[A]: + private var forced = false + private var cache: {this} LzyList[A] = uninitialized private def force = - if !forced then - cache = xs() - forced = true + if !forced then { cache = tl(); forced = true } cache def isEmpty = false - def head = x - def tail: {this} LazyList[T] = force -end LazyCons + def head = hd + def tail: {this} LzyList[A] = force +end LzyCons -extension [A](xs: {*} LazyList[A]) - def map[B](f: A => B): {xs, f} LazyList[B] = - if xs.isEmpty then LazyNil - else LazyCons(f(xs.head), () => xs.tail.map(f)) +extension [A](xs: {*} LzyList[A]) + def map[B](f: A => B): {xs, f} LzyList[B] = + if xs.isEmpty then LzyNil + else LzyCons(f(xs.head), () => xs.tail.map(f)) - def filter(p: A => Boolean): {xs, p} LazyList[A] = - if xs.isEmpty then LazyNil + def filter(p: A => Boolean): {xs, p} LzyList[A] = + if xs.isEmpty then LzyNil else if p(xs.head) then lazyCons(xs.head, xs.tail.filter(p)) else xs.tail.filter(p) - def concat(ys: {*} LazyList[A]): {xs, ys} LazyList[A] = + def concat(ys: {*} LzyList[A]): {xs, ys} LzyList[A] = if xs.isEmpty then ys else xs.head #: xs.tail.concat(ys) + + def drop(n: Int): {xs} LzyList[A] = + if n == 0 then xs else xs.tail.drop(n - 1) end extension extension [A](x: A) - def #:(xs1: => {*} LazyList[A]): {xs1} LazyList[A] = - LazyCons(x, () => xs1) + def #:(xs1: => {*} LzyList[A]): {xs1} LzyList[A] = + LzyCons(x, () => xs1) -def lazyCons[A](x: A, xs1: => {*} LazyList[A]): {xs1} LazyList[A] = - LazyCons(x, () => xs1) +def lazyCons[A](x: A, xs1: => {*} LzyList[A]): {xs1} LzyList[A] = + LzyCons(x, () => xs1) def tabulate[A](n: Int)(gen: Int => A) = - def recur(i: Int): {gen} LazyList[A] = - if i == n then LazyNil + def recur(i: Int): {gen} LzyList[A] = + if i == n then LzyNil else gen(i) #: recur(i + 1) recur(0) @@ -58,7 +58,7 @@ class Ex1 extends Exception class Ex2 extends Exception def test(using cap1: CanThrow[Ex1], cap2: CanThrow[Ex2]) = - val xs = 1 #: LazyNil + val xs = 1 #: LzyNil def f(x: Int): Int throws Ex1 = if x < 0 then throw Ex1() @@ -69,14 +69,21 @@ def test(using cap1: CanThrow[Ex1], cap2: CanThrow[Ex2]) = x * x def x1 = xs.map(f) - def x1c: {cap1} LazyList[Int] = x1 + def x1c: {cap1} LzyList[Int] = x1 def x2 = x1.concat(xs.map(g).filter(_ > 0)) - def x2c: {cap1, cap2} LazyList[Int] = x2 + def x2c: {cap1, cap2} LzyList[Int] = x2 val x3 = tabulate(10) { i => if i > 9 then throw Ex1() i * i } - val x3c: {cap1} LazyList[Int] = x3 + val x3c: {cap1} LzyList[Int] = x3 + +class LimitExceeded extends Exception +def test2(n: Int)(using ct: CanThrow[LimitExceeded]) = + val xs = tabulate(10) { i => + if i > 9 then throw LimitExceeded() + i * i + } diff --git a/tests/pos-custom-args/captures/strictlists.scala b/tests/pos-custom-args/captures/strictlists.scala new file mode 100644 index 000000000000..cb58a09a161f --- /dev/null +++ b/tests/pos-custom-args/captures/strictlists.scala @@ -0,0 +1,41 @@ + +trait StrictList[+A]: + def isEmpty: Boolean + def head: A + def tail: StrictList[A] + +object StrictNil extends StrictList[Nothing]: + def isEmpty = true + def head = ??? + def tail = ??? + +final class StrictCons[+A](hd: A, tl: StrictList[A]) extends StrictList[A]: + def isEmpty = false + def head = hd + def tail: StrictList[A] = tl +end StrictCons + +extension [A](xs: StrictList[A]) + def map[B](f: A => B): StrictList[B] = + if xs.isEmpty then StrictNil + else StrictCons(f(xs.head),xs.tail.map(f)) + + def filter(p: A => Boolean): StrictList[A] = + if xs.isEmpty then StrictNil + else if p(xs.head) then xs.head #: xs.tail.filter(p) + else xs.tail.filter(p) + + def concat(ys: StrictList[A]): StrictList[A] = + if xs.isEmpty then ys + else xs.head #: xs.tail.concat(ys) +end extension + +extension [A](x: A) + def #:(xs1: StrictList[A]): StrictList[A] = + StrictCons(x, xs1) + +def tabulate[A](n: Int)(gen: Int => A) = + def recur(i: Int): StrictList[A] = + if i == n then StrictNil + else gen(i) #: recur(i + 1) + recur(0) \ No newline at end of file From 77cf546ccdd4d97de20c81e0a32a7a50c6bb786f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Fri, 25 Feb 2022 16:52:09 +0100 Subject: [PATCH 32/99] Update checkfiles after rebase --- tests/neg-custom-args/captures/boxmap.check | 4 ++-- tests/neg-custom-args/captures/byname.check | 8 +++---- tests/neg-custom-args/captures/capt1.check | 24 +++++++++---------- tests/neg-custom-args/captures/cc-this.check | 8 +++---- tests/neg-custom-args/captures/cc-this2.check | 4 ++-- tests/neg-custom-args/captures/cc-this3.check | 8 +++---- tests/neg-custom-args/captures/cc-this5.check | 8 +++---- .../captures/class-contra.check | 4 ++-- .../captures/curried-simplified.check | 24 +++++++++---------- tests/neg-custom-args/captures/lazylist.check | 20 ++++++++-------- .../neg-custom-args/captures/lazylists1.check | 4 ++-- .../neg-custom-args/captures/lazylists2.check | 12 +++++----- tests/neg-custom-args/captures/lazyref.check | 16 ++++++------- .../captures/nestedclass.check | 4 ++-- tests/neg-custom-args/captures/try.check | 4 ++-- tests/neg-custom-args/captures/vars.check | 4 ++-- tests/neg/polymorphic-functions1.check | 4 ++-- tests/neg/t5702-neg-bad-and-wild.check | 8 +++---- 18 files changed, 84 insertions(+), 84 deletions(-) diff --git a/tests/neg-custom-args/captures/boxmap.check b/tests/neg-custom-args/captures/boxmap.check index b3d6605989bf..c42699d2c997 100644 --- a/tests/neg-custom-args/captures/boxmap.check +++ b/tests/neg-custom-args/captures/boxmap.check @@ -3,5 +3,5 @@ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: {f} () -> ? Box[B] | Required: () -> Box[B] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/byname.check b/tests/neg-custom-args/captures/byname.check index 3321da3c17db..486f94d599ac 100644 --- a/tests/neg-custom-args/captures/byname.check +++ b/tests/neg-custom-args/captures/byname.check @@ -9,12 +9,12 @@ | ^^^^ | Found: {cap1} (x$0: Int) -> Int | Required: {cap2} Int -> Int - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/byname.scala:19:5 ---------------------------------------- 19 | h(g()) // error | ^^^ | Found: {cap2} () ?-> I | Required: {cap1} () ?-> I - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 0b99f1bac09e..439c16c143be 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -3,23 +3,23 @@ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: {x} () -> ? C | Required: () -> C - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:6:2 ------------------------------------------ 6 | () => if x == null then y else y // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: {x} () -> ? C | Required: Matchable - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:13:2 ----------------------------------------- 13 | def f(y: Int) = if x == null then y else y // error | ^ | Found: {x} Int -> Int | Required: Matchable 14 | f - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:20:2 ----------------------------------------- 20 | class F(y: Int) extends A: // error | ^ @@ -27,20 +27,20 @@ longer explanation available when compiling with `-explain` | Required: A 21 | def m() = if x == null then y else y 22 | F(22) - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:25:2 ----------------------------------------- 25 | new A: // error | ^ | Found: {x} A | Required: A 26 | def m() = if x == null then y else y - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:31:24 ---------------------------------------- 31 | val z2 = h[() -> Cap](() => x)(() => C()) // error | ^^^^^^^ | Found: {x} () -> Cap | Required: () -> Cap - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this.check b/tests/neg-custom-args/captures/cc-this.check index b8aae19214ea..c492df15078f 100644 --- a/tests/neg-custom-args/captures/cc-this.check +++ b/tests/neg-custom-args/captures/cc-this.check @@ -3,8 +3,8 @@ | ^^^^ | Found: (C.this : {C.this.x} C) | Required: C - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/cc-this.scala:10:15 ----------------------------------------------------------- 10 | class C2(val x: () => Int): // error | ^ @@ -14,5 +14,5 @@ longer explanation available when compiling with `-explain` | ^ | illegal inheritance: self type {C4.this.f} C4 of class C4 does not conform to self type C3 | of parent class C3 - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this2.check b/tests/neg-custom-args/captures/cc-this2.check index 8ede9caa7a83..d10519636ca8 100644 --- a/tests/neg-custom-args/captures/cc-this2.check +++ b/tests/neg-custom-args/captures/cc-this2.check @@ -4,5 +4,5 @@ | ^ | illegal inheritance: self type {*} D of class D does not conform to self type C | of parent class C - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this3.check b/tests/neg-custom-args/captures/cc-this3.check index a10f25f7da3c..705cdfbc00d7 100644 --- a/tests/neg-custom-args/captures/cc-this3.check +++ b/tests/neg-custom-args/captures/cc-this3.check @@ -3,12 +3,12 @@ | ^ | illegal inheritance: self type {*} B of class B does not conform to self type {} A | of parent class A - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this3.scala:11:6 -------------------------------------- 11 |class C(val f: () => Int) extends A // error | ^ | illegal inheritance: self type {C.this.f} C of class C does not conform to self type {} A | of parent class A - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/cc-this5.check b/tests/neg-custom-args/captures/cc-this5.check index bce02b0e9fe1..8cc1ac9ccc5d 100644 --- a/tests/neg-custom-args/captures/cc-this5.check +++ b/tests/neg-custom-args/captures/cc-this5.check @@ -7,12 +7,12 @@ | ^^^^ | Found: (A.this : {c} A) | Required: A - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E058] Type Mismatch Error: tests/neg-custom-args/captures/cc-this5.scala:7:9 --------------------------------------- 7 | object D extends C: // error | ^ | illegal inheritance: self type {c} D.type of object D does not conform to self type {} C | of parent class C - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/class-contra.check b/tests/neg-custom-args/captures/class-contra.check index 3825d57b602e..69a5f0097de8 100644 --- a/tests/neg-custom-args/captures/class-contra.check +++ b/tests/neg-custom-args/captures/class-contra.check @@ -3,5 +3,5 @@ | ^ | Found: (a : {x, y} T) | Required: T - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/curried-simplified.check b/tests/neg-custom-args/captures/curried-simplified.check index 055558530a76..937282bc2148 100644 --- a/tests/neg-custom-args/captures/curried-simplified.check +++ b/tests/neg-custom-args/captures/curried-simplified.check @@ -3,40 +3,40 @@ | ^^ | Found: {x} () -> {x} () -> Int | Required: () -> () -> Int - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:9:28 ---------------------------- 9 | def y2: () -> () => Int = x2 // error | ^^ | Found: {x} () -> () => Int | Required: () -> () => Int - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:11:39 --------------------------- 11 | def y3: Cap -> Protect[Int -> Int] = x3 // error | ^^ | Found: (x$0: Cap) -> {x$0} Int -> Int | Required: Cap -> Protect[Int -> Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:15:33 --------------------------- 15 | def y5: Cap -> {} Int -> Int = x5 // error | ^^ | Found: Cap -> {x} Int -> Int | Required: Cap -> {} Int -> Int - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:17:49 --------------------------- 17 | def y6: Cap -> {} Cap -> Protect[Int -> Int] = x6 // error | ^^ | Found: (x$0: Cap) -> {x$0} (x$0: Cap) -> {x$0, x$0} Int -> Int | Required: Cap -> {} Cap -> Protect[Int -> Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:19:49 --------------------------- 19 | def y7: Cap -> Protect[Cap -> {} Int -> Int] = x7 // error | ^^ | Found: (x$0: Cap) -> {x$0} (x: Cap) -> {x$0, x} Int -> Int | Required: Cap -> Protect[Cap -> {} Int -> Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index bdbef10de0d6..82a6de52d26e 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -3,36 +3,36 @@ | ^ | error overriding method tail in class LazyList of type -> lazylists.LazyList[Nothing]; | method tail of type -> {*} lazylists.LazyList[Nothing] has incompatible type - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:35:29 ------------------------------------- 35 | val ref1c: LazyList[Int] = ref1 // error | ^^^^ | Found: (ref1 : {cap1} lazylists.LazyCons[Int]{xs: {cap1} () -> {*} lazylists.LazyList[Int]}) | Required: lazylists.LazyList[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:37:36 ------------------------------------- 37 | val ref2c: {ref1} LazyList[Int] = ref2 // error | ^^^^ | Found: (ref2 : {cap2, ref1} lazylists.LazyList[Int]) | Required: {ref1} lazylists.LazyList[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:39:36 ------------------------------------- 39 | val ref3c: {cap2} LazyList[Int] = ref3 // error | ^^^^ | Found: (ref3 : {cap2, ref1} lazylists.LazyList[Int]) | Required: {cap2} lazylists.LazyList[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylist.scala:41:48 ------------------------------------- 41 | val ref4c: {cap1, ref3, cap3} LazyList[Int] = ref4 // error | ^^^^ | Found: (ref4 : {cap3, cap2, ref1, cap1} lazylists.LazyList[Int]) | Required: {cap1, ref3, cap3} lazylists.LazyList[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/lazylist.scala:17:6 ----------------------------------------------------------- 17 | def tail = xs() // error: cannot have an inferred type | ^^^^^^^^^^^^^^^ diff --git a/tests/neg-custom-args/captures/lazylists1.check b/tests/neg-custom-args/captures/lazylists1.check index 1d23de2ff134..f91e2500dc15 100644 --- a/tests/neg-custom-args/captures/lazylists1.check +++ b/tests/neg-custom-args/captures/lazylists1.check @@ -3,5 +3,5 @@ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: {xs, f} LazyList[A] | Required: {Mapped.this, f} LazyList[A] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 0daefdccd1f8..8fe02dc5cb9b 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -3,8 +3,8 @@ | ^ | error overriding method tail in trait LazyList of type -> {Mapped.this} LazyList[B]; | method tail of type -> {xs, f} LazyList[B] has incompatible type - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ 18 | final class Mapped extends LazyList[B]: // error | ^ @@ -15,8 +15,8 @@ longer explanation available when compiling with `-explain` 21 | def head: B = f(xs.head) 22 | def tail: {this} LazyList[B] = xs.tail.map(f) 23 | new Mapped - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------ 27 | final class Mapped extends LazyList[B]: // error | ^ @@ -27,8 +27,8 @@ longer explanation available when compiling with `-explain` 30 | def head: B = f(xs.head) 31 | def tail: {this} LazyList[B] = xs.tail.map(f) 32 | new Mapped - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/lazylists2.scala:40:20 -------------------------------------------------------- 40 | def head: B = f(xs.head) // error | ^ diff --git a/tests/neg-custom-args/captures/lazyref.check b/tests/neg-custom-args/captures/lazyref.check index e4e06f8c52cb..fcd98d0d67bd 100644 --- a/tests/neg-custom-args/captures/lazyref.check +++ b/tests/neg-custom-args/captures/lazyref.check @@ -3,26 +3,26 @@ | ^^^^ | Found: (ref1 : {cap1} LazyRef[Int]{elem: {cap1} () -> Int}) | Required: LazyRef[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:21:35 -------------------------------------- 21 | val ref2c: {cap2} LazyRef[Int] = ref2 // error | ^^^^ | Found: (ref2 : {cap2, ref1} LazyRef[Int]{elem: {*} () -> Int}) | Required: {cap2} LazyRef[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:23:35 -------------------------------------- 23 | val ref3c: {ref1} LazyRef[Int] = ref3 // error | ^^^^ | Found: (ref3 : {cap2, ref1} LazyRef[Int]{elem: {*} () -> Int}) | Required: {ref1} LazyRef[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyref.scala:25:35 -------------------------------------- 25 | val ref4c: {cap1} LazyRef[Int] = ref4 // error | ^^^^ | Found: (ref4 : {cap2, cap1} LazyRef[Int]{elem: {*} () -> Int}) | Required: {cap1} LazyRef[Int] - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/nestedclass.check b/tests/neg-custom-args/captures/nestedclass.check index d3912d417a4c..cb4421ece0ec 100644 --- a/tests/neg-custom-args/captures/nestedclass.check +++ b/tests/neg-custom-args/captures/nestedclass.check @@ -3,5 +3,5 @@ | ^^ | Found: (xs : {cap1} C) | Required: C - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 01f2cf88aba3..87719da2424f 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -14,8 +14,8 @@ | Required: CanThrow[Exception] => () -> Nothing 29 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) 30 | } { - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/try.scala:39:4 ---------------------------------------------------------------- 34 | val xx = handle { 35 | (x: CanThrow[Exception]) => diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 1ec8a5bb0092..1c41fc5b7460 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -3,8 +3,8 @@ | ^^ | Found: (z2 : {x, cap1} () -> Unit) | Required: () -> Unit - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/vars.scala:13:6 --------------------------------------------------------------- 13 | var a: String => String = f // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/neg/polymorphic-functions1.check b/tests/neg/polymorphic-functions1.check index 86492e96dab5..7374075de072 100644 --- a/tests/neg/polymorphic-functions1.check +++ b/tests/neg/polymorphic-functions1.check @@ -3,5 +3,5 @@ | ^ | Found: [T] => (x: Int) => Int | Required: [T] => (x: T) => x.type - -longer explanation available when compiling with `-explain` + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/t5702-neg-bad-and-wild.check b/tests/neg/t5702-neg-bad-and-wild.check index f6d761a6726f..1a9e4ba4a1f6 100644 --- a/tests/neg/t5702-neg-bad-and-wild.check +++ b/tests/neg/t5702-neg-bad-and-wild.check @@ -38,16 +38,16 @@ | x is already defined as value x | | Note that overloaded methods must all be defined in the same group of toplevel definitions --- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 ------------------------------------------------ +-- [E127] Syntax Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 --------------------------------------------------- 12 | case List(1, _*3,) => // error: pattern expected // error | ^ - | Not found: * + | * cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method | | longer explanation available when compiling with `-explain` --- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 ------------------------------------------------ +-- [E127] Syntax Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 --------------------------------------------------- 13 | case List(1, _*3:) => // error // error | ^ - | Not found: * + | * cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method | | longer explanation available when compiling with `-explain` -- [E045] Cyclic Error: tests/neg/t5702-neg-bad-and-wild.scala:23:19 --------------------------------------------------- From 161e8cd7e9ee552f53eec8468d33d2d128e19efc Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 2 Mar 2022 18:57:52 +0100 Subject: [PATCH 33/99] Refinements to capture checking and a collection strawman Fixes and refinements needed to compile a full collection strawman and tests with capture checking turned on. 1. Exclude ResetPrivate symbols from overriding checks A symbol with the ResetPrivate flag should not take part in overriding checks. We have reset the Private just to enable refinements, but for the purpose of overriding checks it still should be treated as a private member. 2. Refine conforms check The previous check did not work if the whole right hand side was a tracked variable. We now have a more robust check that works by augmenting the expected capture set rather than filtering the actual one. 3. Keep self type around after FirstTransform Under -Ycc we need to keep the self type declaration until CheckCaptures, so that it can be checked for well-formedness. We can't keep the original self (which is a ValDef) since other phases assume it is empty after FirstTransform. Instead we copy the type `S` into a synthetic definition private[this] type $this = S 4. Exclude some synthetic case class methods from checking, since their implementations do not allow for capture sets. We need to get back to them eventually, and annotate the methods in the correct way. 5. Handle function type aliases in preTypeArgs 6. Fixes for handling capture set ranges - We now accept `C[? >: C1 T1 <: C2 T2]` even if `C` is higher-kinded. Previously we widened the range to Nothing...Any. - Special case for handling the subtype test `C[? >: C1 T1 <: C2 T2] <: C3 T3` where `T1 =:= T2`: Unify capture sets C1, C2 and C3 in this case. 7. Refine inferred variable scheme - More refined model when a type gets an inferred variable. - Add inferred variables to pattern matching lets 8. A version of collection strawman that uses abstract types A version of the collection strawman that uses abstract types instead of type parameters for `Repr` and `C[_]`. This is necessary since we want to use `this` in the capture set of some of these types. --- compiler/src/dotty/tools/dotc/ast/tpd.scala | 8 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 8 + compiler/src/dotty/tools/dotc/cc/Setup.scala | 37 +- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../tools/dotc/transform/FirstTransform.scala | 16 +- .../dotc/transform/OverridingPairs.scala | 4 +- .../tools/dotc/transform/PatternMatcher.scala | 2 +- .../dotty/tools/dotc/typer/Applications.scala | 2 +- .../tools/dotc/typer/CheckCaptures.scala | 72 ++- .../src/dotty/tools/dotc/typer/Checking.scala | 2 +- .../src/dotty/tools/dotc/typer/Typer.scala | 2 +- .../dotty/tools/dotc/CompilationTests.scala | 1 + tests/neg-custom-args/captures/boxmap.check | 2 +- .../captures/emptyref-in-self.scala | 3 + tests/neg-custom-args/captures/lazylist.check | 2 +- .../captures/lazylists-exceptions.check | 2 +- .../neg-custom-args/captures/lazylists2.check | 4 +- tests/pending/pos/i14575.scala | 15 + tests/pos-custom-args/captures/hk-param.scala | 17 + .../captures/nonvariant-inf.scala | 9 + .../captures}/colltest5.check | 0 .../colltest5/CollectionStrawManCC5_1.scala | 601 ++++++++++++++++++ .../captures/colltest5/Test_2.scala | 185 ++++++ 24 files changed, 945 insertions(+), 53 deletions(-) create mode 100644 tests/neg-custom-args/captures/emptyref-in-self.scala create mode 100644 tests/pending/pos/i14575.scala create mode 100644 tests/pos-custom-args/captures/hk-param.scala create mode 100644 tests/pos-custom-args/captures/nonvariant-inf.scala rename tests/{run => run-custom-args/captures}/colltest5.check (100%) create mode 100644 tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala create mode 100644 tests/run-custom-args/captures/colltest5/Test_2.scala diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 53ad330eea35..e17979217610 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -164,8 +164,8 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def Inlined(call: Tree, bindings: List[MemberDef], expansion: Tree)(using Context): Inlined = ta.assignType(untpd.Inlined(call, bindings, expansion), bindings, expansion) - def TypeTree(tp: Type)(using Context): TypeTree = - untpd.TypeTree().withType(tp) + def TypeTree(tp: Type, inferred: Boolean = false)(using Context): TypeTree = + (if inferred then new InferredTypeTree() else untpd.TypeTree()).withType(tp) def SingletonTypeTree(ref: Tree)(using Context): SingletonTypeTree = ta.assignType(untpd.SingletonTypeTree(ref), ref) @@ -203,8 +203,8 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { ta.assignType(untpd.UnApply(fun, implicits, patterns), proto) } - def ValDef(sym: TermSymbol, rhs: LazyTree = EmptyTree)(using Context): ValDef = - ta.assignType(untpd.ValDef(sym.name, TypeTree(sym.info), rhs), sym) + def ValDef(sym: TermSymbol, rhs: LazyTree = EmptyTree, inferred: Boolean = false)(using Context): ValDef = + ta.assignType(untpd.ValDef(sym.name, TypeTree(sym.info, inferred), rhs), sym) def SyntheticValDef(name: TermName, rhs: Tree, flags: FlagSet = EmptyFlags)(using Context): ValDef = ValDef(newSymbol(ctx.owner, name, Synthetic | flags, rhs.tpe.widen, coord = rhs.span), rhs) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 338e153809a0..33b8071d5bbb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -492,6 +492,14 @@ object CaptureSet: def mapRefs(xs: Refs, tm: TypeMap, variance: Int)(using Context): CaptureSet = mapRefs(xs, extrapolateCaptureRef(_, tm, variance)) + def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match + case TypeBounds(CapturingType(lo, loRefs, _), CapturingType(hi, hiRefs, _)) if lo =:= hi => + given VarState = VarState() + val cs2 = arg2.captureSet + hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK + case _ => + false + type CompareResult = CompareResult.Type /** None = ok, Some(cs) = failure since not a subset of cs */ diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 7b47832104d8..15f530bdfc69 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -109,21 +109,45 @@ extends tpd.TreeTraverser: case _ => tp case _ => tp + private def superTypeIsImpure(tp: Type): Boolean = { + tp.dealias match + case CapturingType(_, refs, _) => + !refs.isAlwaysEmpty + case tp: (TypeRef | AppliedType) => + val sym = tp.typeSymbol + if sym.isClass then tp.typeSymbol == defn.AnyClass + else superTypeIsImpure(tp.superType) + case tp: (RefinedOrRecType | MatchType) => + superTypeIsImpure(tp.underlying) + case tp: AndType => + superTypeIsImpure(tp.tp1) || canHaveInferredCapture(tp.tp2) + case tp: OrType => + superTypeIsImpure(tp.tp1) && superTypeIsImpure(tp.tp2) + case _ => + false + }.showing(i"super type is impure $tp = $result", capt) + /** Should a capture set variable be added on type `tp`? */ - def canHaveInferredCapture(tp: Type): Boolean = + def canHaveInferredCapture(tp: Type): Boolean = { tp.typeParams.isEmpty && tp.match case tp: (TypeRef | AppliedType) => - val sym = tp.typeSymbol - if sym.isClass then !sym.isValueClass && sym != defn.AnyClass - else canHaveInferredCapture(tp.superType.dealias) + val tp1 = tp.dealias + if tp1 ne tp then canHaveInferredCapture(tp1) + else + val sym = tp1.typeSymbol + if sym.isClass then !sym.isValueClass && sym != defn.AnyClass + else superTypeIsImpure(tp1) case tp: (RefinedOrRecType | MatchType) => canHaveInferredCapture(tp.underlying) case tp: AndType => canHaveInferredCapture(tp.tp1) && canHaveInferredCapture(tp.tp2) case tp: OrType => canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2) + case CapturingType(_, refs, _) => + refs.isConst && !refs.isAlwaysEmpty && !refs.isUniversal case _ => false + }.showing(i"can have inferred capture $tp = $result", capt) /** Add a capture set variable to `tp` if necessary, or maybe pull out * an embedded capture set variables from a part of `tp`. @@ -154,7 +178,10 @@ extends tpd.TreeTraverser: case tp @ OrType(tp1, CapturingType(parent2, refs2, boxed2)) => CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, boxed2) case _ if canHaveInferredCapture(tp) => - CapturingType(tp, CaptureSet.Var(), CapturingKind.Regular) + val cs = tp.dealias match + case CapturingType(_, refs,_) => CaptureSet.Var(refs.elems) + case _ => CaptureSet.Var() + CapturingType(tp, cs, CapturingKind.Regular) case _ => tp diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 65a3d0b106d0..4ff4a625ffa3 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1616,7 +1616,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => arg1 match case arg1: TypeBounds => - compareCaptured(arg1, arg2) + CaptureSet.subCapturesRange(arg1, arg2) || compareCaptured(arg1, arg2) case ExprType(arg1res) if ctx.phaseId > elimByNamePhase.id && !ctx.erasedTypes && defn.isByNameFunction(arg2.dealias) => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index b635b5824267..ad2092f60b1f 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -5881,7 +5881,7 @@ object Types { if args.exists(isRange) then if variance > 0 then tp.derivedAppliedType(tycon, args.map(rangeToBounds)) match - case tp1: AppliedType if tp1.isUnreducibleWild => + case tp1: AppliedType if tp1.isUnreducibleWild && ctx.phase != checkCapturesPhase => // don't infer a type that would trigger an error later in // Checking.checkAppliedType; fall through to default handling instead case tp1 => diff --git a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala index 54483f74163a..6968eb271961 100644 --- a/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala +++ b/compiler/src/dotty/tools/dotc/transform/FirstTransform.scala @@ -101,9 +101,21 @@ class FirstTransform extends MiniPhase with InfoTransformer { thisPhase => reorder(stats, Nil) } - /** eliminate self in Template */ + /** Eliminate self in Template + * Under -Ycc, we keep the self type `S` around in a type definition + * + * private[this] type $this = S + * + * This is so that the type can be checked for well-formedness in the CaptureCheck phase. + */ override def transformTemplate(impl: Template)(using Context): Tree = - cpy.Template(impl)(self = EmptyValDef) + impl.self match + case self: ValDef if !self.tpt.isEmpty && ctx.settings.Ycc.value => + val tsym = newSymbol(ctx.owner, tpnme.SELF, PrivateLocal, TypeAlias(self.tpt.tpe)) + val tdef = untpd.cpy.TypeDef(self)(tpnme.SELF, self.tpt).withType(tsym.typeRef) + cpy.Template(impl)(self = EmptyValDef, body = tdef :: impl.body) + case _ => + cpy.Template(impl)(self = EmptyValDef) override def transformDefDef(ddef: DefDef)(using Context): Tree = val meth = ddef.symbol.asTerm diff --git a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala index b820ae94be4d..b27a75436d86 100644 --- a/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala +++ b/compiler/src/dotty/tools/dotc/transform/OverridingPairs.scala @@ -29,7 +29,9 @@ object OverridingPairs: /** Symbols to exclude: Here these are constructors and private locals. * But it may be refined in subclasses. */ - protected def exclude(sym: Symbol): Boolean = !sym.memberCanMatchInheritedSymbols + protected def exclude(sym: Symbol): Boolean = + !sym.memberCanMatchInheritedSymbols + || ctx.phase == Phases.checkCapturesPhase && sym.is(Recheck.ResetPrivate) /** The parents of base that are checked when deciding whether an overriding * pair has already been treated in a parent class. diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index d74045cd2a21..70fa0e5cc513 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -932,7 +932,7 @@ object PatternMatcher { } emitWithMashedConditions(plan :: Nil) case LetPlan(sym, body) => - val valDef = ValDef(sym, initializer(sym).ensureConforms(sym.info)).withSpan(sym.span) + val valDef = ValDef(sym, initializer(sym).ensureConforms(sym.info), inferred = true).withSpan(sym.span) seq(valDef :: Nil, emit(body)) case LabeledPlan(label, expr) => Labeled(label, emit(expr)) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index c662d6f00045..b7cabbebee71 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -2125,7 +2125,7 @@ trait Applications extends Compatibility { def isUniform[T](xs: List[T])(p: (T, T) => Boolean) = xs.forall(p(_, xs.head)) val formalsForArg: List[Type] = altFormals.map(_.head) def argTypesOfFormal(formal: Type): List[Type] = - formal match { + formal.dealias match { case defn.FunctionOf(args, result, isImplicit, isErased) => args case defn.PartialFunctionOf(arg, result) => arg :: Nil case _ => Nil diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index aaf5bcfdbddc..ff90d7fadad3 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -260,13 +260,21 @@ class CheckCaptures extends Recheck, SymTransformer: interpolateVarsIn(tree.tpt) override def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = - val saved = curEnv - val localSet = capturedVars(sym) - if !localSet.isAlwaysEmpty then curEnv = Env(sym, localSet, false, curEnv) - try super.recheckDefDef(tree, sym) - finally - interpolateVarsIn(tree.tpt) - curEnv = saved + val isExcluded = + sym.is(Synthetic) + && sym.owner.isClass + && ( defn.caseClassSynthesized.exists( + ccsym => sym.overriddenSymbol(ccsym.owner.asClass) == ccsym) + || sym.name == nme.fromProduct + ) + if !isExcluded then + val saved = curEnv + val localSet = capturedVars(sym) + if !localSet.isAlwaysEmpty then curEnv = Env(sym, localSet, false, curEnv) + try super.recheckDefDef(tree, sym) + finally + interpolateVarsIn(tree.tpt) + curEnv = saved override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = val saved = curEnv @@ -413,11 +421,13 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckFinish(tpe, tree, pt) /** This method implements the rule outlined in #14390: - * When checking an expression `e: Ca Ta` against an expected type `Cx Tx` + * When checking an expression `e: T` against an expected type `Cx Tx` * where the capture set of `Cx` contains this and any method inside the class - * `Cls` of `this` that contains `e` has only pure parameters, drop from `Ca` - * all references to variables or this references outside `Cls`. These are all - * accessed through this, so are already accounted for by `Cx`. + * `Cls` of `this` that contains `e` has only pure parameters, add to `Cx` + * all references to variables or this-references in that capture set of `T` + * that are outside `Cls`. These are all accessed through this, so we can assume + * they are already accounted for by `Cx` and adding them explicitly to `Cx` + * changes nothing. */ override def checkConformsExpr(original: Type, actual: Type, expected: Type, tree: Tree)(using Context): Unit = def isPure(info: Type): Boolean = info match @@ -428,26 +438,28 @@ class CheckCaptures extends Recheck, SymTransformer: if owner == limit then true else if !owner.exists then false else isPure(owner.info) && isPureContext(owner.owner, limit) - val actual1 = (expected, actual.widen) match - case (CapturingType(ecore, erefs, _), actualw @ CapturingType(acore, arefs, _)) => - val arefs1 = (arefs /: erefs.elems) { (arefs1, eref) => - eref match - case eref: ThisType if isPureContext(ctx.owner, eref.cls) => - arefs1.filter { - case aref1: TermRef => !eref.cls.isContainedIn(aref1.symbol.owner) - case aref1: ThisType => !eref.cls.isContainedIn(aref1.cls) - case _ => true - } - case _ => - arefs1 - } - if arefs1 eq arefs then actual - else actualw.derivedCapturingType(acore, arefs1) - .showing(i"healing $actual --> $result", capt) + def augment(erefs: CaptureSet, arefs: CaptureSet): CaptureSet = + (erefs /: erefs.elems) { (erefs, eref) => + eref match + case eref: ThisType if isPureContext(ctx.owner, eref.cls) => + erefs ++ arefs.filter { + case aref: TermRef => eref.cls.isContainedIn(aref.symbol.owner) + case aref: ThisType => eref.cls.isContainedIn(aref.cls) + case _ => false + } + case _ => + erefs + } + val expected1 = expected match + case CapturingType(ecore, erefs, _) => + val erefs1 = augment(erefs, actual.captureSet) + if erefs1 ne erefs then + capt.println(i"augmented $expected from ${actual.captureSet} --> $erefs1") + expected.derivedCapturingType(ecore, erefs1) case _ => - actual - //println(i"check conforms $actual1 <<< $expected") - super.checkConformsExpr(original, actual1, expected, tree) + expected + //println(i"check conforms $actual <<< $expected1") + super.checkConformsExpr(original, actual, expected1, tree) override def checkUnit(unit: CompilationUnit)(using Context): Unit = Setup(preRecheckPhase, thisPhase, recheckDef) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 4edcaab9f5eb..af80edec79ce 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -146,7 +146,7 @@ object Checking { tp match case AppliedType(tycon, argTypes) => checkAppliedType( - untpd.AppliedTypeTree(TypeTree(tycon), argTypes.map(TypeTree)) + untpd.AppliedTypeTree(TypeTree(tycon), argTypes.map(TypeTree(_))) .withType(tp).withSpan(tpt.span.toSynthetic), tpt) case _ => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 815c59c46056..5670d816fafb 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -4068,7 +4068,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer else if pt.isInstanceOf[PolyProto] then tree else var typeArgs = tree match - case Select(qual, nme.CONSTRUCTOR) => qual.tpe.widenDealias.argTypesLo.map(TypeTree) + case Select(qual, nme.CONSTRUCTOR) => qual.tpe.widenDealias.argTypesLo.map(TypeTree(_)) case _ => Nil if typeArgs.isEmpty then typeArgs = constrained(poly, tree)._2 convertNewGenericArray(readapt(tree.appliedToTypeTrees(typeArgs))) diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index f61ac453a741..a830a7949bb8 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -213,6 +213,7 @@ class CompilationTests { compileFilesInDir("tests/run-custom-args/fatal-warnings", defaultOptions.and("-Xfatal-warnings")), compileDir("tests/run-custom-args/Xmacro-settings/simple", defaultOptions.and("-Xmacro-settings:one,two,three")), compileDir("tests/run-custom-args/Xmacro-settings/compileTimeEnv", defaultOptions.and("-Xmacro-settings:a,b=1,c.b.a=x.y.z=1,myLogger.level=INFO")), + compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-Ycc")), compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes), compileFilesInDir("tests/run", defaultOptions.and("-Ysafe-init")) ).checkRuns() diff --git a/tests/neg-custom-args/captures/boxmap.check b/tests/neg-custom-args/captures/boxmap.check index c42699d2c997..f472fabfc671 100644 --- a/tests/neg-custom-args/captures/boxmap.check +++ b/tests/neg-custom-args/captures/boxmap.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boxmap.scala:12:2 ---------------------------------------- 12 | () => b[Box[B]]((x: A) => box(f(x))) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: {f} () -> ? Box[B] + | Found: {f} () -> ? Box[? B] | Required: () -> Box[B] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/emptyref-in-self.scala b/tests/neg-custom-args/captures/emptyref-in-self.scala new file mode 100644 index 000000000000..60f782deca6b --- /dev/null +++ b/tests/neg-custom-args/captures/emptyref-in-self.scala @@ -0,0 +1,3 @@ +class Zip[A, B](underlying: String, other: {*} String) { + this: {underlying, other} Zip[A, B] => // error +} diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index 82a6de52d26e..e43538ad97f7 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -37,6 +37,6 @@ 17 | def tail = xs() // error: cannot have an inferred type | ^^^^^^^^^^^^^^^ | Non-local method tail cannot have an inferred result type - | {LazyCons.this.xs} lazylists.LazyList[T] + | {LazyCons.this.xs} lazylists.LazyList[? T] | with non-empty capture set {LazyCons.this.xs}. | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index bd6fad047fe9..68347b7c3ab1 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- 36 | try // error | ^ - | The expression's type {*} LazyList[Int] is not allowed to capture the root capability `*`. + | The expression's type {*} LazyList[? Int] is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. 37 | tabulate(10) { i => 38 | if i > 9 then throw Ex1() diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 8fe02dc5cb9b..41881b57da24 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -8,7 +8,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ 18 | final class Mapped extends LazyList[B]: // error | ^ - | Found: {f, xs} LazyList[B] + | Found: {f, xs} LazyList[? B] | Required: {f} LazyList[B] 19 | this: ({xs, f} Mapped) => 20 | def isEmpty = false @@ -20,7 +20,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------ 27 | final class Mapped extends LazyList[B]: // error | ^ - | Found: {f, xs} LazyList[B] + | Found: {f, xs} LazyList[? B] | Required: {xs} LazyList[B] 28 | this: ({xs, f} Mapped) => 29 | def isEmpty = false diff --git a/tests/pending/pos/i14575.scala b/tests/pending/pos/i14575.scala new file mode 100644 index 000000000000..ebc16018024a --- /dev/null +++ b/tests/pending/pos/i14575.scala @@ -0,0 +1,15 @@ + +class ann(xs: Any) extends annotation.StaticAnnotation + +class C + +class D: + def m: C = ??? + +trait Ops extends Any { + def m: C @ann(this) +} + +class Ops1(s: String) extends AnyVal with Ops: + def a = new D + def m = a.m diff --git a/tests/pos-custom-args/captures/hk-param.scala b/tests/pos-custom-args/captures/hk-param.scala new file mode 100644 index 000000000000..b0e894d865e9 --- /dev/null +++ b/tests/pos-custom-args/captures/hk-param.scala @@ -0,0 +1,17 @@ +/** Concrete collection type: View */ +trait View[+A] extends Itable[A], ILike[A, [X] =>> {*} View[X]]: + override def fromIterable[B](c: {*} Itable[B]): {c} View[B] = ??? + +trait IPolyTransforms[+A, +C[A]] extends Any: + def fromIterable[B](coll: {*} Itable[B]): C[B] + +trait ILike[+A, +C[X] <: {*} Itable[X]] extends IPolyTransforms[A, C] + +/** Base trait for generic collections */ +trait Itable[+A] extends ItableOnce[A] with ILike[A, {*} Itable] + +/** Iterator can be used only once */ +trait ItableOnce[+A] { + this: {*} ItableOnce[A] => + def iterator: {this} Iterator[A] +} diff --git a/tests/pos-custom-args/captures/nonvariant-inf.scala b/tests/pos-custom-args/captures/nonvariant-inf.scala new file mode 100644 index 000000000000..6569f35042e8 --- /dev/null +++ b/tests/pos-custom-args/captures/nonvariant-inf.scala @@ -0,0 +1,9 @@ + +trait Iterable[+A] + +/** Base trait for instances that can construct a collection from an iterable */ +trait FromIterable { + type C[X] <: {*} Iterable[X] + def fromIterable[B](it: {*} Iterable[B]): {it} C[B] + def empty[A]: C[A] = fromIterable(??? : Iterable[A]) +} diff --git a/tests/run/colltest5.check b/tests/run-custom-args/captures/colltest5.check similarity index 100% rename from tests/run/colltest5.check rename to tests/run-custom-args/captures/colltest5.check diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala new file mode 100644 index 000000000000..796dc62bdf22 --- /dev/null +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -0,0 +1,601 @@ +package colltest5 +package strawman.collections + +import Predef.{augmentString as _, wrapString as _, *} +import scala.reflect.ClassTag +import annotation.unchecked.uncheckedVariance +import annotation.tailrec + +/** A strawman architecture for new collections. It contains some + * example collection classes and methods with the intent to expose + * some key issues. It would be good to compare this to other + * implementations of the same functionality, to get an idea of the + * strengths and weaknesses of different collection architectures. + * + * For a test file, see tests/run/CollectionTests.scala. + */ +object CollectionStrawMan5 { + + /* ------------ Base Traits -------------------------------- */ + + /** Iterator can be used only once */ + trait IterableOnce[+A] { + this: {*} IterableOnce[A] => + def iterator: {this} Iterator[A] + } + + /** Base trait for instances that can construct a collection from an iterable */ + trait FromIterable { + type C[X] <: {*} Iterable[X] + def fromIterable[B](it: {*} Iterable[B]): {it} C[B] + } + + type FromIterableOf[+CC[X] <: {*} Iterable[X]] = FromIterable { + type C[X] <: CC[X] + } + + /** Base trait for companion objects of collections */ + trait IterableFactory extends FromIterable { + def empty[A]: C[A] = fromIterable(View.Empty) + def apply[A](xs: A*): C[A] = fromIterable(View.Elems(xs*)) + } + + /** Base trait for generic collections */ + trait Iterable[+A] extends IterableOnce[A] with IterableLike[A] { + this: {*} Iterable[A] => + type C[X] <: {*} Iterable[X] + protected def coll: {this} Iterable[A] = this + def knownLength: Int = -1 + } + + /** Base trait for sequence collections */ + trait Seq[+A] extends Iterable[A] with SeqLike[A] { + this: Seq[A] => + type C[X] <: Seq[X] + def apply(i: Int): A + def length: Int + } + + trait SeqFactory extends IterableFactory { + type C[X] <: Seq[X] + def fromIterable[B](it: {*} Iterable[B]): C[B] + } + + /** Base trait for strict collections */ + trait Buildable[+A] extends Iterable[A] { + protected[this] def newBuilder: Builder[A, Repr] @uncheckedVariance + override def partition(p: A => Boolean): (Repr, Repr) = { + val l, r = newBuilder + iterator.foreach(x => (if (p(x)) l else r) += x) + (l.result, r.result) + } + // one might also override other transforms here to avoid generating + // iterators if it helps efficiency. + } + + /** Base trait for collection builders */ + trait Builder[-A, +To] { + def +=(x: A): this.type + def result: To + + def ++=(xs: {*} IterableOnce[A]): this.type = { + xs.iterator.foreach(+=) + this + } + } + + /* ------------ Operations ----------------------------------- */ + + /** Base trait for Iterable operations + * + * VarianceNote + * ============ + * + * We require that for all child classes of Iterable the variance of + * the child class and the variance of the `C` parameter passed to `IterableLike` + * are the same. We cannot express this since we lack variance polymorphism. That's + * why we have to resort at some places to write `C[A @uncheckedVariance]`. + * + */ + trait IterableLike[+A] + extends FromIterable + with IterableOps[A] + with IterablePolyTransforms[A] + with IterableMonoTransforms[A] { // sound bcs of VarianceNote + type Repr = C[A] @uncheckedVariance + protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): {coll} Repr @uncheckedVariance = + fromIterable(coll) + } + + /** Base trait for Seq operations */ + trait SeqLike[+A] + extends IterableLike[A], SeqMonoTransforms[A], SeqPolyTransforms[A]: // sound bcs of VarianceNote + this: SeqLike[A] => + type C[X] <: Seq[X] + def fromIterable[B](coll: {*} Iterable[B]): C[B] + override protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): Repr = + fromIterable(coll) + + trait IterableOps[+A] extends Any { + this: {*} IterableOps[A] => + def iterator: {this} Iterator[A] + def foreach(f: A => Unit): Unit = iterator.foreach(f) + def foldLeft[B](z: B)(op: (B, A) => B): B = iterator.foldLeft(z)(op) + def foldRight[B](z: B)(op: (A, B) => B): B = iterator.foldRight(z)(op) + def indexWhere(p: A => Boolean): Int = iterator.indexWhere(p) + def isEmpty: Boolean = !iterator.hasNext + def head: A = iterator.next() + def view: {this} View[A] = View.fromIterator(iterator) + } + + trait IterableMonoTransforms[+A] extends Any { + this: {*} IterableMonoTransforms[A] => + type Repr + protected def coll: {this} Iterable[A] + protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): {coll} Repr + def filter(p: A => Boolean): {this, p} Repr = fromLikeIterable(View.Filter(coll, p)) + + def partition(p: A => Boolean): ({this, p} Repr, {this, p} Repr) = { + val pn = View.Partition(coll, p) + (fromLikeIterable(pn.left), fromLikeIterable(pn.right)) + } + def drop(n: Int): {this} Repr = fromLikeIterable(View.Drop(coll, n)) + + def to[C[X] <: Iterable[X]](fi: FromIterableOf[C]): {this} C[A @uncheckedVariance] = + // variance seems sound because `to` could just as well have been added + // as a decorator. We should investigate this further to be sure. + fi.fromIterable(coll) + } + + trait IterablePolyTransforms[+A] extends Any { + this: {*} IterablePolyTransforms[A] => + type C[A] + protected def coll: {this} Iterable[A] + def fromIterable[B](coll: {*} Iterable[B]): {coll} C[B] + def map[B](f: A => B): {this, f} C[B] = fromIterable(View.Map(coll, f)) + def flatMap[B](f: A => {*} IterableOnce[B]): {this, f} C[B] = fromIterable(View.FlatMap(coll, f)) + def ++[B >: A](xs: {*} IterableOnce[B]): {this, xs} C[B] = fromIterable(View.Concat(coll, xs)) + def zip[B](xs: {*} IterableOnce[B]): {this, xs} C[(A @uncheckedVariance, B)] = fromIterable(View.Zip(coll, xs)) + // sound bcs of VarianceNote + } + + trait SeqMonoTransforms[+A] extends Any, IterableMonoTransforms[A] { + this: SeqMonoTransforms[A] => + def reverse: Repr = + var xs: List[A] = Nil + var it = coll.iterator + while (it.hasNext) xs = new Cons(it.next(), xs) + fromLikeIterable(xs) + + override protected[this] def fromLikeIterable(coll: {*} Iterable[A] @uncheckedVariance): Repr + + override def filter(p: A => Boolean): Repr = fromLikeIterable(View.Filter(coll, p)) + + override def partition(p: A => Boolean): (Repr, Repr) = { + val pn = View.Partition(coll, p) + (fromLikeIterable(pn.left), fromLikeIterable(pn.right)) + } + override def drop(n: Int): Repr = fromLikeIterable(View.Drop(coll, n)) + + override def to[C[X] <: Iterable[X]](fi: FromIterableOf[C]): C[A @uncheckedVariance] = + // variance seems sound because `to` could just as well have been added + // as a decorator. We should investigate this further to be sure. + fi.fromIterable(coll) + } + + trait SeqPolyTransforms[+A] extends Any, IterablePolyTransforms[A]: + this: SeqPolyTransforms[A] => + type C[A] + override def fromIterable[B](coll: {*} Iterable[B]): C[B] + override def map[B](f: A => B): C[B] = fromIterable(View.Map(coll, f)) + override def flatMap[B](f: A => {*} IterableOnce[B]): C[B] = fromIterable(View.FlatMap(coll, f)) + override def ++[B >: A](xs: {*} IterableOnce[B]): C[B] = fromIterable(View.Concat(coll, xs)) + override def zip[B](xs: {*} IterableOnce[B]): C[(A @uncheckedVariance, B)] = fromIterable(View.Zip(coll, xs)) + + + /* --------- Concrete collection types ------------------------------- */ + + /** Concrete collection type: List */ + sealed trait List[+A] extends Seq[A] with SeqLike[A] with Buildable[A] { self => + type C[X] = List[X] + def isEmpty: Boolean + def head: A + def tail: List[A] + def iterator = new Iterator[A] { + private[this] var current = self + def hasNext = !current.isEmpty + def next() = { val r = current.head; current = current.tail; r } + } + def fromIterable[B](c: {*} Iterable[B]): List[B] = List.fromIterable(c) + def apply(i: Int): A = { + require(!isEmpty) + if (i == 0) head else tail.apply(i - 1) + } + def length: Int = + if (isEmpty) 0 else 1 + tail.length + protected[this] def newBuilder = new ListBuffer[A] @uncheckedVariance + def ++:[B >: A](prefix: List[B]): List[B] = + if (prefix.isEmpty) this + else Cons(prefix.head, prefix.tail ++: this) + override def ++[B >: A](xs: {*} IterableOnce[B]): List[B] = xs match { + case xs: List[B] => this ++: xs + case _ => fromIterable(View.Concat(this, xs)) + } + @tailrec final override def drop(n: Int) = + if (n > 0) tail.drop(n - 1) else this + } + + case class Cons[+A](x: A, private[collections] var next: List[A @uncheckedVariance]) // sound because `next` is used only locally + extends List[A] { + override def isEmpty = false + override def head = x + def tail = next + } + + case object Nil extends List[Nothing] { + override def isEmpty = true + override def head = ??? + def tail = ??? + } + + object List extends SeqFactory { + type C[X] = List[X] + def fromIterable[B](coll: {*} Iterable[B]): List[B] = coll match { + case coll: List[B] => coll + case _ => ListBuffer.fromIterable(coll).result + } + } + + /** Concrete collection type: ListBuffer */ + class ListBuffer[A] extends Seq[A] with SeqLike[A] with Builder[A, List[A]] { + type C[X] = ListBuffer[X] + private var first, last: List[A] = Nil + private var aliased = false + def iterator = first.iterator + def fromIterable[B](coll: {*} Iterable[B]): ListBuffer[B] = ListBuffer.fromIterable(coll) + def apply(i: Int) = first.apply(i) + def length = first.length + + private def copyElems(): Unit = { + val buf = ListBuffer.fromIterable(result) + first = buf.first + last = buf.last + aliased = false + } + def result = { + aliased = true + first + } + def +=(elem: A) = { + if (aliased) copyElems() + val last1 = Cons(elem, Nil) + last match { + case last: Cons[A] => last.next = last1 + case _ => first = last1 + } + last = last1 + this + } + override def toString: String = + if (first.isEmpty) "ListBuffer()" + else { + val b = new StringBuilder("ListBuffer(").append(first.head) + first.tail.foldLeft(b)(_.append(", ").append(_)).append(")").toString + } + } + + object ListBuffer extends SeqFactory { + type C[X] = ListBuffer[X] + def fromIterable[B](coll: {*} Iterable[B]): ListBuffer[B] = new ListBuffer[B] ++= coll + } + + /** Concrete collection type: ArrayBuffer */ + class ArrayBuffer[A] private (initElems: Array[AnyRef], initLength: Int) + extends Seq[A] with SeqLike[A] with Builder[A, ArrayBuffer[A]] { + this: ArrayBuffer[A] => + type C[X] = ArrayBuffer[X] + def this() = this(new Array[AnyRef](16), 0) + private var elems: Array[AnyRef] = initElems + private var start = 0 + private var end = initLength + def apply(n: Int) = elems(start + n).asInstanceOf[A] + def length = end - start + override def knownLength = length + override def view = new ArrayBufferView(elems, start, end) + def iterator = view.iterator + def fromIterable[B](it: {*} Iterable[B]): ArrayBuffer[B] = + ArrayBuffer.fromIterable(it) + def +=(elem: A): this.type = { + if (end == elems.length) { + if (start > 0) { + Array.copy(elems, start, elems, 0, length) + end -= start + start = 0 + } + else { + val newelems = new Array[AnyRef](end * 2) + Array.copy(elems, 0, newelems, 0, end) + elems = newelems + } + } + elems(end) = elem.asInstanceOf[AnyRef] + end += 1 + this + } + def result = this + def trimStart(n: Int): Unit = start += (n max 0) + override def ++[B >: A](xs: {*} IterableOnce[B]): ArrayBuffer[B] = xs match { + case xs: ArrayBuffer[B] @unchecked => + val elems = new Array[AnyRef](length + xs.length) + Array.copy(this.elems, this.start, elems, 0, this.length) + Array.copy(xs.elems, xs.start, elems, this.length, xs.length) + new ArrayBuffer(elems, elems.length) + case _ => fromIterable(View.Concat(this, xs)) + } + + override def toString = s"ArrayBuffer(${elems.slice(start, end).mkString(", ")})" + } + + object ArrayBuffer extends SeqFactory { + type C[X] = ArrayBuffer[X] + def fromIterable[B](coll: {*} Iterable[B]): ArrayBuffer[B] = + if (coll.knownLength >= 0) { + val elems = new Array[AnyRef](coll.knownLength) + val it = coll.iterator + for (i <- 0 until elems.length) elems(i) = it.next().asInstanceOf[AnyRef] + new ArrayBuffer[B](elems, elems.length): ArrayBuffer[B] + } + else { + val buf = new ArrayBuffer[B] + val it = coll.iterator + while (it.hasNext) buf += it.next() + buf: ArrayBuffer[B] + } + } + + class ArrayBufferView[A](val elems: Array[AnyRef], val start: Int, val end: Int) extends RandomAccessView[A] { + this: ArrayBufferView[A] => + def apply(n: Int) = elems(start + n).asInstanceOf[A] + } + + /** Concrete collection type: String */ + implicit class StringOps(val s: String) + extends IterableOps[Char] + with SeqMonoTransforms[Char] + with IterablePolyTransforms[Char] { + this: StringOps => + type Repr = String + type C[X] = List[X] + protected def coll = new StringView(s) + def iterator = coll.iterator + protected def fromLikeIterable(coll: {*} Iterable[Char]): String = { + val sb = new StringBuilder + for (ch <- coll) sb.append(ch) + sb.toString + } + def fromIterable[B](coll: {*} Iterable[B]): List[B] = List.fromIterable(coll) + def map(f: Char => Char): String = { + val sb = new StringBuilder + for (ch <- s) sb.append(f(ch)) + sb.toString + } + def flatMap(f: Char => String): String = { + val sb = new StringBuilder + for (ch <- s) sb.append(f(ch)) + sb.toString + } + def ++(xs: {*} IterableOnce[Char]): String = { + val sb = new StringBuilder(s) + for (ch <- xs.iterator) sb.append(ch) + sb.toString + } + def ++(xs: String): String = s + xs + } + + case class StringView(s: String) extends RandomAccessView[Char] { + val start = 0 + val end = s.length + def apply(n: Int) = s.charAt(n) + } + +/* ---------- Views -------------------------------------------------------*/ + + /** Concrete collection type: View */ + trait View[+A] extends Iterable[A] with IterableLike[A] { + this: {*} View[A] => + type C[X] = {this} View[X] + override def view: this.type = this + override def fromIterable[B](c: {*} Iterable[B]): {this, c} View[B] = { + c match { + case c: View[B] => c + case _ => View.fromIterator(c.iterator) + } + } + } + + /** View defined in terms of indexing a range */ + trait RandomAccessView[+A] extends View[A] { + def start: Int + def end: Int + def apply(i: Int): A + def iterator: Iterator[A] = new Iterator[A] { + private var current = start + def hasNext = current < end + def next(): A = { + val r = apply(current) + current += 1 + r + } + } + override def knownLength = end - start max 0 + } + + object View { + def fromIterator[A](it: => {*} Iterator[A]): {it} View[A] = new View[A]: + def iterator: {this} Iterator[A] = it + + case object Empty extends View[Nothing] { + def iterator: Iterator[Nothing] = Iterator.empty + override def knownLength = 0 + } + + case class Elems[A](xs: A*) extends View[A] { + def iterator: Iterator[A] = Iterator(xs*) + override def knownLength = xs.length + } + + case class Filter[A](val underlying: {*} Iterable[A], p: A => Boolean) extends View[A] { + this: {underlying, p} Filter[A] => + def iterator: {this} Iterator[A] = underlying.iterator.filter(p) + } + case class Partition[A](val underlying: {*} Iterable[A], p: A => Boolean) { + self: {underlying, p} Partition[A] => + + class Partitioned(expected: Boolean) extends View[A]: + this: {self} Partitioned => + def iterator: {this} Iterator[A] = + underlying.iterator.filter((x: A) => p(x) == expected) + + val left: {self} Partitioned = Partitioned(true) + val right: {self} Partitioned = Partitioned(false) + } + + case class Drop[A](underlying: {*} Iterable[A], n: Int) extends View[A] { + this: {underlying} Drop[A] => + def iterator: {this} Iterator[A] = underlying.iterator.drop(n) + override def knownLength = + if (underlying.knownLength >= 0) underlying.knownLength - n max 0 else -1 + } + + case class Map[A, B](underlying: {*} Iterable[A], f: A => B) extends View[B] { + this: {underlying, f} Map[A, B] => + def iterator: {this} Iterator[B] = underlying.iterator.map(f) + override def knownLength = underlying.knownLength + } + + case class FlatMap[A, B](underlying: {*} Iterable[A], f: A => {*} IterableOnce[B]) extends View[B] { + this: {underlying, f} FlatMap[A, B] => + def iterator: {this} Iterator[B] = underlying.iterator.flatMap(f) + } + + case class Concat[A](underlying: {*} Iterable[A], other: {*} IterableOnce[A]) extends View[A] { + this: {underlying, other} Concat[A] => + def iterator: {this} Iterator[A] = underlying.iterator ++ other + override def knownLength = other match { + case other: Iterable[_] if underlying.knownLength >= 0 && other.knownLength >= 0 => + underlying.knownLength + other.knownLength + case _ => + -1 + } + } + + case class Zip[A, B](underlying: {*} Iterable[A], other: {*} IterableOnce[B]) extends View[(A, B)] { + this: {underlying, other} Zip[A, B] => + def iterator: {this} Iterator[(A, B)] = underlying.iterator.zip(other) + override def knownLength = other match { + case other: Iterable[_] if underlying.knownLength >= 0 && other.knownLength >= 0 => + underlying.knownLength min other.knownLength + case _ => + -1 + } + } + } + +/* ---------- Iterators ---------------------------------------------------*/ + + /** A core Iterator class */ + trait Iterator[+A] extends IterableOnce[A] { self: {*} Iterator[A] => + def hasNext: Boolean + def next(): A + def iterator: this.type = this + def foldLeft[B](z: B)(op: (B, A) => B): B = + if (hasNext) foldLeft(op(z, next()))(op) else z + def foldRight[B](z: B)(op: (A, B) => B): B = + if (hasNext) op(next(), foldRight(z)(op)) else z + def foreach(f: A => Unit): Unit = + while (hasNext) f(next()) + def indexWhere(p: A => Boolean): Int = { + var i = 0 + while (hasNext) { + if (p(next())) return i + i += 1 + } + -1 + } + def filter(p: A => Boolean): {this, p} Iterator[A] = new Iterator[A] { + private var hd: A = compiletime.uninitialized + private var hdDefined: Boolean = false + + def hasNext: Boolean = hdDefined || { + while { + if (!self.hasNext) return false + hd = self.next() + !p(hd) + } do () + hdDefined = true + true + } + + def next() = + if (hasNext) { + hdDefined = false + hd + } + else Iterator.empty.next() + } + + def map[B](f: A => B): {this, f} Iterator[B] = new Iterator[B] { + def hasNext = self.hasNext + def next() = f(self.next()) + } + + def flatMap[B](f: A => {*} IterableOnce[B]): {this, f} Iterator[B] = new Iterator[B] { + private var myCurrent: {this} Iterator[B] = Iterator.empty + private def current = { + while (!myCurrent.hasNext && self.hasNext) + myCurrent = f(self.next()).iterator + myCurrent + } + def hasNext = current.hasNext + def next() = current.next() + } + def ++[B >: A](xs: {*} IterableOnce[B]): {this, xs} Iterator[B] = new Iterator[B] { + private var myCurrent: {self, xs} Iterator[B] = self + private var first = true + private def current = { + if (!myCurrent.hasNext && first) { + myCurrent = xs.iterator + first = false + } + myCurrent + } + def hasNext = current.hasNext + def next() = current.next() + } + def drop(n: Int): {this} Iterator[A] = { + var i = 0 + while (i < n && hasNext) { + next() + i += 1 + } + this + } + def zip[B](that: {*} IterableOnce[B]): {this, that} Iterator[(A, B)] = new Iterator[(A, B)] { + val thatIterator = that.iterator + def hasNext = self.hasNext && thatIterator.hasNext + def next() = (self.next(), thatIterator.next()) + } + } + + object Iterator { + val empty: Iterator[Nothing] = new Iterator[Nothing] { + def hasNext = false + def next() = throw new NoSuchElementException("next on empty iterator") + } + def apply[A](xs: A*): Iterator[A] = new RandomAccessView[A] { + val start = 0 + val end = xs.length + def apply(n: Int) = xs(n) + }.iterator + } +} diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala new file mode 100644 index 000000000000..a0d90a474e7f --- /dev/null +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -0,0 +1,185 @@ +import Predef.{augmentString as _, wrapString as _, *} +import scala.reflect.ClassTag + +object Test { + import colltest5.strawman.collections.* + import CollectionStrawMan5.* + + def seqOps(xs: Seq[Int]) = { // try with {*} Seq[Int] + val strPlusInt: (String, Int) => String = _ + _ + val intPlusStr: (Int, String) => String = _ + _ + val isEven: Int => Boolean = _ % 2 == 0 + val isNonNeg: Int => Boolean = _ > 0 + val flips: Int => List[Int] = x => Cons(x, Cons(-x, Nil)) + val x1 = xs.foldLeft("")(strPlusInt) + val y1: String = x1 + val x2 = xs.foldRight("")(intPlusStr) + val y2: String = x2 + val x3 = xs.indexWhere(isEven) + val y3: Int = x3 + val x4 = xs.head + val y4: Int = x4 + val x5 = xs.to(List) + val y5: List[Int] = x5 + val (xs6, xs7) = xs.partition(isEven) + val ys6: Seq[Int] = xs6 + val ys7: Seq[Int] = xs7 + val xs8 = xs.drop(2) + val ys8: Seq[Int] = xs8 + val xs9 = xs.map(isNonNeg) + val ys9: Seq[Boolean] = xs9 + val xs10 = xs.flatMap(flips) + val ys10: Seq[Int] = xs10 + val xs11 = xs ++ xs + val ys11: Seq[Int] = xs11 + val xs12 = xs ++ Nil + val ys12: Seq[Int] = xs12 + val xs13 = Nil ++ xs + val ys13: Seq[Int] = xs13 + val xs14 = xs.++[Any](Cons("a", Nil)) // !!! + val ys14: Seq[Any] = xs14 + val xs15 = xs.zip(xs9) + val ys15: Seq[(Int, Boolean)] = xs15 + val xs16 = xs.reverse + val ys16: Seq[Int] = xs16 + println("-------") + println(x1) + println(x2) + println(x3) + println(x4) + println(x5) + println(xs6) + println(xs7) + println(xs8) + println(xs9) + println(xs10) + println(xs11) + println(xs12) + println(xs13) + println(xs14) + println(xs15) + println(xs16) + } + + def viewOps(xs: {*} View[Int]) = { + val strPlusInt: (String, Int) => String = _ + _ + val intPlusStr: (Int, String) => String = _ + _ + val isEven: Int => Boolean = _ % 2 == 0 + val isNonNeg: Int => Boolean = _ > 0 + val flips: Int => List[Int] = x => Cons(x, Cons(-x, Nil)) + val x1 = xs.foldLeft("")(strPlusInt) + val y1: String = x1 + val x2 = xs.foldRight("")(intPlusStr) + val y2: String = x2 + val x3 = xs.indexWhere(isEven) + val y3: Int = x3 + val x4 = xs.head + val y4: Int = x4 + val x5 = xs.to(List) + val y5: {x5} List[Int] = x5 // !!! + val (xs6, xs7) = xs.partition(isEven) + val ys6: {xs6, isEven} View[Int] = xs6 + val ys7: {xs7, isEven} View[Int] = xs7 + val (xs6a, xs7a) = xs.partition(_ % 2 == 0) + val ys6a: {xs6} View[Int] = xs6 + val ys7a: {xs7} View[Int] = xs7 + val xs8 = xs.drop(2) + val ys8: {xs8} View[Int] = xs8 + val xs9 = xs.map(isNonNeg) + val ys9: {xs9} View[Boolean] = xs9 + val xs10 = xs.flatMap(flips) + val ys10: {xs10} View[Int] = xs10 + val xs11 = xs ++ xs + val ys11: {xs11} View[Int] = xs11 + val xs12 = xs ++ Nil + val ys12: {xs12} View[Int] = xs12 + val xs13 = Nil ++ xs + val ys13: List[Int] = xs13 + val xs14 = xs ++ Cons("a", Nil) + val ys14: {xs14} View[Any] = xs14 + val xs15 = xs.zip(xs9) + val ys15: {xs15} View[(Int, Boolean)] = xs15 + println("-------") + println(x1) + println(x2) + println(x3) + println(x4) + println(x5) + println(xs6.to(List)) + println(xs7.to(List)) + println(xs8.to(List)) + println(xs9.to(List)) + println(xs10.to(List)) + println(xs11.to(List)) + println(xs12.to(List)) + println(xs13.to(List)) + println(xs14.to(List)) + println(xs15.to(List)) + } + + def stringOps(xs: String) = { + val x1 = xs.foldLeft("")(_ + _) + val y1: String = x1 + val x2 = xs.foldRight("")(_ + _) + val y2: String = x2 + val x3 = xs.indexWhere(_ % 2 == 0) + val y3: Int = x3 + val x4 = xs.head + val y4: Int = x4 + val x5 = xs.to(List) + val y5: List[Char] = x5 + val (xs6, xs7) = xs.partition(_ % 2 == 0) + val ys6: String = xs6 + val ys7: String = xs7 + val xs8 = xs.drop(2) + val ys8: String = xs8 + val xs9 = xs.map(_ + 1) + val ys9: Seq[Int] = xs9 + val xs9a = xs.map(_.toUpper) + val ys9a: String = xs9a + val xs10 = xs.flatMap((x: Char) => s"$x,$x") + val ys10: String = xs10 + val xs11 = xs ++ xs + val ys11: String = xs11 + val xs11a = xs ++ List('x', 'y') // Cons('x', Cons('y', Nil)) + val ys11a: String = xs11a + val xs12 = xs ++ Nil + val ys12: String = xs12 + val xs13 = Nil ++ xs.iterator + val ys13: List[Char] = xs13 + val xs14 = xs ++ Cons("xyz", Nil) + val ys14: Seq[Any] = xs14 + val xs15 = xs.zip(xs9) + val ys15: Seq[(Char, Int)] = xs15 + println("-------") + println(x1) + println(x2) + println(x3) + println(x4) + println(x5) + println(xs6) + println(xs7) + println(xs8) + println(xs9) + println(xs9a) + println(xs10) + println(xs11) + println(xs11a) + println(xs12) + println(xs13) + println(xs14) + println(xs15) + } + + def main(args: Array[String]) = { + val ints = Cons(1, Cons(2, Cons(3, Nil))) + val intsBuf = ints.to(ArrayBuffer) + val intsListBuf = ints.to(ListBuffer) + val intsView = ints.view + seqOps(ints) + seqOps(intsBuf) + seqOps(intsListBuf) + viewOps(intsView) + stringOps("abc") + } +} From 75143cc43184cc69a76773077a4d25d47fa2b4dc Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 6 Mar 2022 16:55:52 +0100 Subject: [PATCH 34/99] Make it compile under -Yexplicit-nulls --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 5 ++--- compiler/src/dotty/tools/dotc/core/Types.scala | 10 +++++----- .../src/dotty/tools/dotc/typer/CheckCaptures.scala | 3 ++- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 33b8071d5bbb..f48fbeb72066 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -261,7 +261,6 @@ object CaptureSet: if elems.isEmpty then empty else Const(elems) class Const private[CaptureSet] (val elems: Refs, val description: String = "") extends CaptureSet: - assert(elems != null) def isConst = true def isAlwaysEmpty = elems.isEmpty @@ -370,7 +369,7 @@ object CaptureSet: val trail = this.match case dv: DerivedVar => dv.source.ids case _ => "" - s"$id${getClass.getSimpleName.take(1)}$trail" + s"$id${getClass.getSimpleName.nn.take(1)}$trail" override def toText(printer: Printer): Text = inContext(printer.printerContext) { for vars <- ctx.property(ShownVars) do vars += this @@ -397,7 +396,7 @@ object CaptureSet: (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) extends DerivedVar(initial.elems): addSub(initial) - val stack = if debugSets then (new Throwable).getStackTrace().take(20) else null + val stack = if debugSets then (new Throwable).getStackTrace().nn.take(20) else null private def whereCreated(using Context): String = if stack == null then "" diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index ad2092f60b1f..355cd746d746 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -2075,9 +2075,9 @@ object Types { /** A trait for references in CaptureSets. These can be NamedTypes, ThisTypes or ParamRefs */ trait CaptureRef extends SingletonType: - private var myCaptureSet: CaptureSet = _ + private var myCaptureSet: CaptureSet | Null = _ private var myCaptureSetRunId: Int = NoRunId - private var mySingletonCaptureSet: CaptureSet.Const = null + private var mySingletonCaptureSet: CaptureSet.Const | Null = null def canBeTracked(using Context): Boolean final def isTracked(using Context): Boolean = canBeTracked && !captureSetOfInfo.isAlwaysEmpty @@ -2087,11 +2087,11 @@ object Types { def singletonCaptureSet(using Context): CaptureSet.Const = if mySingletonCaptureSet == null then mySingletonCaptureSet = CaptureSet(this.normalizedRef) - mySingletonCaptureSet + mySingletonCaptureSet.uncheckedNN def captureSetOfInfo(using Context): CaptureSet = - if ctx.runId == myCaptureSetRunId then myCaptureSet - else if myCaptureSet eq CaptureSet.Pending then CaptureSet.empty + 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) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index ff90d7fadad3..66aa17b959f4 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -35,7 +35,8 @@ object CheckCaptures: else sym end Pre - case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer: Env): + case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer0: Env | Null): + def outer = outer0.nn def isOpen = !captured.isAlwaysEmpty && !isBoxed final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) From 8a1acde104eeb50465c23c3573e0a8384944b4a7 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Mar 2022 11:27:21 +0100 Subject: [PATCH 35/99] Make it compile without language.postfixOps --- compiler/src/dotty/tools/dotc/transform/Recheck.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index a90a0c99de84..eb62c4ad65f4 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -133,7 +133,7 @@ abstract class Recheck extends Phase, SymTransformer: //val pre = ta.maybeSkolemizePrefix(qualType, name) val mbr = qualType.findMember(name, qualType, excluded = if tree.symbol.is(Private) then EmptyFlags else Private - ).suchThat(tree.symbol ==) + ).suchThat(tree.symbol == _) constFold(tree, qualType.select(name, mbr)) //.showing(i"recheck select $qualType . $name : ${mbr.symbol.info} = $result") From 0486a7908be293a357c35be8f3b4bbacbfcab4fa Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 26 Mar 2022 13:19:22 +0100 Subject: [PATCH 36/99] Make derivedAnnotation of universal the identity Fixes #14782 --- compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala | 3 ++- tests/pos/i14782.scala | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tests/pos/i14782.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index 9f4f99ad52f3..773454565b04 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -29,7 +29,8 @@ case class CaptureAnnotation(refs: CaptureSet, kind: CapturingKind) extends Anno if kind == CapturingKind.ByName then defn.RetainsByNameAnnot else defn.RetainsAnnot override def derivedAnnotation(tree: Tree)(using Context): Annotation = - unsupported("derivedAnnotation(Tree)") + if refs == CaptureSet.universal then this + else unsupported("derivedAnnotation(Tree)") def derivedAnnotation(refs: CaptureSet, kind: CapturingKind)(using Context): Annotation = if (this.refs eq refs) && (this.kind == kind) then this diff --git a/tests/pos/i14782.scala b/tests/pos/i14782.scala new file mode 100644 index 000000000000..61a6d2117b03 --- /dev/null +++ b/tests/pos/i14782.scala @@ -0,0 +1,8 @@ +object BugExample { + val urls: List[String] = List("any_url") + val closures: List[Unit => Unit] = + urls.map(url => _ => { + println("Scraping " + url) + }) + for (closure <- closures) closure(()) +} \ No newline at end of file From acbab4bb5b8c2e11044db8717e84b58ac84cf2f3 Mon Sep 17 00:00:00 2001 From: Ondrej Lhotak Date: Tue, 29 Mar 2022 11:30:30 +0200 Subject: [PATCH 37/99] add stack allocation tests from CC paper --- .../neg-custom-args/captures/stack-alloc.scala | 17 +++++++++++++++++ .../pos-custom-args/captures/stack-alloc.scala | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/neg-custom-args/captures/stack-alloc.scala create mode 100644 tests/pos-custom-args/captures/stack-alloc.scala diff --git a/tests/neg-custom-args/captures/stack-alloc.scala b/tests/neg-custom-args/captures/stack-alloc.scala new file mode 100644 index 000000000000..c296135fbbc7 --- /dev/null +++ b/tests/neg-custom-args/captures/stack-alloc.scala @@ -0,0 +1,17 @@ +import scala.collection.mutable + +class Pooled + +val stack = mutable.ArrayBuffer[Pooled]() +var nextFree = 0 + +def withFreshPooled[T](op: ({*} Pooled) => T): T = + if nextFree >= stack.size then stack.append(new Pooled) + val pooled = stack(nextFree) + nextFree = nextFree + 1 + val ret = op(pooled) + nextFree = nextFree - 1 + ret + +def test() = + withFreshPooled(pooled => () => pooled.toString ) // error \ No newline at end of file diff --git a/tests/pos-custom-args/captures/stack-alloc.scala b/tests/pos-custom-args/captures/stack-alloc.scala new file mode 100644 index 000000000000..03b6708a3119 --- /dev/null +++ b/tests/pos-custom-args/captures/stack-alloc.scala @@ -0,0 +1,17 @@ +import scala.collection.mutable + +class Pooled + +val stack = mutable.ArrayBuffer[Pooled]() +var nextFree = 0 + +def withFreshPooled[T](op: ({*} Pooled) => T): T = + if nextFree >= stack.size then stack.append(new Pooled) + val pooled = stack(nextFree) + nextFree = nextFree + 1 + val ret = op(pooled) + nextFree = nextFree - 1 + ret + +def test() = + withFreshPooled(pooled => pooled.toString) \ No newline at end of file From 152225df12ea31d3b80c04890cf5b27e18a18687 Mon Sep 17 00:00:00 2001 From: Ondrej Lhotak Date: Tue, 29 Mar 2022 11:42:29 +0200 Subject: [PATCH 38/99] udpate test to match paper --- tests/neg-custom-args/captures/stack-alloc.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/neg-custom-args/captures/stack-alloc.scala b/tests/neg-custom-args/captures/stack-alloc.scala index c296135fbbc7..b646c0736f2c 100644 --- a/tests/neg-custom-args/captures/stack-alloc.scala +++ b/tests/neg-custom-args/captures/stack-alloc.scala @@ -14,4 +14,5 @@ def withFreshPooled[T](op: ({*} Pooled) => T): T = ret def test() = - withFreshPooled(pooled => () => pooled.toString ) // error \ No newline at end of file + val pooledClosure = withFreshPooled(pooled => () => pooled.toString) // error + pooledClosure() \ No newline at end of file From 2f0bc122cde1c9e111832caccc83d67f4fc4df6c Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 28 Apr 2022 12:23:18 +0200 Subject: [PATCH 39/99] Fix rebase breakage --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 4 +- .../dotty/tools/dotc/core/TypeComparer.scala | 94 +++++++------------ tests/neg/i2887b.check | 1 + 3 files changed, 37 insertions(+), 62 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index f48fbeb72066..fdf71f2433a3 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -410,7 +410,7 @@ object CaptureSet: mapRefs(newElems, tm, variance) else if variance <= 0 && !origin.isConst && (origin ne initial) then - report.warning(i"trying to add elems $newElems from unrecognized source $origin of mapped set $this$whereCreated") + report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") return CompareResult.fail(this) Const(newElems) super.addNewElems(added.elems, origin) @@ -441,7 +441,7 @@ object CaptureSet: super.addNewElems(newElems, origin) .andAlso { source.tryInclude(newElems.map(bimap.backward), this) - .showing(i"propagating new elems $newElems backward from $this to $source", capt) + .showing(i"propagating new elems ${CaptureSet(newElems)} backward from $this to $source", capt) } override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 4ff4a625ffa3..a852f06503c1 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1884,67 +1884,41 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => false } - def qualifies(m: SingleDenotation): Boolean = - // If the member is an abstract type and the prefix is a path, compare the member itself - // instead of its bounds. This case is needed situations like: - // - // class C { type T } - // val foo: C - // foo.type <: C { type T {= , <: , >:} foo.T } - // - // or like: - // - // class C[T] - // C[?] <: C[TV] - // - // where TV is a type variable. See i2397.scala for an example of the latter. - def matchAbstractTypeMember(info1: Type): Boolean = info1 match { - case TypeBounds(lo, hi) if lo ne hi => - tp2.refinedInfo match { - case rinfo2: TypeBounds if tp1.isStable => - val ref1 = tp1.widenExpr.select(name) - isSubType(rinfo2.lo, ref1) && isSubType(ref1, rinfo2.hi) - case _ => - false - } - case _ => false - } + // An additional check for type member matching: If the refinement of the + // supertype `tp2` does not refer to a member symbol defined in the parent of `tp2`. + // then the symbol referred to in the subtype must have a signature that coincides + // in its parameters with the refinement's signature. The reason for the check + // is that if the refinement does not refer to a member symbol, we will have to + // resort to reflection to invoke the member. And Java reflection needs to know exact + // erased parameter types. See neg/i12211.scala. Other reflection algorithms could + // conceivably dispatch without knowning precise parameter signatures. One can signal + // this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait, + // in which case the signature test is elided. + def sigsOK(symInfo: Type, info2: Type) = + tp2.underlyingClassRef(refinementOK = true).member(name).exists + || tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass) + || symInfo.isInstanceOf[MethodType] + && symInfo.signature.consistentParams(info2.signature) + + def tp1IsSingleton: Boolean = tp1.isInstanceOf[SingletonType] + + // A relaxed version of isSubType, which compares method types + // under the standard arrow rule which is contravarient in the parameter types, + // but under the condition that signatures might have to match (see sigsOK) + // This relaxed version is needed to correctly compare dependent function types. + // See pos/i12211.scala. + def isSubInfo(info1: Type, info2: Type, symInfo: Type): Boolean = + info2 match + case info2: MethodType => + info1 match + case info1: MethodType => + val symInfo1 = symInfo.stripPoly + matchingMethodParams(info1, info2, precise = false) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType) + && sigsOK(symInfo1, info2) + case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } + case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } - // An additional check for type member matching: If the refinement of the - // supertype `tp2` does not refer to a member symbol defined in the parent of `tp2`. - // then the symbol referred to in the subtype must have a signature that coincides - // in its parameters with the refinement's signature. The reason for the check - // is that if the refinement does not refer to a member symbol, we will have to - // resort to reflection to invoke the member. And Java reflection needs to know exact - // erased parameter types. See neg/i12211.scala. Other reflection algorithms could - // conceivably dispatch without knowning precise parameter signatures. One can signal - // this by inheriting from the `scala.reflect.SignatureCanBeImprecise` marker trait, - // in which case the signature test is elided. - def sigsOK(symInfo: Type, info2: Type) = - tp2.underlyingClassRef(refinementOK = true).member(name).exists - || tp2.derivesFrom(defn.WithoutPreciseParameterTypesClass) - || symInfo.isInstanceOf[MethodType] - && symInfo.signature.consistentParams(info2.signature) - - def tp1IsSingleton: Boolean = tp1.isInstanceOf[SingletonType] - - // A relaxed version of isSubType, which compares method types - // under the standard arrow rule which is contravarient in the parameter types, - // but under the condition that signatures might have to match (see sigsOK) - // This relaxed version is needed to correctly compare dependent function types. - // See pos/i12211.scala. - def isSubInfo(info1: Type, info2: Type, symInfo: Type): Boolean = - info2 match - case info2: MethodType => - info1 match - case info1: MethodType => - val symInfo1 = symInfo.stripPoly - matchingMethodParams(info1, info2, precise = false) - && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1), symInfo1.resultType) - && sigsOK(symInfo1, info2) - case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } - case _ => inFrozenGadtIf(tp1IsSingleton) { isSubType(info1, info2) } - def qualifies(m: SingleDenotation): Boolean = val info1 = m.info.widenExpr isSubInfo(info1, tp2.refinedInfo.widenExpr, m.symbol.info.orElse(info1)) diff --git a/tests/neg/i2887b.check b/tests/neg/i2887b.check index f5eb5c26dbaf..7b85d1a0223b 100644 --- a/tests/neg/i2887b.check +++ b/tests/neg/i2887b.check @@ -4,6 +4,7 @@ | Recursion limit exceeded. | Maybe there is an illegal cyclic reference? | If that's not the case, you could also try to increase the stacksize using the -Xss JVM option. + | For the unprocessed stack trace, compile with -Yno-decode-stacktraces. | A recurring operation is (inner to outer): | | try to instantiate Z[Z] From 097b179755fd664f29243308dc5691f48f845633 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 28 Apr 2022 14:29:34 +0200 Subject: [PATCH 40/99] Fix universal check for inferred types Checking against capturing the universal capability now works also for inferred types. Also fix breakage from previous rebase --- .../tools/dotc/typer/CheckCaptures.scala | 23 ++++++------ docs/_docs/reference/experimental/cc.md | 4 +-- .../captures/usingLogFile.check | 15 ++++++++ .../captures/usingLogFile.scala | 36 ++++++++++++++++++- tests/semanticdb/metac.expect | 3 -- 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 66aa17b959f4..6bd3792cc091 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -408,17 +408,18 @@ class CheckCaptures extends Recheck, SymTransformer: tree.symbol.info case _ => NoType - if typeToCheck.exists then - typeToCheck.widenDealias match - case wtp @ CapturingType(parent, refs, _) => - refs.disallowRootCapability { () => - val kind = if tree.isInstanceOf[ValDef] then "mutable variable" else "expression" - report.error( - em"""The $kind's type $wtp is not allowed to capture the root capability `*`. - |This usually means that a capability persists longer than its allowed lifetime.""", - tree.srcPos) - } - case _ => + def checkNotUniversal(tp: Type): Unit = tp.widenDealias match + case wtp @ CapturingType(parent, refs, _) => + refs.disallowRootCapability { () => + val kind = if tree.isInstanceOf[ValDef] then "mutable variable" else "expression" + report.error( + em"""The $kind's type $wtp is not allowed to capture the root capability `*`. + |This usually means that a capability persists longer than its allowed lifetime.""", + tree.srcPos) + } + checkNotUniversal(parent) + case _ => + checkNotUniversal(typeToCheck) super.recheckFinish(tpe, tree, pt) /** This method implements the rule outlined in #14390: diff --git a/docs/_docs/reference/experimental/cc.md b/docs/_docs/reference/experimental/cc.md index 592d410a4502..c8fccad0bc4e 100644 --- a/docs/_docs/reference/experimental/cc.md +++ b/docs/_docs/reference/experimental/cc.md @@ -368,7 +368,7 @@ again on access, the capture information "pops out" again. For instance, even th () => p.fst : {ct} () -> {ct} Int -> String ``` In other words, references to capabilities "tunnel through" in generic instantiations from creation to access; they do not affect the capture set of the enclosing generic data constructor applications. -This principle may seem surprising at first, but it is the key to make capture checking concise and practical. +This principle plays an important part in making capture checking concise and practical. ## Escape Checking @@ -398,7 +398,7 @@ This error message was produced by the following logic: - The `f` parameter has type `{*} FileOutputStream`, which makes it a capability. - Therefore, the type of the expression `() => f.write(0)` is `{f} () -> Unit`. - - This makes the whole type of the closure passed to `usingLogFile` the dependent function type + - This makes the type of the whole closure passed to `usingLogFile` the dependent function type `(f: {*} FileOutputStream) -> {f} () -> Unit`. - The expected type of the closure is a simple, parametric, impure function type `({*} FileOutputStream) => T`, for some instantiation of the type variable `T`. diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index b292b230b742..aa1c90f9ea4c 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -18,3 +18,18 @@ | ^^^^^^^^ | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:27 ------------------------------------------------------ +47 | val later = usingLogFile { f => () => f.write(0) } // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:62:25 ------------------------------------------------------ +62 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The expression's type {*} (x$0: Int) -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:48 ------------------------------------------------------ +71 | val later = usingFile("logfile", usingLogger(_, l => () => l.log("test"))) // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index 206986e0bb22..20426c43779a 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -1,4 +1,4 @@ -import java.io.FileOutputStream +import java.io.* import annotation.capability object Test1: @@ -36,3 +36,37 @@ object Test2: usingLogFile { f => later4 = Cell(() => f.write(0)) } later4.x() // error +object Test3: + + def usingLogFile[T](op: ({*} FileOutputStream) => T) = + val logFile = FileOutputStream("log") + val result = op(logFile) + logFile.close() + result + + val later = usingLogFile { f => () => f.write(0) } // error + +object Test4: + class Logger(f: {*} OutputStream): + def log(msg: String): Unit = ??? + + def usingFile[T](name: String, op: ({*} OutputStream) => T): T = + val f = new FileOutputStream(name) + val result = op(f) + f.close() + result + + val xs: List[Int] = ??? + def good = usingFile("out", f => xs.foreach(x => f.write(x))) + def fail = + val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error + later(1) + + + def usingLogger[T](f: {*} OutputStream, op: ({f} Logger) => T): T = + val logger = Logger(f) + op(logger) + + def test = + val later = usingFile("logfile", usingLogger(_, l => () => l.log("test"))) // error + later() diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index cad281110d9c..633b06e71ba2 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -2037,9 +2037,6 @@ Occurrences: [5:4..5:8): List -> scala/package.List. [5:9..5:10): x -> local0 -Synthetics: -[5:4..5:8):List => *.apply[Int] - expect/MatchType.scala ---------------------- From 64fcbcbd135bc2a4f8967aa1804491de9e396c6d Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 30 Apr 2022 12:48:55 +0200 Subject: [PATCH 41/99] Allow references to local class parameters in capture sets Allow references to local class parameters in capture sets of public members. These would have been classified as escaping private references before. --- compiler/src/dotty/tools/dotc/typer/Checking.scala | 14 +++++++++++++- tests/pos-custom-args/captures/pairs.scala | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index af80edec79ce..227d4e9457a9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -599,6 +599,7 @@ object Checking { def checkNoPrivateLeaks(sym: Symbol)(using Context): Type = { class NotPrivate extends TypeMap { var errors: List[() => String] = Nil + var inCaptureSet: Boolean = false def accessBoundary(sym: Symbol): Symbol = if (sym.is(Private) || !sym.owner.isClass) sym.owner @@ -612,12 +613,16 @@ object Checking { * @pre The signature of `sym` refers to `other` */ def isLeaked(other: Symbol) = - other.is(Private, butNot = TypeParam) && { + other.is(Private, butNot = TypeParam) + && { val otherBoundary = other.owner val otherLinkedBoundary = otherBoundary.linkedClass !(symBoundary.isContainedIn(otherBoundary) || otherLinkedBoundary.exists && symBoundary.isContainedIn(otherLinkedBoundary)) } + && !(inCaptureSet && other.isAllOf(LocalParamAccessor)) + // class parameters in capture sets are not treated as leaked since in + // phase -Ycc these are treated as normal vals. def apply(tp: Type): Type = tp match { case tp: NamedType => @@ -652,6 +657,13 @@ object Checking { declaredParents = tp.declaredParents.map(p => transformedParent(apply(p))) ) + case tp @ AnnotatedType(underlying, annot) + if annot.symbol == defn.RetainsAnnot || annot.symbol == defn.RetainsByNameAnnot => + val underlying1 = this(underlying) + inCaptureSet = true + val annot1 = annot.mapWith(this) + inCaptureSet = false + derivedAnnotatedType(tp, underlying1, annot1) case _ => mapOver(tp) } diff --git a/tests/pos-custom-args/captures/pairs.scala b/tests/pos-custom-args/captures/pairs.scala index c4ce3148d762..9c8ec003d28d 100644 --- a/tests/pos-custom-args/captures/pairs.scala +++ b/tests/pos-custom-args/captures/pairs.scala @@ -19,8 +19,8 @@ object Generic: object Monomorphic: class Pair(x: Cap => Unit, y: {*} Cap -> Unit): - def fst = x - def snd = y + def fst: {x} Cap -> Unit = x + def snd: {y} Cap -> Unit = y def test(c: Cap, d: Cap) = def f(x: Cap): Unit = if c == x then () From 94d0d536d73554b9db175b5f305a61861e30d559 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 1 May 2022 18:34:29 +0200 Subject: [PATCH 42/99] Three capture checking fixes 1. Fix nonDependentResultApprox. The previous code would unncessessarily widen covariant occurrences to `*`. 2. Add handling of cases like SingletonType <:< CapturingType. 3. Propagate element additions in a AvoidMapped variable back to source --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 36 ++++++++++++++----- .../dotty/tools/dotc/core/TypeComparer.scala | 13 +++++-- .../src/dotty/tools/dotc/core/Types.scala | 12 +------ tests/neg-custom-args/captures/i15049.scala | 10 ++++++ .../captures/usingLogFile.check | 8 ++--- .../captures/compare-refined.scala | 12 +++++++ 6 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 tests/neg-custom-args/captures/i15049.scala create mode 100644 tests/pos-custom-args/captures/compare-refined.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index fdf71f2433a3..782aad241a72 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -199,6 +199,7 @@ sealed abstract class CaptureSet extends Showable: if isConst then if mapped.isConst && mapped.elems == elems then this else mapped + else if tm.isInstanceOf[TypeOps.AvoidMap] then AvoidMapped(asVar, tm, tm.variance, mapped) else Mapped(asVar, tm, tm.variance, mapped) def substParams(tl: BindingType, to: List[Type])(using Context) = @@ -404,16 +405,23 @@ object CaptureSet: |Stack trace of variable creation:" |${stack.mkString("\n")}""" + protected def handleContraVar(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + if variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) then + report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") + CompareResult.fail(this) + else + CompareResult.OK + + protected def propagateBack(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + CompareResult.OK + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = val added = - if origin eq source then - mapRefs(newElems, tm, variance) - else - if variance <= 0 && !origin.isConst && (origin ne initial) then - report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") - return CompareResult.fail(this) - Const(newElems) - super.addNewElems(added.elems, origin) + if origin eq source then mapRefs(newElems, tm, variance) + else Const(newElems) + handleContraVar(newElems, origin) + .andAlso(super.addNewElems(added.elems, origin)) + .andAlso(propagateBack(newElems, origin)) .andAlso { if added.isConst then CompareResult.OK else if added.asVar.recordDepsState() then { addSub(added); CompareResult.OK } @@ -430,6 +438,16 @@ object CaptureSet: override def toString = s"Mapped$id($source, elems = $elems)" end Mapped + class AvoidMapped private[CaptureSet] + (source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) + extends Mapped(source, tm, variance, initial): + override final def handleContraVar(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + CompareResult.OK + override final def propagateBack(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + if origin ne source then source.tryInclude(newElems, this) + else CompareResult.OK + end AvoidMapped + class BiMapped private[CaptureSet] (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) extends DerivedVar(initialElems): @@ -509,7 +527,7 @@ object CaptureSet: extension (result: Type) def isOK: Boolean = result eq OK def blocking: CaptureSet = result - def show: String = if result.isOK then "OK" else result.toString + def show(using Context): String = if result.isOK then "OK" else i"$result" def andAlso(op: Context ?=> Type)(using Context): Type = if result.isOK then op else result class VarState: diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index a852f06503c1..6de3c761949a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -797,8 +797,17 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } compareTypeBounds - case CapturingType(parent2, _, _) => - recur(tp1, parent2) || fourthTry + case CapturingType(parent2, refs2, _) => + def compareCaptured = + val refs1 = tp1.captureSet + try + if refs1.isAlwaysEmpty then recur(tp1, parent2) + else subCaptures(refs1, refs2, frozenConstraint).isOK + && recur(tp1.widen.stripCapturing, parent2) + catch case ex: AssertionError => + println(i"assertion failed while compare captured $tp1 <:< $tp2") + throw ex + compareCaptured || fourthTry case tp2: AnnotatedType if tp2.isRefining => (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) && recur(tp1, tp2.parent) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 355cd746d746..8184b177add8 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3832,17 +3832,7 @@ object Types { case tp @ TermParamRef(`thisLambdaType`, _) => range(defn.NothingType, atVariance(1)(apply(tp.underlying))) case CapturingType(parent, refs, boxed) => - val parent1 = this(parent) - val elems1 = refs.elems.filter { - case tp @ TermParamRef(`thisLambdaType`, _) => false - case _ => true - } - if elems1.size == refs.elems.size then - derivedCapturingType(tp, parent1, refs) - else - range( - CapturingType(parent1, CaptureSet(elems1), boxed), - CapturingType(parent1, CaptureSet.universal, boxed)) + mapOver(tp) case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => val parent1 = mapOver(parent) if ann.symbol == defn.RetainsAnnot || ann.symbol == defn.RetainsByNameAnnot then diff --git a/tests/neg-custom-args/captures/i15049.scala b/tests/neg-custom-args/captures/i15049.scala new file mode 100644 index 000000000000..4e32172c025d --- /dev/null +++ b/tests/neg-custom-args/captures/i15049.scala @@ -0,0 +1,10 @@ +class Session: + def request = "Response" +class Foo: + private val session: {*} Session = new Session + def withSession[T](f: ({*} Session) => T): T = f(session) + +def Test = + val f = new Foo + f.withSession(s => s).request // error + f.withSession[{*} Session](t => t) // error diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index aa1c90f9ea4c..cade29eff6e8 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -28,8 +28,8 @@ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | The expression's type {*} (x$0: Int) -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. --- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:48 ------------------------------------------------------ +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:25 ------------------------------------------------------ 71 | val later = usingFile("logfile", usingLogger(_, l => () => l.log("test"))) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. - | This usually means that a capability persists longer than its allowed lifetime. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. diff --git a/tests/pos-custom-args/captures/compare-refined.scala b/tests/pos-custom-args/captures/compare-refined.scala new file mode 100644 index 000000000000..c60bfee602b3 --- /dev/null +++ b/tests/pos-custom-args/captures/compare-refined.scala @@ -0,0 +1,12 @@ +abstract class LIST[+T]: + def map[U](f: T => U): LIST[U] = ??? + +class C +type Cap = {*} C + +def test(d: Cap) = + val zsc: LIST[{d} Cap -> Unit] = ??? + val a4 = zsc.map[{d} Cap -> Unit]((x: {d} Cap -> Unit) => x) + val a5 = zsc.map[{d} Cap -> Unit](identity[{d} Cap -> Unit]) + val a6 = zsc.map(identity[{d} Cap -> Unit]) + val a7 = zsc.map(identity) From d61a779a3bd7f955b191a986a46a441f9c8e646a Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 2 May 2022 17:48:00 +0200 Subject: [PATCH 43/99] Clean up mapped set handling Avoid the unsoundness in the previously used technique --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 98 ++++++++++++------- compiler/src/dotty/tools/dotc/cc/Setup.scala | 1 + .../dotty/tools/dotc/core/Substituters.scala | 8 +- .../tools/dotc/core/TypeApplications.scala | 1 + .../src/dotty/tools/dotc/core/TypeOps.scala | 8 +- .../src/dotty/tools/dotc/core/Types.scala | 6 +- .../dotty/tools/dotc/reporting/messages.scala | 3 +- .../tools/dotc/typer/CheckCaptures.scala | 4 +- .../tools/dotc/typer/ImportSuggestions.scala | 1 + 9 files changed, 84 insertions(+), 46 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 782aad241a72..ef9db3cf2c5b 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -183,9 +183,26 @@ sealed abstract class CaptureSet extends Showable: else Const(elems.filter(p)) else Filtered(asVar, p) - /** capture set obtained by applying `f` to all elements of the current capture set + /** Capture set obtained by applying `tm` to all elements of the current capture set * and joining the results. If the current capture set is a variable, the same * transformation is applied to all future additions of new elements. + * + * Note: We have a problem how we handle the situation where we have a mapped set + * + * cs2 = tm(cs1) + * + * and then the propagation solver adds a new element `x` to `cs2`. What do we + * know in this case about `cs1`? We can answer this question in a sound way only + * if `tm` is a bijection on capture references or it is idempotent on capture references. + * (see definition in IdempotentCapRefMap). + * If `tm` is a bijection we know that `tm^-1(x)` must be in `cs1`. If `tm` is idempotent + * one possible is solution is that `x` is in `cs1`, which is what we assume in this case. + * That strategy is sound but not complete. + * + * If `tm` is some other map, we don't know how to handle this case. For now, + * we simply refuse to handle other maps. If they do need to be handled, + * `OtherMapped` provides some approximation to a solution, but it is neither + * sound nor complete. */ def map(tm: TypeMap)(using Context): CaptureSet = tm match case tm: BiTypeMap => @@ -194,13 +211,15 @@ sealed abstract class CaptureSet extends Showable: if mappedElems == elems then this else Const(mappedElems) else BiMapped(asVar, tm, mappedElems) + case tm: IdentityCaptRefMap => this case _ => val mapped = mapRefs(elems, tm, tm.variance) if isConst then if mapped.isConst && mapped.elems == elems then this else mapped - else if tm.isInstanceOf[TypeOps.AvoidMap] then AvoidMapped(asVar, tm, tm.variance, mapped) - else Mapped(asVar, tm, tm.variance, mapped) + else tm match + case tm: IdempotentCaptRefMap => Mapped(asVar, tm, tm.variance, mapped) + case _ if false => OtherMapped(asVar, tm, tm.variance, mapped) def substParams(tl: BindingType, to: List[Type])(using Context) = map(Substituters.SubstParamsMap(tl, to)) @@ -391,7 +410,7 @@ object CaptureSet: end DerivedVar /** A variable that changes when `source` changes, where all additional new elements are mapped - * using ∪ { f(x) | x <- elems } + * using ∪ { f(x) | x <- elems }. */ class Mapped private[CaptureSet] (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) @@ -399,29 +418,16 @@ object CaptureSet: addSub(initial) val stack = if debugSets then (new Throwable).getStackTrace().nn.take(20) else null - private def whereCreated(using Context): String = - if stack == null then "" - else i""" - |Stack trace of variable creation:" - |${stack.mkString("\n")}""" - - protected def handleContraVar(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - if variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) then - report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") - CompareResult.fail(this) - else - CompareResult.OK - - protected def propagateBack(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - CompareResult.OK - override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + addNewElemsImpl(newElems: Refs, origin: CaptureSet) + .andAlso(if origin ne source then source.tryInclude(newElems, this) else CompareResult.OK) + // `tm` is assumed idempotent, propagate back elems from image set. + + protected def addNewElemsImpl(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = val added = if origin eq source then mapRefs(newElems, tm, variance) else Const(newElems) - handleContraVar(newElems, origin) - .andAlso(super.addNewElems(added.elems, origin)) - .andAlso(propagateBack(newElems, origin)) + super.addNewElems(added.elems, origin) .andAlso { if added.isConst then CompareResult.OK else if added.asVar.recordDepsState() then { addSub(added); CompareResult.OK } @@ -438,17 +444,34 @@ object CaptureSet: override def toString = s"Mapped$id($source, elems = $elems)" end Mapped - class AvoidMapped private[CaptureSet] + + /** Should be avoided as much as possible: + * A mapped set that uses neither a BiTypeMap nor an idempotent type map. + * In that case there's no much we can do. + * The patch does not propagate added elements back to source and rejects adding + * elements from variable sources in contra- and non-variant positions. In essence, + * we approximate types resulting from such maps by returning a possible super type + * from the actual type. + */ + final class OtherMapped private[CaptureSet] (source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) extends Mapped(source, tm, variance, initial): - override final def handleContraVar(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - CompareResult.OK - override final def propagateBack(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - if origin ne source then source.tryInclude(newElems, this) - else CompareResult.OK - end AvoidMapped - - class BiMapped private[CaptureSet] + + private def whereCreated(using Context): String = + if stack == null then "" + else i""" + |Stack trace of variable creation:" + |${stack.mkString("\n")}""" + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + if variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) then + report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") + CompareResult.fail(this) + else + addNewElemsImpl(newElems, origin) + end OtherMapped + + final class BiMapped private[CaptureSet] (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) extends DerivedVar(initialElems): @@ -517,6 +540,15 @@ object CaptureSet: case _ => false + /** A TypeMap with the property that every capture reference in the image + * of the map is mapped to itself. I.e. for all capture references r1, r2, + * if M(r1) == r2 then M(r2) == r2. + */ + trait IdempotentCaptRefMap extends TypeMap + + /** A TypeMap that is the identity on capture references */ + trait IdentityCaptRefMap extends TypeMap + type CompareResult = CompareResult.Type /** None = ok, Some(cs) = failure since not a subset of cs */ @@ -528,7 +560,7 @@ object CaptureSet: def isOK: Boolean = result eq OK def blocking: CaptureSet = result def show(using Context): String = if result.isOK then "OK" else i"$result" - def andAlso(op: Context ?=> Type)(using Context): Type = if result.isOK then op else result + inline def andAlso(op: Context ?=> Type)(using Context): Type = if result.isOK then op else result class VarState: private val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 15f530bdfc69..f1b3307b1923 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -9,6 +9,7 @@ import Types.*, StdNames.* import config.Printers.capt import ast.tpd import transform.Recheck.* +import CaptureSet.IdentityCaptRefMap class Setup( preRecheckPhase: DenotTransformer, diff --git a/compiler/src/dotty/tools/dotc/core/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index 953893826039..afabde7f2466 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -1,6 +1,8 @@ -package dotty.tools.dotc.core +package dotty.tools.dotc +package core import Types._, Symbols._, Contexts._ +import cc.CaptureSet.IdempotentCaptRefMap /** Substitution operations on types. See the corresponding `subst` and * `substThis` methods on class Type for an explanation. @@ -191,11 +193,11 @@ object Substituters: def apply(tp: Type): Type = substRecThis(tp, from, to, this)(using mapCtx) } - final class SubstParamMap(from: ParamRef, to: Type)(using Context) extends DeepTypeMap { + final class SubstParamMap(from: ParamRef, to: Type)(using Context) extends DeepTypeMap, IdempotentCaptRefMap { def apply(tp: Type): Type = substParam(tp, from, to, this)(using mapCtx) } - final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends DeepTypeMap { + final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends DeepTypeMap, IdempotentCaptRefMap { def apply(tp: Type): Type = substParams(tp, from, to, this)(using mapCtx) } diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index c09c79a229fb..e3daa64e61ba 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -11,6 +11,7 @@ import util.Stats._ import Names._ import Flags.{Module, Provisional} import dotty.tools.dotc.config.Config +import cc.CaptureSet.IdentityCaptRefMap object TypeApplications { diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index a1eac2b724fa..bad66c200d8d 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -19,7 +19,7 @@ import typer.Inferencing._ import typer.IfBottom import reporting.TestingReporter import cc.{CapturingType, derivedCapturingType, CaptureSet} -import CaptureSet.CompareResult +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -56,7 +56,7 @@ object TypeOps: } /** The TypeMap handling the asSeenFrom */ - class AsSeenFromMap(pre: Type, cls: Symbol)(using Context) extends ApproximatingTypeMap { + class AsSeenFromMap(pre: Type, cls: Symbol)(using Context) extends ApproximatingTypeMap, IdempotentCaptRefMap { /** Set to true when the result of `apply` was approximated to avoid an unstable prefix. */ var approximated: Boolean = false @@ -201,7 +201,7 @@ object TypeOps: } } - class SimplifyMap(using Context) extends TypeMap { + class SimplifyMap(using Context) extends IdentityCaptRefMap { def apply(tp: Type): Type = simplify(tp, this) } @@ -435,7 +435,7 @@ object TypeOps: } /** An approximating map that drops NamedTypes matching `toAvoid` and wildcard types. */ - abstract class AvoidMap(using Context) extends AvoidWildcardsMap: + abstract class AvoidMap(using Context) extends AvoidWildcardsMap, IdempotentCaptRefMap: @threadUnsafe lazy val localParamRefs = util.HashSet[Type]() def toAvoid(tp: NamedType): Boolean diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 8184b177add8..107131b17790 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -37,7 +37,7 @@ import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, CapturingKind, EventuallyCapturingType} -import CaptureSet.CompareResult +import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -3713,7 +3713,7 @@ object Types { override def resultType(using Context): Type = if (dependencyStatus == FalseDeps) { // dealias all false dependencies - val dealiasMap = new TypeMap { + val dealiasMap = new TypeMap with IdentityCaptRefMap { def apply(tp: Type) = tp match { case tp @ TypeRef(pre, _) => tp.info match { @@ -3827,7 +3827,7 @@ object Types { /** The least supertype of `resultType` that does not contain parameter dependencies */ def nonDependentResultApprox(using Context): Type = if isResultDependent then - val dropDependencies = new ApproximatingTypeMap { + val dropDependencies = new ApproximatingTypeMap with IdempotentCaptRefMap { def apply(tp: Type) = tp match { case tp @ TermParamRef(`thisLambdaType`, _) => range(defn.NothingType, atVariance(1)(apply(tp.underlying))) diff --git a/compiler/src/dotty/tools/dotc/reporting/messages.scala b/compiler/src/dotty/tools/dotc/reporting/messages.scala index 9d921bc9aa37..a3af4c1b2582 100644 --- a/compiler/src/dotty/tools/dotc/reporting/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/messages.scala @@ -25,6 +25,7 @@ import ast.Trees._ import ast.untpd import ast.tpd import transform.SymUtils._ +import cc.CaptureSet.IdentityCaptRefMap /** Messages * ======== @@ -250,7 +251,7 @@ import transform.SymUtils._ // the type mismatch on the bounds instead of the original TypeParamRefs, since // these are usually easier to analyze. We exclude F-bounds since these would // lead to a recursive infinite expansion. - object reported extends TypeMap: + object reported extends TypeMap, IdentityCaptRefMap: def setVariance(v: Int) = variance = v val constraint = mapCtx.typerState.constraint var fbounded = false diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 6bd3792cc091..74248d755c5c 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -15,7 +15,7 @@ import transform.SymUtils.* import transform.{Recheck, PreRecheck} import Recheck.* import scala.collection.mutable -import CaptureSet.withCaptureSetsExplained +import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap} import StdNames.nme import reporting.trace @@ -40,7 +40,7 @@ object CheckCaptures: def isOpen = !captured.isAlwaysEmpty && !isBoxed final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) - extends ApproximatingTypeMap: + extends ApproximatingTypeMap, IdempotentCaptRefMap: def apply(tp: Type): Type = tp match case tp: ParamRef => if tp.binder == from then to(tp.paramNum) else tp diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 88e8568dfcb9..48004481ba46 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -319,6 +319,7 @@ trait ImportSuggestions: * If there's nothing to suggest, an empty string is returned. */ override def importSuggestionAddendum(pt: Type)(using Context): String = + if ctx.phase == Phases.checkCapturesPhase then return "" val (fullMatches, headMatches) = importSuggestions(pt)(using ctx.fresh.setExploreTyperState()) implicits.println(i"suggestions for $pt in ${ctx.owner} = ($fullMatches%, %, $headMatches%, %)") From ed69c362d5a87306650f274b3ccffb0887158afe Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 3 May 2022 13:16:05 +0200 Subject: [PATCH 44/99] Address review comments --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 6 +++++- compiler/src/dotty/tools/dotc/config/Config.scala | 6 ++++++ tests/neg-custom-args/captures/unbox.scala | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/neg-custom-args/captures/unbox.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index ef9db3cf2c5b..c68c46da4352 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -15,6 +15,7 @@ import printing.Texts.* import util.{SimpleIdentitySet, Property} import util.common.alwaysTrue import scala.collection.mutable +import config.Config.ccAllowUnsoundMaps /** A class for capture sets. Capture sets can be constants or variables. * Capture sets support inclusion constraints <:< where <:< is subcapturing. @@ -196,7 +197,7 @@ sealed abstract class CaptureSet extends Showable: * if `tm` is a bijection on capture references or it is idempotent on capture references. * (see definition in IdempotentCapRefMap). * If `tm` is a bijection we know that `tm^-1(x)` must be in `cs1`. If `tm` is idempotent - * one possible is solution is that `x` is in `cs1`, which is what we assume in this case. + * one possible solution is that `x` is in `cs1`, which is what we assume in this case. * That strategy is sound but not complete. * * If `tm` is some other map, we don't know how to handle this case. For now, @@ -422,6 +423,9 @@ object CaptureSet: addNewElemsImpl(newElems: Refs, origin: CaptureSet) .andAlso(if origin ne source then source.tryInclude(newElems, this) else CompareResult.OK) // `tm` is assumed idempotent, propagate back elems from image set. + // This is sound, since we know that for `r in newElems: tm(r) = r`, hence + // `r` is _one_ possible solition in `source` that would make an `r` appear in this set. + // It's not necessarily the only possible solultion, so the scheme is incomplete. protected def addNewElemsImpl(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = val added = diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index f000cfae6649..1b0fea9184d1 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -239,4 +239,10 @@ object Config { * If false, print them in the form `T @retains(c)`. */ inline val printCaptureSetsAsPrefix = true + + /** If true, allow mappping capture set variables under -Ycc with maps that are neither + * bijective nor idempotent. We currently do now know how to do this correctly in all + * cases, though. + */ + inline val ccAllowUnsoundMaps = false } diff --git a/tests/neg-custom-args/captures/unbox.scala b/tests/neg-custom-args/captures/unbox.scala new file mode 100644 index 000000000000..c615cf1d9176 --- /dev/null +++ b/tests/neg-custom-args/captures/unbox.scala @@ -0,0 +1,5 @@ +type Proc = {*} () => Unit + +val xs: List[Proc] = ??? + +val x = xs.head // error From bfd5b20b928e36e9312034b70e928999ab7f0f44 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 3 May 2022 14:50:10 +0200 Subject: [PATCH 45/99] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Lhoták --- compiler/src/dotty/tools/dotc/cc/CaptureSet.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index c68c46da4352..d69cd57b7d4c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -220,7 +220,7 @@ sealed abstract class CaptureSet extends Showable: else mapped else tm match case tm: IdempotentCaptRefMap => Mapped(asVar, tm, tm.variance, mapped) - case _ if false => OtherMapped(asVar, tm, tm.variance, mapped) + case _ if ccAllowUnsoundMaps => OtherMapped(asVar, tm, tm.variance, mapped) def substParams(tl: BindingType, to: List[Type])(using Context) = map(Substituters.SubstParamsMap(tl, to)) @@ -424,7 +424,7 @@ object CaptureSet: .andAlso(if origin ne source then source.tryInclude(newElems, this) else CompareResult.OK) // `tm` is assumed idempotent, propagate back elems from image set. // This is sound, since we know that for `r in newElems: tm(r) = r`, hence - // `r` is _one_ possible solition in `source` that would make an `r` appear in this set. + // `r` is _one_ possible solution in `source` that would make an `r` appear in this set. // It's not necessarily the only possible solultion, so the scheme is incomplete. protected def addNewElemsImpl(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = From f00dc5d33d449d28c96d58abcf79f64619e818a7 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 6 May 2022 20:34:13 +0200 Subject: [PATCH 46/99] Fix locility condition for checking inferred types Fixes #15116 --- .../tools/dotc/typer/CheckCaptures.scala | 7 ++-- tests/neg-custom-args/captures/i15116.check | 14 ++++++++ tests/neg-custom-args/captures/i15116.scala | 5 +++ .../captures/lazylists-exceptions.check | 2 +- .../captures/lazylists-exceptions.scala | 2 +- tests/neg-custom-args/captures/lazyref.scala | 2 +- .../captures/usingLogFile.scala | 2 +- .../captures/capt-capability.scala | 3 +- tests/pos-custom-args/captures/capt1.scala | 2 +- tests/pos-custom-args/captures/capt2.scala | 2 +- tests/pos-custom-args/captures/cc-this.scala | 2 +- .../captures/curried-shorthands.scala | 33 ++++++++++--------- .../captures/lazylists-exceptions.scala | 2 +- tests/pos-custom-args/captures/lazyref.scala | 2 +- tests/pos-custom-args/captures/logger.scala | 1 - 15 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 tests/neg-custom-args/captures/i15116.check create mode 100644 tests/neg-custom-args/captures/i15116.scala diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 74248d755c5c..fb402e5cf576 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -2,7 +2,7 @@ package dotty.tools package dotc package cc -import core._ +import core.* import Phases.*, DenotTransformers.*, SymDenotations.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* import Types.*, StdNames.* @@ -536,10 +536,11 @@ class CheckCaptures extends Recheck, SymTransformer: checkWellformedPost(annot.tree) case _ => } - case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] => + case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] + && !t.symbol.is(Synthetic) => // !!! needs to be refined val sym = t.symbol val isLocal = - sym.ownersIterator.exists(_.isTerm) + sym.owner.ownersIterator.exists(_.isTerm) || sym.accessBoundary(defn.RootClass).isContainedIn(sym.topLevelClass) // The following classes of definitions need explicit capture types ... diff --git a/tests/neg-custom-args/captures/i15116.check b/tests/neg-custom-args/captures/i15116.check new file mode 100644 index 000000000000..1c50fd6d1cc9 --- /dev/null +++ b/tests/neg-custom-args/captures/i15116.check @@ -0,0 +1,14 @@ +-- Error: tests/neg-custom-args/captures/i15116.scala:3:6 -------------------------------------------------------------- +3 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Bar.this.m} Foo{m: {Bar.this.m} String} + | with non-empty capture set {Bar.this.m}. + | The type needs to be declared explicitly. +-- Error: tests/neg-custom-args/captures/i15116.scala:5:6 -------------------------------------------------------------- +5 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Baz.this.m} Foo{m: {Baz.this.m} String} + | with non-empty capture set {Baz.this.m}. + | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/i15116.scala b/tests/neg-custom-args/captures/i15116.scala new file mode 100644 index 000000000000..fa8286961ccd --- /dev/null +++ b/tests/neg-custom-args/captures/i15116.scala @@ -0,0 +1,5 @@ +class Foo(m: {*} String) +class Bar(val m: {*} String): + val x = Foo(m) // error +trait Baz(val m: {*} String): + val x = Foo(m) // error diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.check b/tests/neg-custom-args/captures/lazylists-exceptions.check index 68347b7c3ab1..bd6fad047fe9 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.check +++ b/tests/neg-custom-args/captures/lazylists-exceptions.check @@ -1,7 +1,7 @@ -- Error: tests/neg-custom-args/captures/lazylists-exceptions.scala:36:2 ----------------------------------------------- 36 | try // error | ^ - | The expression's type {*} LazyList[? Int] is not allowed to capture the root capability `*`. + | The expression's type {*} LazyList[Int] is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. 37 | tabulate(10) { i => 38 | if i > 9 then throw Ex1() diff --git a/tests/neg-custom-args/captures/lazylists-exceptions.scala b/tests/neg-custom-args/captures/lazylists-exceptions.scala index 6d325abcf936..6cba934d61e8 100644 --- a/tests/neg-custom-args/captures/lazylists-exceptions.scala +++ b/tests/neg-custom-args/captures/lazylists-exceptions.scala @@ -24,7 +24,7 @@ extension [A](x: A) def #:(xs1: => {*} LazyList[A]): {xs1} LazyList[A] = LazyCons(x, () => xs1) -def tabulate[A](n: Int)(gen: Int => A) = +def tabulate[A](n: Int)(gen: Int => A): {gen} LazyList[A] = def recur(i: Int): {gen} LazyList[A] = if i == n then LazyNil else gen(i) #: recur(i + 1) diff --git a/tests/neg-custom-args/captures/lazyref.scala b/tests/neg-custom-args/captures/lazyref.scala index 2b278fd51a43..8395e5cb42cd 100644 --- a/tests/neg-custom-args/captures/lazyref.scala +++ b/tests/neg-custom-args/captures/lazyref.scala @@ -2,7 +2,7 @@ class CC type Cap = {*} CC class LazyRef[T](val elem: () => T): - val get = elem + val get: () => T = elem def map[U](f: T => U): {f, this} LazyRef[U] = new LazyRef(() => f(elem())) diff --git a/tests/neg-custom-args/captures/usingLogFile.scala b/tests/neg-custom-args/captures/usingLogFile.scala index 20426c43779a..fb35b673d46e 100644 --- a/tests/neg-custom-args/captures/usingLogFile.scala +++ b/tests/neg-custom-args/captures/usingLogFile.scala @@ -25,7 +25,7 @@ object Test2: class Cell[+T](val x: T) - val later2 = usingLogFile { f => Cell(() => f.write(0)) } + private val later2 = usingLogFile { f => Cell(() => f.write(0)) } later2.x() // error var later3: () => Unit = () => () // error diff --git a/tests/pos-custom-args/captures/capt-capability.scala b/tests/pos-custom-args/captures/capt-capability.scala index 9990542a199d..4dbd6e32f2a4 100644 --- a/tests/pos-custom-args/captures/capt-capability.scala +++ b/tests/pos-custom-args/captures/capt-capability.scala @@ -25,4 +25,5 @@ def foo() = val z2 = if x == null then () => x else () => Cap() - x + val _ = x + diff --git a/tests/pos-custom-args/captures/capt1.scala b/tests/pos-custom-args/captures/capt1.scala index e8e217435f96..cc39790623d4 100644 --- a/tests/pos-custom-args/captures/capt1.scala +++ b/tests/pos-custom-args/captures/capt1.scala @@ -13,7 +13,7 @@ def f3: Int = val x = g.apply(true) x -def foo() = +def foo(): {*} C = val x: {*} C = ??? val y: {x} C = x val x2: {x} () -> C = ??? diff --git a/tests/pos-custom-args/captures/capt2.scala b/tests/pos-custom-args/captures/capt2.scala index 11bb2d5eb7b5..ac0b085d1253 100644 --- a/tests/pos-custom-args/captures/capt2.scala +++ b/tests/pos-custom-args/captures/capt2.scala @@ -15,6 +15,6 @@ def test2() = z2: (() -> Unit) @retains(y) val p: {*} () -> String = () => "abc" val q: {p} C = ??? - p: ({p} () -> String) + val _ = p: ({p} () -> String) diff --git a/tests/pos-custom-args/captures/cc-this.scala b/tests/pos-custom-args/captures/cc-this.scala index a8e58e7ee4a8..77414fa9b8c0 100644 --- a/tests/pos-custom-args/captures/cc-this.scala +++ b/tests/pos-custom-args/captures/cc-this.scala @@ -14,4 +14,4 @@ def test(using Cap) = def c1 = new C(f) def c2 = c1 def c3 = c2.y - c3: {*} C + val _ = c3: {*} C diff --git a/tests/pos-custom-args/captures/curried-shorthands.scala b/tests/pos-custom-args/captures/curried-shorthands.scala index 44559b13ae95..7c58729a3041 100644 --- a/tests/pos-custom-args/captures/curried-shorthands.scala +++ b/tests/pos-custom-args/captures/curried-shorthands.scala @@ -1,23 +1,24 @@ -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 +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 -def map3(f: Int => Int)(xs: List[Int]): List[Int] = xs.map(f) -val f2 = map3 -val fc2: (Int => Int) -> List[Int] -> List[Int] = f2 + def map3(f: Int => Int)(xs: List[Int]): List[Int] = xs.map(f) + private val f2 = map3 + val fc2: (Int => Int) -> List[Int] -> List[Int] = f2 -val f3 = (f: Int => Int) => - println(f(3)) - (xs: List[Int]) => xs.map(_ + 1) -val f3c: (Int => Int) -> {} List[Int] -> List[Int] = f3 + val f3 = (f: Int => Int) => + println(f(3)) + (xs: List[Int]) => xs.map(_ + 1) + val f3c: (Int => Int) -> {} List[Int] -> List[Int] = f3 -class LL[A]: - def drop(n: Int): {this} LL[A] = ??? + class LL[A]: + def drop(n: Int): {this} LL[A] = ??? -def test(ct: CanThrow[Exception]) = - def xs: {ct} LL[Int] = ??? - val ys = xs.drop(_) - val ysc: Int -> {ct} LL[Int] = ys + def test(ct: CanThrow[Exception]) = + def xs: {ct} LL[Int] = ??? + val ys = xs.drop(_) + val ysc: Int -> {ct} LL[Int] = ys diff --git a/tests/pos-custom-args/captures/lazylists-exceptions.scala b/tests/pos-custom-args/captures/lazylists-exceptions.scala index a02c98b71bb8..2d4ebb245dca 100644 --- a/tests/pos-custom-args/captures/lazylists-exceptions.scala +++ b/tests/pos-custom-args/captures/lazylists-exceptions.scala @@ -48,7 +48,7 @@ extension [A](x: A) def lazyCons[A](x: A, xs1: => {*} LzyList[A]): {xs1} LzyList[A] = LzyCons(x, () => xs1) -def tabulate[A](n: Int)(gen: Int => A) = +def tabulate[A](n: Int)(gen: Int => A): {gen} LzyList[A] = def recur(i: Int): {gen} LzyList[A] = if i == n then LzyNil else gen(i) #: recur(i + 1) diff --git a/tests/pos-custom-args/captures/lazyref.scala b/tests/pos-custom-args/captures/lazyref.scala index 2ab770178a16..0d988dc3e17b 100644 --- a/tests/pos-custom-args/captures/lazyref.scala +++ b/tests/pos-custom-args/captures/lazyref.scala @@ -1,7 +1,7 @@ @annotation.capability class Cap class LazyRef[T](val elem: () => T): - val get = elem + val get: {elem} () -> T = elem def map[U](f: T => U): {f, this} LazyRef[U] = new LazyRef(() => f(elem())) diff --git a/tests/pos-custom-args/captures/logger.scala b/tests/pos-custom-args/captures/logger.scala index 42da5fbe26ef..e5b6c834ffe0 100644 --- a/tests/pos-custom-args/captures/logger.scala +++ b/tests/pos-custom-args/captures/logger.scala @@ -15,7 +15,6 @@ def test(using fs: FileSystem) = l.log(s"computing elem # $i") i * i } - xs trait LazyList[+A]: def isEmpty: Boolean From 8e8b336570da487e2c41936d102dc23dc9433224 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 18 May 2022 14:52:56 +0200 Subject: [PATCH 47/99] Fix rebase breakage --- compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index fb402e5cf576..182086e7808b 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -341,7 +341,7 @@ class CheckCaptures extends Recheck, SymTransformer: end instantiate def recheckByNameArg(tree: Tree, pt: Type)(using Context): Type = - val closureDef(mdef) = tree + val closureDef(mdef) = tree: @unchecked val arg = mdef.rhs val localSet = CaptureSet.Var() curEnv = Env(mdef.symbol, localSet, isBoxed = false, curEnv) From 65ee41a6d83a78b2ffe79db23ede4ee4576f52d9 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 18 May 2022 14:35:35 +0200 Subject: [PATCH 48/99] Explicitly generate capturing types for synthetic methods Synthetic apply and copy methods of case classes as well as copy default getters have inferred types that do not correctly convey capture information before phase CC. To allow for separate compilation, we have to fix the types of these methods in a denotation transformer, and then fix them back after CC. Also: Explicit type for unapply - Tighten condition on ignored definitions - Implement avoidance for return expressions --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 5 +- .../src/dotty/tools/dotc/cc/Synthetics.scala | 161 ++++++++++++++++++ .../dotty/tools/dotc/transform/Recheck.scala | 7 +- .../tools/dotc/typer/CheckCaptures.scala | 26 +-- .../captures/caseclass/Ref_1.scala | 1 + .../captures/caseclass/Test_2.scala | 30 ++++ .../pos-custom-args/captures/caseclass.scala | 7 + 7 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/cc/Synthetics.scala create mode 100644 tests/neg-custom-args/captures/caseclass/Ref_1.scala create mode 100644 tests/neg-custom-args/captures/caseclass/Test_2.scala create mode 100644 tests/pos-custom-args/captures/caseclass.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index f1b3307b1923..ffa6a830d8ad 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -10,6 +10,7 @@ import config.Printers.capt import ast.tpd import transform.Recheck.* import CaptureSet.IdentityCaptRefMap +import Synthetics.isExcluded class Setup( preRecheckPhase: DenotTransformer, @@ -325,8 +326,10 @@ extends tpd.TreeTraverser: then transformInferredType(tree.tpe, boxed) else transformExplicitType(tree.tpe, boxed)) - def traverse(tree: Tree)(using Context) = + def traverse(tree: Tree)(using Context): Unit = tree match + case tree: DefDef if isExcluded(tree.symbol) => + return case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) => transformTT(tpt, boxed = true) traverse(tree.rhs) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala new file mode 100644 index 000000000000..71422d30205b --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -0,0 +1,161 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Symbols.*, SymDenotations.*, Contexts.*, Flags.*, Types.*, Decorators.* +import StdNames.nme +import NameKinds.DefaultGetterName + +/** Classification and transformation methods for synthetic + * case class methods that need to be treated specially. + * In particular, compute capturing types for some of these methods which + * have inferred (result-)types that need to be established under separate + * compilation. + */ +object Synthetics: + def isSyntheticCopyMethod(sym: SymDenotation)(using Context) = + sym.name == nme.copy && sym.is(Synthetic) && sym.owner.isClass && sym.owner.is(Case) + + def isSyntheticApplyMethod(sym: SymDenotation)(using Context) = + sym.name == nme.apply && sym.is(Synthetic) && sym.owner.is(Module) && sym.owner.companionClass.is(Case) + + def isSyntheticUnapplyMethod(sym: SymDenotation)(using Context) = + sym.name == nme.unapply && sym.is(Synthetic) && sym.owner.is(Module) && sym.owner.companionClass.is(Case) + + def isSyntheticCopyDefaultGetterMethod(sym: SymDenotation)(using Context) = sym.name match + case DefaultGetterName(nme.copy, _) => sym.is(Synthetic) + case _ => false + + /** Is `sym` a synthetic apply, copy, or copy default getter method? */ + def needsTransform(sym: SymDenotation)(using Context): Boolean = + isSyntheticCopyMethod(sym) + || isSyntheticApplyMethod(sym) + || isSyntheticUnapplyMethod(sym) + || isSyntheticCopyDefaultGetterMethod(sym) + + /** Method is excluded from regular capture checking */ + def isExcluded(sym: Symbol)(using Context): Boolean = + sym.is(Synthetic) + && sym.owner.isClass + && ( defn.caseClassSynthesized.exists( + ccsym => sym.overriddenSymbol(ccsym.owner.asClass) == ccsym) + || sym.name == nme.fromProduct + || needsTransform(sym) + ) + + /** Add capture dependencies to the type of `apply` or `copy` method of a case class */ + private def addCaptureDeps(info: Type)(using Context): Type = info match + case info: MethodType => + val trackedParams = info.paramRefs.filter(atPhase(ctx.phase.next)(_.isTracked)) + def augmentResult(tp: Type): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = augmentResult(tp.resType)) + case _ => + val refined = trackedParams.foldLeft(tp) { (parent, pref) => + RefinedType(parent, pref.paramName, + CapturingType( + atPhase(ctx.phase.next)(pref.underlying.stripCapturing), + CaptureSet(pref), CapturingKind.Regular)) + } + CapturingType(refined, CaptureSet(trackedParams*), CapturingKind.Regular) + if trackedParams.isEmpty then info else augmentResult(info) + case info: PolyType => + info.derivedLambdaType(resType = addCaptureDeps(info.resType)) + case _ => + info + + /** Drop capture dependencies from the type of `apply` or `copy` method of a case class */ + private def dropCaptureDeps(tp: Type)(using Context): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = dropCaptureDeps(tp.resType)) + case CapturingType(parent, _, _) => + dropCaptureDeps(parent) + case RefinedType(parent, _, _) => + dropCaptureDeps(parent) + case _ => + tp + + /** Add capture information to the type of the default getter of a case class copy method */ + private def addDefaultGetterCapture(info: Type, owner: Symbol, idx: Int)(using Context): Type = info match + case info: MethodOrPoly => + info.derivedLambdaType(resType = addDefaultGetterCapture(info.resType, owner, idx)) + case info: ExprType => + info.derivedExprType(addDefaultGetterCapture(info.resType, owner, idx)) + case EventuallyCapturingType(parent, _, _) => + addDefaultGetterCapture(parent, owner, idx) + case info @ AnnotatedType(parent, annot) => + info.derivedAnnotatedType(addDefaultGetterCapture(parent, owner, idx), annot) + case _ if idx < owner.asClass.paramGetters.length => + val param = owner.asClass.paramGetters(idx) + val pinfo = param.info + atPhase(ctx.phase.next) { + if pinfo.captureSet.isAlwaysEmpty then info + else CapturingType(pinfo.stripCapturing, CaptureSet(param.termRef), CapturingKind.Regular) + } + case _ => + info + + /** Drop capture information from the type of the default getter of a case class copy method */ + private def dropDefaultGetterCapture(info: Type)(using Context): Type = info match + case info: MethodOrPoly => + info.derivedLambdaType(resType = dropDefaultGetterCapture(info.resType)) + case CapturingType(parent, _, _) => + parent + case info @ AnnotatedType(parent, annot) => + info.derivedAnnotatedType(dropDefaultGetterCapture(parent), annot) + case _ => + info + + private def addUnapplyCaptures(info: Type)(using Context): Type = info match + case info: MethodType => + val paramInfo :: Nil = info.paramInfos: @unchecked + val newParamInfo = + CapturingType(paramInfo, CaptureSet.universal, CapturingKind.Regular) + val trackedParam = info.paramRefs.head + def newResult(tp: Type): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = newResult(tp.resType)) + case _ => + CapturingType(tp, CaptureSet(trackedParam), CapturingKind.Regular) + info.derivedLambdaType(paramInfos = newParamInfo :: Nil, resType = newResult(info.resType)) + case info: PolyType => + info.derivedLambdaType(resType = addUnapplyCaptures(info.resType)) + + private def dropUnapplyCaptures(info: Type)(using Context): Type = info match + case info: MethodType => + val CapturingType(oldParamInfo, _, _) :: Nil = info.paramInfos: @unchecked + def oldResult(tp: Type): Type = tp match + case tp: MethodOrPoly => + tp.derivedLambdaType(resType = oldResult(tp.resType)) + case CapturingType(tp, _, _) => + tp + info.derivedLambdaType(paramInfos = oldParamInfo :: Nil, resType = oldResult(info.resType)) + case info: PolyType => + info.derivedLambdaType(resType = dropUnapplyCaptures(info.resType)) + + /** If `sym` refers to a synthetic apply, copy, or copy default getter method + * of a case class, transform it to account for capture information. + * @pre needsTransform(sym) + */ + def transformToCC(sym: SymDenotation)(using Context): SymDenotation = sym.name match + case DefaultGetterName(nme.copy, n) if sym.is(Synthetic) && sym.owner.is(Case) => + sym.copySymDenotation(info = addDefaultGetterCapture(sym.info, sym.owner, n)) + case nme.unapply => + sym.copySymDenotation(info = addUnapplyCaptures(sym.info)) + case _ => + sym.copySymDenotation(info = addCaptureDeps(sym.info)) + + /** If `sym` refers to a synthetic apply, copy, or copy default getter method + * of a case class, transform it back to what it was before the CC phase. + * @pre needsTransform(sym) + */ + def transformFromCC(sym: SymDenotation)(using Context): SymDenotation = + if isSyntheticCopyDefaultGetterMethod(sym) then + sym.copySymDenotation(info = dropDefaultGetterCapture(sym.info)) + else if sym.name == nme.unapply then + sym.copySymDenotation(info = dropUnapplyCaptures(sym.info)) + else + sym.copySymDenotation(info = dropCaptureDeps(sym.info)) + +end Synthetics \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index eb62c4ad65f4..a8b113be214e 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -254,7 +254,12 @@ abstract class Recheck extends Phase, SymTransformer: recheck(tree.body, pt) def recheckReturn(tree: Return)(using Context): Type = - recheck(tree.expr, tree.from.symbol.returnProto) + val rawType = recheck(tree.expr) + def avoidMap = new TypeOps.AvoidMap: + def toAvoid(tp: NamedType) = + tp.symbol.is(Case) && tp.symbol.owner.isContainedIn(ctx.owner) + val ownType = avoidMap(rawType) + checkConforms(ownType, tree.from.symbol.returnProto, tree) defn.NothingType def recheckWhileDo(tree: WhileDo)(using Context): Type = diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 182086e7808b..57d62f11d568 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -30,9 +30,12 @@ object CheckCaptures: * in Setup if they have non-empty capture sets */ def transformSym(sym: SymDenotation)(using Context): SymDenotation = - if sym.isAllOf(PrivateParamAccessor) && !sym.hasAnnotation(defn.ConstructorOnlyAnnot) - then sym.copySymDenotation(initFlags = sym.flags &~ Private | Recheck.ResetPrivate) - else sym + if sym.isAllOf(PrivateParamAccessor) && !sym.hasAnnotation(defn.ConstructorOnlyAnnot) then + sym.copySymDenotation(initFlags = sym.flags &~ Private | Recheck.ResetPrivate) + else if Synthetics.needsTransform(sym) then + Synthetics.transformToCC(sym) + else + sym end Pre case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer0: Env | Null): @@ -106,6 +109,10 @@ class CheckCaptures extends Recheck, SymTransformer: checkOverrides.traverse(ctx.compilationUnit.tpdTree) super.run + override def transformSym(sym: SymDenotation)(using Context): SymDenotation = + if Synthetics.needsTransform(sym) then Synthetics.transformFromCC(sym) + else super.transformSym(sym) + def checkOverrides = new TreeTraverser: def traverse(t: Tree)(using Context) = t match @@ -261,14 +268,7 @@ class CheckCaptures extends Recheck, SymTransformer: interpolateVarsIn(tree.tpt) override def recheckDefDef(tree: DefDef, sym: Symbol)(using Context): Unit = - val isExcluded = - sym.is(Synthetic) - && sym.owner.isClass - && ( defn.caseClassSynthesized.exists( - ccsym => sym.overriddenSymbol(ccsym.owner.asClass) == ccsym) - || sym.name == nme.fromProduct - ) - if !isExcluded then + if !Synthetics.isExcluded(sym) then val saved = curEnv val localSet = capturedVars(sym) if !localSet.isAlwaysEmpty then curEnv = Env(sym, localSet, false, curEnv) @@ -536,8 +536,8 @@ class CheckCaptures extends Recheck, SymTransformer: checkWellformedPost(annot.tree) case _ => } - case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] - && !t.symbol.is(Synthetic) => // !!! needs to be refined + case t: ValOrDefDef + if t.tpt.isInstanceOf[InferredTypeTree] && !Synthetics.isExcluded(t.symbol) => val sym = t.symbol val isLocal = sym.owner.ownersIterator.exists(_.isTerm) diff --git a/tests/neg-custom-args/captures/caseclass/Ref_1.scala b/tests/neg-custom-args/captures/caseclass/Ref_1.scala new file mode 100644 index 000000000000..492457b05cd3 --- /dev/null +++ b/tests/neg-custom-args/captures/caseclass/Ref_1.scala @@ -0,0 +1 @@ +case class Ref(x: () => Unit) diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala new file mode 100644 index 000000000000..bba5988923a2 --- /dev/null +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -0,0 +1,30 @@ +@annotation.capability class C +def test(c: C) = + val pure: () -> Unit = () => () + val impure: () => Unit = pure + val mixed: {c} () -> Unit = pure + val x = Ref(impure) + val _: Ref = x // error + val y = x.copy() + val yc: Ref = y // error + val y0 = x.copy(pure) + val yc0: Ref = y0 + + val x2 = Ref(pure) + val _: Ref = x2 + val y2 = x2.copy() + val yc2: Ref = y2 + + val x3 = Ref(mixed) + val _: {c} Ref = x3 + val y3 = x3.copy() + val yc3: {c} Ref = y3 + + val y4 = y3 match + case Ref(xx) => xx + val y4c: {x3} () -> Unit = y4 // error (?) found: (y4 : {*} () -> Unit) required: {x3} () -> Unit. (But in fact it should work) + + + + + diff --git a/tests/pos-custom-args/captures/caseclass.scala b/tests/pos-custom-args/captures/caseclass.scala new file mode 100644 index 000000000000..2d85e612a8ef --- /dev/null +++ b/tests/pos-custom-args/captures/caseclass.scala @@ -0,0 +1,7 @@ +case class Ref(x: {*} String) + +@annotation.capability class C +def test(c: C) = + val x1 = Ref("hello") + val y = x1 match + case Ref(z) => z From 33f9ea946fa830110e3658b9c221be977a88bce3 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 18 May 2022 14:40:36 +0200 Subject: [PATCH 49/99] Better implementation of capture set intersection Note: using `accountsFor` criterion would be too pessimistic and therefore underapproximates elements of intersections, which means soundness would be lost. We now overapproximate with `mightAccountFor`, at a possible loss of completeness. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index d69cd57b7d4c..7724fc0ba253 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -117,6 +117,17 @@ sealed abstract class CaptureSet extends Showable: || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK } + /** A more optimistic version of accountsFor, which does not take variable supersets + * of the `x` reference into account. A set might account for `x` if it accounts + * for `x` in a state where we assume all supersets of `x` have just the elements + * known at this point. + */ + def mightAccountFor(x: CaptureRef)(using ctx: Context): Boolean = + reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { + elems.exists(_.subsumes(x)) + || !x.isRootCapability && x.captureSetOfInfo.elems.forall(mightAccountFor) + } + /** The subcapturing test */ final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) @@ -160,9 +171,8 @@ sealed abstract class CaptureSet extends Showable: def **(that: CaptureSet)(using Context): CaptureSet = 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(elems.intersect(that.elems)) - else if that.isConst then Intersected(this.asVar, that) - else Intersected(that.asVar, this) + else if this.isConst && that.isConst then Const(elemIntersection(this, that)) + else Intersected(this, that) def -- (that: CaptureSet.Const)(using Context): CaptureSet = val elems1 = elems.filter(!that.accountsFor(_)) @@ -516,10 +526,29 @@ object CaptureSet: class Diff(source: Var, other: Const)(using Context) extends Filtered(source, !other.accountsFor(_)) - /** A variable with elements given at any time as { x <- source.elems | other.accountsFor(x) } */ - class Intersected(source: Var, other: CaptureSet)(using Context) - extends Filtered(source, other.accountsFor(_)): - addSub(other) + def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = + cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + + class Intersected(cs1: CaptureSet, cs2: CaptureSet)(using Context) + extends Var(elemIntersection(cs1, cs2)): + addSub(cs1) + addSub(cs2) + deps += cs1 + deps += cs2 + + override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = + super.addNewElems( + if origin eq cs1 then newElems.filter(cs2.accountsFor) + else if origin eq cs2 then newElems.filter(cs1.accountsFor) + else newElems, origin) + + override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = + if (origin eq cs1) || (origin eq cs2) then universal + else CaptureSet(elemIntersection(cs1.upperApprox(this), cs2.upperApprox(this))) + + override def propagateSolved()(using Context) = + if cs1.isConst && cs2.isConst && !isConst then markSolved() + end Intersected def extrapolateCaptureRef(r: CaptureRef, tm: TypeMap, variance: Int)(using Context): CaptureSet = val r1 = tm(r) From c83751d277687c1be0db99909e9661dfefab8b9b Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 18 May 2022 14:42:54 +0200 Subject: [PATCH 50/99] Fix some issues with box handling 1. Make box insertion functional; don't rely on side effects. This causes neg.../bounded.scala to break, which is fixed by the other two points. (bounded.scala already was wrong before if the type variable X was unbounded). 2. Insert boxes for function results. 3. Propagate boxed captures to the left in function types. 4. Drop CapturingKind, use a boolean for `boxed` and factor out by-nameness in a separate mechanism. --- .../tools/dotc/cc/CaptureAnnotation.scala | 20 ++-- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 22 ++--- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 9 +- .../dotty/tools/dotc/cc/CapturingKind.scala | 9 -- .../dotty/tools/dotc/cc/CapturingType.scala | 28 ++---- compiler/src/dotty/tools/dotc/cc/Setup.scala | 95 +++++++++++-------- .../src/dotty/tools/dotc/cc/Synthetics.scala | 20 ++-- .../dotty/tools/dotc/core/Definitions.scala | 8 +- .../tools/dotc/core/OrderingConstraint.scala | 2 +- .../tools/dotc/core/SymDenotations.scala | 2 +- .../dotty/tools/dotc/core/TypeComparer.scala | 14 +-- .../src/dotty/tools/dotc/core/TypeOps.scala | 6 +- .../src/dotty/tools/dotc/core/Types.scala | 29 +++--- .../tools/dotc/printing/PlainPrinter.scala | 11 ++- .../tools/dotc/typer/CheckCaptures.scala | 47 +++++++-- .../dotty/tools/dotc/typer/Inferencing.scala | 2 +- .../dotty/tools/dotc/typer/TypeAssigner.scala | 2 +- tests/neg-custom-args/captures/bounded.scala | 12 +++ 18 files changed, 183 insertions(+), 155 deletions(-) delete mode 100644 compiler/src/dotty/tools/dotc/cc/CapturingKind.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index 773454565b04..7a1f8ca4eb8a 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -12,7 +12,7 @@ import printing.Printer import printing.Texts.Text -case class CaptureAnnotation(refs: CaptureSet, kind: CapturingKind) extends Annotation: +case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) extends Annotation: import CaptureAnnotation.* import tpd.* @@ -25,19 +25,19 @@ case class CaptureAnnotation(refs: CaptureSet, kind: CapturingKind) extends Anno val arg = repeated(elems, TypeTree(defn.AnyType)) New(symbol.typeRef, arg :: Nil) - override def symbol(using Context) = - if kind == CapturingKind.ByName then defn.RetainsByNameAnnot else defn.RetainsAnnot + override def symbol(using Context) = cls override def derivedAnnotation(tree: Tree)(using Context): Annotation = if refs == CaptureSet.universal then this else unsupported("derivedAnnotation(Tree)") - def derivedAnnotation(refs: CaptureSet, kind: CapturingKind)(using Context): Annotation = - if (this.refs eq refs) && (this.kind == kind) then this - else CaptureAnnotation(refs, kind) + def derivedAnnotation(refs: CaptureSet, boxed: Boolean)(using Context): Annotation = + if (this.refs eq refs) && (this.boxed == boxed) then this + else CaptureAnnotation(refs, boxed)(cls) override def sameAnnotation(that: Annotation)(using Context): Boolean = that match - case CaptureAnnotation(refs2, kind2) => refs == refs2 && kind == kind2 + case CaptureAnnotation(refs, boxed) => + this.refs == refs && this.boxed == boxed && this.symbol == that.symbol case _ => false override def mapWith(tp: TypeMap)(using Context) = @@ -45,7 +45,7 @@ case class CaptureAnnotation(refs: CaptureSet, kind: CapturingKind) extends Anno val elems1 = elems.mapConserve(tp) if elems1 eq elems then this else if elems1.forall(_.isInstanceOf[CaptureRef]) - then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), kind) + then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), boxed) else EmptyAnnotation override def refersToParamOf(tl: TermLambda)(using Context): Boolean = @@ -57,10 +57,10 @@ case class CaptureAnnotation(refs: CaptureSet, kind: CapturingKind) extends Anno override def toText(printer: Printer): Text = refs.toText(printer) override def hash: Int = - (refs.hashCode << 1) | (if kind == CapturingKind.Regular then 0 else 1) + (refs.hashCode << 1) | (if boxed then 1 else 0) override def eql(that: Annotation) = that match - case that: CaptureAnnotation => (this.refs eq that.refs) && (this.kind == kind) + case that: CaptureAnnotation => (this.refs eq that.refs) && (this.boxed == that.boxed) case _ => false end CaptureAnnotation diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 23cb802356fc..960453407d10 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -11,7 +11,6 @@ import util.Property.Key import tpd.* private val Captures: Key[CaptureSet] = Key() -private val IsBoxed: Key[Unit] = Key() def retainedElems(tree: Tree)(using Context): List[Tree] = tree match case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems @@ -34,18 +33,12 @@ extension (tree: Tree) tree.putAttachment(Captures, refs) refs - def isBoxedCapturing(using Context): Boolean = - tree.hasAttachment(IsBoxed) - - def setBoxedCapturing()(using Context): Unit = - tree.putAttachment(IsBoxed, ()) - extension (tp: Type) def derivedCapturingType(parent: Type, refs: CaptureSet)(using Context): Type = tp match - case CapturingType(p, r, k) => + case tp @ CapturingType(p, r) => if (parent eq p) && (refs eq r) then tp - else CapturingType(parent, refs, k) + else CapturingType(parent, refs, tp.isBoxed) /** If this is type variable instantiated or upper bounded with a capturing type, * the capture set associated with that type. Extended to and-or types and @@ -54,8 +47,8 @@ extension (tp: Type) */ def boxedCaptured(using Context): CaptureSet = def getBoxed(tp: Type): CaptureSet = tp match - case CapturingType(_, refs, CapturingKind.Boxed) => refs - case CapturingType(_, _, _) => CaptureSet.empty + case tp @ CapturingType(parent, refs) => + if tp.isBoxed then refs else getBoxed(parent) case tp: TypeProxy => getBoxed(tp.superType) case tp: AndType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) case tp: OrType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) @@ -65,7 +58,7 @@ extension (tp: Type) def isBoxedCapturing(using Context) = !tp.boxedCaptured.isAlwaysEmpty def stripCapturing(using Context): Type = tp.dealiasKeepAnnots match - case CapturingType(parent, _, _) => + case CapturingType(parent, _) => parent.stripCapturing case atd @ AnnotatedType(parent, annot) => atd.derivedAnnotatedType(parent.stripCapturing, annot) @@ -109,3 +102,8 @@ extension (sym: Symbol) case _ => false containsEnclTypeParam(sym.info.finalResultType) && !sym.allowsRootCapture + +extension (tp: AnnotatedType) + def isBoxed(using Context): Boolean = tp.annot match + case ann: CaptureAnnotation => ann.boxed + case _ => false diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 7724fc0ba253..3b473fc9c8f9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -257,9 +257,8 @@ sealed abstract class CaptureSet extends Showable: ((NoType: Type) /: elems) ((tp, ref) => if tp.exists then OrType(tp, ref, soft = false) else ref) - def toRegularAnnotation(byName: Boolean)(using Context): Annotation = - val kind = if byName then CapturingKind.ByName else CapturingKind.Regular - Annotation(CaptureAnnotation(this, kind).tree) + def toRegularAnnotation(cls: Symbol)(using Context): Annotation = + Annotation(CaptureAnnotation(this, boxed = false)(cls).tree) override def toText(printer: Printer): Text = Str("{") ~ Text(elems.toList.map(printer.toTextCaptureRef), ", ") ~ Str("}") ~~ description @@ -566,7 +565,7 @@ object CaptureSet: mapRefs(xs, extrapolateCaptureRef(_, tm, variance)) def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match - case TypeBounds(CapturingType(lo, loRefs, _), CapturingType(hi, hiRefs, _)) if lo =:= hi => + case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi => given VarState = VarState() val cs2 = arg2.captureSet hiRefs.subCaptures(cs2).isOK && cs2.subCaptures(loRefs).isOK @@ -662,7 +661,7 @@ object CaptureSet: if tp.classSymbol.hasAnnotation(defn.CapabilityAnnot) then universal else empty case _: TypeParamRef => empty - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => recur(parent) ++ refs case AppliedType(tycon, args) => val cs = recur(tycon) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingKind.scala b/compiler/src/dotty/tools/dotc/cc/CapturingKind.scala deleted file mode 100644 index 3bb00b110b21..000000000000 --- a/compiler/src/dotty/tools/dotc/cc/CapturingKind.scala +++ /dev/null @@ -1,9 +0,0 @@ -package dotty.tools -package dotc -package cc - -/** Possible kinds of captures */ -enum CapturingKind: - case Regular // normal capture - case Boxed // capture under box - case ByName // capture applies to enclosing by-name type (only possible before ElimByName) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index d19850b72e4f..19fe9728780c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -7,22 +7,16 @@ import Types.*, Symbols.*, Contexts.* /** A capturing type. This is internally represented as an annotated type with a `retains` * annotation, but the extractor will succeed only at phase CheckCaptures. - * Annotated types with `@retainsByName` annotation can also be created that way, by - * giving a `CapturingKind.ByName` as `kind` argument, but they are never extracted, - * since they have already been converted to regular capturing types before CheckCaptures. */ object CapturingType: - def apply(parent: Type, refs: CaptureSet, kind: CapturingKind)(using Context): Type = + def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type = if refs.isAlwaysEmpty then parent - else AnnotatedType(parent, CaptureAnnotation(refs, kind)) - - def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, CapturingKind)] = - if ctx.phase == Phases.checkCapturesPhase then - val r = EventuallyCapturingType.unapply(tp) - r match - case Some((_, _, CapturingKind.ByName)) => None - case _ => r + else AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) + + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = + if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then + EventuallyCapturingType.unapply(tp) else None end CapturingType @@ -33,18 +27,14 @@ end CapturingType */ object EventuallyCapturingType: - def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet, CapturingKind)] = + def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = val sym = tp.annot.symbol if sym == defn.RetainsAnnot || sym == defn.RetainsByNameAnnot then tp.annot match case ann: CaptureAnnotation => - Some((tp.parent, ann.refs, ann.kind)) + Some((tp.parent, ann.refs)) case ann => - val kind = - if ann.tree.isBoxedCapturing then CapturingKind.Boxed - else if sym == defn.RetainsByNameAnnot then CapturingKind.ByName - else CapturingKind.Regular - try Some((tp.parent, ann.tree.toCaptureSet, kind)) + try Some((tp.parent, ann.tree.toCaptureSet)) catch case ex: IllegalCaptureRef => None else None diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ffa6a830d8ad..0b278893d094 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -26,25 +26,36 @@ extends tpd.TreeTraverser: )(argTypes, resType) .toFunctionType(isJava = false, alwaysDependent = true) - private def box(tp: Type)(using Context): Type = tp match - case CapturingType(parent, refs, CapturingKind.Regular) => - CapturingType(parent, refs, CapturingKind.Boxed) + private def box(tp: Type)(using Context): Type = tp.dealias match + case tp @ CapturingType(parent, refs) if !tp.isBoxed => + CapturingType(parent, refs, boxed = true) + case tp1 @ AppliedType(tycon, args) if defn.isNonRefinedFunction(tp1) => + val res = args.last + val boxedRes = box(res) + if boxedRes eq res then tp + else tp1.derivedAppliedType(tycon, args.init :+ boxedRes) + case tp1 @ RefinedType(_, _, rinfo) if defn.isFunctionType(tp1) => + val boxedRinfo = box(rinfo) + if boxedRinfo eq rinfo then tp + else boxedRinfo.toFunctionType(isJava = false, alwaysDependent = true) + case tp1: MethodOrPoly => + val res = tp1.resType + val boxedRes = box(res) + if boxedRes eq res then tp + else tp1.derivedLambdaType(resType = boxedRes) case _ => tp - private def setBoxed(tp: Type)(using Context) = tp match - case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => - annot.tree.setBoxedCapturing() - case _ => - - private def addBoxes(using Context) = new TypeTraverser: - def traverse(t: Type) = - t match - case AppliedType(tycon, args) if !defn.isNonRefinedFunction(t) => - args.foreach(setBoxed) - case TypeBounds(lo, hi) => - setBoxed(lo); setBoxed(hi) - case _ => - traverseChildren(t) + private def addBoxes(using Context) = new TypeMap: + def apply(t: Type) = mapOver(t) match + case t1 @ AppliedType(tycon, args) if !defn.isNonRefinedFunction(t1) => + t1.derivedAppliedType(tycon, args.mapConserve(box)) + case t1 @ TypeBounds(lo, hi) => + t1.derivedTypeBounds(box(lo), box(hi)) + case t1 => + t1 + override def mapCapturingType(tp: Type, parent: Type, refs: CaptureSet, v: Int): Type = + tp.derivedCapturingType(this(parent), refs) + end addBoxes /** Expand some aliases of function types to the underlying functions. * Right now, these are only $throws aliases, but this could be generalized. @@ -103,7 +114,7 @@ extends tpd.TreeTraverser: cls.paramGetters.foldLeft(tp) { (core, getter) => if getter.termRef.isTracked then val getterType = tp.memberInfo(getter).strippedDealias - RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var(), CapturingKind.Regular)) + RefinedType(core, getter.name, CapturingType(getterType, CaptureSet.Var())) .showing(i"add capture refinement $tp --> $result", capt) else core @@ -113,7 +124,7 @@ extends tpd.TreeTraverser: private def superTypeIsImpure(tp: Type): Boolean = { tp.dealias match - case CapturingType(_, refs, _) => + case CapturingType(_, refs) => !refs.isAlwaysEmpty case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol @@ -145,7 +156,7 @@ extends tpd.TreeTraverser: canHaveInferredCapture(tp.tp1) && canHaveInferredCapture(tp.tp2) case tp: OrType => canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2) - case CapturingType(_, refs, _) => + case CapturingType(_, refs) => refs.isConst && !refs.isAlwaysEmpty && !refs.isUniversal case _ => false @@ -155,35 +166,35 @@ extends tpd.TreeTraverser: * an embedded capture set variables from a part of `tp`. */ def addVar(tp: Type) = tp match - case tp @ RefinedType(parent @ CapturingType(parent1, refs, boxed), rname, rinfo) => - CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, boxed) + case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => + CapturingType(tp.derivedRefinedType(parent1, rname, rinfo), refs, parent.isBoxed) case tp: RecType => tp.parent match - case CapturingType(parent1, refs, boxed) => - CapturingType(tp.derivedRecType(parent1), refs, boxed) + case parent @ CapturingType(parent1, refs) => + CapturingType(tp.derivedRecType(parent1), refs, parent.isBoxed) case _ => tp // can return `tp` here since unlike RefinedTypes, RecTypes are never created // by `mapInferred`. Hence if the underlying type admits capture variables // a variable was already added, and the first case above would apply. - case AndType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => + case AndType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => assert(refs1.asVar.elems.isEmpty) assert(refs2.asVar.elems.isEmpty) - assert(boxed1 == boxed2) - CapturingType(AndType(parent1, parent2), refs1, boxed1) - case tp @ OrType(CapturingType(parent1, refs1, boxed1), CapturingType(parent2, refs2, boxed2)) => + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(AndType(parent1, parent2), refs1, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => assert(refs1.asVar.elems.isEmpty) assert(refs2.asVar.elems.isEmpty) - assert(boxed1 == boxed2) - CapturingType(OrType(parent1, parent2, tp.isSoft), refs1, boxed1) - case tp @ OrType(CapturingType(parent1, refs1, boxed1), tp2) => - CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, boxed1) - case tp @ OrType(tp1, CapturingType(parent2, refs2, boxed2)) => - CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, boxed2) + assert(tp1.isBoxed == tp2.isBoxed) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1, tp1.isBoxed) + case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => + CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) + case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => + CapturingType(OrType(tp1, parent2, tp.isSoft), refs2, tp2.isBoxed) case _ if canHaveInferredCapture(tp) => val cs = tp.dealias match - case CapturingType(_, refs,_) => CaptureSet.Var(refs.elems) + case CapturingType(_, refs) => CaptureSet.Var(refs.elems) case _ => CaptureSet.Var() - CapturingType(tp, cs, CapturingKind.Regular) + CapturingType(tp, cs) case _ => tp @@ -272,7 +283,7 @@ extends tpd.TreeTraverser: mapOver(tp) def apply(tp: Type): Type = tp match - case CapturingType(parent, cs, boxed) => + case CapturingType(parent, cs) => tp.derivedCapturingType(propagateEnclosing(parent, cs, CaptureSet.empty), cs) case _ => propagateEnclosing(tp, CaptureSet.empty, CaptureSet.empty) @@ -283,9 +294,9 @@ extends tpd.TreeTraverser: if boxed then box(tp1) else tp1 private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = - addBoxes.traverse(tp) - if boxed then setBoxed(tp) - val tp1 = expandInlineAliases(tp) + var tp1 = addBoxes(tp) + if boxed then tp1 = box(tp1) + tp1 = expandInlineAliases(tp1) if tp1 ne tp then capt.println(i"expanded: $tp --> $tp1") if ctx.settings.YccNoAbbrev.value then tp1 else expandAbbreviations(tp1) @@ -388,14 +399,14 @@ extends tpd.TreeTraverser: if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then val localRefs = CaptureSet.Var() val newInfo = ClassInfo(prefix, cls, ps, decls, - CapturingType(cinfo.selfType, localRefs, CapturingKind.Regular) + CapturingType(cinfo.selfType, localRefs) .showing(i"inferred self type for $cls: $result", capt)) cls.updateInfoBetween(preRecheckPhase, thisPhase, newInfo) cls.thisType.asInstanceOf[ThisType].invalidateCaches() if cls.is(ModuleClass) then val modul = cls.sourceModule modul.updateInfoBetween(preRecheckPhase, thisPhase, - CapturingType(modul.info, localRefs, CapturingKind.Regular)) + CapturingType(modul.info, localRefs)) modul.termRef.invalidateCaches() case _ => end Setup diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 71422d30205b..12e4fdfba0da 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -56,9 +56,9 @@ object Synthetics: RefinedType(parent, pref.paramName, CapturingType( atPhase(ctx.phase.next)(pref.underlying.stripCapturing), - CaptureSet(pref), CapturingKind.Regular)) + CaptureSet(pref))) } - CapturingType(refined, CaptureSet(trackedParams*), CapturingKind.Regular) + CapturingType(refined, CaptureSet(trackedParams*)) if trackedParams.isEmpty then info else augmentResult(info) case info: PolyType => info.derivedLambdaType(resType = addCaptureDeps(info.resType)) @@ -69,7 +69,7 @@ object Synthetics: private def dropCaptureDeps(tp: Type)(using Context): Type = tp match case tp: MethodOrPoly => tp.derivedLambdaType(resType = dropCaptureDeps(tp.resType)) - case CapturingType(parent, _, _) => + case CapturingType(parent, _) => dropCaptureDeps(parent) case RefinedType(parent, _, _) => dropCaptureDeps(parent) @@ -82,7 +82,7 @@ object Synthetics: info.derivedLambdaType(resType = addDefaultGetterCapture(info.resType, owner, idx)) case info: ExprType => info.derivedExprType(addDefaultGetterCapture(info.resType, owner, idx)) - case EventuallyCapturingType(parent, _, _) => + case EventuallyCapturingType(parent, _) => addDefaultGetterCapture(parent, owner, idx) case info @ AnnotatedType(parent, annot) => info.derivedAnnotatedType(addDefaultGetterCapture(parent, owner, idx), annot) @@ -91,7 +91,7 @@ object Synthetics: val pinfo = param.info atPhase(ctx.phase.next) { if pinfo.captureSet.isAlwaysEmpty then info - else CapturingType(pinfo.stripCapturing, CaptureSet(param.termRef), CapturingKind.Regular) + else CapturingType(pinfo.stripCapturing, CaptureSet(param.termRef)) } case _ => info @@ -100,7 +100,7 @@ object Synthetics: private def dropDefaultGetterCapture(info: Type)(using Context): Type = info match case info: MethodOrPoly => info.derivedLambdaType(resType = dropDefaultGetterCapture(info.resType)) - case CapturingType(parent, _, _) => + case CapturingType(parent, _) => parent case info @ AnnotatedType(parent, annot) => info.derivedAnnotatedType(dropDefaultGetterCapture(parent), annot) @@ -111,24 +111,24 @@ object Synthetics: case info: MethodType => val paramInfo :: Nil = info.paramInfos: @unchecked val newParamInfo = - CapturingType(paramInfo, CaptureSet.universal, CapturingKind.Regular) + CapturingType(paramInfo, CaptureSet.universal) val trackedParam = info.paramRefs.head def newResult(tp: Type): Type = tp match case tp: MethodOrPoly => tp.derivedLambdaType(resType = newResult(tp.resType)) case _ => - CapturingType(tp, CaptureSet(trackedParam), CapturingKind.Regular) + CapturingType(tp, CaptureSet(trackedParam)) info.derivedLambdaType(paramInfos = newParamInfo :: Nil, resType = newResult(info.resType)) case info: PolyType => info.derivedLambdaType(resType = addUnapplyCaptures(info.resType)) private def dropUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => - val CapturingType(oldParamInfo, _, _) :: Nil = info.paramInfos: @unchecked + val CapturingType(oldParamInfo, _) :: Nil = info.paramInfos: @unchecked def oldResult(tp: Type): Type = tp match case tp: MethodOrPoly => tp.derivedLambdaType(resType = oldResult(tp.resType)) - case CapturingType(tp, _, _) => + case CapturingType(tp, _) => tp info.derivedLambdaType(paramInfos = oldParamInfo :: Nil, resType = oldResult(info.resType)) case info: PolyType => diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 69709b9ce347..3673704e3471 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.CommentsContext import Comments.Comment import util.Spans.NoSpan import Symbols.requiredModuleRef -import cc.{CapturingType, CaptureSet, CapturingKind, EventuallyCapturingType} +import cc.{CapturingType, CaptureSet, EventuallyCapturingType} import scala.annotation.tailrec @@ -136,7 +136,7 @@ class Definitions { HKTypeLambda(argParamNames :+ "R".toTypeName, argVariances :+ Covariant)( tl => List.fill(arity + 1)(TypeBounds.empty), tl => CapturingType(underlyingClass.typeRef.appliedTo(tl.paramRefs), - CaptureSet.universal, CapturingKind.Regular) + CaptureSet.universal) )) else val cls = denot.asClass.classSymbol @@ -1158,8 +1158,8 @@ class Definitions { */ object ByNameFunction: def apply(tp: Type)(using Context): Type = tp match - case EventuallyCapturingType(tp1, refs, CapturingKind.ByName) => - CapturingType(apply(tp1), refs, CapturingKind.Regular) + case tp @ EventuallyCapturingType(tp1, refs) if tp.annot.symbol == RetainsByNameAnnot => + CapturingType(apply(tp1), refs) case _ => defn.ContextFunction0.typeRef.appliedTo(tp :: Nil) def unapply(tp: Type)(using Context): Option[Type] = tp match diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 633ff03389d5..5f267a1c242a 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -334,7 +334,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, case tp: TypeVar => val underlying1 = recur(tp.underlying, fromBelow) if underlying1 ne tp.underlying then underlying1 else tp - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => val parent1 = recur(parent, fromBelow) if parent1 ne parent then tp.derivedCapturingType(parent1, refs) else tp case tp: AnnotatedType => diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index f9eb00b24ae4..d8486dc48e42 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -2174,7 +2174,7 @@ object SymDenotations { case tp: TypeParamRef => // uncachable, since baseType depends on context bounds recur(TypeComparer.bounds(tp).hi) - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => tp.derivedCapturingType(recur(parent), refs) case tp: TypeProxy => diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6de3c761949a..e7fe549c2f67 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,7 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly -import cc.{CapturingType, derivedCapturingType, CaptureSet, CapturingKind, stripCapturing} +import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing} /** Provides methods to compare types. */ @@ -340,7 +340,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling compareWild case tp2: LazyRef => isBottom(tp1) || !tp2.evaluating && recur(tp1, tp2.ref) - case CapturingType(_, _, _) => + case CapturingType(_, _) => secondTry case tp2: AnnotatedType if !tp2.isRefining => recur(tp1, tp2.parent) @@ -513,7 +513,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // recursively, so we do it only once. See i14870.scala as a test case, which would // loop for a very long time without the recursion brake. - case CapturingType(parent1, refs1, _) => + case CapturingType(parent1, refs1) => if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK then recur(parent1, tp2) else @@ -797,7 +797,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } compareTypeBounds - case CapturingType(parent2, refs2, _) => + case CapturingType(parent2, refs2) => def compareCaptured = val refs1 = tp1.captureSet try @@ -870,7 +870,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp: AppliedType => isNullable(tp.tycon) case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2) case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2) - case CapturingType(tp1, _, _) => isNullable(tp1) + case CapturingType(tp1, _) => isNullable(tp1) case _ => false val sym1 = tp1.symbol (sym1 eq NothingClass) && tp2.isValueTypeOrLambda || @@ -893,7 +893,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling tp1 match case tp1: CaptureRef if tp1.isTracked => val stripped = tp1w.stripCapturing - tp1w = CapturingType(stripped, tp1.singletonCaptureSet, CapturingKind.Regular) + tp1w = CapturingType(stripped, tp1.singletonCaptureSet) case _ => isSubType(tp1w, tp2, approx.addLow) } @@ -2484,7 +2484,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } case tp1: TypeVar if tp1.isInstantiated => tp1.underlying & tp2 - case CapturingType(parent1, refs1, _) => + case CapturingType(parent1, refs1) => if subCaptures(tp2.captureSet, refs1, frozen = true).isOK then parent1 & tp2 else diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index bad66c200d8d..7233f3ec7f0a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -168,7 +168,7 @@ object TypeOps: // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take // corrective steps, so no widening is wanted. simplify(l, theMap) | simplify(r, theMap) - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) && refs.subCaptures(parent.captureSet, frozen = true).isOK then simplify(parent, theMap) @@ -293,7 +293,7 @@ object TypeOps: tp1 match { case tp1: RecType => return tp1.rebind(approximateOr(tp1.parent, tp2)) - case CapturingType(parent1, refs1, _) => + case CapturingType(parent1, refs1) => return tp1.derivedCapturingType(approximateOr(parent1, tp2), refs1) case err: ErrorType => return err @@ -302,7 +302,7 @@ object TypeOps: tp2 match { case tp2: RecType => return tp2.rebind(approximateOr(tp1, tp2.parent)) - case CapturingType(parent2, refs2, _) => + case CapturingType(parent2, refs2) => return tp2.derivedCapturingType(approximateOr(tp1, parent2), refs2) case err: ErrorType => return err diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 107131b17790..e9b347778ed3 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,7 +36,7 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized -import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, CapturingKind, EventuallyCapturingType} +import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, EventuallyCapturingType} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -366,7 +366,7 @@ object Types { case tp: AndOrType => tp.tp1.unusableForInference || tp.tp2.unusableForInference case tp: LambdaType => tp.resultType.unusableForInference || tp.paramInfos.exists(_.unusableForInference) case WildcardType(optBounds) => optBounds.unusableForInference - case CapturingType(parent, refs, _) => parent.unusableForInference || refs.elems.exists(_.unusableForInference) + case CapturingType(parent, refs) => parent.unusableForInference || refs.elems.exists(_.unusableForInference) case _: ErrorType => true case _ => false @@ -1391,7 +1391,7 @@ object Types { case tp: AnnotatedType => val parent1 = tp.parent.dealias1(keep, keepOpaques) tp match - case tp @ CapturingType(parent, refs, _) => + case tp @ CapturingType(parent, refs) => tp.derivedCapturingType(parent1, refs) case _ => if keep(tp) then tp.derivedAnnotatedType(parent1, tp.annot) @@ -1880,15 +1880,13 @@ object Types { def capturing(ref: CaptureRef)(using Context): Type = if captureSet.accountsFor(ref) then this - else CapturingType(this, ref.singletonCaptureSet, - if this.isBoxedCapturing then CapturingKind.Boxed else CapturingKind.Regular) + else CapturingType(this, ref.singletonCaptureSet) def capturing(cs: CaptureSet)(using Context): Type = if cs.isConst && cs.subCaptures(captureSet, frozen = true).isOK then this else this match - case CapturingType(parent, cs1, boxed) => parent.capturing(cs1 ++ cs) - case _ => CapturingType(this, cs, - if this.isBoxedCapturing then CapturingKind.Boxed else CapturingKind.Regular) + 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] = @@ -3759,7 +3757,7 @@ object Types { case tp: TermParamRef if tp.binder eq thisLambdaType => TrueDeps case tp: AnnotatedType => tp match - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => (compute(status, parent, theAcc) /: refs.elems) { (s, ref) => ref match case tp: TermParamRef if tp.binder eq thisLambdaType => combine(s, CaptureDeps) @@ -3831,15 +3829,14 @@ object Types { def apply(tp: Type) = tp match { case tp @ TermParamRef(`thisLambdaType`, _) => range(defn.NothingType, atVariance(1)(apply(tp.underlying))) - case CapturingType(parent, refs, boxed) => + case CapturingType(_, _) => mapOver(tp) case AnnotatedType(parent, ann) if ann.refersToParamOf(thisLambdaType) => val parent1 = mapOver(parent) if ann.symbol == defn.RetainsAnnot || ann.symbol == defn.RetainsByNameAnnot then - val byName = ann.symbol == defn.RetainsByNameAnnot range( - AnnotatedType(parent1, CaptureSet.empty.toRegularAnnotation(byName)), - AnnotatedType(parent1, CaptureSet.universal.toRegularAnnotation(byName))) + AnnotatedType(parent1, CaptureSet.empty.toRegularAnnotation(ann.symbol)), + AnnotatedType(parent1, CaptureSet.universal.toRegularAnnotation(ann.symbol))) else parent1 case _ => mapOver(tp) @@ -4909,7 +4906,7 @@ object Types { else if (clsd.is(Module)) givenSelf else if (ctx.erasedTypes) appliedRef else givenSelf match - case givenSelf @ EventuallyCapturingType(tp, refs, kind) => + case givenSelf @ EventuallyCapturingType(tp, _) => givenSelf.derivedAnnotatedType(tp & appliedRef, givenSelf.annot) case _ => AndType(givenSelf, appliedRef) @@ -5601,7 +5598,7 @@ object Types { case tp: ExprType => derivedExprType(tp, this(tp.resultType)) - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => mapCapturingType(tp, parent, refs, variance) case tp @ AnnotatedType(underlying, annot) => @@ -6087,7 +6084,7 @@ object Types { val x2 = atVariance(0)(this(x1, tp.scrutinee)) foldOver(x2, tp.cases) - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => (this(x, parent) /: refs.elems)(this) case AnnotatedType(underlying, annot) => diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index d11467895437..a393643acbe2 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 -import cc.{CapturingType, EventuallyCapturingType, CaptureSet, CapturingKind} +import cc.{CapturingType, EventuallyCapturingType, CaptureSet, isBoxed} class PlainPrinter(_ctx: Context) extends Printer { @@ -200,9 +200,9 @@ class PlainPrinter(_ctx: Context) extends Printer { keywordStr(" match ") ~ "{" ~ casesText ~ "}" ~ (" <: " ~ toText(bound) provided !bound.isAny) }.close - case EventuallyCapturingType(parent, refs, kind) => + case tp @ EventuallyCapturingType(parent, refs) => def box = - Str("box ") provided kind == CapturingKind.Boxed && ctx.settings.YccDebug.value + Str("box ") provided tp.isBoxed && ctx.settings.YccDebug.value if printDebug && !refs.isConst then changePrec(GlobalPrec)(box ~ s"$refs " ~ toText(parent)) else if ctx.settings.YccDebug.value then @@ -233,9 +233,10 @@ class PlainPrinter(_ctx: Context) extends Printer { ~ (if tp.resultType.isInstanceOf[MethodType] then ")" else "): ") ~ toText(tp.resultType) } - case ExprType(ct @ EventuallyCapturingType(parent, refs, CapturingKind.ByName)) => + case ExprType(ct @ EventuallyCapturingType(parent, refs)) + if ct.annot.symbol == defn.RetainsByNameAnnot => if refs.isUniversal then changePrec(GlobalPrec) { "=> " ~ toText(parent) } - else toText(CapturingType(ExprType(parent), refs, CapturingKind.Regular)) + else toText(CapturingType(ExprType(parent), refs)) case ExprType(restp) => changePrec(GlobalPrec) { (if ctx.settings.Ycc.value then "-> " else "=> ") ~ toText(restp) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 57d62f11d568..f00ab6877b94 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -72,7 +72,7 @@ object CheckCaptures: * This check is performed after capture sets are computed in phase cc. */ def checkWellformedPost(tp: Type, pos: SrcPos)(using Context): Unit = tp match - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => for ref <- refs.elems do if ref.captureSetOfInfo.elems.isEmpty then report.error(em"$ref cannot be tracked since its capture set is empty", pos) @@ -131,7 +131,7 @@ class CheckCaptures extends Recheck, SymTransformer: variance = startingVariance override def traverse(t: Type) = t match - case CapturingType(parent, refs: CaptureSet.Var, _) => + case CapturingType(parent, refs: CaptureSet.Var) => if variance < 0 then capt.println(i"solving $t") refs.solve() @@ -194,6 +194,34 @@ class CheckCaptures extends Recheck, SymTransformer: } checkSubset(targetSet, curEnv.captured, pos) + /** If result type of a function type has toplevel boxed captures, propagate + * them to the function type as a whole. Such boxed captures + * can be created by substitution or as-seen-from. Propagating captures to the + * left simulates an unbox operation on the result. I.e. if f has type `A -> box C B` + * then in theory we need to unbox with + * + * x => C o- f(x) + * + * and that also propagates C into the type of the unboxing expression. + * TODO: Generalize this to boxed captues in other parts of a function type. + */ + def addResultBoxes(tp: Type)(using Context): Type = + def includeBoxed(res: Type) = tp.capturing(res.boxedCaptured) + val tpw = tp.widen + val boxedTpw = tpw.dealias match + case tp1 @ AppliedType(_, args) if defn.isNonRefinedFunction(tp1) => + includeBoxed(args.last) + case tp1 @ RefinedType(_, _, rinfo) if defn.isFunctionType(tp1) => + includeBoxed(rinfo.finalResultType) + case tp1 @ CapturingType(parent, refs) => + val boxedParent = addResultBoxes(parent) + if boxedParent eq parent then tpw + else boxedParent.capturing(refs) + case _ => + tpw + if boxedTpw eq tpw then tp else boxedTpw + end addResultBoxes + def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") @@ -327,7 +355,7 @@ class CheckCaptures extends Recheck, SymTransformer: def augmentConstructorType(core: Type, initCs: CaptureSet): Type = core match case core: MethodType => core.derivedLambdaType(resType = augmentConstructorType(core.resType, initCs)) - case CapturingType(parent, refs, _) => + case CapturingType(parent, refs) => augmentConstructorType(parent, initCs ++ refs) case _ => val (refined, cs) = addParamArgRefinements(core, initCs) @@ -369,7 +397,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def recheckApply(tree: Apply, pt: Type)(using Context): Type = includeCallCaptures(tree.symbol, tree.srcPos) super.recheckApply(tree, pt) match - case tp @ CapturingType(tp1, refs, kind) => + case tp @ CapturingType(tp1, refs) => tree.fun match case Select(qual, nme.apply) if defn.isFunctionType(qual.tpe.widen) => @@ -409,7 +437,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => NoType def checkNotUniversal(tp: Type): Unit = tp.widenDealias match - case wtp @ CapturingType(parent, refs, _) => + case wtp @ CapturingType(parent, refs) => refs.disallowRootCapability { () => val kind = if tree.isInstanceOf[ValDef] then "mutable variable" else "expression" report.error( @@ -420,7 +448,8 @@ class CheckCaptures extends Recheck, SymTransformer: checkNotUniversal(parent) case _ => checkNotUniversal(typeToCheck) - super.recheckFinish(tpe, tree, pt) + val tpe1 = if tree.isTerm then addResultBoxes(tpe) else tpe + super.recheckFinish(tpe1, tree, pt) /** This method implements the rule outlined in #14390: * When checking an expression `e: T` against an expected type `Cx Tx` @@ -453,7 +482,7 @@ class CheckCaptures extends Recheck, SymTransformer: erefs } val expected1 = expected match - case CapturingType(ecore, erefs, _) => + case CapturingType(ecore, erefs) => val erefs1 = augment(erefs, actual.captureSet) if erefs1 ne erefs then capt.println(i"augmented $expected from ${actual.captureSet} --> $erefs1") @@ -508,7 +537,7 @@ class CheckCaptures extends Recheck, SymTransformer: interpolator(startingVariance = -1).traverse(selfType) if !root.isEffectivelySealed then selfType match - case CapturingType(_, refs: CaptureSet.Var, _) if !refs.isUniversal => + case CapturingType(_, refs: CaptureSet.Var) if !refs.isUniversal => report.error( i"""$root needs an explicitly declared self type since its |inferred self type $selfType @@ -550,7 +579,7 @@ class CheckCaptures extends Recheck, SymTransformer: then val inferred = t.tpt.knownType def checkPure(tp: Type) = tp match - case CapturingType(_, refs, _) if !refs.elems.isEmpty => + case CapturingType(_, refs) if !refs.elems.isEmpty => val resultStr = if t.isInstanceOf[DefDef] then " result" else "" report.error( em"""Non-local $sym cannot have an inferred$resultStr type diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 5bb85c1ee67d..358297690cb1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -545,7 +545,7 @@ object Inferencing { case tp: RefinedType => tp.derivedRefinedType(captureWildcards(tp.parent), tp.refinedName, tp.refinedInfo) case tp: RecType => tp.derivedRecType(captureWildcards(tp.parent)) case tp: LazyRef => captureWildcards(tp.ref) - case CapturingType(parent, refs, _) => tp.derivedCapturingType(captureWildcards(parent), refs) + case CapturingType(parent, refs) => tp.derivedCapturingType(captureWildcards(parent), refs) case tp: AnnotatedType => tp.derivedAnnotatedType(captureWildcards(tp.parent), tp.annot) case _ => tp } diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index c2f928a7d6c7..30cb40bc4d14 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -366,7 +366,7 @@ trait TypeAssigner { // reference to a polytype would have to be a fresh copy of that type, // but we want to avoid that because it would increase compilation cost. // See pos/i6682a.scala for a test case where the defensive copying matters. - val ensureFresh = new TypeMap: + val ensureFresh = new TypeMap with CaptureSet.IdempotentCaptRefMap: def apply(tp: Type) = mapOver( if tp eq pt then pt.newLikeThis(pt.paramNames, pt.paramInfos, pt.resType) else tp) diff --git a/tests/neg-custom-args/captures/bounded.scala b/tests/neg-custom-args/captures/bounded.scala index fb6b198fb3a3..12bdb9ac8854 100644 --- a/tests/neg-custom-args/captures/bounded.scala +++ b/tests/neg-custom-args/captures/bounded.scala @@ -6,6 +6,18 @@ def test(c: Cap) = def elem = x def lateElem = () => x + def f(x: Int): Int = if c == c then x else 0 + val b = new B(f) + val r1 = b.elem + val r1c: {c} Int -> Int = r1 + val r2 = b.lateElem + val r2c: () -> {c} Int -> Int = r2 // error + +def test2(c: Cap) = + class B[X](x: X): + def elem = x + def lateElem = () => x + def f(x: Int): Int = if c == c then x else 0 val b = new B(f) val r1 = b.elem From 71847a40cbe74e581087bfc7017e2028539ffcfa Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 13 May 2022 17:52:35 +0200 Subject: [PATCH 51/99] Use intersection to type selections --- .../dotty/tools/dotc/transform/Recheck.scala | 25 ++++++------ .../tools/dotc/typer/CheckCaptures.scala | 12 ++++++ .../captures/caseclass/Test_2.scala | 7 +--- tests/neg-custom-args/captures/ctest.scala | 6 +++ tests/neg-custom-args/captures/i15116.check | 18 ++++++++- tests/neg-custom-args/captures/i15116.scala | 4 ++ .../pos-custom-args/captures/caseclass.scala | 39 ++++++++++++++++--- 7 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 tests/neg-custom-args/captures/ctest.scala diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index a8b113be214e..7a3a1ce1edbe 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -6,6 +6,7 @@ import core.* import Symbols.*, Contexts.*, Types.*, ContextOps.*, Decorators.*, SymDenotations.* import Flags.*, SymUtils.*, NameKinds.* import ast.* +import Names.Name import Phases.Phase import DenotTransformers.{DenotTransformer, IdentityDenotTransformer, SymTransformer} import NamerOps.{methodType, linkConstructorParams} @@ -125,17 +126,19 @@ abstract class Recheck extends Phase, SymTransformer: tree.tpe /** Keep the symbol of the `select` but re-infer its type */ - def recheckSelect(tree: Select)(using Context): Type = tree match - case Select(qual, name) => - val qualType = recheck(qual).widenIfUnstable - if name.is(OuterSelectName) then tree.tpe - else - //val pre = ta.maybeSkolemizePrefix(qualType, name) - val mbr = qualType.findMember(name, qualType, - excluded = if tree.symbol.is(Private) then EmptyFlags else Private - ).suchThat(tree.symbol == _) - constFold(tree, qualType.select(name, mbr)) - //.showing(i"recheck select $qualType . $name : ${mbr.symbol.info} = $result") + def recheckSelect(tree: Select)(using Context): Type = + val Select(qual, name) = tree + recheckSelection(tree, recheck(qual).widenIfUnstable, name) + + def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = + if name.is(OuterSelectName) then tree.tpe + else + //val pre = ta.maybeSkolemizePrefix(qualType, name) + val mbr = qualType.findMember(name, qualType, + excluded = if tree.symbol.is(Private) then EmptyFlags else Private + ).suchThat(tree.symbol == _) + constFold(tree, qualType.select(name, mbr)) + //.showing(i"recheck select $qualType . $name : ${mbr.symbol.info} = $result") def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match case Bind(name, body) => diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index f00ab6877b94..ac36528d278e 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -238,6 +238,18 @@ class CheckCaptures extends Recheck, SymTransformer: else i"references $cs1 are not all" report.error(i"$header included in allowed capture set ${res.blocking}", pos) + override def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = { + val selType = super.recheckSelection(tree, qualType, name) + val selCs = selType.widen.captureSet + if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then + selType + else + val qualCs = qualType.captureSet + //println(i"intersect $qualType, ${selType.widen}, $qualCs, $selCs") + if selCs.subCaptures(qualCs, frozen = true).isOK then selType + else selType.widen.stripCapturing.capturing(selCs ** qualCs) + }//.showing(i"recheck sel $tree, $qualType = $result") + override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = val cs = capturedVars(tree.meth.symbol) capt.println(i"typing closure $tree with cvs $cs") diff --git a/tests/neg-custom-args/captures/caseclass/Test_2.scala b/tests/neg-custom-args/captures/caseclass/Test_2.scala index bba5988923a2..4eac6a260292 100644 --- a/tests/neg-custom-args/captures/caseclass/Test_2.scala +++ b/tests/neg-custom-args/captures/caseclass/Test_2.scala @@ -22,9 +22,4 @@ def test(c: C) = val y4 = y3 match case Ref(xx) => xx - val y4c: {x3} () -> Unit = y4 // error (?) found: (y4 : {*} () -> Unit) required: {x3} () -> Unit. (But in fact it should work) - - - - - + val y4c: {x3} () -> Unit = y4 diff --git a/tests/neg-custom-args/captures/ctest.scala b/tests/neg-custom-args/captures/ctest.scala new file mode 100644 index 000000000000..08bec16d8177 --- /dev/null +++ b/tests/neg-custom-args/captures/ctest.scala @@ -0,0 +1,6 @@ +class CC +type Cap = {*} CC + +def test(cap1: Cap, cap2: Cap) = + var b: List[String => String] = Nil // was error, now OK + val bc = b.head // error diff --git a/tests/neg-custom-args/captures/i15116.check b/tests/neg-custom-args/captures/i15116.check index 1c50fd6d1cc9..83c552087646 100644 --- a/tests/neg-custom-args/captures/i15116.check +++ b/tests/neg-custom-args/captures/i15116.check @@ -9,6 +9,20 @@ 5 | val x = Foo(m) // error | ^^^^^^^^^^^^^^ | Non-local value x cannot have an inferred type - | {Baz.this.m} Foo{m: {Baz.this.m} String} - | with non-empty capture set {Baz.this.m}. + | {Baz.this} Foo{m: {Baz.this} String} + | with non-empty capture set {Baz.this}. + | The type needs to be declared explicitly. +-- Error: tests/neg-custom-args/captures/i15116.scala:7:6 -------------------------------------------------------------- +7 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Bar1.this.m} Foo{m: {Bar1.this.m} String} + | with non-empty capture set {Bar1.this.m}. + | The type needs to be declared explicitly. +-- Error: tests/neg-custom-args/captures/i15116.scala:9:6 -------------------------------------------------------------- +9 | val x = Foo(m) // error + | ^^^^^^^^^^^^^^ + | Non-local value x cannot have an inferred type + | {Baz2.this} Foo{m: {Baz2.this} String} + | with non-empty capture set {Baz2.this}. | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/i15116.scala b/tests/neg-custom-args/captures/i15116.scala index fa8286961ccd..1659f251df3e 100644 --- a/tests/neg-custom-args/captures/i15116.scala +++ b/tests/neg-custom-args/captures/i15116.scala @@ -3,3 +3,7 @@ class Bar(val m: {*} String): val x = Foo(m) // error trait Baz(val m: {*} String): val x = Foo(m) // error +class Bar1(m: {*} String): + val x = Foo(m) // error +trait Baz2(m: {*} String): + val x = Foo(m) // error diff --git a/tests/pos-custom-args/captures/caseclass.scala b/tests/pos-custom-args/captures/caseclass.scala index 2d85e612a8ef..a845da181e9f 100644 --- a/tests/pos-custom-args/captures/caseclass.scala +++ b/tests/pos-custom-args/captures/caseclass.scala @@ -1,7 +1,34 @@ -case class Ref(x: {*} String) - @annotation.capability class C -def test(c: C) = - val x1 = Ref("hello") - val y = x1 match - case Ref(z) => z +object test1: + case class Ref(x: {*} String) + + def test(c: C) = + val x1 = Ref("hello") + val y = x1 match + case Ref(z) => z + val yc: String = y + +object test2: + case class Ref(x: () => Unit) + def test(c: C) = + + val pure: () -> Unit = () => () + val impure: () => Unit = pure + val mixed: {c} () -> Unit = pure + val x = Ref(impure) + val y0 = x.copy(pure) + val yc0: Ref = y0 + + val x2 = Ref(pure) + val _: Ref = x2 + val y2 = x2.copy() + val yc2: Ref = y2 + + val x3 = Ref(mixed) + val _: {c} Ref = x3 + val y3 = x3.copy() + val yc3: {c} Ref = y3 + + val y4 = y3 match + case Ref(xx) => xx + val y4c: {x3} () -> Unit = y4 From ef00273d105e23f2921d57a290d3cde513826c8b Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 29 May 2022 14:48:25 +0200 Subject: [PATCH 52/99] Update semanticDB expect file --- tests/semanticdb/metac.expect | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/semanticdb/metac.expect b/tests/semanticdb/metac.expect index 633b06e71ba2..4090ce6b6001 100644 --- a/tests/semanticdb/metac.expect +++ b/tests/semanticdb/metac.expect @@ -3355,7 +3355,6 @@ Text => empty Language => Scala Symbols => 2 entries Occurrences => 5 entries -Synthetics => 2 entries Symbols: example/Tabs$package. => final package object example extends Object { self: example.type => +2 decls } @@ -3368,11 +3367,6 @@ Occurrences: [4:3..4:6): map -> scala/collection/immutable/List#map(). [4:9..4:10): + -> scala/Int#`+`(+4). -Synthetics: -[3:1..4:6):List(1,2,3) - .map => *[Int] -[3:1..3:5):List => *.apply[Int] - expect/Traits.scala ------------------- From 78e5fa4bf601d2d691a74357db6b8edef59833d4 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 12 Jun 2022 19:38:36 +0200 Subject: [PATCH 53/99] Avoi intersections in typing selections and applications Use either the qualifier type or the result type depending on a pre-check using a new method `mightSubcapture`. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 11 +++++++++-- .../tools/dotc/typer/CheckCaptures.scala | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 3b473fc9c8f9..4563be74c158 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -111,7 +111,7 @@ sealed abstract class CaptureSet extends Showable: /** {x} <:< this where <:< is subcapturing, but treating all variables * as frozen. */ - def accountsFor(x: CaptureRef)(using ctx: Context): Boolean = + def accountsFor(x: CaptureRef)(using Context): Boolean = reporting.trace(i"$this accountsFor $x, ${x.captureSetOfInfo}?", show = true) { elems.exists(_.subsumes(x)) || !x.isRootCapability && x.captureSetOfInfo.subCaptures(this, frozen = true).isOK @@ -122,12 +122,19 @@ sealed abstract class CaptureSet extends Showable: * for `x` in a state where we assume all supersets of `x` have just the elements * known at this point. */ - def mightAccountFor(x: CaptureRef)(using ctx: Context): Boolean = + def mightAccountFor(x: CaptureRef)(using Context): Boolean = reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { elems.exists(_.subsumes(x)) || !x.isRootCapability && x.captureSetOfInfo.elems.forall(mightAccountFor) } + /** A more optimistic version of subCaptures used to choose one of two typing rules + * for selctions and applications. `cs1 mightSubcapture cs2` if `cs2` might account for + * every element currently known to be in `cs1`. + */ + def mightSubcapture(that: CaptureSet)(using Context): Boolean = + elems.forall(that.mightAccountFor) + /** The subcapturing test */ final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index ac36528d278e..59b912a63cb6 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -246,8 +246,10 @@ class CheckCaptures extends Recheck, SymTransformer: else val qualCs = qualType.captureSet //println(i"intersect $qualType, ${selType.widen}, $qualCs, $selCs") - if selCs.subCaptures(qualCs, frozen = true).isOK then selType - else selType.widen.stripCapturing.capturing(selCs ** qualCs) + if qualCs.mightSubcapture(selCs) then + selType.widen.stripCapturing.capturing(qualCs) + else + selType }//.showing(i"recheck sel $tree, $qualType = $result") override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = @@ -412,13 +414,12 @@ class CheckCaptures extends Recheck, SymTransformer: case tp @ CapturingType(tp1, refs) => tree.fun match case Select(qual, nme.apply) - if defn.isFunctionType(qual.tpe.widen) => - qual.tpe match - case ref: CaptureRef - if ref.isTracked && tree.args.forall(_.tpe.captureSet.isAlwaysEmpty) => - tp.derivedCapturingType(tp1, refs ** ref.singletonCaptureSet) - .showing(i"narrow $tree: $tp --> $result", capt) - case _ => tp + if defn.isFunctionType(qual.tpe.widen) + && tree.args.forall(_.tpe.captureSet.isAlwaysEmpty) + && qual.tpe.captureSet.mightSubcapture(refs) + => + tp.derivedCapturingType(tp1, qual.tpe.captureSet) + .showing(i"narrow $tree: $tp --> $result", capt) case _ => tp case tp => tp From 2724ac75d995bd9207d18ec80bf28ca14ba32150 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 12 Jun 2022 19:57:11 +0200 Subject: [PATCH 54/99] Generalize new rule for applications Allow impure arguments --- .../tools/dotc/typer/CheckCaptures.scala | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 59b912a63cb6..0c64d47e7582 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -238,6 +238,16 @@ class CheckCaptures extends Recheck, SymTransformer: else i"references $cs1 are not all" report.error(i"$header included in allowed capture set ${res.blocking}", pos) + /** A specialized implementation of the selection rule. + * + * E |- f: Cf { m: Cr R } + * ------------------------ + * E |- f.m: C R + * + * The implementation picks as `C` one of `{f}` or `Cr`, depending on the + * outcome of a `mightSubcapture` test. It picks `{f}` if this might subcapture Cr + * and Cr otherwise. + */ override def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = { val selType = super.recheckSelection(tree, qualType, name) val selCs = selType.widen.captureSet @@ -395,18 +405,16 @@ class CheckCaptures extends Recheck, SymTransformer: finally curEnv = curEnv.outer recheckFinish(result, arg, pt) - /** A specialized implementation of the apply rule from https://github.com/lampepfl/dotty/discussions/14387: + /** A specialized implementation of the apply rule. * - * E |- f: Cf (Ra -> Cr Rr) - * E |- a: Ra - * ------------------------ - * E |- f a: Cr /\ {f} Rr + * E |- f: Cf (Ra -> Cr Rr) + * E |- a: Ra + * ------------------------ + * E |- f a: C Rr * - * Specialized for the case where `f` is a tracked and the arguments are pure. - * This replaces the previous rule #13657 while still allowing the code in pos/lazylists1.scala. - * We could consider generalizing to the case where the function arguments have non-empty - * capture sets as suggested in #14387, but that would make capture set computations more complex, - * so we should also evaluate the performance impact. + * 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 = includeCallCaptures(tree.symbol, tree.srcPos) @@ -415,10 +423,11 @@ class CheckCaptures extends Recheck, SymTransformer: tree.fun match case Select(qual, nme.apply) if defn.isFunctionType(qual.tpe.widen) - && tree.args.forall(_.tpe.captureSet.isAlwaysEmpty) && qual.tpe.captureSet.mightSubcapture(refs) + && tree.args.forall(_.tpe.captureSet.mightSubcapture(refs)) => - tp.derivedCapturingType(tp1, qual.tpe.captureSet) + tp.derivedCapturingType(tp1, tree.args.foldLeft(qual.tpe.captureSet)((cs, arg) => + cs ++ arg.tpe.captureSet)) .showing(i"narrow $tree: $tp --> $result", capt) case _ => tp case tp => tp From 8c8d6e4b0c730dd0fee23abf9b9436a9b4a17a01 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 12 Jun 2022 23:03:21 +0200 Subject: [PATCH 55/99] Generalize application rule to all methods --- compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 0c64d47e7582..0966c5ea19aa 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -421,8 +421,8 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckApply(tree, pt) match case tp @ CapturingType(tp1, refs) => tree.fun match - case Select(qual, nme.apply) - if defn.isFunctionType(qual.tpe.widen) + case Select(qual, _) + if !tree.fun.symbol.isConstructor && qual.tpe.captureSet.mightSubcapture(refs) && tree.args.forall(_.tpe.captureSet.mightSubcapture(refs)) => From c90f0037b757c9c1b1e684a48b634d36f930fcb4 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 26 Jul 2022 13:36:40 +0200 Subject: [PATCH 56/99] Fix rebase breakage --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 2 +- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 11 +++++++---- compiler/src/dotty/tools/dotc/transform/Recheck.scala | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e7fe549c2f67..263a66d5fc3a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -635,7 +635,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def isSubInfo(info1: Type, info2: Type): Boolean = (info1, info2) match case (info1: PolyType, info2: PolyType) => - sameLength(info1.paramNames, info2.paramNames) + info1.paramNames.hasSameLengthAs(info2.paramNames) && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) case (info1: MethodType, info2: MethodType) => matchingMethodParams(info1, info2, precise = false) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index fc1c64611bbd..9059719444a3 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -904,7 +904,7 @@ object Parsers { && { lookahead.observeColonEOL(inTemplate = false) lookahead.nextToken() - canStartTypeTokens.contains(lookahead.token) + canStartInfixTypeTokens.contains(lookahead.token) } /** Is current ident a `*`, and is it followed by a `)`, `, )`, `,EOF`? The latter two are not @@ -4031,8 +4031,8 @@ object Parsers { stats.toList } - /** SelfType ::= id [‘:’ InfixType] ‘=>’ - * | ‘this’ ‘:’ InfixType ‘=>’ + /** SelfType ::= id [‘:’ [CaptureSet] InfixType] ‘=>’ + * | ‘this’ ‘:’ [CaptureSet] InfixType ‘=>’ */ def selfType(): ValDef = if (in.isIdent || in.token == THIS) @@ -4048,7 +4048,10 @@ object Parsers { val selfTpt = if in.isColon then in.nextToken() - infixType() + if in.token == LBRACE && followingIsCaptureSet() then + CapturingTypeTree(captureSet(), infixType()) + else + infixType() else if selfName == nme.WILDCARD then accept(COLONfollow) TypeTree() diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 7a3a1ce1edbe..2f0f1085ae12 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -187,7 +187,7 @@ abstract class Recheck extends Phase, SymTransformer: def recheckApply(tree: Apply, pt: Type)(using Context): Type = recheck(tree.fun).widen match case fntpe: MethodType => - assert(sameLength(fntpe.paramInfos, tree.args)) + assert(fntpe.paramInfos.hasSameLengthAs(tree.args)) val formals = if tree.symbol.is(JavaDefined) then mapJavaArgs(fntpe.paramInfos) else fntpe.paramInfos @@ -208,7 +208,7 @@ abstract class Recheck extends Phase, SymTransformer: def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = recheck(tree.fun).widen match case fntpe: PolyType => - assert(sameLength(fntpe.paramInfos, tree.args)) + assert(fntpe.paramInfos.hasSameLengthAs(tree.args)) val argTypes = tree.args.map(recheck(_)) constFold(tree, fntpe.instantiate(argTypes)) From 768dba257a03179566f71c28a39a3661f1c2c8ef Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Jul 2022 15:17:35 +0200 Subject: [PATCH 57/99] Don't elide capture sets when printing (empty) bounds --- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index a393643acbe2..058cc05bbf25 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -422,8 +422,8 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp: AliasingBounds => " = " ~ toText(tp.alias) case TypeBounds(lo, hi) => - (if (lo isRef defn.NothingClass) Text() else " >: " ~ toText(lo)) - ~ (if hi.isAny || (!printDebug && hi.isFromJavaObject) then Text() else " <: " ~ toText(hi)) + (if lo.isExactlyNothing then Text() else " >: " ~ toText(lo)) + ~ (if hi.isExactlyAny || (!printDebug && hi.isFromJavaObject) then Text() else " <: " ~ toText(hi)) tparamStr ~ binder case tp @ ClassInfo(pre, cls, cparents, decls, selfInfo) => val preText = toTextLocal(pre) From 275218da9878393f0965aa52c5f58740ab744db7 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Jul 2022 15:13:44 +0200 Subject: [PATCH 58/99] Several fixes to boxing 1. Update info of TypeDef symbols to account for boxing. Previously, the changes were made to the tree, but were not propagated to the symbol. 2. Maintain alias types when adding boxes. 3. Don't accidentally widen in addResultBoxes. The boxmap pos test started failing after (1), (2) before the fix (3) was added. Fixes #15749 --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 22 ++++++++++++++----- .../dotty/tools/dotc/transform/Recheck.scala | 3 ++- .../tools/dotc/typer/CheckCaptures.scala | 12 +++++----- tests/neg-custom-args/captures/i15749.scala | 15 +++++++++++++ 4 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 tests/neg-custom-args/captures/i15749.scala diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 0b278893d094..1a22791b7ca3 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -49,6 +49,8 @@ extends tpd.TreeTraverser: def apply(t: Type) = mapOver(t) match case t1 @ AppliedType(tycon, args) if !defn.isNonRefinedFunction(t1) => t1.derivedAppliedType(tycon, args.mapConserve(box)) + case t1: AliasingBounds => + t1.derivedAlias(t1.alias) case t1 @ TypeBounds(lo, hi) => t1.derivedTypeBounds(box(lo), box(hi)) case t1 => @@ -337,6 +339,9 @@ extends tpd.TreeTraverser: then transformInferredType(tree.tpe, boxed) else transformExplicitType(tree.tpe, boxed)) + private def updateInfo(sym: Symbol, tpe: Type)(using Context) = + sym.updateInfoBetween(preRecheckPhase, thisPhase, tpe) + def traverse(tree: Tree)(using Context): Unit = tree match case tree: DefDef if isExcluded(tree.symbol) => @@ -388,11 +393,10 @@ extends tpd.TreeTraverser: def complete(denot: SymDenotation)(using Context) = denot.info = newInfo recheckDef(tree, sym) - sym.updateInfoBetween(preRecheckPhase, thisPhase, completer) + updateInfo(sym, completer) case tree: Bind => val sym = tree.symbol - sym.updateInfoBetween(preRecheckPhase, thisPhase, - transformInferredType(sym.info, boxed = false)) + updateInfo(sym, transformInferredType(sym.info, boxed = false)) case tree: TypeDef if tree.symbol.isClass => val cls = tree.symbol.asClass val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo @@ -401,12 +405,18 @@ extends tpd.TreeTraverser: val newInfo = ClassInfo(prefix, cls, ps, decls, CapturingType(cinfo.selfType, localRefs) .showing(i"inferred self type for $cls: $result", capt)) - cls.updateInfoBetween(preRecheckPhase, thisPhase, newInfo) + updateInfo(cls, newInfo) cls.thisType.asInstanceOf[ThisType].invalidateCaches() if cls.is(ModuleClass) then val modul = cls.sourceModule - modul.updateInfoBetween(preRecheckPhase, thisPhase, - CapturingType(modul.info, localRefs)) + updateInfo(modul, CapturingType(modul.info, localRefs)) modul.termRef.invalidateCaches() + case tree: TypeDef => + val info = atPhase(preRecheckPhase)(tree.symbol.info) + val newInfo = transformExplicitType(info, boxed = false) + if newInfo ne info then + updateInfo(tree.symbol, newInfo) + capt.println(i"update info of ${tree.symbol} from $info to $newInfo") + case _ => end Setup diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 2f0f1085ae12..62e65d306c16 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -138,7 +138,7 @@ abstract class Recheck extends Phase, SymTransformer: excluded = if tree.symbol.is(Private) then EmptyFlags else Private ).suchThat(tree.symbol == _) constFold(tree, qualType.select(name, mbr)) - //.showing(i"recheck select $qualType . $name : ${mbr.symbol.info} = $result") + //.showing(i"recheck select $qualType . $name : ${mbr.info} = $result") def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match case Bind(name, body) => @@ -204,6 +204,7 @@ abstract class Recheck extends Phase, SymTransformer: Nil val argTypes = recheckArgs(tree.args, formals, fntpe.paramRefs) constFold(tree, instantiate(fntpe, argTypes, tree.fun.symbol)) + //.showing(i"typed app $tree : $fntpe with ${tree.args}%, % : $argTypes%, % = $result") def recheckTypeApply(tree: TypeApply, pt: Type)(using Context): Type = recheck(tree.fun).widen match diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 0966c5ea19aa..4bb4534fd61b 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -207,19 +207,19 @@ class CheckCaptures extends Recheck, SymTransformer: */ def addResultBoxes(tp: Type)(using Context): Type = def includeBoxed(res: Type) = tp.capturing(res.boxedCaptured) - val tpw = tp.widen - val boxedTpw = tpw.dealias match + val tp1 = tp.dealias + val boxedTp = tp1 match case tp1 @ AppliedType(_, args) if defn.isNonRefinedFunction(tp1) => includeBoxed(args.last) case tp1 @ RefinedType(_, _, rinfo) if defn.isFunctionType(tp1) => includeBoxed(rinfo.finalResultType) case tp1 @ CapturingType(parent, refs) => val boxedParent = addResultBoxes(parent) - if boxedParent eq parent then tpw + if boxedParent eq parent then tp1 else boxedParent.capturing(refs) case _ => - tpw - if boxedTpw eq tpw then tp else boxedTpw + tp1 + if boxedTp eq tp1 then tp else boxedTp end addResultBoxes def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = @@ -255,7 +255,7 @@ class CheckCaptures extends Recheck, SymTransformer: selType else val qualCs = qualType.captureSet - //println(i"intersect $qualType, ${selType.widen}, $qualCs, $selCs") + capt.println(i"intersect $qualType, ${selType.widen}, $qualCs, $selCs in $tree") if qualCs.mightSubcapture(selCs) then selType.widen.stripCapturing.capturing(qualCs) else diff --git a/tests/neg-custom-args/captures/i15749.scala b/tests/neg-custom-args/captures/i15749.scala new file mode 100644 index 000000000000..00d1811498f7 --- /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 : □ {*} Unit -> A + v.x(unit) // error: (unbox v.x)(unit), where (unbox v.x) should be untypable \ No newline at end of file From 54c0ebf002efc8a9a4118e6dcc6d20d04971b62b Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Jul 2022 16:18:06 +0200 Subject: [PATCH 59/99] Always print boxes (for now) It's a necessary measure to diagnose mishandlings of boxes faster. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 4 ++-- .../dotty/tools/dotc/printing/PlainPrinter.scala | 2 +- tests/neg-custom-args/captures/capt1.check | 2 +- tests/neg-custom-args/captures/real-try.check | 2 +- tests/neg-custom-args/captures/try.check | 6 +++--- .../neg-custom-args/captures/usingLogFile.check | 16 ++++++++-------- tests/neg-custom-args/captures/vars.check | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 1a22791b7ca3..474b08240c0b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -339,8 +339,8 @@ extends tpd.TreeTraverser: then transformInferredType(tree.tpe, boxed) else transformExplicitType(tree.tpe, boxed)) - private def updateInfo(sym: Symbol, tpe: Type)(using Context) = - sym.updateInfoBetween(preRecheckPhase, thisPhase, tpe) + private def updateInfo(sym: Symbol, info: Type)(using Context) = + sym.updateInfoBetween(preRecheckPhase, thisPhase, info) def traverse(tree: Tree)(using Context): Unit = tree match diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 058cc05bbf25..56edd2b68342 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -202,7 +202,7 @@ class PlainPrinter(_ctx: Context) extends Printer { }.close case tp @ EventuallyCapturingType(parent, refs) => def box = - Str("box ") provided tp.isBoxed && ctx.settings.YccDebug.value + Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value if printDebug && !refs.isConst then changePrec(GlobalPrec)(box ~ s"$refs " ~ toText(parent)) else if ctx.settings.YccDebug.value then diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 439c16c143be..473121b01f81 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -41,6 +41,6 @@ 31 | val z2 = h[() -> Cap](() => x)(() => C()) // error | ^^^^^^^ | Found: {x} () -> Cap - | Required: () -> Cap + | Required: () -> box {*} C | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index 8becde577e0f..9745470f219c 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -19,5 +19,5 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:30:4 ----------------------------------------------------------- 30 | b.x // error | ^^^ - | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 87719da2424f..95fe843db57d 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -3,7 +3,7 @@ 23 | (x: CanThrow[Exception]) => x 24 | }{ // error | ^ - | The expression's type {*} CT[Exception] is not allowed to capture the root capability `*`. + | The expression's type box {*} CT[Exception] is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. 25 | (ex: Exception) => ??? 26 | } @@ -24,7 +24,7 @@ 38 | 22 39 | } { // error | ^ - | The expression's type {*} () -> Int is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Int is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. 40 | (ex: Exception) => () => 22 41 | } @@ -36,7 +36,7 @@ 50 | 22 51 |} { // error | ^ - | The expression's type {*} () -> Int is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Int is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. 52 | (ex: Exception) => () => 22 53 |} diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index cade29eff6e8..2a04145699ba 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -1,35 +1,35 @@ -- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:27 ------------------------------------------------------ 23 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:29:9 ------------------------------------------------------- 29 | later2.x() // error | ^^^^^^^^ - | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:31:6 ------------------------------------------------------- 31 | var later3: () => Unit = () => () // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The mutable variable's type {*} () -> Unit is not allowed to capture the root capability `*`. + | The mutable variable's type box {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:37:9 ------------------------------------------------------- 37 | later4.x() // error | ^^^^^^^^ - | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:27 ------------------------------------------------------ 47 | val later = usingLogFile { f => () => f.write(0) } // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:62:25 ------------------------------------------------------ 62 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type {*} (x$0: Int) -> Unit is not allowed to capture the root capability `*`. - | This usually means that a capability persists longer than its allowed lifetime. + | The expression's type box {*} (x$0: Int) -> Unit is not allowed to capture the root capability `*`. + | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:25 ------------------------------------------------------ 71 | val later = usingFile("logfile", usingLogger(_, l => () => l.log("test"))) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type {*} () -> Unit is not allowed to capture the root capability `*`. + | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 1c41fc5b7460..b5a01558cd2c 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -8,17 +8,17 @@ -- Error: tests/neg-custom-args/captures/vars.scala:13:6 --------------------------------------------------------------- 13 | var a: String => String = f // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The mutable variable's type {*} String -> String is not allowed to capture the root capability `*`. + | The mutable variable's type box {*} String -> String is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/vars.scala:15:4 --------------------------------------------------------------- 15 | b.head // error | ^^^^^^ - | The expression's type {*} String -> String is not allowed to capture the root capability `*`. + | The expression's type box {*} String -> String is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/vars.scala:30:8 --------------------------------------------------------------- 30 | local { cap3 => // error | ^ - | The expression's type {*} (x$0: ? String) -> ? String is not allowed to capture the root capability `*`. + | The expression's type box {*} (x$0: ? String) -> ? String is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. 31 | def g(x: String): String = if cap3 == cap3 then "" else "a" 32 | g From 02217444f8a2d538c9fe4b97691617c0c40fd331 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Jul 2022 17:23:03 +0200 Subject: [PATCH 60/99] Box arguments of type applications This required a change in `lists.scala`, where capture sets buried to the right had to be repeated on the whole type. I believe there is no desugaring yet that would do these things automatically. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 6 +++++- tests/pos-custom-args/captures/lists.scala | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 474b08240c0b..ae3005ea75dc 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -333,7 +333,7 @@ extends tpd.TreeTraverser: mapOver(t) end SubstParams - private def transformTT(tree: TypeTree, boxed: Boolean)(using Context) = + private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = tree.rememberType( if tree.isInstanceOf[InferredTypeTree] then transformInferredType(tree.tpe, boxed) @@ -349,6 +349,10 @@ extends tpd.TreeTraverser: case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) => transformTT(tpt, boxed = true) traverse(tree.rhs) + case tree @ TypeApply(fn, args) => + traverse(fn) + for case arg: TypeTree <- args do + transformTT(arg, boxed = true) case _ => traverseChildren(tree) tree match diff --git a/tests/pos-custom-args/captures/lists.scala b/tests/pos-custom-args/captures/lists.scala index f52727af7b94..6389ec933b32 100644 --- a/tests/pos-custom-args/captures/lists.scala +++ b/tests/pos-custom-args/captures/lists.scala @@ -50,7 +50,7 @@ def test(c: Cap, d: Cap, e: Cap) = val eff2 = [A] => (x: A) => if x == e then x else x val a0 = identity[{d, y} Cap -> Unit] - val a0c: ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = a0 + val a0c: {d, y} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = a0 val a1 = zs.map[{d, y} Cap -> Unit](a0) val a1c: LIST[{d, y} Cap -> Unit] = a1 val a2 = zs.map[{d, y} Cap -> Unit](identity[{d, y} Cap -> Unit]) @@ -65,7 +65,7 @@ def test(c: Cap, d: Cap, e: Cap) = val a6c: LIST[{d, c} Cap -> Unit] = a6 val b0 = eff[{d, y} Cap -> Unit] - val b0c: {e} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = b0 + val b0c: {e, d, y} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = b0 val b1 = zs.map[{d, y} Cap -> Unit](a0) val b1c: {e} LIST[{d, y} Cap -> Unit] = b1 val b2 = zs.map[{d, y} Cap -> Unit](eff[{d, y} Cap -> Unit]) @@ -80,7 +80,7 @@ def test(c: Cap, d: Cap, e: Cap) = val b6c: {e} LIST[{d, c} Cap -> Unit] = b6 val c0 = eff2[{d, y} Cap -> Unit] - val c0c: {e} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = c0 + val c0c: {e, d, y} ({d, y} Cap -> Unit) -> {d, y} Cap -> Unit = c0 val c1 = zs.map[{d, y} Cap -> Unit](a0) val c1c: {e} LIST[{d, y} Cap -> Unit] = c1 val c2 = zs.map[{d, y} Cap -> Unit](eff2[{d, y} Cap -> Unit]) From e08a59f8c7b9a8e0824ec95d68955329c5e6e483 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Jul 2022 18:22:39 +0200 Subject: [PATCH 61/99] Make classes added by cc experimental --- library/src-bootstrapped/scala/Retains.scala | 3 ++- .../scala/internal/requiresCapability.scala | 4 ++-- library/src-bootstrapped/scala/retainsByName.scala | 3 ++- library/src/scala/annotation/capability.scala | 3 ++- library/src/scala/annotation/internal/CaptureChecked.scala | 3 ++- .../tasty-inspector/stdlibExperimentalDefinitions.scala | 7 +++++++ 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/library/src-bootstrapped/scala/Retains.scala b/library/src-bootstrapped/scala/Retains.scala index f3bfa282a012..33a6b32a07d3 100644 --- a/library/src-bootstrapped/scala/Retains.scala +++ b/library/src-bootstrapped/scala/Retains.scala @@ -1,6 +1,7 @@ package scala +import annotation.experimental /** An annotation that indicates capture */ -class retains(xs: Any*) extends annotation.StaticAnnotation +@experimental class retains(xs: Any*) extends annotation.StaticAnnotation diff --git a/library/src-bootstrapped/scala/internal/requiresCapability.scala b/library/src-bootstrapped/scala/internal/requiresCapability.scala index 371c44173f0b..d376ba565211 100644 --- a/library/src-bootstrapped/scala/internal/requiresCapability.scala +++ b/library/src-bootstrapped/scala/internal/requiresCapability.scala @@ -1,8 +1,8 @@ package scala.annotation.internal -import scala.annotation.StaticAnnotation +import annotation.{StaticAnnotation, experimental} /** An annotation to record a required capaility in the type of a throws */ -class requiresCapability(capability: Any) extends StaticAnnotation +@experimental class requiresCapability(capability: Any) extends StaticAnnotation diff --git a/library/src-bootstrapped/scala/retainsByName.scala b/library/src-bootstrapped/scala/retainsByName.scala index c530f35ec0e4..ed4991c2da36 100644 --- a/library/src-bootstrapped/scala/retainsByName.scala +++ b/library/src-bootstrapped/scala/retainsByName.scala @@ -1,6 +1,7 @@ package scala +import annotation.experimental /** An annotation that indicates capture of an enclosing by-name type */ -class retainsByName(xs: Any*) extends annotation.StaticAnnotation +@experimental class retainsByName(xs: Any*) extends annotation.StaticAnnotation diff --git a/library/src/scala/annotation/capability.scala b/library/src/scala/annotation/capability.scala index 98c6e15e023a..4696ed6a015e 100644 --- a/library/src/scala/annotation/capability.scala +++ b/library/src/scala/annotation/capability.scala @@ -1,4 +1,5 @@ package scala.annotation +import annotation.experimental /** Marks an annotated class as a capability. * If the annotation is present and -Ycc is set, any (possibly aliased @@ -10,4 +11,4 @@ package scala.annotation * THere, the capture set of any instance of `CanThrow` is assumed to be * `{*}`. */ -final class capability extends StaticAnnotation +@experimental final class capability extends StaticAnnotation diff --git a/library/src/scala/annotation/internal/CaptureChecked.scala b/library/src/scala/annotation/internal/CaptureChecked.scala index 3ffea31b898c..8392189f11f7 100644 --- a/library/src/scala/annotation/internal/CaptureChecked.scala +++ b/library/src/scala/annotation/internal/CaptureChecked.scala @@ -1,8 +1,9 @@ package scala.annotation package internal +import annotation.experimental /** A marker annotation on a toplevel class that indicates * that the class was checked under -Ycc */ -class CaptureChecked extends StaticAnnotation +@experimental class CaptureChecked extends StaticAnnotation diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index a65a50ab54ad..f934958ea90c 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -46,6 +46,13 @@ val experimentalDefinitionInLibrary = Set( "scala.annotation.newMain.Help$", "scala.annotation.newMain.Names", + //// New feature: capture checking + "scala.annotation.capability", + "scala.annotation.internal.CaptureChecked", + "scala.annotation.internal.requiresCapability", + "scala.retains", + "scala.retainsByName", + //// New APIs: Mirror // Can be stabilized in 3.3.0 or later. "scala.deriving.Mirror$.fromProductTyped", // This API is a bit convoluted. We may need some more feedback before we can stabilize it. From 8c2b6bf90aeee722a58fbf298683881d06470a6f Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 27 Jul 2022 18:38:32 +0200 Subject: [PATCH 62/99] Also print boxes for variable capture sets with no elems defined --- compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala | 2 +- tests/neg-custom-args/captures/boxmap.check | 2 +- tests/neg-custom-args/captures/lazylist.check | 2 +- tests/neg-custom-args/captures/lazylists2.check | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 56edd2b68342..4aeced3d9522 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -208,7 +208,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if ctx.settings.YccDebug.value then changePrec(GlobalPrec)(box ~ refs.show ~ " " ~ toText(parent)) else if !refs.isConst && refs.elems.isEmpty then - changePrec(GlobalPrec)("?" ~ " " ~ toText(parent)) + changePrec(GlobalPrec)(box ~ "?" ~ " " ~ toText(parent)) else if Config.printCaptureSetsAsPrefix then changePrec(GlobalPrec)(box ~ toText(refs) ~ " " ~ toText(parent)) else diff --git a/tests/neg-custom-args/captures/boxmap.check b/tests/neg-custom-args/captures/boxmap.check index f472fabfc671..7b7b3e0f6e96 100644 --- a/tests/neg-custom-args/captures/boxmap.check +++ b/tests/neg-custom-args/captures/boxmap.check @@ -1,7 +1,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boxmap.scala:12:2 ---------------------------------------- 12 | () => b[Box[B]]((x: A) => box(f(x))) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: {f} () -> ? Box[? B] + | Found: {f} () -> ? Box[box ? B] | Required: () -> Box[B] | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index e43538ad97f7..bde717689a32 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -37,6 +37,6 @@ 17 | def tail = xs() // error: cannot have an inferred type | ^^^^^^^^^^^^^^^ | Non-local method tail cannot have an inferred result type - | {LazyCons.this.xs} lazylists.LazyList[? T] + | {LazyCons.this.xs} lazylists.LazyList[box ? T] | with non-empty capture set {LazyCons.this.xs}. | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index 41881b57da24..f431bca2d086 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -8,7 +8,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ 18 | final class Mapped extends LazyList[B]: // error | ^ - | Found: {f, xs} LazyList[? B] + | Found: {f, xs} LazyList[box ? B] | Required: {f} LazyList[B] 19 | this: ({xs, f} Mapped) => 20 | def isEmpty = false @@ -20,7 +20,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------ 27 | final class Mapped extends LazyList[B]: // error | ^ - | Found: {f, xs} LazyList[? B] + | Found: {f, xs} LazyList[box ? B] | Required: {xs} LazyList[B] 28 | this: ({xs, f} Mapped) => 29 | def isEmpty = false From 6d30fd2875805761445ab2139384a71666323d91 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Jul 2022 13:40:08 +0200 Subject: [PATCH 63/99] More fixes related to boxing 1. Refine mightAccountFor Previously we had {x} <: {} if the capture set of `x` did not yet have any elements defined in its capture set. We now require that `x` has at least one element in its capture set before we compare it recursively. On the other hand, every set might subcapture a set that contains `*`. 2. Restrict narrowing rule in applications As for selections, we can choose the original capanilities of an application over its result only if they are not boxed. 3. Don't charge environent with references inside box operations. 4. Disable addResultBoxes For now it is not clear what it should achieve. We should get all the other boxing related operations in order before coming back to it. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 8 +++-- .../dotty/tools/dotc/transform/Recheck.scala | 8 ++--- .../tools/dotc/typer/CheckCaptures.scala | 36 +++++++++++++------ tests/neg-custom-args/captures/bounded.scala | 5 +-- tests/neg-custom-args/captures/boxmap.scala | 12 ------- tests/pos-custom-args/captures/boxed1.scala | 11 ++++++ tests/pos-custom-args/captures/boxmap.scala | 4 +-- 7 files changed, 52 insertions(+), 32 deletions(-) delete mode 100644 tests/neg-custom-args/captures/boxmap.scala create mode 100644 tests/pos-custom-args/captures/boxed1.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 4563be74c158..e5e9da9e3309 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -124,8 +124,12 @@ sealed abstract class CaptureSet extends Showable: */ def mightAccountFor(x: CaptureRef)(using Context): Boolean = reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { - elems.exists(_.subsumes(x)) - || !x.isRootCapability && x.captureSetOfInfo.elems.forall(mightAccountFor) + elems.exists(elem => elem.subsumes(x) || elem.isRootCapability) + || !x.isRootCapability + && { + val elems = x.captureSetOfInfo.elems + !elems.isEmpty && elems.forall(mightAccountFor) + } } /** A more optimistic version of subCaptures used to choose one of two typing rules diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 62e65d306c16..0975e5fde975 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -388,17 +388,17 @@ abstract class Recheck extends Phase, SymTransformer: // Don't report closure nodes, since their span is a point; wait instead // for enclosing block to preduce an error case _ => - checkConformsExpr(tpe, tpe.widenExpr, pt.widenExpr, tree) + checkConformsExpr(tpe.widenExpr, pt.widenExpr, tree) - def checkConformsExpr(original: Type, actual: Type, expected: Type, tree: Tree)(using Context): Unit = + def checkConformsExpr(actual: Type, expected: Type, tree: Tree)(using Context): Unit = //println(i"check conforms $actual <:< $expected") val isCompatible = actual <:< expected || expected.isRepeatedParam && actual <:< expected.translateFromRepeated(toArray = tree.tpe.isRef(defn.ArrayClass)) if !isCompatible then - recheckr.println(i"conforms failed for ${tree}: $original vs $expected") - err.typeMismatch(tree.withType(original), expected) + recheckr.println(i"conforms failed for ${tree}: $actual vs $expected") + err.typeMismatch(tree.withType(actual), expected) else if debugSuccesses then tree match case _: Ident => diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 4bb4534fd61b..81b780ad203f 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -149,7 +149,7 @@ class CheckCaptures extends Recheck, SymTransformer: interpolator().traverse(tpt.knownType) .showing(i"solved vars in ${tpt.knownType}", capt) - private var curEnv: Env = Env(NoSymbol, CaptureSet.empty, false, null) + private var curEnv: Env = Env(NoSymbol, CaptureSet.empty, isBoxed = false, null) private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap() def capturedVars(sym: Symbol)(using Context) = @@ -204,9 +204,13 @@ class CheckCaptures extends Recheck, SymTransformer: * * and that also propagates C into the type of the unboxing expression. * TODO: Generalize this to boxed captues in other parts of a function type. + * Test case in pos-.../boxed1.scala. */ def addResultBoxes(tp: Type)(using Context): Type = - def includeBoxed(res: Type) = tp.capturing(res.boxedCaptured) + def includeBoxed(res: Type) = + //if !res.boxedCaptured.isAlwaysEmpty then + // println(i"add boxed $tp from ${res.boxedCaptured}") + tp.capturing(res.boxedCaptured) val tp1 = tp.dealias val boxedTp = tp1 match case tp1 @ AppliedType(_, args) if defn.isNonRefinedFunction(tp1) => @@ -219,7 +223,7 @@ class CheckCaptures extends Recheck, SymTransformer: else boxedParent.capturing(refs) case _ => tp1 - if boxedTp eq tp1 then tp else boxedTp + if (boxedTp eq tp1) then tp else boxedTp end addResultBoxes def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = @@ -323,7 +327,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !Synthetics.isExcluded(sym) then val saved = curEnv val localSet = capturedVars(sym) - if !localSet.isAlwaysEmpty then curEnv = Env(sym, localSet, false, curEnv) + if !localSet.isAlwaysEmpty then curEnv = Env(sym, localSet, isBoxed = false, curEnv) try super.recheckDefDef(tree, sym) finally interpolateVarsIn(tree.tpt) @@ -334,7 +338,7 @@ class CheckCaptures extends Recheck, SymTransformer: val localSet = capturedVars(cls) for parent <- impl.parents do checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos) - if !localSet.isAlwaysEmpty then curEnv = Env(cls, localSet, false, curEnv) + if !localSet.isAlwaysEmpty then curEnv = Env(cls, localSet, isBoxed = false, curEnv) try val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") checkSubset(localSet, thisSet, tree.srcPos) @@ -425,10 +429,12 @@ class CheckCaptures extends Recheck, SymTransformer: if !tree.fun.symbol.isConstructor && qual.tpe.captureSet.mightSubcapture(refs) && tree.args.forall(_.tpe.captureSet.mightSubcapture(refs)) + && !qual.tpe.isBoxedCapturing + && !tree.args.exists(_.tpe.isBoxedCapturing) => tp.derivedCapturingType(tp1, tree.args.foldLeft(qual.tpe.captureSet)((cs, arg) => cs ++ arg.tpe.captureSet)) - .showing(i"narrow $tree: $tp --> $result", capt) + .showing(i"narrow $tree: $tp, refs = $refs, qual = ${qual.tpe.captureSet} --> $result", capt) case _ => tp case tp => tp @@ -443,7 +449,13 @@ class CheckCaptures extends Recheck, SymTransformer: super.recheckTyped(tree) override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = - val res = super.recheck(tree, pt) + val res = + if tree.isTerm && pt.isBoxedCapturing then + val saved = curEnv + curEnv = Env(curEnv.owner, CaptureSet.Var(), isBoxed = true, curEnv) + try super.recheck(tree, pt) + finally curEnv = saved + else super.recheck(tree, pt) if tree.isTerm then includeBoxedCaptures(res, tree.srcPos) res @@ -470,7 +482,7 @@ class CheckCaptures extends Recheck, SymTransformer: checkNotUniversal(parent) case _ => checkNotUniversal(typeToCheck) - val tpe1 = if tree.isTerm then addResultBoxes(tpe) else tpe + val tpe1 = if false && tree.isTerm then addResultBoxes(tpe) else tpe super.recheckFinish(tpe1, tree, pt) /** This method implements the rule outlined in #14390: @@ -482,7 +494,7 @@ class CheckCaptures extends Recheck, SymTransformer: * they are already accounted for by `Cx` and adding them explicitly to `Cx` * changes nothing. */ - override def checkConformsExpr(original: Type, actual: Type, expected: Type, tree: Tree)(using Context): Unit = + override def checkConformsExpr(actual: Type, expected: Type, tree: Tree)(using Context): Unit = def isPure(info: Type): Boolean = info match case info: PolyType => isPure(info.resType) case info: MethodType => info.paramInfos.forall(_.captureSet.isAlwaysEmpty) && isPure(info.resType) @@ -511,8 +523,12 @@ class CheckCaptures extends Recheck, SymTransformer: expected.derivedCapturingType(ecore, erefs1) case _ => expected + val actual1 = adaptBoxed(actual, expected1, covariant = true) //println(i"check conforms $actual <<< $expected1") - super.checkConformsExpr(original, actual, expected1, tree) + super.checkConformsExpr(actual1, expected1, tree) + + def adaptBoxed(actual: Type, expected: Type, covariant: Boolean)(using Context): Type = + actual override def checkUnit(unit: CompilationUnit)(using Context): Unit = Setup(preRecheckPhase, thisPhase, recheckDef) diff --git a/tests/neg-custom-args/captures/bounded.scala b/tests/neg-custom-args/captures/bounded.scala index 12bdb9ac8854..59f4c6345289 100644 --- a/tests/neg-custom-args/captures/bounded.scala +++ b/tests/neg-custom-args/captures/bounded.scala @@ -1,3 +1,4 @@ +// To be revisited class CC type Cap = {*} CC @@ -14,7 +15,7 @@ def test(c: Cap) = val r2c: () -> {c} Int -> Int = r2 // error def test2(c: Cap) = - class B[X](x: X): + class B[X <: {*} Any](x: X): def elem = x def lateElem = () => x @@ -23,4 +24,4 @@ def test2(c: Cap) = val r1 = b.elem val r1c: {c} Int -> Int = r1 val r2 = b.lateElem - val r2c: () -> {c} Int -> Int = r2 // error \ No newline at end of file + val r2c: () -> {c} Int -> Int = r2 // was error now OK \ No newline at end of file diff --git a/tests/neg-custom-args/captures/boxmap.scala b/tests/neg-custom-args/captures/boxmap.scala deleted file mode 100644 index 114aaccb6bb5..000000000000 --- a/tests/neg-custom-args/captures/boxmap.scala +++ /dev/null @@ -1,12 +0,0 @@ -type Top = Any @retains(*) - -type Box[+T <: Top] = ([K <: Top] -> (T => K) -> K) - -def box[T <: Top](x: T): Box[T] = - [K <: Top] => (k: T => K) => k(x) - -def map[A <: Top, B <: Top](b: Box[A])(f: A => B): Box[B] = - b[Box[B]]((x: A) => box(f(x))) - -def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B): () -> Box[B] = - () => b[Box[B]]((x: A) => box(f(x))) // error diff --git a/tests/pos-custom-args/captures/boxed1.scala b/tests/pos-custom-args/captures/boxed1.scala new file mode 100644 index 000000000000..ba198335f51d --- /dev/null +++ b/tests/pos-custom-args/captures/boxed1.scala @@ -0,0 +1,11 @@ +class Box[T](val x: T) + +@annotation.capability class Cap + +def foo(x: => Int): Unit = () + +def test(c: Cap) = + val f = () => { c; 1 } + val _: {c} () -> Int = f + val g = () => Box(f) + val _: () -> Box[{f} () -> Int] = g diff --git a/tests/pos-custom-args/captures/boxmap.scala b/tests/pos-custom-args/captures/boxmap.scala index 003e46804a9d..2b610e55b871 100644 --- a/tests/pos-custom-args/captures/boxmap.scala +++ b/tests/pos-custom-args/captures/boxmap.scala @@ -8,11 +8,11 @@ def box[T <: Top](x: T): Box[T] = def map[A <: Top, B <: Top](b: Box[A])(f: A => B): Box[B] = b[Box[B]]((x: A) => box(f(x))) -def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B): (() -> Box[B]) @retains(f) = +def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B): (() -> Box[B]) = () => b[Box[B]]((x: A) => box(f(x))) def test[A <: Top, B <: Top] = def lazymap[A <: Top, B <: Top](b: Box[A])(f: A => B) = () => b[Box[B]]((x: A) => box(f(x))) - val x: (b: Box[A]) -> (f: A => B) -> (() -> Box[B]) @retains(f) = lazymap[A, B] + val x: (b: Box[A]) -> (f: A => B) -> (() -> Box[B]) = lazymap[A, B] () From f12f221c9909e09763c0991026594071226e511a Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 29 Jul 2022 17:56:37 +0200 Subject: [PATCH 64/99] Don't follow abstract in boxed capturing Also: Don't include TermParamRefs in includeBoxed --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 10 ++++------ .../src/dotty/tools/dotc/typer/CheckCaptures.scala | 12 +++++++++--- .../bounded.scala => pos-custom-args/bounded1.scala} | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) rename tests/{neg-custom-args/captures/bounded.scala => pos-custom-args/bounded1.scala} (90%) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 960453407d10..e82ca7f4ebcb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -40,15 +40,13 @@ extension (tp: Type) if (parent eq p) && (refs eq r) then tp else CapturingType(parent, refs, tp.isBoxed) - /** If this is type variable instantiated or upper bounded with a capturing type, - * the capture set associated with that type. Extended to and-or types and - * type proxies in the obvious way. If a term has a type with a boxed captureset, - * that captureset counts towards the capture variables of the envirionment. - */ + /** The boxed capture set of a type */ def boxedCaptured(using Context): CaptureSet = def getBoxed(tp: Type): CaptureSet = tp match case tp @ CapturingType(parent, refs) => - if tp.isBoxed then refs else getBoxed(parent) + val pcs = getBoxed(parent) + if tp.isBoxed then refs ++ pcs else pcs + case tp: TypeRef if tp.symbol.isAbstractType => CaptureSet.empty case tp: TypeProxy => getBoxed(tp.superType) case tp: AndType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) case tp: OrType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 81b780ad203f..0dbb2133cd2e 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -186,11 +186,15 @@ class CheckCaptures extends Recheck, SymTransformer: includeIn(curEnv.outer) def includeBoxedCaptures(tp: Type, pos: SrcPos)(using Context): Unit = + includeBoxedCaptures(tp.boxedCaptured, pos) + + def includeBoxedCaptures(refs: CaptureSet, pos: SrcPos)(using Context): Unit = if curEnv.isOpen then val ownEnclosure = ctx.owner.enclosingMethodOrClass - val targetSet = tp.boxedCaptured.filter { + val targetSet = refs.filter { case ref: TermRef => ref.symbol.enclosure != ownEnclosure - case _ => true + case ref: ThisType => true + case _ => false } checkSubset(targetSet, curEnv.captured, pos) @@ -255,7 +259,9 @@ class CheckCaptures extends Recheck, SymTransformer: override def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = { val selType = super.recheckSelection(tree, qualType, name) val selCs = selType.widen.captureSet - if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then + if selCs.isAlwaysEmpty + || selType.widen.isBoxedCapturing + || qualType.isBoxedCapturing then selType else val qualCs = qualType.captureSet diff --git a/tests/neg-custom-args/captures/bounded.scala b/tests/pos-custom-args/bounded1.scala similarity index 90% rename from tests/neg-custom-args/captures/bounded.scala rename to tests/pos-custom-args/bounded1.scala index 59f4c6345289..5fb7f0da904b 100644 --- a/tests/neg-custom-args/captures/bounded.scala +++ b/tests/pos-custom-args/bounded1.scala @@ -12,7 +12,7 @@ def test(c: Cap) = val r1 = b.elem val r1c: {c} Int -> Int = r1 val r2 = b.lateElem - val r2c: () -> {c} Int -> Int = r2 // error + val r2c: () -> {c} Int -> Int = r2 // was error now OK def test2(c: Cap) = class B[X <: {*} Any](x: X): From f6407e377a32397a20ad54aab9386038bfd5abc9 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Aug 2022 16:29:45 +0200 Subject: [PATCH 65/99] WIP: Turn on box-aware subtyping --- .../src/dotty/tools/dotc/config/Config.scala | 2 + .../dotty/tools/dotc/core/Definitions.scala | 4 +- .../dotty/tools/dotc/core/TypeComparer.scala | 15 +++-- .../tools/dotc/typer/CheckCaptures.scala | 66 +++++++++++++++++-- tests/neg-custom-args/captures/capt1.check | 11 +++- tests/neg-custom-args/captures/capt1.scala | 3 +- .../captures/curried-simplified.check | 2 +- tests/neg-custom-args/captures/i15749a.scala | 21 ++++++ tests/neg-custom-args/captures/try.check | 18 ++--- tests/neg-custom-args/captures/try.scala | 4 +- tests/neg-custom-args/captures/vars.check | 7 ++ tests/neg-custom-args/captures/vars.scala | 2 +- .../captures/lazylists-mono.scala | 2 +- .../captures/colltest5/Test_2.scala | 6 +- 14 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 tests/neg-custom-args/captures/i15749a.scala diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index 1b0fea9184d1..e99e41284cce 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -245,4 +245,6 @@ object Config { * cases, though. */ inline val ccAllowUnsoundMaps = false + + @annotation.internal.sharable var checkBoxes = true } diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 3673704e3471..6c9e3915bd80 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1650,8 +1650,8 @@ class Definitions { def isFunctionType(tp: Type)(using Context): Boolean = isNonRefinedFunction(tp.dropDependentRefinement) - def isFunctionOrPolyType(tp: RefinedType)(using Context): Boolean = - isFunctionType(tp) || (tp.parent.typeSymbol eq defn.PolyFunctionClass) + def isFunctionOrPolyType(tp: Type)(using Context): Boolean = + isFunctionType(tp) || (tp.typeSymbol eq defn.PolyFunctionClass) private def withSpecMethods(cls: ClassSymbol, bases: List[Name], paramTypes: Set[TypeRef]) = for base <- bases; tp <- paramTypes do diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 263a66d5fc3a..db7711c41862 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,7 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly -import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing} +import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing} /** Provides methods to compare types. */ @@ -514,10 +514,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // loop for a very long time without the recursion brake. case CapturingType(parent1, refs1) => - if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK then - recur(parent1, tp2) - else - thirdTry + if subCaptures(refs1, tp2.captureSet, frozenConstraint).isOK && sameBoxed(tp1, tp2, refs1) + then recur(parent1, tp2) + else thirdTry case tp1: AnnotatedType if !tp1.isRefining => recur(tp1.parent, tp2) case tp1: MatchType => @@ -803,6 +802,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling try if refs1.isAlwaysEmpty then recur(tp1, parent2) else subCaptures(refs1, refs2, frozenConstraint).isOK + && sameBoxed(tp1, tp2, refs1) && recur(tp1.widen.stripCapturing, parent2) catch case ex: AssertionError => println(i"assertion failed while compare captured $tp1 <:< $tp2") @@ -2554,6 +2554,11 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult.Type = refs1.subCaptures(refs2, frozen) + protected def sameBoxed(tp1: Type, tp2: Type, refs: CaptureSet)(using Context): Boolean = + !Config.checkBoxes + || (tp1.isBoxedCapturing == tp2.isBoxedCapturing) + || refs.subCaptures(CaptureSet.empty, frozenConstraint).isOK + // ----------- Diagnostics -------------------------------------------------- /** A hook for showing subtype traces. Overridden in ExplainingTypeComparer */ diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 0dbb2133cd2e..9df524374d27 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -7,6 +7,7 @@ import Phases.*, DenotTransformers.*, SymDenotations.* import Contexts.*, Names.*, Flags.*, Symbols.*, Decorators.* import Types.*, StdNames.* import config.Printers.{capt, recheckr} +import config.Config import ast.{tpd, untpd, Trees} import Trees.* import typer.RefChecks.{checkAllOverrides, checkParents} @@ -120,7 +121,9 @@ class CheckCaptures extends Recheck, SymTransformer: // ^^^ TODO: Can we avoid doing overrides checks twice? // We need to do them here since only at this phase CaptureTypes are relevant // But maybe we can then elide the check during the RefChecks phase if -Ycc is set? + Config.checkBoxes = false // !!! checkAllOverrides(ctx.owner.asClass) + Config.checkBoxes = true // !!! case _ => traverseChildren(t) @@ -529,12 +532,63 @@ class CheckCaptures extends Recheck, SymTransformer: expected.derivedCapturingType(ecore, erefs1) case _ => expected - val actual1 = adaptBoxed(actual, expected1, covariant = true) - //println(i"check conforms $actual <<< $expected1") - super.checkConformsExpr(actual1, expected1, tree) - - def adaptBoxed(actual: Type, expected: Type, covariant: Boolean)(using Context): Type = - actual + val normActual = adaptBoxed(actual, expected1, tree.srcPos) + //println(i"check conforms $actual1 <<< $expected1") + super.checkConformsExpr(normActual, expected1, tree) + + /** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions */ + def adaptBoxed(actual: Type, expected: Type, pos: SrcPos)(using Context): Type = + + def adaptFun(actual: Type, aargs: List[Type], ares: Type, expected: Type, + covariant: Boolean, + reconstruct: (List[Type], Type) => Type): Type = + val (eargs, eres) = expected.dealias match + case defn.FunctionOf(eargs, eres, _, _) => (eargs, eres) + case _ => (aargs.map(_ => WildcardType), WildcardType) + val aargs1 = aargs.zipWithConserve(eargs)(adapt(_, _, !covariant)) + val ares1 = adapt(ares, eres, covariant) + if (ares1 eq ares) && (aargs1 eq aargs) then actual + else reconstruct(aargs1, ares1) + + def adapt(actual: Type, expected: Type, covariant: Boolean): Type = actual.dealias match + case actual @ CapturingType(parent, refs) => + val parent1 = adapt(parent, expected, covariant) + if actual.isBoxed != expected.isBoxedCapturing then + val uni = if covariant then refs.isUniversal else expected.captureSet.isUniversal + if uni then // TODO: better to constrain the set to be not universal + capt.println(i"ABORTING $actual vs $expected") + actual + else + if covariant == actual.isBoxed then includeBoxedCaptures(refs, pos) + CapturingType(parent1, refs, boxed = !actual.isBoxed) + else if parent1 eq parent then actual + else CapturingType(parent1, refs, boxed = actual.isBoxed) + case actual @ AppliedType(tycon, args) if defn.isNonRefinedFunction(actual) => + adaptFun(actual, args.init, args.last, expected, covariant, + (aargs1, ares1) => actual.derivedAppliedType(tycon, aargs1 :+ ares1)) + case actual @ RefinedType(_, _, rinfo: MethodType) if defn.isFunctionType(actual) => + adaptFun(actual, rinfo.paramInfos, rinfo.resType, expected, covariant, + (aargs1, ares1) => + rinfo.derivedLambdaType(paramInfos = aargs1, resType = ares1) + .toFunctionType(isJava = false, alwaysDependent = true)) + case _ => actual + + if Config.checkBoxes then + var actualw = actual.widenDealias + actual match + case ref: CaptureRef if ref.isTracked => + actualw match + case CapturingType(p, refs) => + actualw = actualw.derivedCapturingType(p, ref.singletonCaptureSet) + case _ => + case _ => + val adapted = adapt(actualw, expected, covariant = true) + if adapted ne actualw then + capt.println(i"adapt boxed $actual vs $expected ===> $adapted") + adapted + else actual + else actual + end adaptBoxed override def checkUnit(unit: CompilationUnit)(using Context): Unit = Setup(preRecheckPhase, thisPhase, recheckDef) diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 473121b01f81..62caa3e99d7e 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -38,9 +38,16 @@ | | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:31:24 ---------------------------------------- -31 | val z2 = h[() -> Cap](() => x)(() => C()) // error +31 | val z2 = h[() -> Cap](() => x) // error | ^^^^^^^ - | Found: {x} () -> Cap + | Found: {x} () -> {*} C | Required: () -> box {*} C | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:32:5 ----------------------------------------- +32 | (() => C()) // error + | ^^^^^^^^^ + | Found: ? () -> {*} C + | Required: () -> box {*} C + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index e230defda170..a7c78eb521c8 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -28,7 +28,8 @@ def h4(x: Cap, y: Int): A = def foo() = val x: C @retains(*) = ??? def h[X](a: X)(b: X) = a - val z2 = h[() -> Cap](() => x)(() => C()) // error + val z2 = h[() -> Cap](() => x) // error + (() => C()) // error val z3 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // ok val z4 = h[(() -> Cap) @retains(x)](() => x)(() => C()) // what was inferred for z3 diff --git a/tests/neg-custom-args/captures/curried-simplified.check b/tests/neg-custom-args/captures/curried-simplified.check index 937282bc2148..b91914f72404 100644 --- a/tests/neg-custom-args/captures/curried-simplified.check +++ b/tests/neg-custom-args/captures/curried-simplified.check @@ -8,7 +8,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/curried-simplified.scala:9:28 ---------------------------- 9 | def y2: () -> () => Int = x2 // error | ^^ - | Found: {x} () -> () => Int + | Found: {x} () -> {*} () -> Int | Required: () -> () => Int | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15749a.scala b/tests/neg-custom-args/captures/i15749a.scala new file mode 100644 index 000000000000..9e439e28e98c --- /dev/null +++ b/tests/neg-custom-args/captures/i15749a.scala @@ -0,0 +1,21 @@ +class Unit +object unit extends Unit + +type Top = {*} Any + +type Wrapper[T] = [X] -> (op: {*} T -> X) -> X + +def test = + + def wrapper[T](x: T): Wrapper[T] = + [X] => (op: {*} T -> X) => op(x) + + def strictMap[A <: Top, B <: Top](mx: Wrapper[A])(f: {*} A -> B): Wrapper[B] = + mx((x: A) => wrapper(f(x))) + + def force[A](thunk: {*} Unit -> A): A = thunk(unit) + + def forceWrapper[A](mx: Wrapper[{*} Unit -> A]): Wrapper[A] = + // Γ ⊢ mx: Wrapper[□ {*} Unit => A] + // `force` should be typed as ∀(□ {*} Unit -> A) A, but it can not + strictMap[{*} Unit -> A, A](mx)(t => force[A](t)) // error diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 95fe843db57d..b101acc383b6 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,16 +1,16 @@ --- Error: tests/neg-custom-args/captures/try.scala:24:3 ---------------------------------------------------------------- -22 | val a = handle[Exception, CanThrow[Exception]] { +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:22:49 ------------------------------------------ +22 | val a = handle[Exception, CanThrow[Exception]] { // error !!! was for 2nd arg + | ^ + | Found: ? ({*} CT[Exception]) -> {*} CT[? >: box ? Exception <: box ? Exception] + | Required: CanThrow[Exception] => box {*} CT[Exception] 23 | (x: CanThrow[Exception]) => x -24 | }{ // error - | ^ - | The expression's type box {*} CT[Exception] is not allowed to capture the root capability `*`. - | This usually means that a capability persists longer than its allowed lifetime. -25 | (ex: Exception) => ??? -26 | } +24 | }{ + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:28:43 ------------------------------------------ 28 | val b = handle[Exception, () -> Nothing] { // error | ^ - | Found: ? (x: CanThrow[Exception]) -> {x} () -> ? Nothing + | Found: ? (x: {*} CT[Exception]) -> {x} () -> ? Nothing | Required: CanThrow[Exception] => () -> Nothing 29 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) 30 | } { diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index c76da6641780..5696db515a85 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -19,9 +19,9 @@ def handle[E <: Exception, R <: Top](op: CanThrow[E] => R)(handler: E => R): R = catch case ex: E => handler(ex) def test = - val a = handle[Exception, CanThrow[Exception]] { + val a = handle[Exception, CanThrow[Exception]] { // error !!! was for 2nd arg (x: CanThrow[Exception]) => x - }{ // error + }{ (ex: Exception) => ??? } diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index b5a01558cd2c..1d06dcf3445c 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -15,6 +15,13 @@ | ^^^^^^ | The expression's type box {*} String -> String is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:21:13 ----------------------------------------- +21 | b = List(g) // error !!! Probably spurious due to box comparison + | ^ + | Found: Seq[{cap3} String -> ? String] + | Required: Seq[box {cap3} (x$0: ? String) -> ? String] + | + | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/vars.scala:30:8 --------------------------------------------------------------- 30 | local { cap3 => // error | ^ diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index 5e413b7ea3fb..8c10c93e7709 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -18,7 +18,7 @@ def test(cap1: Cap, cap2: Cap) = val cap3: Cap = CC() def g(x: String): String = if cap3 == cap3 then "" else "a" a = g - b = List(g) + b = List(g) // error !!! Probably spurious due to box comparison val gc = g g diff --git a/tests/pos-custom-args/captures/lazylists-mono.scala b/tests/pos-custom-args/captures/lazylists-mono.scala index 44ab36ded6a2..57faad6392e3 100644 --- a/tests/pos-custom-args/captures/lazylists-mono.scala +++ b/tests/pos-custom-args/captures/lazylists-mono.scala @@ -24,4 +24,4 @@ def test(E: Cap) = if xs.isEmpty then LazyNil else val cons = () => (f(xs.head), xs.tail.map(f)) - LazyCons(cons) + new LazyCons(cons.asInstanceOf) // !!! diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index a0d90a474e7f..4bf2c8227dd0 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -76,11 +76,15 @@ object Test { val x4 = xs.head val y4: Int = x4 val x5 = xs.to(List) - val y5: {x5} List[Int] = x5 // !!! + val y5: {x5} List[Int] = x5 val (xs6, xs7) = xs.partition(isEven) + .asInstanceOf[({xs, isEven} View[Int], {xs, isEven} View[Int])] + // !!! fails deep subtyping test without the cast val ys6: {xs6, isEven} View[Int] = xs6 val ys7: {xs7, isEven} View[Int] = xs7 val (xs6a, xs7a) = xs.partition(_ % 2 == 0) + .asInstanceOf[({xs} View[Int], {xs} View[Int])] + // !!! fails deep subtyping test without the cast val ys6a: {xs6} View[Int] = xs6 val ys7a: {xs7} View[Int] = xs7 val xs8 = xs.drop(2) From 7066ef7a875325ade7760de4c7581122e80e7215 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Aug 2022 17:35:19 +0200 Subject: [PATCH 66/99] Cache boxed versions of CapturingTypes --- .../tools/dotc/cc/CaptureAnnotation.scala | 2 ++ .../src/dotty/tools/dotc/cc/CaptureOps.scala | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index 7a1f8ca4eb8a..402583d2eda4 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -16,6 +16,8 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte import CaptureAnnotation.* import tpd.* + var boxedType: Type = NoType + override def tree(using Context) = val elems = refs.elems.toList.map { case cr: TermRef => ref(cr) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index e82ca7f4ebcb..082af5c29515 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -11,6 +11,7 @@ import util.Property.Key import tpd.* private val Captures: Key[CaptureSet] = Key() +private val Boxed: Key[Type] = Key() def retainedElems(tree: Tree)(using Context): List[Tree] = tree match case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems @@ -40,6 +41,22 @@ extension (tp: Type) if (parent eq p) && (refs eq r) then tp else CapturingType(parent, refs, tp.isBoxed) + def boxed(using Context): Type = tp.dealias match + case tp @ CapturingType(parent, refs) => + def boxedTp = CapturingType(parent, refs, boxed = true) + if tp.isBoxed || refs.isAlwaysEmpty then tp + else tp.annot match + case ann: CaptureAnnotation => + if !ann.boxedType.exists then ann.boxedType = boxedTp + ann.boxedType + case ann => + ann.tree.getAttachment(Boxed) match + case None => ann.tree.putAttachment(Boxed, boxedTp) + case _ => + ann.tree.attachment(Boxed) + case _ => + tp + /** The boxed capture set of a type */ def boxedCaptured(using Context): CaptureSet = def getBoxed(tp: Type): CaptureSet = tp match @@ -105,3 +122,4 @@ extension (tp: AnnotatedType) def isBoxed(using Context): Boolean = tp.annot match case ann: CaptureAnnotation => ann.boxed case _ => false + From f73a73a586ccc5193ac7a6b3745612124a307b0c Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 7 Aug 2022 18:48:36 +0200 Subject: [PATCH 67/99] Auto-boxing of arguments of applied types --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 12 +++++++++++- compiler/src/dotty/tools/dotc/config/Config.scala | 2 +- .../src/dotty/tools/dotc/core/TypeApplications.scala | 3 ++- .../src/dotty/tools/dotc/core/TypeComparer.scala | 4 ++-- compiler/src/dotty/tools/dotc/core/Types.scala | 6 +++--- .../src/dotty/tools/dotc/typer/CheckCaptures.scala | 2 -- tests/neg-custom-args/captures/vars.check | 7 ------- tests/neg-custom-args/captures/vars.scala | 2 +- 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 082af5c29515..2e015f6b3f9e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -43,7 +43,11 @@ extension (tp: Type) def boxed(using Context): Type = tp.dealias match case tp @ CapturingType(parent, refs) => - def boxedTp = CapturingType(parent, refs, boxed = true) + def boxedTp = parent.boxed match + case CapturingType(parent1, refs1) => + CapturingType(parent1, refs ++ refs1, boxed = true) + case parent1 => + CapturingType(parent1, refs, boxed = true) if tp.isBoxed || refs.isAlwaysEmpty then tp else tp.annot match case ann: CaptureAnnotation => @@ -57,6 +61,12 @@ extension (tp: Type) case _ => tp + def boxedUnlessFun(tycon: Type)(using Context) = + if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionClass(tycon.typeSymbol) + then tp + else tp.boxed + //.showing(i"boxedUF $tp in $tycon = $result") + /** The boxed capture set of a type */ def boxedCaptured(using Context): CaptureSet = def getBoxed(tp: Type): CaptureSet = tp match diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index e99e41284cce..e4ca4105e3c0 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -246,5 +246,5 @@ object Config { */ inline val ccAllowUnsoundMaps = false - @annotation.internal.sharable var checkBoxes = true + val checkBoxes = true } diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index e3daa64e61ba..7ab0a20277d2 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -12,6 +12,7 @@ import Names._ import Flags.{Module, Provisional} import dotty.tools.dotc.config.Config import cc.CaptureSet.IdentityCaptRefMap +import cc.boxedUnlessFun object TypeApplications { @@ -494,7 +495,7 @@ class TypeApplications(val self: Type) extends AnyVal { * Existential types in arguments are returned as TypeBounds instances. */ final def argInfos(using Context): List[Type] = self.stripped match { - case AppliedType(tycon, args) => args + case AppliedType(tycon, args) => args.mapconserve(_.boxedUnlessFun(tycon)) case _ => Nil } diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index db7711c41862..a6710ee77785 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,7 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly -import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing} +import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxedUnlessFun} /** Provides methods to compare types. */ @@ -1635,7 +1635,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling else if v > 0 then isSubType(arg1, arg2) else isSameType(arg2, arg1) - isSubArg(args1.head, args2.head) + isSubArg(args1.head.boxedUnlessFun(tp1), args2.head.boxedUnlessFun(tp1)) } && recurArgs(args1.tail, args2.tail, tparams2.tail) recurArgs(args1, args2, tparams2) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e9b347778ed3..cc75a73d1f37 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,7 +36,7 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized -import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, EventuallyCapturingType} +import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -2538,7 +2538,7 @@ object Types { val cls = tparam.owner val base = pre.baseType(cls) base.stripped match { - case AppliedType(_, allArgs) => + case AppliedType(tycon, allArgs) => var tparams = cls.typeParams var args = allArgs var idx = 0 @@ -2546,7 +2546,7 @@ object Types { if (tparams.head.eq(tparam)) return args.head match { case _: TypeBounds if !widenAbstract => TypeRef(pre, tparam) - case arg => arg + case arg => arg.boxedUnlessFun(tycon) } tparams = tparams.tail args = args.tail diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 9df524374d27..7be27129c5c9 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -121,9 +121,7 @@ class CheckCaptures extends Recheck, SymTransformer: // ^^^ TODO: Can we avoid doing overrides checks twice? // We need to do them here since only at this phase CaptureTypes are relevant // But maybe we can then elide the check during the RefChecks phase if -Ycc is set? - Config.checkBoxes = false // !!! checkAllOverrides(ctx.owner.asClass) - Config.checkBoxes = true // !!! case _ => traverseChildren(t) diff --git a/tests/neg-custom-args/captures/vars.check b/tests/neg-custom-args/captures/vars.check index 1d06dcf3445c..b5a01558cd2c 100644 --- a/tests/neg-custom-args/captures/vars.check +++ b/tests/neg-custom-args/captures/vars.check @@ -15,13 +15,6 @@ | ^^^^^^ | The expression's type box {*} String -> String is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/vars.scala:21:13 ----------------------------------------- -21 | b = List(g) // error !!! Probably spurious due to box comparison - | ^ - | Found: Seq[{cap3} String -> ? String] - | Required: Seq[box {cap3} (x$0: ? String) -> ? String] - | - | longer explanation available when compiling with `-explain` -- Error: tests/neg-custom-args/captures/vars.scala:30:8 --------------------------------------------------------------- 30 | local { cap3 => // error | ^ diff --git a/tests/neg-custom-args/captures/vars.scala b/tests/neg-custom-args/captures/vars.scala index 8c10c93e7709..5e413b7ea3fb 100644 --- a/tests/neg-custom-args/captures/vars.scala +++ b/tests/neg-custom-args/captures/vars.scala @@ -18,7 +18,7 @@ def test(cap1: Cap, cap2: Cap) = val cap3: Cap = CC() def g(x: String): String = if cap3 == cap3 then "" else "a" a = g - b = List(g) // error !!! Probably spurious due to box comparison + b = List(g) val gc = g g From ad9d8f9e99b8d5deb967cc20003bb40d4fdef9f9 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Aug 2022 15:57:16 +0200 Subject: [PATCH 68/99] Fuse CapturingTypes --- compiler/src/dotty/tools/dotc/cc/CapturingType.scala | 6 +++++- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala | 2 +- tests/neg-custom-args/captures/try.scala | 2 +- tests/pos-custom-args/captures/lazylists-mono.scala | 2 +- tests/run-custom-args/captures/colltest5/Test_2.scala | 4 ---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index 19fe9728780c..f228804f9dd5 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -12,7 +12,11 @@ object CapturingType: def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type = if refs.isAlwaysEmpty then parent - else AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) + else parent match + case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed => + apply(parent1, refs ++ refs1, boxed) + case _ => + AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index ae3005ea75dc..3b68dd730513 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -28,7 +28,7 @@ extends tpd.TreeTraverser: private def box(tp: Type)(using Context): Type = tp.dealias match case tp @ CapturingType(parent, refs) if !tp.isBoxed => - CapturingType(parent, refs, boxed = true) + tp.boxed case tp1 @ AppliedType(tycon, args) if defn.isNonRefinedFunction(tp1) => val res = args.last val boxedRes = box(res) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 7be27129c5c9..011862c2ef18 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -531,7 +531,7 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => expected val normActual = adaptBoxed(actual, expected1, tree.srcPos) - //println(i"check conforms $actual1 <<< $expected1") + //println(i"check conforms $normActual <<< $expected1") super.checkConformsExpr(normActual, expected1, tree) /** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions */ diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 5696db515a85..3aa3a47f69b4 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -19,7 +19,7 @@ def handle[E <: Exception, R <: Top](op: CanThrow[E] => R)(handler: E => R): R = catch case ex: E => handler(ex) def test = - val a = handle[Exception, CanThrow[Exception]] { // error !!! was for 2nd arg + val a = handle[Exception, CanThrow[Exception]] { // error (x: CanThrow[Exception]) => x }{ (ex: Exception) => ??? diff --git a/tests/pos-custom-args/captures/lazylists-mono.scala b/tests/pos-custom-args/captures/lazylists-mono.scala index 57faad6392e3..44ab36ded6a2 100644 --- a/tests/pos-custom-args/captures/lazylists-mono.scala +++ b/tests/pos-custom-args/captures/lazylists-mono.scala @@ -24,4 +24,4 @@ def test(E: Cap) = if xs.isEmpty then LazyNil else val cons = () => (f(xs.head), xs.tail.map(f)) - new LazyCons(cons.asInstanceOf) // !!! + LazyCons(cons) diff --git a/tests/run-custom-args/captures/colltest5/Test_2.scala b/tests/run-custom-args/captures/colltest5/Test_2.scala index 4bf2c8227dd0..934223a9840b 100644 --- a/tests/run-custom-args/captures/colltest5/Test_2.scala +++ b/tests/run-custom-args/captures/colltest5/Test_2.scala @@ -78,13 +78,9 @@ object Test { val x5 = xs.to(List) val y5: {x5} List[Int] = x5 val (xs6, xs7) = xs.partition(isEven) - .asInstanceOf[({xs, isEven} View[Int], {xs, isEven} View[Int])] - // !!! fails deep subtyping test without the cast val ys6: {xs6, isEven} View[Int] = xs6 val ys7: {xs7, isEven} View[Int] = xs7 val (xs6a, xs7a) = xs.partition(_ % 2 == 0) - .asInstanceOf[({xs} View[Int], {xs} View[Int])] - // !!! fails deep subtyping test without the cast val ys6a: {xs6} View[Int] = xs6 val ys7a: {xs7} View[Int] = xs7 val xs8 = xs.drop(2) From 4c2bdc9fdc01a878087014c6ec74e9b5b2d3975b Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 8 Aug 2022 16:54:59 +0200 Subject: [PATCH 69/99] Add test --- tests/neg-custom-args/captures/i15772.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/neg-custom-args/captures/i15772.scala diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala new file mode 100644 index 000000000000..0479111a1d45 --- /dev/null +++ b/tests/neg-custom-args/captures/i15772.scala @@ -0,0 +1,19 @@ +class C { + def bad() = println("I've gone bad!") +} + +def newC: {*} C = C() + +type Observe[T] = (T => Unit) -> Unit +def unsafe(cap: {*} C) = cap.bad() + +def box[T](v: T) : Observe[T] = { + (fn: T => Unit) => fn(v) +} + +def main() : Int = { + val boxed : Observe[{*} C] = box(newC) + boxed(unsafe) + + 0 +} From 0f3d3ddb9a5832fbe03714fc17de301131d264ff Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 00:16:44 +0200 Subject: [PATCH 70/99] Fix init checker error --- 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 e5e9da9e3309..3e3925c0e272 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -423,6 +423,7 @@ object CaptureSet: abstract class DerivedVar(initialElems: Refs)(using @constructorOnly ctx: Context) extends Var(initialElems): def source: Var + val stack = if debugSets && this.isInstanceOf[Mapped] then (new Throwable).getStackTrace().nn.take(20) else null addSub(source) @@ -437,7 +438,6 @@ object CaptureSet: (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) extends DerivedVar(initial.elems): addSub(initial) - val stack = if debugSets then (new Throwable).getStackTrace().nn.take(20) else null override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = addNewElemsImpl(newElems: Refs, origin: CaptureSet) From 7006bea2ed686c3a88a173e73ddd23fda0c1e752 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 11:36:16 +0200 Subject: [PATCH 71/99] Fix test --- tests/neg-custom-args/captures/i15772.scala | 2 +- tests/neg-custom-args/captures/try.check | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index 0479111a1d45..f37a1066135c 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -12,7 +12,7 @@ def box[T](v: T) : Observe[T] = { } def main() : Int = { - val boxed : Observe[{*} C] = box(newC) + val boxed : Observe[{*} C] = box(newC) // error boxed(unsafe) 0 diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index b101acc383b6..1464499eddac 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,5 +1,5 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:22:49 ------------------------------------------ -22 | val a = handle[Exception, CanThrow[Exception]] { // error !!! was for 2nd arg +22 | val a = handle[Exception, CanThrow[Exception]] { // error | ^ | Found: ? ({*} CT[Exception]) -> {*} CT[? >: box ? Exception <: box ? Exception] | Required: CanThrow[Exception] => box {*} CT[Exception] From b9a559e34e5efbe409875ab7c04b0b20acad63d5 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 11:50:37 +0200 Subject: [PATCH 72/99] Don't box type arguments in Setup They will be boxed anyway when they are accessed. --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 26 ++++---------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 3b68dd730513..bdd9a8565853 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -45,33 +45,19 @@ extends tpd.TreeTraverser: else tp1.derivedLambdaType(resType = boxedRes) case _ => tp - private def addBoxes(using Context) = new TypeMap: - def apply(t: Type) = mapOver(t) match - case t1 @ AppliedType(tycon, args) if !defn.isNonRefinedFunction(t1) => - t1.derivedAppliedType(tycon, args.mapConserve(box)) - case t1: AliasingBounds => - t1.derivedAlias(t1.alias) - case t1 @ TypeBounds(lo, hi) => - t1.derivedTypeBounds(box(lo), box(hi)) - case t1 => - t1 - override def mapCapturingType(tp: Type, parent: Type, refs: CaptureSet, v: Int): Type = - tp.derivedCapturingType(this(parent), refs) - end addBoxes - /** Expand some aliases of function types to the underlying functions. * Right now, these are only $throws aliases, but this could be generalized. */ - def expandInlineAlias(tp: Type)(using Context) = tp match + def expandThrowsAlias(tp: Type)(using Context) = tp match case AppliedType(tycon, res :: exc :: Nil) if tycon.typeSymbol == defn.throwsAlias => // hard-coded expansion since $throws aliases in stdlib are defined with `?=>` rather than `?->` defn.FunctionOf(defn.CanThrowClass.typeRef.appliedTo(exc) :: Nil, res, isContextual = true, isErased = true) case _ => tp - private def expandInlineAliases(using Context) = new TypeMap: + private def expandThrowsAliases(using Context) = new TypeMap: def apply(t: Type) = t match case _: AppliedType => - val t1 = expandInlineAlias(t) + val t1 = expandThrowsAlias(t) if t1 ne t then apply(t1) else mapOver(t) case _: LazyRef => t @@ -208,7 +194,7 @@ extends tpd.TreeTraverser: try ts.mapConserve(this) finally isTopLevel = saved def apply(t: Type) = - val tp = expandInlineAlias(t) + val tp = expandThrowsAlias(t) val tp1 = tp match case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => apply(parent) @@ -296,9 +282,7 @@ extends tpd.TreeTraverser: if boxed then box(tp1) else tp1 private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = - var tp1 = addBoxes(tp) - if boxed then tp1 = box(tp1) - tp1 = expandInlineAliases(tp1) + val tp1 = expandThrowsAliases(if boxed then box(tp) else tp) if tp1 ne tp then capt.println(i"expanded: $tp --> $tp1") if ctx.settings.YccNoAbbrev.value then tp1 else expandAbbreviations(tp1) From 5659aafa7e8a4f1d95b06a55045f27f261a56124 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 11:55:56 +0200 Subject: [PATCH 73/99] Optimize boxedUnlessFun --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 10 ++++++++-- .../src/dotty/tools/dotc/core/TypeApplications.scala | 5 ++--- .../src/dotty/tools/dotc/typer/CheckCaptures.scala | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 2e015f6b3f9e..5c5d066edcb1 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -68,7 +68,7 @@ extension (tp: Type) //.showing(i"boxedUF $tp in $tycon = $result") /** The boxed capture set of a type */ - def boxedCaptured(using Context): CaptureSet = + def boxedCaptureSet(using Context): CaptureSet = def getBoxed(tp: Type): CaptureSet = tp match case tp @ CapturingType(parent, refs) => val pcs = getBoxed(parent) @@ -80,7 +80,7 @@ extension (tp: Type) case _ => CaptureSet.empty getBoxed(tp) - def isBoxedCapturing(using Context) = !tp.boxedCaptured.isAlwaysEmpty + def isBoxedCapturing(using Context) = !tp.boxedCaptureSet.isAlwaysEmpty def stripCapturing(using Context): Type = tp.dealiasKeepAnnots match case CapturingType(parent, _) => @@ -133,3 +133,9 @@ extension (tp: AnnotatedType) case ann: CaptureAnnotation => ann.boxed case _ => false +extension (ts: List[Type]) + def boxedUnlessFun(tycon: Type)(using Context) = + if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionClass(tycon.typeSymbol) + then ts + else ts.mapconserve(_.boxed) + diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index 7ab0a20277d2..94416fa8d160 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -494,10 +494,9 @@ class TypeApplications(val self: Type) extends AnyVal { * otherwise return Nil. * Existential types in arguments are returned as TypeBounds instances. */ - final def argInfos(using Context): List[Type] = self.stripped match { - case AppliedType(tycon, args) => args.mapconserve(_.boxedUnlessFun(tycon)) + final def argInfos(using Context): List[Type] = self.stripped match + case AppliedType(tycon, args) => args.boxedUnlessFun(tycon) case _ => Nil - } /** Argument types where existential types in arguments are disallowed */ def argTypes(using Context): List[Type] = argInfos mapConserve noBounds diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 011862c2ef18..0c19599a330b 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -187,7 +187,7 @@ class CheckCaptures extends Recheck, SymTransformer: includeIn(curEnv.outer) def includeBoxedCaptures(tp: Type, pos: SrcPos)(using Context): Unit = - includeBoxedCaptures(tp.boxedCaptured, pos) + includeBoxedCaptures(tp.boxedCaptureSet, pos) def includeBoxedCaptures(refs: CaptureSet, pos: SrcPos)(using Context): Unit = if curEnv.isOpen then @@ -213,9 +213,9 @@ class CheckCaptures extends Recheck, SymTransformer: */ def addResultBoxes(tp: Type)(using Context): Type = def includeBoxed(res: Type) = - //if !res.boxedCaptured.isAlwaysEmpty then - // println(i"add boxed $tp from ${res.boxedCaptured}") - tp.capturing(res.boxedCaptured) + //if !res.boxedCaptureSet.isAlwaysEmpty then + // println(i"add boxed $tp from ${res.boxedCaptureSet}") + tp.capturing(res.boxedCaptureSet) val tp1 = tp.dealias val boxedTp = tp1 match case tp1 @ AppliedType(_, args) if defn.isNonRefinedFunction(tp1) => From fa373584b57e0ded9b7ae45e6d0ee19e53df203b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 19:31:50 +0200 Subject: [PATCH 74/99] Stabilize boxedType cache Make sure we cache the correct type. --- .../dotty/tools/dotc/cc/CaptureAnnotation.scala | 15 ++++++++++++++- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 11 +++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index 402583d2eda4..1f93887555d8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -16,7 +16,7 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte import CaptureAnnotation.* import tpd.* - var boxedType: Type = NoType + val boxedType = BoxedTypeCache() override def tree(using Context) = val elems = refs.elems.toList.map { @@ -66,3 +66,16 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte case _ => false end CaptureAnnotation + +/** A one-element cache for the boxed version of an unboxed capturing type */ +class BoxedTypeCache: + private var boxed: Type = compiletime.uninitialized + private var unboxed: Type = NoType + + def apply(tp: AnnotatedType)(using Context): Type = + if tp ne unboxed then + unboxed = tp + val CapturingType(parent, refs) = tp: @unchecked + boxed = CapturingType(parent, refs, boxed = true) + boxed +end BoxedTypeCache \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 5c5d066edcb1..c114b38c68bb 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -11,7 +11,7 @@ import util.Property.Key import tpd.* private val Captures: Key[CaptureSet] = Key() -private val Boxed: Key[Type] = Key() +private val BoxedType: Key[BoxedTypeCache] = Key() def retainedElems(tree: Tree)(using Context): List[Tree] = tree match case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems @@ -51,13 +51,12 @@ extension (tp: Type) if tp.isBoxed || refs.isAlwaysEmpty then tp else tp.annot match case ann: CaptureAnnotation => - if !ann.boxedType.exists then ann.boxedType = boxedTp - ann.boxedType + ann.boxedType(tp) case ann => - ann.tree.getAttachment(Boxed) match - case None => ann.tree.putAttachment(Boxed, boxedTp) + ann.tree.getAttachment(BoxedType) match + case None => ann.tree.putAttachment(BoxedType, BoxedTypeCache()) case _ => - ann.tree.attachment(Boxed) + ann.tree.attachment(BoxedType)(tp) case _ => tp From d2b9c9dae91dec32d371ec3364d1d1f2f41ecc33 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 19:33:43 +0200 Subject: [PATCH 75/99] Drop checkBoxes switch Always account for boxes in subtyping --- .../src/dotty/tools/dotc/config/Config.scala | 2 -- .../dotty/tools/dotc/core/TypeComparer.scala | 5 ++-- .../tools/dotc/typer/CheckCaptures.scala | 26 +++++++++---------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/Config.scala b/compiler/src/dotty/tools/dotc/config/Config.scala index e4ca4105e3c0..1b0fea9184d1 100644 --- a/compiler/src/dotty/tools/dotc/config/Config.scala +++ b/compiler/src/dotty/tools/dotc/config/Config.scala @@ -245,6 +245,4 @@ object Config { * cases, though. */ inline val ccAllowUnsoundMaps = false - - val checkBoxes = true } diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index a6710ee77785..8452498bceab 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2555,9 +2555,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling refs1.subCaptures(refs2, frozen) protected def sameBoxed(tp1: Type, tp2: Type, refs: CaptureSet)(using Context): Boolean = - !Config.checkBoxes - || (tp1.isBoxedCapturing == tp2.isBoxedCapturing) - || refs.subCaptures(CaptureSet.empty, frozenConstraint).isOK + (tp1.isBoxedCapturing == tp2.isBoxedCapturing) + || refs.subCaptures(CaptureSet.empty, frozenConstraint).isOK // ----------- Diagnostics -------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 0c19599a330b..40d532d85e99 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -571,20 +571,18 @@ class CheckCaptures extends Recheck, SymTransformer: .toFunctionType(isJava = false, alwaysDependent = true)) case _ => actual - if Config.checkBoxes then - var actualw = actual.widenDealias - actual match - case ref: CaptureRef if ref.isTracked => - actualw match - case CapturingType(p, refs) => - actualw = actualw.derivedCapturingType(p, ref.singletonCaptureSet) - case _ => - case _ => - val adapted = adapt(actualw, expected, covariant = true) - if adapted ne actualw then - capt.println(i"adapt boxed $actual vs $expected ===> $adapted") - adapted - else actual + var actualw = actual.widenDealias + actual match + case ref: CaptureRef if ref.isTracked => + actualw match + case CapturingType(p, refs) => + actualw = actualw.derivedCapturingType(p, ref.singletonCaptureSet) + case _ => + case _ => + val adapted = adapt(actualw, expected, covariant = true) + if adapted ne actualw then + capt.println(i"adapt boxed $actual vs $expected ===> $adapted") + adapted else actual end adaptBoxed From 77d08ef73f7d5c34d6ba63a76c3a341716c71065 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 19:58:58 +0200 Subject: [PATCH 76/99] Drop addResultBoxes --- .../tools/dotc/typer/CheckCaptures.scala | 39 +------------------ 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala index 40d532d85e99..10b39ae736f9 100644 --- a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala @@ -199,38 +199,6 @@ class CheckCaptures extends Recheck, SymTransformer: } checkSubset(targetSet, curEnv.captured, pos) - /** If result type of a function type has toplevel boxed captures, propagate - * them to the function type as a whole. Such boxed captures - * can be created by substitution or as-seen-from. Propagating captures to the - * left simulates an unbox operation on the result. I.e. if f has type `A -> box C B` - * then in theory we need to unbox with - * - * x => C o- f(x) - * - * and that also propagates C into the type of the unboxing expression. - * TODO: Generalize this to boxed captues in other parts of a function type. - * Test case in pos-.../boxed1.scala. - */ - def addResultBoxes(tp: Type)(using Context): Type = - def includeBoxed(res: Type) = - //if !res.boxedCaptureSet.isAlwaysEmpty then - // println(i"add boxed $tp from ${res.boxedCaptureSet}") - tp.capturing(res.boxedCaptureSet) - val tp1 = tp.dealias - val boxedTp = tp1 match - case tp1 @ AppliedType(_, args) if defn.isNonRefinedFunction(tp1) => - includeBoxed(args.last) - case tp1 @ RefinedType(_, _, rinfo) if defn.isFunctionType(tp1) => - includeBoxed(rinfo.finalResultType) - case tp1 @ CapturingType(parent, refs) => - val boxedParent = addResultBoxes(parent) - if boxedParent eq parent then tp1 - else boxedParent.capturing(refs) - case _ => - tp1 - if (boxedTp eq tp1) then tp else boxedTp - end addResultBoxes - def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") @@ -260,9 +228,7 @@ class CheckCaptures extends Recheck, SymTransformer: override def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = { val selType = super.recheckSelection(tree, qualType, name) val selCs = selType.widen.captureSet - if selCs.isAlwaysEmpty - || selType.widen.isBoxedCapturing - || qualType.isBoxedCapturing then + if selCs.isAlwaysEmpty || selType.widen.isBoxedCapturing || qualType.isBoxedCapturing then selType else val qualCs = qualType.captureSet @@ -489,8 +455,7 @@ class CheckCaptures extends Recheck, SymTransformer: checkNotUniversal(parent) case _ => checkNotUniversal(typeToCheck) - val tpe1 = if false && tree.isTerm then addResultBoxes(tpe) else tpe - super.recheckFinish(tpe1, tree, pt) + super.recheckFinish(tpe, tree, pt) /** This method implements the rule outlined in #14390: * When checking an expression `e: T` against an expected type `Cx Tx` From 3e2d6aa40671c9ff91d664180d18e84bbd9d3581 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 9 Aug 2022 20:05:40 +0200 Subject: [PATCH 77/99] Polishings --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 11 ++--------- compiler/src/dotty/tools/dotc/cc/CapturingType.scala | 1 + 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index c114b38c68bb..6c7d96db27db 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -42,14 +42,8 @@ extension (tp: Type) else CapturingType(parent, refs, tp.isBoxed) def boxed(using Context): Type = tp.dealias match - case tp @ CapturingType(parent, refs) => - def boxedTp = parent.boxed match - case CapturingType(parent1, refs1) => - CapturingType(parent1, refs ++ refs1, boxed = true) - case parent1 => - CapturingType(parent1, refs, boxed = true) - if tp.isBoxed || refs.isAlwaysEmpty then tp - else tp.annot match + case tp @ CapturingType(parent, refs) if !tp.isBoxed && !refs.isAlwaysEmpty => + tp.annot match case ann: CaptureAnnotation => ann.boxedType(tp) case ann => @@ -64,7 +58,6 @@ extension (tp: Type) if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionClass(tycon.typeSymbol) then tp else tp.boxed - //.showing(i"boxedUF $tp in $tycon = $result") /** The boxed capture set of a type */ def boxedCaptureSet(using Context): CaptureSet = diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index f228804f9dd5..49f0678e48e7 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -14,6 +14,7 @@ object CapturingType: if refs.isAlwaysEmpty then parent else parent match case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed => + // Fuse types except if nested type is boxed and current one isn't. apply(parent1, refs ++ refs1, boxed) case _ => AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) From dfe28e41fb4ff5e1e647864f1fe4d3b9b24a3338 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Aug 2022 09:50:41 +0200 Subject: [PATCH 78/99] Test case 15772 --- tests/neg-custom-args/captures/i15772.check | 36 ++++++++++++++++ tests/neg-custom-args/captures/i15772.scala | 47 ++++++++++++++++----- 2 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 tests/neg-custom-args/captures/i15772.check diff --git a/tests/neg-custom-args/captures/i15772.check b/tests/neg-custom-args/captures/i15772.check new file mode 100644 index 000000000000..0c59e81742f1 --- /dev/null +++ b/tests/neg-custom-args/captures/i15772.check @@ -0,0 +1,36 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:18:2 ---------------------------------------- +18 | () => // error + | ^ + | Found: {x} () -> Int + | Required: () -> Int +19 | val c : {x} C = new C(x) +20 | val boxed1 : (({*} C) => Unit) -> Unit = box1(c) +21 | boxed1((cap: {*} C) => unsafe(c)) +22 | 0 + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:25:2 ---------------------------------------- +25 | () => // error + | ^ + | Found: {x} () -> Int + | Required: () -> Int +26 | val c : {x} C = new C(x) +27 | val boxed2 : Observe[{*} C] = box2(c) +28 | boxed2((cap: {*} C) => unsafe(c)) +29 | 0 + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:33:37 --------------------------------------- +33 | val boxed2 : Observe[{*} C] = box2(c) // error + | ^ + | Found: {*} C + | Required: box {*} C{arg: ? C} + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i15772.scala:44:2 ---------------------------------------- +44 | x: (() -> Unit) // error + | ^ + | Found: (x : {sayHello, io} () -> Unit) + | Required: () -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i15772.scala b/tests/neg-custom-args/captures/i15772.scala index f37a1066135c..29794443c297 100644 --- a/tests/neg-custom-args/captures/i15772.scala +++ b/tests/neg-custom-args/captures/i15772.scala @@ -1,19 +1,44 @@ -class C { - def bad() = println("I've gone bad!") -} - -def newC: {*} C = C() - type Observe[T] = (T => Unit) -> Unit + def unsafe(cap: {*} C) = cap.bad() -def box[T](v: T) : Observe[T] = { +def box1[T](v: T) : (T => Unit) -> Unit = { (fn: T => Unit) => fn(v) } -def main() : Int = { - val boxed : Observe[{*} C] = box(newC) // error - boxed(unsafe) +def box2[T](v: T) : Observe[T] = { + (fn: T => Unit) => fn(v) +} - 0 +class C(val arg: {*} C) { + def bad() = println("I've gone bad!") } + +def main1(x: {*} C) : () -> Int = + () => // error + val c : {x} C = new C(x) + val boxed1 : (({*} C) => Unit) -> Unit = box1(c) + boxed1((cap: {*} C) => unsafe(c)) + 0 + +def main2(x: {*} C) : () -> Int = + () => // error + val c : {x} C = new C(x) + val boxed2 : Observe[{*} C] = box2(c) + boxed2((cap: {*} C) => unsafe(c)) + 0 + +def main3(x: {*} C) = + def c : {*} C = new C(x) + val boxed2 : Observe[{*} C] = box2(c) // error + boxed2((cap: {*} C) => unsafe(c)) + 0 + +trait File: + def write(s: String): Unit + +def main(io: {*} Any) = + val sayHello: (({io} File) => Unit) = (file: {io} File) => file.write("Hello World!\r\n") + val filesList : List[{io} File] = ??? + val x = () => filesList.foreach(sayHello) + x: (() -> Unit) // error \ No newline at end of file From 0230b5a13671ba6b63e3c138cb06737864dc3b0d Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Aug 2022 12:50:17 +0200 Subject: [PATCH 79/99] Another test from conversation with Alex --- tests/neg-custom-args/captures/eta.scala | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/neg-custom-args/captures/eta.scala diff --git a/tests/neg-custom-args/captures/eta.scala b/tests/neg-custom-args/captures/eta.scala new file mode 100644 index 000000000000..12cb858a14d8 --- /dev/null +++ b/tests/neg-custom-args/captures/eta.scala @@ -0,0 +1,7 @@ + type Proc = (() -> Unit) + def foo(f: {*} Proc): {} Proc = + def bar[A <: {f} Proc](g: () -> A): () -> {f} Proc = + g + val stowaway: () -> {f} Proc = + bar( () => f ) // error + () => { stowaway.apply().apply() } \ No newline at end of file From 2e7c363ad9ab49b1c61353fbdacc58a86fa646f8 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Aug 2022 14:27:48 +0200 Subject: [PATCH 80/99] Drop check file --- tests/neg-custom-args/captures/boxmap.check | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 tests/neg-custom-args/captures/boxmap.check diff --git a/tests/neg-custom-args/captures/boxmap.check b/tests/neg-custom-args/captures/boxmap.check deleted file mode 100644 index 7b7b3e0f6e96..000000000000 --- a/tests/neg-custom-args/captures/boxmap.check +++ /dev/null @@ -1,7 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/boxmap.scala:12:2 ---------------------------------------- -12 | () => b[Box[B]]((x: A) => box(f(x))) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Found: {f} () -> ? Box[box ? B] - | Required: () -> Box[B] - | - | longer explanation available when compiling with `-explain` From 562f9d78619089249c9a3f837bdad9936e70d787 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Aug 2022 20:10:28 +0200 Subject: [PATCH 81/99] Recognize capture related syntax and names only under -Ycc Only under -Ycc: - recognize -> and ?-> as function types, - recognize {...} T as a capturing type, - recognize `scala.*` as a value --- .../dotty/tools/dotc/core/Definitions.scala | 5 +- .../dotty/tools/dotc/parsing/Parsers.scala | 74 ++++++++++--------- .../dotty/tools/dotc/parsing/Scanners.scala | 3 - tests/neg/cc-only-defs.scala | 11 +++ tests/neg/multiLineOps.scala | 2 +- tests/neg/t5702-neg-bad-and-wild.check | 8 +- 6 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 tests/neg/cc-only-defs.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 6c9e3915bd80..255adb03573a 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1152,7 +1152,7 @@ class Definitions { } } - /** Extractor for function types representing by-name parameters, of the form + /** Extractor for context function types representing by-name parameters, of the form * `() ?=> T`. * Under -Ycc, this becomes `() ?-> T` or `{r1, ..., rN} () ?-> T`. */ @@ -1977,7 +1977,8 @@ class Definitions { this.initCtx = ctx if (!isInitialized) { // force initialization of every symbol that is synthesized or hijacked by the compiler - val forced = syntheticCoreClasses ++ syntheticCoreMethods ++ ScalaValueClasses() ++ List(JavaEnumClass, captureRoot) + val forced = syntheticCoreClasses ++ syntheticCoreMethods ++ ScalaValueClasses() + ++ (JavaEnumClass :: (if ctx.settings.Ycc.value then captureRoot :: Nil else Nil)) isInitialized = true } diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 9059719444a3..309dd8a20aba 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -188,6 +188,8 @@ object Parsers { def isIdent = in.isIdent def isIdent(name: Name) = in.isIdent(name) + def isPureArrow(name: Name): Boolean = ctx.settings.Ycc.value && isIdent(name) + def isPureArrow: Boolean = isPureArrow(nme.PUREARROW) || isPureArrow(nme.PURECTXARROW) def isErased = isIdent(nme.erased) && in.erasedEnabled def isSimpleLiteral = simpleLiteralTokens.contains(in.token) @@ -427,7 +429,7 @@ object Parsers { */ def convertToParams(tree: Tree): List[ValDef] = val mods = - if in.token == CTXARROW || in.isIdent(nme.PURECTXARROW) + if in.token == CTXARROW || isPureArrow(nme.PURECTXARROW) then Modifiers(Given) else EmptyModifiers tree match @@ -958,23 +960,28 @@ object Parsers { isArrowIndent() else false + /** Under -Ycc: is the following token sequuence a capture set `{ref1, ..., refN}` + * followed by a token that can start a type? + */ def followingIsCaptureSet(): Boolean = - val lookahead = in.LookaheadScanner() - def followingIsTypeStart() = - lookahead.nextToken() - canStartInfixTypeTokens.contains(lookahead.token) - || lookahead.token == LBRACKET - def recur(): Boolean = - (lookahead.isIdent || lookahead.token == THIS) && { + ctx.settings.Ycc.value && { + val lookahead = in.LookaheadScanner() + def followingIsTypeStart() = lookahead.nextToken() - if lookahead.token == COMMA then + canStartInfixTypeTokens.contains(lookahead.token) + || lookahead.token == LBRACKET + def recur(): Boolean = + (lookahead.isIdent || lookahead.token == THIS) && { lookahead.nextToken() - recur() - else - lookahead.token == RBRACE && followingIsTypeStart() - } - lookahead.nextToken() - if lookahead.token == RBRACE then followingIsTypeStart() else recur() + if lookahead.token == COMMA then + lookahead.nextToken() + recur() + else + lookahead.token == RBRACE && followingIsTypeStart() + } + lookahead.nextToken() + if lookahead.token == RBRACE then followingIsTypeStart() else recur() + } /* --------- OPERAND/OPERATOR STACK --------------------------------------- */ @@ -1431,20 +1438,26 @@ object Parsers { def captureRef(): Tree = if in.token == THIS then simpleRef() else termIdent() + /** CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` -- under -Ycc + */ + def captureSet(): List[Tree] = inBraces { + if in.token == RBRACE then Nil else commaSeparated(captureRef) + } + /** Type ::= FunType * | HkTypeParamClause ‘=>>’ Type * | FunParamClause ‘=>>’ Type * | MatchType * | InfixType - * | CaptureSet Type + * | CaptureSet Type -- under -Ycc * FunType ::= (MonoFunType | PolyFunType) - * MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’ | ‘->’ | ‘?->’ ) Type - * PolyFunType ::= HKTypeParamClause ('=>' | ‘->’_) Type + * MonoFunType ::= FunTypeArgs (‘=>’ | ‘?=>’) Type + * | (‘->’ | ‘?->’ ) Type -- under -Ycc + * PolyFunType ::= HKTypeParamClause '=>' Type + * | HKTypeParamClause ‘->’ Type -- under -Ycc * FunTypeArgs ::= InfixType * | `(' [ [ ‘[using]’ ‘['erased'] FunArgType {`,' FunArgType } ] `)' * | '(' [ ‘[using]’ ‘['erased'] TypedFunParam {',' TypedFunParam } ')' - * CaptureSet ::= `{` CaptureRef {`,` CaptureRef} `}` - * CaptureRef ::= Ident */ def typ(): Tree = val start = in.offset @@ -1453,9 +1466,9 @@ object Parsers { val paramSpan = Span(start, in.lastOffset) atSpan(start, in.offset) { var token = in.token - if in.isIdent(nme.PUREARROW) then + if isPureArrow(nme.PUREARROW) then token = ARROW - else if in.isIdent(nme.PURECTXARROW) then + else if isPureArrow(nme.PURECTXARROW) then token = CTXARROW else if token == TLARROW then if !imods.flags.isEmpty || params.isEmpty then @@ -1463,7 +1476,6 @@ object Parsers { token = ARROW else if ctx.settings.Ycc.value then // `=>` means impure function under -Ycc whereas `->` is a regular function. - // Without -Ycc they both mean regular function. imods |= Impure if token == CTXARROW then @@ -1514,7 +1526,7 @@ object Parsers { commaSeparatedRest(t, funArgType) } accept(RPAREN) - if isValParamList || in.isArrow || in.isPureArrow then + if isValParamList || in.isArrow || isPureArrow then functionRest(ts) else { val ts1 = ts.mapConserve { t => @@ -1538,7 +1550,7 @@ object Parsers { val tparams = typeParamClause(ParamOwner.TypeParam) if (in.token == TLARROW) atSpan(start, in.skipToken())(LambdaTypeTree(tparams, toplevelTyp())) - else if (in.token == ARROW || in.isIdent(nme.PUREARROW)) { + else if (in.token == ARROW || isPureArrow(nme.PUREARROW)) { val arrowOffset = in.skipToken() val body = toplevelTyp() atSpan(start, arrowOffset) { @@ -1562,7 +1574,7 @@ object Parsers { case MATCH => matchType(t) case FORSOME => syntaxError(ExistentialTypesNoLongerSupported()); t case _ => - if isIdent(nme.PUREARROW) || isIdent(nme.PURECTXARROW) then + if isPureArrow then functionRest(t :: Nil) else if (imods.is(Erased) && !t.isInstanceOf[FunctionWithMods]) @@ -1625,7 +1637,7 @@ object Parsers { def infixTypeRest(t: Tree): Tree = infixOps(t, canStartInfixTypeTokens, refinedTypeFn, Location.ElseWhere, ParseKind.Type, - isOperator = !followingIsVararg() && !isIdent(nme.PUREARROW) && !isIdent(nme.PURECTXARROW)) + isOperator = !followingIsVararg() && !isPureArrow) /** RefinedType ::= WithType {[nl] Refinement} */ @@ -1864,7 +1876,7 @@ object Parsers { } def paramTypeOf(core: () => Tree): Tree = - if in.token == ARROW || isIdent(nme.PUREARROW) then + if in.token == ARROW || isPureArrow(nme.PUREARROW) then val isImpure = in.token == ARROW val tp = atSpan(in.skipToken()) { ByNameTypeTree(core()) } if isImpure && ctx.settings.Ycc.value then ImpureByNameTypeTree(tp) else tp @@ -1974,10 +1986,6 @@ object Parsers { CapturingTypeTree(captureSet(), infixType()) else infixType() - def captureSet(): List[Tree] = inBraces { - if in.token == RBRACE then Nil else commaSeparated(captureRef) - } - /* ----------- EXPRESSIONS ------------------------------------------------ */ /** Does the current conditional expression continue after @@ -4074,8 +4082,6 @@ object Parsers { * | * EnumStat ::= TemplateStat * | Annotations Modifiers EnumCase - * SelfType ::= id [‘:’ [CaptureSet] InfixType] ‘=>’ - * | ‘this’ ‘:’ [CaptureSet] InfixType ‘=>’ */ def templateStatSeq(): (ValDef, List[Tree]) = checkNoEscapingPlaceholders { val stats = new ListBuffer[Tree] diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 726e6b7980ce..082112d800d9 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -88,9 +88,6 @@ object Scanners { def isArrow = token == ARROW || token == CTXARROW - - def isPureArrow = - isIdent(nme.PUREARROW) || isIdent(nme.PURECTXARROW) } abstract class ScannerCommon(source: SourceFile)(using Context) extends CharArrayReader with TokenData { diff --git a/tests/neg/cc-only-defs.scala b/tests/neg/cc-only-defs.scala new file mode 100644 index 000000000000..236fb0ae3fd2 --- /dev/null +++ b/tests/neg/cc-only-defs.scala @@ -0,0 +1,11 @@ +trait Test { + + val x: Int -> Int // error + val y: Int ?-> Int // error + + val z: *.type // error + + val b: ImpureFuntion1[Int, Int] // error + + val a: {z} String // error +} // error diff --git a/tests/neg/multiLineOps.scala b/tests/neg/multiLineOps.scala index 08a0a3925fd1..8499cc9fe710 100644 --- a/tests/neg/multiLineOps.scala +++ b/tests/neg/multiLineOps.scala @@ -5,7 +5,7 @@ val x = 1 val b1 = { 22 * 22 // ok - */*one more*/22 // error: end of statement expected + */*one more*/22 // error: end of statement expected // error: not found: * } val b2: Boolean = { diff --git a/tests/neg/t5702-neg-bad-and-wild.check b/tests/neg/t5702-neg-bad-and-wild.check index 1a9e4ba4a1f6..f6d761a6726f 100644 --- a/tests/neg/t5702-neg-bad-and-wild.check +++ b/tests/neg/t5702-neg-bad-and-wild.check @@ -38,16 +38,16 @@ | x is already defined as value x | | Note that overloaded methods must all be defined in the same group of toplevel definitions --- [E127] Syntax Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 --------------------------------------------------- +-- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:12:20 ------------------------------------------------ 12 | case List(1, _*3,) => // error: pattern expected // error | ^ - | * cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method + | Not found: * | | longer explanation available when compiling with `-explain` --- [E127] Syntax Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 --------------------------------------------------- +-- [E006] Not Found Error: tests/neg/t5702-neg-bad-and-wild.scala:13:20 ------------------------------------------------ 13 | case List(1, _*3:) => // error // error | ^ - | * cannot be used as an extractor in a pattern because it lacks an unapply or unapplySeq method + | Not found: * | | longer explanation available when compiling with `-explain` -- [E045] Cyclic Error: tests/neg/t5702-neg-bad-and-wild.scala:23:19 --------------------------------------------------- From 01f4e0b2e1edef7d6cc2643c8e1a211e5c74812d Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Aug 2022 23:14:24 +0200 Subject: [PATCH 82/99] Recognize ImpureFunction only under -Ycc --- .../src/dotty/tools/dotc/core/NameOps.scala | 26 ++++++++++--------- tests/neg/cc-only-defs.scala | 2 +- tests/pos/impurefun.scala | 4 --- 3 files changed, 15 insertions(+), 17 deletions(-) delete mode 100644 tests/pos/impurefun.scala diff --git a/compiler/src/dotty/tools/dotc/core/NameOps.scala b/compiler/src/dotty/tools/dotc/core/NameOps.scala index 1cf6009e5b8d..8ec095036b35 100644 --- a/compiler/src/dotty/tools/dotc/core/NameOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NameOps.scala @@ -199,7 +199,7 @@ object NameOps { else collectDigits(acc * 10 + d, idx + 1) collectDigits(0, suffixStart + 8) - private def isFunctionPrefix(suffixStart: Int, mustHave: String = ""): Boolean = + private def isFunctionPrefix(suffixStart: Int, mustHave: String = "")(using Context): Boolean = suffixStart >= 0 && { val first = name.firstPart @@ -209,34 +209,36 @@ object NameOps { if str == mustHave then found = true idx + str.length else idx - skip(skip(skip(0, "Impure"), "Erased"), "Context") == suffixStart + val start = if ctx.settings.Ycc.value then skip(0, "Impure") else 0 + skip(skip(start, "Erased"), "Context") == suffixStart && found } /** Same as `funArity`, except that it returns -1 if the prefix - * is not one of "", "Context", "Erased", "ErasedContext" + * is not one of a (possibly empty) concatenation of a subset of + * "Impure" (only under -Ycc), "Erased" and "Context" (in that order). */ - private def checkedFunArity(suffixStart: Int): Int = + private def checkedFunArity(suffixStart: Int)(using Context): Int = if isFunctionPrefix(suffixStart) then funArity(suffixStart) else -1 /** Is a function name, i.e one of FunctionXXL, FunctionN, ContextFunctionN, ErasedFunctionN, ErasedContextFunctionN for N >= 0 */ - def isFunction: Boolean = + def isFunction(using Context): Boolean = (name eq tpnme.FunctionXXL) || checkedFunArity(functionSuffixStart) >= 0 /** Is a function name * - FunctionN for N >= 0 */ - def isPlainFunction: Boolean = functionArity >= 0 + def isPlainFunction(using Context): Boolean = functionArity >= 0 /** Is a function name that contains `mustHave` as a substring */ - private def isSpecificFunction(mustHave: String): Boolean = + private def isSpecificFunction(mustHave: String)(using Context): Boolean = val suffixStart = functionSuffixStart isFunctionPrefix(suffixStart, mustHave) && funArity(suffixStart) >= 0 - def isContextFunction: Boolean = isSpecificFunction("Context") - def isErasedFunction: Boolean = isSpecificFunction("Erased") - def isImpureFunction: Boolean = isSpecificFunction("Impure") + def isContextFunction(using Context): Boolean = isSpecificFunction("Context") + def isErasedFunction(using Context): Boolean = isSpecificFunction("Erased") + def isImpureFunction(using Context): Boolean = isSpecificFunction("Impure") /** Is a synthetic function name, i.e. one of * - FunctionN for N > 22 @@ -244,12 +246,12 @@ object NameOps { * - ErasedFunctionN for N >= 0 * - ErasedContextFunctionN for N >= 0 */ - def isSyntheticFunction: Boolean = + def isSyntheticFunction(using Context): Boolean = val suffixStart = functionSuffixStart if suffixStart == 0 then funArity(suffixStart) > MaxImplementedFunctionArity else checkedFunArity(suffixStart) >= 0 - def functionArity: Int = + def functionArity(using Context): Int = val suffixStart = functionSuffixStart if suffixStart >= 0 then checkedFunArity(suffixStart) else -1 diff --git a/tests/neg/cc-only-defs.scala b/tests/neg/cc-only-defs.scala index 236fb0ae3fd2..a9b480f9f590 100644 --- a/tests/neg/cc-only-defs.scala +++ b/tests/neg/cc-only-defs.scala @@ -5,7 +5,7 @@ trait Test { val z: *.type // error - val b: ImpureFuntion1[Int, Int] // error + val b: ImpureFunction1[Int, Int] // error val a: {z} String // error } // error diff --git a/tests/pos/impurefun.scala b/tests/pos/impurefun.scala deleted file mode 100644 index c9f4c54a0b90..000000000000 --- a/tests/pos/impurefun.scala +++ /dev/null @@ -1,4 +0,0 @@ -object Test: - - val f: ImpureFunction1[Int, Int] = (x: Int) => x + 1 - From 9f2a801dd1c783ad0a25a3b5aedac55dcc3926a4 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 10 Aug 2022 23:40:38 +0200 Subject: [PATCH 83/99] Drop widenSkolems Does not seem to be needed anymore --- compiler/src/dotty/tools/dotc/core/Phases.scala | 3 --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 2 -- compiler/src/dotty/tools/dotc/transform/Recheck.scala | 2 -- 3 files changed, 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Phases.scala b/compiler/src/dotty/tools/dotc/core/Phases.scala index 75c13835bc02..b4a2dcac1b85 100644 --- a/compiler/src/dotty/tools/dotc/core/Phases.scala +++ b/compiler/src/dotty/tools/dotc/core/Phases.scala @@ -303,9 +303,6 @@ object Phases { /** If set, implicit search is enabled */ def allowsImplicitSearch: Boolean = false - /** If set equate Skolem types with underlying types */ - def widenSkolems: Boolean = false - /** List of names of phases that should precede this phase */ def runsAfter: Set[String] = Set.empty diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 8452498bceab..47ada4a97e0d 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -819,8 +819,6 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } compareClassInfo - case tp2: SkolemType => - ctx.phase.widenSkolems && recur(tp1, tp2.info) || fourthTry case _ => fourthTry } diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index 0975e5fde975..aa23e5a1cdd5 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -98,8 +98,6 @@ abstract class Recheck extends Phase, SymTransformer: // TODO: investigate what goes wrong we Ycheck directly after rechecking. // One failing test is pos/i583a.scala - override def widenSkolems = true - /** Change any `ResetPrivate` flags back to `Private` */ def transformSym(sym: SymDenotation)(using Context): SymDenotation = if sym.isAllOf(Recheck.ResetPrivateParamAccessor) then From 8bab2ac2c21770d2d6320dd78a095bad021c9b4a Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Aug 2022 19:47:45 +0200 Subject: [PATCH 84/99] Polishings in dotc, core, ast, parsing --- .../src/dotty/tools/dotc/ast/Desugar.scala | 2 + .../src/dotty/tools/dotc/ast/TreeInfo.scala | 8 ++ compiler/src/dotty/tools/dotc/ast/tpd.scala | 2 +- compiler/src/dotty/tools/dotc/ast/untpd.scala | 2 + .../src/dotty/tools/dotc/cc/CaptureOps.scala | 4 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 6 ++ .../src/dotty/tools/dotc/core/Flags.scala | 4 +- .../src/dotty/tools/dotc/core/NamerOps.scala | 24 +++-- .../src/dotty/tools/dotc/core/StdNames.scala | 2 - .../dotty/tools/dotc/core/Substituters.scala | 2 +- .../tools/dotc/core/SymDenotations.scala | 3 + .../tools/dotc/core/TypeApplications.scala | 1 - .../dotty/tools/dotc/core/TypeComparer.scala | 93 ++++++++++++------- .../src/dotty/tools/dotc/core/TypeOps.scala | 9 +- .../src/dotty/tools/dotc/core/Types.scala | 55 +++++++---- .../src/dotty/tools/dotc/core/Variances.scala | 1 - .../tools/dotc/core/tasty/TreePickler.scala | 1 - .../tools/dotc/core/tasty/TreeUnpickler.scala | 4 +- .../core/unpickleScala2/Scala2Unpickler.scala | 4 +- .../tools/dotc/printing/PlainPrinter.scala | 10 +- .../src/dotty/tools/dotc/typer/Typer.scala | 10 +- 21 files changed, 158 insertions(+), 89 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index b8b81b565e6c..3d783b6e0d5b 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1810,6 +1810,8 @@ object desugar { case ext: ExtMethods => Block(List(ext), Literal(Constant(())).withSpan(ext.span)) case CapturingTypeTree(refs, parent) => + // convert `{refs} T` to `T @retains refs` + // `{refs}-> T` to `-> (T @retainsByName refs)` def annotate(annotName: TypeName, tp: Tree) = Annotated(tp, New(scalaDot(annotName), List(refs))) parent match diff --git a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala index 43d40779982a..083a92b26d11 100644 --- a/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala +++ b/compiler/src/dotty/tools/dotc/ast/TreeInfo.scala @@ -195,9 +195,11 @@ trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] => case arg => arg.typeOpt.widen.isRepeatedParam } + /** Is tree a type tree of the form `=> T` or (under -Ycc) `{refs}-> T`? */ def isByNameType(tree: Tree)(using Context): Boolean = stripByNameType(tree) ne tree + /** Strip `=> T` to `T` and (under -Ycc) `{refs}-> T` to `T` */ def stripByNameType(tree: Tree)(using Context): Tree = unsplice(tree) match case ByNameTypeTree(t1) => t1 case untpd.CapturingTypeTree(_, parent) => @@ -398,10 +400,16 @@ trait UntypedTreeInfo extends TreeInfo[Untyped] { self: Trees.Instance[Untyped] } } + /** Under -Ycc: A builder and extractor for `=> T`, which is an alias for `{*}-> T`. + * Only trees of the form `=> T` are matched; trees written directly as `{*}-> T` + * are ignored by the extractor. + */ object ImpureByNameTypeTree: + def apply(tp: ByNameTypeTree)(using Context): untpd.CapturingTypeTree = untpd.CapturingTypeTree( Ident(nme.CAPTURE_ROOT).withSpan(tp.span.startPos) :: Nil, tp) + def unapply(tp: Tree)(using Context): Option[ByNameTypeTree] = tp match case untpd.CapturingTypeTree(id @ Ident(nme.CAPTURE_ROOT) :: Nil, bntp: ByNameTypeTree) if id.span == bntp.span.startPos => Some(bntp) diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index e17979217610..52325e36037d 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -165,7 +165,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { ta.assignType(untpd.Inlined(call, bindings, expansion), bindings, expansion) def TypeTree(tp: Type, inferred: Boolean = false)(using Context): TypeTree = - (if inferred then new InferredTypeTree() else untpd.TypeTree()).withType(tp) + (if inferred then untpd.InferredTypeTree() else untpd.TypeTree()).withType(tp) def SingletonTypeTree(ref: Tree)(using Context): SingletonTypeTree = ta.assignType(untpd.SingletonTypeTree(ref), ref) diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index 3b74f5c1e121..6f3f134f9342 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -217,6 +217,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { case class Infix()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Infix) + /** Used under -Ycc to mark impure function types `A => B` in `FunctionWithMods` */ case class Impure()(implicit @constructorOnly src: SourceFile) extends Mod(Flags.Impure) } @@ -395,6 +396,7 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo { def JavaSeqLiteral(elems: List[Tree], elemtpt: Tree)(implicit src: SourceFile): JavaSeqLiteral = new JavaSeqLiteral(elems, elemtpt) def Inlined(call: tpd.Tree, bindings: List[MemberDef], expansion: Tree)(implicit src: SourceFile): Inlined = new Inlined(call, bindings, expansion) def TypeTree()(implicit src: SourceFile): TypeTree = new TypeTree() + def InferredTypeTree()(implicit src: SourceFile): TypeTree = new InferredTypeTree() def SingletonTypeTree(ref: Tree)(implicit src: SourceFile): SingletonTypeTree = new SingletonTypeTree(ref) def RefinedTypeTree(tpt: Tree, refinements: List[Tree])(implicit src: SourceFile): RefinedTypeTree = new RefinedTypeTree(tpt, refinements) def AppliedTypeTree(tpt: Tree, args: List[Tree])(implicit src: SourceFile): AppliedTypeTree = new AppliedTypeTree(tpt, args) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 6c7d96db27db..e7ff0fad3c94 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -51,6 +51,8 @@ extension (tp: Type) case None => ann.tree.putAttachment(BoxedType, BoxedTypeCache()) case _ => ann.tree.attachment(BoxedType)(tp) + case tp: RealTypeBounds => + tp.derivedTypeBounds(tp.lo.boxed, tp.hi.boxed) case _ => tp @@ -84,7 +86,7 @@ extension (tp: Type) /** Under -Ycc, map regular function type to impure function type */ - def adaptFunctionType(using Context): Type = tp match + def adaptFunctionTypeUnderCC(using Context): Type = tp match case AppliedType(fn, args) if ctx.settings.Ycc.value && defn.isFunctionClass(fn.typeSymbol) => val fname = fn.typeSymbol.name diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index 3e3925c0e272..f21519bc811c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -575,6 +575,12 @@ object CaptureSet: def mapRefs(xs: Refs, tm: TypeMap, variance: Int)(using Context): CaptureSet = mapRefs(xs, extrapolateCaptureRef(_, tm, variance)) + /** Return true iff + * - arg1 is a TypeBounds >: CL T <: CH T of two capturing types with equal parents. + * - arg2 is a capturing type CA U + * - CH <: CA <: CL + * In other words, we can unify CL, CH and CA. + */ def subCapturesRange(arg1: TypeBounds, arg2: Type)(using Context): Boolean = arg1 match case TypeBounds(CapturingType(lo, loRefs), CapturingType(hi, hiRefs)) if lo =:= hi => given VarState = VarState() diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 152b2075b514..72428d02f5d3 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -314,7 +314,7 @@ object Flags { /** A Scala 2x super accessor / an unpickled Scala 2.x class */ val (SuperParamAliasOrScala2x @ _, SuperParamAlias @ _, Scala2x @ _) = newFlags(26, "", "") - /** A parameter with a default value / an impure untpd.Function type */ + /** A parameter with a default value / an impure untpd.FunctionWithMods type */ val (_, HasDefault @ _, Impure @ _) = newFlags(27, "", "<{*}>") /** An extension method, or a collective extension instance */ @@ -405,7 +405,7 @@ object Flags { val (_, _, ChildrenQueried @ _) = newFlags(56, "") /** A module variable (Scala 2.x only) - * (re-used as a capture checking flag in CheckCaptures) + * (re-used as a flag for private parameter accessors in Recheck) */ val (_, Scala2ModuleVar @ _, _) = newFlags(57, "") diff --git a/compiler/src/dotty/tools/dotc/core/NamerOps.scala b/compiler/src/dotty/tools/dotc/core/NamerOps.scala index 9d826f293689..fa0a89349b5e 100644 --- a/compiler/src/dotty/tools/dotc/core/NamerOps.scala +++ b/compiler/src/dotty/tools/dotc/core/NamerOps.scala @@ -192,23 +192,31 @@ object NamerOps: modcls.registeredCompanion = cls /** For secondary constructors, make it known in the context that their type parameters - * are aliases of the class type parameters. This is done by (ab?)-using GADT constraints. - * See pos/i941.scala + * are aliases of the class type parameters. + * @return if `sym` is a secondary constructor, a fresh context that + * contains GADT constraints linking the type parameters. */ def linkConstructorParams(sym: Symbol)(using Context): Context = if sym.isConstructor && !sym.isPrimaryConstructor then sym.rawParamss match case (tparams @ (tparam :: _)) :: _ if tparam.isType => val rhsCtx = ctx.fresh.setFreshGADTBounds - rhsCtx.gadt.addToConstraint(tparams) - tparams.lazyZip(sym.owner.typeParams).foreach { (psym, tparam) => - val tr = tparam.typeRef - rhsCtx.gadt.addBound(psym, tr, isUpper = false) - rhsCtx.gadt.addBound(psym, tr, isUpper = true) - } + linkConstructorParams(sym, tparams, rhsCtx) rhsCtx case _ => ctx else ctx + /** For secondary constructor `sym`, make it known in the given context `rhsCtx` + * that their type parameters are aliases of the class type parameters. This is done + * by (ab?)-using GADT constraints. See pos/i941.scala. + */ + def linkConstructorParams(sym: Symbol, tparams: List[Symbol], rhsCtx: Context)(using Context): Unit = + rhsCtx.gadt.addToConstraint(tparams) + tparams.lazyZip(sym.owner.typeParams).foreach { (psym, tparam) => + val tr = tparam.typeRef + rhsCtx.gadt.addBound(psym, tr, isUpper = false) + rhsCtx.gadt.addBound(psym, tr, isUpper = true) + } + end NamerOps diff --git a/compiler/src/dotty/tools/dotc/core/StdNames.scala b/compiler/src/dotty/tools/dotc/core/StdNames.scala index 81e130779912..bff957721b23 100644 --- a/compiler/src/dotty/tools/dotc/core/StdNames.scala +++ b/compiler/src/dotty/tools/dotc/core/StdNames.scala @@ -282,7 +282,6 @@ object StdNames { // ----- Term names ----------------------------------------- // Compiler-internal - val ANYname: N = "" val CAPTURE_ROOT: N = "*" val CONSTRUCTOR: N = "" val STATIC_CONSTRUCTOR: N = "" @@ -350,7 +349,6 @@ object StdNames { val AppliedTypeTree: N = "AppliedTypeTree" val ArrayAnnotArg: N = "ArrayAnnotArg" val CAP: N = "CAP" - val ClassManifestFactory: N = "ClassManifestFactory" val Constant: N = "Constant" val ConstantType: N = "ConstantType" val Eql: N = "Eql" diff --git a/compiler/src/dotty/tools/dotc/core/Substituters.scala b/compiler/src/dotty/tools/dotc/core/Substituters.scala index afabde7f2466..3e32340b21bd 100644 --- a/compiler/src/dotty/tools/dotc/core/Substituters.scala +++ b/compiler/src/dotty/tools/dotc/core/Substituters.scala @@ -182,7 +182,7 @@ object Substituters: final class SubstSymMap(from: List[Symbol], to: List[Symbol])(using Context) extends DeepTypeMap, BiTypeMap { def apply(tp: Type): Type = substSym(tp, from, to, this)(using mapCtx) - def inverse(tp: Type) = tp.substSym(to, from) + def inverse(tp: Type) = tp.substSym(to, from) // implicitly requires that `to` contains no duplicates. } final class SubstThisMap(from: ClassSymbol, to: Type)(using Context) extends DeepTypeMap { diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index d8486dc48e42..66a1e44622b8 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -230,6 +230,9 @@ object SymDenotations { ensureCompleted(); myAnnotations } + /** The annotations without ensuring that the symbol is completed. + * Used for diagnostics where we don't want to force symbols. + */ final def annotationsUNSAFE(using Context): List[Annotation] = myAnnotations /** Update the annotations of this denotation */ diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index 94416fa8d160..26132c7c0205 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -11,7 +11,6 @@ import util.Stats._ import Names._ import Flags.{Module, Provisional} import dotty.tools.dotc.config.Config -import cc.CaptureSet.IdentityCaptRefMap import cc.boxedUnlessFun object TypeApplications { diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 47ada4a97e0d..0e981a8a5417 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -610,6 +610,35 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling def compareRefined: Boolean = val tp1w = tp1.widen + + if ctx.phase == Phases.checkCapturesPhase then + + // A relaxed version of subtyping for dependent functions where method types + // are treated as contravariant. + // TODO: Merge with isSubInfo in hasMatchingMember. Currently, we can't since + // the isSubinfo of hasMatchingMember has problems dealing with PolyTypes + // (---> orphan params during pickling) + def isSubInfo(info1: Type, info2: Type): Boolean = (info1, info2) match + case (info1: PolyType, info2: PolyType) => + info1.paramNames.hasSameLengthAs(info2.paramNames) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) + case (info1: MethodType, info2: MethodType) => + matchingMethodParams(info1, info2, precise = false) + && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) + case _ => + isSubType(info1, info2) + + if defn.isFunctionType(tp2) then + tp1w.widenDealias match + case tp1: RefinedType => + return isSubInfo(tp1.refinedInfo, tp2.refinedInfo) + case _ => + else if tp2.parent.typeSymbol == defn.PolyFunctionClass then + tp1.member(nme.apply).info match + case info1: PolyType => + return isSubInfo(info1, tp2.refinedInfo) + case _ => + val skipped2 = skipMatching(tp1w, tp2) if (skipped2 eq tp2) || !Config.fastPathForRefinedSubtype then if containsAnd(tp1) then @@ -631,28 +660,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling else // fast path, in particular for refinements resulting from parameterization. isSubRefinements(tp1w.asInstanceOf[RefinedType], tp2, skipped2) && recur(tp1, skipped2) - - def isSubInfo(info1: Type, info2: Type): Boolean = (info1, info2) match - case (info1: PolyType, info2: PolyType) => - info1.paramNames.hasSameLengthAs(info2.paramNames) - && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) - case (info1: MethodType, info2: MethodType) => - matchingMethodParams(info1, info2, precise = false) - && isSubInfo(info1.resultType, info2.resultType.subst(info2, info1)) - case _ => - isSubType(info1, info2) - - if ctx.phase == Phases.checkCapturesPhase then - if defn.isFunctionType(tp2) then - tp1.widenDealias match - case tp1: RefinedType => - return isSubInfo(tp1.refinedInfo, tp2.refinedInfo) - case _ => - else if tp2.parent.typeSymbol == defn.PolyFunctionClass then - tp1.member(nme.apply).info match - case info1: PolyType => - return isSubInfo(info1, tp2.refinedInfo) - case _ => + end compareRefined compareRefined case tp2: RecType => @@ -797,7 +805,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } compareTypeBounds case CapturingType(parent2, refs2) => - def compareCaptured = + def compareCapturing = val refs1 = tp1.captureSet try if refs1.isAlwaysEmpty then recur(tp1, parent2) @@ -807,7 +815,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling catch case ex: AssertionError => println(i"assertion failed while compare captured $tp1 <:< $tp2") throw ex - compareCaptured || fourthTry + compareCapturing || fourthTry case tp2: AnnotatedType if tp2.isRefining => (tp1.derivesAnnotWith(tp2.annot.sameAnnotation) || tp1.isBottomType) && recur(tp1, tp2.parent) @@ -868,7 +876,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp: AppliedType => isNullable(tp.tycon) case AndType(tp1, tp2) => isNullable(tp1) && isNullable(tp2) case OrType(tp1, tp2) => isNullable(tp1) || isNullable(tp2) - case CapturingType(tp1, _) => isNullable(tp1) + case AnnotatedType(tp1, _) => isNullable(tp1) case _ => false val sym1 = tp1.symbol (sym1 eq NothingClass) && tp2.isValueTypeOrLambda || @@ -886,15 +894,16 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => false } case _ => false - comparePaths || { - var tp1w = tp1.underlying.widenExpr + + def tp1widened = + val tp1w = tp1.underlying.widenExpr tp1 match case tp1: CaptureRef if tp1.isTracked => - val stripped = tp1w.stripCapturing - tp1w = CapturingType(stripped, tp1.singletonCaptureSet) + CapturingType(tp1w.stripCapturing, tp1.singletonCaptureSet) case _ => - isSubType(tp1w, tp2, approx.addLow) - } + tp1w + + comparePaths || isSubType(tp1widened, tp2, approx.addLow) case tp1: RefinedType => isNewSubType(tp1.parent) case tp1: RecType => @@ -1623,7 +1632,14 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => arg1 match case arg1: TypeBounds => - CaptureSet.subCapturesRange(arg1, arg2) || compareCaptured(arg1, arg2) + CaptureSet.subCapturesRange(arg1, arg2) + // subCapturesRange is important for invariant arguments that get expanded + // to TypeBounds where each bound is obtained by adding a captureset variable + // to the argument type. If subCapturesRange returns true we know that arg1's' + // capture set can be unified with arg2's capture set, so it only remains to + // check the underlying types with `isSubArg`. + && isSubArg(arg1.hi.stripCapturing, arg2.stripCapturing) + || compareCaptured(arg1, arg2) case ExprType(arg1res) if ctx.phaseId > elimByNamePhase.id && !ctx.erasedTypes && defn.isByNameFunction(arg2.dealias) => @@ -2060,6 +2076,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling if precise then isSameTypeWhenFrozen(formal1, formal2a) else if ctx.phase == Phases.checkCapturesPhase then + // allow to constrain capture set variables isSubType(formal2a, formal1) else isSubTypeWhenFrozen(formal2a, formal1) @@ -2483,7 +2500,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: TypeVar if tp1.isInstantiated => tp1.underlying & tp2 case CapturingType(parent1, refs1) => - if subCaptures(tp2.captureSet, refs1, frozen = true).isOK then + if subCaptures(tp2.captureSet, refs1, frozen = true).isOK + && tp1.isBoxedCapturing == tp2.isBoxedCapturing + then parent1 & tp2 else tp1.derivedCapturingType(parent1 & tp2, refs1) @@ -2552,9 +2571,13 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult.Type = refs1.subCaptures(refs2, frozen) - protected def sameBoxed(tp1: Type, tp2: Type, refs: CaptureSet)(using Context): Boolean = + /** 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. + */ + protected def sameBoxed(tp1: Type, tp2: Type, refs1: CaptureSet)(using Context): Boolean = (tp1.isBoxedCapturing == tp2.isBoxedCapturing) - || refs.subCaptures(CaptureSet.empty, frozenConstraint).isOK + || refs1.subCaptures(CaptureSet.empty, frozenConstraint).isOK // ----------- Diagnostics -------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 7233f3ec7f0a..c087aac83cb8 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} +import cc.{CapturingType, derivedCapturingType, CaptureSet, isBoxed, isBoxedCapturing} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable @@ -168,9 +168,12 @@ object TypeOps: // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take // corrective steps, so no widening is wanted. simplify(l, theMap) | simplify(r, theMap) - case CapturingType(parent, refs) => + case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) - && refs.subCaptures(parent.captureSet, frozen = true).isOK then + && refs.subCaptures(parent.captureSet, frozen = true).isOK + && (tp.isBoxed || !parent.isBoxedCapturing) + // fuse types with same boxed status and outer boxed with any type + then simplify(parent, theMap) else mapOver diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index cc75a73d1f37..2f5cc6236913 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1537,6 +1537,7 @@ object Types { 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) // ----- Normalizing typerefs over refined types ---------------------------- @@ -1831,8 +1832,9 @@ object Types { /** Turn type into a function type. * @pre this is a method type without parameter dependencies. - * @param dropLast The number of trailing parameters that should be dropped - * when forming the function type. + * @param dropLast the number of trailing parameters that should be dropped + * when forming the function type. + * @param alwaysDependent if true, always create a dependent function type. */ def toFunctionType(isJava: Boolean, dropLast: Int = 0, alwaysDependent: Boolean = false)(using Context): Type = this match { case mt: MethodType if !mt.isParamDependent => @@ -1878,10 +1880,14 @@ object Types { 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.isConst && cs.subCaptures(captureSet, frozen = true).isOK then this else this match @@ -2077,16 +2083,29 @@ object Types { private var myCaptureSetRunId: Int = NoRunId private var mySingletonCaptureSet: CaptureSet.Const | Null = null + /** Can the reference be tracked? This is true for all ThisTypes or ParamRefs + * but only for some NamedTypes. + */ def canBeTracked(using Context): Boolean + + /** Is the reference tracked? This is true if it can be tracked and the capture + * set of the underlying type is not always empty. + */ final def isTracked(using Context): Boolean = canBeTracked && !captureSetOfInfo.isAlwaysEmpty + + /** Is this reference the root capability `*` ? */ def isRootCapability(using Context): Boolean = false + + /** Normalize reference so that it can be compared with `eq` for equality */ def normalizedRef(using Context): CaptureRef = this + /** The capture set consisting of exactly this reference */ 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 = if ctx.runId == myCaptureSetRunId then myCaptureSet.nn else if myCaptureSet.asInstanceOf[AnyRef] eq CaptureSet.Pending then CaptureSet.empty @@ -2174,9 +2193,6 @@ object Types { private var mySignature: Signature = _ private var mySignatureRunId: Int = NoRunId - private var myCaptureSet: CaptureSet = _ - private var myCaptureSetRunId: Int = NoRunId - // Invariants: // (1) checkedPeriod != Nowhere => lastDenotation != null // (2) lastDenotation != null => lastSymbol != null @@ -2763,7 +2779,7 @@ object Types { * 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 avout call-by-name? + * TODO: ^^^ What about call-by-name? */ def canBeTracked(using Context) = ((prefix eq NoPrefix) @@ -3815,8 +3831,8 @@ object Types { */ def isParamDependent(using Context): Boolean = paramDependencyStatus == TrueDeps - /** Is there either a true or false type dependency, or does the result - * type capture a parameter? + /** Is there a dependency involving a reference in a capture set, but + * otherwise no true result dependency? */ def isCaptureDependent(using Context) = dependencyStatus == CaptureDeps @@ -4210,12 +4226,12 @@ object Types { private object DepStatus { type DependencyStatus = Byte - final val Unknown: DependencyStatus = 0 // not yet computed - final val NoDeps: DependencyStatus = 1 // no dependent parameters found - final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types - final val CaptureDeps: DependencyStatus = 3 - final val TrueDeps: DependencyStatus = 4 // some truly dependent parameters exist - final val StatusMask: DependencyStatus = 7 // the bits indicating actual dependency status + final val Unknown: DependencyStatus = 0 // not yet computed + final val NoDeps: DependencyStatus = 1 // no dependent parameters found + final val FalseDeps: DependencyStatus = 2 // all dependent parameters are prefixes of non-depended alias types + final val CaptureDeps: DependencyStatus = 3 // dependencies in capture sets under -Ycc, otherwise only false dependencoes + final val TrueDeps: DependencyStatus = 4 // some truly dependent parameters exist + final val StatusMask: DependencyStatus = 7 // the bits indicating actual dependency status final val Provisional: DependencyStatus = 8 // set if dependency status can still change due to type variable instantiations } @@ -5469,20 +5485,27 @@ object Types { } end VariantTraversal - /** A supertrait for some typemaps that are bijections. Used for capture checking + /** A supertrait for some typemaps that are bijections. Used for capture checking. * BiTypeMaps should map capture references to capture references. */ trait BiTypeMap extends TypeMap: thisMap => + + /** The inverse of the type map as a function */ def inverse(tp: Type): Type + /** The inverse of the type map as a BiTypeMap map, which + * has the original type map as its own inverse. + */ def inverseTypeMap(using Context) = new BiTypeMap: def apply(tp: Type) = thisMap.inverse(tp) def inverse(tp: Type) = thisMap.apply(tp) + /** A restriction of this map to a function on tracked CaptureRefs */ def forward(ref: CaptureRef): CaptureRef = this(ref) match case result: CaptureRef if result.canBeTracked => result + /** A restriction of the inverse to a function on tracked CaptureRefs */ def backward(ref: CaptureRef): CaptureRef = inverse(ref) match case result: CaptureRef if result.canBeTracked => result end BiTypeMap @@ -5929,7 +5952,7 @@ object Types { else tp.derivedAnnotatedType(underlying, annot) } override protected def derivedCapturingType(tp: Type, parent: Type, refs: CaptureSet): Type = - parent match // ^^^ handle ranges in capture sets as well + parent match // TODO ^^^ handle ranges in capture sets as well case Range(lo, hi) => range(derivedCapturingType(tp, lo, refs), derivedCapturingType(tp, hi, refs)) case _ => diff --git a/compiler/src/dotty/tools/dotc/core/Variances.scala b/compiler/src/dotty/tools/dotc/core/Variances.scala index 0304825891ac..2401b43c8e17 100644 --- a/compiler/src/dotty/tools/dotc/core/Variances.scala +++ b/compiler/src/dotty/tools/dotc/core/Variances.scala @@ -4,7 +4,6 @@ package core import Types._, Contexts._, Flags._, Symbols._, Annotations._ import TypeApplications.TypeParamInfo import Decorators._ -import cc.CapturingType object Variances { diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala index 27fb9f68da32..475a258e8330 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreePickler.scala @@ -208,7 +208,6 @@ class TreePickler(pickler: TastyPickler) { writeByte(if (tpe.isType) TYPEREFdirect else TERMREFdirect) if Config.checkLevelsOnConstraints && !symRefs.contains(sym) && !sym.isPatternBound && !sym.hasAnnotation(defn.QuotedRuntimePatterns_patternTypeAnnot) then report.error(i"pickling reference to as yet undefined $tpe with symbol ${sym}", sym.srcPos) - // todo: find out why this happens for pos-customargs/captures/capt2 pickleSymRef(sym) } else tpe.designator match { diff --git a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala index 7becaff1386b..6887937ed6fe 100644 --- a/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/tasty/TreeUnpickler.scala @@ -32,7 +32,7 @@ import ast.{Trees, tpd, untpd} import Trees._ import Decorators._ import transform.SymUtils._ -import cc.adaptFunctionType +import cc.adaptFunctionTypeUnderCC import dotty.tools.tasty.{TastyBuffer, TastyReader} import TastyBuffer._ @@ -493,7 +493,7 @@ class TreeUnpickler(reader: TastyReader, * unless the unpickled class was also compiled with -Ycc. */ private def postProcessFunction(tp: Type)(using Context): Type = - if wasCaptureChecked then tp else tp.adaptFunctionType + if wasCaptureChecked then tp else tp.adaptFunctionTypeUnderCC // ------ Reading definitions ----------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala index 0c088abc0b2b..333cd9fa9ec3 100644 --- a/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala +++ b/compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala @@ -32,7 +32,7 @@ import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.annotation.switch import reporting._ -import cc.adaptFunctionType +import cc.adaptFunctionTypeUnderCC object Scala2Unpickler { @@ -825,7 +825,7 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas } else if args.nonEmpty then tycon.safeAppliedTo(EtaExpandIfHK(sym.typeParams, args.map(translateTempPoly))) - .adaptFunctionType + .adaptFunctionTypeUnderCC else if (sym.typeParams.nonEmpty) tycon.EtaExpand(sym.typeParams) else tycon case TYPEBOUNDStpe => diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 4aeced3d9522..926b0b4aff7e 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -203,14 +203,16 @@ class PlainPrinter(_ctx: Context) extends Printer { case tp @ EventuallyCapturingType(parent, refs) => def box = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value + def printRegular(refsText: Text) = + changePrec(GlobalPrec)(box ~ refsText ~ " " ~ toText(parent)) if printDebug && !refs.isConst then - changePrec(GlobalPrec)(box ~ s"$refs " ~ toText(parent)) + printRegular(refs.toString) else if ctx.settings.YccDebug.value then - changePrec(GlobalPrec)(box ~ refs.show ~ " " ~ toText(parent)) + printRegular(refs.show) else if !refs.isConst && refs.elems.isEmpty then - changePrec(GlobalPrec)(box ~ "?" ~ " " ~ toText(parent)) + printRegular("?") else if Config.printCaptureSetsAsPrefix then - changePrec(GlobalPrec)(box ~ toText(refs) ~ " " ~ toText(parent)) + printRegular(toText(refs)) else changePrec(InfixPrec)(toText(parent) ~ " retains " ~ box ~ toText(refs.toRetainsTypeArg)) case tp: PreviousErrorType if ctx.settings.XprintTypes.value => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 5670d816fafb..fee8a48fb168 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2340,15 +2340,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer // constructors are an exception as we don't allow constraining type params of classes rhsCtx.gadt.addToConstraint(tparamSyms) else if !sym.isPrimaryConstructor then - // otherwise, for secondary constructors we need a context that "knows" - // that their type parameters are aliases of the class type parameters. - // See pos/i941.scala - rhsCtx.gadt.addToConstraint(tparamSyms) - tparamSyms.lazyZip(sym.owner.typeParams).foreach { (psym, tparam) => - val tr = tparam.typeRef - rhsCtx.gadt.addBound(psym, tr, isUpper = false) - rhsCtx.gadt.addBound(psym, tr, isUpper = true) - } + linkConstructorParams(sym, tparamSyms, rhsCtx) if sym.isInlineMethod then rhsCtx.addMode(Mode.InlineableBody) if sym.is(ExtensionMethod) then rhsCtx.addMode(Mode.InExtensionMethod) From 0ce75e107771242082979fab44127c637d07c1a9 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 11 Aug 2022 23:29:35 +0200 Subject: [PATCH 85/99] Polishings mostly in typer --- compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala | 1 - compiler/src/dotty/tools/dotc/typer/Checking.scala | 11 ++++++++--- .../dotty/tools/dotc/typer/ImportSuggestions.scala | 3 ++- compiler/src/dotty/tools/dotc/typer/Inferencing.scala | 11 ++++------- .../src/dotty/tools/dotc/typer/TypeAssigner.scala | 6 ++---- compiler/src/dotty/tools/dotc/typer/Typer.scala | 6 ++++-- compiler/test/dotty/tools/dotc/CompilationTests.scala | 5 +++-- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala index 6de6ac02cf7d..e561b26abf6d 100644 --- a/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala +++ b/compiler/src/dotty/tools/dotc/sbt/ExtractAPI.scala @@ -196,7 +196,6 @@ private class ExtractAPICollector(using Context) extends ThunkHolder { private val byNameMarker = marker("ByName") private val matchMarker = marker("Match") private val superMarker = marker("Super") - private val retainsMarker = marker("Retains") /** Extract the API representation of a source file */ def apiSource(tree: Tree): Seq[api.ClassLike] = { diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 227d4e9457a9..a89fa91dc6a6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -599,7 +599,7 @@ object Checking { def checkNoPrivateLeaks(sym: Symbol)(using Context): Type = { class NotPrivate extends TypeMap { var errors: List[() => String] = Nil - var inCaptureSet: Boolean = false + private var inCaptureSet: Boolean = false def accessBoundary(sym: Symbol): Symbol = if (sym.is(Private) || !sym.owner.isClass) sym.owner @@ -622,7 +622,7 @@ object Checking { } && !(inCaptureSet && other.isAllOf(LocalParamAccessor)) // class parameters in capture sets are not treated as leaked since in - // phase -Ycc these are treated as normal vals. + // phase CheckCaptures these are treated as normal vals. def apply(tp: Type): Type = tp match { case tp: NamedType => @@ -660,9 +660,10 @@ object Checking { case tp @ AnnotatedType(underlying, annot) if annot.symbol == defn.RetainsAnnot || annot.symbol == defn.RetainsByNameAnnot => val underlying1 = this(underlying) + val saved = inCaptureSet inCaptureSet = true val annot1 = annot.mapWith(this) - inCaptureSet = false + inCaptureSet = saved derivedAnnotatedType(tp, underlying1, annot1) case _ => mapOver(tp) @@ -1434,6 +1435,10 @@ trait Checking { val kind = if pattern then "pattern selector" else "value" report.warning(MatchableWarning(tp, pattern), pos) + /** Check that there is an implicit capability to throw a checked exception + * if the saferExceptions feature is turned on. Return that capability is it exists, + * EmptyTree otherwise. + */ def checkCanThrow(tp: Type, span: Span)(using Context): Tree = if Feature.enabled(Feature.saferExceptions) && tp.isCheckedException then ctx.typer.implicitArgTree(defn.CanThrowClass.typeRef.appliedTo(tp), span) diff --git a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala index 48004481ba46..a9b53f0783bd 100644 --- a/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala +++ b/compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala @@ -319,7 +319,8 @@ trait ImportSuggestions: * If there's nothing to suggest, an empty string is returned. */ override def importSuggestionAddendum(pt: Type)(using Context): String = - if ctx.phase == Phases.checkCapturesPhase then return "" + if ctx.phase == Phases.checkCapturesPhase then + return "" // it's too late then to look for implicits val (fullMatches, headMatches) = importSuggestions(pt)(using ctx.fresh.setExploreTyperState()) implicits.println(i"suggestions for $pt in ${ctx.owner} = ($fullMatches%, %, $headMatches%, %)") diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 358297690cb1..27b83e025cf9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -13,7 +13,6 @@ import Decorators._ import config.Printers.{gadts, typr} import annotation.tailrec import reporting._ -import cc.{CapturingType, derivedCapturingType} import collection.mutable import scala.annotation.internal.sharable @@ -526,10 +525,10 @@ object Inferencing { } /** Replace every top-level occurrence of a wildcard type argument by - * a fresh skolem type. The skolem types are of the form $i.CAP, where - * $i is a skolem of type `scala.internal.TypeBox`, and `CAP` is its - * type member. See the documentation of `TypeBox` for a rationale why we do this. - */ + * a fresh skolem type. The skolem types are of the form $i.CAP, where + * $i is a skolem of type `scala.internal.TypeBox`, and `CAP` is its + * type member. See the documentation of `TypeBox` for a rationale why we do this. + */ def captureWildcards(tp: Type)(using Context): Type = derivedOnDealias(tp) { case tp @ AppliedType(tycon, args) if tp.hasWildcardArg => val tparams = tycon.typeParamSymbols @@ -545,7 +544,6 @@ object Inferencing { case tp: RefinedType => tp.derivedRefinedType(captureWildcards(tp.parent), tp.refinedName, tp.refinedInfo) case tp: RecType => tp.derivedRecType(captureWildcards(tp.parent)) case tp: LazyRef => captureWildcards(tp.ref) - case CapturingType(parent, refs) => tp.derivedCapturingType(captureWildcards(parent), refs) case tp: AnnotatedType => tp.derivedAnnotatedType(captureWildcards(tp.parent), tp.annot) case _ => tp } @@ -736,7 +734,6 @@ trait Inferencing { this: Typer => if !argType.isSingleton then argType = SkolemType(argType) argType <:< tvar case _ => - () // scala-meta complains if this is missing, but I could not mimimize further end constrainIfDependentParamRef } diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index 30cb40bc4d14..c8b01b3407b7 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -291,16 +291,14 @@ trait TypeAssigner { def safeSubstMethodParams(mt: MethodType, argTypes: List[Type])(using Context): Type = if mt.isResultDependent then safeSubstParams(mt.resultType, mt.paramRefs, argTypes) + else if mt.isCaptureDependent then mt.resultType.substParams(mt, argTypes) else mt.resultType def assignType(tree: untpd.Apply, fn: Tree, args: List[Tree])(using Context): Apply = { val ownType = fn.tpe.widen match { case fntpe: MethodType => if (fntpe.paramInfos.hasSameLengthAs(args) || ctx.phase.prev.relaxedTyping) - if fntpe.isCaptureDependent then - fntpe.resultType.substParams(fntpe, args.tpes) - else - safeSubstMethodParams(fntpe, args.tpes) + safeSubstMethodParams(fntpe, args.tpes) else errorType(i"wrong number of arguments at ${ctx.phase.prev} for $fntpe: ${fn.tpe}, expected: ${fntpe.paramInfos.length}, found: ${args.length}", tree.srcPos) case t => diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index fee8a48fb168..36e600faefc6 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1874,12 +1874,14 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val expr1 = typed(tree.expr, defn.ThrowableType) val cap = checkCanThrow(expr1.tpe.widen, tree.span) val res = Throw(expr1).withSpan(tree.span) - if cap.isEmpty || !ctx.settings.Ycc.value || ctx.isAfterTyper then res - else + if ctx.settings.Ycc.value && !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. Typed(res, TypeTree( AnnotatedType(res.tpe, Annotation(defn.RequiresCapabilityAnnot, cap)))) + else res def typedSeqLiteral(tree: untpd.SeqLiteral, pt: Type)(using Context): SeqLiteral = { val elemProto = pt.stripNull.elemType match { diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index a830a7949bb8..cbb7e867d0bb 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -241,8 +241,9 @@ class CompilationTests { given TestGroup = TestGroup("recheck") aggregateTests( compileFilesInDir("tests/new", recheckOptions), - compileFilesInDir("tests/pos", recheckOptions, FileFilter.exclude(TestSources.posTestRecheckExcluded)), - compileFilesInDir("tests/run", recheckOptions, FileFilter.exclude(TestSources.runTestRecheckExcluded)) + //Following are disabled since they take too long for what they provide. + //compileFilesInDir("tests/pos", recheckOptions, FileFilter.exclude(TestSources.posTestRecheckExcluded)), + //compileFilesInDir("tests/run", recheckOptions, FileFilter.exclude(TestSources.runTestRecheckExcluded)) ).checkCompile() // Explicit nulls tests From 30a53efff9aadc502f0e05eda12b506e23ad7fad Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Aug 2022 11:00:25 +0200 Subject: [PATCH 86/99] Reorganize retains annotation classes Since they are just backup syntax, no need to put them in the scala package. --- .../src/dotty/tools/dotc/ast/Desugar.scala | 2 +- .../dotty/tools/dotc/core/Definitions.scala | 4 +- library/src-bootstrapped/scala/Retains.scala | 7 --- .../scala/annotation/retains.scala | 15 ++++++ .../{ => annotation}/retainsByName.scala | 3 +- .../captures/capt-depfun.scala | 1 + .../captures/capt-depfun2.scala | 1 + .../neg-custom-args/captures/capt-test.scala | 1 + .../captures/capt-wf-typer.scala | 1 + tests/neg-custom-args/captures/capt1.check | 36 ++++++------- tests/neg-custom-args/captures/capt1.scala | 1 + tests/neg-custom-args/captures/capt3.scala | 1 + tests/neg-custom-args/captures/cc1.scala | 1 + tests/neg-custom-args/captures/io.scala | 1 + tests/neg-custom-args/captures/try.check | 52 +++++++++---------- tests/neg-custom-args/captures/try.scala | 1 + tests/pos-custom-args/captures/boxmap.scala | 1 + tests/pos-custom-args/captures/byname.scala | 3 ++ .../captures/capt-depfun.scala | 1 + .../captures/capt-depfun2.scala | 1 + tests/pos-custom-args/captures/capt0.scala | 4 +- tests/pos-custom-args/captures/capt2.scala | 2 +- .../pos-custom-args/captures/cc-expand.scala | 1 + tests/pos-custom-args/captures/classes.scala | 1 + .../captures/list-encoding.scala | 1 + tests/pos-custom-args/captures/try.scala | 1 + 26 files changed, 85 insertions(+), 59 deletions(-) delete mode 100644 library/src-bootstrapped/scala/Retains.scala create mode 100644 library/src-bootstrapped/scala/annotation/retains.scala rename library/src-bootstrapped/scala/{ => annotation}/retainsByName.scala (77%) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 3d783b6e0d5b..1e1db19bcf25 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1813,7 +1813,7 @@ object desugar { // convert `{refs} T` to `T @retains refs` // `{refs}-> T` to `-> (T @retainsByName refs)` def annotate(annotName: TypeName, tp: Tree) = - Annotated(tp, New(scalaDot(annotName), List(refs))) + Annotated(tp, New(scalaAnnotationDot(annotName), List(refs))) parent match case ByNameTypeTree(restpt) => cpy.ByNameTypeTree(parent)(annotate(tpnme.retainsByName, restpt)) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 255adb03573a..83d945352321 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1017,8 +1017,8 @@ class Definitions { @tu lazy val VarargsAnnot: ClassSymbol = requiredClass("scala.annotation.varargs") @tu lazy val SinceAnnot: ClassSymbol = requiredClass("scala.annotation.since") @tu lazy val RequiresCapabilityAnnot: ClassSymbol = requiredClass("scala.annotation.internal.requiresCapability") - @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.retains") - @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.retainsByName") + @tu lazy val RetainsAnnot: ClassSymbol = requiredClass("scala.annotation.retains") + @tu lazy val RetainsByNameAnnot: ClassSymbol = requiredClass("scala.annotation.retainsByName") @tu lazy val JavaRepeatableAnnot: ClassSymbol = requiredClass("java.lang.annotation.Repeatable") diff --git a/library/src-bootstrapped/scala/Retains.scala b/library/src-bootstrapped/scala/Retains.scala deleted file mode 100644 index 33a6b32a07d3..000000000000 --- a/library/src-bootstrapped/scala/Retains.scala +++ /dev/null @@ -1,7 +0,0 @@ -package scala -import annotation.experimental - -/** An annotation that indicates capture - */ -@experimental class retains(xs: Any*) extends annotation.StaticAnnotation - diff --git a/library/src-bootstrapped/scala/annotation/retains.scala b/library/src-bootstrapped/scala/annotation/retains.scala new file mode 100644 index 000000000000..0d0099de75fb --- /dev/null +++ b/library/src-bootstrapped/scala/annotation/retains.scala @@ -0,0 +1,15 @@ +package scala.annotation + +/** An annotation that indicates capture of a set of references under -Ycc. + * + * T @retains(x, y, z) + * + * is the internal representation used for the capturing type + * + * {x, y, z} T + * + * The annotation can also be written explicitly if one wants to avoid the + * non-standard capturing type syntax. + */ +@experimental class retains(xs: Any*) extends annotation.StaticAnnotation + diff --git a/library/src-bootstrapped/scala/retainsByName.scala b/library/src-bootstrapped/scala/annotation/retainsByName.scala similarity index 77% rename from library/src-bootstrapped/scala/retainsByName.scala rename to library/src-bootstrapped/scala/annotation/retainsByName.scala index ed4991c2da36..421e0400c4e1 100644 --- a/library/src-bootstrapped/scala/retainsByName.scala +++ b/library/src-bootstrapped/scala/annotation/retainsByName.scala @@ -1,5 +1,4 @@ -package scala -import annotation.experimental +package scala.annotation /** An annotation that indicates capture of an enclosing by-name type */ diff --git a/tests/neg-custom-args/captures/capt-depfun.scala b/tests/neg-custom-args/captures/capt-depfun.scala index 6b0beb92b313..14f08f569725 100644 --- a/tests/neg-custom-args/captures/capt-depfun.scala +++ b/tests/neg-custom-args/captures/capt-depfun.scala @@ -1,3 +1,4 @@ +import annotation.retains class C type Cap = C @retains(*) diff --git a/tests/neg-custom-args/captures/capt-depfun2.scala b/tests/neg-custom-args/captures/capt-depfun2.scala index 874d753b048d..62c2381e01ad 100644 --- a/tests/neg-custom-args/captures/capt-depfun2.scala +++ b/tests/neg-custom-args/captures/capt-depfun2.scala @@ -1,3 +1,4 @@ +import annotation.retains class C type Cap = C @retains(*) diff --git a/tests/neg-custom-args/captures/capt-test.scala b/tests/neg-custom-args/captures/capt-test.scala index 0face680a285..7080d6da67c6 100644 --- a/tests/neg-custom-args/captures/capt-test.scala +++ b/tests/neg-custom-args/captures/capt-test.scala @@ -1,3 +1,4 @@ +import annotation.retains import language.experimental.erasedDefinitions class CT[E <: Exception] diff --git a/tests/neg-custom-args/captures/capt-wf-typer.scala b/tests/neg-custom-args/captures/capt-wf-typer.scala index 5120e2b288d5..4fc50caed1f7 100644 --- a/tests/neg-custom-args/captures/capt-wf-typer.scala +++ b/tests/neg-custom-args/captures/capt-wf-typer.scala @@ -1,3 +1,4 @@ +import annotation.retains class C type Cap = {*} C diff --git a/tests/neg-custom-args/captures/capt1.check b/tests/neg-custom-args/captures/capt1.check index 62caa3e99d7e..4a9e3999f731 100644 --- a/tests/neg-custom-args/captures/capt1.check +++ b/tests/neg-custom-args/captures/capt1.check @@ -1,51 +1,51 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:3:2 ------------------------------------------ -3 | () => if x == null then y else y // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:4:2 ------------------------------------------ +4 | () => if x == null then y else y // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: {x} () -> ? C | Required: () -> C | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:6:2 ------------------------------------------ -6 | () => if x == null then y else y // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:7:2 ------------------------------------------ +7 | () => if x == null then y else y // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Found: {x} () -> ? C | Required: Matchable | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:13:2 ----------------------------------------- -13 | def f(y: Int) = if x == null then y else y // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:14:2 ----------------------------------------- +14 | def f(y: Int) = if x == null then y else y // error | ^ | Found: {x} Int -> Int | Required: Matchable -14 | f +15 | f | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:20:2 ----------------------------------------- -20 | class F(y: Int) extends A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:21:2 ----------------------------------------- +21 | class F(y: Int) extends A: // error | ^ | Found: {x} A | Required: A -21 | def m() = if x == null then y else y -22 | F(22) +22 | def m() = if x == null then y else y +23 | F(22) | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:25:2 ----------------------------------------- -25 | new A: // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:26:2 ----------------------------------------- +26 | new A: // error | ^ | Found: {x} A | Required: A -26 | def m() = if x == null then y else y +27 | def m() = if x == null then y else y | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:31:24 ---------------------------------------- -31 | val z2 = h[() -> Cap](() => x) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:32:24 ---------------------------------------- +32 | val z2 = h[() -> Cap](() => x) // error | ^^^^^^^ | Found: {x} () -> {*} C | Required: () -> box {*} C | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:32:5 ----------------------------------------- -32 | (() => C()) // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/capt1.scala:33:5 ----------------------------------------- +33 | (() => C()) // error | ^^^^^^^^^ | Found: ? () -> {*} C | Required: () -> box {*} C diff --git a/tests/neg-custom-args/captures/capt1.scala b/tests/neg-custom-args/captures/capt1.scala index a7c78eb521c8..ce69e77057e0 100644 --- a/tests/neg-custom-args/captures/capt1.scala +++ b/tests/neg-custom-args/captures/capt1.scala @@ -1,3 +1,4 @@ +import annotation.retains class C def f(x: C @retains(*), y: C): () -> C = () => if x == null then y else y // error diff --git a/tests/neg-custom-args/captures/capt3.scala b/tests/neg-custom-args/captures/capt3.scala index 6e9ea02fe8e3..4ffaf4a73c06 100644 --- a/tests/neg-custom-args/captures/capt3.scala +++ b/tests/neg-custom-args/captures/capt3.scala @@ -1,3 +1,4 @@ +import annotation.retains class C type Cap = C @retains(*) diff --git a/tests/neg-custom-args/captures/cc1.scala b/tests/neg-custom-args/captures/cc1.scala index ebd983c58fe9..7f3cd784ef84 100644 --- a/tests/neg-custom-args/captures/cc1.scala +++ b/tests/neg-custom-args/captures/cc1.scala @@ -1,3 +1,4 @@ +import annotation.retains object Test: def f[A <: Matchable @retains(*)](x: A): Matchable = x // error diff --git a/tests/neg-custom-args/captures/io.scala b/tests/neg-custom-args/captures/io.scala index c0cb11686b32..91af0167c9f9 100644 --- a/tests/neg-custom-args/captures/io.scala +++ b/tests/neg-custom-args/captures/io.scala @@ -1,3 +1,4 @@ +import annotation.retains sealed trait IO: def puts(msg: Any): Unit = println(msg) diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index 1464499eddac..c1e70dfcb54d 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,42 +1,42 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:22:49 ------------------------------------------ -22 | val a = handle[Exception, CanThrow[Exception]] { // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:23:49 ------------------------------------------ +23 | val a = handle[Exception, CanThrow[Exception]] { // error | ^ | Found: ? ({*} CT[Exception]) -> {*} CT[? >: box ? Exception <: box ? Exception] | Required: CanThrow[Exception] => box {*} CT[Exception] -23 | (x: CanThrow[Exception]) => x -24 | }{ +24 | (x: CanThrow[Exception]) => x +25 | }{ | | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:28:43 ------------------------------------------ -28 | val b = handle[Exception, () -> Nothing] { // error +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:29:43 ------------------------------------------ +29 | val b = handle[Exception, () -> Nothing] { // error | ^ | Found: ? (x: {*} CT[Exception]) -> {x} () -> ? Nothing | Required: CanThrow[Exception] => () -> Nothing -29 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) -30 | } { +30 | (x: CanThrow[Exception]) => () => raise(new Exception)(using x) +31 | } { | | longer explanation available when compiling with `-explain` --- Error: tests/neg-custom-args/captures/try.scala:39:4 ---------------------------------------------------------------- -34 | val xx = handle { -35 | (x: CanThrow[Exception]) => -36 | () => -37 | raise(new Exception)(using x) -38 | 22 -39 | } { // error +-- Error: tests/neg-custom-args/captures/try.scala:40:4 ---------------------------------------------------------------- +35 | val xx = handle { +36 | (x: CanThrow[Exception]) => +37 | () => +38 | raise(new Exception)(using x) +39 | 22 +40 | } { // error | ^ | The expression's type box {*} () -> Int is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -40 | (ex: Exception) => () => 22 -41 | } --- Error: tests/neg-custom-args/captures/try.scala:51:2 ---------------------------------------------------------------- -46 |val global = handle { -47 | (x: CanThrow[Exception]) => -48 | () => -49 | raise(new Exception)(using x) -50 | 22 -51 |} { // error +41 | (ex: Exception) => () => 22 +42 | } +-- Error: tests/neg-custom-args/captures/try.scala:52:2 ---------------------------------------------------------------- +47 |val global = handle { +48 | (x: CanThrow[Exception]) => +49 | () => +50 | raise(new Exception)(using x) +51 | 22 +52 |} { // error | ^ | The expression's type box {*} () -> Int is not allowed to capture the root capability `*`. | This usually means that a capability persists longer than its allowed lifetime. -52 | (ex: Exception) => () => 22 -53 |} +53 | (ex: Exception) => () => 22 +54 |} diff --git a/tests/neg-custom-args/captures/try.scala b/tests/neg-custom-args/captures/try.scala index 3aa3a47f69b4..35c7ea4829f2 100644 --- a/tests/neg-custom-args/captures/try.scala +++ b/tests/neg-custom-args/captures/try.scala @@ -1,3 +1,4 @@ +import annotation.retains import language.experimental.erasedDefinitions class CT[E <: Exception] diff --git a/tests/pos-custom-args/captures/boxmap.scala b/tests/pos-custom-args/captures/boxmap.scala index 2b610e55b871..5642763b5511 100644 --- a/tests/pos-custom-args/captures/boxmap.scala +++ b/tests/pos-custom-args/captures/boxmap.scala @@ -1,3 +1,4 @@ +import annotation.retains type Top = Any @retains(*) type Box[+T <: Top] = ([K <: Top] -> (T => K) -> K) diff --git a/tests/pos-custom-args/captures/byname.scala b/tests/pos-custom-args/captures/byname.scala index 5cb5255d4652..35b8876d0058 100644 --- a/tests/pos-custom-args/captures/byname.scala +++ b/tests/pos-custom-args/captures/byname.scala @@ -1,3 +1,4 @@ +import annotation.retainsByName class CC type Cap = {*} CC @@ -7,4 +8,6 @@ def test(cap1: Cap, cap2: Cap): {cap1} I = def f() = if cap1 == cap1 then I() else I() def h(x: {cap1}-> I) = x h(f()) // OK + def hh(x: -> I @retainsByName(cap1)) = x + h(f()) diff --git a/tests/pos-custom-args/captures/capt-depfun.scala b/tests/pos-custom-args/captures/capt-depfun.scala index 808b1b5e85f3..861f4a0d1c14 100644 --- a/tests/pos-custom-args/captures/capt-depfun.scala +++ b/tests/pos-custom-args/captures/capt-depfun.scala @@ -1,3 +1,4 @@ +import annotation.retains class C type Cap = C @retains(*) diff --git a/tests/pos-custom-args/captures/capt-depfun2.scala b/tests/pos-custom-args/captures/capt-depfun2.scala index 98ee9dbfdc6b..837d143d5141 100644 --- a/tests/pos-custom-args/captures/capt-depfun2.scala +++ b/tests/pos-custom-args/captures/capt-depfun2.scala @@ -1,3 +1,4 @@ +import annotation.retains class C type Cap = C @retains(*) diff --git a/tests/pos-custom-args/captures/capt0.scala b/tests/pos-custom-args/captures/capt0.scala index c8ff8a102856..2544e8abe5f1 100644 --- a/tests/pos-custom-args/captures/capt0.scala +++ b/tests/pos-custom-args/captures/capt0.scala @@ -2,6 +2,6 @@ object Test: def test() = val x: {*} Any = "abc" - val y: Object @scala.retains(x) = ??? - val z: Object @scala.retains(x, *) = y: Object @scala.retains(x) + val y: Object @scala.annotation.retains(x) = ??? + val z: Object @scala.annotation.retains(x, *) = y: Object @annotation.retains(x) diff --git a/tests/pos-custom-args/captures/capt2.scala b/tests/pos-custom-args/captures/capt2.scala index ac0b085d1253..204310d21ddf 100644 --- a/tests/pos-custom-args/captures/capt2.scala +++ b/tests/pos-custom-args/captures/capt2.scala @@ -1,4 +1,4 @@ -import scala.retains +import annotation.retains class C type Cap = C @retains(*) diff --git a/tests/pos-custom-args/captures/cc-expand.scala b/tests/pos-custom-args/captures/cc-expand.scala index 7bce1ea8387e..eba97f182385 100644 --- a/tests/pos-custom-args/captures/cc-expand.scala +++ b/tests/pos-custom-args/captures/cc-expand.scala @@ -1,3 +1,4 @@ +import annotation.retains object Test: class A diff --git a/tests/pos-custom-args/captures/classes.scala b/tests/pos-custom-args/captures/classes.scala index 243f70e02899..f14a7e6dd84e 100644 --- a/tests/pos-custom-args/captures/classes.scala +++ b/tests/pos-custom-args/captures/classes.scala @@ -1,3 +1,4 @@ +import annotation.retains class B type Cap = {*} B class C(val n: Cap): diff --git a/tests/pos-custom-args/captures/list-encoding.scala b/tests/pos-custom-args/captures/list-encoding.scala index 59ae61273af7..87630467023e 100644 --- a/tests/pos-custom-args/captures/list-encoding.scala +++ b/tests/pos-custom-args/captures/list-encoding.scala @@ -1,4 +1,5 @@ package listEncoding +import annotation.retains class Cap diff --git a/tests/pos-custom-args/captures/try.scala b/tests/pos-custom-args/captures/try.scala index 73e8a1d27ec7..dbc952cad3c0 100644 --- a/tests/pos-custom-args/captures/try.scala +++ b/tests/pos-custom-args/captures/try.scala @@ -1,3 +1,4 @@ +import annotation.retains import language.experimental.erasedDefinitions class CT[E <: Exception] From c091c336d55d24727a3f270719b8f24194d219ad Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Aug 2022 13:22:05 +0200 Subject: [PATCH 87/99] Move some cc classes to their proper place in the file hierarchy --- .../dotty/tools/dotc/cc/BoxedTypeCache.scala | 19 +++++++++++++++++++ .../dotc/{typer => cc}/CheckCaptures.scala | 0 2 files changed, 19 insertions(+) create mode 100644 compiler/src/dotty/tools/dotc/cc/BoxedTypeCache.scala rename compiler/src/dotty/tools/dotc/{typer => cc}/CheckCaptures.scala (100%) diff --git a/compiler/src/dotty/tools/dotc/cc/BoxedTypeCache.scala b/compiler/src/dotty/tools/dotc/cc/BoxedTypeCache.scala new file mode 100644 index 000000000000..56b3f5ba5047 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/cc/BoxedTypeCache.scala @@ -0,0 +1,19 @@ +package dotty.tools +package dotc +package cc + +import core.* +import Types.*, Symbols.*, Contexts.* + +/** A one-element cache for the boxed version of an unboxed capturing type */ +class BoxedTypeCache: + private var boxed: Type = compiletime.uninitialized + private var unboxed: Type = NoType + + def apply(tp: AnnotatedType)(using Context): Type = + if tp ne unboxed then + unboxed = tp + val CapturingType(parent, refs) = tp: @unchecked + boxed = CapturingType(parent, refs, boxed = true) + boxed +end BoxedTypeCache \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala similarity index 100% rename from compiler/src/dotty/tools/dotc/typer/CheckCaptures.scala rename to compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala From a158840fcea0697ee22bc42733652271a232f6af Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 12 Aug 2022 13:22:54 +0200 Subject: [PATCH 88/99] Polishing some cc classes --- .../tools/dotc/cc/CaptureAnnotation.scala | 32 ++++++++----------- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 21 ++++++++++-- .../dotty/tools/dotc/cc/CapturingType.scala | 27 ++++++++++++++-- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../stdlibExperimentalDefinitions.scala | 4 +-- 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala index 1f93887555d8..0fd96fe2462c 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureAnnotation.scala @@ -11,13 +11,23 @@ import config.Printers.capt import printing.Printer import printing.Texts.Text - +/** An annotation representing a capture set and whether it is boxed. + * It simulates a normal @retains annotation except that it is more efficient, + * supports variables as capture sets, and adds a `boxed` flag. + * These annotations are created during capture checking. Before that + * there are only regular @retains and @retainsByName annotations. + * @param refs the capture set + * @param boxed whether the type carrying the annotation is boxed + * @param cls the underlying class (either annotation.retains or annotation.retainsByName) + */ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) extends Annotation: import CaptureAnnotation.* import tpd.* + /** A cache for boxed version of a capturing type with this annotation */ val boxedType = BoxedTypeCache() + /** Reconstitute annotation tree from capture set */ override def tree(using Context) = val elems = refs.elems.toList.map { case cr: TermRef => ref(cr) @@ -30,8 +40,7 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte override def symbol(using Context) = cls override def derivedAnnotation(tree: Tree)(using Context): Annotation = - if refs == CaptureSet.universal then this - else unsupported("derivedAnnotation(Tree)") + unsupported(i"derivedAnnotation(Tree), $tree, $refs") def derivedAnnotation(refs: CaptureSet, boxed: Boolean)(using Context): Annotation = if (this.refs eq refs) && (this.boxed == boxed) then this @@ -42,9 +51,9 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte this.refs == refs && this.boxed == boxed && this.symbol == that.symbol case _ => false - override def mapWith(tp: TypeMap)(using Context) = + override def mapWith(tm: TypeMap)(using Context) = val elems = refs.elems.toList - val elems1 = elems.mapConserve(tp) + val elems1 = elems.mapConserve(tm) if elems1 eq elems then this else if elems1.forall(_.isInstanceOf[CaptureRef]) then derivedAnnotation(CaptureSet(elems1.asInstanceOf[List[CaptureRef]]*), boxed) @@ -66,16 +75,3 @@ case class CaptureAnnotation(refs: CaptureSet, boxed: Boolean)(cls: Symbol) exte case _ => false end CaptureAnnotation - -/** A one-element cache for the boxed version of an unboxed capturing type */ -class BoxedTypeCache: - private var boxed: Type = compiletime.uninitialized - private var unboxed: Type = NoType - - def apply(tp: AnnotatedType)(using Context): Type = - if tp ne unboxed then - unboxed = tp - val CapturingType(parent, refs) = tp: @unchecked - boxed = CapturingType(parent, refs, boxed = true) - boxed -end BoxedTypeCache \ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index e7ff0fad3c94..a2e21c17c501 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -13,18 +13,24 @@ import tpd.* private val Captures: Key[CaptureSet] = Key() private val BoxedType: Key[BoxedTypeCache] = Key() -def retainedElems(tree: Tree)(using Context): List[Tree] = tree match +/** The arguments of a @retains or @retainsByName annotation */ +private[cc] def retainedElems(tree: Tree)(using Context): List[Tree] = tree match case Apply(_, Typed(SeqLiteral(elems, _), _) :: Nil) => elems case _ => Nil +/** An exception thrown if a @retains argument is not syntactically a CaptureRef */ class IllegalCaptureRef(tpe: Type) extends Exception extension (tree: Tree) + /** Map tree with CaptureRef type to its type, throw IllegalCaptureRef otherwise */ def toCaptureRef(using Context): CaptureRef = tree.tpe match case ref: CaptureRef => ref case tpe => throw IllegalCaptureRef(tpe) + /** Convert a @retains or @retainsByName annotation tree to the capture set it represents. + * For efficience, the result is cached as an Attachment on the tree. + */ def toCaptureSet(using Context): CaptureSet = tree.getAttachment(Captures) match case Some(refs) => refs @@ -36,11 +42,16 @@ extension (tree: Tree) extension (tp: Type) + /** @pre `tp` is a CapturingType */ def derivedCapturingType(parent: Type, refs: CaptureSet)(using Context): Type = tp match case tp @ CapturingType(p, r) => if (parent eq p) && (refs eq r) then tp else CapturingType(parent, refs, tp.isBoxed) + /** If this is a unboxed capturing type with nonempty capture set, its boxed version. + * Or, if type is a TypeBounds of capturing types, the version where the bounds are boxed. + * The identity for all other types. + */ def boxed(using Context): Type = tp.dealias match case tp @ CapturingType(parent, refs) if !tp.isBoxed && !refs.isAlwaysEmpty => tp.annot match @@ -56,12 +67,16 @@ extension (tp: Type) case _ => tp + /** The boxed version of `tp`, unless `tycon` is a function symbol */ def boxedUnlessFun(tycon: Type)(using Context) = - if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionClass(tycon.typeSymbol) + if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionSymbol(tycon.typeSymbol) then tp else tp.boxed - /** The boxed capture set of a type */ + /** The capture set of `tp` consisting of all top-level captures under a box. + * Unlike for `boxed` this also considers parents of capture types, unions and + * intersections, and type proxies other than abstract types. + */ def boxedCaptureSet(using Context): CaptureSet = def getBoxed(tp: Type): CaptureSet = tp match case tp @ CapturingType(parent, refs) => diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index 49f0678e48e7..05e813793a63 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -5,20 +5,41 @@ package cc import core.* import Types.*, Symbols.*, Contexts.* -/** A capturing type. This is internally represented as an annotated type with a `retains` - * annotation, but the extractor will succeed only at phase CheckCaptures. +/** A (possibly boxed) capturing type. This is internally represented as an annotated type with a @retains + * or @retainsByName annotation, but the extractor will succeed only at phase CheckCaptures. + * That way, we can ignore caturing information until phase CheckCaptures since it is + * wrapped in a plain annotation. + * + * The same trick does not work for the boxing information. Boxing is context dependent, so + * we have to add that information in the Setup step preceding CheckCaptures. Boxes are + * added for all type arguments of methods. For type arguments of applied types a different + * strategy is used where we box arguments of applied types that are not functions when + * accessing the argument. + * + * An alternative strategy would add boxes also to arguments of applied types during setup. + * But this would have to be done for all possibly accessibly types from the compiled units + * as well as their dependencies. It's difficult to do this in a DenotationTransformer without + * accidentally forcing symbol infos. That's why this alternative was not implemented. + * If we would go back on this it would make sense to also treat captuyring types different + * from annotations and to generate them all during Setup and in DenotationTransformers. */ object CapturingType: + /** Smart constructor that drops empty capture sets and fuses compatible capturiong types. + * An outer type capturing type A can be fused with an inner capturing type B if their + * boxing status is the same or if A is boxed. + */ def apply(parent: Type, refs: CaptureSet, boxed: Boolean = false)(using Context): Type = if refs.isAlwaysEmpty then parent else parent match case parent @ CapturingType(parent1, refs1) if boxed || !parent.isBoxed => - // Fuse types except if nested type is boxed and current one isn't. apply(parent1, refs ++ refs1, boxed) case _ => AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) + /** An extractor that succeeds only during CheckCapturingPhase. Boxing statis is + * returned separately by CaptureOps.isBoxed. + */ def unapply(tp: AnnotatedType)(using Context): Option[(Type, CaptureSet)] = if ctx.phase == Phases.checkCapturesPhase && tp.annot.symbol == defn.RetainsAnnot then EventuallyCapturingType.unapply(tp) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 2f5cc6236913..935d22d14de2 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -36,7 +36,7 @@ import config.Printers.{core, typr, matchTypes} import reporting.{trace, Message} import java.lang.ref.WeakReference import compiletime.uninitialized -import cc.{CapturingType, CaptureSet, derivedCapturingType, retainedElems, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun} +import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} import scala.annotation.internal.sharable diff --git a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala index f934958ea90c..e8e2f0c08179 100644 --- a/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala +++ b/tests/run-custom-args/tasty-inspector/stdlibExperimentalDefinitions.scala @@ -50,8 +50,8 @@ val experimentalDefinitionInLibrary = Set( "scala.annotation.capability", "scala.annotation.internal.CaptureChecked", "scala.annotation.internal.requiresCapability", - "scala.retains", - "scala.retainsByName", + "scala.annotation.retains", + "scala.annotation.retainsByName", //// New APIs: Mirror // Can be stabilized in 3.3.0 or later. From 9ecfbc1ce88e8e2417871bce782d68dc988ddea4 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 14 Aug 2022 12:51:36 +0200 Subject: [PATCH 89/99] Polish and comment CaptureSet --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 10 +- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 414 ++++++++++++------ .../dotty/tools/dotc/core/TypeComparer.scala | 4 +- .../tools/dotc/printing/PlainPrinter.scala | 2 +- 4 files changed, 303 insertions(+), 127 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index a2e21c17c501..ee52d9b0eb6e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -73,7 +73,7 @@ extension (tp: Type) then tp else tp.boxed - /** The capture set of `tp` consisting of all top-level captures under a box. + /** The capture set consisting of all top-level captures of `tp` that appear under a box. * Unlike for `boxed` this also considers parents of capture types, unions and * intersections, and type proxies other than abstract types. */ @@ -89,8 +89,12 @@ extension (tp: Type) case _ => CaptureSet.empty getBoxed(tp) + /** Is the boxedCaptureSet of this type nonempty? */ def isBoxedCapturing(using Context) = !tp.boxedCaptureSet.isAlwaysEmpty + /** Map capturing type to their parents. Capturing types accessible + * via dealising are also stripped. + */ def stripCapturing(using Context): Type = tp.dealiasKeepAnnots match case CapturingType(parent, _) => parent.stripCapturing @@ -138,11 +142,15 @@ extension (sym: Symbol) && !sym.allowsRootCapture extension (tp: AnnotatedType) + /** Is this a boxed capturing type? */ def isBoxed(using Context): Boolean = tp.annot match case ann: CaptureAnnotation => ann.boxed case _ => false extension (ts: List[Type]) + /** Equivalent to ts.mapconserve(_.boxedUnlessFun(tycon)) but more efficient where + * it is the identity. + */ def boxedUnlessFun(tycon: Type)(using Context) = if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionClass(tycon.typeSymbol) then ts diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index f21519bc811c..fb726a73c486 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -19,15 +19,23 @@ import config.Config.ccAllowUnsoundMaps /** A class for capture sets. Capture sets can be constants or variables. * Capture sets support inclusion constraints <:< where <:< is subcapturing. - * They also allow mapping with arbitrary functions from elements to capture sets, - * by supporting a monadic flatMap operation. That is, constraints can be - * of one of the following forms + * + * They also allow + * - mapping with functions from elements to capture sets + * - filtering with predicates on elements + * - intersecting wo capture sets + * + * That is, constraints can be of the forms * * cs1 <:< cs2 - * cs1 = ∪ {f(x) | x ∈ cs2} + * cs1 = ∪ {f(x) | x ∈ cs2} where f is a function from capture references to capture sets. + * cs1 = ∪ {x | x ∈ cs2, p(x)} where p is a predicate on capture references + * cs1 = cs2 ∩ cs2 * - * where the `f`s are arbitrary functions from capture references to capture sets. * We call the resulting constraint system "monadic set constraints". + * To support capture propagation across maps, mappings are supported only + * if the mapped function is either a bijection or if it is idempotent + * on capture references (c.f. doc comment on `map` below). */ sealed abstract class CaptureSet extends Showable: import CaptureSet.* @@ -42,52 +50,53 @@ sealed abstract class CaptureSet extends Showable: */ def isConst: Boolean - /** Is this capture set always empty? For capture veraiables, returns - * always false + /** Is this capture set always empty? For unsolved capture veriables, returns + * always false. */ def isAlwaysEmpty: Boolean /** Is this capture set definitely non-empty? */ final def isNotEmpty: Boolean = !elems.isEmpty - /** Cast to Const. @pre: isConst */ + /** Convert to Const. @pre: isConst */ def asConst: Const = this match case c: Const => c case v: Var => assert(v.isConst) Const(v.elems) + /** Cast to variable. @pre: !isConst */ + def asVar: Var = + assert(!isConst) + asInstanceOf[Var] + + /** Does this capture set contain the root reference `*` as element? */ final def isUniversal(using Context) = elems.exists { case ref: TermRef => ref.symbol == defn.captureRoot case _ => false } - /** Cast to variable. @pre: !isConst */ - def asVar: Var = - assert(!isConst) - asInstanceOf[Var] - /** Add new elements to this capture set if allowed. * @pre `newElems` is not empty and does not overlap with `this.elems`. * Constant capture sets never allow to add new elements. * Variables allow it if and only if the new elements can be included - * in all their supersets. + * in all their dependent sets. * @param origin The set where the elements come from, or `empty` if not known. * @return CompareResult.OK if elements were added, or a conflicting * capture set that prevents addition otherwise. */ protected def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult - /** If this is a variable, add `cs` as a super set */ - protected def addSuper(cs: CaptureSet)(using Context, VarState): CompareResult + /** If this is a variable, add `cs` as a dependent set */ + protected def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult - /** If `cs` is a variable, add this capture set as one of its super sets */ - protected def addSub(cs: CaptureSet)(using Context): this.type = - cs.addSuper(this)(using ctx, UnrecordedState) + /** If `cs` is a variable, add this capture set as one of its dependent sets */ + protected def addAsDependentTo(cs: CaptureSet)(using Context): this.type = + cs.addDependent(this)(using ctx, UnrecordedState) this - /** Try to include all references of `elems` that are not yet accounted by this + /** Try to include all references of `elems` that are not yet accounted for by this * capture set. Inclusion is via `addNewElems`. * @param origin The set where the elements come from, or `empty` if not known. * @return CompareResult.OK if all unaccounted elements could be added, @@ -98,10 +107,12 @@ sealed abstract class CaptureSet extends Showable: if unaccounted.isEmpty then CompareResult.OK else addNewElems(unaccounted, origin) + /** Equivalent to `tryInclude({elem}, origin)`, but more efficient */ protected final def tryInclude(elem: CaptureRef, origin: CaptureSet)(using Context, VarState): CompareResult = if accountsFor(elem) then CompareResult.OK else addNewElems(elem.singletonCaptureSet.elems, origin) + /* x subsumes y if x is the same as y, or x is a this reference and y refers to a field of x */ extension (x: CaptureRef) private def subsumes(y: CaptureRef) = (x eq y) || y.match @@ -120,7 +131,9 @@ sealed abstract class CaptureSet extends Showable: /** A more optimistic version of accountsFor, which does not take variable supersets * of the `x` reference into account. A set might account for `x` if it accounts * for `x` in a state where we assume all supersets of `x` have just the elements - * known at this point. + * known at this point. On the other hand if x's capture set has no known elements, + * a set `cs` might account for `x` only if it subsumes `x` or it contains the + * root capability `*`. */ def mightAccountFor(x: CaptureRef)(using Context): Boolean = reporting.trace(i"$this mightAccountFor $x, ${x.captureSetOfInfo}?", show = true) { @@ -133,16 +146,21 @@ sealed abstract class CaptureSet extends Showable: } /** A more optimistic version of subCaptures used to choose one of two typing rules - * for selctions and applications. `cs1 mightSubcapture cs2` if `cs2` might account for + * for selections and applications. `cs1 mightSubcapture cs2` if `cs2` might account for * every element currently known to be in `cs1`. */ def mightSubcapture(that: CaptureSet)(using Context): Boolean = elems.forall(that.mightAccountFor) - /** The subcapturing test */ + /** The subcapturing test. + * @param frozen if true, no new variables or dependent sets are allowed to + * be added when making this test. An attempt to add either + * will result in failure. + */ final def subCaptures(that: CaptureSet, frozen: Boolean)(using Context): CompareResult = subCaptures(that)(using ctx, if frozen then FrozenState else VarState()) + /** The subcapturing test, using a given VarState */ private def subCaptures(that: CaptureSet)(using Context, VarState): CompareResult = def recur(elems: List[CaptureRef]): CompareResult = elems match case elem :: elems1 => @@ -152,13 +170,16 @@ sealed abstract class CaptureSet extends Showable: if result.isOK then recur(elems1) else - varState.abort() + varState.rollBack() result case Nil => - addSuper(that) + addDependent(that) recur(elems.toList) .showing(i"subcaptures $this <:< $that = ${result.show}", capt) + /** Two capture sets are considered =:= equal if they mutually subcapture each other + * in a frozen state. + */ def =:= (that: CaptureSet)(using Context): Boolean = this.subCaptures(that, frozen = true).isOK && that.subCaptures(this, frozen = true).isOK @@ -170,7 +191,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.elems ++ that.elems).addSub(this).addSub(that) + else Var(this.elems ++ that.elems).addAsDependentTo(this).addAsDependentTo(that) /** The smallest superset (via <:<) of this capture set that also contains `ref`. */ @@ -185,19 +206,22 @@ sealed abstract class CaptureSet extends Showable: else if this.isConst && that.isConst then Const(elemIntersection(this, that)) else Intersected(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` + */ def -- (that: CaptureSet.Const)(using Context): CaptureSet = val elems1 = elems.filter(!that.accountsFor(_)) if elems1.size == elems.size then this else if this.isConst then Const(elems1) else Diff(asVar, that) + /** The largest subset (via <:<) of this capture set that does not account for `ref` */ def - (ref: CaptureRef)(using Context): CaptureSet = this -- ref.singletonCaptureSet - def disallowRootCapability(handler: () => Unit)(using Context): this.type = - if isUniversal then handler() - this - + /** The largest subset (via <:<) of this capture set that only contains elements + * for which `p` is true. + */ def filter(p: CaptureRef => Boolean)(using Context): CaptureSet = if this.isConst then val elems1 = elems.filter(p) @@ -233,28 +257,38 @@ sealed abstract class CaptureSet extends Showable: if mappedElems == elems then this else Const(mappedElems) else BiMapped(asVar, tm, mappedElems) - case tm: IdentityCaptRefMap => this + case tm: IdentityCaptRefMap => + this case _ => val mapped = mapRefs(elems, tm, tm.variance) if isConst then if mapped.isConst && mapped.elems == elems then this else mapped - else tm match - case tm: IdempotentCaptRefMap => Mapped(asVar, tm, tm.variance, mapped) - case _ if ccAllowUnsoundMaps => OtherMapped(asVar, tm, tm.variance, mapped) + else Mapped(asVar, tm, tm.variance, mapped) + /** A mapping resulting from substituting parameters of a BindingType to a list of types */ def substParams(tl: BindingType, to: List[Type])(using Context) = map(Substituters.SubstParamsMap(tl, to)) - /** An upper approximation of this capture set. This is the set itself - * except for real (non-mapped, non-filtered) capture set variables, where + /** Invoke handler if this set has (or later aquires) the root capability `*` */ + def disallowRootCapability(handler: () => Unit)(using Context): this.type = + if isUniversal then handler() + this + + /** An upper approximation of this capture set, i.e. a constant set that is + * subcaptured by this set. If the current set is a variable * it is the intersection of all upper approximations of known supersets * of the variable. * The upper approximation is meaningful only if it is constant. If not, * `upperApprox` can return an arbitrary capture set variable. + * `upperApprox` is used in `solve`. */ protected def upperApprox(origin: CaptureSet)(using Context): CaptureSet + /** Assuming set this set dependds on was just solved to be constant, propagate this info + * to this set. This might result in the set being solved to be constant + * itself. + */ protected def propagateSolved()(using Context): Unit = () /** This capture set with a description that tells where it comes from */ @@ -263,11 +297,7 @@ sealed abstract class CaptureSet extends Showable: /** The provided description (using `withDescription`) for this capture set or else "" */ def description: String - def toRetainsTypeArg(using Context): Type = - assert(isConst) - ((NoType: Type) /: elems) ((tp, ref) => - if tp.exists then OrType(tp, ref, soft = false) else ref) - + /** A regular @retains or @retainsByName annotation with the elements of this set as arguments. */ def toRegularAnnotation(cls: Symbol)(using Context): Annotation = Annotation(CaptureAnnotation(this, boxed = false)(cls).tree) @@ -279,12 +309,14 @@ object CaptureSet: type Vars = SimpleIdentitySet[Var] type Deps = SimpleIdentitySet[CaptureSet] + @sharable private var varId = 0 + /** If set to `true`, capture stack traces that tell us where sets are created */ private final val debugSets = false private val emptySet = SimpleIdentitySet.empty - @sharable private var varId = 0 + /** The empty capture set `{}` */ val empty: CaptureSet.Const = Const(emptySet) /** The universal capture set `{*}` */ @@ -301,6 +333,7 @@ object CaptureSet: def apply(elems: Refs)(using Context): CaptureSet.Const = if elems.isEmpty then empty else Const(elems) + /** The subclass of constant capture sets with given elements `elems` */ class Const private[CaptureSet] (val elems: Refs, val description: String = "") extends CaptureSet: def isConst = true def isAlwaysEmpty = elems.isEmpty @@ -308,7 +341,7 @@ object CaptureSet: def addNewElems(elems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = CompareResult.fail(this) - def addSuper(cs: CaptureSet)(using Context, VarState) = CompareResult.OK + def addDependent(cs: CaptureSet)(using Context, VarState) = CompareResult.OK def upperApprox(origin: CaptureSet)(using Context): CaptureSet = this @@ -317,34 +350,54 @@ object CaptureSet: override def toString = elems.toString end Const + /** The subclass of captureset variables with given initial elements */ class Var(initialElems: Refs = emptySet) extends CaptureSet: + + /** A unique identification number for diagnostics */ val id = varId += 1 varId + /** A variable is solved if it is aproximated to a from-then-on constant set. */ private var isSolved: Boolean = false + /** The elements currently known to be in the set */ var elems: Refs = initialElems + + /** The sets currently known to be dependent sets (i.e. new additions to this set + * are propagated to these dependent sets.) + */ var deps: Deps = emptySet + def isConst = isSolved def isAlwaysEmpty = false + + /** A handler to be invoked if the root reference `*` is added to this set */ var addRootHandler: () => Unit = () => () var description: String = "" + /** Record current elements in given VarState provided it does not yet + * contain an entry for this variable. + */ private def recordElemsState()(using VarState): Boolean = varState.getElems(this) match case None => varState.putElems(this, elems) case _ => true + /** Record current dependent sets in given VarState provided it does not yet + * contain an entry for this variable. + */ private[CaptureSet] def recordDepsState()(using VarState): Boolean = varState.getDeps(this) match case None => varState.putDeps(this, deps) case _ => true + /** Reset elements to what was recorded in `state` */ def resetElems()(using state: VarState): Unit = elems = state.elems(this) + /** Reset dependent sets to what was recorded in `state` */ def resetDeps()(using state: VarState): Unit = deps = state.deps(this) @@ -356,11 +409,11 @@ object CaptureSet: (CompareResult.OK /: deps) { (r, dep) => r.andAlso(dep.tryInclude(newElems, this)) } - else + else // fail if variable is solved or given VarState is frozen CompareResult.fail(this) - def addSuper(cs: CaptureSet)(using Context, VarState): CompareResult = - if (cs eq this) || cs.elems.contains(defn.captureRoot.termRef) || isConst then + def addDependent(cs: CaptureSet)(using Context, VarState): CompareResult = + if (cs eq this) || cs.isUniversal || isConst then CompareResult.OK else if recordDepsState() then deps += cs @@ -374,6 +427,10 @@ object CaptureSet: private var computingApprox = false + /** Roughly: the intersection of all constant known supersets of this set. + * The aim is to find an as-good-as-possible constant set that is a superset + * of this set. The universal set {*} is a sound fallback. + */ final def upperApprox(origin: CaptureSet)(using Context): CaptureSet = if computingApprox then universal else if isConst then this @@ -382,17 +439,13 @@ object CaptureSet: try computeApprox(origin).ensuring(_.isConst) finally computingApprox = false - def withDescription(description: String): this.type = - this.description = - if this.description.isEmpty then description - else s"${this.description} and $description" - this - + /** The intersection of all upper approximations of dependent sets */ protected def computeApprox(origin: CaptureSet)(using Context): CaptureSet = (universal /: deps) { (acc, sup) => acc ** sup.upperApprox(this) } /** Widen the variable's elements to its upper approximation and - * mark it as constant from now on. + * mark it as constant from now on. This is used for contra-variant type variables + * in the results of defs and vals. */ def solve()(using Context): Unit = if !isConst then @@ -402,16 +455,33 @@ object CaptureSet: if newElems.isEmpty || addNewElems(newElems, empty)(using ctx, VarState()).isOK then markSolved() + /** Mark set as solved and propagate this info to all dependent sets */ def markSolved()(using Context): Unit = isSolved = true deps.foreach(_.propagateSolved()) + def withDescription(description: String): this.type = + this.description = + if this.description.isEmpty then description + else s"${this.description} and $description" + this + + /** Used for diagnostics and debugging: A string that traces the creation + * history of a variable by following source links. Each variable on the + * path is characterized by the variable's id and the first letter of the + * variable's class name. The path ends in a plain variable with letter `V` that + * is not derived from some other variable. + */ protected def ids(using Context): String = val trail = this.match case dv: DerivedVar => dv.source.ids case _ => "" s"$id${getClass.getSimpleName.nn.take(1)}$trail" + /** Adds variables to the ShownVars context property if that exists, which + * establishes a record of all variables printed in an error message. + * Prints variables wih ids under -Ycc-debug. + */ override def toText(printer: Printer): Text = inContext(printer.printerContext) { for vars <- ctx.property(ShownVars) do vars += this super.toText(printer) ~ (Str(ids) provided !isConst && ctx.settings.YccDebug.value) @@ -420,47 +490,88 @@ object CaptureSet: override def toString = s"Var$id$elems" end Var + /** A variable that is derived from some other variable via a map or filter. */ abstract class DerivedVar(initialElems: Refs)(using @constructorOnly ctx: Context) extends Var(initialElems): - def source: Var + + // For debugging: A trace where a set was created. Note that logically it would make more + // sense to place this variable in Mapped, but that runs afoul of the initializatuon checker. val stack = if debugSets && this.isInstanceOf[Mapped] then (new Throwable).getStackTrace().nn.take(20) else null - addSub(source) + /** The variable from which this variable is derived */ + def source: Var + + addAsDependentTo(source) override def propagateSolved()(using Context) = if source.isConst && !isConst then markSolved() end DerivedVar /** A variable that changes when `source` changes, where all additional new elements are mapped - * using ∪ { f(x) | x <- elems }. + * using ∪ { tm(x) | x <- source.elems }. + * @param source the original set that is mapped + * @param tm the type map, which is assumed to be idempotent on capture refs + * (except if ccUnsoundMaps is enabled) + * @param variance the assumed variance with which types with capturesets of size >= 2 are approximated + * (i.e. co: full capture set, contra: empty set, nonvariant is not allowed.) + * @param initial The initial mappings of source's elements at the point the Mapped set is created. */ class Mapped private[CaptureSet] (val source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) extends DerivedVar(initial.elems): - addSub(initial) + addAsDependentTo(initial) // initial mappings could change by propagation + + private def mapIsIdempotent = tm.isInstanceOf[IdempotentCaptRefMap] + + assert(ccAllowUnsoundMaps || mapIsIdempotent) + + private def whereCreated(using Context): String = + if stack == null then "" + else i""" + |Stack trace of variable creation:" + |${stack.mkString("\n")}""" override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - addNewElemsImpl(newElems: Refs, origin: CaptureSet) - .andAlso(if origin ne source then source.tryInclude(newElems, this) else CompareResult.OK) - // `tm` is assumed idempotent, propagate back elems from image set. - // This is sound, since we know that for `r in newElems: tm(r) = r`, hence - // `r` is _one_ possible solution in `source` that would make an `r` appear in this set. - // It's not necessarily the only possible solultion, so the scheme is incomplete. - - protected def addNewElemsImpl(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = val added = - if origin eq source then mapRefs(newElems, tm, variance) - else Const(newElems) + if origin eq source then // elements have to be mapped + mapRefs(newElems, tm, variance) + else + // elements are added by subcapturing propagation with this Mapped set + // as superset; no mapping is necessary or allowed. + Const(newElems) super.addNewElems(added.elems, origin) .andAlso { if added.isConst then CompareResult.OK - else if added.asVar.recordDepsState() then { addSub(added); CompareResult.OK } + else if added.asVar.recordDepsState() then { addAsDependentTo(added); CompareResult.OK } else CompareResult.fail(this) } + .andAlso { + if (origin ne source) && mapIsIdempotent then + // `tm` is idempotent, propagate back elems from image set. + // This is sound, since we know that for `r in newElems: tm(r) = r`, hence + // `r` is _one_ possible solution in `source` that would make an `r` appear in this set. + // It's not necessarily the only possible solution, so the scheme is incomplete. + source.tryInclude(newElems, this) + else if !mapIsIdempotent && variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) then + // The map is neither a BiTypeMap nor an idempotent type map. + // In that case there's no much we can do. + // The scheme then does not propagate added elements back to source and rejects adding + // elements from variable sources in contra- and non-variant positions. In essence, + // we approximate types resulting from such maps by returning a possible super type + // from the actual type. But this is neither sound nor complete. + report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") + CompareResult.fail(this) + else + CompareResult.OK + } override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = - if source eq origin then universal - else source.upperApprox(this).map(tm) + if source eq origin then + // it's a mapping of origin, so not a superset of `origin`, + // therefore don't contribute to the intersection. + universal + else + source.upperApprox(this).map(tm) override def propagateSolved()(using Context) = if initial.isConst then super.propagateSolved() @@ -468,33 +579,9 @@ object CaptureSet: override def toString = s"Mapped$id($source, elems = $elems)" end Mapped - - /** Should be avoided as much as possible: - * A mapped set that uses neither a BiTypeMap nor an idempotent type map. - * In that case there's no much we can do. - * The patch does not propagate added elements back to source and rejects adding - * elements from variable sources in contra- and non-variant positions. In essence, - * we approximate types resulting from such maps by returning a possible super type - * from the actual type. + /** A mapping where the type map is required to be a bijection. + * Parameters as in Mapped. */ - final class OtherMapped private[CaptureSet] - (source: Var, tm: TypeMap, variance: Int, initial: CaptureSet)(using @constructorOnly ctx: Context) - extends Mapped(source, tm, variance, initial): - - private def whereCreated(using Context): String = - if stack == null then "" - else i""" - |Stack trace of variable creation:" - |${stack.mkString("\n")}""" - - override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - if variance <= 0 && !origin.isConst && (origin ne initial) && (origin ne source) then - report.warning(i"trying to add elems ${CaptureSet(newElems)} from unrecognized source $origin of mapped set $this$whereCreated") - CompareResult.fail(this) - else - addNewElemsImpl(newElems, origin) - end OtherMapped - final class BiMapped private[CaptureSet] (val source: Var, bimap: BiTypeMap, initialElems: Refs)(using @constructorOnly ctx: Context) extends DerivedVar(initialElems): @@ -509,6 +596,13 @@ object CaptureSet: .showing(i"propagating new elems ${CaptureSet(newElems)} backward from $this to $source", capt) } + /** For a BiTypeMap, supertypes of the mapped type also constrain + * the source via the inverse type mapping and vice versa. That is, if + * B = f(A) and B <: C, then A <: f^-1(C), so C should flow into + * the upper approximation of A. + * Conversely if A <: C2, then we also know that B <: f(C2). + * These situations are modeled by the two branches of the conditional below. + */ override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = val supApprox = super.computeApprox(this) if source eq origin then supApprox.map(bimap.inverseTypeMap) @@ -523,11 +617,25 @@ object CaptureSet: extends DerivedVar(source.elems.filter(p)): override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - super.addNewElems(newElems.filter(p), origin) + val filtered = newElems.filter(p) + if origin eq source then + super.addNewElems(filtered, origin) + else + // Filtered elements have to be back-propagated to source. + // Elements that don't satisfy `p` are not allowed. + super.addNewElems(newElems, origin) + .andAlso { + if filtered.size == newElems.size then source.tryInclude(newElems, this) + else CompareResult.fail(this) + } override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = - if source eq origin then universal - else source.upperApprox(this).filter(p) + if source eq origin then + // it's a filter of origin, so not a superset of `origin`, + // therefore don't contribute to the intersection. + universal + else + source.upperApprox(this).filter(p) override def toString = s"${getClass.getSimpleName}$id($source, elems = $elems)" end Filtered @@ -536,30 +644,45 @@ object CaptureSet: class Diff(source: Var, other: Const)(using Context) extends Filtered(source, !other.accountsFor(_)) - def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = - cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) - class Intersected(cs1: CaptureSet, cs2: CaptureSet)(using Context) extends Var(elemIntersection(cs1, cs2)): - addSub(cs1) - addSub(cs2) + addAsDependentTo(cs1) + addAsDependentTo(cs2) deps += cs1 deps += cs2 override def addNewElems(newElems: Refs, origin: CaptureSet)(using Context, VarState): CompareResult = - super.addNewElems( + val added = if origin eq cs1 then newElems.filter(cs2.accountsFor) else if origin eq cs2 then newElems.filter(cs1.accountsFor) - else newElems, origin) + else newElems + // If origin is not cs1 or cs2, then newElems will be propagated to + // cs1, cs2 since they are in deps. + super.addNewElems(added, origin) override def computeApprox(origin: CaptureSet)(using Context): CaptureSet = - if (origin eq cs1) || (origin eq cs2) then universal - else CaptureSet(elemIntersection(cs1.upperApprox(this), cs2.upperApprox(this))) + if (origin eq cs1) || (origin eq cs2) then + // it's a combination of origin with some other set, so not a superset of `origin`, + // therefore don't contribute to the intersection. + universal + else + CaptureSet(elemIntersection(cs1.upperApprox(this), cs2.upperApprox(this))) override def propagateSolved()(using Context) = if cs1.isConst && cs2.isConst && !isConst then markSolved() end Intersected + def elemIntersection(cs1: CaptureSet, cs2: CaptureSet)(using Context): Refs = + cs1.elems.filter(cs2.mightAccountFor) ++ cs2.elems.filter(cs1.mightAccountFor) + + /** Extrapolate tm(r) according to `variance`. Let r1 be the result of tm(r). + * - If r1 is a tracked CaptureRef, return {r1} + * - If r1 has an empty capture set, return {} + * - Otherwise, + * - if the variance is covariant, return r1's capture set + * - if the variance is contravariant, return {} + * - Otherwise assertion failure + */ def extrapolateCaptureRef(r: CaptureRef, tm: TypeMap, variance: Int)(using Context): CaptureSet = val r1 = tm(r) val upper = r1.captureSet @@ -569,9 +692,11 @@ object CaptureSet: else if variance < 0 then CaptureSet.empty else assert(false, i"trying to add $upper from $r via ${tm.getClass} in a non-variant setting") + /** Apply `f` to each element in `xs`, and join result sets with `++` */ def mapRefs(xs: Refs, f: CaptureRef => CaptureSet)(using Context): CaptureSet = ((empty: CaptureSet) /: xs)((cs, x) => cs ++ f(x)) + /** Apply extrapolated `tm` to each element in `xs`, and join result sets with `++` */ def mapRefs(xs: Refs, tm: TypeMap, variance: Int)(using Context): CaptureSet = mapRefs(xs, extrapolateCaptureRef(_, tm, variance)) @@ -598,53 +723,93 @@ object CaptureSet: /** A TypeMap that is the identity on capture references */ trait IdentityCaptRefMap extends TypeMap - type CompareResult = CompareResult.Type + type CompareResult = CompareResult.TYPE - /** None = ok, Some(cs) = failure since not a subset of cs */ + /** The result of subcapturing comparisons is an opaque type CompareResult.TYPE. + * This is either OK, indicating success, or + * another capture set, indicating failure. The failure capture set + * is the one that did not allow propagaton of elements into it. + */ object CompareResult: - opaque type Type = CaptureSet - val OK: Type = Const(emptySet) - def fail(cs: CaptureSet): Type = cs - extension (result: Type) + opaque type TYPE = CaptureSet + val OK: TYPE = Const(emptySet) + def fail(cs: CaptureSet): TYPE = cs + + extension (result: TYPE) + /** The result is OK */ def isOK: Boolean = result eq OK + /** If not isOK, the blocking capture set */ def blocking: CaptureSet = result + inline def andAlso(op: Context ?=> TYPE)(using Context): TYPE = if result.isOK then op else result def show(using Context): String = if result.isOK then "OK" else i"$result" - inline def andAlso(op: Context ?=> Type)(using Context): Type = if result.isOK then op else result + end CompareResult + /** A VarState serves as a snapshot mechanism that can undo + * additions of elements or super sets if an operation fails + */ class VarState: + + /** A map from captureset variables to their elements at the time of the snapshot. */ private val elemsMap: util.EqHashMap[Var, Refs] = new util.EqHashMap + + /** A map from captureset variables to their dependent sets at the time of the snapshot. */ private val depsMap: util.EqHashMap[Var, Deps] = new util.EqHashMap + /** The recorded elements of `v` (it's required that a recording was made) */ def elems(v: Var): Refs = elemsMap(v) + + /** Optionally the recorded elements of `v`, None if nothing was recorded for `v` */ def getElems(v: Var): Option[Refs] = elemsMap.get(v) + + /** Record elements, return whether this was allowed. + * By default, recording is allowed but the special state FrozenState + * overrides this. + */ def putElems(v: Var, elems: Refs): Boolean = { elemsMap(v) = elems; true } + /** The recorded dependent sets of `v` (it's required that a recording was made) */ def deps(v: Var): Deps = depsMap(v) + + /** Optionally the recorded dependent sets of `v`, None if nothing was recorded for `v` */ def getDeps(v: Var): Option[Deps] = depsMap.get(v) + + /** Record dependent sets, return whether this was allowed. + * By default, recording is allowed but the special state FrozenState + * overrides this. + */ def putDeps(v: Var, deps: Deps): Boolean = { depsMap(v) = deps; true } - def abort(): Unit = + /** Roll back global state to what was recorded in this VarState */ + def rollBack(): Unit = elemsMap.keysIterator.foreach(_.resetElems()(using this)) depsMap.keysIterator.foreach(_.resetDeps()(using this)) end VarState + /** A special state that does not allow to record elements or dependent sets. + * In effect this means that no new elements or dependent sets can be added + * in this state (since the previous state cannot be recorded in a snapshot) + */ @sharable object FrozenState extends VarState: override def putElems(v: Var, refs: Refs) = false override def putDeps(v: Var, deps: Deps) = false - override def abort(): Unit = () + override def rollBack(): Unit = () @sharable - object UnrecordedState extends VarState: + /** A special state that turns off recording of elements. Used only + * in `addSub` to prevent cycles in recordings. + */ + private object UnrecordedState extends VarState: override def putElems(v: Var, refs: Refs) = true override def putDeps(v: Var, deps: Deps) = true - override def abort(): Unit = () + override def rollBack(): Unit = () + /** The current VarState, as passed by the implicit context */ def varState(using state: VarState): VarState = state + /* Not needed: def ofClass(cinfo: ClassInfo, argTypes: List[Type])(using Context): CaptureSet = CaptureSet.empty - /* def captureSetOf(tp: Type): CaptureSet = tp match case tp: TypeRef if tp.symbol.is(ParamAccessor) => def mapArg(accs: List[Symbol], tps: List[Type]): CaptureSet = accs match @@ -662,12 +827,14 @@ object CaptureSet: arg <- parent.argInfos yield captureSetOf(arg) css.foldLeft(empty)(_ ++ _) - */ + */ + /** The capture set of the type underlying a CaptureRef */ def ofInfo(ref: CaptureRef)(using Context): CaptureSet = ref match case ref: TermRef if ref.isRootCapability => ref.singletonCaptureSet case _ => ofType(ref.underlying) + /** Capture set of a type */ def ofType(tp: Type)(using Context): CaptureSet = def recur(tp: Type): CaptureSet = tp.dealias match case tp: TermRef => @@ -691,8 +858,6 @@ object CaptureSet: recur(tp1) ** recur(tp2) case OrType(tp1, tp2) => recur(tp1) ++ recur(tp2) - case tp: ClassInfo => - ofClass(tp, Nil) case _ => empty recur(tp) @@ -700,6 +865,9 @@ object CaptureSet: private val ShownVars: Property.Key[mutable.Set[Var]] = Property.Key() + /** Perform `op`. Under -Ycc-debug, collect and print info about all variables reachable + * via `(_.deps)*` from the variables that were shown in `op`. + */ def withCaptureSetsExplained[T](op: Context ?=> T)(using ctx: Context): T = if ctx.settings.YccDebug.value then val shownVars = mutable.Set[Var]() diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 0e981a8a5417..f05e12ccea13 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2568,7 +2568,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling false } - protected def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult.Type = + protected def subCaptures(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 @@ -3215,7 +3215,7 @@ class ExplainingTypeComparer(initctx: Context) extends TypeComparer(initctx) { super.gadtAddBound(sym, b, isUpper) } - override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult.Type = + override def subCaptures(refs1: CaptureSet, refs2: CaptureSet, frozen: Boolean)(using Context): CaptureSet.CompareResult = traceIndented(i"subcaptures $refs1 <:< $refs2 ${if frozen then "frozen" else ""}") { super.subCaptures(refs1, refs2, frozen) } diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index 926b0b4aff7e..11001fc4d115 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -214,7 +214,7 @@ class PlainPrinter(_ctx: Context) extends Printer { else if Config.printCaptureSetsAsPrefix then printRegular(toText(refs)) else - changePrec(InfixPrec)(toText(parent) ~ " retains " ~ box ~ toText(refs.toRetainsTypeArg)) + changePrec(InfixPrec)(box ~ toText(parent) ~ " @retains(" ~ toText(refs.elems.toList, ",") ~ ")") case tp: PreviousErrorType if ctx.settings.XprintTypes.value => "" // do not print previously reported error message because they may try to print this error type again recuresevely case tp: ErrorType => From aeafb05eca74c853c58afbbd5b9aaa4e1ff5dbb4 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 15 Aug 2022 14:20:18 +0200 Subject: [PATCH 90/99] Work on rechecker - comments - some refactorings - enable rechecking tests --- compiler/src/dotty/tools/dotc/Compiler.scala | 2 + .../dotty/tools/dotc/cc/CheckCaptures.scala | 4 + .../tools/dotc/config/ScalaSettings.scala | 2 +- .../tools/dotc/transform/PreRecheck.scala | 2 +- .../dotty/tools/dotc/transform/Recheck.scala | 79 ++++++++++++++----- compiler/test/dotc/pos-test-recheck.excludes | 17 ++++ compiler/test/dotc/run-test-recheck.excludes | 13 +++ .../dotty/tools/dotc/CompilationTests.scala | 4 +- .../tools/vulpix/TestConfiguration.scala | 2 +- tests/pos/patmatch-in-catch.scala | 15 ++++ 10 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 tests/pos/patmatch-in-catch.scala diff --git a/compiler/src/dotty/tools/dotc/Compiler.scala b/compiler/src/dotty/tools/dotc/Compiler.scala index fb972c5e7973..46d36c4412c7 100644 --- a/compiler/src/dotty/tools/dotc/Compiler.scala +++ b/compiler/src/dotty/tools/dotc/Compiler.scala @@ -79,6 +79,8 @@ class Compiler { new SpecializeApplyMethods, // Adds specialized methods to FunctionN new TryCatchPatterns, // Compile cases in try/catch new PatternMatcher) :: // Compile pattern matches + List(new TestRecheck.Pre) :: // Test only: run rechecker, enabled under -Yrecheck-test + List(new TestRecheck) :: // Test only: run rechecker, enabled under -Yrecheck-test List(new CheckCaptures.Pre) :: // Preparations for check captures phase, enabled under -Ycc List(new CheckCaptures) :: // Check captures, enabled under -Ycc List(new ElimOpaque, // Turn opaque into normal aliases diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 10b39ae736f9..6864b4f5fe45 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -128,6 +128,10 @@ class CheckCaptures extends Recheck, SymTransformer: class CaptureChecker(ictx: Context) extends Rechecker(ictx): import ast.tpd.* + override def keepType(tree: Tree) = + super.keepType(tree) + || tree.isInstanceOf[Try] // type of `try` needs tp be checked for * escapes + private def interpolator(startingVariance: Int = 1)(using Context) = new TypeTraverser: variance = startingVariance override def traverse(t: Type) = diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index b8f08e09fa6d..266b22cfc7ef 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -325,7 +325,7 @@ private sealed trait YSettings: val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") - val Yrecheck: Setting[Boolean] = BooleanSetting("-Yrecheck", "Run type rechecks (test only)") + val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only)") val Ycc: Setting[Boolean] = BooleanSetting("-Ycc", "Check captured references") val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Debug info for captured references") val YccNoAbbrev: Setting[Boolean] = BooleanSetting("-Ycc-no-abbrev", "Used in conjunction with -Ycc, suppress type abbreviations") diff --git a/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala index 53a1548a4227..db9e28d7aad7 100644 --- a/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/PreRecheck.scala @@ -5,7 +5,7 @@ import core.Phases.Phase import core.DenotTransformers.DenotTransformer import core.Contexts.{Context, ctx} -/** A phase that precedes the rechecker and that allows installing +/** A base class for a phase that precedes a rechecker and that allows installing * new types for local symbols. */ abstract class PreRecheck extends Phase, DenotTransformer: diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index aa23e5a1cdd5..36044e6bcb91 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -20,10 +20,12 @@ import config.Printers.recheckr import util.Property import StdNames.nme import reporting.trace +import annotation.constructorOnly object Recheck: + import tpd.Tree - /** A flag used to indicate that a ParamAccessor has been temporily made not-private + /** A flag used to indicate that a ParamAccessor has been temporarily made not-private * Only used at the start of the Recheck phase, reset at its end. * The flag repurposes the Scala2ModuleVar flag. No confusion is possible since * Scala2ModuleVar cannot be also ParamAccessors. @@ -31,8 +33,6 @@ object Recheck: val ResetPrivate = Scala2ModuleVar val ResetPrivateParamAccessor = ResetPrivate | ParamAccessor - import tpd.Tree - /** Attachment key for rechecked types of TypeTrees */ val RecheckedType = Property.Key[Type] @@ -83,6 +83,10 @@ object Recheck: def hasRememberedType: Boolean = tree.hasAttachment(RecheckedType) +/** A base class that runs a simplified typer pass over an already re-typed program. The pass + * does not transform trees but returns instead the re-typed type of each tree as it is + * traversed. The Recheck phase must be directly preceded by a phase of type PreRecheck. + */ abstract class Recheck extends Phase, SymTransformer: thisPhase => @@ -91,7 +95,6 @@ abstract class Recheck extends Phase, SymTransformer: def preRecheckPhase = this.prev.asInstanceOf[PreRecheck] - override def isEnabled(using Context) = ctx.settings.Yrecheck.value override def changesBaseTypes: Boolean = true override def isCheckable = false @@ -109,13 +112,24 @@ abstract class Recheck extends Phase, SymTransformer: def newRechecker()(using Context): Rechecker - class Rechecker(ictx: Context): + /** The typechecker pass */ + class Rechecker(@constructorOnly ictx: Context): private val ta = ictx.typeAssigner - private val keepTypes = inContext(ictx) { + + /** If true, remember types of all tree nodes in attachments so that they + * can be retrieved with `knownType` + */ + private val keepAllTypes = inContext(ictx) { ictx.settings.Xprint.value.containsPhase(thisPhase) } - def constFold(tree: Tree, tp: Type)(using Context): Type = + /** Should type of `tree` be kept in an attachment so that it can be retrieved with + * `knownType`? By default true only is `keepAllTypes` hold, but can be overridden. + */ + def keepType(tree: Tree): Boolean = keepAllTypes + + /** Constant-folded rechecked type `tp` of tree `tree` */ + private def constFold(tree: Tree, tp: Type)(using Context): Type = val tree1 = tree.withType(tp) val tree2 = ConstFold(tree1) if tree2 ne tree1 then tree2.tpe else tp @@ -123,11 +137,11 @@ abstract class Recheck extends Phase, SymTransformer: def recheckIdent(tree: Ident)(using Context): Type = tree.tpe - /** Keep the symbol of the `select` but re-infer its type */ def recheckSelect(tree: Select)(using Context): Type = val Select(qual, name) = tree recheckSelection(tree, recheck(qual).widenIfUnstable, name) + /** Keep the symbol of the `select` but re-infer its type */ def recheckSelection(tree: Select, qualType: Type, name: Name)(using Context) = if name.is(OuterSelectName) then tree.tpe else @@ -169,7 +183,9 @@ abstract class Recheck extends Phase, SymTransformer: recheckStats(impl.body) sym.typeRef - // Need to remap Object to FromJavaObject since it got lost in ElimRepeated + /** Assuming `formals` are parameters of a Java-defined method, remap Object + * to FromJavaObject since it got lost in ElimRepeated + */ private def mapJavaArgs(formals: List[Type])(using Context): List[Type] = val tm = new TypeMap: def apply(t: Type) = t match @@ -177,8 +193,7 @@ abstract class Recheck extends Phase, SymTransformer: case _ => mapOver(t) formals.mapConserve(tm) - /** Hook for method type instantiation - */ + /** Hook for method type instantiation */ protected def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = mt.instantiate(argTypes) @@ -256,13 +271,21 @@ abstract class Recheck extends Phase, SymTransformer: recheck(tree.body, pt) def recheckReturn(tree: Return)(using Context): Type = - val rawType = recheck(tree.expr) + // Avoid local pattern defined symbols in returns from matchResult blocks + // that are inserted by the PatternMatcher transform. + // For regular symbols in the case branch, we already avoid them by the rule + // for blocks since a case branch is of the form `return[MatchResultN] { ... }` + // For source-level returns from methods, there's nothing to avoid, since the + // result type of a method with a return must be given explicitly. def avoidMap = new TypeOps.AvoidMap: def toAvoid(tp: NamedType) = - tp.symbol.is(Case) && tp.symbol.owner.isContainedIn(ctx.owner) + tp.symbol.is(Case) && tp.symbol.owner.isContainedIn(ctx.owner) + + val rawType = recheck(tree.expr) val ownType = avoidMap(rawType) checkConforms(ownType, tree.from.symbol.returnProto, tree) defn.NothingType + end recheckReturn def recheckWhileDo(tree: WhileDo)(using Context): Type = recheck(tree.cond, defn.BooleanType) @@ -285,7 +308,7 @@ abstract class Recheck extends Phase, SymTransformer: seqLitType(tree, TypeComparer.lub(declaredElemType :: elemTypes)) def recheckTypeTree(tree: TypeTree)(using Context): Type = - knownType(tree) + knownType(tree) // allows to install new types at Setup def recheckAnnotated(tree: Annotated)(using Context): Type = tree.tpe match @@ -326,10 +349,13 @@ abstract class Recheck extends Phase, SymTransformer: case tree: ValOrDefDef => if tree.isEmpty then NoType else - if sym.isUpdatedAfter(preRecheckPhase) then sym.ensureCompleted() - else recheckDef(tree, sym) + 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.termRef case tree: TypeDef => + // TODO: Should we allow for completers as for ValDefs or DefDefs? tree.rhs match case impl: Template => recheckClassDef(tree, impl, sym.asClass)(using ctx.localContext(tree, sym)) @@ -364,11 +390,15 @@ abstract class Recheck extends Phase, SymTransformer: case tree => recheckUnnamed(tree, pt) end recheckStart + /** Finish rechecking a tree node: check rechecked type against expected type + * and remember rechecked type in a tree attachment if required. + * @param tpe the recheched type of `tree` + * @param tree the rechecked tree + * @param pt the expected type + */ def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = checkConforms(tpe, pt, tree) - if keepTypes - || tree.isInstanceOf[Try] // type needs tp be checked for * escapes - then tree.rememberType(tpe) + if keepType(tree) then tree.rememberType(tpe) tpe def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = @@ -379,8 +409,12 @@ abstract class Recheck extends Phase, SymTransformer: throw ex } + /** If true, print info for some successful checkConforms operations (failing ones give + * an error message in any case). + */ private val debugSuccesses = false + /** Check that widened types of `tpe` and `pt` are compatible. */ def checkConforms(tpe: Type, pt: Type, tree: Tree)(using Context): Unit = tree match case _: DefTree | EmptyTree | _: TypeTree | _: Closure => // Don't report closure nodes, since their span is a point; wait instead @@ -408,6 +442,7 @@ abstract class Recheck extends Phase, SymTransformer: end Rechecker + /** Show tree with rechecked types instead of the types stored in the `.tpe` field */ override def show(tree: untpd.Tree)(using Context): String = val addRecheckedTypes = new TreeMap: override def transform(tree: Tree)(using Context): Tree = @@ -420,12 +455,14 @@ abstract class Recheck extends Phase, SymTransformer: } end Recheck +/** A class that can be used to test basic rechecking without any customaization */ object TestRecheck: - class Pre extends PreRecheck, IdentityDenotTransformer + class Pre extends PreRecheck, IdentityDenotTransformer: + override def isEnabled(using Context) = ctx.settings.YrecheckTest.value class TestRecheck extends Recheck: def phaseName: String = "recheck" - //override def isEnabled(using Context) = ctx.settings.YrefineTypes.value + override def isEnabled(using Context) = ctx.settings.YrecheckTest.value def newRechecker()(using Context): Rechecker = Rechecker(ctx) diff --git a/compiler/test/dotc/pos-test-recheck.excludes b/compiler/test/dotc/pos-test-recheck.excludes index e973b2cd529f..b8b75f50adc5 100644 --- a/compiler/test/dotc/pos-test-recheck.excludes +++ b/compiler/test/dotc/pos-test-recheck.excludes @@ -1,3 +1,20 @@ # Cannot compensate dealiasing due to false result dependency i6635a.scala i6682a.scala + +# unknown reason +patmatch-in-catch.scala +t0095.scala +i8344-1.scala +i8900-promote.scala +i6199c.scala +gadt-TailCalls.scala +i13349.scala +bynamefuns.scala +i15174.scala +gadt-cast-singleton.scala +6709.scala +11463.scala +i11247.scala +i6199b.scala +t3440.scala diff --git a/compiler/test/dotc/run-test-recheck.excludes b/compiler/test/dotc/run-test-recheck.excludes index e69de29bb2d1..de2fb3f8aed8 100644 --- a/compiler/test/dotc/run-test-recheck.excludes +++ b/compiler/test/dotc/run-test-recheck.excludes @@ -0,0 +1,13 @@ +# unknown reason +i10930.scala +t7584b.scala +opaque-inline +fragables-extension.scala +t7899.scala +t7181.scala +option-extract.scala +i5976.scala +tagless.scala +safeThrowsStrawman2.scala +t7584.scala +function-arity.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index cbb7e867d0bb..8d7a16dad8a4 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -241,9 +241,9 @@ class CompilationTests { given TestGroup = TestGroup("recheck") aggregateTests( compileFilesInDir("tests/new", recheckOptions), - //Following are disabled since they take too long for what they provide. + compileFilesInDir("tests/run", recheckOptions, FileFilter.exclude(TestSources.runTestRecheckExcluded)) + //Disabled to save some time. //compileFilesInDir("tests/pos", recheckOptions, FileFilter.exclude(TestSources.posTestRecheckExcluded)), - //compileFilesInDir("tests/run", recheckOptions, FileFilter.exclude(TestSources.runTestRecheckExcluded)) ).checkCompile() // Explicit nulls tests diff --git a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala index 1e9d0d4697ea..3ea364cc3a68 100644 --- a/compiler/test/dotty/tools/vulpix/TestConfiguration.scala +++ b/compiler/test/dotty/tools/vulpix/TestConfiguration.scala @@ -83,7 +83,7 @@ object TestConfiguration { ) val picklingWithCompilerOptions = picklingOptions.withClasspath(withCompilerClasspath).withRunClasspath(withCompilerClasspath) - val recheckOptions = defaultOptions.and("-Yrecheck") + val recheckOptions = defaultOptions.and("-Yrecheck-test") val scala2CompatMode = defaultOptions.and("-source", "3.0-migration") val explicitUTF8 = defaultOptions and ("-encoding", "UTF8") val explicitUTF16 = defaultOptions and ("-encoding", "UTF16") diff --git a/tests/pos/patmatch-in-catch.scala b/tests/pos/patmatch-in-catch.scala new file mode 100644 index 000000000000..c83a3eb78eb5 --- /dev/null +++ b/tests/pos/patmatch-in-catch.scala @@ -0,0 +1,15 @@ +sealed abstract class Action +case object MainReturn extends Action +case object ExceptionNormalExit extends Action +case class CaughtException(action: Action) extends RuntimeException + +def driver(action: Action): Action = + val result = + try action + catch case CaughtException(action) => + action match + case ExceptionNormalExit => + action + case _ => + ??? + result \ No newline at end of file From b3eeb232d74aaca762dc91235de2954b0e6dc7c1 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 15 Aug 2022 17:26:41 +0200 Subject: [PATCH 91/99] Don't box arguments of inferred applied types at Setup Rely on auto-boxing when accessing the arguments instead --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 2 +- tests/neg-custom-args/captures/lazylist.check | 2 +- tests/neg-custom-args/captures/lazylists2.check | 4 ++-- tests/neg-custom-args/captures/try.check | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index bdd9a8565853..71524fcdc5e7 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -209,7 +209,7 @@ extends tpd.TreeTraverser: else tp.derivedAppliedType(tycon1, args1 :+ res1) else - tp.derivedAppliedType(tycon1, args.mapConserve(arg => box(this(arg)))) + tp.derivedAppliedType(tycon1, args.mapConserve(arg => this(arg))) case tp @ RefinedType(core, rname, rinfo) if defn.isFunctionType(tp) => val rinfo1 = apply(rinfo) if rinfo1 ne rinfo then rinfo1.toFunctionType(isJava = false, alwaysDependent = true) diff --git a/tests/neg-custom-args/captures/lazylist.check b/tests/neg-custom-args/captures/lazylist.check index bde717689a32..e43538ad97f7 100644 --- a/tests/neg-custom-args/captures/lazylist.check +++ b/tests/neg-custom-args/captures/lazylist.check @@ -37,6 +37,6 @@ 17 | def tail = xs() // error: cannot have an inferred type | ^^^^^^^^^^^^^^^ | Non-local method tail cannot have an inferred result type - | {LazyCons.this.xs} lazylists.LazyList[box ? T] + | {LazyCons.this.xs} lazylists.LazyList[? T] | with non-empty capture set {LazyCons.this.xs}. | The type needs to be declared explicitly. diff --git a/tests/neg-custom-args/captures/lazylists2.check b/tests/neg-custom-args/captures/lazylists2.check index f431bca2d086..41881b57da24 100644 --- a/tests/neg-custom-args/captures/lazylists2.check +++ b/tests/neg-custom-args/captures/lazylists2.check @@ -8,7 +8,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:18:4 ------------------------------------ 18 | final class Mapped extends LazyList[B]: // error | ^ - | Found: {f, xs} LazyList[box ? B] + | Found: {f, xs} LazyList[? B] | Required: {f} LazyList[B] 19 | this: ({xs, f} Mapped) => 20 | def isEmpty = false @@ -20,7 +20,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazylists2.scala:27:4 ------------------------------------ 27 | final class Mapped extends LazyList[B]: // error | ^ - | Found: {f, xs} LazyList[box ? B] + | Found: {f, xs} LazyList[? B] | Required: {xs} LazyList[B] 28 | this: ({xs, f} Mapped) => 29 | def isEmpty = false diff --git a/tests/neg-custom-args/captures/try.check b/tests/neg-custom-args/captures/try.check index c1e70dfcb54d..30ebb910d34d 100644 --- a/tests/neg-custom-args/captures/try.check +++ b/tests/neg-custom-args/captures/try.check @@ -1,8 +1,8 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/try.scala:23:49 ------------------------------------------ 23 | val a = handle[Exception, CanThrow[Exception]] { // error | ^ - | Found: ? ({*} CT[Exception]) -> {*} CT[? >: box ? Exception <: box ? Exception] - | Required: CanThrow[Exception] => box {*} CT[Exception] + | Found: ? ({*} CT[Exception]) -> {*} CT[? >: ? Exception <: ? Exception] + | Required: CanThrow[Exception] => box {*} CT[Exception] 24 | (x: CanThrow[Exception]) => x 25 | }{ | From 84c1b24e8f1bc14fca990781f236b95b1b3393b2 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 15 Aug 2022 23:40:28 +0200 Subject: [PATCH 92/99] Polish Setup --- compiler/src/dotty/tools/dotc/cc/Setup.scala | 265 +++++++++++------- .../src/dotty/tools/dotc/cc/Synthetics.scala | 12 +- 2 files changed, 169 insertions(+), 108 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 71524fcdc5e7..a3e88699e424 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -12,6 +12,18 @@ import transform.Recheck.* import CaptureSet.IdentityCaptRefMap import Synthetics.isExcluded +/** A tree traverser that prepares a compilation unit to be capture checked. + * It does the following: + * - For every inferred type, drop any retains annotations, + * add capture sets to all its parts, add refinements to class types and function types. + * (c.f. mapInferred) + * - For explicit capturing types, expand throws aliases to the underlying (pure) function, + * and add some implied capture sets to curried functions (c.f. expandThrowsAlias, expandAbbreviations). + * - Add capture sets to self types of classes and objects, unless the self type was written explicitly. + * - Box the types of mutable variables and type arguments to methods (type arguments of types + * are boxed on access). + * - Link the external types of val and def symbols with the inferred types based on their parameter symbols. + */ class Setup( preRecheckPhase: DenotTransformer, thisPhase: DenotTransformer, @@ -19,6 +31,9 @@ class Setup( extends tpd.TreeTraverser: import tpd.* + /** Create dependent function with underlying function class `tycon` and given + * arguments `argTypes` and result `resType`. + */ private def depFun(tycon: Type, argTypes: List[Type], resType: Type)(using Context): Type = MethodType.companion( isContextual = defn.isContextFunctionClass(tycon.classSymbol), @@ -26,55 +41,40 @@ extends tpd.TreeTraverser: )(argTypes, resType) .toFunctionType(isJava = false, alwaysDependent = true) - private def box(tp: Type)(using Context): Type = tp.dealias match - case tp @ CapturingType(parent, refs) if !tp.isBoxed => - tp.boxed - case tp1 @ AppliedType(tycon, args) if defn.isNonRefinedFunction(tp1) => - val res = args.last - val boxedRes = box(res) - if boxedRes eq res then tp - else tp1.derivedAppliedType(tycon, args.init :+ boxedRes) - case tp1 @ RefinedType(_, _, rinfo) if defn.isFunctionType(tp1) => - val boxedRinfo = box(rinfo) - if boxedRinfo eq rinfo then tp - else boxedRinfo.toFunctionType(isJava = false, alwaysDependent = true) - case tp1: MethodOrPoly => - val res = tp1.resType - val boxedRes = box(res) - if boxedRes eq res then tp - else tp1.derivedLambdaType(resType = boxedRes) - case _ => tp - - /** Expand some aliases of function types to the underlying functions. - * Right now, these are only $throws aliases, but this could be generalized. + /** If `tp` is an unboxed capturing type or a function returning an unboxed capturing type, + * convert it to be boxed. */ - def expandThrowsAlias(tp: Type)(using Context) = tp match - case AppliedType(tycon, res :: exc :: Nil) if tycon.typeSymbol == defn.throwsAlias => - // hard-coded expansion since $throws aliases in stdlib are defined with `?=>` rather than `?->` - defn.FunctionOf(defn.CanThrowClass.typeRef.appliedTo(exc) :: Nil, res, isContextual = true, isErased = true) - case _ => tp - - private def expandThrowsAliases(using Context) = new TypeMap: - def apply(t: Type) = t match - case _: AppliedType => - val t1 = expandThrowsAlias(t) - if t1 ne t then apply(t1) else mapOver(t) - case _: LazyRef => - t - case t @ AnnotatedType(t1, ann) => - // Don't map capture sets, since that would implicitly normalize sets that - // are not well-formed. - t.derivedAnnotatedType(apply(t1), ann) - case _ => - mapOver(t) + private def box(tp: Type)(using Context): Type = + def recur(tp: Type): Type = tp.dealias match + case tp @ CapturingType(parent, refs) if !tp.isBoxed => + tp.boxed + case tp1 @ AppliedType(tycon, args) if defn.isNonRefinedFunction(tp1) => + val res = args.last + val boxedRes = recur(res) + if boxedRes eq res then tp + else tp1.derivedAppliedType(tycon, args.init :+ boxedRes) + case tp1 @ RefinedType(_, _, rinfo) if defn.isFunctionType(tp1) => + val boxedRinfo = recur(rinfo) + if boxedRinfo eq rinfo then tp + else boxedRinfo.toFunctionType(isJava = false, alwaysDependent = true) + case tp1: MethodOrPoly => + val res = tp1.resType + val boxedRes = recur(res) + if boxedRes eq res then tp + else tp1.derivedLambdaType(resType = boxedRes) + case _ => tp + tp match + case tp: MethodOrPoly => tp // don't box results of methods outside refinements + case _ => recur(tp) /** Perform the following transformation steps everywhere in a type: * 1. Drop retains annotations * 2. Turn plain function types into dependent function types, so that - * we can refer to their parameter in capture sets. Currently this is + * we can refer to their parameters in capture sets. Currently this is * only done at the toplevel, i.e. for function types that are not * themselves argument types of other function types. Without this restriction - * boxmap-paper.scala fails. Need to figure out why. + * pos.../lists.scala and pos/...curried-shorthands.scala fail. + * Need to figure out why. * 3. Refine other class types C by adding capture set variables to their parameter getters * (see addCaptureRefinements) * 4. Add capture set variables to all types that can be tracked @@ -116,7 +116,10 @@ extends tpd.TreeTraverser: !refs.isAlwaysEmpty case tp: (TypeRef | AppliedType) => val sym = tp.typeSymbol - if sym.isClass then tp.typeSymbol == defn.AnyClass + if sym.isClass then + tp.typeSymbol == defn.AnyClass + // we assume Any is a shorthand of {*} Any, so if Any is an upper + // bound, the type is taken to be impure. else superTypeIsImpure(tp.superType) case tp: (RefinedOrRecType | MatchType) => superTypeIsImpure(tp.underlying) @@ -145,13 +148,13 @@ extends tpd.TreeTraverser: case tp: OrType => canHaveInferredCapture(tp.tp1) || canHaveInferredCapture(tp.tp2) case CapturingType(_, refs) => - refs.isConst && !refs.isAlwaysEmpty && !refs.isUniversal + refs.isConst && !refs.isUniversal case _ => false }.showing(i"can have inferred capture $tp = $result", capt) /** Add a capture set variable to `tp` if necessary, or maybe pull out - * an embedded capture set variables from a part of `tp`. + * an embedded capture set variable from a part of `tp`. */ def addVar(tp: Type) = tp match case tp @ RefinedType(parent @ CapturingType(parent1, refs), rname, rinfo) => @@ -168,12 +171,12 @@ extends tpd.TreeTraverser: assert(refs1.asVar.elems.isEmpty) assert(refs2.asVar.elems.isEmpty) assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(AndType(parent1, parent2), refs1, tp1.isBoxed) + CapturingType(AndType(parent1, parent2), refs1 ** refs2, tp1.isBoxed) case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2 @ CapturingType(parent2, refs2)) => assert(refs1.asVar.elems.isEmpty) assert(refs2.asVar.elems.isEmpty) assert(tp1.isBoxed == tp2.isBoxed) - CapturingType(OrType(parent1, parent2, tp.isSoft), refs1, tp1.isBoxed) + CapturingType(OrType(parent1, parent2, tp.isSoft), refs1 ++ refs2, tp1.isBoxed) case tp @ OrType(tp1 @ CapturingType(parent1, refs1), tp2) => CapturingType(OrType(parent1, tp2, tp.isSoft), refs1, tp1.isBoxed) case tp @ OrType(tp1, tp2 @ CapturingType(parent2, refs2)) => @@ -186,9 +189,9 @@ extends tpd.TreeTraverser: case _ => tp - var isTopLevel = true + private var isTopLevel = true - def mapNested(ts: List[Type]): List[Type] = + private def mapNested(ts: List[Type]): List[Type] = val saved = isTopLevel isTopLevel = false try ts.mapConserve(this) finally isTopLevel = saved @@ -197,15 +200,21 @@ extends tpd.TreeTraverser: val tp = expandThrowsAlias(t) val tp1 = tp match case AnnotatedType(parent, annot) if annot.symbol == defn.RetainsAnnot => + // Drop explicit retains annotations apply(parent) case tp @ AppliedType(tycon, args) => val tycon1 = this(tycon) if defn.isNonRefinedFunction(tp) then - val args1 = mapNested(args.init) - val res1 = this(args.last) + // Convert toplevel generic function types to dependent functions + val args0 = args.init + var res0 = args.last + val args1 = mapNested(args0) + val res1 = this(res0) if isTopLevel then depFun(tycon1, args1, res1) .showing(i"add function refinement $tp --> $result", capt) + else if (tycon1 eq tycon) && (args1 eq args0) && (res1 eq res0) then + tp else tp.derivedAppliedType(tycon1, args1 :+ res1) else @@ -226,26 +235,63 @@ extends tpd.TreeTraverser: case _ => mapOver(tp) addVar(addCaptureRefinements(tp1)) + end apply end mapInferred - private def expandAbbreviations(using Context) = new TypeMap: + private def transformInferredType(tp: Type, boxed: Boolean)(using Context): Type = + val tp1 = mapInferred(tp) + if boxed then box(tp1) else tp1 - def propagateMethodResult(tp: Type, outerCs: CaptureSet, deep: Boolean): Type = tp match - case tp: MethodType => - if deep then - val tp1 = tp.derivedLambdaType(paramInfos = tp.paramInfos.mapConserve(this)) - propagateMethodResult(tp1, outerCs, deep = false) - else - val localCs = CaptureSet(tp.paramRefs.filter(_.isTracked)*) - tp.derivedLambdaType( - resType = propagateEnclosing(tp.resType, CaptureSet.empty, outerCs ++ localCs)) + /** Expand some aliases of function types to the underlying functions. + * Right now, these are only $throws aliases, but this could be generalized. + */ + private def expandThrowsAlias(tp: Type)(using Context) = tp match + case AppliedType(tycon, res :: exc :: Nil) if tycon.typeSymbol == defn.throwsAlias => + // hard-coded expansion since $throws aliases in stdlib are defined with `?=>` rather than `?->` + defn.FunctionOf(defn.CanThrowClass.typeRef.appliedTo(exc) :: Nil, res, isContextual = true, isErased = true) + case _ => tp + + private def expandThrowsAliases(using Context) = new TypeMap: + def apply(t: Type) = t match + case _: AppliedType => + val t1 = expandThrowsAlias(t) + if t1 ne t then apply(t1) else mapOver(t) + case _: LazyRef => + t + case t @ AnnotatedType(t1, ann) => + // Don't map capture sets, since that would implicitly normalize sets that + // are not well-formed. + t.derivedAnnotatedType(apply(t1), ann) + case _ => + mapOver(t) - def propagateDepFunctionResult(tp: Type, outerCs: CaptureSet, deep: Boolean): Type = tp match - case tp @ RefinedType(parent, nme.apply, rinfo: MethodType) => - val rinfo1 = propagateMethodResult(rinfo, outerCs, deep) + /** Fill in capture sets of curried function types from left to right, using + * a combination of the following two rules: + * + * 1. Expand `{c} (x: A) -> (y: B) -> C` + * to `{c} (x: A) -> {c} (y: B) -> C` + * 2. Expand `(x: A) -> (y: B) -> C` where `x` is tracked + * to `(x: A) -> {x} (y: B) -> C` + * + * TODO: Should we also propagate capture sets to the left? + */ + private def expandAbbreviations(using Context) = new TypeMap: + + /** Propagate `outerCs` as well as all tracked parameters as capture set to the result type + * of the dependent function type `tp`. + */ + def propagateDepFunctionResult(tp: Type, outerCs: CaptureSet): Type = tp match + case RefinedType(parent, nme.apply, rinfo: MethodType) => + val localCs = CaptureSet(rinfo.paramRefs.filter(_.isTracked)*) + val rinfo1 = rinfo.derivedLambdaType( + resType = propagateEnclosing(rinfo.resType, CaptureSet.empty, outerCs ++ localCs)) if rinfo1 ne rinfo then rinfo1.toFunctionType(isJava = false, alwaysDependent = true) else tp + /** If `tp` is a function type: + * - add `outerCs` as its capture set, + * - propagate `currentCs`, `outerCs`, and all tracked parameters of `tp` to the right. + */ def propagateEnclosing(tp: Type, currentCs: CaptureSet, outerCs: CaptureSet): Type = tp match case tp @ AppliedType(tycon, args) if defn.isFunctionClass(tycon.typeSymbol) => val tycon1 = this(tycon) @@ -253,19 +299,20 @@ extends tpd.TreeTraverser: val tp1 = if args1.exists(!_.captureSet.isAlwaysEmpty) then val propagated = propagateDepFunctionResult( - depFun(tycon, args1, args.last), currentCs ++ outerCs, deep = false) + depFun(tycon, args1, args.last), currentCs ++ outerCs) propagated match case RefinedType(_, _, mt: MethodType) => - val following = mt.resType.captureSet.elems - if mt.paramRefs.exists(following.contains(_)) then propagated - else tp.derivedAppliedType(tycon1, args1 :+ mt.resType) + if mt.isCaptureDependent then propagated + else + // No need to introduce dependent type, switch back to generic function type + tp.derivedAppliedType(tycon1, args1 :+ mt.resType) else val resType1 = propagateEnclosing( args.last, CaptureSet.empty, currentCs ++ outerCs) tp.derivedAppliedType(tycon1, args1 :+ resType1) tp1.capturing(outerCs) case tp @ RefinedType(parent, nme.apply, rinfo: MethodType) if defn.isFunctionType(tp) => - propagateDepFunctionResult(tp, currentCs ++ outerCs, deep = true) + propagateDepFunctionResult(mapOver(tp), currentCs ++ outerCs) .capturing(outerCs) case _ => mapOver(tp) @@ -277,18 +324,24 @@ extends tpd.TreeTraverser: propagateEnclosing(tp, CaptureSet.empty, CaptureSet.empty) end expandAbbreviations - private def transformInferredType(tp: Type, boxed: Boolean)(using Context): Type = - val tp1 = mapInferred(tp) - if boxed then box(tp1) else tp1 - private def transformExplicitType(tp: Type, boxed: Boolean)(using Context): Type = val tp1 = expandThrowsAliases(if boxed then box(tp) else tp) if tp1 ne tp then capt.println(i"expanded: $tp --> $tp1") if ctx.settings.YccNoAbbrev.value then tp1 else expandAbbreviations(tp1) - // Substitute parameter symbols in `from` to paramRefs in corresponding - // method or poly types `to`. We use a single BiTypeMap to do everything. + /** Transform type of type tree, and remember the transformed type as the type the tree */ + private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = + tree.rememberType( + if tree.isInstanceOf[InferredTypeTree] + then transformInferredType(tree.tpe, boxed) + else transformExplicitType(tree.tpe, boxed)) + + /** Substitute parameter symbols in `from` to paramRefs in corresponding + * method or poly types `to`. We use a single BiTypeMap to do everything. + * @param from a list of lists of type or term parameter symbols of a curried method + * @param to a list of method or poly types corresponding one-to-one to the parameter lists + */ private class SubstParams(from: List[List[Symbol]], to: List[LambdaType])(using Context) extends DeepTypeMap, BiTypeMap: @@ -317,12 +370,7 @@ extends tpd.TreeTraverser: mapOver(t) end SubstParams - private def transformTT(tree: TypeTree, boxed: Boolean)(using Context): Unit = - tree.rememberType( - if tree.isInstanceOf[InferredTypeTree] - then transformInferredType(tree.tpe, boxed) - else transformExplicitType(tree.tpe, boxed)) - + /** Update info of `sym` for CheckCaptures phase only */ private def updateInfo(sym: Symbol, info: Type)(using Context) = sym.updateInfoBetween(preRecheckPhase, thisPhase, info) @@ -331,24 +379,26 @@ extends tpd.TreeTraverser: case tree: DefDef if isExcluded(tree.symbol) => return case tree @ ValDef(_, tpt: TypeTree, _) if tree.symbol.is(Mutable) => - transformTT(tpt, boxed = true) + transformTT(tpt, boxed = true) // types of mutable variables are boxed traverse(tree.rhs) case tree @ TypeApply(fn, args) => traverse(fn) for case arg: TypeTree <- args do - transformTT(arg, boxed = true) + transformTT(arg, boxed = true) // type arguments in type applications are boxed case _ => traverseChildren(tree) tree match case tree: TypeTree => - transformTT(tree, boxed = false) + transformTT(tree, boxed = false) // other types are not boxed case tree: ValOrDefDef => val sym = tree.symbol - // replace an existing symbol info with inferred types + // replace an existing symbol info with inferred types where capture sets of + // TypeParamRefs and TermParamRefs put in correspondence by BiTypeMaps with the + // capture sets of the types of the method's parameter symbols and result type. def integrateRT( info: Type, // symbol info to replace - psymss: List[List[Symbol]], // the local (type and trem) parameter symbols corresponding to `info` + psymss: List[List[Symbol]], // the local (type and term) parameter symbols corresponding to `info` prevPsymss: List[List[Symbol]], // the local parameter symbols seen previously in reverse order prevLambdas: List[LambdaType] // the outer method and polytypes generated previously in reverse order ): Type = @@ -385,26 +435,29 @@ extends tpd.TreeTraverser: case tree: Bind => val sym = tree.symbol updateInfo(sym, transformInferredType(sym.info, boxed = false)) - case tree: TypeDef if tree.symbol.isClass => - val cls = tree.symbol.asClass - val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then - val localRefs = CaptureSet.Var() - val newInfo = ClassInfo(prefix, cls, ps, decls, - CapturingType(cinfo.selfType, localRefs) - .showing(i"inferred self type for $cls: $result", capt)) - updateInfo(cls, newInfo) - cls.thisType.asInstanceOf[ThisType].invalidateCaches() - if cls.is(ModuleClass) then - val modul = cls.sourceModule - updateInfo(modul, CapturingType(modul.info, localRefs)) - modul.termRef.invalidateCaches() case tree: TypeDef => - val info = atPhase(preRecheckPhase)(tree.symbol.info) - val newInfo = transformExplicitType(info, boxed = false) - if newInfo ne info then - updateInfo(tree.symbol, newInfo) - capt.println(i"update info of ${tree.symbol} from $info to $newInfo") - + tree.symbol match + case cls: ClassSymbol => + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo + if (selfInfo eq NoType) || cls.is(ModuleClass) && !cls.isStatic then + // add capture set to self type of nested classes if no self type is given explicitly + val localRefs = CaptureSet.Var() + val newInfo = ClassInfo(prefix, cls, ps, decls, + CapturingType(cinfo.selfType, localRefs) + .showing(i"inferred self type for $cls: $result", capt)) + updateInfo(cls, newInfo) + cls.thisType.asInstanceOf[ThisType].invalidateCaches() + if cls.is(ModuleClass) then + // if it's a module, the capture set of the module reference is the capture set of the self type + val modul = cls.sourceModule + updateInfo(modul, CapturingType(modul.info, localRefs)) + modul.termRef.invalidateCaches() + case _ => + val info = atPhase(preRecheckPhase)(tree.symbol.info) + val newInfo = transformExplicitType(info, boxed = false) + if newInfo ne info then + updateInfo(tree.symbol, newInfo) + capt.println(i"update info of ${tree.symbol} from $info to $newInfo") case _ => + end traverse end Setup diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index 12e4fdfba0da..baae4b75cf8c 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -27,14 +27,22 @@ object Synthetics: case DefaultGetterName(nme.copy, _) => sym.is(Synthetic) case _ => false - /** Is `sym` a synthetic apply, copy, or copy default getter method? */ + /** Is `sym` a synthetic apply, copy, or copy default getter method? + * The types of these symbols are transformed in a special way without + * looking at the definitions's RHS + */ def needsTransform(sym: SymDenotation)(using Context): Boolean = isSyntheticCopyMethod(sym) || isSyntheticApplyMethod(sym) || isSyntheticUnapplyMethod(sym) || isSyntheticCopyDefaultGetterMethod(sym) - /** Method is excluded from regular capture checking */ + /** Method is excluded from regular capture checking. + * Excluded are synthetic class members + * - that override a synthesized case class symbol, or + * - the fromProduct method, or + * - members transformed specially as indicated by `needsTransform`. + */ def isExcluded(sym: Symbol)(using Context): Boolean = sym.is(Synthetic) && sym.owner.isClass From a0cbb87915aa8b6eca21872cd3e28627684314c1 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 16 Aug 2022 16:47:32 +0200 Subject: [PATCH 93/99] Treat bounds of type variables as boxed --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 4 ++++ .../src/dotty/tools/dotc/core/TypeComparer.scala | 12 ++++++------ tests/neg-custom-args/captures/eta.check | 14 ++++++++++++++ tests/neg-custom-args/captures/eta.scala | 2 +- .../neg-custom-args/captures/override-boxed.scala | 7 +++++++ 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 tests/neg-custom-args/captures/eta.check create mode 100644 tests/neg-custom-args/captures/override-boxed.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index ee52d9b0eb6e..e4b02e1efed1 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -67,6 +67,10 @@ extension (tp: Type) case _ => tp + /** If `sym` is a type parameter, the boxed version of `tp`, otherwise `tp` */ + def boxedIfTypeParam(sym: Symbol)(using Context) = + if sym.is(TypeParam) then tp.boxed else tp + /** The boxed version of `tp`, unless `tycon` is a function symbol */ def boxedUnlessFun(tycon: Type)(using Context) = if ctx.phase != Phases.checkCapturesPhase || defn.isFunctionSymbol(tycon.typeSymbol) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index f05e12ccea13..b5fe41095516 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -23,7 +23,7 @@ import typer.ProtoTypes.constrained import typer.Applications.productSelectorTypes import reporting.trace import annotation.constructorOnly -import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxedUnlessFun} +import cc.{CapturingType, derivedCapturingType, CaptureSet, stripCapturing, isBoxedCapturing, boxed, boxedUnlessFun, boxedIfTypeParam} /** Provides methods to compare types. */ @@ -426,7 +426,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp2: TypeParamRef => constraint.isLess(tp1, tp2) case _ => false } || - isSubTypeWhenFrozen(bounds(tp1).hi, tp2) || { + isSubTypeWhenFrozen(bounds(tp1).hi.boxed, tp2) || { if (canConstrain(tp1) && !approx.high) addConstraint(tp1, tp2, fromBelow = false) && flagNothingBound else thirdTry @@ -547,7 +547,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling || narrowGADTBounds(tp2, tp1, approx, isUpper = false)) && (isBottom(tp1) || GADTusage(tp2.symbol)) - isSubApproxHi(tp1, info2.lo) && (trustBounds || isSubApproxHi(tp1, info2.hi)) + isSubApproxHi(tp1, info2.lo.boxedIfTypeParam(tp2.symbol)) && (trustBounds || isSubApproxHi(tp1, info2.hi)) || compareGADT || tryLiftedToThis2 || fourthTry @@ -586,7 +586,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling // So if the constraint is not yet frozen, we do the same comparison again // with a frozen constraint, which means that we get a chance to do the // widening in `fourthTry` before adding to the constraint. - if (frozenConstraint) recur(tp1, bounds(tp2).lo) + if (frozenConstraint) recur(tp1, bounds(tp2).lo.boxed) else isSubTypeWhenFrozen(tp1, tp2) alwaysTrue || { if (canConstrain(tp2) && !approx.low) @@ -832,7 +832,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling } def tryBaseType(cls2: Symbol) = { - val base = nonExprBaseType(tp1, cls2) + val base = nonExprBaseType(tp1, cls2).boxedIfTypeParam(tp1.typeSymbol) if base.exists && (base ne tp1) && (!caseLambda.exists || canWidenAbstract || tp1.widen.underlyingClassRef(refinementOK = true).exists) then @@ -856,7 +856,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling && (tp2.isAny || GADTusage(tp1.symbol)) (!caseLambda.exists || canWidenAbstract) - && isSubType(hi1, tp2, approx.addLow) && (trustBounds || isSubType(lo1, tp2, approx.addLow)) + && isSubType(hi1.boxedIfTypeParam(tp1.symbol), tp2, approx.addLow) && (trustBounds || isSubType(lo1, tp2, approx.addLow)) || compareGADT || tryLiftedToThis1 case _ => diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check new file mode 100644 index 000000000000..da3609a86771 --- /dev/null +++ b/tests/neg-custom-args/captures/eta.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/eta.scala:4:9 -------------------------------------------- +4 | g // error + | ^ + | Found: (g : () -> A) + | Required: () -> {f} Proc + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/eta.scala:6:14 ------------------------------------------- +6 | bar( () => f ) // error + | ^^^^^^^ + | Found: {f} () -> box {f} () -> Unit + | Required: () -> box ? () -> Unit + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/eta.scala b/tests/neg-custom-args/captures/eta.scala index 12cb858a14d8..3d9d759d2203 100644 --- a/tests/neg-custom-args/captures/eta.scala +++ b/tests/neg-custom-args/captures/eta.scala @@ -1,7 +1,7 @@ type Proc = (() -> Unit) def foo(f: {*} Proc): {} Proc = def bar[A <: {f} Proc](g: () -> A): () -> {f} Proc = - g + g // error val stowaway: () -> {f} Proc = bar( () => f ) // error () => { stowaway.apply().apply() } \ No newline at end of file diff --git a/tests/neg-custom-args/captures/override-boxed.scala b/tests/neg-custom-args/captures/override-boxed.scala new file mode 100644 index 000000000000..720b50732f61 --- /dev/null +++ b/tests/neg-custom-args/captures/override-boxed.scala @@ -0,0 +1,7 @@ +class A + +def test(x: {*} Any) = + abstract class Getter: + def get(): {x} A + class PolyGetter[T <: {x} A] extends Getter: + override def get(): T = ??? // error From acf8286feb79bf105e897b86050db0bbdd45ad74 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 16 Aug 2022 20:32:51 +0200 Subject: [PATCH 94/99] Reorganize the way capture sets are included in the current environment stack --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 130 +++++++++++------- 1 file changed, 80 insertions(+), 50 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6864b4f5fe45..6d84d651dee8 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -39,10 +39,25 @@ object CheckCaptures: sym end Pre + /** A class describing environments. + * @param owner the current owner + * @param captured the caputure set containing all references to tracked free variables outside of boxes + * @param isBoxed true if the environment is inside a box (in which case references are not counted) + * @param outer0 the next enclosing environment or null + */ case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer0: Env | Null): def outer = outer0.nn + + def isOutermost = outer0 == null + + /** If an environment is open it tracks free references */ def isOpen = !captured.isAlwaysEmpty && !isBoxed + end Env + /** Similar normal substParams, but this is an approximating type map that + * maps parameters in contravariant capture sets to the empty set. + * TODO: check what happens with non-variant. + */ final class SubstParamsMap(from: BindingType, to: List[Type])(using Context) extends ApproximatingTypeMap, IdempotentCaptRefMap: def apply(tp: Type): Type = tp match @@ -56,7 +71,7 @@ object CheckCaptures: case _ => mapOver(tp) - /** Check that a @retains annotation only mentions references that can be tracked + /** Check that a @retains annotation only mentions references that can be tracked. * This check is performed at Typer. */ def checkWellformed(ann: Tree)(using Context): Unit = @@ -149,75 +164,90 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => traverseChildren(t) + /** If `tpt` is an inferred type, interpolate capture set variables appearing contra- + * variantly in it. + */ private def interpolateVarsIn(tpt: Tree)(using Context): Unit = if tpt.isInstanceOf[InferredTypeTree] then interpolator().traverse(tpt.knownType) .showing(i"solved vars in ${tpt.knownType}", capt) + /** Assert subcapturing `cs1 <: cs2` */ + def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = + assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") + + /** Check subcapturing `{elem} <: cs`, report error on failure */ + def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos)(using Context) = + val res = elem.singletonCaptureSet.subCaptures(cs, frozen = false) + if !res.isOK then + report.error(i"$elem cannot be referenced here; it is not included in the allowed capture set ${res.blocking}", pos) + + /** Check subcapturing `cs1 <: cs2`, report error on failure */ + def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos)(using Context) = + val res = cs1.subCaptures(cs2, frozen = false) + if !res.isOK then + def header = + if cs1.elems.size == 1 then i"reference ${cs1.elems.toList}%, % is not" + else i"references $cs1 are not all" + report.error(i"$header included in allowed capture set ${res.blocking}", pos) + + /** The current environment */ private var curEnv: Env = Env(NoSymbol, CaptureSet.empty, isBoxed = false, null) private val myCapturedVars: util.EqHashMap[Symbol, CaptureSet] = EqHashMap() + + /** If `sym` is a class or method nested inside a term, a capture set variable representing + * the captured variables of the environment associated with `sym`. + */ def capturedVars(sym: Symbol)(using Context) = myCapturedVars.getOrElseUpdate(sym, if sym.ownersIterator.exists(_.isTerm) then CaptureSet.Var() else CaptureSet.empty) + /** For all nested environments up to `limit` perform `op` */ + def forallOuterEnvsUpTo(limit: Symbol)(op: Env => Unit)(using Context): Unit = + def recur(env: Env): Unit = + if env.isOpen && env.owner != limit then + op(env) + if !env.isOutermost then + var nextEnv = env.outer + if env.owner.isConstructor then + if nextEnv.owner != limit && !nextEnv.isOutermost then + recur(nextEnv.outer) + else recur(nextEnv) + recur(curEnv) + + /** Include `sym` in the capture sets of all enclosing environments nested in the + * the environment in which `sym` is defined. + */ def markFree(sym: Symbol, pos: SrcPos)(using Context): Unit = if sym.exists then val ref = sym.termRef - def recur(env: Env): Unit = - if env.isOpen && env.owner != sym.enclosure then + if ref.isTracked then + forallOuterEnvsUpTo(sym.enclosure) { env => capt.println(i"Mark $sym with cs ${ref.captureSet} free in ${env.owner}") checkElem(ref, env.captured, pos) - if env.owner.isConstructor then - if env.outer.owner != sym.enclosure then recur(env.outer.outer) - else recur(env.outer) - if ref.isTracked then recur(curEnv) + } - def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = - if curEnv.isOpen then - val ownEnclosure = ctx.owner.enclosingMethodOrClass - var targetSet = capturedVars(sym) - if !targetSet.isAlwaysEmpty && sym.enclosure == ownEnclosure then - targetSet = targetSet.filter { - case ref: TermRef => ref.symbol.enclosure != ownEnclosure - case _ => true + /** Make sure (projected) `cs` is a subset of the capture sets of all enclosing + * environments. At each stage, only include references from `cs` that are outside + * the environment's owner + */ + def markFree(cs: CaptureSet, pos: SrcPos)(using Context): Unit = + if !cs.isAlwaysEmpty then + forallOuterEnvsUpTo(ctx.owner.topLevelClass) { env => + val included = cs.filter { + case ref: TermRef => env.owner.isProperlyContainedIn(ref.symbol.owner) + case ref: ThisType => env.owner.isProperlyContainedIn(ref.cls) + case _ => false } - def includeIn(env: Env) = - capt.println(i"Include call capture $targetSet in ${env.owner}") - checkSubset(targetSet, env.captured, pos) - includeIn(curEnv) - if curEnv.owner.isTerm && curEnv.outer.owner.isClass then - includeIn(curEnv.outer) - - def includeBoxedCaptures(tp: Type, pos: SrcPos)(using Context): Unit = - includeBoxedCaptures(tp.boxedCaptureSet, pos) - - def includeBoxedCaptures(refs: CaptureSet, pos: SrcPos)(using Context): Unit = - if curEnv.isOpen then - val ownEnclosure = ctx.owner.enclosingMethodOrClass - val targetSet = refs.filter { - case ref: TermRef => ref.symbol.enclosure != ownEnclosure - case ref: ThisType => true - case _ => false + capt.println(i"Include call capture $included in ${env.owner}") + checkSubset(included, env.captured, pos) } - checkSubset(targetSet, curEnv.captured, pos) - - def assertSub(cs1: CaptureSet, cs2: CaptureSet)(using Context) = - assert(cs1.subCaptures(cs2, frozen = false).isOK, i"$cs1 is not a subset of $cs2") - def checkElem(elem: CaptureRef, cs: CaptureSet, pos: SrcPos)(using Context) = - val res = elem.singletonCaptureSet.subCaptures(cs, frozen = false) - if !res.isOK then - report.error(i"$elem cannot be referenced here; it is not included in the allowed capture set ${res.blocking}", pos) - - def checkSubset(cs1: CaptureSet, cs2: CaptureSet, pos: SrcPos)(using Context) = - val res = cs1.subCaptures(cs2, frozen = false) - if !res.isOK then - def header = - if cs1.elems.size == 1 then i"reference ${cs1.elems.toList}%, % is not" - else i"references $cs1 are not all" - report.error(i"$header included in allowed capture set ${res.blocking}", pos) + /** 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) /** A specialized implementation of the selection rule. * @@ -434,7 +464,7 @@ class CheckCaptures extends Recheck, SymTransformer: finally curEnv = saved else super.recheck(tree, pt) if tree.isTerm then - includeBoxedCaptures(res, tree.srcPos) + markFree(res.boxedCaptureSet, tree.srcPos) res override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = @@ -526,7 +556,7 @@ class CheckCaptures extends Recheck, SymTransformer: capt.println(i"ABORTING $actual vs $expected") actual else - if covariant == actual.isBoxed then includeBoxedCaptures(refs, pos) + if covariant == actual.isBoxed then markFree(refs, pos) CapturingType(parent1, refs, boxed = !actual.isBoxed) else if parent1 eq parent then actual else CapturingType(parent1, refs, boxed = actual.isBoxed) From e15d31c7f799097d426fa8db36ca4e64ec040834 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 17 Aug 2022 14:21:56 +0200 Subject: [PATCH 95/99] Refactor and comment CheckCaptures --- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 6 + .../dotty/tools/dotc/cc/CheckCaptures.scala | 371 +++++++++++------- .../captures/usingLogFile.check | 28 +- 3 files changed, 242 insertions(+), 163 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index e4b02e1efed1..ec9f71a1dc72 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -135,6 +135,12 @@ extension (sym: Symbol) sym == defn.Compiletime_erasedValue || defn.isFunctionClass(sym.maybeOwner) + /** When applying `sym`, would the result type be unboxed? + * This is the case if the result type contains a top-level reference to an enclosing + * class or method type parameter and the method does not allow root capture. + * If the type parameter is instantiated to a boxed type, that type would + * have to be unboxed in the method's result. + */ def unboxesResult(using Context): Boolean = def containsEnclTypeParam(tp: Type): Boolean = tp.strippedDealias match case tp @ TypeRef(pre: ThisType, _) => tp.symbol.is(Param) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 6d84d651dee8..fe22f9f49e13 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -20,6 +20,7 @@ import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap} import StdNames.nme import reporting.trace +/** The capture checker */ object CheckCaptures: import ast.tpd.* @@ -28,7 +29,8 @@ object CheckCaptures: override def isEnabled(using Context) = ctx.settings.Ycc.value /** Reset `private` flags of parameter accessors so that we can refine them - * in Setup if they have non-empty capture sets + * in Setup if they have non-empty capture sets. Special handling of some + * symbols defined for case classes. */ def transformSym(sym: SymDenotation)(using Context): SymDenotation = if sym.isAllOf(PrivateParamAccessor) && !sym.hasAnnotation(defn.ConstructorOnlyAnnot) then @@ -43,7 +45,7 @@ object CheckCaptures: * @param owner the current owner * @param captured the caputure set containing all references to tracked free variables outside of boxes * @param isBoxed true if the environment is inside a box (in which case references are not counted) - * @param outer0 the next enclosing environment or null + * @param outer0 the next enclosing environment */ case class Env(owner: Symbol, captured: CaptureSet, isBoxed: Boolean, outer0: Env | Null): def outer = outer0.nn @@ -81,10 +83,10 @@ object CheckCaptures: if !ref.canBeTracked then report.error(em"$elem cannot be tracked since it is not a parameter or a local variable", elem.srcPos) case tpe => - report.error(em"$tpe is not a legal type for a capture set", elem.srcPos) + report.error(em"$elem: $tpe is not a legal element of a capture set", elem.srcPos) /** If `tp` is a capturing type, check that all references it mentions have non-empty - * capture sets. + * capture sets. Also: warn about redundant capture annotations. * This check is performed after capture sets are computed in phase cc. */ def checkWellformedPost(tp: Type, pos: SrcPos)(using Context): Unit = tp match @@ -96,10 +98,16 @@ object CheckCaptures: report.warning(em"redundant capture: $parent already accounts for $ref", pos) case _ => - def checkWellformedPost(ann: Tree)(using Context): Unit = - /** The lists `elems(i) :: prev.reerse :: elems(0),...,elems(i-1),elems(i+1),elems(n)` - * where `n == elems.length-1`, i <- 0..n`. - */ + /** Warn if `ann`, which is a tree of a @retains annotation, defines some elements that + * are already accounted for by other elements of the same annotation. + * Note: We need to perform the check on the original annotation rather than its + * capture set since the conversion to a capture set already eliminates redundant elements. + */ + def warnIfRedundantCaptureSet(ann: Tree)(using Context): Unit = + // The lists `elems(i) :: prev.reverse :: elems(0),...,elems(i-1),elems(i+1),elems(n)` + // where `n == elems.length-1`, i <- 0..n`. + // I.e. + // choices(Nil, elems) = [[elems(i), elems(0), ..., elems(i-1), elems(i+1), .... elems(n)] | i <- 0..n] def choices(prev: List[Tree], elems: List[Tree]): List[List[Tree]] = elems match case Nil => Nil case elem :: elems => @@ -129,14 +137,15 @@ class CheckCaptures extends Recheck, SymTransformer: if Synthetics.needsTransform(sym) then Synthetics.transformFromCC(sym) else super.transformSym(sym) + /** Check overrides again, taking capture sets into account. + * TODO: Can we avoid doing overrides checks twice? + * We need to do them here since only at this phase CaptureTypes are relevant + * But maybe we can then elide the check during the RefChecks phase if -Ycc is set? + */ def checkOverrides = new TreeTraverser: def traverse(t: Tree)(using Context) = t match - case t: Template => - // ^^^ TODO: Can we avoid doing overrides checks twice? - // We need to do them here since only at this phase CaptureTypes are relevant - // But maybe we can then elide the check during the RefChecks phase if -Ycc is set? - checkAllOverrides(ctx.owner.asClass) + case t: Template => checkAllOverrides(ctx.owner.asClass) case _ => traverseChildren(t) @@ -147,6 +156,9 @@ class CheckCaptures extends Recheck, SymTransformer: super.keepType(tree) || tree.isInstanceOf[Try] // type of `try` needs tp be checked for * escapes + /** Instantiate capture set variables appearing contra-variantly to their + * upper approximation. + */ private def interpolator(startingVariance: Int = 1)(using Context) = new TypeTraverser: variance = startingVariance override def traverse(t: Type) = @@ -249,9 +261,14 @@ class CheckCaptures extends Recheck, SymTransformer: def includeCallCaptures(sym: Symbol, pos: SrcPos)(using Context): Unit = if sym.exists && curEnv.isOpen then markFree(capturedVars(sym), pos) + override def recheckIdent(tree: Ident)(using Context): Type = + if tree.symbol.is(Method) then includeCallCaptures(tree.symbol, tree.srcPos) + else markFree(tree.symbol, tree.srcPos) + super.recheckIdent(tree) + /** A specialized implementation of the selection rule. * - * E |- f: Cf { m: Cr R } + * E |- f: Cf f { m: Cr R } * ------------------------ * E |- f.m: C R * @@ -273,12 +290,104 @@ class CheckCaptures extends Recheck, SymTransformer: selType }//.showing(i"recheck sel $tree, $qualType = $result") + /** A specialized implementation of the apply rule. + * + * E |- f: Cf (Ra -> Cr Rr) + * E |- a: Ca Ra + * ------------------------ + * E |- f a: C Rr + * + * 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 = + includeCallCaptures(tree.symbol, tree.srcPos) + 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 = ${qual.tpe.captureSet} --> $result", capt) + case _ => appType + case appType => appType + + /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. + * This means: + * - Instantiate result type with actual arguments + * - If call is to a constructor: + * - remember types of arguments corresponding to tracked + * parameters in refinements. + * - add capture set of instantiated class to capture set of result type. + */ + override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = + val ownType = + if mt.isResultDependent then SubstParamsMap(mt, argTypes)(mt.resType) + else mt.resType + + if sym.isConstructor then + val cls = sym.owner.asClass + + /** First half of result pair: + * Refine the type of a constructor call `new C(t_1, ..., t_n)` + * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked + * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. + * + * Second half: union of all capture sets of arguments to tracked parameters. + */ + def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = + mt.paramNames.lazyZip(argTypes).foldLeft((core, initCs)) { (acc, refine) => + val (core, allCaptures) = acc + val (getterName, argType) = refine + val getter = cls.info.member(getterName).suchThat(_.is(ParamAccessor)).symbol + if getter.termRef.isTracked && !getter.is(Private) + then (RefinedType(core, getterName, argType), allCaptures ++ argType.captureSet) + else (core, allCaptures) + } + + def augmentConstructorType(core: Type, initCs: CaptureSet): Type = core match + case core: MethodType => + // more parameters to follow; augment result type + core.derivedLambdaType(resType = augmentConstructorType(core.resType, initCs)) + case CapturingType(parent, refs) => + // can happen for curried constructors if instantiate of a previous step + // added capture set to result. + augmentConstructorType(parent, initCs ++ refs) + case _ => + val (refined, cs) = addParamArgRefinements(core, initCs) + refined.capturing(cs) + + augmentConstructorType(ownType, CaptureSet.empty) match + case augmented: MethodType => + augmented + case augmented => + // add capture sets of class and constructor to final result of constructor call + augmented.capturing(capturedVars(cls) ++ capturedVars(sym)) + .showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt) + else ownType + end instantiate + override def recheckClosure(tree: Closure, pt: Type)(using Context): Type = val cs = capturedVars(tree.meth.symbol) capt.println(i"typing closure $tree with cvs $cs") super.recheckClosure(tree, pt).capturing(cs) .showing(i"rechecked $tree / $pt = $result", capt) + /** Additionally to normal processing, update types of closures if the expected type + * is a function with only pure parameters. In that case, make the anonymous function + * also have the same parameters as the prototype. + * TODO: Develop a clearer rationale for this. + * TODO: Can we generalize this to arbitrary parameters? + * Currently some tests fail if we do this. (e.g. neg.../stackAlloc.scala, others) + */ override def recheckBlock(block: Block, pt: Type)(using Context): Type = block match case closureDef(mdef) => @@ -286,7 +395,7 @@ class CheckCaptures extends Recheck, SymTransformer: case defn.FunctionOf(ptformals, _, _, _) if ptformals.forall(_.captureSet.isAlwaysEmpty) => // Redo setup of the anonymous function so that formal parameters don't // get capture sets. This is important to avoid false widenings to `*` - // when taking the base type of the actual clsoures's dependent function + // when taking the base type of the actual closures's dependent function // type so that it conforms to the expected non-dependent function type. // See withLogFile.scala for a test case. val meth = mdef.symbol @@ -314,17 +423,13 @@ class CheckCaptures extends Recheck, SymTransformer: case _ => super.recheckBlock(block, pt) - override def recheckIdent(tree: Ident)(using Context): Type = - markFree(tree.symbol, tree.srcPos) - if tree.symbol.is(Method) then includeCallCaptures(tree.symbol, tree.srcPos) - super.recheckIdent(tree) - override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Unit = try - if !sym.is(Module) then super.recheckValDef(tree, sym) + if !sym.is(Module) then // Modules are checked by checking the module class + super.recheckValDef(tree, sym) finally if !sym.is(Param) then - // parameters with inferred types belong to anonymous methods. We need to wait + // Parameters with inferred types belong to anonymous methods. We need to wait // for more info from the context, so we cannot interpolate. Note that we cannot // expect to have all necessary info available at the point where the anonymous // function is compiled since we do not propagate expected types into blocks. @@ -340,69 +445,42 @@ class CheckCaptures extends Recheck, SymTransformer: interpolateVarsIn(tree.tpt) curEnv = saved + /** Class-specific capture set relations: + * 1. The capture set of a class includes the capture sets of its parents. + * 2. The capture set of the self type of a class includes the capture set of the class. + * 3. The capture set of the self type of a class includes the capture set of every class parameter. + */ override def recheckClassDef(tree: TypeDef, impl: Template, cls: ClassSymbol)(using Context): Type = val saved = curEnv val localSet = capturedVars(cls) - for parent <- impl.parents do + for parent <- impl.parents do // (1) checkSubset(capturedVars(parent.tpe.classSymbol), localSet, parent.srcPos) if !localSet.isAlwaysEmpty then curEnv = Env(cls, localSet, isBoxed = false, curEnv) try val thisSet = cls.classInfo.selfType.captureSet.withDescription(i"of the self type of $cls") - checkSubset(localSet, thisSet, tree.srcPos) + checkSubset(localSet, thisSet, tree.srcPos) // (2) for param <- cls.paramGetters do - checkSubset(param.termRef.captureSet, thisSet, param.srcPos) + checkSubset(param.termRef.captureSet, thisSet, param.srcPos) // (3) super.recheckClassDef(tree, impl, cls) finally curEnv = saved - /** Handle an application of method `sym` with type `mt` to arguments of types `argTypes`. - * This means: - * - Instantiate result type with actual arguments - * - If call is to a constructor: - * - remember types of arguments corresponding to tracked - * parameters in refinements. - * - add capture set of instantiated class to capture set of result type. + /** If type is of the form `T @requiresCapability(x)`, + * mark `x` as free in the current environment. This is used to require the + * correct `CanThrow` capability when encountering a `throw`. */ - override def instantiate(mt: MethodType, argTypes: List[Type], sym: Symbol)(using Context): Type = - val ownType = - if mt.isResultDependent then SubstParamsMap(mt, argTypes)(mt.resType) - else mt.resType - - if sym.isConstructor then - val cls = sym.owner.asClass - - /** First half: Refine the type of a constructor call `new C(t_1, ..., t_n)` - * to C{val x_1: T_1, ..., x_m: T_m} where x_1, ..., x_m are the tracked - * parameters of C and T_1, ..., T_m are the types of the corresponding arguments. - * - * Second half: union of all capture sets of arguments to tracked parameters. - */ - def addParamArgRefinements(core: Type, initCs: CaptureSet): (Type, CaptureSet) = - mt.paramNames.lazyZip(argTypes).foldLeft((core, initCs)) { (acc, refine) => - val (core, allCaptures) = acc - val (getterName, argType) = refine - val getter = cls.info.member(getterName).suchThat(_.is(ParamAccessor)).symbol - if getter.termRef.isTracked && !getter.is(Private) - then (RefinedType(core, getterName, argType), allCaptures ++ argType.captureSet) - else (core, allCaptures) - } - - def augmentConstructorType(core: Type, initCs: CaptureSet): Type = core match - case core: MethodType => - core.derivedLambdaType(resType = augmentConstructorType(core.resType, initCs)) - case CapturingType(parent, refs) => - augmentConstructorType(parent, initCs ++ refs) - case _ => - val (refined, cs) = addParamArgRefinements(core, initCs) - refined.capturing(cs) - - val augmented = augmentConstructorType(ownType, CaptureSet.empty) - { if augmented.isInstanceOf[MethodType] then augmented - else augmented.capturing(capturedVars(cls) ++ capturedVars(sym)) - }.showing(i"constr type $mt with $argTypes%, % in $cls = $result", capt) - else ownType - end instantiate + override def recheckTyped(tree: Typed)(using Context): Type = + tree.tpt.tpe match + case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => + annot.tree match + case Apply(_, cap :: Nil) => + markFree(cap.symbol, tree.srcPos) + case _ => + case _ => + super.recheckTyped(tree) + /* Currently not needed, since capture checking takes place after ElimByName. + * Keep around in case we need to get back to it def recheckByNameArg(tree: Tree, pt: Type)(using Context): Type = val closureDef(mdef) = tree: @unchecked val arg = mdef.rhs @@ -415,65 +493,37 @@ class CheckCaptures extends Recheck, SymTransformer: } finally curEnv = curEnv.outer recheckFinish(result, arg, pt) + */ - /** A specialized implementation of the apply rule. - * - * E |- f: Cf (Ra -> Cr Rr) - * E |- a: Ra - * ------------------------ - * E |- f a: C Rr - * - * 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 expected type `pt` is boxed, don't propagate free variables. + * Otherwise, if the result type is boxed, simulate an unboxing by + * adding all references in the boxed capture set to the current environment. */ - override def recheckApply(tree: Apply, pt: Type)(using Context): Type = - includeCallCaptures(tree.symbol, tree.srcPos) - super.recheckApply(tree, pt) match - case tp @ CapturingType(tp1, refs) => - tree.fun match - case Select(qual, _) - if !tree.fun.symbol.isConstructor - && qual.tpe.captureSet.mightSubcapture(refs) - && tree.args.forall(_.tpe.captureSet.mightSubcapture(refs)) - && !qual.tpe.isBoxedCapturing - && !tree.args.exists(_.tpe.isBoxedCapturing) - => - tp.derivedCapturingType(tp1, tree.args.foldLeft(qual.tpe.captureSet)((cs, arg) => - cs ++ arg.tpe.captureSet)) - .showing(i"narrow $tree: $tp, refs = $refs, qual = ${qual.tpe.captureSet} --> $result", capt) - case _ => tp - case tp => tp - - override def recheckTyped(tree: Typed)(using Context): Type = - tree.tpt.tpe match - case AnnotatedType(_, annot) if annot.symbol == defn.RequiresCapabilityAnnot => - annot.tree match - case Apply(_, cap :: Nil) => - markFree(cap.symbol, tree.srcPos) - case _ => - case _ => - super.recheckTyped(tree) - override def recheck(tree: Tree, pt: Type = WildcardType)(using Context): Type = - val res = - if tree.isTerm && pt.isBoxedCapturing then - val saved = curEnv - curEnv = Env(curEnv.owner, CaptureSet.Var(), isBoxed = true, curEnv) - try super.recheck(tree, pt) - finally curEnv = saved - else super.recheck(tree, pt) - if tree.isTerm then - markFree(res.boxedCaptureSet, tree.srcPos) - res - + if tree.isTerm && pt.isBoxedCapturing then + val saved = curEnv + curEnv = Env(curEnv.owner, CaptureSet.Var(), isBoxed = true, curEnv) + try super.recheck(tree, pt) + finally curEnv = saved + else + val res = super.recheck(tree, pt) + if tree.isTerm then markFree(res.boxedCaptureSet, tree.srcPos) + res + + /** If `tree` is a reference or an application where the result type refers + * to an enclosing class or method parameter of the reference, check that the result type + * does not capture the universal capability. This is justified since the + * result type would have to be implicitly unboxed. + * TODO: Can we find a cleaner way to achieve this? Logically, this should be part + * of simulated boxing and unboxing. + */ override def recheckFinish(tpe: Type, tree: Tree, pt: Type)(using Context): Type = val typeToCheck = tree match case _: Ident | _: Select | _: Apply | _: TypeApply if tree.symbol.unboxesResult => tpe case _: Try => tpe - case ValDef(_, tpt, _) if tree.symbol.is(Mutable) => + case _: ValDef if tree.symbol.is(Mutable) => tree.symbol.info case _ => NoType @@ -491,16 +541,24 @@ class CheckCaptures extends Recheck, SymTransformer: checkNotUniversal(typeToCheck) super.recheckFinish(tpe, tree, pt) - /** This method implements the rule outlined in #14390: - * When checking an expression `e: T` against an expected type `Cx Tx` - * where the capture set of `Cx` contains this and any method inside the class - * `Cls` of `this` that contains `e` has only pure parameters, add to `Cx` - * all references to variables or this-references in that capture set of `T` - * that are outside `Cls`. These are all accessed through this, so we can assume - * they are already accounted for by `Cx` and adding them explicitly to `Cx` - * changes nothing. - */ + /** Massage `actual` and `expected` types using the methods below before checking conformance */ override def checkConformsExpr(actual: Type, expected: Type, tree: Tree)(using Context): Unit = + val expected1 = addOuterRefs(expected, actual) + val actual1 = adaptBoxed(actual, expected1, tree.srcPos) + //println(i"check conforms $actual1 <<< $expected1") + super.checkConformsExpr(actual1, expected1, tree) + + /** For the expected type, implement the rule outlined in #14390: + * - when checking an expression `a: Ca Ta` against an expected type `Ce Te`, + * - where the capture set `Ce` contains Cls.this, + * - and where and all method definitions enclosing `a` inside class `Cls` + * have only pure parameters, + * - add to `Ce` all references to variables or this-references in `Ca` + * that are outside `Cls`. These are all accessed through `Cls.this`, + * so we can assume they are already accounted for by `Ce` and adding + * them explicitly to `Ce` changes nothing. + */ + private def addOuterRefs(expected: Type, actual: Type)(using Context): Type = def isPure(info: Type): Boolean = info match case info: PolyType => isPure(info.resType) case info: MethodType => info.paramInfos.forall(_.captureSet.isAlwaysEmpty) && isPure(info.resType) @@ -514,14 +572,14 @@ class CheckCaptures extends Recheck, SymTransformer: eref match case eref: ThisType if isPureContext(ctx.owner, eref.cls) => erefs ++ arefs.filter { - case aref: TermRef => eref.cls.isContainedIn(aref.symbol.owner) - case aref: ThisType => eref.cls.isContainedIn(aref.cls) + case aref: TermRef => eref.cls.isProperlyContainedIn(aref.symbol.owner) + case aref: ThisType => eref.cls.isProperlyContainedIn(aref.cls) case _ => false } case _ => erefs } - val expected1 = expected match + expected match case CapturingType(ecore, erefs) => val erefs1 = augment(erefs, actual.captureSet) if erefs1 ne erefs then @@ -529,13 +587,14 @@ class CheckCaptures extends Recheck, SymTransformer: expected.derivedCapturingType(ecore, erefs1) case _ => expected - val normActual = adaptBoxed(actual, expected1, tree.srcPos) - //println(i"check conforms $normActual <<< $expected1") - super.checkConformsExpr(normActual, expected1, tree) /** Adapt `actual` type to `expected` type by inserting boxing and unboxing conversions */ def adaptBoxed(actual: Type, expected: Type, pos: SrcPos)(using Context): Type = + /** Adapt function type `actual`, which is `aargs -> ares` (possibly with dependencies) + * to `expected` type. + * @param reconstruct how to rebuild the adapted function type + */ def adaptFun(actual: Type, aargs: List[Type], ares: Type, expected: Type, covariant: Boolean, reconstruct: (List[Type], Type) => Type): Type = @@ -551,19 +610,32 @@ class CheckCaptures extends Recheck, SymTransformer: case actual @ CapturingType(parent, refs) => val parent1 = adapt(parent, expected, covariant) if actual.isBoxed != expected.isBoxedCapturing then - val uni = if covariant then refs.isUniversal else expected.captureSet.isUniversal - if uni then // TODO: better to constrain the set to be not universal - capt.println(i"ABORTING $actual vs $expected") + val criticalSet = // the set which is not allowed to have `*` + if covariant then refs // can't box with `*` + else expected.captureSet // can't unbox with `*` + if criticalSet.isUniversal 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 + // messages than disallowing the root capability in `criticalSet`. + capt.println(i"cannot box/unbox $actual vs $expected") actual else + // Disallow future addition of `*` to `criticalSet`. + criticalSet.disallowRootCapability { () => + report.error( + em"""$actual cannot be box-converted to $expected + |since one of their capture sets contains the root capability `*`""", + pos) + } if covariant == actual.isBoxed then markFree(refs, pos) CapturingType(parent1, refs, boxed = !actual.isBoxed) - else if parent1 eq parent then actual - else CapturingType(parent1, refs, boxed = actual.isBoxed) + else + actual.derivedCapturingType(parent1, refs) case actual @ AppliedType(tycon, args) if defn.isNonRefinedFunction(actual) => adaptFun(actual, args.init, args.last, expected, covariant, (aargs1, ares1) => actual.derivedAppliedType(tycon, aargs1 :+ ares1)) case actual @ RefinedType(_, _, rinfo: MethodType) if defn.isFunctionType(actual) => + // TODO Find a way to combine handling of generic and dependent function types (here and elsewhere) adaptFun(actual, rinfo.paramInfos, rinfo.resType, expected, covariant, (aargs1, ares1) => rinfo.derivedLambdaType(paramInfos = aargs1, resType = ares1) @@ -576,6 +648,7 @@ class CheckCaptures extends Recheck, SymTransformer: actualw match case CapturingType(p, refs) => actualw = actualw.derivedCapturingType(p, ref.singletonCaptureSet) + // given `a: C T`, improve `C T` to `{a} T` case _ => case _ => val adapted = adapt(actualw, expected, covariant = true) @@ -643,20 +716,20 @@ class CheckCaptures extends Recheck, SymTransformer: /** Perform the following kinds of checks * - Check all explicitly written capturing types for well-formedness using `checkWellFormedPost`. - * - Check that externally visible `val`s or `def`s have empty capture sets. If not + * - Check that externally visible `val`s or `def`s have empty capture sets. If not, * suggest an explicit type. This is so that separate compilation (where external - * symbols have empty capture sets) gives the same results as jount compilation. + * symbols have empty capture sets) gives the same results as joint compilation. */ def postCheck(unit: tpd.Tree)(using Context): Unit = unit.foreachSubTree { case _: InferredTypeTree => case tree: TypeTree if !tree.span.isZeroExtent => - tree.knownType.foreachPart( - checkWellformedPost(_, tree.srcPos)) - tree.knownType.foreachPart { - case AnnotatedType(_, annot) => - checkWellformedPost(annot.tree) - case _ => + tree.knownType.foreachPart { tp => + checkWellformedPost(tp, tree.srcPos) + tp match + case AnnotatedType(_, annot) if annot.symbol == defn.RetainsAnnot => + warnIfRedundantCaptureSet(annot.tree) + case _ => } case t: ValOrDefDef if t.tpt.isInstanceOf[InferredTypeTree] && !Synthetics.isExcluded(t.symbol) => diff --git a/tests/neg-custom-args/captures/usingLogFile.check b/tests/neg-custom-args/captures/usingLogFile.check index 2a04145699ba..03c413cbb3ef 100644 --- a/tests/neg-custom-args/captures/usingLogFile.check +++ b/tests/neg-custom-args/captures/usingLogFile.check @@ -1,8 +1,8 @@ -- Error: tests/neg-custom-args/captures/usingLogFile.scala:23:27 ------------------------------------------------------ 23 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. - | This usually means that a capability persists longer than its allowed lifetime. + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | {f, *} () -> Unit cannot be box-converted to box ? () -> Unit + | since one of their capture sets contains the root capability `*` -- Error: tests/neg-custom-args/captures/usingLogFile.scala:29:9 ------------------------------------------------------- 29 | later2.x() // error | ^^^^^^^^ @@ -20,16 +20,16 @@ | This usually means that a capability persists longer than its allowed lifetime. -- Error: tests/neg-custom-args/captures/usingLogFile.scala:47:27 ------------------------------------------------------ 47 | val later = usingLogFile { f => () => f.write(0) } // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. - | This usually means that a capability persists longer than its allowed lifetime. --- Error: tests/neg-custom-args/captures/usingLogFile.scala:62:25 ------------------------------------------------------ + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | {f, *} () -> Unit cannot be box-converted to box ? () -> Unit + | since one of their capture sets contains the root capability `*` +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:62:33 ------------------------------------------------------ 62 | val later = usingFile("out", f => (y: Int) => xs.foreach(x => f.write(x + y))) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type box {*} (x$0: Int) -> Unit is not allowed to capture the root capability `*`. - | This usually means that a capability persists longer than its allowed lifetime. --- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:25 ------------------------------------------------------ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | {f, *} (x$0: Int) -> Unit cannot be box-converted to box ? (x$0: Int) -> Unit + | since one of their capture sets contains the root capability `*` +-- Error: tests/neg-custom-args/captures/usingLogFile.scala:71:37 ------------------------------------------------------ 71 | val later = usingFile("logfile", usingLogger(_, l => () => l.log("test"))) // error - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | The expression's type box {*} () -> Unit is not allowed to capture the root capability `*`. - | This usually means that a capability persists longer than its allowed lifetime. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | {_$1, *} () -> Unit cannot be box-converted to box ? () -> Unit + | since one of their capture sets contains the root capability `*` From cf55ddb8536c19521a4de42787eccd660f4c4948 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 17 Aug 2022 16:37:03 +0200 Subject: [PATCH 96/99] Polish Synthetics.scala --- .../src/dotty/tools/dotc/cc/Synthetics.scala | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala index baae4b75cf8c..e8f7fd502baa 100644 --- a/compiler/src/dotty/tools/dotc/cc/Synthetics.scala +++ b/compiler/src/dotty/tools/dotc/cc/Synthetics.scala @@ -5,7 +5,10 @@ package cc import core.* import Symbols.*, SymDenotations.*, Contexts.*, Flags.*, Types.*, Decorators.* import StdNames.nme +import Names.Name import NameKinds.DefaultGetterName +import Phases.checkCapturesPhase +import config.Printers.capt /** Classification and transformation methods for synthetic * case class methods that need to be treated specially. @@ -14,17 +17,14 @@ import NameKinds.DefaultGetterName * compilation. */ object Synthetics: - def isSyntheticCopyMethod(sym: SymDenotation)(using Context) = + private def isSyntheticCopyMethod(sym: SymDenotation)(using Context) = sym.name == nme.copy && sym.is(Synthetic) && sym.owner.isClass && sym.owner.is(Case) - def isSyntheticApplyMethod(sym: SymDenotation)(using Context) = - sym.name == nme.apply && sym.is(Synthetic) && sym.owner.is(Module) && sym.owner.companionClass.is(Case) + private def isSyntheticCompanionMethod(sym: SymDenotation, names: Name*)(using Context): Boolean = + names.contains(sym.name) && sym.is(Synthetic) && sym.owner.is(Module) && sym.owner.companionClass.is(Case) - def isSyntheticUnapplyMethod(sym: SymDenotation)(using Context) = - sym.name == nme.unapply && sym.is(Synthetic) && sym.owner.is(Module) && sym.owner.companionClass.is(Case) - - def isSyntheticCopyDefaultGetterMethod(sym: SymDenotation)(using Context) = sym.name match - case DefaultGetterName(nme.copy, _) => sym.is(Synthetic) + private def isSyntheticCopyDefaultGetterMethod(sym: SymDenotation)(using Context) = sym.name match + case DefaultGetterName(nme.copy, _) => sym.is(Synthetic) && sym.owner.isClass && sym.owner.is(Case) case _ => false /** Is `sym` a synthetic apply, copy, or copy default getter method? @@ -33,8 +33,7 @@ object Synthetics: */ def needsTransform(sym: SymDenotation)(using Context): Boolean = isSyntheticCopyMethod(sym) - || isSyntheticApplyMethod(sym) - || isSyntheticUnapplyMethod(sym) + || isSyntheticCompanionMethod(sym, nme.apply, nme.unapply) || isSyntheticCopyDefaultGetterMethod(sym) /** Method is excluded from regular capture checking. @@ -48,14 +47,20 @@ object Synthetics: && sym.owner.isClass && ( defn.caseClassSynthesized.exists( ccsym => sym.overriddenSymbol(ccsym.owner.asClass) == ccsym) - || sym.name == nme.fromProduct - || needsTransform(sym) - ) - - /** Add capture dependencies to the type of `apply` or `copy` method of a case class */ + || isSyntheticCompanionMethod(sym, nme.fromProduct) + || needsTransform(sym)) + + /** Add capture dependencies to the type of the `apply` or `copy` method of a case class. + * An apply method in a case class like this: + * case class CC(a: {d} A, b: B, {*} c: C) + * would get type + * def apply(a': {d} A, b: B, {*} c': C): {a', c'} CC { val a = {a'} A, val c = {c'} C } + * where `'` is used to indicate the difference between parameter symbol and refinement name. + * Analogous for the copy method. + */ private def addCaptureDeps(info: Type)(using Context): Type = info match case info: MethodType => - val trackedParams = info.paramRefs.filter(atPhase(ctx.phase.next)(_.isTracked)) + val trackedParams = info.paramRefs.filter(atPhase(checkCapturesPhase)(_.isTracked)) def augmentResult(tp: Type): Type = tp match case tp: MethodOrPoly => tp.derivedLambdaType(resType = augmentResult(tp.resType)) @@ -67,7 +72,8 @@ object Synthetics: CaptureSet(pref))) } CapturingType(refined, CaptureSet(trackedParams*)) - if trackedParams.isEmpty then info else augmentResult(info) + if trackedParams.isEmpty then info + else augmentResult(info).showing(i"augment apply/copy type $info to $result", capt) case info: PolyType => info.derivedLambdaType(resType = addCaptureDeps(info.resType)) case _ => @@ -115,6 +121,7 @@ object Synthetics: case _ => info + /** Augment an unapply of type `(x: C): D` to `(x: {*} C): {x} D` */ private def addUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => val paramInfo :: Nil = info.paramInfos: @unchecked @@ -127,9 +134,11 @@ object Synthetics: case _ => CapturingType(tp, CaptureSet(trackedParam)) info.derivedLambdaType(paramInfos = newParamInfo :: Nil, resType = newResult(info.resType)) + .showing(i"augment unapply type $info to $result", capt) case info: PolyType => info.derivedLambdaType(resType = addUnapplyCaptures(info.resType)) + /** Drop added capture information from the type of an `unapply` */ private def dropUnapplyCaptures(info: Type)(using Context): Type = info match case info: MethodType => val CapturingType(oldParamInfo, _) :: Nil = info.paramInfos: @unchecked @@ -142,28 +151,30 @@ object Synthetics: case info: PolyType => info.derivedLambdaType(resType = dropUnapplyCaptures(info.resType)) - /** If `sym` refers to a synthetic apply, copy, or copy default getter method + /** If `sym` refers to a synthetic apply, unapply, copy, or copy default getter method * of a case class, transform it to account for capture information. + * The method is run in phase CheckCaptures.Pre * @pre needsTransform(sym) */ def transformToCC(sym: SymDenotation)(using Context): SymDenotation = sym.name match - case DefaultGetterName(nme.copy, n) if sym.is(Synthetic) && sym.owner.is(Case) => + case DefaultGetterName(nme.copy, n) => sym.copySymDenotation(info = addDefaultGetterCapture(sym.info, sym.owner, n)) case nme.unapply => sym.copySymDenotation(info = addUnapplyCaptures(sym.info)) - case _ => + case nme.apply | nme.copy => sym.copySymDenotation(info = addCaptureDeps(sym.info)) - /** If `sym` refers to a synthetic apply, copy, or copy default getter method + + /** If `sym` refers to a synthetic apply, unapply, copy, or copy default getter method * of a case class, transform it back to what it was before the CC phase. * @pre needsTransform(sym) */ - def transformFromCC(sym: SymDenotation)(using Context): SymDenotation = - if isSyntheticCopyDefaultGetterMethod(sym) then + def transformFromCC(sym: SymDenotation)(using Context): SymDenotation = sym.name match + case DefaultGetterName(nme.copy, n) => sym.copySymDenotation(info = dropDefaultGetterCapture(sym.info)) - else if sym.name == nme.unapply then + case nme.unapply => sym.copySymDenotation(info = dropUnapplyCaptures(sym.info)) - else + case nme.apply | nme.copy => sym.copySymDenotation(info = dropCaptureDeps(sym.info)) end Synthetics \ No newline at end of file From 39f035f76de446ecce87f96de763b673bdc18a78 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 17 Aug 2022 17:17:01 +0200 Subject: [PATCH 97/99] Change -Ycc setting explanations --- compiler/src/dotty/tools/dotc/config/ScalaSettings.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 266b22cfc7ef..9e34f8d726b5 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -320,14 +320,14 @@ private sealed trait YSettings: val YprofileRunGcBetweenPhases: Setting[List[String]] = PhasesSetting("-Yprofile-run-gc", "Run a GC between phases - this allows heap size to be accurate at the expense of more time. Specify a list of phases, or *", "_") //.withPostSetHook( _ => YprofileEnabled.value = true ) - // Extremely experimental language features + // Experimental language features val YnoKindPolymorphism: Setting[Boolean] = BooleanSetting("-Yno-kind-polymorphism", "Disable kind polymorphism.") val YexplicitNulls: Setting[Boolean] = BooleanSetting("-Yexplicit-nulls", "Make reference types non-nullable. Nullable types can be expressed with unions: e.g. String|Null.") val YcheckInit: Setting[Boolean] = BooleanSetting("-Ysafe-init", "Ensure safe initialization of objects") val YrequireTargetName: Setting[Boolean] = BooleanSetting("-Yrequire-targetName", "Warn if an operator is defined without a @targetName annotation") val YrecheckTest: Setting[Boolean] = BooleanSetting("-Yrecheck-test", "Run basic rechecking (internal test only)") - val Ycc: Setting[Boolean] = BooleanSetting("-Ycc", "Check captured references") - val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Debug info for captured references") + val Ycc: Setting[Boolean] = BooleanSetting("-Ycc", "Check captured references (warning: extremely experimental and unstable)") + val YccDebug: Setting[Boolean] = BooleanSetting("-Ycc-debug", "Used in conjunction with -Ycc, debug info for captured references") val YccNoAbbrev: Setting[Boolean] = BooleanSetting("-Ycc-no-abbrev", "Used in conjunction with -Ycc, suppress type abbreviations") /** Area-specific debug output */ From 4bab1eddc689e3c0c81d7b5204404bf72eb665c9 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 17 Aug 2022 21:11:04 +0200 Subject: [PATCH 98/99] Drop mention of cc-experiment in docs --- docs/_docs/reference/experimental/cc.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/_docs/reference/experimental/cc.md b/docs/_docs/reference/experimental/cc.md index c8fccad0bc4e..878bc0a64ed6 100644 --- a/docs/_docs/reference/experimental/cc.md +++ b/docs/_docs/reference/experimental/cc.md @@ -3,9 +3,8 @@ layout: doc-page title: "Capture Checking" --- -Capture checking is a research project that modifies the Scala type system to track references to capabilities in values. It is currently -implemented in an experimental branch [cc-experiment](https://github.com/lampepfl/dotty/tree/cc-experiment) in the dotty -repo and can be enabled on this branch with a `-Ycc` compiler option. +Capture checking is a research project that modifies the Scala type system to track references to capabilities in values. It can be enabled with a `-Ycc` compiler option. +At present, capture checking is still highly experimental and unstable. To get an idea what capture checking can do, let's start with a small example: ```scala From 943d84aadfb3f4a07dfaec92b9b5696196977b24 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 29 Aug 2022 17:40:48 +0200 Subject: [PATCH 99/99] Address review comment --- compiler/src/dotty/tools/dotc/cc/CaptureOps.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index ec9f71a1dc72..0ebf7c1c01e9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -88,8 +88,8 @@ extension (tp: Type) if tp.isBoxed then refs ++ pcs else pcs case tp: TypeRef if tp.symbol.isAbstractType => CaptureSet.empty case tp: TypeProxy => getBoxed(tp.superType) - case tp: AndType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) - case tp: OrType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) + case tp: AndType => getBoxed(tp.tp1) ** getBoxed(tp.tp2) + case tp: OrType => getBoxed(tp.tp1) ++ getBoxed(tp.tp2) case _ => CaptureSet.empty getBoxed(tp)