Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should the instructions in an unwind block return results? #129

Closed
ioannad opened this issue Oct 1, 2020 · 14 comments
Closed

Should the instructions in an unwind block return results? #129

ioannad opened this issue Oct 1, 2020 · 14 comments

Comments

@ioannad
Copy link
Collaborator

ioannad commented Oct 1, 2020

In my draft formal spec for this (3rd) EH proposal, in the validation step for try bt instr_1* unwind instr_2* end, the unwind-instructions instr_2* are required to have blocktype []->[].

I think this makes sense because unwind probably just concerns side effects (open/close a file, alter a mutable global, etc). And although instr_2* will only get executed while the stack is being unwound due to an exception throw, instr_2* could contain a br or a return, in which case such result values would be ignored.

Should instr_2* be allowed to return values?

@RossTate
Copy link
Contributor

RossTate commented Oct 1, 2020

In order to support finally clauses from most languages, it is necessary to be able to branch/return out of unwind blocks (i.e. out of instr_2*). If this happens, the unwinding process (and exception throw) ends.

@ioannad
Copy link
Collaborator Author

ioannad commented Oct 6, 2020

In order to support finally clauses from most languages, it is necessary to be able to branch/return out of unwind blocks (i.e. out of instr_2*). If this happens, the unwinding process (and exception throw) ends.

@RossTate, I am not sure how finally works in various languages, but I agree that unwind blocks returning or trapping, should probably end the exception throw and unwinding process. My question is rather what happens with the return values of instr_2*, if we allow instr_2* to return values, that is.

Now that I think about it, I am also unsure what should happen when instr_2* breaks to a label "inside" the catching try block.

@RossTate
Copy link
Contributor

RossTate commented Oct 6, 2020

