Skip to content

Commit d4c084c

Browse files
committed
Refine isParametric tests
Mutable variables can appeal to parametricty only if they are not captured. We use "not captured by any closure" as a sound approximation for that, since variables themselves are currently not tracked, so we cannot use soemthing more finegrained.
1 parent 5610730 commit d4c084c

File tree

5 files changed

+160
-21
lines changed

5 files changed

+160
-21
lines changed

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

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import typer.ErrorReporting.{Addenda, err}
1616
import typer.ProtoTypes.{AnySelectionProto, LhsProto}
1717
import util.{SimpleIdentitySet, EqHashMap, EqHashSet, SrcPos, Property}
1818
import transform.SymUtils.*
19-
import transform.{Recheck, PreRecheck}
19+
import transform.{Recheck, PreRecheck, CapturedVars}
2020
import Recheck.*
2121
import scala.collection.mutable
2222
import CaptureSet.{withCaptureSetsExplained, IdempotentCaptRefMap, CompareResult}
@@ -149,15 +149,25 @@ object CheckCaptures:
149149

150150
private val seen = new EqHashSet[TypeRef]
151151

152+
/** Check that there is at least one method containing carrier and defined
153+
* in the scope of tparam. E.g. this is OK:
154+
* def f[T] = { ... var x: T ... }
155+
* So is this:
156+
* class C[T] { def f() = { class D { var x: T }}}
157+
* But this is not OK:
158+
* class C[T] { object o { var x: T }}
159+
*/
152160
extension (tparam: Symbol) def isParametricIn(carrier: Symbol): Boolean =
153-
val encl = carrier.maybeOwner.enclosingMethodOrClass
154-
if encl.isClass then tparam.isParametricIn(encl)
155-
else
156-
def recur(encl: Symbol): Boolean =
157-
if tparam.owner == encl then true
158-
else if encl.isStatic || !encl.exists then false
159-
else recur(encl.owner.enclosingMethodOrClass)
160-
recur(encl)
161+
carrier.exists && {
162+
val encl = carrier.owner.enclosingMethodOrClass
163+
if encl.isClass then tparam.isParametricIn(encl)
164+
else
165+
def recur(encl: Symbol): Boolean =
166+
if tparam.owner == encl then true
167+
else if encl.isStatic || !encl.exists then false
168+
else recur(encl.owner.enclosingMethodOrClass)
169+
recur(encl)
170+
}
161171

