From de1c3b7e020a441697140729d4cb4cf0e1dc0ebd Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 11:25:51 +0200 Subject: [PATCH 1/9] Fixes to capture annotations in stdlib These problems were discovered once we started recording uses of this references. The recording is not yet part of this commit because it requires downstream bugfixes in the compiler. --- library/src/scala/collection/Iterator.scala | 12 ++++++------ library/src/scala/collection/LazyZipOps.scala | 2 +- library/src/scala/collection/SeqView.scala | 4 ++-- library/src/scala/collection/Stepper.scala | 6 +++--- library/src/scala/collection/View.scala | 4 ++-- .../scala/collection/convert/StreamExtensions.scala | 1 + library/src/scala/collection/mutable/HashTable.scala | 2 +- .../captures/colltest5/CollectionStrawManCC5_1.scala | 2 +- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/library/src/scala/collection/Iterator.scala b/library/src/scala/collection/Iterator.scala index 6a6132990f76..2b8b9d45d6e4 100644 --- a/library/src/scala/collection/Iterator.scala +++ b/library/src/scala/collection/Iterator.scala @@ -418,7 +418,7 @@ trait Iterator[+A] extends IterableOnce[A] with IterableOnceOps[A, Iterator, Ite @deprecated("Call scanRight on an Iterable instead.", "2.13.0") def scanRight[B](z: B)(op: (A, B) => B): Iterator[B]^{this, op} = ArrayBuffer.from(this).scanRight(z)(op).iterator - + /** Finds index of the first element satisfying some predicate after or at some start index. * * $mayNotTerminateInf @@ -494,9 +494,9 @@ trait Iterator[+A] extends IterableOnce[A] with IterableOnceOps[A, Iterator, Ite while (p(hd) == isFlipped) { if (!self.hasNext) return false hd = self.next() - } + } hdDefined = true - true + true } def next() = @@ -874,7 +874,7 @@ trait Iterator[+A] extends IterableOnce[A] with IterableOnceOps[A, Iterator, Ite */ def duplicate: (Iterator[A]^{this}, Iterator[A]^{this}) = { val gap = new scala.collection.mutable.Queue[A] - var ahead: Iterator[A] = null + var ahead: Iterator[A]^ = null class Partner extends AbstractIterator[A] { override def knownSize: Int = self.synchronized { val thisSize = self.knownSize @@ -890,7 +890,7 @@ trait Iterator[+A] extends IterableOnce[A] with IterableOnceOps[A, Iterator, Ite if (gap.isEmpty) ahead = this if (this eq ahead) { val e = self.next() - gap enqueue e + gap.enqueue(e) e } else gap.dequeue() } @@ -918,7 +918,7 @@ trait Iterator[+A] extends IterableOnce[A] with IterableOnceOps[A, Iterator, Ite */ def patch[B >: A](from: Int, patchElems: Iterator[B]^, replaced: Int): Iterator[B]^{this, patchElems} = new AbstractIterator[B] { - private[this] var origElems = self + private[this] var origElems: Iterator[B]^ = self // > 0 => that many more elems from `origElems` before switching to `patchElems` // 0 => need to drop elems from `origElems` and start using `patchElems` // -1 => have dropped elems from `origElems`, will be using `patchElems` until it's empty diff --git a/library/src/scala/collection/LazyZipOps.scala b/library/src/scala/collection/LazyZipOps.scala index 9a7dc3173926..5849de61448b 100644 --- a/library/src/scala/collection/LazyZipOps.scala +++ b/library/src/scala/collection/LazyZipOps.scala @@ -389,7 +389,7 @@ final class LazyZip4[+El1, +El2, +El3, +El4, C1] private[collection](src: C1, } private def toIterable: View[(El1, El2, El3, El4)]^{this} = new AbstractView[(El1, El2, El3, El4)] { - def iterator: AbstractIterator[(El1, El2, El3, El4)] = new AbstractIterator[(El1, El2, El3, El4)] { + def iterator: AbstractIterator[(El1, El2, El3, El4)]^{this} = new AbstractIterator[(El1, El2, El3, El4)] { private[this] val elems1 = coll1.iterator private[this] val elems2 = coll2.iterator private[this] val elems3 = coll3.iterator diff --git a/library/src/scala/collection/SeqView.scala b/library/src/scala/collection/SeqView.scala index 960de6f1d752..3928a20aef70 100644 --- a/library/src/scala/collection/SeqView.scala +++ b/library/src/scala/collection/SeqView.scala @@ -202,10 +202,10 @@ object SeqView { override def knownSize: Int = len override def isEmpty: Boolean = len == 0 override def to[C1](factory: Factory[A, C1]): C1 = _sorted.to(factory) - override def reverse: SeqView[A] = new ReverseSorted + override def reverse: SeqView[A]^{this} = new ReverseSorted // we know `_sorted` is either tiny or has efficient random access, // so this is acceptable for `reversed` - override protected def reversed: Iterable[A] = new ReverseSorted + override protected def reversed: Iterable[A]^{this} = new ReverseSorted override def sorted[B1 >: A](implicit ord1: Ordering[B1]): SeqView[A]^{this} = if (ord1 == this.ord) this diff --git a/library/src/scala/collection/Stepper.scala b/library/src/scala/collection/Stepper.scala index 7c4b5821aef9..7685c2304c7c 100644 --- a/library/src/scala/collection/Stepper.scala +++ b/library/src/scala/collection/Stepper.scala @@ -260,7 +260,7 @@ trait IntStepper extends Stepper[Int] { def spliterator[B >: Int]: Spliterator.OfInt^{this} = new IntStepper.IntStepperSpliterator(this) - def javaIterator[B >: Int]: PrimitiveIterator.OfInt = new PrimitiveIterator.OfInt { + def javaIterator[B >: Int]: PrimitiveIterator.OfInt^{this} = new PrimitiveIterator.OfInt { def hasNext: Boolean = hasStep def nextInt(): Int = nextStep() } @@ -298,7 +298,7 @@ trait DoubleStepper extends Stepper[Double] { def spliterator[B >: Double]: Spliterator.OfDouble^{this} = new DoubleStepper.DoubleStepperSpliterator(this) - def javaIterator[B >: Double]: PrimitiveIterator.OfDouble = new PrimitiveIterator.OfDouble { + def javaIterator[B >: Double]: PrimitiveIterator.OfDouble^{this} = new PrimitiveIterator.OfDouble { def hasNext: Boolean = hasStep def nextDouble(): Double = nextStep() } @@ -337,7 +337,7 @@ trait LongStepper extends Stepper[Long] { def spliterator[B >: Long]: Spliterator.OfLong^{this} = new LongStepper.LongStepperSpliterator(this) - def javaIterator[B >: Long]: PrimitiveIterator.OfLong = new PrimitiveIterator.OfLong { + def javaIterator[B >: Long]: PrimitiveIterator.OfLong^{this} = new PrimitiveIterator.OfLong { def hasNext: Boolean = hasStep def nextLong(): Long = nextStep() } diff --git a/library/src/scala/collection/View.scala b/library/src/scala/collection/View.scala index 6b862aece4ea..78a3f49b1f24 100644 --- a/library/src/scala/collection/View.scala +++ b/library/src/scala/collection/View.scala @@ -172,7 +172,7 @@ object View extends IterableFactory[View] { @SerialVersionUID(3L) class LeftPartitionMapped[A, A1, A2](underlying: SomeIterableOps[A]^, f: A => Either[A1, A2]) extends AbstractView[A1] { - def iterator: AbstractIterator[A1] = new AbstractIterator[A1] { + def iterator: AbstractIterator[A1]^{this} = new AbstractIterator[A1] { private[this] val self = underlying.iterator private[this] var hd: A1 = _ private[this] var hdDefined: Boolean = false @@ -197,7 +197,7 @@ object View extends IterableFactory[View] { @SerialVersionUID(3L) class RightPartitionMapped[A, A1, A2](underlying: SomeIterableOps[A]^, f: A => Either[A1, A2]) extends AbstractView[A2] { - def iterator: AbstractIterator[A2] = new AbstractIterator[A2] { + def iterator: AbstractIterator[A2]^{this} = new AbstractIterator[A2] { private[this] val self = underlying.iterator private[this] var hd: A2 = _ private[this] var hdDefined: Boolean = false diff --git a/library/src/scala/collection/convert/StreamExtensions.scala b/library/src/scala/collection/convert/StreamExtensions.scala index 5e70ed1b4fd0..28b762b425f0 100644 --- a/library/src/scala/collection/convert/StreamExtensions.scala +++ b/library/src/scala/collection/convert/StreamExtensions.scala @@ -30,6 +30,7 @@ import scala.jdk._ * [[scala.jdk.javaapi.StreamConverters]]. */ trait StreamExtensions { + this: StreamExtensions => // collections implicit class IterableHasSeqStream[A](cc: IterableOnce[A]) { diff --git a/library/src/scala/collection/mutable/HashTable.scala b/library/src/scala/collection/mutable/HashTable.scala index 738adce7dfaa..5d0d1f35bdca 100644 --- a/library/src/scala/collection/mutable/HashTable.scala +++ b/library/src/scala/collection/mutable/HashTable.scala @@ -211,7 +211,7 @@ private[collection] trait HashTable[A, B, Entry >: Null <: HashEntry[A, Entry]] /** An iterator returning all entries. */ - def entriesIterator: Iterator[Entry] = new AbstractIterator[Entry] { + def entriesIterator: Iterator[Entry]^{this} = new AbstractIterator[Entry] { val iterTable = table var idx = lastPopulatedIndex var es = iterTable(idx) diff --git a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala index ea0bdc240e0c..1143cdb30d5e 100644 --- a/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala +++ b/tests/run-custom-args/captures/colltest5/CollectionStrawManCC5_1.scala @@ -423,7 +423,7 @@ object CollectionStrawMan5 { def start: Int def end: Int def apply(i: Int): A - def iterator: Iterator[A] = new Iterator[A] { + def iterator: Iterator[A]^{this} = new Iterator[A] { private var current = start def hasNext = current < end def next(): A = { From a4e1546a8c8f01efd2a2c5c7c827f1602f65b99c Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 11:38:13 +0200 Subject: [PATCH 2/9] Check use sets of non-static inner classes These fell through the cracks before since we only considered named outer refs. But inner classes can have outer this references check needd to be tracked in use sets. --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 74c243a20684..a4ff27f29466 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -407,12 +407,12 @@ class CheckCaptures extends Recheck, SymTransformer: else i"references $cs1$cs1description are not all", cs1, cs2, pos, provenance) - /** 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`. + /** If `sym` is a method or a non-static inner class, a capture set variable + * representing the captured variables of the environment associated with `sym`. */ def capturedVars(sym: Symbol)(using Context): CaptureSet = myCapturedVars.getOrElseUpdate(sym, - if sym.ownersIterator.exists(_.isTerm) + if sym.isTerm || !sym.owner.isStaticOwner then CaptureSet.Var(sym.owner, level = ccState.symLevel(sym)) else CaptureSet.empty) From 182a26ab156f728abb1660650437c33e6e701b21 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 11:44:30 +0200 Subject: [PATCH 3/9] markFree of a constructor should never continue with the enclosing class Previously there was a boundary condition where this could be the case for outermost classes. --- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a4ff27f29466..8823b302f290 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -420,12 +420,9 @@ class CheckCaptures extends Recheck, SymTransformer: /** The next environment enclosing `env` that needs to be charged * with free references. - * @param included Whether an environment is included in the range of - * environments to charge. Once `included` is false, no - * more environments need to be charged. */ - def nextEnvToCharge(env: Env, included: Env => Boolean)(using Context): Env = - if env.owner.isConstructor && included(env.outer) then env.outer.outer + def nextEnvToCharge(env: Env)(using Context): Env | Null = + if env.owner.isConstructor then env.outer.outer0 else env.outer /** A description where this environment comes from */ @@ -535,7 +532,9 @@ class CheckCaptures extends Recheck, SymTransformer: checkSubset(included, env.captured, tree.srcPos, provenance(env)) capt.println(i"Include call or box capture $included from $cs in ${env.owner} --> ${env.captured}") if !isOfNestedMethod(env) then - recur(included, nextEnvToCharge(env, !_.owner.isStaticOwner), env) + val nextEnv = nextEnvToCharge(env) + if nextEnv != null && !nextEnv.owner.isStaticOwner then + recur(included, nextEnv, env) // Under deferredReaches, don't propagate out of methods inside terms. // The use set of these methods will be charged when that method is called. @@ -2021,7 +2020,9 @@ class CheckCaptures extends Recheck, SymTransformer: if env.kind == EnvKind.Boxed then env.owner else if isOfNestedMethod(env) then env.owner.owner else if env.owner.isStaticOwner then NoSymbol - else boxedOwner(nextEnvToCharge(env, alwaysTrue)) + else + val nextEnv = nextEnvToCharge(env) + if nextEnv == null then NoSymbol else boxedOwner(nextEnv) def checkUseUnlessBoxed(c: Capability, croot: NamedType) = if !boxedOwner(env).isContainedIn(croot.symbol.owner) then From d71358f21b720189bdc4a891e763d4d2e61fe2e9 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 11:49:22 +0200 Subject: [PATCH 4/9] Don't include use sets of class constructors in use sets of enclosing classes --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- tests/neg-custom-args/captures/real-try.check | 2 +- tests/neg-custom-args/captures/unsound-reach-4.check | 2 +- tests/neg-custom-args/captures/unsound-reach.check | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 8823b302f290..f35381a75bf0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -865,7 +865,7 @@ class CheckCaptures extends Recheck, SymTransformer: val (refined, cs) = addParamArgRefinements(core, initCs) refined.capturing(cs) - augmentConstructorType(resType, capturedVars(cls) ++ capturedVars(constr)) + augmentConstructorType(resType, capturedVars(cls)) .showing(i"constr type $mt with $argTypes%, % in $constr = $result", capt) end refineConstructorInstance diff --git a/tests/neg-custom-args/captures/real-try.check b/tests/neg-custom-args/captures/real-try.check index bca841e11094..3fde3af88fd9 100644 --- a/tests/neg-custom-args/captures/real-try.check +++ b/tests/neg-custom-args/captures/real-try.check @@ -43,7 +43,7 @@ -- Error: tests/neg-custom-args/captures/real-try.scala:32:10 ---------------------------------------------------------- 32 | val b = try // error | ^ - | The result of `try` cannot have type Cell[() => Unit]^'s2 since + | The result of `try` cannot have type Cell[() => Unit] since | the part () => Unit of that type captures the root capability `cap`. | This is often caused by a locally generated exception capability leaking as part of its result. | diff --git a/tests/neg-custom-args/captures/unsound-reach-4.check b/tests/neg-custom-args/captures/unsound-reach-4.check index 573e9a31d068..cddcc58e38b4 100644 --- a/tests/neg-custom-args/captures/unsound-reach-4.check +++ b/tests/neg-custom-args/captures/unsound-reach-4.check @@ -8,7 +8,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach-4.scala:20:29 ------------------------------ 20 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). | ^^^^^^^ - | Found: Bar^'s1 + | Found: Bar | Required: Foo[File^] | | Note that capability cap is not included in capture set {cap²} diff --git a/tests/neg-custom-args/captures/unsound-reach.check b/tests/neg-custom-args/captures/unsound-reach.check index 7f713586b6ec..0313ebe37b88 100644 --- a/tests/neg-custom-args/captures/unsound-reach.check +++ b/tests/neg-custom-args/captures/unsound-reach.check @@ -15,7 +15,7 @@ -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/unsound-reach.scala:18:31 -------------------------------- 18 | val backdoor: Foo[File^] = new Bar // error (follow-on, since the parent Foo[File^] of bar is illegal). | ^^^^^^^ - | Found: Bar^'s1 + | Found: Bar | Required: Foo[File^] | | Note that capability cap is not included in capture set {cap²} From 668ceb1c691db93ccaaf27a2df0a49ff44b98bc1 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 11:53:02 +0200 Subject: [PATCH 5/9] Don't assume `this` is visible in constructors A constructor should never capture `this`, the object it constructs. --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index f35381a75bf0..c5f3353e2366 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -466,10 +466,13 @@ class CheckCaptures extends Recheck, SymTransformer: // if `sym` is not defined inside the owner of the environment. inline def isVisibleFromEnv(sym: Symbol, env: Env) = sym.exists && { + val effectiveOwner = + if env.owner.isConstructor then env.owner.owner + else env.owner if env.kind == EnvKind.NestedInOwner then - !sym.isProperlyContainedIn(env.owner) + !sym.isProperlyContainedIn(effectiveOwner) else - !sym.isContainedIn(env.owner) + !sym.isContainedIn(effectiveOwner) } /** Avoid locally defined capability by charging the underlying type From 137d3b91ba583f0bf02a6aaefc102adb0d6199ee Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 12:17:57 +0200 Subject: [PATCH 6/9] Record uses of this Was shockingly missing before. --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 47 ++++++++++++------- .../dotty/tools/dotc/transform/Recheck.scala | 10 +++- .../neg-custom-args/captures/class-caps.check | 22 +++++++++ .../neg-custom-args/captures/class-caps.scala | 39 +++++++++++++++ .../captures/constr-uses.scala | 13 +++++ .../captures/nested-classes.scala | 1 + .../captures/secondary-constructors.scala | 13 +++++ 7 files changed, 126 insertions(+), 19 deletions(-) create mode 100644 tests/neg-custom-args/captures/class-caps.check create mode 100644 tests/neg-custom-args/captures/class-caps.scala create mode 100644 tests/pos-custom-args/captures/constr-uses.scala create mode 100644 tests/pos-custom-args/captures/secondary-constructors.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index c5f3353e2366..5942f5874712 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -455,7 +455,10 @@ class CheckCaptures extends Recheck, SymTransformer: markFree(sym, sym.termRef, tree) def markFree(sym: Symbol, ref: Capability, tree: Tree)(using Context): Unit = - if sym.exists && ref.isTracked then markFree(ref.singletonCaptureSet, tree) + if sym.exists then markFree(ref, tree) + + def markFree(ref: Capability, tree: Tree)(using Context): Unit = + if ref.isTracked then markFree(ref.singletonCaptureSet, tree) /** Make sure the (projected) `cs` is a subset of the capture sets of all enclosing * environments. At each stage, only include references from `cs` that are outside @@ -628,25 +631,33 @@ class CheckCaptures extends Recheck, SymTransformer: // If ident refers to a parameterless method, charge its cv to the environment includeCallCaptures(sym, sym.info, tree) else if !sym.isStatic then - // Otherwise charge its symbol, but add all selections and also any `.rd` - // modifier implied by the expected type `pt`. - // Example: If we have `x` and the expected type says we select that with `.a.b` - // where `b` is a read-only method, we charge `x.a.b.rd` instead of `x`. - def addSelects(ref: TermRef, pt: Type): Capability = pt match - case pt: PathSelectionProto if ref.isTracked => - if pt.sym.isReadOnlyMethod then - ref.readOnly - else - // if `ref` is not tracked then the selection could not give anything new - // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. - addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) - case _ => ref - var pathRef: Capability = addSelects(sym.termRef, pt) - if pathRef.derivesFromMutable && pt.isValueType && !pt.isMutableType then - pathRef = pathRef.readOnly - markFree(sym, pathRef, tree) + markFree(sym, pathRef(sym.termRef, pt), tree) mapResultRoots(super.recheckIdent(tree, pt), tree.symbol) + override def recheckThis(tree: This, pt: Type)(using Context): Type = + markFree(pathRef(tree.tpe.asInstanceOf[ThisType], pt), tree) + super.recheckThis(tree, pt) + + /** Add all selections and also any `.rd modifier implied by the expected + * type `pt` to `base`. Example: + * If we have `x` and the expected type says we select that with `.a.b` + * where `b` is a read-only method, we charge `x.a.b.rd` instead of `x`. + */ + private def pathRef(base: TermRef | ThisType, pt: Type)(using Context): Capability = + def addSelects(ref: TermRef | ThisType, pt: Type): Capability = pt match + case pt: PathSelectionProto if ref.isTracked => + if pt.sym.isReadOnlyMethod then + ref.readOnly + else + // if `ref` is not tracked then the selection could not give anything new + // class SerializationProxy in stdlib-cc/../LazyListIterable.scala has an example where this matters. + addSelects(ref.select(pt.sym).asInstanceOf[TermRef], pt.pt) + case _ => ref + val ref: Capability = addSelects(base, pt) + if ref.derivesFromMutable && pt.isValueType && !pt.isMutableType + then ref.readOnly + else ref + /** The expected type for the qualifier of a selection. If the selection * could be part of a capability path or is a a read-only method, we return * a PathSelectionProto. diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index fff8f3e94762..ffe28254fb08 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -248,6 +248,12 @@ abstract class Recheck extends Phase, SymTransformer: def recheckSelection(tree: Select, qualType: Type, name: Name, pt: Type)(using Context): Type = recheckSelection(tree, qualType, name, sharpen = identity[Denotation]) + def recheckThis(tree: This, pt: Type)(using Context): Type = + tree.tpe + + def recheckSuper(tree: Super, pt: Type)(using Context): Type = + tree.tpe + def recheckBind(tree: Bind, pt: Type)(using Context): Type = tree match case Bind(name, body) => recheck(body, pt) @@ -540,7 +546,9 @@ abstract class Recheck extends Phase, SymTransformer: 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: This => recheckThis(tree, pt) + case tree: Super => recheckSuper(tree, pt) + case _: New | _: Literal => tree.tpe case tree: Typed => recheckTyped(tree) case tree: Assign => recheckAssign(tree) case tree: Block => recheckBlock(tree, pt) diff --git a/tests/neg-custom-args/captures/class-caps.check b/tests/neg-custom-args/captures/class-caps.check new file mode 100644 index 000000000000..ef6462eefbb3 --- /dev/null +++ b/tests/neg-custom-args/captures/class-caps.check @@ -0,0 +1,22 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-caps.scala:18:46 ----------------------------------- +18 | def addWritesToConsole: (Int, Int) -> Int = (a, b) => // error + | ^ + | Found: (a: Int, b: Int) ->{Test.this.console} Int + | Required: (Int, Int) -> Int + | + | Note that capability Test.this.console is not included in capture set {}. +19 | log(s"adding a ($a) to b ($b)")(using console) +20 | a + b + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/class-caps.scala:28:46 ----------------------------------- +28 | def addWritesToConsole: (Int, Int) -> Int = (a, b) => // error + | ^ + | Found: (a: Int, b: Int) ->{Test1.this.console} Int + | Required: (Int, Int) -> Int + | + | Note that capability Test1.this.console is not included in capture set {}. +29 | log(s"adding a ($a) to b ($b)")(using console) +30 | a + b + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/class-caps.scala b/tests/neg-custom-args/captures/class-caps.scala new file mode 100644 index 000000000000..f49356c8aecb --- /dev/null +++ b/tests/neg-custom-args/captures/class-caps.scala @@ -0,0 +1,39 @@ +//> using scala 3.nightly + +import scala.language.experimental.captureChecking +import scala.caps.* + +class Console() extends SharedCapability: + def println(s: String): Unit = System.out.println(s"console: $s") + +def log(s: String)(using c: Console): Unit = + summon[Console].println(s) + +class Test: + this: Test^ => + val addPure: (Int, Int) -> Int = (a, b) => a + b // same as before + + val console: Console^ = Console() // provide capability locally + + def addWritesToConsole: (Int, Int) -> Int = (a, b) => // error + log(s"adding a ($a) to b ($b)")(using console) + a + b + + +class Test1: + val addPure: (Int, Int) -> Int = (a, b) => a + b // same as before + + val console: Console^ = Console() // provide capability locally + + def addWritesToConsole: (Int, Int) -> Int = (a, b) => // error + log(s"adding a ($a) to b ($b)")(using console) + a + b + +object Test2: + val addPure: (Int, Int) -> Int = (a, b) => a + b // same as before + + val console: Console^ = Console() // provide capability locally + + def addWritesToConsole: (Int, Int) -> Int = (a, b) => // ok since `console` is static (maybe flag this?) + log(s"adding a ($a) to b ($b)")(using console) + a + b diff --git a/tests/pos-custom-args/captures/constr-uses.scala b/tests/pos-custom-args/captures/constr-uses.scala new file mode 100644 index 000000000000..cf32cfcfe4e3 --- /dev/null +++ b/tests/pos-custom-args/captures/constr-uses.scala @@ -0,0 +1,13 @@ +abstract class Test: + this: Test^ => + def outer: Int + + class Inner(x: Int): + def this() = this(outer) + + val i = Inner() + val _: Inner = i + + val f = () => Inner() + val _: () ->{this} Inner = f + diff --git a/tests/pos-custom-args/captures/nested-classes.scala b/tests/pos-custom-args/captures/nested-classes.scala index e9227889f10e..bb422ceabd8f 100644 --- a/tests/pos-custom-args/captures/nested-classes.scala +++ b/tests/pos-custom-args/captures/nested-classes.scala @@ -5,6 +5,7 @@ class IO extends caps.SharedCapability class Blah class Pkg(using io: IO): class Foo: + this: Foo^{Pkg.this} => def m(foo: Blah^{io}) = ??? class Pkg2(using io: IO): class Foo: diff --git a/tests/pos-custom-args/captures/secondary-constructors.scala b/tests/pos-custom-args/captures/secondary-constructors.scala new file mode 100644 index 000000000000..abc923a3a8a3 --- /dev/null +++ b/tests/pos-custom-args/captures/secondary-constructors.scala @@ -0,0 +1,13 @@ +package scala +package collection +package immutable + +final class LazyListIterable[+A](): + this: LazyListIterable[A]^ => + var _head: Any = 0 + private def this(head: A, tail: LazyListIterable[A]^) = + this() + _head = head + +object LazyListIterable: + @inline private def eagerCons[A](hd: A, tl: LazyListIterable[A]^): LazyListIterable[A]^{tl} = new LazyListIterable[A](hd, tl) \ No newline at end of file From b7387d1766cf8d96170dc726081c32f2395173ea Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 18:37:49 +0200 Subject: [PATCH 7/9] Refine treatment of fields - Charge the use set of the initializer to the class constructor - Charge the declared capture set to the class --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 31 +++++++++++++++++-- .../dotty/tools/dotc/transform/Recheck.scala | 5 ++- .../captures/class-constr.scala | 6 ++-- .../captures/exception-definitions.check | 10 +++--- .../captures/exception-definitions.scala | 6 ++-- .../captures/method-uses.scala | 6 ++-- tests/neg-custom-args/captures/uses.check | 22 ++++++------- tests/neg-custom-args/captures/uses.scala | 4 +-- .../captures/nested-traits.scala | 7 +++++ 9 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 tests/pos-custom-args/captures/nested-traits.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 5942f5874712..a7ecf18f387e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -464,7 +464,7 @@ class CheckCaptures extends Recheck, SymTransformer: * environments. At each stage, only include references from `cs` that are outside * the environment's owner */ - def markFree(cs: CaptureSet, tree: Tree)(using Context): Unit = + def markFree(cs: CaptureSet, tree: Tree, addUseInfo: Boolean = true)(using Context): Unit = // A captured reference with the symbol `sym` is visible from the environment // if `sym` is not defined inside the owner of the environment. inline def isVisibleFromEnv(sym: Symbol, env: Env) = @@ -546,7 +546,7 @@ class CheckCaptures extends Recheck, SymTransformer: if !cs.isAlwaysEmpty then recur(cs, curEnv, null) - useInfos += ((tree, cs, curEnv)) + if addUseInfo then useInfos += ((tree, cs, curEnv)) end markFree /** If capability `c` refers to a parameter that is not implicitly or explicitly @@ -988,6 +988,8 @@ class CheckCaptures extends Recheck, SymTransformer: * - Interpolate contravariant capture set variables in result type. */ override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type = + val savedEnv = curEnv + val runInConstructor = !sym.isOneOf(Param | ParamAccessor | Lazy | NonMember) try if sym.is(Module) then sym.info // Modules are checked by checking the module class else @@ -1006,6 +1008,8 @@ class CheckCaptures extends Recheck, SymTransformer: "" disallowBadRootsIn( tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos) + if runInConstructor then + pushConstructorEnv() checkInferredResult(super.recheckValDef(tree, sym), tree) finally if !sym.is(Param) then @@ -1015,6 +1019,11 @@ class CheckCaptures extends Recheck, SymTransformer: // function is compiled since we do not propagate expected types into blocks. interpolateIfInferred(tree.tpt, sym) + if runInConstructor && savedEnv.owner.isClass then + curEnv = savedEnv + markFree(tree.tpt.nuType.captureSet, tree, addUseInfo = false) + end recheckValDef + /** Recheck method definitions: * - check body in a nested environment that tracks uses, in a nested level, * and in a nested context that knows abaout Contains parameters so that we @@ -1241,6 +1250,24 @@ class CheckCaptures extends Recheck, SymTransformer: recheckFinish(result, arg, pt) */ + /** If environment is owned by a class, run in a new environment owned by + * its primary constructor instead. + */ + def pushConstructorEnv()(using Context): Unit = + if curEnv.owner.isClass then + val constr = curEnv.owner.primaryConstructor + if constr.exists then + val constrSet = capturedVars(constr) + if capturedVars(constr) ne CaptureSet.empty then + curEnv = Env(constr, EnvKind.Regular, constrSet, curEnv) + + override def recheckStat(stat: Tree)(using Context): Unit = + val saved = curEnv + if !stat.isInstanceOf[MemberDef] then + pushConstructorEnv() + try recheck(stat) + finally curEnv = saved + /** The main recheck method does some box adapation for all nodes: * - If expected type `pt` is boxed and the tree is a lambda or a reference, * don't propagate free variables. diff --git a/compiler/src/dotty/tools/dotc/transform/Recheck.scala b/compiler/src/dotty/tools/dotc/transform/Recheck.scala index ffe28254fb08..d6fd49336e9d 100644 --- a/compiler/src/dotty/tools/dotc/transform/Recheck.scala +++ b/compiler/src/dotty/tools/dotc/transform/Recheck.scala @@ -493,12 +493,15 @@ abstract class Recheck extends Phase, SymTransformer: recheckStats(tree.stats) NoType + def recheckStat(stat: Tree)(using Context): Unit = + recheck(stat) + def recheckStats(stats: List[Tree])(using Context): Unit = @tailrec def traverse(stats: List[Tree])(using Context): Unit = stats match case (imp: Import) :: rest => traverse(rest)(using ctx.importContext(imp, imp.symbol)) case stat :: rest => - recheck(stat) + recheckStat(stat) traverse(rest) case _ => traverse(stats) diff --git a/tests/neg-custom-args/captures/class-constr.scala b/tests/neg-custom-args/captures/class-constr.scala index e86263fb0714..ab150868bef1 100644 --- a/tests/neg-custom-args/captures/class-constr.scala +++ b/tests/neg-custom-args/captures/class-constr.scala @@ -19,6 +19,6 @@ def test(a: Cap, b: Cap) = println(b) 2 val d = () => new D() - val d_ok1: () ->{a, b} D^{a, b} = d - val d_ok2: () -> D^{a, b} = d // because of function shorthand - val d_ok3: () ->{a, b} D^{b} = d // error, but should work + val d_ok1: () ->{a, b} D^{a, b} = d // ok + val d_ok2: () ->{a} D^{b} = d // ok + val d_ok3: () -> D^{a, b} = d // error diff --git a/tests/neg-custom-args/captures/exception-definitions.check b/tests/neg-custom-args/captures/exception-definitions.check index be5ea4304bf5..9429628b4bde 100644 --- a/tests/neg-custom-args/captures/exception-definitions.check +++ b/tests/neg-custom-args/captures/exception-definitions.check @@ -2,11 +2,11 @@ 3 | self: Err^ => // error | ^^^^ | Err is a pure type, it makes no sense to add a capture set to it --- Error: tests/neg-custom-args/captures/exception-definitions.scala:7:12 ---------------------------------------------- +-- Error: tests/neg-custom-args/captures/exception-definitions.scala:7:8 ----------------------------------------------- 7 | val x = c // error - | ^ - | reference (c : Any^) is not included in the allowed capture set {} of the self type of class Err2 + | ^^^^^^^^^ + | reference (c : Object^) is not included in the allowed capture set {} of the self type of class Err2 -- Error: tests/neg-custom-args/captures/exception-definitions.scala:8:13 ---------------------------------------------- -8 | class Err3(c: Any^) extends Exception // error +8 | class Err3(c: Object^) extends Exception // error | ^ - | reference (Err3.this.c : Any^) is not included in the allowed capture set {} of the self type of class Err3 + | reference (Err3.this.c : Object^) is not included in the allowed capture set {} of the self type of class Err3 diff --git a/tests/neg-custom-args/captures/exception-definitions.scala b/tests/neg-custom-args/captures/exception-definitions.scala index fbc9f3fd1d33..4c3370464ffe 100644 --- a/tests/neg-custom-args/captures/exception-definitions.scala +++ b/tests/neg-custom-args/captures/exception-definitions.scala @@ -2,11 +2,11 @@ class Err extends Exception: self: Err^ => // error -def test(c: Any^) = +def test(c: Object^) = class Err2 extends Exception: val x = c // error - class Err3(c: Any^) extends Exception // error + class Err3(c: Object^) extends Exception // error -class Err4(c: Any^) extends AnyVal // was error, now ok +class Err4(c: Object^) extends AnyVal // was error, now ok diff --git a/tests/neg-custom-args/captures/method-uses.scala b/tests/neg-custom-args/captures/method-uses.scala index da8f226685c0..576e227e5d74 100644 --- a/tests/neg-custom-args/captures/method-uses.scala +++ b/tests/neg-custom-args/captures/method-uses.scala @@ -11,7 +11,7 @@ def test(xs: List[() => Unit]) = foo // error bar() // error - Foo() // OK, but could be error + Foo() // error def test2(xs: List[() => Unit]) = def foo = xs.head // error, ok under deferredReaches @@ -23,8 +23,8 @@ def test3(xs: List[() => Unit]): () ->{xs*} Unit = () => def test4(xs: List[() => Unit]) = () => xs.head // error, ok under deferredReaches def test5(xs: List[() => Unit]) = new: - println(xs.head) // error, ok under deferredReaches + println(xs.head) // error, ok under deferredReaches // error def test6(xs: List[() => Unit]) = - val x= new { println(xs.head) } // error + val x= new { println(xs.head) } // error // error x diff --git a/tests/neg-custom-args/captures/uses.check b/tests/neg-custom-args/captures/uses.check index 08c79dd8fb5f..5ada150ddf79 100644 --- a/tests/neg-custom-args/captures/uses.check +++ b/tests/neg-custom-args/captures/uses.check @@ -1,21 +1,21 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:8:17 ------------------------------------------ -8 | val _: D^{y} = d // error, should be ok - | ^ - | Found: (d : D^{x, y}) - | Required: D^{y} - | - | Note that capability x is not included in capture set {y}. - | - | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:9:13 ------------------------------------------ 9 | val _: D = d // error | ^ - | Found: (d : D^{x, y}) + | Found: (d : D^{y}) | Required: D | - | Note that capability x is not included in capture set {}. + | Note that capability y is not included in capture set {}. | | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:13:22 ----------------------------------------- +13 | val _: () -> Unit = f // error + | ^ + | Found: (f : () ->{x} Unit) + | Required: () -> Unit + | + | Note that capability x is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` -- [E007] Type Mismatch Error: tests/neg-custom-args/captures/uses.scala:18:34 ----------------------------------------- 18 | val _: () ->{x} () ->{y} Unit = g // error, should be ok | ^ diff --git a/tests/neg-custom-args/captures/uses.scala b/tests/neg-custom-args/captures/uses.scala index b872c7b03ec7..28d85b64687d 100644 --- a/tests/neg-custom-args/captures/uses.scala +++ b/tests/neg-custom-args/captures/uses.scala @@ -5,12 +5,12 @@ def test(x: C^, y: C^) = def foo() = println(y) } val d = D() - val _: D^{y} = d // error, should be ok + val _: D^{y} = d val _: D = d // error val f = () => println(D()) val _: () ->{x} Unit = f // ok - val _: () -> Unit = f // should be error + val _: () -> Unit = f // error def g = () => println(x) diff --git a/tests/pos-custom-args/captures/nested-traits.scala b/tests/pos-custom-args/captures/nested-traits.scala new file mode 100644 index 000000000000..b532feedeeee --- /dev/null +++ b/tests/pos-custom-args/captures/nested-traits.scala @@ -0,0 +1,7 @@ +trait MapOps[K]: + this: MapOps[K]^ => + def keysIterator: Iterator[K] + + trait GenKeySet[K]: + this: collection.Set[K] => + private[MapOps] val allKeys = MapOps.this.keysIterator.toSet \ No newline at end of file From 5c613919e9e1b2f3688c92213e1fc2b12abe5da9 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 19:10:55 +0200 Subject: [PATCH 8/9] Issue a warning when a global value is declared with a capture set --- .../src/dotty/tools/dotc/cc/CheckCaptures.scala | 13 ++++++++++++- tests/neg-custom-args/captures/class-caps.check | 6 ++++++ .../captures/nonsensical-for-pure.check | 6 ++++++ tests/pos-custom-args/captures/i23421.scala | 2 +- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index a7ecf18f387e..86b34d302144 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1019,9 +1019,20 @@ class CheckCaptures extends Recheck, SymTransformer: // function is compiled since we do not propagate expected types into blocks. interpolateIfInferred(tree.tpt, sym) + def declaredCaptures = tree.tpt.nuType.captureSet if runInConstructor && savedEnv.owner.isClass then curEnv = savedEnv - markFree(tree.tpt.nuType.captureSet, tree, addUseInfo = false) + markFree(declaredCaptures, tree, addUseInfo = false) + + if sym.owner.isStaticOwner && !declaredCaptures.elems.isEmpty then + def where = + if sym.effectiveOwner.is(Package) then "top-level definition" + else i"member of static ${sym.owner}" + report.warning( + em"""$sym has a non-empty capture set but will not be added as + |a capability to computed capture sets since it is globally accessible + |as a $where. Global values cannot be capabilities.""", + tree.namePos) end recheckValDef /** Recheck method definitions: diff --git a/tests/neg-custom-args/captures/class-caps.check b/tests/neg-custom-args/captures/class-caps.check index ef6462eefbb3..362eee3e6170 100644 --- a/tests/neg-custom-args/captures/class-caps.check +++ b/tests/neg-custom-args/captures/class-caps.check @@ -20,3 +20,9 @@ 30 | a + b | | longer explanation available when compiling with `-explain` +-- Warning: tests/neg-custom-args/captures/class-caps.scala:35:6 ------------------------------------------------------- +35 | val console: Console^ = Console() // provide capability locally + | ^^^^^^^ + | value console has a non-empty capture set but will not be added as + | a capability to computed capture sets since it is globally accessible + | as a member of static object Test2. Global values cannot be capabilities. diff --git a/tests/neg-custom-args/captures/nonsensical-for-pure.check b/tests/neg-custom-args/captures/nonsensical-for-pure.check index 6860446c4177..2df943df4262 100644 --- a/tests/neg-custom-args/captures/nonsensical-for-pure.check +++ b/tests/neg-custom-args/captures/nonsensical-for-pure.check @@ -2,3 +2,9 @@ 1 |val x: Int^ = 2 // error | ^^^^ | Int is a pure type, it makes no sense to add a capture set to it +-- Warning: tests/neg-custom-args/captures/nonsensical-for-pure.scala:1:4 ---------------------------------------------- +1 |val x: Int^ = 2 // error + | ^ + | value x has a non-empty capture set but will not be added as + | a capability to computed capture sets since it is globally accessible + | as a top-level definition. Global values cannot be capabilities. diff --git a/tests/pos-custom-args/captures/i23421.scala b/tests/pos-custom-args/captures/i23421.scala index ef5e7564073e..87458e6faeac 100644 --- a/tests/pos-custom-args/captures/i23421.scala +++ b/tests/pos-custom-args/captures/i23421.scala @@ -12,5 +12,5 @@ object Collection: trait Foo: val thunks: Collection[() => Unit] // that's fine -object FooImpl1 extends Foo: +class FooImpl1 extends Foo: val thunks: Collection[() => Unit] = Collection.empty // was error, now ok From 4e8f7d9eac84230f5aa89b3df47e863b294b0cb3 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 4 Sep 2025 22:46:59 +0200 Subject: [PATCH 9/9] No warning for caps.cap --- compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 86b34d302144..21567d694bc9 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1024,7 +1024,7 @@ class CheckCaptures extends Recheck, SymTransformer: curEnv = savedEnv markFree(declaredCaptures, tree, addUseInfo = false) - if sym.owner.isStaticOwner && !declaredCaptures.elems.isEmpty then + if sym.owner.isStaticOwner && !declaredCaptures.elems.isEmpty && sym != defn.captureRoot then def where = if sym.effectiveOwner.is(Package) then "top-level definition" else i"member of static ${sym.owner}"