Skip to content

Commit 5d43d02

Browse files
authored
Don't prematurely force info of fields with inferred types (#24336)
Don't prematurely force info of currently defined fields with inferred types when computing captureSetImpliedByFields. Fixes #24335
2 parents d47c0c6 + 0ad5aa0 commit 5d43d02

File tree

11 files changed

+379
-105
lines changed

11 files changed

+379
-105
lines changed

compiler/src/dotty/tools/dotc/cc/Capability.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,14 @@ object Capabilities:
375375
case tp: SetCapability => tp.captureSetOfInfo.isReadOnly
376376
case _ => this ne stripReadOnly
377377

378-
/** The restriction of this capability or NoSymbol if there is none */
379-
final def restriction(using Context): Symbol = this match
378+
/** The classifier, either given in an explicit `.only` or assumed for a
379+
* FreshCap. AnyRef for unclassified FreshCaps. Otherwise NoSymbol if no
380+
* classifier is given.
381+
*/
382+
final def classifier(using Context): Symbol = this match
380383
case Restricted(_, cls) => cls
381-
case Maybe(ref1) => ref1.restriction
384+
case Maybe(ref1) => ref1.classifier
385+
case self: FreshCap => self.hiddenSet.classifier
382386
case _ => NoSymbol
383387

384388
/** Is this a reach reference of the form `x*` or a readOnly or maybe variant
@@ -609,7 +613,7 @@ object Capabilities:
609613
case Reach(_) =>
610614
captureSetOfInfo.transClassifiers
611615
case self: CoreCapability =>
612-
if self.derivesFromCapability then toClassifiers(self.classifier)
616+
if self.derivesFromCapability then toClassifiers(self.inheritedClassifier)
613617
else captureSetOfInfo.transClassifiers
614618
if myClassifiers != UnknownClassifier then
615619
classifiersValid == currentId

compiler/src/dotty/tools/dotc/cc/CaptureOps.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ extension (tp: Type)
488488
case _ =>
489489
tp
490490

491-
def classifier(using Context): ClassSymbol =
491+
def inheritedClassifier(using Context): ClassSymbol =
492492
tp.classSymbols.map(_.classifier).foldLeft(defn.AnyClass)(leastClassifier)
493493

494494
extension (tp: MethodType)

compiler/src/dotty/tools/dotc/cc/CapturingType.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ object CapturingType:
4040
apply(parent1, refs ++ refs1, boxed)
4141
case _ =>
4242
if parent.derivesFromMutable then refs.associateWithMutable()
43-
refs.adoptClassifier(parent.classifier)
43+
refs.adoptClassifier(parent.inheritedClassifier)
4444
AnnotatedType(parent, CaptureAnnotation(refs, boxed)(defn.RetainsAnnot))
4545

4646
/** An extractor for CapturingTypes. Capturing types are recognized if

compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,17 @@ class CheckCaptures extends Recheck, SymTransformer:
945945
.showing(i"constr type $mt with $argTypes%, % in $constr = $result", capt)
946946
end refineConstructorInstance
947947

948+
/** If `mbr` is a field that has (possibly restricted) FreshCaps in its span capture set,
949+
* their classifiers, otherwise the empty list.
950+
*/
951+
private def classifiersOfFreshInType(mbr: Symbol)(using Context): List[ClassSymbol] =
952+
if contributesFreshToClass(mbr) then
953+
mbr.info.spanCaptureSet.elems
954+
.filter(_.isTerminalCapability)
955+
.toList
956+
.map(_.classifier.asClass)
957+
else Nil
958+
948959
/** The additional capture set implied by the capture sets of its fields. This
949960
* is either empty or, if some fields have a terminal capability in their span
950961
* capture sets, it consists of a single fresh cap that subsumes all these terminal
@@ -962,16 +973,9 @@ class CheckCaptures extends Recheck, SymTransformer:
962973
*/
963974
def impliedClassifiers(cls: Symbol): List[ClassSymbol] = cls match
964975
case cls: ClassSymbol =>
965-
var fieldClassifiers =
966-
for
967-
sym <- cls.info.decls.toList
968-
if contributesFreshToClass(sym)
969-
case fresh: FreshCap <- sym.info.spanCaptureSet.elems
970-
.filter(_.isTerminalCapability)
971-
.map(_.stripReadOnly)
972-
.toList
973-
_ = pushInfo(i"Note: ${sym.showLocated} captures a $fresh")
974-
yield fresh.hiddenSet.classifier
976+
var fieldClassifiers = setup.fieldsWithExplicitTypes // pick fields with explicit types for classes in this compilation unit
977+
.getOrElse(cls, cls.info.decls.toList) // pick all symbols in class scope for other classes
978+
.flatMap(classifiersOfFreshInType)
975979
if cls.typeRef.isMutableType then
976980
fieldClassifiers = defn.Caps_Mutable :: fieldClassifiers
977981
val parentClassifiers =
@@ -1225,11 +1229,20 @@ class CheckCaptures extends Recheck, SymTransformer:
12251229
curEnv = saved
12261230
end recheckDefDef
12271231