162172
def traverse(t: Type) =
163173
t.dealiasKeepAnnots match
@@ -168,9 +178,12 @@ object CheckCaptures:
168178
t.info match
169179
case TypeBounds(_, hi) if !t.isSealed && !t.symbol.isParametricIn(carrier) =>
170180
if hi.isAny then
181+
val detailStr =
182+
if t eq tp then "variable"
183+
else i"refers to the type variable $t, which"
171184
report.error(
172185
em"""$what cannot $have $tp since
173-
|that type refers to the type variable $t, which is not sealed.
186+
|that type $detailStr is not sealed.
174187
|$addendum""",
175188
pos)
176189
else
@@ -549,7 +562,7 @@ class CheckCaptures extends Recheck, SymTransformer:
549562
for case (arg: TypeTree, formal, pname) <- args.lazyZip(polyType.paramRefs).lazyZip((polyType.paramNames)) do
550563
if formal.isSealed then
551564
def where = if fn.symbol.exists then i" in an argument of ${fn.symbol}" else ""
552-
disallowRootCapabilitiesIn(arg.knownType, fn.symbol,
565+
disallowRootCapabilitiesIn(arg.knownType, NoSymbol,
553566
i"Sealed type variable $pname", "be instantiated to",
554567
i"This is often caused by a local capability$where\nleaking as part of its result.",
555568
tree.srcPos)
@@ -590,13 +603,58 @@ class CheckCaptures extends Recheck, SymTransformer:
590603
openClosures = openClosures.tail
591604
end recheckClosureBlock
592605

606+
/** Maps mutable variables to the symbols that capture them (in the
607+
* CheckCaptures sense, i.e. symbol is referred to from a different method
608+
* than the one it is defined in).
609+
*/
610+
private val capturedBy = util.HashMap[Symbol, Symbol]()
611+
612+
/** Maps anonymous functions appearing as function arguments to
613+
* the function that is called.
614+
*/
615+
private val anonFunCallee = util.HashMap[Symbol, Symbol]()
616+
617+
/** Populates `capturedBy` and `anonFunCallee`. Called by `checkUnit`.
618+
*/
619+
private def collectCapturedMutVars(using Context) = new TreeTraverser:
620+
def traverse(tree: Tree)(using Context) = tree match
621+
case id: Ident =>
622+
val sym = id.symbol
623+
if sym.is(Mutable, butNot = Method) && sym.owner.isTerm then
624+
val enclMeth = ctx.owner.enclosingMethod
625+
if sym.enclosingMethod != enclMeth then
626+
capturedBy(sym) = enclMeth
627+
case Apply(fn, args) =>
628+
for case closureDef(mdef) <- args do
629+
anonFunCallee(mdef.symbol) = fn.symbol
630+
traverseChildren(tree)
631+
case Inlined(_, bindings, expansion) =>
632+
traverse(bindings)
633+
traverse(expansion)
634+
case mdef: DefDef =>
635+
if !mdef.symbol.isInlineMethod then traverseChildren(tree)
636+
case _ =>
637+
traverseChildren(tree)
638+
593639
override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type =
594640
try
595641
if sym.is(Module) then sym.info // Modules are checked by checking the module class
596642
else
597643
if sym.is(Mutable) && !sym.hasAnnotation(defn.UncheckedCapturesAnnot) then
598-
disallowRootCapabilitiesIn(tree.tpt.knownType, sym,
599-
i"mutable $sym", "have type", "", sym.srcPos)
644+
val (carrier, addendum) = capturedBy.get(sym) match
645+
case Some(encl) =>
646+
val enclStr =
647+
if encl.isAnonymousFunction then
648+
val location = anonFunCallee.get(encl) match
649+
case Some(meth) if meth.exists => i" argument in a call to $meth"
650+
case _ => ""
651+
s"an anonymous function$location"
652+
else encl.show
653+
(NoSymbol, i"\nNote that $sym does not count as local since it is captured by $enclStr")
654+
case _ =>
655+
(sym, "")
656+
disallowRootCapabilitiesIn(
657+
tree.tpt.knownType, carrier, i"Mutable $sym", "have type", addendum, sym.srcPos)
600658
checkInferredResult(super.recheckValDef(tree, sym), tree)
601659
finally
602660
if !sym.is(Param) then
@@ -1168,11 +1226,12 @@ class CheckCaptures extends Recheck, SymTransformer:
11681226
private val setup: SetupAPI = thisPhase.prev.asInstanceOf[Setup]
11691227

11701228
override def checkUnit(unit: CompilationUnit)(using Context): Unit =
1171-
setup.setupUnit(ctx.compilationUnit.tpdTree, completeDef)
1229+
setup.setupUnit(unit.tpdTree, completeDef)
1230+
collectCapturedMutVars.traverse(unit.tpdTree)
11721231

11731232
if ctx.settings.YccPrintSetup.value then
11741233
val echoHeader = "[[syntax tree at end of cc setup]]"
1175-
val treeString = show(ctx.compilationUnit.tpdTree)
1234+
val treeString = show(unit.tpdTree)
11761235
report.echo(s"$echoHeader\n$treeString\n")
11771236

11781237
withCaptureSetsExplained:

tests/neg-custom-args/captures/buffers.check

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- Error: tests/neg-custom-args/captures/buffers.scala:11:6 ------------------------------------------------------------
22
11 | var elems: Array[A] = new Array[A](10) // error // error
33
| ^
4-
| mutable variable elems cannot have type Array[A] since
4+
| Mutable variable elems cannot have type Array[A] since
55
| that type refers to the type variable A, which is not sealed.
66
-- Error: tests/neg-custom-args/captures/buffers.scala:16:38 -----------------------------------------------------------
77
16 | def make[A: ClassTag](xs: A*) = new ArrayBuffer: // error
@@ -14,13 +14,13 @@
1414
11 | var elems: Array[A] = new Array[A](10) // error // error
1515
| ^^^^^^^^
1616
| Array cannot have element type A since
17-
| that type refers to the type variable A, which is not sealed.
17+
| that type variable is not sealed.
1818
| Since arrays are mutable, they have to be treated like variables,
1919
| so their element type must be sealed.
2020
-- Error: tests/neg-custom-args/captures/buffers.scala:22:9 ------------------------------------------------------------
2121
22 | val x: Array[A] = new Array[A](10) // error
2222
| ^^^^^^^^
2323
| Array cannot have element type A since
24-
| that type refers to the type variable A, which is not sealed.
24+
| that type variable is not sealed.
2525
| Since arrays are mutable, they have to be treated like variables,
2626
| so their element type must be sealed.

tests/neg-custom-args/captures/levels.check

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
-- Error: tests/neg-custom-args/captures/levels.scala:6:16 -------------------------------------------------------------
22
6 | private var v: T = init // error
33
| ^
4-
| mutable variable v cannot have type T since
5-
| that type refers to the type variable T, which is not sealed.
4+
| Mutable variable v cannot have type T since
5+
| that type variable is not sealed.
66
-- Error: tests/neg-custom-args/captures/levels.scala:17:13 ------------------------------------------------------------
77
17 | val _ = Ref[String => String]((x: String) => x) // error
88
| ^^^^^^^^^^^^^^^^^^^^^
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
-- [E129] Potential Issue Warning: tests/neg-custom-args/captures/sealed-leaks.scala:31:6 ------------------------------
2+
31 | ()
3+
| ^^
4+
| A pure expression does nothing in statement position
5+
|
6+
| longer explanation available when compiling with `-explain`
7+
-- Error: tests/neg-custom-args/captures/sealed-leaks.scala:12:27 ------------------------------------------------------
8+
12 | val later2 = usingLogFile[(() => Unit) | Null] { f => () => f.write(0) } // error
9+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
10+
| Sealed type variable T cannot be instantiated to (() => Unit) | Null since
11+
| that type captures the root capability `cap`.
12+
| This is often caused by a local capability in an argument of method usingLogFile
13+
| leaking as part of its result.
14+
-- [E007] Type Mismatch Error: tests/neg-custom-args/captures/sealed-leaks.scala:19:26 ---------------------------------
15+
19 | usingLogFile { f => x = f } // error
16+
| ^
17+
| Found: (f : java.io.FileOutputStream^)
18+
| Required: (java.io.FileOutputStream | Null)^{cap[Test2]}
19+
|
20+
| longer explanation available when compiling with `-explain`
21+
-- Error: tests/neg-custom-args/captures/sealed-leaks.scala:30:10 ------------------------------------------------------
22+
30 | var x: T = y // error
23+
| ^
24+
| Mutable variable x cannot have type T since
25+
| that type variable is not sealed.
26+
-- Error: tests/neg-custom-args/captures/sealed-leaks.scala:39:8 -------------------------------------------------------
27+
39 | var x: T = y // error
28+
| ^
29+
| Mutable variable x cannot have type T since
30+
| that type variable is not sealed.
31+
|
32+
| Note that variable x does not count as local since it is captured by an anonymous function
33+
-- Error: tests/neg-custom-args/captures/sealed-leaks.scala:43:8 -------------------------------------------------------
34+
43 | var x: T = y // error
35+
| ^
36+
|Mutable variable x cannot have type T since
37+
|that type variable is not sealed.
38+
|
39+
|Note that variable x does not count as local since it is captured by an anonymous function argument in a call to method identity
40+
-- Error: tests/neg-custom-args/captures/sealed-leaks.scala:47:8 -------------------------------------------------------
41+
47 | var x: T = y // error
42+
| ^
43+
| Mutable variable x cannot have type T since
44+
| that type variable is not sealed.
45+
|
46+
| Note that variable x does not count as local since it is captured by method foo
47+
-- Error: tests/neg-custom-args/captures/sealed-leaks.scala:11:14 ------------------------------------------------------
48+
11 | val later = usingLogFile { f => () => f.write(0) } // error
49+
| ^^^^^^^^^^^^
50+
| local reference f leaks into outer capture set of type parameter T of method usingLogFile

tests/neg-custom-args/captures/sealed-leaks.scala

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,34 @@ def Test2 =
1818

1919
usingLogFile { f => x = f } // error
2020

21-
later()
21+
later()
22+
23+
def Test3 =
24+
def f[T](y: T) =
25+
var x: T = y
26+
()
27+
28+
class C[T](y: T):
29+
object o:
30+
var x: T = y // error
31+
()
32+
33+
class C2[T](y: T):
34+
def f =
35+
var x: T = y // ok
36+
()
37+
38+
def g1[T](y: T): T => Unit =
39+
var x: T = y // error
40+
y => x = y
41+
42+
def g2[T](y: T): T => Unit =
43+
var x: T = y // error
44+
identity(y => x = y)
45+
46+
def g3[T](y: T): Unit =
47+
var x: T = y // error
48+
def foo =
49+
x = y
50+
()
51+

0 commit comments

Comments
 (0)