-
Notifications
You must be signed in to change notification settings - Fork 36
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
Problem of rethrow instruction #30
Comments
I like this suggestion, and I think it fits nicely within the existing proposal. Most of the concepts needed to make this happen are already lurking under the covers. This proposal would just bring them to the surface and make them accessible to Wasm. Here's how I would frame it: We keep all the terminology given here by @rossberg. Then we add a new base type in addition to Note that we do not add any new memory instructions, meaning there is no way to store a Next, we change the typing rule for Here are a couple examples of how this works: (try
...
(catch_all
rethrow)) In this case, (try
....
(catch_all
(try
...
(catch_all
<code goes here>)))) If we filled in However, once we do this, it's pretty straightforward to allow tagged values in local variables: (try
...
(catch_all
(set_local 0)
...
(rethrow (get_local 0))) However, once we have this much, I'd propose some more ambitious changes. Here are some of them, in order of increasing radicalness. First, adding (match <tag index>
<match body>
(else
<else body>)) A
Including the tagged value on the stack for the else body allows for easy chaining of matches, such as: (match 0
...
(else (match 1
...
(else unreachable))) For code size concerns, we may even want to introduce a new opcode for A So now there's no reason to have a separation between Now the original code, (try
...
(catch 0
<catch 0>)
(catch 1
<catch 1>)
(catch_all
<catch all>)) becomes (try
...
(catch
(match 0
<catch 0>
(else-match 1
<catch 1>
(else
<catch all>))))) Next, I'd propose adding another instruction, (i32.const 1)
(i32.const 2)
(tag 0) The typing rule would be that At this point, we have tagged values that can be created and used completely independently of exceptions, which could have other uses. Now we can also get rid of the distinction between (i32.const 1)
(i32.const 2)
(throw 0) Now we would write: (i32.const 1)
(i32.const 2)
(tag 0)
throw This means we only have one There was a question in the original post about lifetimes. I think these are actually not a problem. Right now, tagged values can only exist on the stack, meaning we don't have to worry about indefinite extend. Once they go out of scope, they are safe to clean up. These will usually be backed by JS objects anyway, so we may represent tagged values as a handle to the JS object so that the garbage collector keeps it alive. The more challenging issue is what to do about stack traces. In the existing proposal, I believe exceptions carry a stack trace, because JavaScript needs to be able to show this. We could say that all tagged values carries the stack of when they were created with them, but this seems less than ideal. For example, it may impose constraints on VMs to keep stacks around for no reason, especially if tagged values end up being used for things besides exceptions. Perhaps a better option is to add another type, |
One potential downside to my proposal is that it might limit our options for adding resumable exceptions in the future. If we want this, it'd probably be could to tweak my proposal to anticipate this. The currently existing exceptions proposal already seems to anticipate resumable exceptions. |
@eholk, I remember there were a few earlier discussions of this sort of decomposition. It is nice design-wise, but has a number of potential issues:
|
Yeah, as I was thinking about this some more, trying to shoehorn general tagged values into the exception system doesn't feel like a good fit. To do resumable exceptions, I think we'd end up with exception declarations again, and then the idea that exceptions can only take tagged values as arguments seems weird. Better to design exceptions in a way that makes sense and if tagged values are useful we could add them as a separate proposal. I wonder if there's a hybrid that would make sense. For example, we could preserve As I've described it, tagged values/exceptions could not be stored to the linear heap, meaning we can manage the lifetimes of exceptions purely using a stack discipline. This does not have to interact with the host GC, and does not depend on managed types. It may even be reasonable to say exceptions cannot be return values or parameters, which then means we only have to worry about them living on one stack frame. One question is whether first class exceptions (or maybe 1.5 class, since I'm still proposing leaving them on the stack only) would be enough to solve all of @aheejin's problems. If so, I think that gives us more reason to consider this seriously. As it is now, I get the sense that @aheejin will have to end up emulating a more general mechanism, which will almost certainly be slower than anything we are able to build into Wasm directly. |
Even if you can only assign exceptions to locals you can no longer statically tell their lifetime in general, because assignments may be conditional, and they may create aliases. So I think this would already require some form of reference counting to release associated host resources? Also, our local declarations currently rely on the ability to initialise them with a default value; for exceptions it wouldn't be clear what that means. Without assignment to locals on the other hand there isn't even a way to reorder operands on the stack, so I think you would have a very hard time generating code for non-trivial cases. |
If exceptions can't outlive a call frame (i.e. exceptions are not allowed as return values), then the VM can keep track of what exceptions are in the frame and destroy all of them when the function returns or unwinds. Of course, with loops we could get an unbounded number of exceptions, so we'll still need some mechanism to clean them up sooner. This seems like an issue. Coming up with a meaningful default value is also indeed problematic. One possibility is to have a "empty exception" which has no data, stack trace, etc. Any attempts to match on it will fail, since it has no defined tag. We could then make It's unfortunate that it's currently hard to use Wasm as a pure stack machine. Hopefully multi-valued blocks will help, but ti seems like we'll still need a way to reorder the stack. |
With |
Problem
The current
rethrow
instruction has some problems that make its use verylimited. For example, suppose there are two try/catch blocks and in both catch
they jump to some common code, which is followed by a
rethrow
instruction.This cannot be supported in the current spec because
rethrow
can occur only inthe scope of a
catch
block. But this code pattern is very common especiallywhen there is some cleanup code (calling destructors) to run before rethrowing
an exception up to a caller. If you compile the code below,
The generated code will look like this:
In this case, cleanup action consists of only a single destructor call, but it
can take more code space if there are many objects to destroy, so I think
duplicating this cleanup code into possibly
n
catch blocks is not a viableidea. This is a classic case in which we need a
rethrow
, but it cannot beexecuted at the end of this code because it's not in the scope of a
catch
.This is one example of code sharing that's very common, but code sharing between
catch blocks can occur in other cases as well. You can use
goto
within catchclauses to jump to a common block. Or any middle-IR-level compiler optimization
pass can factor out some common code in catch blocks.
While this problem can be worked around using not a
rethrow
but just a normalthrow
,throw
is considered as throwing a completely new exception, and theVM wouldn't be able to carry an attached backtrace with it, which can be useful
when we later support backtrace debugging in the future. And more importantly,
this problem effectively makes
rethrow
unusable at all, because the mostcommon usecase of it is, as illustrated above, when it occurs after some common
cleanup code which is shared between many catch blocks. It can also occur when
there is shared cleanup code between
catch i
andcatch_all
clauses, whichwill be very common case as well, but I'm actually planning on proposing
something else for
catch
clauses... but anyway.Idea?
This is a rough idea and not a complete spec yet. And it's not I'm proposing
this as a single concrete alternative and I appreciate comments and suggestions.
I think it is necessary to make it possible to access some kind of handle to an
exception object outside a catch block. (The reason it is not a i32 value but a
handle is it can be opaque if it is for a foreign exception) There can be
multiple ways to do it. We can make
catch
instruction to return a handle, saveit to a local or something, and then use it after we exit a catch block. In this
case,
rethrow
should take an handle as an argument now.In this case, I think when the VM can destroy an exception object is unclear,
and it can be an issue, maybe? Maybe the VM should maintain a map of handle to
an exception object until the program ends.
Or, to make the VM can destroy exception object when they are not necessary
anymore, we can add some reference count to exception objects, and make a way to
capture an exception handle within a catch block. Suppose
capture_exception
instruction captures the current exception handle.
The reference count for an exception starts as 1 when any
catch
block isentered.
try_end
instruction.capture_exception
instruction is executed within acatch
blockrethrow
instruction is executed on the handle.This approach is in a way similar to the newly added library functions to the
C++11 spec:
std::current_exception
, andstd::rethrow_exception
.The text was updated successfully, but these errors were encountered: