Skip to content

Some usability improvements relating to errors #23370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/cc/CaptureOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,9 @@ extension (tp: Type)
case tp: (TypeRef | AppliedType) =>
val sym = tp.typeSymbol
if sym.isClass then sym.isPureClass
else tp.superType.isAlwaysPure
else !tp.superType.isAny && tp.superType.isAlwaysPure
case tp: TypeProxy =>
tp.superType.isAlwaysPure
!tp.superType.isAny && tp.superType.isAlwaysPure
case tp: AndType =>
tp.tp1.isAlwaysPure || tp.tp2.isAlwaysPure
case tp: OrType =>
Expand Down
52 changes: 36 additions & 16 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1059,14 +1059,24 @@ class CheckCaptures extends Recheck, SymTransformer:

def canUseInferred = // If canUseInferred is false, all capturing types in the type of `sym` need to be given explicitly
sym.isLocalToCompilationUnit // Symbols that can't be seen outside the compilation unit can always have inferred types
|| sym.privateWithin == defn.EmptyPackageClass
// We make an exception for private symbols in a toplevel file in the empty package
|| ctx.owner.enclosingPackageClass.isEmptyPackage
// We make an exception for symbols in the empty package.
// these could theoretically be accessed from other files in the empty package, but
// it would be too annoying to require explicit types.
// usually it would be too annoying to require explicit types.
|| sym.name.is(DefaultGetterName) // Default getters are exempted since otherwise it would be
// too annoying. This is a hole since a defualt getter's result type
// might leak into a type variable.

def fail(tree: Tree, expected: Type, addenda: Addenda): Unit =
def maybeResult = if sym.is(Method) then " result" else ""
report.error(
em"""$sym needs an explicit$maybeResult type because the inferred type does not conform to
|the type that is externally visible in other compilation units.
|
| Inferred type : ${tree.tpe}
| Externally visible type: $expected""",
tree.srcPos)

def addenda(expected: Type) = new Addenda:
override def toAdd(using Context) =
def result = if tree.isInstanceOf[ValDef] then"" else " result"
Expand All @@ -1083,7 +1093,7 @@ class CheckCaptures extends Recheck, SymTransformer:
val expected = tpt.tpe.dropAllRetains
todoAtPostCheck += { () =>
withCapAsRoot:
checkConformsExpr(tp, expected, tree.rhs, addenda(expected))
testAdapted(tp, expected, tree.rhs, addenda(expected))(fail)
// The check that inferred <: expected is done after recheck so that it
// does not interfere with normal rechecking by constraining capture set variables.
}
Expand Down Expand Up @@ -1322,7 +1332,12 @@ class CheckCaptures extends Recheck, SymTransformer:
* where local capture roots are instantiated to root variables.
*/
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
testAdapted(actual, expected, tree, addenda)(err.typeMismatch)

inline def testAdapted(actual: Type, expected: Type, tree: Tree, addenda: Addenda)
(fail: (Tree, Type, Addenda) => Unit)(using Context): Type =
var expected1 = alignDependentFunction(expected, actual.stripCapturing)
val falseDeps = expected1 ne expected
val actualBoxed = adapt(actual, expected1, tree)
//println(i"check conforms $actualBoxed <<< $expected1")

Expand All @@ -1332,26 +1347,23 @@ class CheckCaptures extends Recheck, SymTransformer:
TypeComparer.compareResult(isCompatible(actualBoxed, expected1)) match
case TypeComparer.CompareResult.Fail(notes) =>
capt.println(i"conforms failed for ${tree}: $actual vs $expected")
err.typeMismatch(tree.withType(actualBoxed), expected1,
addApproxAddenda(
addenda ++ errorNotes(notes),
expected1))
if falseDeps then expected1 = unalignFunction(expected1)
fail(tree.withType(actualBoxed), expected1,
addApproxAddenda(addenda ++ errorNotes(notes), expected1))
actual
case /*OK*/ _ =>
if debugSuccesses then tree match
case Ident(_) =>
println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}")
case _ =>
case Ident(_) =>
println(i"SUCCESS $tree for $actual <:< $expected:\n${TypeComparer.explained(_.isSubType(actualBoxed, expected1))}")
case _ =>
actualBoxed
end checkConformsExpr
end testAdapted

