Skip to content

Commit fa0b169

Browse files
Merge pull request #5910 from dotty-staging/fix/return-in-try
Fix code gen with returns in nested try-finally blocks
2 parents 162d748 + 955c560 commit fa0b169

File tree

5 files changed

+288
-18
lines changed

5 files changed

+288
-18
lines changed

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -563,16 +563,11 @@ trait BCodeBodyBuilder extends BCodeSkelBuilder {
563563
bc emitRETURN returnType
564564
case nextCleanup :: rest =>
565565
if (saveReturnValue) {
566-
if (insideCleanupBlock) {
567-
int.warning(r.pos, "Return statement found in finally-clause, discarding its return-value in favor of that of a more deeply nested return.")
568-
bc drop returnType
569-
} else {
570-
// regarding return value, the protocol is: in place of a `return-stmt`, a sequence of `adapt, store, jump` are inserted.
571-
if (earlyReturnVar == null) {
572-
earlyReturnVar = locals.makeLocal(returnType, "earlyReturnVar", expr.tpe, expr.pos)
573-
}
574-
locals.store(earlyReturnVar)
566+
// regarding return value, the protocol is: in place of a `return-stmt`, a sequence of `adapt, store, jump` are inserted.
567+
if (earlyReturnVar == null) {
568+
earlyReturnVar = locals.makeLocal(returnType, "earlyReturnVar", expr.tpe, expr.pos)
575569
}
570+
locals.store(earlyReturnVar)
576571
}
577572
bc goTo nextCleanup
578573
shouldEmitCleanup = true

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,6 @@ trait BCodeSkelBuilder extends BCodeHelpers {
255255
// used by genLoadTry() and genSynchronized()
256256
var earlyReturnVar: Symbol = null
257257
var shouldEmitCleanup = false
258-
var insideCleanupBlock = false
259258
// line numbers
260259
var lastEmittedLineNr = -1
261260

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

Lines changed: 38 additions & 8 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,14 +340,11 @@ 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) {
311-
val savedInsideCleanup = insideCleanupBlock
312-
insideCleanupBlock = true
343+
if (hasFinally && currentFinallyBlockNeedsCleanup) {
313344
markProgramPoint(finCleanup)
314345
// regarding return value, the protocol is: in place of a `return-stmt`, a sequence of `adapt, store, jump` are inserted.
315346
emitFinalizer(finalizer, null, isDuplicate = true)
316347
pendingCleanups()
317-
insideCleanupBlock = savedInsideCleanup
318348
}
319349

320350
/* ------ (4) finally-clause-for-normal-nonEarlyReturn-exit

tests/run/t10032.check

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
t1
2+
i1
3+
a1
4+
t2
5+
i1
6+
a1
7+
a2
8+
a3
9+
t3
10+
i1
11+
a1
12+
a3
13+
t3
14+
e1
15+
a1
16+
i2
17+
a2
18+
a3
19+
t4
20+
i1
21+
i2
22+
t4
23+
e1
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+
t6
39+
e1
40+
i2
41+
i3
42+
t7
43+
i1
44+
a1
45+
t7
46+
e1
47+
i2
48+
a1
49+
t8
50+
i1
51+
i2
52+
a1
53+
a2
54+
t8
55+
e1
56+
i2
57+
a1
58+
a2
59+
t9
60+
i1
61+
i2
62+
a1
63+
t9
64+
e1
65+
i2
66+
a1
67+
t10
68+
i1
69+
i2
70+
i3
71+
t10
72+
e1
73+
i2
74+
i3
75+
t11
76+
i1
77+
i2
78+
a1
79+
t11
80+
e1
81+
i2
82+
a1

tests/run/t10032.scala

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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() }
28+
finally { a3() }
29+
}
30+
}
31+
32+
def t3(i: => Int): Int = {
33+
println("t3")
34+
try {
35+
try { return i }
36+
finally { a1() }
37+
} catch {
38+
case _: Throwable =>
39+
try { i2 }
40+
finally { a2() } // no cleanup version
41+
} finally {
42+
a3()
43+
}
44+
}
45+
46+
def t4(i: => Int): Int = {
47+
println("t4")
48+
try {
49+
return i
50+
} finally {
51+
return i2
52+
}
53+
}
54+
55+
def t5(i: => Int): Int = {
56+
println("t5")
57+
try {
58+
try {
59+
try { return i }
60+
finally { a1() }
61+
} catch {
62+
case _: Throwable => i2
63+
}
64+
} finally {
65+
a3()
66+
}
67+
}
68+
69+
def t6(i: => Int): Int = {
70+
println("t6")
71+
try {
72+
try { return i }
73+
finally { return i2 }
74+
} finally {
75+
return i3
76+
}
77+
}
78+
79+
def t7(i: => Int): Int = {
80+
println("t7")
81+
try { i }
82+
catch {
83+
case _: Throwable =>
84+
return i2
85+
} finally {
86+
a1() // cleanup required, early return in handler
87+
}
88+
}
89+
90+
def t8(i: => Int): Int = {
91+
println("t8")
92+
try {
93+
try { i }
94+
finally { // no cleanup version
95+
try { return i2 }
96+
finally { a1() } // cleanup version required
97+
}
98+
} finally { // cleanup version required
99+
a2()
100+
}
101+
}
102+
103+
def t9(i: => Int): Int = {
104+
println("t9")
105+
try {
106+
return i
107+
} finally {
108+
try { return i2 }
109+
finally { a1() }
110+
}
111+
}
112+
113+
def t10(i: => Int): Int = {
114+
println("t10")
115+
try {
116+
return i
117+
} finally {
118+
try { return i2 }
119+
finally { return i3 }
120+
}
121+
}
122+
123+
// this changed semantics between 2.12.0 and 2.12.1, see https://github.com/scala/scala/pull/5509#issuecomment-259291609
124+
def t11(i: => Int): Int = {
125+
println("t11")
126+
try {
127+
try { return i }
128+
finally { return i2 }
129+
} finally {
130+
a1()
131+
}
132+
}
133+
134+
assert(t1 == 1)
135+
136+
assert(t2 == 1)
137+
138+
assert(t3(i1) == 1)
139+
assert(t3(e1) == 2)
140+
141+
assert(t4(i1) == 2)
142+
assert(t4(e1) == 2)
143+
144+
assert(t5(i1) == 1)
145+
assert(t5(e1) == 2)
146+
147+
assert(t6(i1) == 3)
148+
assert(t6(e1) == 3)
149+
150+
assert(t7(i1) == 1)
151+
assert(t7(e1) == 2)
152+
153+
assert(t8(i1) == 2)
154+
assert(t8(e1) == 2)
155+
156+
assert(t9(i1) == 2)
157+
assert(t9(e1) == 2)
158+
159+
assert(t10(i1) == 3)
160+
assert(t10(e1) == 3)
161+
162+
assert(t11(i1) == 2)
163+
assert(t11(e1) == 2)
164+
}

0 commit comments

Comments
 (0)