From fbc89a8ee69928bbd2b14ab71fc2b9fbaafe413c Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 25 Nov 2025 17:36:24 +0100 Subject: [PATCH 1/2] Somewhat better error notes for box failures At least we don't get Note that cap is not included in {} anymore. To make this even better we'd have to trace back a box failure to the original types we were trying to compare. Right now the box failure error notes are too disconnected from the rest. But they become clearer when one compiles with -explain. --- .../src/dotty/tools/dotc/cc/CaptureSet.scala | 20 ++++++++++++---- .../dotty/tools/dotc/core/TypeComparer.scala | 4 ++-- tests/neg-custom-args/captures/eta.check | 2 +- tests/neg-custom-args/captures/i24543.check | 24 +++++++++++++++++++ tests/neg-custom-args/captures/i24543.scala | 12 ++++++++++ 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 tests/neg-custom-args/captures/i24543.check create mode 100644 tests/neg-custom-args/captures/i24543.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala index ee94d9bf1d31..1eb6744a39b0 100644 --- a/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala +++ b/compiler/src/dotty/tools/dotc/cc/CaptureSet.scala @@ -588,6 +588,9 @@ object CaptureSet: cs.mutability = Mutability.Reader cs + class EmptyOfBoxed(val tp1: Type, val tp2: Type) extends Const(emptyRefs): + override def toString = "{} of boxed mismatch" + /** The universal capture set `{cap}` */ def universal(using Context): Const = Const(SimpleIdentitySet(GlobalCap)) @@ -1341,15 +1344,19 @@ object CaptureSet: case _ => false - /** An include failure F1 covers another include failure F2 unless F2 - * strictly subsumes F1, which means they describe the same capture sets - * and the element in F2 is more specific than the element in F1. + /** An include failure F1 covers another include failure F2 unless one + * of the following two conditons holds: + * 1. F2 strictly subsumes F1, which means they describe the same capture sets + * and the element in F2 is more specific than the element in F1. + * 2. Both F1 and F2 are the empty set, but only F2 is an empty set synthesized + * when comparing types with different box status */ override def covers(other: Note)(using Context) = other match case other @ IncludeFailure(cs1, elem1, _) => val strictlySubsumes = cs.elems == cs1.elems - && elem1.singletonCaptureSet.mightSubcapture(elem.singletonCaptureSet) + && (elem1.singletonCaptureSet.mightSubcapture(elem.singletonCaptureSet) + || cs1.isInstanceOf[EmptyOfBoxed] && !cs.isInstanceOf[EmptyOfBoxed]) !strictlySubsumes case _ => false @@ -1390,6 +1397,11 @@ object CaptureSet: else trailing: i"capability ${elem.showAsCapability} cannot be included in capture set $cs" + case cs: EmptyOfBoxed => + trailing: + val (boxed, unboxed) = + if cs.tp1.isBoxedCapturing then (cs.tp1, cs.tp2) else (cs.tp2, cs.tp1) + i"${cs.tp1} does not conform to ${cs.tp2} because $boxed is boxed but $unboxed is not" case _ => def why = val reasons = cs.elems.toList.collect: diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 6a7e4733f9d9..0cb398010587 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -2915,8 +2915,8 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case _ => subc && (tp1.isBoxedCapturing == tp2.isBoxedCapturing - || refs1.subCaptures(CaptureSet.empty, makeVarState())) - + || refs1.subCaptures(CaptureSet.EmptyOfBoxed(tp1, tp2), makeVarState())) + protected def logUndoAction(action: () => Unit) = undoLog += action diff --git a/tests/neg-custom-args/captures/eta.check b/tests/neg-custom-args/captures/eta.check index dfce53accd4d..09878f84135d 100644 --- a/tests/neg-custom-args/captures/eta.check +++ b/tests/neg-custom-args/captures/eta.check @@ -4,6 +4,6 @@ | Found: (g : () -> A) | Required: () -> Proc^{f} | - | Note that capability f is not included in capture set {}. + | Note that () ->{f} Unit does not conform to Proc^{f} because () ->{f} Unit is boxed but Proc^{f} is not. | | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i24543.check b/tests/neg-custom-args/captures/i24543.check new file mode 100644 index 000000000000..9f6c30db747c --- /dev/null +++ b/tests/neg-custom-args/captures/i24543.check @@ -0,0 +1,24 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i24543.scala:7:23 ---------------------------------------- +7 | val y: (Ref^, Int) = (x.elem, 1) // error + | ^^^^^^^^^^^ + | Found: (T^'s1, Int) + | Required: (Ref^, Int) + | + | Note that Ref^² does not conform to Ref^ because Ref^² is boxed but Ref^ is not. + | + | where: ^ refers to a fresh root capability in the type of value y + | ^² refers to a fresh root capability in the type of type T + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i24543.scala:9:45 ---------------------------------------- +9 |def h2[T <: Ref^](x: List[T]): (Ref^, Int) = (x.head, 1) // error + | ^^^^^^^^^^^ + | Found: (T^'s2, Int) + | Required: (Ref^, Int) + | + | Note that Ref^² does not conform to Ref^ because Ref^² is boxed but Ref^ is not. + | + | where: ^ refers to a fresh root capability in the result type of method h2 + | ^² refers to a fresh root capability in the type of type T + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i24543.scala b/tests/neg-custom-args/captures/i24543.scala new file mode 100644 index 000000000000..a52d84e1cd78 --- /dev/null +++ b/tests/neg-custom-args/captures/i24543.scala @@ -0,0 +1,12 @@ +import scala.language.experimental.captureChecking + +class Ref +case class Box[T](elem: T) + +def h1[T <: Ref^](x: Box[T]): Unit = + val y: (Ref^, Int) = (x.elem, 1) // error + +def h2[T <: Ref^](x: List[T]): (Ref^, Int) = (x.head, 1) // error + + + From 5d00194deadcda3d1c1fbad0bf2abd2aeb1a0cb1 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 27 Nov 2025 10:58:39 +0100 Subject: [PATCH 2/2] Fix underlying problem of box mismatches The neg tests are now pos tests. I kept the special error message for better diagnosis if the mismatch arises elsewhere (it does in eta.scala). --- .../dotty/tools/dotc/core/TypeComparer.scala | 13 ++++++---- tests/neg-custom-args/captures/i24543.check | 24 ------------------- .../captures/i24543.scala | 4 ++-- tests/pos-custom-args/captures/i24543a.scala | 9 +++++++ 4 files changed, 20 insertions(+), 30 deletions(-) delete mode 100644 tests/neg-custom-args/captures/i24543.check rename tests/{neg-custom-args => pos-custom-args}/captures/i24543.scala (52%) create mode 100644 tests/pos-custom-args/captures/i24543a.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 0cb398010587..b9e57445ce94 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -550,10 +550,15 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling || !ctx.mode.is(Mode.CheckBoundsOrSelfType) && tp1.isAlwaysPure || parent1.isSingleton && refs1.elems.forall(parent1 eq _) then + def remainsBoxed1 = parent1.isBoxedCapturing || parent1.dealias.match + case parent1: TypeRef => + parent1.superType.isBoxedCapturing + // When comparing a type parameter with boxed upper bound on the left + // we should not strip the box on the right. See i24543.scala. + case _ => + false val tp2a = - if tp1.isBoxedCapturing && !parent1.isBoxedCapturing - then tp2.unboxed - else tp2 + if tp1.isBoxedCapturing && !remainsBoxed1 then tp2.unboxed else tp2 recur(parent1, tp2a) else thirdTry compareCapturing @@ -2916,7 +2921,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling subc && (tp1.isBoxedCapturing == tp2.isBoxedCapturing || refs1.subCaptures(CaptureSet.EmptyOfBoxed(tp1, tp2), makeVarState())) - + protected def logUndoAction(action: () => Unit) = undoLog += action diff --git a/tests/neg-custom-args/captures/i24543.check b/tests/neg-custom-args/captures/i24543.check deleted file mode 100644 index 9f6c30db747c..000000000000 --- a/tests/neg-custom-args/captures/i24543.check +++ /dev/null @@ -1,24 +0,0 @@ --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i24543.scala:7:23 ---------------------------------------- -7 | val y: (Ref^, Int) = (x.elem, 1) // error - | ^^^^^^^^^^^ - | Found: (T^'s1, Int) - | Required: (Ref^, Int) - | - | Note that Ref^² does not conform to Ref^ because Ref^² is boxed but Ref^ is not. - | - | where: ^ refers to a fresh root capability in the type of value y - | ^² refers to a fresh root capability in the type of type T - | - | longer explanation available when compiling with `-explain` --- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i24543.scala:9:45 ---------------------------------------- -9 |def h2[T <: Ref^](x: List[T]): (Ref^, Int) = (x.head, 1) // error - | ^^^^^^^^^^^ - | Found: (T^'s2, Int) - | Required: (Ref^, Int) - | - | Note that Ref^² does not conform to Ref^ because Ref^² is boxed but Ref^ is not. - | - | where: ^ refers to a fresh root capability in the result type of method h2 - | ^² refers to a fresh root capability in the type of type T - | - | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i24543.scala b/tests/pos-custom-args/captures/i24543.scala similarity index 52% rename from tests/neg-custom-args/captures/i24543.scala rename to tests/pos-custom-args/captures/i24543.scala index a52d84e1cd78..e09607cbf53c 100644 --- a/tests/neg-custom-args/captures/i24543.scala +++ b/tests/pos-custom-args/captures/i24543.scala @@ -4,9 +4,9 @@ class Ref case class Box[T](elem: T) def h1[T <: Ref^](x: Box[T]): Unit = - val y: (Ref^, Int) = (x.elem, 1) // error + val y: (Ref^, Int) = (x.elem, 1) // was error -def h2[T <: Ref^](x: List[T]): (Ref^, Int) = (x.head, 1) // error +def h2[T <: Ref^](x: List[T]): (Ref^, Int) = (x.head, 1) // was error diff --git a/tests/pos-custom-args/captures/i24543a.scala b/tests/pos-custom-args/captures/i24543a.scala new file mode 100644 index 000000000000..034a45db2df0 --- /dev/null +++ b/tests/pos-custom-args/captures/i24543a.scala @@ -0,0 +1,9 @@ +import scala.language.experimental.captureChecking +class Ref +case class Box[+T](elem: T) + +def test1(a: Ref^): Unit = + def hh[T <: Ref^{a}](x: Box[T]): Unit = + val y: (Ref^{a}, Int) = (x.elem, 1) + val z = (x.elem, 1) + val _: (Ref^{a}, Int) = z