/** Turn `expected` into a dependent function when `actual` is dependent. */
private def alignDependentFunction(expected: Type, actual: Type)(using Context): Type =
def recur(expected: Type): Type = expected.dealias match
case expected0 @ CapturingType(eparent, refs) =>
val eparent1 = recur(eparent)
if eparent1 eq eparent then expected
else CapturingType(eparent1, refs, boxed = expected0.isBoxed)
case expected @ CapturingType(eparent, refs) =>
expected.derivedCapturingType(recur(eparent), refs)
case expected @ defn.FunctionOf(args, resultType, isContextual)
if defn.isNonRefinedFunction(expected) =>
actual match
Expand All @@ -1369,6 +1381,14 @@ class CheckCaptures extends Recheck, SymTransformer:
case _ => expected
recur(expected)

private def unalignFunction(tp: Type)(using Context): Type = tp match
case tp @ CapturingType(parent, refs) =>
tp.derivedCapturingType(unalignFunction(parent), refs)
case defn.RefinedFunctionOf(mt) =>
mt.toFunctionType(alwaysDependent = false)
case _ =>
tp

/** For the expected type, implement the rule outlined in #14390:
* - when checking an expression `a: Ta^Ca` against an expected type `Te^Ce`,
* - where the capture set `Ce` contains Cls.this,
Expand Down
33 changes: 20 additions & 13 deletions compiler/src/dotty/tools/dotc/cc/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -406,19 +406,26 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
CapturingType(fntpe, cs, boxed = false)
else fntpe

/** Check that types extending SharedCapability don't have a `cap` in their capture set.
* TODO This is not enough.
* We need to also track that we cannot get exclusive capabilities in paths
* where some prefix derives from SharedCapability. Also, can we just
* exclude `cap`, or do we have to extend this to all exclusive capabilties?
* The problem is that we know what is exclusive in general only after capture
* checking, not before.
/** 1. Check that parents of capturing types are not pure.
* 2. Check that types extending SharedCapability don't have a `cap` in their capture set.
* TODO This is not enough.
* We need to also track that we cannot get exclusive capabilities in paths
* where some prefix derives from SharedCapability. Also, can we just
* exclude `cap`, or do we have to extend this to all exclusive capabilties?
* The problem is that we know what is exclusive in general only after capture
* checking, not before.
*/
def checkSharedOK(tp: Type): tp.type =
def checkRetainsOK(tp: Type): tp.type =
tp match
case CapturingType(parent, refs)
if refs.isUniversal && parent.derivesFromSharedCapability =>
fail(em"$tp extends SharedCapability, so it cannot capture `cap`")
case CapturingType(parent, refs) =>
if parent.isAlwaysPure && !tptToCheck.span.isZeroExtent then
// If tptToCheck is zero-extent it could be copied from an overridden
// method's result type. In that case, there's no point requiring
// an explicit result type in the override, the inherited capture set
// will be ignored anyway.
fail(em"$parent is a pure type, it makes no sense to add a capture set to it")
else if refs.isUniversal && parent.derivesFromSharedCapability then
fail(em"$tp extends SharedCapability, so it cannot capture `cap`")
case _ =>
tp

Expand All @@ -436,7 +443,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
def innerApply(t: Type) =
t match
case t @ CapturingType(parent, refs) =>
checkSharedOK:
checkRetainsOK:
t.derivedCapturingType(stripImpliedCaptureSet(this(parent)), refs)
case t @ AnnotatedType(parent, ann) =>
val parent1 = this(parent)
Expand All @@ -445,7 +452,7 @@ class Setup extends PreRecheck, SymTransformer, SetupAPI:
if !tptToCheck.isEmpty then
checkWellformedLater(parent2, ann.tree, tptToCheck)
try
checkSharedOK:
checkRetainsOK:
CapturingType(parent2, ann.tree.toCaptureSet)
catch case ex: IllegalCaptureRef =>
if !tptToCheck.isEmpty then
Expand Down
12 changes: 5 additions & 7 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1657,19 +1657,17 @@ object Types extends TypeUtils {
NoType
}

