From 825f0e9dc493e0d815f03618613bec1e7b42f053 Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Mon, 25 Aug 2025 18:33:40 +0200 Subject: [PATCH 1/2] Widen type parameters before box adaptation --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 53 +++++++++++++++++-- tests/neg-custom-args/captures/i23746.check | 21 ++++++++ tests/neg-custom-args/captures/i23746.scala | 10 ++++ tests/pos-custom-args/captures/i19076.scala | 4 ++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 tests/neg-custom-args/captures/i23746.check create mode 100644 tests/neg-custom-args/captures/i23746.scala create mode 100644 tests/pos-custom-args/captures/i19076.scala diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index 56dcabe8429f..ca6897494d75 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -15,7 +15,7 @@ import typer.Inferencing.isFullyDefined import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker} import typer.Checking.{checkBounds, checkAppliedTypesIn} import typer.ErrorReporting.{Addenda, NothingToAdd, err} -import typer.ProtoTypes.{LhsProto, WildcardSelectionProto} +import typer.ProtoTypes.{LhsProto, WildcardSelectionProto, SelectionProto} import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property} import transform.{Recheck, PreRecheck, CapturedVars} import Recheck.* @@ -1309,17 +1309,61 @@ class CheckCaptures extends Recheck, SymTransformer: override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type = testAdapted(actual, expected, tree, addenda)(err.typeMismatch) + @annotation.tailrec + private def widenNamed(tp: Type)(using Context): Type = tp match + case stp: SingletonType => widenNamed(stp.widen) + case ntp: NamedType => ntp.info match + case info: TypeBounds => widenNamed(info.hi) + case _ => tp + case _ => tp + 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) + val actual1 = + if expected.stripCapturing.isInstanceOf[SelectionProto] then + // If the expected type is a `SelectionProto`, we should be careful about cases when + // the actual type is a type parameter (for instance, `X <: box IO^`). + // If `X` were not widen to reveal the boxed type, both sides are unboxed and thus + // no box adaptation happens. But it is unsound: selecting a member from `X` implicitly + // unboxes the value. + // + // Therefore, when the expected type is a selection proto, we conservatively widen + // the actual type to strip type parameters. + widenNamed(actual) + else actual + val actualBoxed = adapt(actual1, expected1, tree) //println(i"check conforms $actualBoxed <<< $expected1") if actualBoxed eq actual then // Only `addOuterRefs` when there is no box adaptation expected1 = addOuterRefs(expected1, actual, tree.srcPos) - TypeComparer.compareResult(isCompatible(actualBoxed, expected1)) match + + def tryCurrentType: Boolean = + isCompatible(actualBoxed, expected1) + + /** When the actual type is a named type, and the previous attempt failed, try to widen the named type + * and try another time. + * + * This is useful for cases like: + * + * def id[X <: box IO^{a}](x: X): IO^{a} = x + * + * When typechecking the body, we need to show that `(x: X)` can be typed at `IO^{a}`. + * In the first attempt, since `X` is simply a parameter reference, we treat it as non-boxed and perform + * no box adptation. But its upper bound is in fact boxed, and adaptation is needed for typechecking the body. + * In those cases, we widen such types and try box adaptation another time. + */ + def tryWidenNamed: Boolean = + val actual1 = widenNamed(actual) + (actual1 ne actual) && { + val actualBoxed1 = adapt(actual1, expected1, tree) + isCompatible(actualBoxed1, expected1) + } + + TypeComparer.compareResult(tryCurrentType || tryWidenNamed) match case TypeComparer.CompareResult.Fail(notes) => capt.println(i"conforms failed for ${tree}: $actual vs $expected") if falseDeps then expected1 = unalignFunction(expected1) @@ -1477,7 +1521,8 @@ class CheckCaptures extends Recheck, SymTransformer: (actualShape, CaptureSet()) end adaptShape - def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected" + //val adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected" + //println(adaptStr) // Get wildcards out of the way expected match diff --git a/tests/neg-custom-args/captures/i23746.check b/tests/neg-custom-args/captures/i23746.check new file mode 100644 index 000000000000..0f1dc797e10f --- /dev/null +++ b/tests/neg-custom-args/captures/i23746.check @@ -0,0 +1,21 @@ +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i23746.scala:5:2 ----------------------------------------- +5 | () => op.run() // error + | ^^^^^^^^^^^^^^ + | Found: () => Unit + | Required: () -> Unit + | + | Note that capability cap is not included in capture set {}. + | + | where: => refers to a fresh root capability in the type of type X + | cap is a fresh root capability in the type of type X + | + | longer explanation available when compiling with `-explain` +-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i23746.scala:10:4 ---------------------------------------- +10 | () => op.run() // error + | ^^^^^^^^^^^^^^ + | Found: () ->{a} Unit + | Required: () -> Unit + | + | Note that capability a is not included in capture set {}. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg-custom-args/captures/i23746.scala b/tests/neg-custom-args/captures/i23746.scala new file mode 100644 index 000000000000..2dd29c73e49b --- /dev/null +++ b/tests/neg-custom-args/captures/i23746.scala @@ -0,0 +1,10 @@ +import language.experimental.captureChecking +trait Op: + def run(): Unit +def helper[X <: Op^](op: X): () -> Unit = + () => op.run() // error +def test1(a: Op^): Unit = + def foo[X <: Op^{a}](op: X): () ->{a} Unit = + () => op.run() // ok + def bar[X <: Op^{a}](op: X): () ->{} Unit = + () => op.run() // error diff --git a/tests/pos-custom-args/captures/i19076.scala b/tests/pos-custom-args/captures/i19076.scala new file mode 100644 index 000000000000..bb34195c80b9 --- /dev/null +++ b/tests/pos-custom-args/captures/i19076.scala @@ -0,0 +1,4 @@ +import language.experimental.captureChecking +trait IO +def main(a: IO^): Unit = + def foo[X <: IO^{a}](x: X): IO^{a} = x // now ok From dbb9db5feea5a204cf8f071cd1e16a4edbd641dc Mon Sep 17 00:00:00 2001 From: Yichen Xu Date: Mon, 25 Aug 2025 21:44:19 +0200 Subject: [PATCH 2/2] Refactor type parameter widening --- .../dotty/tools/dotc/cc/CheckCaptures.scala | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala index ca6897494d75..74c243a20684 100644 --- a/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala +++ b/compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala @@ -1310,12 +1310,14 @@ class CheckCaptures extends Recheck, SymTransformer: testAdapted(actual, expected, tree, addenda)(err.typeMismatch) @annotation.tailrec - private def widenNamed(tp: Type)(using Context): Type = tp match - case stp: SingletonType => widenNamed(stp.widen) - case ntp: NamedType => ntp.info match - case info: TypeBounds => widenNamed(info.hi) - case _ => tp - case _ => tp + private def findImpureUpperBound(tp: Type)(using Context): Type = tp match + case _: SingletonType => findImpureUpperBound(tp.widen) + case tp: TypeRef if tp.symbol.isAbstractOrParamType => + tp.info match + case TypeBounds(_, hi) if hi.isBoxedCapturing => hi + case TypeBounds(_, hi) => findImpureUpperBound(hi) + case _ => NoType + case _ => NoType inline def testAdapted(actual: Type, expected: Type, tree: Tree, addenda: Addenda) (fail: (Tree, Type, Addenda) => Unit)(using Context): Type = @@ -1332,7 +1334,8 @@ class CheckCaptures extends Recheck, SymTransformer: // // Therefore, when the expected type is a selection proto, we conservatively widen // the actual type to strip type parameters. - widenNamed(actual) + val hi = findImpureUpperBound(actual) + if !hi.exists then actual else hi else actual val actualBoxed = adapt(actual1, expected1, tree) //println(i"check conforms $actualBoxed <<< $expected1") @@ -1357,8 +1360,8 @@ class CheckCaptures extends Recheck, SymTransformer: * In those cases, we widen such types and try box adaptation another time. */ def tryWidenNamed: Boolean = - val actual1 = widenNamed(actual) - (actual1 ne actual) && { + val actual1 = findImpureUpperBound(actual) + actual1.exists && { val actualBoxed1 = adapt(actual1, expected1, tree) isCompatible(actualBoxed1, expected1) }