Skip to content
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
56 changes: 52 additions & 4 deletions compiler/src/dotty/tools/dotc/cc/CheckCaptures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -1309,17 +1309,64 @@ 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 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 =

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.
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")

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 = findImpureUpperBound(actual)
actual1.exists && {
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)
Expand Down Expand Up @@ -1477,7 +1524,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
Expand Down
21 changes: 21 additions & 0 deletions tests/neg-custom-args/captures/i23746.check
Original file line number Diff line number Diff line change
@@ -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`
10 changes: 10 additions & 0 deletions tests/neg-custom-args/captures/i23746.scala
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions tests/pos-custom-args/captures/i19076.scala
Original file line number Diff line number Diff line change
@@ -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
Loading