1228-
/** If val or def definition with inferred (result) type is visible
1229-
* in other compilation units, check that the actual inferred type
1230-
* conforms to the expected type where all inferred capture sets are dropped.
1231-
* This ensures that if files compile separately, they will also compile
1232-
* in a joint compilation.
1232+
/** Two tests for member definitions with inferred types:
1233+
*
1234+
* 1. If val or def definition with inferred (result) type is visible
1235+
* in other compilation units, check that the actual inferred type
1236+
* conforms to the expected type where all inferred capture sets are dropped.
1237+
* This ensures that if files compile separately, they will also compile
1238+
* in a joint compilation.
1239+
* 2. If a val has an inferred type with a terminal capability in its span capset,
1240+
* check that it this capability is subsumed by the capset that was inferred
1241+
* for the class from its other fields via `captureSetImpliedByFields`.
1242+
* That capset is defined to take into account all fields but is computed
1243+
* only from fields with explicitly given types in order to avoid cycles.
1244+
* See comment on Setup.fieldsWithExplicitTypes. So we have to make sure
1245+
* that fields with inferred types would not change that capset.
12331246
*/
12341247
def checkInferredResult(tp: Type, tree: ValOrDefDef)(using Context): Type =
12351248
val sym = tree.symbol
@@ -1264,11 +1277,17 @@ class CheckCaptures extends Recheck, SymTransformer:
12641277
|The new inferred type $tp
12651278
|must conform to this type."""
12661279

1280+
def covers(classCapset: CaptureSet, fieldClassifiers: List[ClassSymbol]): Boolean =
1281+
fieldClassifiers.forall: cls =>
1282+
classCapset.elems.exists:
1283+
case fresh: FreshCap => cls.isSubClass(fresh.hiddenSet.classifier)
1284+
case _ => false
1285+
12671286
tree.tpt match
1268-
case tpt: InferredTypeTree if !isExemptFromChecks =>
1269-
if !sym.isLocalToCompilationUnit
1270-
// Symbols that can't be seen outside the compilation unit can have inferred types
1271-
// except for the else clause below.
1287+
case tpt: InferredTypeTree =>
1288+
// Test point (1) of doc comment above
1289+
if !sym.isLocalToCompilationUnit && !isExemptFromChecks
1290+
// Symbols that can't be seen outside the compilation unit can have inferred types
12721291
then
12731292
val expected = tpt.tpe.dropAllRetains
12741293
todoAtPostCheck += { () =>
@@ -1277,18 +1296,21 @@ class CheckCaptures extends Recheck, SymTransformer:
12771296
// The check that inferred <: expected is done after recheck so that it
12781297
// does not interfere with normal rechecking by constraining capture set variables.
12791298
}
1280-
else if sym.is(Private)
1281-
&& !sym.isLocalToCompilationUnitIgnoringPrivate
1282-
&& tree.tpt.nuType.spanCaptureSet.containsTerminalCapability
1299+
// Test point (2) of doc comment above
1300+
if sym.owner.isClass && !sym.owner.isStaticOwner
12831301
&& contributesFreshToClass(sym)
1284-
// Private symbols capturing a root capability need explicit types
1285-
// so that we can compute field constributions to class instance
1286-
// capture sets across compilation units.
12871302
then
1288-
report.error(
1289-
em"""$sym needs an explicit type because it captures a root capability in its type ${tree.tpt.nuType}.
1290-
|Fields of publicily accessible classes that capture a root capability need to be given an explicit type.""",
1291-
tpt.srcPos)
1303+
todoAtPostCheck += { () =>
1304+
val cls = sym.owner.asClass
1305+
val fieldClassifiers = classifiersOfFreshInType(sym)
1306+
val classCapset = captureSetImpliedByFields(cls, cls.appliedRef)
1307+
if !covers(classCapset, fieldClassifiers) then
1308+
report.error(
1309+
em"""$sym needs an explicit type because it captures a root capability in its type ${tree.tpt.nuType}.
1310+
|Fields capturing a root capability need to be given an explicit type unless the capability is already
1311+
|subsumed by the computed capability of the enclosing class.""",
1312+
tpt.srcPos)
1313+
}
12921314
case _ =>
12931315
tp
12941316
end checkInferredResult

compiler/src/dotty/tools/dotc/cc/Setup.scala

Lines changed: 75 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ trait SetupAPI:
4040
/** Check to do after the capture checking traversal */
4141
def postCheck()(using Context): Unit
4242

43+
/** A map from currently compiled class symbols to those of their fields
44+
* that have an explicit type given. Used in `captureSetImpliedByFields`
45+
* to avoid forcing fields with inferred types prematurely. The test file
46+
* where this matters is i24335.scala. The precise failure scenario which
47+
* this avoids is described in #24335.
48+
*/
49+
def fieldsWithExplicitTypes: collection.Map[ClassSymbol, List[Symbol]]
50+
4351
/** Used for error reporting:
4452
* Maps mutable variables to the symbols that capture them (in the
4553
* CheckCaptures sense, i.e. symbol is referred to from a different method
@@ -52,6 +60,7 @@ trait SetupAPI:
5260
* the function that is called.
5361
*/
5462
def anonFunCallee: collection.Map[Symbol, Symbol]
63+
5564
end SetupAPI
5665

5766
object Setup:
@@ -489,6 +498,12 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
489498
extension (sym: Symbol) def nextInfo(using Context): Type =
490499
atPhase(thisPhase.next)(sym.info)
491500

501+
val fieldsWithExplicitTypes: mutable.HashMap[ClassSymbol, List[Symbol]] = mutable.HashMap()
502+
503+
val capturedBy: mutable.HashMap[Symbol, Symbol] = mutable.HashMap()
504+
505+
val anonFunCallee: mutable.HashMap[Symbol, Symbol] = mutable.HashMap()
506+
492507
/** A traverser that adds knownTypes and updates symbol infos */
493508
def setupTraverser(checker: CheckerAPI) = new TreeTraverserWithPreciseImportContexts:
494509
import checker.*
@@ -693,59 +708,65 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
693708
case tree: Bind =>
694709
val sym = tree.symbol
695710
updateInfo(sym, transformInferredType(sym.info), sym.owner)
696-
case tree: TypeDef =>
697-
tree.symbol match
698-
case cls: ClassSymbol =>
699-
checkClassifiedInheritance(cls)
700-
val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo
701-
702-
// Compute new self type
703-
def isInnerModule = cls.is(ModuleClass) && !cls.isStatic
704-
val selfInfo1 =
705-
if (selfInfo ne NoType) && !isInnerModule then
706-
// if selfInfo is explicitly given then use that one, except if
707-
// self info applies to non-static modules, these still need to be inferred
708-
selfInfo
709-
else if cls.isPureClass then
710-
// is cls is known to be pure, nothing needs to be added to self type
711-
selfInfo
712-
else if !cls.isEffectivelySealed && !cls.baseClassHasExplicitNonUniversalSelfType then
713-
// assume {cap} for completely unconstrained self types of publicly extensible classes
714-
CapturingType(cinfo.selfType, CaptureSet.universal)
715-
else
716-
// Infer the self type for the rest, which is all classes without explicit
717-
// self types (to which we also add nested module classes), provided they are
718-
// neither pure, nor are publicily extensible with an unconstrained self type.
719-
val cs = CaptureSet.ProperVar(cls, CaptureSet.emptyRefs, nestedOK = false, isRefining = false)
720-
if cls.derivesFrom(defn.Caps_Capability) then
721-
// If cls is a capability class, we need to add a fresh capability to ensure
722-
// we cannot treat the class as pure.
723-
CaptureSet.fresh(cls, cls.thisType, Origin.InDecl(cls)).subCaptures(cs)
724-
CapturingType(cinfo.selfType, cs)
725-
726-
// Compute new parent types
727-
val ps1 = inContext(ctx.withOwner(cls)):
728-
ps.mapConserve(transformExplicitType(_, NoSymbol, freshen = false))
729-
730-
// Install new types and if it is a module class also update module object
731-
if (selfInfo1 ne selfInfo) || (ps1 ne ps) then
732-
val newInfo = ClassInfo(prefix, cls, ps1, decls, selfInfo1)
733-
updateInfo(cls, newInfo, cls.owner)
734-
capt.println(i"update class info of $cls with parents $ps selfinfo $selfInfo to $newInfo")
735-
cls.thisType.asInstanceOf[ThisType].invalidateCaches()
736-
if cls.is(ModuleClass) then
737-
// if it's a module, the capture set of the module reference is the capture set of the self type
738-
val modul = cls.sourceModule
739-
val selfCaptures = selfInfo1 match
740-
case CapturingType(_, refs) => refs
741-
case _ => CaptureSet.empty
742-
// Note: Can't do val selfCaptures = selfInfo1.captureSet here.
743-
// This would potentially give stackoverflows when setup is run repeatedly.
744-
// One test case is pos-custom-args/captures/checkbounds.scala under
745-
// ccConfig.alwaysRepeatRun = true.
746-
updateInfo(modul, CapturingType(modul.info, selfCaptures), modul.owner)
747-
modul.termRef.invalidateCaches()
748-
case _ =>
711+
case tree @ TypeDef(_, impl: Template) =>
712+
val cls: ClassSymbol = tree.symbol.asClass
713+
714+
fieldsWithExplicitTypes(cls) =
715+
for
716+
case vd @ ValDef(_, tpt: TypeTree, _) <- impl.body
717+
if !tpt.isInferred && vd.symbol.exists && !vd.symbol.is(NonMember)
718+
yield
719+
vd.symbol
720+
721+
checkClassifiedInheritance(cls)
722+
val cinfo @ ClassInfo(prefix, _, ps, decls, selfInfo) = cls.classInfo
723+
724+
// Compute new self type
725+
def isInnerModule = cls.is(ModuleClass) && !cls.isStatic
726+
val selfInfo1 =
727+
if (selfInfo ne NoType) && !isInnerModule then
728+
// if selfInfo is explicitly given then use that one, except if
729+
// self info applies to non-static modules, these still need to be inferred
730+
selfInfo
731+
else if cls.isPureClass then
732+
// is cls is known to be pure, nothing needs to be added to self type
733+
selfInfo
734+
else if !cls.isEffectivelySealed && !cls.baseClassHasExplicitNonUniversalSelfType then
735+
// assume {cap} for completely unconstrained self types of publicly extensible classes
736+
CapturingType(cinfo.selfType, CaptureSet.universal)
737+
else
738+
// Infer the self type for the rest, which is all classes without explicit
739+
// self types (to which we also add nested module classes), provided they are
740+
// neither pure, nor are publicily extensible with an unconstrained self type.
741+
val cs = CaptureSet.ProperVar(cls, CaptureSet.emptyRefs, nestedOK = false, isRefining = false)
742+
if cls.derivesFrom(defn.Caps_Capability) then
743+
// If cls is a capability class, we need to add a fresh capability to ensure
744+
// we cannot treat the class as pure.
745+
CaptureSet.fresh(cls, cls.thisType, Origin.InDecl(cls)).subCaptures(cs)
746+
CapturingType(cinfo.selfType, cs)
747+
748+
// Compute new parent types
749+
val ps1 = inContext(ctx.withOwner(cls)):
750+
ps.mapConserve(transformExplicitType(_, NoSymbol, freshen = false))
751+
752+
// Install new types and if it is a module class also update module object
753+
if (selfInfo1 ne selfInfo) || (ps1 ne ps) then
754+
val newInfo = ClassInfo(prefix, cls, ps1, decls, selfInfo1)
755+
updateInfo(cls, newInfo, cls.owner)
756+
capt.println(i"update class info of $cls with parents $ps selfinfo $selfInfo to $newInfo")
757+
cls.thisType.asInstanceOf[ThisType].invalidateCaches()
758+
if cls.is(ModuleClass) then
759+
// if it's a module, the capture set of the module reference is the capture set of the self type
760+
val modul = cls.sourceModule
761+
val selfCaptures = selfInfo1 match
762+
case CapturingType(_, refs) => refs
763+
case _ => CaptureSet.empty
764+
// Note: Can't do val selfCaptures = selfInfo1.captureSet here.
765+
// This would potentially give stackoverflows when setup is run repeatedly.
766+
// One test case is pos-custom-args/captures/checkbounds.scala under
767+
// ccConfig.alwaysRepeatRun = true.
768+
updateInfo(modul, CapturingType(modul.info, selfCaptures), modul.owner)
769+
modul.termRef.invalidateCaches()
749770
case _ =>
750771
end postProcess
751772

@@ -918,16 +939,11 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
918939
else t
919940
case _ => mapFollowingAliases(t)
920941

921-
val capturedBy: mutable.HashMap[Symbol, Symbol] = mutable.HashMap[Symbol, Symbol]()
922-
923-
val anonFunCallee: mutable.HashMap[Symbol, Symbol] = mutable.HashMap[Symbol, Symbol]()
924-
925942
/** Run setup on a compilation unit with given `tree`.
926943
* @param recheckDef the function to run for completing a val or def
927944
*/
928945
def setupUnit(tree: Tree, checker: CheckerAPI)(using Context): Unit =
929-
inContext(ctx.withPhase(thisPhase)):
930-
setupTraverser(checker).traverse(tree)
946+
setupTraverser(checker).traverse(tree)(using ctx.withPhase(thisPhase))
931947

932948
// ------ Checks to run at Setup ----------------------------------------
933949

0 commit comments

Comments
 (0)