Skip to content

Commit 4886e94

Browse files
committed
Initial lazy vals support
1 parent ec60015 commit 4886e94

File tree

4 files changed

+114
-6
lines changed

4 files changed

+114
-6
lines changed

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ object CheckCaptures:
8181
end Env
8282

8383
def definesEnv(sym: Symbol)(using Context): Boolean =
84-
sym.is(Method) || sym.isClass
84+
sym.is(Method) || sym.isClass || sym.is(Lazy)
8585

8686
/** Similar normal substParams, but this is an approximating type map that
8787
* maps parameters in contravariant capture sets to the empty set.
@@ -225,7 +225,7 @@ object CheckCaptures:
225225
def needsSepCheck: Boolean
226226

227227
/** If a tree is an argument for which needsSepCheck is true,
228-
* the type of the formal paremeter corresponding to the argument.
228+
* the type of the formal parameter corresponding to the argument.
229229
*/
230230
def formalType: Type
231231

@@ -441,7 +441,7 @@ class CheckCaptures extends Recheck, SymTransformer:
441441
*/
442442
def capturedVars(sym: Symbol)(using Context): CaptureSet =
443443
myCapturedVars.getOrElseUpdate(sym,
444-
if sym.isTerm || !sym.owner.isStaticOwner
444+
if sym.isTerm || !sym.owner.isStaticOwner || sym.is(Lazy) // FIXME: are lazy vals in static owners a thing?
445445
then CaptureSet.Var(sym, nestedOK = false)
446446
else CaptureSet.empty)
447447

@@ -655,8 +655,10 @@ class CheckCaptures extends Recheck, SymTransformer:
655655
*/
656656
override def recheckIdent(tree: Ident, pt: Type)(using Context): Type =
657657
val sym = tree.symbol
658-
if sym.is(Method) then
659-
// If ident refers to a parameterless method, charge its cv to the environment
658+
if sym.is(Method) || sym.is(Lazy) then
659+
// If ident refers to a parameterless method or lazy val, charge its cv to the environment.
660+
// Lazy vals are like parameterless methods: accessing them may trigger initialization
661+
// that uses captured references.
660662
includeCallCaptures(sym, sym.info, tree)
661663
else if sym.exists && !sym.isStatic then
662664
markPathFree(sym.termRef, pt, tree)
@@ -1083,6 +1085,7 @@ class CheckCaptures extends Recheck, SymTransformer:
10831085
* - for externally visible definitions: check that their inferred type
10841086
* does not refine what was known before capture checking.
10851087
* - Interpolate contravariant capture set variables in result type.
1088+
* - for lazy vals: create a nested environment to track captures (similar to methods)
10861089
*/
10871090
override def recheckValDef(tree: ValDef, sym: Symbol)(using Context): Type =
10881091
val savedEnv = curEnv
@@ -1105,8 +1108,16 @@ class CheckCaptures extends Recheck, SymTransformer:
11051108
""
11061109
disallowBadRootsIn(
11071110
tree.tpt.nuType, NoSymbol, i"Mutable $sym", "have type", addendum, sym.srcPos)
1108-
if runInConstructor then
1111+
1112+
// Lazy vals need their own environment to track captures from their RHS,
1113+
// similar to how methods work
1114+
if sym.is(Lazy) then
1115+
val localSet = capturedVars(sym)
1116+
if localSet ne CaptureSet.empty then
1117+
curEnv = Env(sym, EnvKind.Regular, localSet, curEnv, nestedClosure = NoSymbol)
1118+
else if runInConstructor then
11091119
pushConstructorEnv()
1120+
11101121
checkInferredResult(super.recheckValDef(tree, sym), tree)
11111122
finally
11121123
if !sym.is(Param) then
@@ -1120,6 +1131,9 @@ class CheckCaptures extends Recheck, SymTransformer:
11201131
if runInConstructor && savedEnv.owner.isClass then
11211132
curEnv = savedEnv
11221133
markFree(declaredCaptures, tree, addUseInfo = false)
1134+
else if sym.is(Lazy) then
1135+
// Restore environment after checking lazy val
1136+
curEnv = savedEnv
11231137

11241138
if sym.owner.isStaticOwner && !declaredCaptures.elems.isEmpty && sym != defn.captureRoot then
11251139
def where =
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import language.experimental.captureChecking
2+
import caps.*
3+
4+
class Console extends SharedCapability:
5+
def println(msg: String): Unit = Predef.println("CONSOLE: " + msg)
6+
7+
@main def run =
8+
val console: Console^ = Console()
9+
lazy val x: () -> String = {
10+
console.println("Computing x")
11+
() => "Hello, World!"
12+
}
13+
14+
val fun: () ->{console} String = () => x() // ok
15+
val fun2: () -> String = () => x() // error
16+
val fun3: () ->{x} String = () => x() // error // error
17+
18+
println("Before accessing x")
19+
println(s"x = ${x()}")
20+
println(s"x again = ${x()}")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import language.experimental.captureChecking
2+
import caps.*
3+
4+
class Console extends SharedCapability:
5+
def println(msg: String): Unit = Predef.println("CONSOLE: " + msg)
6+
7+
class IO extends SharedCapability:
8+
def readLine(): String = scala.io.StdIn.readLine()
9+
10+
@main def run =
11+
val console: Console^ = Console()
12+
val io: IO^ = IO()
13+
lazy val x: () ->{io} String = {
14+
console.println("Computing x")
15+
() => io.readLine()
16+
}
17+
18+
val fun: () ->{console,io} String = () => x() // ok
19+
val fun2: () ->{io} String = () => x() // error
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import language.experimental.captureChecking
2+
import caps.*
3+
4+
class C
5+
type Cap = C^
6+
7+
class Console extends SharedCapability:
8+
def println(msg: String): Unit = Predef.println("CONSOLE: " + msg)
9+
10+
class IO extends SharedCapability:
11+
def readLine(): String = scala.io.StdIn.readLine()
12+
13+
def test(c: Cap, console: Console^, io: IO^): Unit =
14+
lazy val ev: (Int -> Boolean) = (n: Int) =>
15+
lazy val od: (Int -> Boolean) = (n: Int) =>
16+
if n == 1 then true else ev(n - 1)
17+
if n == 0 then true else od(n - 1)
18+
19+
// In a mutually recursive lazy val, the result types accumulate the captures of both the initializers and results themselves.
20+
// So, this is not ok:
21+
lazy val ev1: (Int ->{io,console} Boolean) =
22+
println(c)
23+
(n: Int) =>
24+
lazy val od1: (Int ->{ev1,console} Boolean) = (n: Int) => // error
25+
if n == 1 then
26+
console.println("CONSOLE: 1")
27+
true
28+
else
29+
ev1(n - 1)
30+
if n == 0 then
31+
io.readLine() // just to capture io
32+
true
33+
else
34+
od1(n - 1)
35+
36+
// But this is ok:
37+
lazy val ev2: (Int ->{c,io,console} Boolean) =
38+
println(c)
39+
(n: Int) =>
40+
lazy val od2: (Int ->{c,io,console} Boolean) = (n: Int) =>
41+
if n == 1 then
42+
console.println("CONSOLE: 1")
43+
true
44+
else
45+
ev2(n - 1)
46+
if n == 0 then
47+
io.readLine() // just to capture io
48+
true
49+
else
50+
od2(n - 1)
51+
52+
val even: Int -> Boolean = (n: Int) => ev(n) // ok
53+
val even2: Int ->{io,console,c} Boolean = (n: Int) => ev1(n) // ok
54+
55+
()

0 commit comments

Comments
 (0)