Skip to content
This repository was archived by the owner on Jul 12, 2024. It is now read-only.

Fix #101: Interaction between TryFinally and Labeled/Return. #102

Merged
merged 1 commit into from
Apr 18, 2024

Conversation

sjrd
Copy link
Collaborator

@sjrd sjrd commented Apr 15, 2024

See the big "HERE BE DRAGONS" comment for details.

@sjrd sjrd force-pushed the fix-labeled-try-finally-return branch from 7720a48 to bfbb90c Compare April 15, 2024 12:30
@sjrd sjrd changed the title WIP Fix TryFinally combined with Labeled/Return. Fix #101: Interaction between TryFinally and Labeled/Return. Apr 15, 2024
@sjrd sjrd force-pushed the fix-labeled-try-finally-return branch from bfbb90c to 190a739 Compare April 15, 2024 12:37
@sjrd sjrd marked this pull request as ready for review April 15, 2024 12:37
@sjrd sjrd requested a review from tanishiking April 15, 2024 12:37
@sjrd
Copy link
Collaborator Author

sjrd commented Apr 15, 2024

Well, that was even harder than I thought ^^ But now it should handle all the possible weird situations.

@sjrd sjrd linked an issue Apr 15, 2024 that may be closed by this pull request
@sjrd sjrd force-pushed the fix-labeled-try-finally-return branch from 190a739 to a585b6b Compare April 16, 2024 07:18
Copy link
Owner

@tanishiking tanishiking left a 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 🎉

Comment on lines +2851 to +2966
case None =>
// Easy case: directly branch out of the block
instrs += BR(targetEntry.regularWasmLabel)
Copy link
Owner

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:
  // ...

Copy link
Collaborator Author

@sjrd sjrd Apr 17, 2024

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note]

Suggested change
* 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.

Copy link
Collaborator Author

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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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)
Copy link
Owner

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

Comment on lines +2490 to +2494
* 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.
Copy link
Owner

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`

Copy link
Collaborator Author

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.

Comment on lines 2508 to 2510
* 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.
Copy link
Owner

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 {
    // ...
  }
}

Comment on lines +2798 to +2915
for (nextTry <- nextTryFinallyEntry) {
// Transfer the destinationTag to the next try..finally in line
instrs += LOCAL_TEE(nextTry.requireCrossInfo()._1)
}
emitBRTable(brTableDests, doneLabel)
Copy link
Owner

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?

Copy link
Owner

@tanishiking tanishiking Apr 17, 2024

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

Copy link
Collaborator Author

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. ;)

@sjrd sjrd force-pushed the fix-labeled-try-finally-return branch from a585b6b to 3c489e8 Compare April 17, 2024 08:35
…Return`.

See the big "HERE BE DRAGONS" comment for details.
@sjrd sjrd force-pushed the fix-labeled-try-finally-return branch from 3c489e8 to 3da9ca2 Compare April 17, 2024 11:06
@sjrd
Copy link
Collaborator Author

sjrd commented Apr 17, 2024

I think I addressed all the comments.

@sjrd sjrd requested a review from tanishiking April 17, 2024 11:08
Copy link
Owner

@tanishiking tanishiking left a 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

@tanishiking tanishiking merged commit 1f19bad into tanishiking:main Apr 18, 2024
1 check passed
@sjrd sjrd deleted the fix-labeled-try-finally-return branch April 18, 2024 11:23
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

try..finally does not intercept Returns to enclosing Labeled blocks.
2 participants