-
Notifications
You must be signed in to change notification settings - Fork 3
Fix #101: Interaction between TryFinally
and Labeled
/Return
.
#102
Fix #101: Interaction between TryFinally
and Labeled
/Return
.
#102
Conversation
7720a48
to
bfbb90c
Compare
TryFinally
and Labeled
/Return
.
bfbb90c
to
190a739
Compare
Well, that was even harder than I thought ^^ But now it should handle all the possible weird situations. |
190a739
to
a585b6b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much @sjrd great work!
I left several comments and questions, but, overall, LGTM 🎉
case None => | ||
// Easy case: directly branch out of the block | ||
instrs += BR(targetEntry.regularWasmLabel) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do I understand correctly that
- Scala3's boundary break is also compiled to
Labeled/Return
? - We hit this case in the following case?
import scala.util.boundary, boundary.break
try:
boundary: // Labeled(l)
// ...
break(x) // Return(l)
finally:
// ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scala 3's boundary/break is indeed compiled to Labeled/Return
. However the optimization is explicitly not allowed if there is a try..finally
in the middle of the boundary/break
. In that case it falls back to actually throwing and catching an exception. That's because the JVM backend cannot deal with it. The JVM backend only handles return
in that situation.
We have to deal with it because a return
keyword in Scala source is compiled as a Labeled/Return
as well. After inlining (once we enable the optimizer), we can truly get arbitrary combinations.
The case that would result in what we have here would be:
boundary:
try
break(x)
finally
...
When both the boundary
and break
are inside the try
, there is no issue because we're not crossing the try..finally.
* block $catch (result exnref) | ||
* block $cross | ||
* try_table (catch_all_ref $catch) | ||
* body |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[note]
* body | |
* body ;; `Return` in body will be compiled to `br $cross` |
instrs += BR_ON_NULL(doneLabel) | ||
instrs += THROW_REF | ||
} else { | ||
// If the `exnref` is non-null, rethrow it, but stay within the `$done` block |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// If the `exnref` is non-null, rethrow it, but stay within the `$done` block | |
// If the `exnref` is non-null, rethrow it | |
// Otherwise, stay within the `$done` block |
You meant like this?
It's understandable to stay in $done
if none of the uncaught exception is thrown (this is the case where (1) we hit Return
(destinationTag
= 0, fall-through) or (2) executed return without throwing exception / exception is caught by catch clauses.
We need to jump to the appropriate destination using br_table
.
IIUC, throw_ref
goes out from the $done
block to find an enclosing try-catch-finally block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, yes, good catch!
} | ||
|
||
/* Otherwise, use a br_table to dispatch to the right destination | ||
* based on the value of the function's destinationTagLocal. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* based on the value of the function's destinationTagLocal. | |
* based on the value of the try..finally's destinationTagLocal, which is set by `Return` or 0 as fall-through. |
Is this more accurate? I couldn't find what is the "function" in this context.
destinationTag -> label | ||
} | ||
|
||
instrs += LOCAL_GET(entry.requireCrossInfo()._1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[note]
Set by genReturn
or 0 for fall-through
* Now what if there are *several* labels for which we cross that | ||
* `try..finally`? Well we need to deal with all the possible labels. This | ||
* means that, in general, we in fact have `2 + n` possible outcomes, where | ||
* `n` is the number of labels for which we found a `Return` that crosses the | ||
* boundary. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[note]
alpha[int]: {
beta[int]: {
try {
// set `destinationTag` of the try..finally to `alpha`,
// store the result into locals,
// and go-to finally block
if (A) return@alpha 5
else if (B) return@beta 10
else //...
} finally {
doTheFinally()
// Where we jump to after the `finally` block?
// Jump to the appropriate position based on the
// `destionalTag` of this try..finally block, set by return or 0 as fall-through
}
// fall-through (destinationTag = 0)
someOtherThings(bar)
}
// destinationTag for `beta`
}
// destionalTag for `alpha`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I elaborated with a complete example in the comments, with the description of how it gets compiled, based on your example here.
* One more complication: if the `finally` block itself contains another | ||
* `try..finally`, they may need a `destinationTag` concurrently. Therefore, | ||
* every `try..finally` gets its own `destinationTag` local. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[note]
alpha[int]: {
try {
try {
// ...
} finally {
// ...
// There's outer try..finally block (inside `alpha` Labeled block)
// In this case, we need to execute the outer `finally` before jump to `alpha`
}
// fall-through
} finally {
// ...
}
}
for (nextTry <- nextTryFinallyEntry) { | ||
// Transfer the destinationTag to the next try..finally in line | ||
instrs += LOCAL_TEE(nextTry.requireCrossInfo()._1) | ||
} | ||
emitBRTable(brTableDests, doneLabel) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[solved] 😅
Added test that the fall-through is executed before the outer finally block
diff --git a/test-suite/src/main/scala/testsuite/core/TryFinallyReturnTest.scala b/test-suite/src/main/scala/testsuite/core/TryFinallyReturnTest.scala
index 9a71700..2b3d15c 100644
--- a/test-suite/src/main/scala/testsuite/core/TryFinallyReturnTest.scala
+++ b/test-suite/src/main/scala/testsuite/core/TryFinallyReturnTest.scala
@@ -45,6 +45,12 @@ Running nestedFinallyBlocks
in finally 1
in finally 2
----------------------------------------
+Running nestedFinallyBlocks2
+in try 1
+in finally 1
+in fall-through
+in finally 2
+----------------------------------------
"""
def main(): Unit = {
@@ -56,6 +62,7 @@ in finally 2
test(retFinally(), "retFinally")
test(throwFinally(), "throwFinally")
test(nestedFinallyBlocks(), "nestedFinallyBlocks")
+ test(nestedFinallyBlocks2(), "nestedFinallyBlocks2")
assertSame(expectedOutput, printlnOutput)
}
@@ -162,6 +169,19 @@ in finally 2
println("in finally 2")
}
+ def nestedFinallyBlocks2(): Int =
+ try {
+ try {
+ println("in try 1")
+ } finally {
+ println("in finally 1")
+ }
+ println("in fall-through")
+ 0
+ } finally {
+ println("in finally 2")
+ }
+
def test[A](m: => A, name: String): Unit = {
println("Running %s".format(name))
try {
and this test passed. While I assumed that this test will fail because it seems that we anyway overwrite the destinationTag to the outer finally block instead of the fall-through.
How this test is passing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, never mind, possibleTargetEntries
doesn't contain the fall-thgrough
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the test anyway. ;)
a585b6b
to
3c489e8
Compare
…Return`. See the big "HERE BE DRAGONS" comment for details.
3c489e8
to
3da9ca2
Compare
I think I addressed all the comments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thank you very much for elaborating on things
See the big "HERE BE DRAGONS" comment for details.