/** The iterator of underlying types as long as type is a TypeProxy.
* Useful for diagnostics
/** The iterator of underlying types staring with `this` and followed by
* repeatedly applying `f` as long as type is a TypeProxy. Useful for diagnostics.
*/
def underlyingIterator(using Context): Iterator[Type] = new Iterator[Type] {
def iterate(f: TypeProxy => Type): Iterator[Type] = new Iterator[Type]:
var current = Type.this
var hasNext = true
def next() = {
def next() =
val res = current
hasNext = current.isInstanceOf[TypeProxy]
if (hasNext) current = current.asInstanceOf[TypeProxy].underlying
if hasNext then current = f(current.asInstanceOf[TypeProxy])
res
}
}

/** A prefix-less refined this or a termRef to a new skolem symbol
* that has the given type as info.
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ class PlainPrinter(_ctx: Context) extends Printer {
(" <: " ~ toText(bound) provided !bound.isAny)
}.close
case tp @ CapturingType(parent, refs) =>
val boxText: Text = Str("box ") provided tp.isBoxed //&& ctx.settings.YccDebug.value
val boxText: Text = Str("box ") provided tp.isBoxed && ccVerbose
if elideCapabilityCaps
&& parent.derivesFrom(defn.Caps_Capability)
&& refs.containsTerminalCapability
Expand Down
4 changes: 2 additions & 2 deletions scala2-library-cc/src/scala/collection/Iterable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ trait SortedSetFactoryDefaults[+A,
+WithFilterCC[x] <: IterableOps[x, WithFilterCC, WithFilterCC[x]] with Set[x]] extends SortedSetOps[A @uncheckedVariance, CC, CC[A @uncheckedVariance]] {
self: IterableOps[A, WithFilterCC, _] =>

override protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]^): CC[A @uncheckedVariance]^{coll} = sortedIterableFactory.from(coll)(using ordering)
override protected def fromSpecific(coll: IterableOnce[A @uncheckedVariance]^): CC[A @uncheckedVariance] = sortedIterableFactory.from(coll)(using ordering)
override protected def newSpecificBuilder: mutable.Builder[A @uncheckedVariance, CC[A @uncheckedVariance]] = sortedIterableFactory.newBuilder[A](using ordering)
override def empty: CC[A @uncheckedVariance] = sortedIterableFactory.empty(using ordering)

Expand Down Expand Up @@ -1050,7 +1050,7 @@ trait SortedMapFactoryDefaults[K, +V,
self: IterableOps[(K, V), WithFilterCC, _] =>

override def empty: CC[K, V @uncheckedVariance] = sortedMapFactory.empty(using ordering)
override protected def fromSpecific(coll: IterableOnce[(K, V @uncheckedVariance)]^): CC[K, V @uncheckedVariance]^{coll} = sortedMapFactory.from(coll)(using ordering)
override protected def fromSpecific(coll: IterableOnce[(K, V @uncheckedVariance)]^): CC[K, V @uncheckedVariance] = sortedMapFactory.from(coll)(using ordering)
override protected def newSpecificBuilder: mutable.Builder[(K, V @uncheckedVariance), CC[K, V @uncheckedVariance]] = sortedMapFactory.newBuilder[K, V](using ordering)

override def withFilter(p: ((K, V)) => Boolean): collection.SortedMapOps.WithFilter[K, V, WithFilterCC, UnsortedCC, CC]^{p} =
Expand Down
12 changes: 6 additions & 6 deletions tests/neg-custom-args/captures/boundschecks3.check
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:9:11 ---------------------------------
9 | val foo: C[Tree^] = ??? // error
| ^
| Type argument box test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[box test.Tree^]
| Type argument test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[test.Tree^]
|
| where: ^ refers to the universal root capability
| where: ^ refers to the universal root capability
|
| longer explanation available when compiling with `-explain`
-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:10:11 --------------------------------
10 | type T = C[Tree^] // error
| ^
| Type argument box test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[box test.Tree^]
| Type argument test.Tree^ does not conform to upper bound test.Tree in inferred type test.C[test.Tree^]
|
| where: ^ refers to the universal root capability
| where: ^ refers to the universal root capability
|
| longer explanation available when compiling with `-explain`
-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:11:11 --------------------------------
11 | val bar: T -> T = ??? // error
| ^
|Type argument box test.Tree^ does not conform to upper bound test.Tree in subpart test.C[box test.Tree^] of inferred type test.C[box test.Tree^] -> test.C[box test.Tree^]
|Type argument test.Tree^ does not conform to upper bound test.Tree in subpart test.C[test.Tree^] of inferred type test.C[test.Tree^] -> test.C[test.Tree^]
|
|where: ^ refers to the universal root capability
|
| longer explanation available when compiling with `-explain`
-- [E057] Type Mismatch Error: tests/neg-custom-args/captures/boundschecks3.scala:12:11 --------------------------------
12 | val baz: C[Tree^] -> Unit = ??? // error
| ^
|Type argument box test.Tree^ does not conform to upper bound test.Tree in subpart test.C[box test.Tree^] of inferred type test.C[box test.Tree^] -> Unit
|Type argument test.Tree^ does not conform to upper bound test.Tree in subpart test.C[test.Tree^] of inferred type test.C[test.Tree^] -> Unit
|
|where: ^ refers to the universal root capability
|
Expand Down
12 changes: 6 additions & 6 deletions tests/neg-custom-args/captures/box-adapt-cases.check
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:8:10 -------------------------------
8 | x.value(cap => cap.use()) // error, was OK
| ^^^^^^^^^^^^^^^^
| Found: (cap: box Cap^?) => Int
| Required: (cap: box Cap^) =>² Int
| Found: (cap: Cap^?) => Int
| Required: Cap^ =>² Int
|
| where: => refers to the universal root capability
| =>² refers to a fresh root capability created in method test1
Expand All @@ -12,14 +12,14 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:15:10 ------------------------------
15 | x.value(cap => cap.use()) // error
| ^^^^^^^^^^^^^^^^
| Found: (cap: box Cap^{io}) ->{io} Int
| Required: (cap: box Cap^{io}) -> Int
| Found: (cap: Cap^{io}) ->{io} Int
| Required: Cap^{io} -> Int
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/box-adapt-cases.scala:29:10 ------------------------------
29 | x.value(cap => cap.use()) // error
| ^^^^^^^^^^^^^^^^
| Found: (cap: box Cap^?) ->{io, fs} Int
| Required: (cap: box Cap^{io, fs}) ->{io} Int
| Found: (cap: Cap^?) ->{io, fs} Int
| Required: Cap^{io, fs} ->{io} Int
|
| longer explanation available when compiling with `-explain`
16 changes: 8 additions & 8 deletions tests/neg-custom-args/captures/box-adapt-contra.check
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:9:52 ---------------------------------------------------
9 | val f: (Cap^{c} -> Unit) -> Unit = useCap[Cap^{c}](c) // error
| ^^^^^^^^^^^^^^^^^^
| Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} -> Unit
| Cap^{c} -> Unit cannot be box-converted to Cap^{c} ->{c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in Cap^{c} -> Unit
-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:13:57 --------------------------------------------------
13 | val f1: (Cap^{c} => Unit) ->{c} Unit = useCap1[Cap^{c}](c) // error, was ok when cap was a root
| ^^^^^^^^^^^^^^^^^^^
| Cap^{c} => Unit cannot be box-converted to box Cap^{c} ->{cap, c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} => Unit
| Cap^{c} => Unit cannot be box-converted to Cap^{c} ->{cap, c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in Cap^{c} => Unit
|
| where: => refers to the universal root capability
| cap is the universal root capability
| where: => refers to the universal root capability
| cap is the universal root capability
-- Error: tests/neg-custom-args/captures/box-adapt-contra.scala:19:54 --------------------------------------------------
19 | val f3: (Cap^{c} -> Unit) => Unit = useCap3[Cap^{c}](c) // error
| ^^^^^^^^^^^^^^^^^^^
| Cap^{c} -> Unit cannot be box-converted to box Cap^{c} ->{d, c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in box Cap^{c} ->{d} Unit
| Cap^{c} -> Unit cannot be box-converted to Cap^{c} ->{d, c} Unit
| since the additional capture set {c} resulting from box conversion is not allowed in Cap^{c} ->{d} Unit
Loading
Loading