From 7c233bc18d20c985a439ffaa6179fe285caed089 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 4 Nov 2025 16:34:07 +0100 Subject: [PATCH 1/4] Don't prematurely force info of currently defined fields with inferred types Don't prematurely force info of currently defined fields with inferred types when computing captureSetImpliedByFields. Fixes #24335 --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 2 +- compiler/src/dotty/tools/dotc/cc/Setup.scala | 134 ++++++++++-------- tests/neg-custom-args/captures/i24335.check | 9 ++ tests/neg-custom-args/captures/i24335.scala | 9 ++ 4 files changed, 94 insertions(+), 60 deletions(-) create mode 100644 tests/neg-custom-args/captures/i24335.check create mode 100644 tests/neg-custom-args/captures/i24335.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 8ca321b49ee6..9a83f6c22c4e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -966,7 +966,7 @@ class CheckCaptures extends Recheck, SymTransformer: case cls: ClassSymbol => var fieldClassifiers = for - sym <- cls.info.decls.toList + sym <- setup.fieldsWithExplicitTypes.getOrElse(cls, cls.info.decls.toList) if contributesFreshToClass(sym) case fresh: FreshCap <- sym.info.spanCaptureSet.elems .filter(_.isTerminalCapability) diff --git a/compiler/src/dotty/tools/dotc/cc/Setup.scala b/compiler/src/dotty/tools/dotc/cc/Setup.scala index 38b60744c6f7..9c5ab335d99b 100644 --- a/compiler/src/dotty/tools/dotc/cc/Setup.scala +++ b/compiler/src/dotty/tools/dotc/cc/Setup.scala @@ -40,6 +40,14 @@ trait SetupAPI: /** Check to do after the capture checking traversal */ def postCheck()(using Context): Unit + /** A map from currently compiled class symbols to those of their fields + * that have an explicit type given. Used in `captureSetImpliedByFields` + * to avoid forcing fields with inferred types prematurely. The test file + * where this matters is i24335.scala. The precise failure scenario which + * this avoids is described in #24335. + */ + def fieldsWithExplicitTypes: collection.Map[ClassSymbol, List[Symbol]] + /** Used for error reporting: * Maps mutable variables to the symbols that capture them (in the * CheckCaptures sense, i.e. symbol is referred to from a different method @@ -52,6 +60,7 @@ trait SetupAPI: * the function that is called. */ def anonFunCallee: collection.Map[Symbol, Symbol] + end SetupAPI object Setup: @@ -489,6 +498,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: extension (sym: Symbol) def nextInfo(using Context): Type = atPhase(thisPhase.next)(sym.info) + val fieldsWithExplicitTypes: mutable.HashMap[ClassSymbol, List[Symbol]] = mutable.HashMap() + + val capturedBy: mutable.HashMap[Symbol, Symbol] = mutable.HashMap() + + val anonFunCallee: mutable.HashMap[Symbol, Symbol] = mutable.HashMap() + /** A traverser that adds knownTypes and updates symbol infos */ def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts: import checker.* @@ -693,59 +708,65 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: case tree: Bind => val sym = tree.symbol updateInfo(sym, transformInferredType(sym.info), sym.owner) - case tree: TypeDef => - tree.symbol match - case cls: ClassSymbol => - checkClassifiedInheritance(cls) - val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo - - // Compute new self type - def isInnerModule = cls.is(ModuleClass) && !cls.isStatic - val selfInfo1 = - if (selfInfo ne NoType) && !isInnerModule then - // if selfInfo is explicitly given then use that one, except if - // self info applies to non-static modules, these still need to be inferred - selfInfo - else if cls.isPureClass then - // is cls is known to be pure, nothing needs to be added to self type - selfInfo - else if !cls.isEffectivelySealed && !cls.baseClassHasExplicitNonUniversalSelfType then - // assume {cap} for completely unconstrained self types of publicly extensible classes - CapturingType(cinfo.selfType, CaptureSet.universal) - else - // Infer the self type for the rest, which is all classes without explicit - // self types (to which we also add nested module classes), provided they are - // neither pure, nor are publicily extensible with an unconstrained self type. - val cs = CaptureSet.ProperVar(cls, CaptureSet.emptyRefs, nestedOK = false, isRefining = false) - if cls.derivesFrom(defn.Caps_Capability) then - // If cls is a capability class, we need to add a fresh capability to ensure - // we cannot treat the class as pure. - CaptureSet.fresh(cls, cls.thisType, Origin.InDecl(cls)).subCaptures(cs) - CapturingType(cinfo.selfType, cs) - - // Compute new parent types - val ps1 = inContext(ctx.withOwner(cls)): - ps.mapConserve(transformExplicitType(_, NoSymbol, freshen = false)) - - // Install new types and if it is a module class also update module object - if (selfInfo1 ne selfInfo) || (ps1 ne ps) then - val newInfo = ClassInfo(prefix, cls, ps1, decls, selfInfo1) - updateInfo(cls, newInfo, cls.owner) - capt.println(i"update class info of $cls with parents $ps selfinfo $selfInfo to $newInfo") - cls.thisType.asInstanceOf[ThisType].invalidateCaches() - if cls.is(ModuleClass) then - // if it's a module, the capture set of the module reference is the capture set of the self type - val modul = cls.sourceModule - val selfCaptures = selfInfo1 match - case CapturingType(_, refs) => refs - case _ => CaptureSet.empty - // Note: Can't do val selfCaptures = selfInfo1.captureSet here. - // This would potentially give stackoverflows when setup is run repeatedly. - // One test case is pos-custom-args/captures/checkbounds.scala under - // ccConfig.alwaysRepeatRun = true. - updateInfo(modul, CapturingType(modul.info, selfCaptures), modul.owner) - modul.termRef.invalidateCaches() - case _ => + case tree @ TypeDef(_, impl: Template) => + val cls: ClassSymbol = tree.symbol.asClass + + fieldsWithExplicitTypes(cls) = + for + case vd @ ValDef(_, tpt: TypeTree, _) <- impl.body + if !tpt.isInferred && vd.symbol.exists && !vd.symbol.is(NonMember) + yield + vd.symbol + + checkClassifiedInheritance(cls) + val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo + + // Compute new self type + def isInnerModule = cls.is(ModuleClass) && !cls.isStatic + val selfInfo1 = + if (selfInfo ne NoType) && !isInnerModule then + // if selfInfo is explicitly given then use that one, except if + // self info applies to non-static modules, these still need to be inferred + selfInfo + else if cls.isPureClass then + // is cls is known to be pure, nothing needs to be added to self type + selfInfo + else if !cls.isEffectivelySealed && !cls.baseClassHasExplicitNonUniversalSelfType then + // assume {cap} for completely unconstrained self types of publicly extensible classes + CapturingType(cinfo.selfType, CaptureSet.universal) + else + // Infer the self type for the rest, which is all classes without explicit + // self types (to which we also add nested module classes), provided they are + // neither pure, nor are publicily extensible with an unconstrained self type. + val cs = CaptureSet.ProperVar(cls, CaptureSet.emptyRefs, nestedOK = false, isRefining = false) + if cls.derivesFrom(defn.Caps_Capability) then + // If cls is a capability class, we need to add a fresh capability to ensure + // we cannot treat the class as pure. + CaptureSet.fresh(cls, cls.thisType, Origin.InDecl(cls)).subCaptures(cs) + CapturingType(cinfo.selfType, cs) + + // Compute new parent types + val ps1 = inContext(ctx.withOwner(cls)): + ps.mapConserve(transformExplicitType(_, NoSymbol, freshen = false)) + + // Install new types and if it is a module class also update module object + if (selfInfo1 ne selfInfo) || (ps1 ne ps) then + val newInfo = ClassInfo(prefix, cls, ps1, decls, selfInfo1) + updateInfo(cls, newInfo, cls.owner) + capt.println(i"update class info of $cls with parents $ps selfinfo $selfInfo to $newInfo") + cls.thisType.asInstanceOf[ThisType].invalidateCaches() + if cls.is(ModuleClass) then + // if it's a module, the capture set of the module reference is the capture set of the self type + val modul = cls.sourceModule + val selfCaptures = selfInfo1 match + case CapturingType(_, refs) => refs + case _ => CaptureSet.empty + // Note: Can't do val selfCaptures = selfInfo1.captureSet here. + // This would potentially give stackoverflows when setup is run repeatedly. + // One test case is pos-custom-args/captures/checkbounds.scala under + // ccConfig.alwaysRepeatRun = true. + updateInfo(modul, CapturingType(modul.info, selfCaptures), modul.owner) + modul.termRef.invalidateCaches() case _ => end postProcess @@ -918,16 +939,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI: else t case _ => mapFollowingAliases(t) - val capturedBy: mutable.HashMap[Symbol, Symbol] = mutable.HashMap[Symbol, Symbol]() - - val anonFunCallee: mutable.HashMap[Symbol, Symbol] = mutable.HashMap[Symbol, Symbol]() - /** Run setup on a compilation unit with given `tree`. * @param recheckDef the function to run for completing a val or def */ def setupUnit(tree: Tree, checker: CheckerAPI)(using Context): Unit = - inContext(ctx.withPhase(thisPhase)): - setupTraverser(checker).traverse(tree) + setupTraverser(checker).traverse(tree)(using ctx.withPhase(thisPhase)) // ------ Checks to run at Setup ---------------------------------------- diff --git a/tests/neg-custom-args/captures/i24335.check b/tests/neg-custom-args/captures/i24335.check new file mode 100644 index 000000000000..bea699ef67f1 --- /dev/null +++ b/tests/neg-custom-args/captures/i24335.check @@ -0,0 +1,9 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i24335.scala:7:22 ---------------------------------------- +7 | val _: () -> Unit = l1 // error + | ^^ + | Found: (C.this.l1 : () ->{C.this.c.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.c.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i24335.scala b/tests/neg-custom-args/captures/i24335.scala new file mode 100644 index 000000000000..1a0e9d96e977 --- /dev/null +++ b/tests/neg-custom-args/captures/i24335.scala @@ -0,0 +1,9 @@ +class IO extends caps.SharedCapability: + def write(): Unit = () + +class C(val io: IO): + val c = C(io) + val l1 = () => c.io.write() + val _: () -> Unit = l1 // error + + From f6875d25a09915cca23d75378333e32b07165780 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Nov 2025 10:55:57 +0100 Subject: [PATCH 2/4] Generalize inferred types check We now need to also check that fields with inferred types would not contribute a fresh capability to the implied capture set of the class. --- .../src/dotty/tools/dotc/cc/Capability.scala | 13 ++- .../src/dotty/tools/dotc/cc/CaptureOps.scala | 2 +- .../dotty/tools/dotc/cc/CapturingType.scala | 2 +- .../dotty/tools/dotc/cc/CheckCaptures.scala | 80 ++++++++++++------- .../captures/check-inferred.check | 42 ++++++++-- .../captures/check-inferred.scala | 22 ++++- tests/neg-custom-args/captures/i24335.check | 8 ++ tests/neg-custom-args/captures/i24335.scala | 9 +++ 8 files changed, 132 insertions(+), 46 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/Capability.scala b/compiler/src/dotty/tools/dotc/cc/Capability.scala index 64ca777f1b76..cc6e44d716aa 100644 --- a/compiler/src/dotty/tools/dotc/cc/Capability.scala +++ b/compiler/src/dotty/tools/dotc/cc/Capability.scala @@ -378,10 +378,15 @@ object Capabilities: case tp: SetCapability => tp.captureSetOfInfo.isReadOnly case _ => this ne stripReadOnly - final def restriction(using Context): Symbol = this match + /** The classifier, either given in an explicit `.only` or assumed for a + * FreshCap. AnyRef for unclassified FreshCaps. Otherwise NoSymbol if no + * classifier is given. + */ + final def classifier(using Context): Symbol = this match case Restricted(_, cls) => cls - case ReadOnly(ref1) => ref1.restriction - case Maybe(ref1) => ref1.restriction + case ReadOnly(ref1) => ref1.classifier + case Maybe(ref1) => ref1.classifier + case self: FreshCap => self.hiddenSet.classifier case _ => NoSymbol /** Is this a reach reference of the form `x*` or a readOnly or maybe variant @@ -617,7 +622,7 @@ object Capabilities: case Reach(_) => captureSetOfInfo.transClassifiers case self: CoreCapability => - if self.derivesFromCapability then toClassifiers(self.classifier) + if self.derivesFromCapability then toClassifiers(self.inheritedClassifier) else captureSetOfInfo.transClassifiers if myClassifiers != UnknownClassifier then classifiersValid == currentId diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala index 125569c16033..66d9ef49c876 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureOps.scala @@ -487,7 +487,7 @@ extension (tp: Type) case _ => tp - def classifier(using Context): ClassSymbol = + def inheritedClassifier(using Context): ClassSymbol = tp.classSymbols.map(_.classifier).foldLeft(defn.AnyClass)(leastClassifier) extension (tp: MethodType) diff --git a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala index 415fc094923f..de694b38f11e 100644 --- a/compiler/src/dotty/tools/dotc/cc/CapturingType.scala +++ b/compiler/src/dotty/tools/dotc/cc/CapturingType.scala @@ -40,7 +40,7 @@ object CapturingType: apply(parent1, refs ++ refs1, boxed) case _ => if parent.derivesFromMutable then refs.associateWithMutable() - refs.adoptClassifier(parent.classifier) + refs.adoptClassifier(parent.inheritedClassifier) AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot)) /** An extractor for CapturingTypes. Capturing types are recognized if diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 9a83f6c22c4e..a5501b4fe711 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -947,6 +947,17 @@ class CheckCaptures extends Recheck, SymTransformer: .showing(i"constr type $mt with $argTypes%, % in $constr = $result", capt) end refineConstructorInstance + /** If `mbr` is a field that has (possibly restricted) FreshCaps in its span capture set, + * their classifiers, otherwise the empty list. + */ + private def classifiersOfFreshInType(mbr: Symbol)(using Context): List[ClassSymbol] = + if contributesFreshToClass(mbr) then + mbr.info.spanCaptureSet.elems + .filter(_.isTerminalCapability) + .toList + .map(_.classifier.asClass) + else Nil + /** The additional capture set implied by the capture sets of its fields. This * is either empty or, if some fields have a terminal capability in their span * capture sets, it consists of a single fresh cap that subsumes all these terminal @@ -964,16 +975,9 @@ class CheckCaptures extends Recheck, SymTransformer: */ def impliedClassifiers(cls: Symbol): List[ClassSymbol] = cls match case cls: ClassSymbol => - var fieldClassifiers = - for - sym <- setup.fieldsWithExplicitTypes.getOrElse(cls, cls.info.decls.toList) - if contributesFreshToClass(sym) - case fresh: FreshCap <- sym.info.spanCaptureSet.elems - .filter(_.isTerminalCapability) - .map(_.stripReadOnly) - .toList - _ = pushInfo(i"Note: ${sym.showLocated} captures a $fresh") - yield fresh.hiddenSet.classifier + var fieldClassifiers = setup.fieldsWithExplicitTypes // pick fields with explicit types for classes in this compilation unit + .getOrElse(cls, cls.info.decls.toList) // pick all symbols in class scope for other classes + .flatMap(classifiersOfFreshInType) if cls.typeRef.isMutableType then fieldClassifiers = defn.Caps_Mutable :: fieldClassifiers val parentClassifiers = @@ -1227,11 +1231,20 @@ class CheckCaptures extends Recheck, SymTransformer: curEnv = saved end recheckDefDef - /** If val or def definition with inferred (result) type is visible - * in other compilation units, check that the actual inferred type - * conforms to the expected type where all inferred capture sets are dropped. - * This ensures that if files compile separately, they will also compile - * in a joint compilation. + /** Two tests for member definitions with inferred types: + * + * 1. If val or def definition with inferred (result) type is visible + * in other compilation units, check that the actual inferred type + * conforms to the expected type where all inferred capture sets are dropped. + * This ensures that if files compile separately, they will also compile + * in a joint compilation. + * 2. If a val has an inferred type with a terminal capability in its span capset, + * check that it this capability is subsumed by the capset that was inferred + * for the class from its other fields via `captureSetImpliedByFields`. + * That capset is defined to take into account all fields but is computed + * only from fields with explicitly given types in order to avoid cycles. + * See comment on Setup.fieldsWithExplicitTypes. So we have to make sure + * that fields with inferred types would not change that capset. */ def checkInferredResult(tp: Type, tree: ValOrDefDef)(using Context): Type = val sym = tree.symbol @@ -1266,11 +1279,17 @@ class CheckCaptures extends Recheck, SymTransformer: |The new inferred type $tp |must conform to this type.""" + def covers(classCapset: CaptureSet, fieldClassifiers: List[ClassSymbol]): Boolean = + fieldClassifiers.forall: cls => + classCapset.elems.exists: + case fresh: FreshCap => cls.isSubClass(fresh.hiddenSet.classifier) + case _ => false + tree.tpt match - case tpt: InferredTypeTree if !isExemptFromChecks => - if !sym.isLocalToCompilationUnit - // Symbols that can't be seen outside the compilation unit can have inferred types - // except for the else clause below. + case tpt: InferredTypeTree => + // Test point (1) of doc comment above + if !sym.isLocalToCompilationUnit && !isExemptFromChecks + // Symbols that can't be seen outside the compilation unit can have inferred types then val expected = tpt.tpe.dropAllRetains todoAtPostCheck += { () => @@ -1279,18 +1298,21 @@ class CheckCaptures extends Recheck, SymTransformer: // The check that inferred <: expected is done after recheck so that it // does not interfere with normal rechecking by constraining capture set variables. } - else if sym.is(Private) - && !sym.isLocalToCompilationUnitIgnoringPrivate - && tree.tpt.nuType.spanCaptureSet.containsTerminalCapability + // Test point (2) of doc comment above + if sym.owner.isClass && !sym.owner.isStaticOwner && contributesFreshToClass(sym) - // Private symbols capturing a root capability need explicit types - // so that we can compute field constributions to class instance - // capture sets across compilation units. then - report.error( - em"""$sym needs an explicit type because it captures a root capability in its type ${tree.tpt.nuType}. - |Fields of publicily accessible classes that capture a root capability need to be given an explicit type.""", - tpt.srcPos) + todoAtPostCheck += { () => + val cls = sym.owner.asClass + val fieldClassifiers = classifiersOfFreshInType(sym) + val classCapset = captureSetImpliedByFields(cls, cls.appliedRef) + if !covers(classCapset, fieldClassifiers) then + report.error( + em"""$sym needs an explicit type because it captures a root capability in its type ${tree.tpt.nuType}. + |Fields capturing a root capability need to be given an explicit type unless the capability is already + |subsumed by the computed capability of the enclosing class.""", + tpt.srcPos) + } case _ => tp end checkInferredResult diff --git a/tests/neg-custom-args/captures/check-inferred.check b/tests/neg-custom-args/captures/check-inferred.check index 7e3f9022f5a5..45309a7edbc4 100644 --- a/tests/neg-custom-args/captures/check-inferred.check +++ b/tests/neg-custom-args/captures/check-inferred.check @@ -1,10 +1,3 @@ --- Error: tests/neg-custom-args/captures/check-inferred.scala:12:19 ---------------------------------------------------- -12 | private val count = Ref() // error - | ^ - | value count needs an explicit type because it captures a root capability in its type test.Ref^. - | Fields of publicily accessible classes that capture a root capability need to be given an explicit type. - | - | where: ^ refers to a fresh root capability classified as Mutable in the type of value count -- Error: tests/neg-custom-args/captures/check-inferred.scala:18:13 ---------------------------------------------------- 18 | val incr = () => // error | ^ @@ -24,7 +17,7 @@ | Externally visible type: () -> Unit 21 | count.put(count.get - 1) -- Error: tests/neg-custom-args/captures/check-inferred.scala:24:14 ---------------------------------------------------- -24 | val count = Ref(): Object^ // error +24 | val count = Ref(): Object^ // error // error | ^^^^^^^^^^^^^^ | value count needs an explicit type because the inferred type does not conform to | the type that is externally visible in other compilation units. @@ -33,3 +26,36 @@ | Externally visible type: Object | | where: ^ refers to a fresh root capability created in value count +-- Error: tests/neg-custom-args/captures/check-inferred.scala:24:11 ---------------------------------------------------- +24 | val count = Ref(): Object^ // error // error + | ^ + | value count needs an explicit type because it captures a root capability in its type Object^. + | Fields capturing a root capability need to be given an explicit type unless the capability is already + | subsumed by the computed capability of the enclosing class. + | + | where: ^ refers to a fresh root capability in the type of value count +-- Error: tests/neg-custom-args/captures/check-inferred.scala:45:15 ---------------------------------------------------- +45 | private val y = ??? : A^ // error + | ^ + | value y needs an explicit type because it captures a root capability in its type test.A^. + | Fields capturing a root capability need to be given an explicit type unless the capability is already + | subsumed by the computed capability of the enclosing class. + | + | where: ^ refers to a fresh root capability in the type of value y +-- Error: tests/neg-custom-args/captures/check-inferred.scala:49:15 ---------------------------------------------------- +49 | private val y = ??? : (() => A^{cap.only[caps.Read]}) // error + | ^ + | value y needs an explicit type because it captures a root capability in its type () => test.A^{cap.only[Read]}. + | Fields capturing a root capability need to be given an explicit type unless the capability is already + | subsumed by the computed capability of the enclosing class. + | + | where: => refers to a fresh root capability in the type of value y + | cap is a fresh root capability created in value y +-- Error: tests/neg-custom-args/captures/check-inferred.scala:29:21 ---------------------------------------------------- +29 | private val count = Ref() // error + | ^ + | value count needs an explicit type because it captures a root capability in its type test.Ref^. + | Fields capturing a root capability need to be given an explicit type unless the capability is already + | subsumed by the computed capability of the enclosing class. + | + | where: ^ refers to a fresh root capability classified as Mutable in the type of value count diff --git a/tests/neg-custom-args/captures/check-inferred.scala b/tests/neg-custom-args/captures/check-inferred.scala index 2462e4cb2e82..a520e8f64db8 100644 --- a/tests/neg-custom-args/captures/check-inferred.scala +++ b/tests/neg-custom-args/captures/check-inferred.scala @@ -9,7 +9,7 @@ class Ref extends Mutable: update def put(y: Int): Unit = x = y class Counter: - private val count = Ref() // error + private val count = Ref() private val altCount: Ref^ = Ref() // ok @untrackedCaptures @@ -21,14 +21,30 @@ class Counter: count.put(count.get - 1) trait CounterAPI: - val count = Ref(): Object^ // error + val count = Ref(): Object^ // error // error private def count2 = Ref() // ok def test() = class Counter: - private val count = Ref() // ok + private val count = Ref() // error val incr = () => count.put(count.get + 1) val decr = () => count.put(count.get - 1) +class A: + val x: A^{cap.only[caps.Control]} = ??? + private val y = ??? : A^{cap.only[caps.Control]} // ok + +class B: + val x: A^ = ??? + private val y = ??? : A^{cap.only[caps.Control]} // ok + +class C: + val x: A^{cap.only[caps.Control]} = ??? + private val y = ??? : A^ // error + +class D: + val x: A^{cap.only[caps.Control]} = ??? + private val y = ??? : (() => A^{cap.only[caps.Read]}) // error + diff --git a/tests/neg-custom-args/captures/i24335.check b/tests/neg-custom-args/captures/i24335.check index bea699ef67f1..685f96a18661 100644 --- a/tests/neg-custom-args/captures/i24335.check +++ b/tests/neg-custom-args/captures/i24335.check @@ -7,3 +7,11 @@ | Note that capability C.this.c.io is not included in capture set {}. | | longer explanation available when compiling with `-explain` +-- Error: tests/neg-custom-args/captures/i24335.scala:13:7 ------------------------------------------------------------- +13 | val r = Ref() // error + | ^ + | value r needs an explicit type because it captures a root capability in its type Ref^. + | Fields capturing a root capability need to be given an explicit type unless the capability is already + | subsumed by the computed capability of the enclosing class. + | + | where: ^ refers to a fresh root capability classified as Mutable in the type of value r diff --git a/tests/neg-custom-args/captures/i24335.scala b/tests/neg-custom-args/captures/i24335.scala index 1a0e9d96e977..5af9a3fdac50 100644 --- a/tests/neg-custom-args/captures/i24335.scala +++ b/tests/neg-custom-args/captures/i24335.scala @@ -6,4 +6,13 @@ class C(val io: IO): val l1 = () => c.io.write() val _: () -> Unit = l1 // error +class Ref extends caps.Mutable: + var x: Int = 0 + +class D: + val r = Ref() // error + +def test = + val d = D() + val _: D^{} = d From 4de706b2d3129dedb0aed8b21413fccece0a42a2 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 5 Nov 2025 16:35:33 +0100 Subject: [PATCH 3/4] Add lazyvals test that started the problem --- .../neg-custom-args/captures/lazyvals6.check | 99 +++++++++++++++++++ .../neg-custom-args/captures/lazyvals6.scala | 56 +++++++++++ 2 files changed, 155 insertions(+) create mode 100644 tests/neg-custom-args/captures/lazyvals6.check create mode 100644 tests/neg-custom-args/captures/lazyvals6.scala diff --git a/tests/neg-custom-args/captures/lazyvals6.check b/tests/neg-custom-args/captures/lazyvals6.check new file mode 100644 index 000000000000..1ce7f1f5515f --- /dev/null +++ b/tests/neg-custom-args/captures/lazyvals6.check @@ -0,0 +1,99 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:7:22 ------------------------------------- +7 | val _: () -> Unit = x // error + | ^ + | Found: (C.this.x : () ->{C.this.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:12:22 ------------------------------------ +12 | val _: () -> Unit = l1 // error + | ^^ + | Found: (C.this.l1 : () ->{C.this.c.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.c.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:16:22 ------------------------------------ +16 | val _: () -> Unit = l2 // error + | ^^ + | Found: (C.this.l2 : () ->{C.this.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:20:22 ------------------------------------ +20 | val _: () -> Unit = s1 // error + | ^^ + | Found: (C.this.s1 : () ->{C.this.c.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.c.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:24:22 ------------------------------------ +24 | val _: () -> Unit = s2 // error + | ^^ + | Found: (C.this.s2 : () ->{C.this.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:32:22 ------------------------------------ +32 | val _: () -> Unit = l11 // error + | ^^^ + | Found: (C.this.l11 : () ->{C.this} Unit) + | Required: () -> Unit + | + | Note that capability C.this is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:36:22 ------------------------------------ +36 | val _: () -> Unit = l12 // error + | ^^^ + | Found: (C.this.l12 : () ->{C.this.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:41:22 ------------------------------------ +41 | val _: () -> Unit = s11 // error + | ^^^ + | Found: (C.this.s11 : () ->{C.this} Unit) + | Required: () -> Unit + | + | Note that capability C.this is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:45:22 ------------------------------------ +45 | val _: () -> Unit = s12 // error + | ^^^ + | Found: (C.this.s12 : () ->{C.this.io} Unit) + | Required: () -> Unit + | + | Note that capability C.this.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:50:22 ------------------------------------ +50 | val _: () -> Unit = x // error + | ^ + | Found: (x : () ->{io} Unit) + | Required: () -> Unit + | + | Note that capability io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/lazyvals6.scala:55:22 ------------------------------------ +55 | val _: () -> Unit = z // error + | ^ + | Found: (z : () ->{c.io} Unit) + | Required: () -> Unit + | + | Note that capability c.io is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/lazyvals6.scala b/tests/neg-custom-args/captures/lazyvals6.scala new file mode 100644 index 000000000000..6b2eaaabbe57 --- /dev/null +++ b/tests/neg-custom-args/captures/lazyvals6.scala @@ -0,0 +1,56 @@ +class IO extends caps.SharedCapability: + def write(): Unit = () + +class C(val io: IO): + lazy val x = () => io.write() + val y: () ->{io} Unit = x + val _: () -> Unit = x // error + + val c = C(io) + lazy val l1 = () => c.io.write() + val _: () ->{c.io} Unit = l1 + val _: () -> Unit = l1 // error + + lazy val l2 = () => this.io.write() + val _: () ->{this.io} Unit = l2 + val _: () -> Unit = l2 // error + + val s1 = () => c.io.write() + val _: () ->{c.io} Unit = s1 + val _: () -> Unit = s1 // error + + val s2 = () => this.io.write() + val _: () ->{this.io} Unit = s2 + val _: () -> Unit = s2 // error + + lazy val d = C(io) + lazy val l11 = () => d.io.write() + val _: () ->{this} Unit = l11 + // it's just `this`, not `this.d`, since we also have to account for the fact + // that `d` might be initialized here, so we have to charge its call captures + // which are approximated by `this`. + val _: () -> Unit = l11 // error + + lazy val l12 = () => this.io.write() + val _: () ->{this.io} Unit = l12 + val _: () -> Unit = l12 // error + + val s11 = () => d.io.write() + val _: () ->{this} Unit = s11 + // just `this`` not `this.d`, same reason as above + val _: () -> Unit = s11 // error + + val s12 = () => this.io.write() + val _: () ->{this.io} Unit = s1 + val _: () -> Unit = s12 // error + +def test(io: IO) = + lazy val x = () => io.write() + val y: () ->{io} Unit = x + val _: () -> Unit = x // error + + val c = C(io) + lazy val z = () => c.io.write() + val _: () -> {c.io} Unit = z + val _: () -> Unit = z // error + From 9eddfe2f101bbebe17d5bdb6b90f483a9480da05 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 6 Nov 2025 20:58:04 +0100 Subject: [PATCH 4/4] Update compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Oliver Bračevac --- 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 a5501b4fe711..48c7d2e63290 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1239,7 +1239,7 @@ class CheckCaptures extends Recheck, SymTransformer: * This ensures that if files compile separately, they will also compile * in a joint compilation. * 2. If a val has an inferred type with a terminal capability in its span capset, - * check that it this capability is subsumed by the capset that was inferred + * check that this capability is subsumed by the capset that was inferred * for the class from its other fields via `captureSetImpliedByFields`. * That capset is defined to take into account all fields but is computed * only from fields with explicitly given types in order to avoid cycles.