Skip to content

Commit 289e48e

Browse files
lrytzsmarter
authored andcommitted
SI-10032 Fix code gen with returns in nested try-finally blocks
Return statements within `try` or `catch` blocks need special treatement if there's also a `finally` try { return 1 } finally { println() } For the return, the code generator emits a store to a local and a jump to a "cleanup" version of the finally block. There will be 3 versions of the finally block: - One reached through a handler, if the code in the try block throws; re-throws at the end - A "cleanup" version reached from returns within the try; reads the local and returns the value at the end - One reached for ordinary control flow, if there's no return and no exception within the try If there are multiple enclosing finally blocks, a "cleanup" version is emitted for each of them. The nested ones jump to the enclosing ones, the outermost one reads the local and returns. A global variable `shouldEmitCleanup` stores whether cleanup versions are required for the curren finally blocks. By mistake, this variable was not reset to `false` when emitting a `try-finally` nested within a `finally`: try { try { return 1 } finally { println() } // need cleanup version } finally { // need cleanup version try { println() } finally { println() } // no cleanup version needed! } In this commit we ensure that the variable is reset when emitting nested `try-finally` blocks.
1 parent 51853d6 commit 289e48e

File tree

3 files changed

+200
-5
lines changed

3 files changed

+200
-5
lines changed

compiler/src/dotty/tools/backend/jvm/BCodeSyncAndTry.scala

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
2828
// if the synchronized block returns a result, store it in a local variable.
2929
// Just leaving it on the stack is not valid in MSIL (stack is cleaned when leaving try-blocks).
3030
val hasResult = (expectedType != UNIT)
31-
val monitorResult: Symbol = if (hasResult) locals.makeLocal(tpeTK(args.head), "monitorResult", Object_Type, tree.pos) else null;
31+
val monitorResult: Symbol = if (hasResult) locals.makeLocal(tpeTK(args.head), "monitorResult", Object_Type, tree.pos) else null
3232

3333
/* ------ (1) pushing and entering the monitor, also keeping a reference to it in a local var. ------ */
3434
genLoadQualifier(fun)
@@ -206,7 +206,7 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
206206
* please notice `tmp` has type tree.tpe, while `earlyReturnVar` has the method return type.
207207
* Because those two types can be different, dedicated vars are needed.
208208
*/
209-
val tmp = if (guardResult) locals.makeLocal(tpeTK(tree), "tmp", tree.tpe, tree.pos) else null;
209+
val tmp = if (guardResult) locals.makeLocal(tpeTK(tree), "tmp", tree.tpe, tree.pos) else null
210210

211211
/*
212212
* upon early return from the try-body or one of its EHs (but not the EH-version of the finally-clause)
@@ -229,6 +229,34 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
229229
val endTryBody = currProgramPoint()
230230
bc goTo postHandlers
231231

232+
/**
233+
* A return within a `try` or `catch` block where a `finally` is present ("early return")
234+
* emits a store of the result to a local, jump to a "cleanup" version of the `finally` block,
235+
* and sets `shouldEmitCleanup = true` (see [[PlainBodyBuilder.genReturn]]).
236+
*
237+
* If the try-catch is nested, outer `finally` blocks need to be emitted in a cleanup version
238+
* as well, so the `shouldEmitCleanup` variable remains `true` until the outermost `finally`.
239+
* Nested cleanup `finally` blocks jump to the next enclosing one. For the outermost, we emit
240+
* a read of the local variable, a return, and we set `shouldEmitCleanup = false` (see
241+
* [[pendingCleanups]]).
242+
*
243+
* Now, assume we have
244+
*
245+
* try { return 1 } finally {
246+
* try { println() } finally { println() }
247+
* }
248+
*
249+
* Here, the outer `finally` needs a cleanup version, but the inner one does not. The method
250+
* here makes sure that `shouldEmitCleanup` is only propagated outwards, not inwards to
251+
* nested `finally` blocks.
252+
*/
253+
def withFreshCleanupScope(body: => Unit) = {
254+
val savedShouldEmitCleanup = shouldEmitCleanup
255+
shouldEmitCleanup = false
256+
body
257+
shouldEmitCleanup = savedShouldEmitCleanup || shouldEmitCleanup
258+
}
259+
232260
/* ------ (2) One EH for each case-clause (this does not include the EH-version of the finally-clause)
233261
* An EH in (2) is reached upon abrupt termination of (1).
234262
* An EH in (2) is protected by:
@@ -237,7 +265,7 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
237265
* ------
238266
*/
239267

240-
for (ch <- caseHandlers) {
268+
for (ch <- caseHandlers) withFreshCleanupScope {
241269

242270
// (2.a) emit case clause proper
243271
val startHandler = currProgramPoint()
@@ -271,6 +299,11 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
271299

272300
}
273301

302+
// Need to save the state of `shouldEmitCleanup` at this point: while emitting the first
303+
// version of the `finally` block below, the variable may become true. But this does not mean
304+
// that we need a cleanup version for the current block, only for the enclosing ones.
305+
val currentFinallyBlockNeedsCleanup = shouldEmitCleanup
306+
274307
/* ------ (3.A) The exception-handler-version of the finally-clause.
275308
* Reached upon abrupt termination of (1) or one of the EHs in (2).
276309
* Protected only by whatever protects the whole try-catch-finally expression.
@@ -279,7 +312,7 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
279312

280313
// a note on terminology: this is not "postHandlers", despite appearences.
281314
// "postHandlers" as in the source-code view. And from that perspective, both (3.A) and (3.B) are invisible implementation artifacts.
282-
if (hasFinally) {
315+
if (hasFinally) withFreshCleanupScope {
283316
nopIfNeeded(startTryBody)
284317
val finalHandler = currProgramPoint() // version of the finally-clause reached via unhandled exception.
285318
protect(startTryBody, finalHandler, finalHandler, null)
@@ -307,7 +340,7 @@ trait BCodeSyncAndTry extends BCodeBodyBuilder {
307340
// this is not "postHandlers" either.
308341
// `shouldEmitCleanup` can be set, and at the same time this try expression may lack a finally-clause.
309342
// In other words, all combinations of (hasFinally, shouldEmitCleanup) are valid.
310-
if (hasFinally && shouldEmitCleanup) {
343+
if (hasFinally && currentFinallyBlockNeedsCleanup) {
311344
val savedInsideCleanup = insideCleanupBlock
312345
insideCleanupBlock = true
313346
markProgramPoint(finCleanup)

tests/run/t10032.check

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
t10032.scala:72: warning: Return statement found in finally-clause, discarding its return-value in favor of that of a more deeply nested return.
2+
finally { return i2 }
3+
^
4+
t1
5+
i1
6+
a1
7+
t2
8+
i1
9+
a1
10+
a2
11+
a3
12+
t3
13+
i1
14+
a1
15+
a3
16+
t3
17+
e1
18+
a1
19+
i2
20+
a2
21+
a3
22+
t4
23+
i1
24+
i2
25+
t5
26+
i1
27+
a1
28+
a3
29+
t5
30+
e1
31+
a1
32+
i2
33+
a3
34+
t6
35+
i1
36+
i2
37+
i3
38+
t7
39+
i1
40+
a1
41+
t7
42+
e1
43+
i2
44+
a1
45+
t8
46+
i1
47+
i2
48+
a1
49+
a2

tests/run/t10032.scala

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
object Test extends App {
2+
def a1(): Unit = println(" a1")
3+
def a2(): Unit = println(" a2")
4+
def a3(): Unit = println(" a3")
5+
6+
def i1: Int = { println(" i1"); 1 }
7+
def i2: Int = { println(" i2"); 2 }
8+
def i3: Int = { println(" i3"); 3 }
9+
10+
def e1: Int = { println(" e1"); throw new Exception() }
11+
12+
def t1: Int = {
13+
println("t1")
14+
try {
15+
synchronized { return i1 }
16+
} finally {
17+
synchronized { a1() }
18+
}
19+
}
20+
21+
def t2: Int = {
22+
println("t2")
23+
try {
24+
try { return i1 }
25+
finally { a1() }
26+
} finally {
27+
try { a2() } finally { a3() }
28+
}
29+
}
30+
31+
def t3(i: => Int): Int = {
32+
println("t3")
33+
try {
34+
try { return i }
35+
finally { a1() }
36+
} catch {
37+
case _: Throwable =>
38+
try { i2 }
39+
finally { a2() } // no cleanup version
40+
} finally {
41+
a3()
42+
}
43+
}
44+
45+
def t4: Int = {
46+
println("t4")
47+
try {
48+
return i1
49+
} finally {
50+
return i2
51+
}
52+
}
53+
54+
def t5(i: => Int): Int = {
55+
println("t5")
56+
try {
57+
try {
58+
try { return i }
59+
finally { a1() }
60+
} catch {
61+
case _: Throwable => i2
62+
}
63+
} finally {
64+
a3()
65+
}
66+
}
67+
68+
def t6: Int = {
69+
println("t6")
70+
try {
71+
try { return i1 }
72+
finally { return i2 }
73+
} finally {
74+
return i3
75+
}
76+
}
77+
78+
def t7(i: => Int): Int = {
79+
println("t7")
80+
try { i }
81+
catch {
82+
case _: Throwable =>
83+
return i2
84+
} finally {
85+
a1() // cleanup required, early return in handler
86+
}
87+
}
88+
89+
def t8(): Int = {
90+
println("t8")
91+
try {
92+
try { i1 }
93+
finally { // no cleanup version
94+
try { return i2 }
95+
finally { a1() } // cleanup version required
96+
}
97+
} finally { // cleanup version required
98+
a2()
99+
}
100+
}
101+
102+
assert(t1 == 1)
103+
assert(t2 == 1)
104+
assert(t3(i1) == 1)
105+
assert(t3(e1) == 2)
106+
assert(t4 == 2)
107+
assert(t5(i1) == 1)
108+
assert(t5(e1) == 2)
109+
assert(t6 == 3)
110+
assert(t7(i1) == 1)
111+
assert(t7(e1) == 2)
112+
assert(t8 == 2)
113+
}

0 commit comments

Comments
 (0)