Skip to content

Commit 81da39a

Browse files
authored
Improve error messages for scope extrusion errors (#24318)
Several improvements for error messages signalling level errors. - Level errors now come first in the message, followed by the actual type mismatch - More information on owners - Merge ExistentialSubsumes notes with IncludeFailure notes. Also, several refactorings that simplify and unify the schemas for processing error notes.
2 parents 212d87e + 2ea4ecd commit 81da39a

40 files changed

+522
-272
lines changed

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package cc
55
import core.*
66
import Types.*, Symbols.*, Contexts.*, Decorators.*
77
import util.{SimpleIdentitySet, EqHashMap}
8-
import typer.ErrorReporting.Addenda
98
import util.common.alwaysTrue
109
import scala.collection.mutable
1110
import CCState.*
@@ -787,12 +786,9 @@ object Capabilities:
787786
&& prefixAllowsAddHidden
788787
&& vs.addHidden(x.hiddenSet, y)
789788
case x: ResultCap =>
790-
val result = y match
789+
y match
791790
case y: ResultCap => vs.unify(x, y)
792791
case _ => y.derivesFromShared
793-
if !result then
794-
TypeComparer.addErrorNote(CaptureSet.ExistentialSubsumesFailure(x, y))
795-
result
796792
case GlobalCap =>
797793
y match
798794
case GlobalCap => true
@@ -900,7 +896,7 @@ object Capabilities:
900896
case _ => c1
901897

902898
def showAsCapability(using Context) =
903-
i"capability ${ctx.printer.toTextCapability(this).show}"
899+
i"${ctx.printer.toTextCapability(this).show}"
904900

905901
def toText(printer: Printer): Text = printer.toTextCapability(this)
906902
end Capability

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import CaptureSet.VarState
1515
import Capabilities.*
1616
import StdNames.nme
1717
import config.Feature
18+
import dotty.tools.dotc.core.NameKinds.TryOwnerName
1819

1920
/** Attachment key for capturing type trees */
2021
private val Captures: Key[CaptureSet] = Key()
@@ -624,9 +625,13 @@ extension (sym: Symbol)
624625
|| sym.info.hasAnnotation(defn.ConsumeAnnot)
625626

626627
def qualString(prefix: String)(using Context): String =
628+
if !sym.exists then "" else i" $prefix ${sym.ownerString}"
629+
630+
def ownerString(using Context): String =
627631
if !sym.exists then ""
628-
else if sym.isAnonymousFunction then i" $prefix enclosing function"
629-
else i" $prefix $sym"
632+
else if sym.isAnonymousFunction then i"an enclosing function"
633+
else if sym.name.is(TryOwnerName) then i"an enclosing try expression"
634+
else sym.show
630635

631636
extension (tp: AnnotatedType)
632637
/** Is this a boxed capturing type? */

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

Lines changed: 87 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ import annotation.threadUnsafe
1010
import annotation.constructorOnly
1111
import annotation.internal.sharable
1212
import reporting.trace
13+
import reporting.Message.Note
1314
import printing.{Showable, Printer}
1415
import printing.Texts.*
1516
import util.{SimpleIdentitySet, Property, EqHashMap}
16-
import typer.ErrorReporting.Addenda
1717
import scala.collection.{mutable, immutable}
18-
import TypeComparer.ErrorNote
1918
import CCState.*
2019
import TypeOps.AvoidMap
2120
import compiletime.uninitialized
@@ -161,8 +160,8 @@ sealed abstract class CaptureSet extends Showable:
161160

162161
final def keepAlways: Boolean = this.isInstanceOf[EmptyWithProvenance]
163162

164-
def failWith(fail: TypeComparer.ErrorNote)(using Context): false =
165-
TypeComparer.addErrorNote(fail)
163+
def failWith(note: Note)(using Context): false =
164+
TypeComparer.addErrorNote(note)
166165
false
167166

168167
/** Try to include an element in this capture set.
@@ -1302,20 +1301,8 @@ object CaptureSet:
13021301
/** A TypeMap that is the identity on capabilities */
13031302
trait IdentityCaptRefMap extends TypeMap
13041303

1305-
/** A value of this class is produced and added as a note to ccState
1306-
* when a subsumes check decides that an existential variable `ex` cannot be
1307-
* instantiated to the other capability `other`.
1308-
*/
1309-
case class ExistentialSubsumesFailure(val ex: ResultCap, val other: Capability) extends ErrorNote:
1310-
def description(using Context): String =
1311-
def reason =
1312-
if other.isTerminalCapability then ""
1313-
else " since that capability is not a `Sharable` capability"
1314-
i"""the existential capture root in ${ex.originalBinder.resType}
1315-
|cannot subsume the capability $other$reason."""
1316-
13171304
/** Failure indicating that `elem` cannot be included in `cs` */
1318-
case class IncludeFailure(cs: CaptureSet, elem: Capability, levelError: Boolean = false) extends ErrorNote, Showable:
1305+
case class IncludeFailure(cs: CaptureSet, elem: Capability, levelError: Boolean = false) extends Note, Showable:
13191306
private var myTrace: List[CaptureSet] = cs :: Nil
13201307

13211308
def trace: List[CaptureSet] = myTrace
@@ -1324,36 +1311,77 @@ object CaptureSet:
13241311
res.myTrace = cs1 :: this.myTrace
13251312
res
13261313

1327-
def description(using Context): String =
1328-
def why =
1329-
val reasons = cs.elems.toList.collect:
1330-
case c: FreshCap if !c.acceptsLevelOf(elem) =>
1331-
i"$elem${elem.levelOwner.qualString("in")} is not visible from $c${c.ccOwner.qualString("in")}"
1332-
case c: FreshCap if !elem.tryClassifyAs(c.hiddenSet.classifier) =>
1333-
i"$c is classified as ${c.hiddenSet.classifier} but $elem is not"
1334-
if reasons.isEmpty then ""
1335-
else reasons.mkString("\nbecause ", "\nand ", "")
1336-
cs match
1337-
case cs: Var =>
1338-
if !cs.levelOK(elem) then
1339-
val levelStr = elem match
1340-
case ref: TermRef => i", defined in ${ref.symbol.maybeOwner}\n"
1341-
case _ => " "
1342-
i"""${elem.showAsCapability}${levelStr}cannot be included in outer capture set $cs"""
1343-
else if !elem.tryClassifyAs(cs.classifier) then
1344-
i"""${elem.showAsCapability} is not classified as ${cs.classifier}, therefore it
1345-
|cannot be included in capture set $cs of ${cs.classifier.name} elements"""
1346-
else if cs.isBadRoot(elem) then
1347-
elem match
1348-
case elem: FreshCap =>
1349-
i"""local ${elem.showAsCapability} created in ${elem.ccOwner}
1350-
|cannot be included in outer capture set $cs"""
1351-
case _ =>
1352-
i"universal ${elem.showAsCapability} cannot be included in capture set $cs"
1353-
else
1354-
i"${elem.showAsCapability} cannot be included in capture set $cs"
1355-
case _ =>
1356-
i"${elem.showAsCapability} is not included in capture set $cs$why"
1314+
override def showAsPrefix(using Context) = cs match
1315+
case cs: Var =>
1316+
!cs.levelOK(elem)
1317+
|| cs.isBadRoot(elem) && elem.isInstanceOf[FreshCap]
1318+
case _ =>
1319+
false
1320+
1321+
/** An include failure F1 covers another include failure F2 unless F2
1322+
* strictly subsumes F1, which means they describe the same capture sets
1323+
* and the element in F2 is more specific than the element in F1.
1324+
*/
1325+
override def covers(other: Note)(using Context) = other match
1326+
case other @ IncludeFailure(cs1, elem1, _) =>
1327+
val strictlySubsumes =
1328+
cs.elems == cs1.elems
1329+
&& elem1.singletonCaptureSet.mightSubcapture(elem.singletonCaptureSet)
1330+
!strictlySubsumes
1331+
case _ => false
1332+
1333+
def trailing(msg: String)(using Context): String =
1334+
i"""
1335+
|
1336+
|Note that $msg."""
1337+
1338+
def leading(msg: String)(using Context): String =
1339+
i"""$msg.
1340+
|The leakage occurred when trying to match the following types:
1341+
|
1342+
|"""
1343+
1344+
def render(using Context): String = cs match
1345+
case cs: Var =>
1346+
def ownerStr =
1347+
if !cs.description.isEmpty then "" else cs.owner.qualString("which is owned by")
1348+
if !cs.levelOK(elem) then
1349+
val outlivesStr = elem match
1350+
case ref: TermRef => i"${ref.symbol.maybeOwner.qualString("defined in")} outlives its scope:\n"
1351+
case _ => " outlives its scope: "
1352+
leading:
1353+
i"""Capability ${elem.showAsCapability}${outlivesStr}it leaks into outer capture set $cs$ownerStr"""
1354+
else if !elem.tryClassifyAs(cs.classifier) then
1355+
trailing:
1356+
i"""capability ${elem.showAsCapability} is not classified as ${cs.classifier}, therefore it
1357+
|cannot be included in capture set $cs of ${cs.classifier.name} elements"""
1358+
else if cs.isBadRoot(elem) then
1359+
elem match
1360+
case elem: FreshCap =>
1361+
leading:
1362+
i"""Local capability ${elem.showAsCapability} created in ${elem.ccOwner} outlives its scope:
1363+
|It leaks into outer capture set $cs$ownerStr"""
1364+
case _ =>
1365+
trailing:
1366+
i"universal capability ${elem.showAsCapability} cannot be included in capture set $cs"
1367+
else
1368+
trailing:
1369+
i"capability ${elem.showAsCapability} cannot be included in capture set $cs"
1370+
case _ =>
1371+
def why =
1372+
val reasons = cs.elems.toList.collect:
1373+
case c: FreshCap if !c.acceptsLevelOf(elem) =>
1374+
i"$elem${elem.levelOwner.qualString("in")} is not visible from $c${c.ccOwner.qualString("in")}"
1375+
case c: FreshCap if !elem.tryClassifyAs(c.hiddenSet.classifier) =>
1376+
i"$c is classified as ${c.hiddenSet.classifier} but ${elem.showAsCapability} is not"
1377+
case c: ResultCap if !c.subsumes(elem) =>
1378+
val toAdd = if elem.isTerminalCapability then "" else " since that capability is not a SharedCapability"
1379+
i"$c, which is existentially bound in ${c.originalBinder.resType}, cannot subsume ${elem.showAsCapability}$toAdd"
1380+
if reasons.isEmpty then ""
1381+
else reasons.mkString("\nbecause ", "\nand ", "")
1382+
1383+
trailing:
1384+
i"capability ${elem.showAsCapability} is not included in capture set $cs$why"
13571385

13581386
override def toText(printer: Printer): Text =
13591387
inContext(printer.printerContext):
@@ -1371,11 +1399,19 @@ object CaptureSet:
13711399
* @param lo the lower type of the orginal type comparison, or NoType if not known
13721400
* @param hi the upper type of the orginal type comparison, or NoType if not known
13731401
*/
1374-
case class MutAdaptFailure(cs: CaptureSet, lo: Type = NoType, hi: Type = NoType) extends ErrorNote:
1375-
def description(using Context): String =
1402+
case class MutAdaptFailure(cs: CaptureSet, lo: Type = NoType, hi: Type = NoType) extends Note:
1403+
1404+
def render(using Context): String =
13761405
def ofType(tp: Type) = if tp.exists then i"of the mutable type $tp" else "of a mutable type"
1377-
i"""$cs is an exclusive capture set ${ofType(hi)},
1378-
|it cannot subsume a read-only capture set ${ofType(lo)}"""
1406+
i"""
1407+
|
1408+
|Note that $cs is an exclusive capture set ${ofType(hi)},
1409+
|it cannot subsume a read-only capture set ${ofType(lo)}."""
1410+
1411+
// Show only one failure of this kind
1412+
override def covers(other: Note)(using Context) =
1413+
other.isInstanceOf[MutAdaptFailure]
1414+
end MutAdaptFailure
13791415

13801416
/** A VarState serves as a snapshot mechanism that can undo
13811417
* additions of elements or super sets if an operation fails

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

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@ import typer.ForceDegree
1414
import typer.Inferencing.isFullyDefined
1515
import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker}
1616
import typer.Checking.{checkBounds, checkAppliedTypesIn}
17-
import typer.ErrorReporting.{Addenda, NothingToAdd, err}
17+
import typer.ErrorReporting.err
1818
import typer.ProtoTypes.{LhsProto, WildcardSelectionProto, SelectionProto}
1919
import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property}
2020
import util.chaining.tap
2121
import transform.{Recheck, PreRecheck, CapturedVars}
2222
import Recheck.*
2323
import scala.collection.mutable
24-
import CaptureSet.{withCaptureSetsExplained, IncludeFailure, ExistentialSubsumesFailure, MutAdaptFailure}
24+
import CaptureSet.{withCaptureSetsExplained, IncludeFailure, MutAdaptFailure}
2525
import CCState.*
2626
import StdNames.nme
2727
import NameKinds.{DefaultGetterName, WildcardParamName, UniqueNameKind}
2828
import reporting.{trace, Message, OverrideError}
29+
import reporting.Message.Note
2930
import Annotations.Annotation
3031
import Capabilities.*
3132
import Mutability.*
@@ -200,9 +201,6 @@ object CheckCaptures:
200201
&& !sym.isOneOf(DeferredOrTermParamOrAccessor)
201202
&& !sym.hasAnnotation(defn.UntrackedCapturesAnnot)
202203

203-
private def ownerStr(owner: Symbol)(using Context): String =
204-
if owner.isAnonymousFunction then "enclosing function" else owner.show
205-
206204
trait CheckerAPI:
207205
/** Complete symbol info of a val or a def */
208206
def completeDef(tree: ValOrDefDef, sym: Symbol, completer: LazyType)(using Context): Type
@@ -421,7 +419,7 @@ class CheckCaptures extends Recheck, SymTransformer:
421419
case (fail: IncludeFailure) :: _ => fail.cs
422420
case _ => target
423421
def msg(provisional: Boolean) =
424-
def toAdd: String = errorNotes(otherNotes).toAdd.mkString
422+
def toAdd: String = otherNotes.map(_.render).mkString
425423
def descr: String =
426424
val d = realTarget.description
427425
if d.isEmpty then provenance else ""
@@ -1246,7 +1244,7 @@ class CheckCaptures extends Recheck, SymTransformer:
12461244
// too annoying. This is a hole since a defualt getter's result type
12471245
// might leak into a type variable.
12481246

1249-
def fail(tree: Tree, expected: Type, addenda: Addenda): Unit =
1247+
def fail(tree: Tree, expected: Type, notes: List[Note]): Unit =
12501248
def maybeResult = if sym.is(Method) then " result" else ""
12511249
report.error(
12521250
em"""$sym needs an explicit$maybeResult type because the inferred type does not conform to
@@ -1256,7 +1254,7 @@ class CheckCaptures extends Recheck, SymTransformer:
12561254
| Externally visible type: $expected""",
12571255
tree.srcPos)
12581256

1259-
def addenda(expected: Type) = Addenda:
1257+
def addendum(expected: Type) = Note:
12601258
def result = if tree.isInstanceOf[ValDef] then"" else " result"
12611259
i"""
12621260
|
@@ -1275,7 +1273,7 @@ class CheckCaptures extends Recheck, SymTransformer:
12751273
val expected = tpt.tpe.dropAllRetains
12761274
todoAtPostCheck += { () =>
12771275
withCapAsRoot:
1278-
testAdapted(tp, expected, tree.rhs, addenda(expected))(fail)
1276+
testAdapted(tp, expected, tree.rhs, addendum(expected) :: Nil)(fail)
12791277
// The check that inferred <: expected is done after recheck so that it
12801278
// does not interfere with normal rechecking by constraining capture set variables.
12811279
}
@@ -1482,34 +1480,27 @@ class CheckCaptures extends Recheck, SymTransformer:
14821480

14831481
type BoxErrors = mutable.ListBuffer[Message] | Null
14841482

1485-
private def errorNotes(notes: List[TypeComparer.ErrorNote])(using Context): Addenda =
1486-
if notes.isEmpty then NothingToAdd
1487-
else new Addenda:
1488-
override def toAdd(using Context) = notes.map: note =>
1489-
i"""
1490-
|
1491-
|Note that ${note.description}."""
1492-
14931483
/** Addendas for error messages that show where we have under-approximated by
1494-
* mapping a a capability in contravariant position to the empty set because
1484+
* mapping of a capability in contravariant position to the empty set because
14951485
* the original result type of the map was not itself a capability.
14961486
*/
1497-
private def addApproxAddenda(using Context) =
1498-
new TypeAccumulator[Addenda]:
1499-
def apply(add: Addenda, t: Type) = t match
1487+
private def addApproxAddenda(using Context): TypeAccumulator[List[Note]] =
1488+
new TypeAccumulator:
1489+
def apply(notes: List[Note], t: Type) = t match
15001490
case CapturingType(t, CaptureSet.EmptyWithProvenance(ref, mapped)) =>
15011491
/* val (origCore, kind) = original match
15021492
case tp @ AnnotatedType(parent, ann) if ann.hasSymbol(defn.ReachCapabilityAnnot) =>
15031493
(parent, " deep")
15041494
case _ =>
15051495
(original, "")*/
1506-
add ++ Addenda:
1496+
Note:
15071497
i"""
15081498
|
15091499
|Note that a capability $ref in a capture set appearing in contravariant position
15101500
|was mapped to $mapped which is not a capability. Therefore, it was under-approximated to the empty set."""
1501+
:: notes
15111502
case _ =>
1512-
foldOver(add, t)
1503+
foldOver(notes, t)
15131504

15141505
/** Massage `actual` and `expected` types before checking conformance.
15151506
* Massaging is done by the methods following this one:
@@ -1518,8 +1509,8 @@ class CheckCaptures extends Recheck, SymTransformer:
15181509
* If the resulting types are not compatible, try again with an actual type
15191510
* where local capture roots are instantiated to root variables.
15201511
*/
1521-
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
1522-
try testAdapted(actual, expected, tree, addenda)(err.typeMismatch)
1512+
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, notes: List[Note])(using Context): Type =
1513+
try testAdapted(actual, expected, tree, notes: List[Note])(err.typeMismatch)
15231514
catch case ex: AssertionError =>
15241515
println(i"error while checking $tree: $actual against $expected")
15251516
throw ex
@@ -1534,8 +1525,8 @@ class CheckCaptures extends Recheck, SymTransformer:
15341525
case _ => NoType
15351526
case _ => NoType
15361527

1537-
inline def testAdapted(actual: Type, expected: Type, tree: Tree, addenda: Addenda)
1538-
(fail: (Tree, Type, Addenda) => Unit)(using Context): Type =
1528+
inline def testAdapted(actual: Type, expected: Type, tree: Tree, notes: List[Note])
1529+
(fail: (Tree, Type, List[Note]) => Unit)(using Context): Type =
15391530

15401531
var expected1 = alignDependentFunction(expected, actual.stripCapturing)
15411532
val falseDeps = expected1 ne expected
@@ -1582,11 +1573,12 @@ class CheckCaptures extends Recheck, SymTransformer:
15821573
}
15831574

15841575
TypeComparer.compareResult(tryCurrentType || tryWidenNamed) match
1585-
case TypeComparer.CompareResult.Fail(notes) =>
1576+
case TypeComparer.CompareResult.Fail(cmpNotes) =>
15861577
capt.println(i"conforms failed for ${tree}: $actual vs $expected")
15871578
if falseDeps then expected1 = unalignFunction(expected1)
1588-
fail(tree.withType(actualBoxed), expected1,
1589-
addApproxAddenda(addenda ++ errorNotes(notes), expected1))
1579+
val toAdd0 = notes ++ cmpNotes
1580+
val toAdd1 = addApproxAddenda(toAdd0, expected1)
1581+
fail(tree.withType(actualBoxed), expected1, toAdd1)
15901582
actual
15911583
case /*OK*/ _ =>
15921584
if debugSuccesses then tree match

0 commit comments

Comments
 (0)