A return inside an unwind block should end the exception throw and unwinding process and then return the values. As for your second question, instr_2* should not syntactically be able to refer to labels inside instr_1* (and consequently can't branch to them).

@ioannad
Copy link
Collaborator Author

ioannad commented Oct 6, 2020

instr_2* should not syntactically be able to refer to labels inside instr_1* (and consequently can't branch to them).

Sorry I wasn't clear, this is not what I meant with "a label inside" the catching try block, perhaps "a label between" is a better way to put it.

To be concrete, I was thinking of something like the following example:

try $out
  try $mid
    try $in
      throw x
    unwind
      br $mid
    end
  catch y   ;; y =/= x
    instr1*
  end
  instr2*
catch x
  instr3*
end
instr4*

This is currently possible, I think, and the thrown exception from inside $in is caught by the catch of $out. But when the "cleanup form" of $in's unwind tries to break to $mid, I can't see what should happen, because in my eyes at that point $mid's "dynamic extent" ends, so to say in common-lisp terms. I guess here one could say $mid has been unwound when the cleanup form br $mid gets executed.

@RossTate
Copy link
Contributor

RossTate commented Oct 7, 2020

Thanks for the concrete example! To support unwinding clauses in various languages (like finally), this program should execute as follows:

  1. throw x starts a search for an appropriate catch, unwinding along the way.
  2. The unwind clause for $in gets executed such that if its end is reached the throw/search/unwinding will continue.
  3. This causes br $mid to execute, which ends the unwinding/searching process because it branches out of the unwind clause, and then jumps to $mid.
  4. instr2* execute, within the context where instr3* will execute if an x gets thrown.
  5. instr4* execute unless there's some exceptional control flow during instr2* or instr3*.

Does that make sense?

@tlively
Copy link
Member

tlively commented Oct 8, 2020

Ross, the semantics you describe make sense to me, but I'm not clear on the motivation behind them. You've mentioned that branching out of unwind ending unwinding is necessary to compile finally clauses in multiple languages. Can you share a small end-to-end example demonstrating how that works?

@aheejin
Copy link
Member

aheejin commented Oct 8, 2020

@RossTate is suggesting that we should be able to use unwind to compile finally, but I don't think that's something we agreed on. unwind contains cleanup code that's executed in the exception path, not the normal path. (And for one LLVM compiles cleanup code for the exception path and the normal path separately.) This is the reason why we don't run unwind blocks between when we branch out from an inner scope to an outer scope. In the current version of the proposal, the semantics of unwind is the same as catch_all, except that it's end causes the exception to be rethrown.

As I said, LLVM compiles away finally, and many compilers that uses CFG probably do the same thing. But in case someone really wants the behavior of finally in the spec level, I think we should add finally separately. So far we don't have a plan to support such a language with such a compiler that needs that feature, but if someone can present a concrete use case, I think we can consider it.

@aheejin
Copy link
Member

aheejin commented Oct 8, 2020

@ioannad In your code example, I think the execution sequence should be,

  1. Run throw x
  2. Enter unwind block and run br $mid
  3. Branch out to the end of try $mid and run instr2*
    4-1. In case instr2* doesn't throw, run instr4*.
    4-2. In case instr2* throws, check catch x, and if the new exception is caught by catch x, run instr3*. If it's not caught by catch x, throw the exception up to the caller.

@ioannad
Copy link
Collaborator Author

ioannad commented Oct 8, 2020

In the current version of the proposal, the semantics of unwind is the same as catch_all, except that it's end causes the exception to be rethrown.

@aheejin I am not sure where I should comment on this. I commented on the relevant part of your PR#137 but we could talk about this here instead if you think that's better.

@RossTate
Copy link
Contributor

RossTate commented Oct 8, 2020

There are two aspects to finally. One aspect needs unwind, whereas the other aspect can make use of unwind to both reduce code size and ease generation (for non-LLVM compilers).

As an example, take the following Java code (with instructions sequences A, B, and C):

void foo() {
  try {
    A
  } catch (Exception e) {
    B
  } finally {
    C
  }
}

From a generator standpoint, the easiest thing would be to compile this to

(func $void
  (try
    (try
      compiled-A
    catch $java_exn
      compiled-B
    end)
  unwind
    compiled-C
  end)
)

This would be possible with the unwinding extension (#124), because one could compile a Java return to a wasm unwinding return. But it's not clear if such an extension is compatible with "skip over" delegate (#130).

Without that, we instead generate the following:

(func $void
  (block $fault-free-finally
    (try
      (try
        compiled-A[return |-> br $fault-free-finally]
      catch $java_exn
        compiled-B[return |-> br $fault-free-finally]
      end)
    unwind
      compiled-C
    end)
    return
  end)
  compiled-C
)

Note that we still need to put compiled-C in an unwind clause here so that it gets run if an exception is thrown by either compiled-A or compiled-B. And if C has a return in it, compiled-C has a return in it as well (according to Java's semantics).


To summarize, we need unwind to support at least the exception path of finally for Java (and many other languages). Consequently, we need to be able to return (and br) from unwind clauses to support these languages. A generator could optionally use an unwinding extension to furthermore consolidate paths, but it's unclear if/how such an extension can be added. (That said, finally is not the real application of unwinding. I'm including discussion of it for the sake of completeness, since it was mentioned above.)

@aheejin
Copy link
Member

aheejin commented Oct 8, 2020

@RossTate

I hope we can keep issues to their original author's questions; so in this issue I'd like discuss semantics of unwind in the current spec and not its interactions with hypothetical future proposals in your plan.

To answer your question, I think in the current status of unwind can contain exception path of finally, but not the normal path, which is not in the intended behavior of this instruction anyway. If you want to support finally (both normal and exception path) in the spec level, I think having finally instruction directly is more intuitive and easier to implement than introducing an instruction like your unwinding branch that changes the behavior of existing instructions. Surely, we need to gauge whether we have a concrete plan to support such use cases in future.

But as I said, I hope we can focus on the semantics of current unwind in this issue, and that's what @ioannad asked.

@aheejin
Copy link
Member

aheejin commented Oct 8, 2020

@ioannad

I don't think we necessarily need a restriction on block types for try-unwind blocks. Compilers can possibly make use of the return type for optimizations; for example when try return values and unwind doesn't, compilers can make unwind return dummy values, in case it's better in terms of code size or something. But more importantly, I wouldn't want to make try-unwind special and different from other block types. All existing block types (block, loop, and try for catch) don't have any restrictions on their block type, and I think it would be inconsistent only try-unwind has some restrictions on its block type. If compilers don't need that block type, they can just set it to void.

@aheejin
Copy link
Member

aheejin commented Oct 12, 2020

So the original question was whether unwind block should be able to return results or not, and I think it should. If there's no objections on this, can we close this issue?

@ioannad
Copy link
Collaborator Author

ioannad commented Oct 12, 2020

Thank you, @aheejin, and sorry I involved a more complex question on the semantics of unwind. This discussion is currently on your PR #137 anyway, so I close this as you suggested.

@ioannad ioannad closed this as completed Oct 12, 2020
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Feb 23, 2021
Ms2ger pushed a commit to Ms2ger/exception-handling that referenced this issue Jun 24, 2021
* [interpreter] Simplify zero-len and drop semantics

* Update overview

* [spec] Change drop semantics

* [spec] Forgot to adjust prose for *.init ops

* [spec] Adapt to early OOB checks

* [spec] Fix OOB for table rules

* [spec] Spec memory OOB

* [spec] Extend store typing to elem and data instances

* Apply suggestions from code review

Co-Authored-By: Ryan Hunt <[email protected]>

* Comments

* [spec] Fix uses of table.set
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants