Skip to content

Commit 451b745

Browse files
authored
Widen type parameters before box adaptation (#23809)
Fixes #23746 and #19076. Widen type parameters to their upper bounds in certain cases before box adaptation. This helps revealing a boxed type inside the upper bound of a type parameter.
2 parents 77c30e5 + dbb9db5 commit 451b745

File tree

4 files changed

+87
-4
lines changed

4 files changed

+87
-4
lines changed

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

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import typer.Inferencing.isFullyDefined
1515
import typer.RefChecks.{checkAllOverrides, checkSelfAgainstParents, OverridingPairsChecker}
1616
import typer.Checking.{checkBounds, checkAppliedTypesIn}
1717
import typer.ErrorReporting.{Addenda, NothingToAdd, err}
18-
import typer.ProtoTypes.{LhsProto, WildcardSelectionProto}
18+
import typer.ProtoTypes.{LhsProto, WildcardSelectionProto, SelectionProto}
1919
import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property}
2020
import transform.{Recheck, PreRecheck, CapturedVars}
2121
import Recheck.*
@@ -1309,17 +1309,64 @@ class CheckCaptures extends Recheck, SymTransformer:
13091309
override def checkConformsExpr(actual: Type, expected: Type, tree: Tree, addenda: Addenda)(using Context): Type =
13101310
testAdapted(actual, expected, tree, addenda)(err.typeMismatch)
13111311

1312+
@annotation.tailrec
1313+
private def findImpureUpperBound(tp: Type)(using Context): Type = tp match
1314+
case _: SingletonType => findImpureUpperBound(tp.widen)
1315+
case tp: TypeRef if tp.symbol.isAbstractOrParamType =>
1316+
tp.info match
1317+
case TypeBounds(_, hi) if hi.isBoxedCapturing => hi
1318+
case TypeBounds(_, hi) => findImpureUpperBound(hi)
1319+
case _ => NoType
1320+
case _ => NoType
1321+
13121322
inline def testAdapted(actual: Type, expected: Type, tree: Tree, addenda: Addenda)
13131323
(fail: (Tree, Type, Addenda) => Unit)(using Context): Type =
1324+
13141325
var expected1 = alignDependentFunction(expected, actual.stripCapturing)
13151326
val falseDeps = expected1 ne expected
1316-
val actualBoxed = adapt(actual, expected1, tree)
1327+
val actual1 =
1328+
if expected.stripCapturing.isInstanceOf[SelectionProto] then
1329+
// If the expected type is a `SelectionProto`, we should be careful about cases when
1330+
// the actual type is a type parameter (for instance, `X <: box IO^`).
1331+
// If `X` were not widen to reveal the boxed type, both sides are unboxed and thus
1332+
// no box adaptation happens. But it is unsound: selecting a member from `X` implicitly
1333+
// unboxes the value.
1334+
//
1335+
// Therefore, when the expected type is a selection proto, we conservatively widen
1336+
// the actual type to strip type parameters.
1337+
val hi = findImpureUpperBound(actual)
1338+
if !hi.exists then actual else hi
1339+
else actual
1340+
val actualBoxed = adapt(actual1, expected1, tree)
13171341
//println(i"check conforms $actualBoxed <<< $expected1")
13181342

13191343
if actualBoxed eq actual then
13201344
// Only `addOuterRefs` when there is no box adaptation
13211345
expected1 = addOuterRefs(expected1, actual, tree.srcPos)
1322-
TypeComparer.compareResult(isCompatible(actualBoxed, expected1)) match
1346+
1347+
def tryCurrentType: Boolean =
1348+
isCompatible(actualBoxed, expected1)
1349+
1350+
/** When the actual type is a named type, and the previous attempt failed, try to widen the named type
1351+
* and try another time.
1352+
*
1353+
* This is useful for cases like:
1354+
*
1355+
* def id[X <: box IO^{a}](x: X): IO^{a} = x
1356+
*
1357+
* When typechecking the body, we need to show that `(x: X)` can be typed at `IO^{a}`.
1358+
* In the first attempt, since `X` is simply a parameter reference, we treat it as non-boxed and perform
1359+
* no box adptation. But its upper bound is in fact boxed, and adaptation is needed for typechecking the body.
1360+
* In those cases, we widen such types and try box adaptation another time.
1361+
*/
1362+
def tryWidenNamed: Boolean =
1363+
val actual1 = findImpureUpperBound(actual)
1364+
actual1.exists && {
1365+
val actualBoxed1 = adapt(actual1, expected1, tree)
1366+
isCompatible(actualBoxed1, expected1)
1367+
}
1368+
1369+
TypeComparer.compareResult(tryCurrentType || tryWidenNamed) match
13231370
case TypeComparer.CompareResult.Fail(notes) =>
13241371
capt.println(i"conforms failed for ${tree}: $actual vs $expected")
13251372
if falseDeps then expected1 = unalignFunction(expected1)
@@ -1477,7 +1524,8 @@ class CheckCaptures extends Recheck, SymTransformer:
14771524
(actualShape, CaptureSet())
14781525
end adaptShape
14791526

1480-
def adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected"
1527+
//val adaptStr = i"adapting $actual ${if covariant then "~~>" else "<~~"} $expected"
1528+
//println(adaptStr)
14811529

14821530
// Get wildcards out of the way
14831531
expected match
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i23746.scala:5:2 -----------------------------------------
2+
5 | () => op.run() // error
3+
| ^^^^^^^^^^^^^^
4+
| Found: () => Unit
5+
| Required: () -> Unit
6+
|
7+
| Note that capability cap is not included in capture set {}.
8+
|
9+
| where: => refers to a fresh root capability in the type of type X
10+
| cap is a fresh root capability in the type of type X
11+
|
12+
| longer explanation available when compiling with `-explain`
13+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/i23746.scala:10:4 ----------------------------------------
14+
10 | () => op.run() // error
15+
| ^^^^^^^^^^^^^^
16+
| Found: () ->{a} Unit
17+
| Required: () -> Unit
18+
|
19+
| Note that capability a is not included in capture set {}.
20+
|
21+
| longer explanation available when compiling with `-explain`
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import language.experimental.captureChecking
2+
trait Op:
3+
def run(): Unit
4+
def helper[X <: Op^](op: X): () -> Unit =
5+
() => op.run() // error
6+
def test1(a: Op^): Unit =
7+
def foo[X <: Op^{a}](op: X): () ->{a} Unit =
8+
() => op.run() // ok
9+
def bar[X <: Op^{a}](op: X): () ->{} Unit =
10+
() => op.run() // error
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import language.experimental.captureChecking
2+
trait IO
3+
def main(a: IO^): Unit =
4+
def foo[X <: IO^{a}](x: X): IO^{a} = x // now ok

0 commit comments

Comments
 (0)