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

What counts as an exception? #1

Closed
KarlSchimpf opened this issue Apr 27, 2017 · 39 comments · Fixed by #93
Closed

What counts as an exception? #1

KarlSchimpf opened this issue Apr 27, 2017 · 39 comments · Fixed by #93

Comments

@KarlSchimpf
Copy link
Contributor

KarlSchimpf commented Apr 27, 2017

Restoring lost issue:

@wibblymat

Should things like unreachable, out-of-bounds memory access, call_indirect with an invalid table index, etc., become catchable exceptions?

@dschuff
Copy link
Member

dschuff commented Apr 27, 2017

I think there is a meaningful difference between explicitly-raised exceptions (which I guess includes JS exceptions but ignoring that for now) and machine-type error traps (OOB, signature mismatch etc) and it's not clear that they should be catchable in the same way that user exceptions are. So obviously we can debate that in this issue (and we can design such a feature) but I think the first proposal should exclude them because the use cases are very different.

@jfbastien
Copy link
Member

Right, we've talked about having support for signals as well as exceptions. It would be interesting to define what we see in each bucket, and what other platforms have done. Specifically, I think the Windows SEH approach, its ups and downs, can inform our decision.

@AndrewScheidecker
Copy link
Contributor

One advantage the Windows SEH approach has over handling runtime traps with signals is that it avoids global state that determines how a trap will be handled. There isn't a good way with POSIX signals to handle a trap from just one thread.

A Windows SEH filter is effectively a stack-scoped signal handler: when an exception occurs, the SEH filter functions are called "on top" of the stack, so the information is still available to resume execution, or get a stack trace, or unwind to the handler. A C++ catch maps to a SEH filter that checks the exception information and returns a value that indicates the associated handler should be executed.

Thread-local signal handlers would provide the same functionality (could you throw from them?), but require some additional mechanism to compose (a stack of handlers, perhaps?).

My most preferred design would be very close to Windows SEH. My least preferred design would be something that unifies traps and exceptions, but doesn't allow resuming or inspecting the stack at the trapping code.

@KarlSchimpf
Copy link
Contributor Author

This proposal intentionally did not focus on what exceptions should be produced by WebAsembly. Rather, any such exception would need to be imported to be caught, since it is not one the module defines.

Hence, the framework needed to handle such exceptions, should they be added, is already there. We would not need to add any new concepts.

@AndrewScheidecker
Copy link
Contributor

Hence, the framework needed to handle such exceptions, should they be added, is already there. We would not need to add any new concepts.

If runtime-generated traps can be caught as exceptions, then there should be a mechanism to retry the trapping code. There should also be a way to handle a runtime trap by capturing a stack trace and other state information at the point of the trap for automatic bug reporting.

I think it's fine to start with exceptions as they are and add the ability to catch runtime traps later, but there's additional functionality required if the above cases are to be supported. Would that require changing any of base exception functionality?

I can imagine adding a retry_throw operator that can be used similarly to the rethrow operator. That seems like it is orthogonal to this proposal.

A way to capture a stack trace is not specified by WebAssembly; without considering whether it should be, what would be needed from this proposal for an implementation to expose the trapping call stack to a runtime trap handler?

@KarlSchimpf
Copy link
Contributor Author

Good point. I hadn't thought of those issues. I agree that sometime in the future, we should consider runtime traps.

@KarlSchimpf
Copy link
Contributor Author

Marked as (future) enhancement to add handling of runtime traps.

@jfbastien
Copy link
Member

I don't think we want to outright declare this as a future feature. It's worth exploring having it in the first version as well, and if that's not viable then move to future.

@rossberg
Copy link
Member

@jfbastien, are you referring to reifying traps as exceptions, or supporting resumption? Both are separate features, and the latter is far more difficult and debatable than the former.

@jfbastien
Copy link
Member

@jfbastien, are you referring to reifying traps as exceptions, or supporting resumption?

Both, separately.

Both are separate features, and the latter is far more difficult and debatable than the former.

Agreed.

@lukewagner
Copy link
Member

At least in the future I've been imagining, the distinction between traps and exceptions is:

  • traps: execute at the instruction, as if they were called by the instruction, can inspect local and stack state, and finally choose how to proceed, either by: (1) resume (with one of several suboptions here depending on the faulting instruction category, e.g., "resume with this value" for value-producing insns), (2) throw (with a given exception tag)
  • exceptions are all about unwinding to an enclosing try block, taking for granted that this instruction has already chosen to throw.

With this distinction, exception handling is single-pass unwind-as-you-go, unlike, e.g., itanium ABI's two-pass design. Also, it is an orthogonal question of how to define which trap handler to call (module-scoped, lexically-scoped, dynamically-scoped like SEH, etc).

Also, with this split, trap handlers are definitely the more advanced feature with the more narrow (but important, in the limit, if we want to allow all the fancy VM tricks) use case, so I think we should keep them separate from the exception handling MVP knowing they can be added later in a complementary way.

@jfbastien
Copy link
Member

knowing they can be added later in a complementary way

That's the bit I'm not convinced about.

@lukewagner
Copy link
Member

Is there are particular scenario you're thinking about?

@jfbastien
Copy link
Member

Is there are particular scenario you're thinking about?

Maybe I haven't thought about it enough. I'm not convinced this ins't a plausible scenario:

  1. We standardize exceptions.
  2. We want traps.
  3. Traps are kinda different from exceptions, but not that much, and turn out to be incompatible in a weird way.
  4. We end up with two mechanisms that are almost, but not quite, the same.
  5. We're sad.

I'm just not convinced this isn't the case. As well as the above scenario, it could unfold as:

  • We standardize both at the same time (either as one feature or as two), it works great.
  • We standardize one then the other, but they magically coexist well.

@lukewagner
Copy link
Member

Yeah, that is the risk. It seems like we could mitigate it, without increasing the size of the EH MVP, by doing some work to collect all the interesting use cases for traps we can think of and reasoning through how the final traps+EH design would address them.

@aheejin
Copy link
Member

aheejin commented Feb 5, 2019

I think it’s time to revive this old issue. Should we catch wasm traps with wasm catch instruction?

  1. Why would we want to catch them and what's the use case for it?

I'm not arguing we should catch traps, but anyway all below are based on the assumption that “what if we catch traps”, just for discussion.

  1. If we decide to catch them, what should we do? Once a trap occurs, the contents of memory are unreliable and we can't expect sensible things to happen if we catch it and continue execution. Then should we do reliable termination of a program if a trap happens? We can actually do that by not catching traps, because they are gonna end up in the embedder. In case we both want to catch traps with our catch and provide reliable termination of a program after a trap occurs, that poses different problems. First there should be a way from wasm code to tell a trap from a normal exception (including normal foreign exceptions). And if there's a way, what should we do? Maybe continue rethrowing it until we reach the embedder? We may not want to even run the C++ destructor functions after catching a trap, which we run in case of foreign exceptions. Should traps have a predetermined special tag? Even with that, we will have increased code size if we add the check “if this is a trap, don’t do anything and only rethrow” to every catch. Maybe we can add really_catch_all instruction (I’m not serious about the name) that catches both exceptions and traps while leaving the current catch to catch only exceptions.

  2. Currently when traps surface to the JS embedder they become JS exceptions. Should we unify the way they are handled before and after they hit the embedder?

  3. If wasm catch instruction catches traps, Should C++’s try clause catch them too? This is not actually easy for reasons:

4-a.

try {
trappable instruction
foo();
} catch (...) {
// This part of code may assume foo(); has been run, but it may not be
}

4-b. It is not even easy to unwind to the right ‘catch’ clause. LLVM generates invoke instruction for every possibly throwing function call, which has two destination BBs: normal destination and unwind destination. The unwind destination is where we go when an exception occurs. But we can’t do that for every trappable instruction. Other instructions don’t even have info on where to go if an exception occurs. SEH’s __except catches traps and they solved this problem by outlining every __try block as a separate function, but I don’t think we should do this as well. (This is of course 1. More code size, 2. Less optimization opportunities, 3. More function call overhead, 4. Possibly huge bloat in the number of function table entries.)

@Horcrux7
Copy link

Horcrux7 commented Mar 9, 2019

From my point of view as a Java to Wasm compiler writer, I would suggest the follow solution:

  • Add an implicit system event with index -1 (or any other negativ value). Because the own declared exceptions starts with 0 there is no conflict.
  • This event should have a parameter i32. (event (param i32))
  • The parameter contains an standardize error code.

Then the compilers of the different languages can add a simple handler with br_on_exn x -1

@rossberg
Copy link
Member

rossberg commented Mar 9, 2019

Note that index spaces are unsigned. But there could be other ways of binding primitive exceptions. However, the harder questions are different ones, e.g., potential performance implications of making every trap catchable.

@Horcrux7
Copy link

Horcrux7 commented Mar 9, 2019

the harder questions are different ones, e.g., potential performance implications of making every trap catchable.

I think the performance decrement will be larger if the trap can't catch. This required a compiler of a language that support such catching (e.g. Java) to check every trap self. For example the compiler must check for every division that there is not a zero. The result will be that for every instruction that produce a trap the compiler will call a function.

The idea that runtime exception should result in a program termination seems me absurdly. Another thing is if there are traps that should never occur if the compiler has work correctly like unreachable.

For an high level application developer there is no difference between an trap and a self fired exception or an exception from some library.

@rossberg
Copy link
Member

rossberg commented Mar 9, 2019

I understand, but there are relevant languages that do not benefit from the check and would be penalised by any extra cost implicit in the semantics -- such as C, where e.g. division by zero is undefined behaviour. You would also penalise code where the producing compiler was already able to prove the absence of failure.

@Horcrux7
Copy link

Horcrux7 commented Mar 9, 2019

but there are relevant languages that do not benefit

This is valid for every feature in wasm.

would be penalised by any extra cost implicit in the semantics

An option for different behavior in the wasm can be a solution for it. If you can not decide on a feature, add an option! Like the command line switches for experimental feature currently this can be enabled in the wasm binary self. Of course would this increment the complexity of the runtime and I can understand if you does not accept this as a solution.

You would also penalise code where the producing compiler was already able to prove the absence of failure.

This can be possible for DivisionByZero, ArrayIndexOut, NullPointer but what is with OutOfMemory? I think it is not possible to check if any allocation would work.

@rossberg
Copy link
Member

rossberg commented Mar 9, 2019

OOM is a very hard case anyway. IME, very few systems (if any) are able to recover from it reliably and allow general execution to proceed afterwards in a stable manner.

@andrewackerman
Copy link

How about IO and threading related exceptions?

@rossberg
Copy link
Member

rossberg commented Mar 10, 2019

Well, there is no I/O in Wasm, nor any traps specific to threading.

@rossberg
Copy link
Member

The only existing traps in Wasm are from undefined arithmetics, illegal memory/table accesses, and ill-typed indirect function calls. Of these, only the arithmetic ones would actually make sense to catch, since the others should never occur in a correctly compiled program.

@Horcrux7
Copy link

In the GC spec there are a large count of traps like wrong casting, null reference to complete the list of possible traps.

@rossberg
Copy link
Member

rossberg commented Mar 10, 2019

True, but these all fall into the same category as ill-typed function calls.

@andrewackerman
Copy link

I would argue that it's very possible for an incorrect cast or null reference to occur during runtime. Not all programmers are savvy enough to write their code in a way that would prevent them from happening, and there's only so much that compilers can do to detect and eliminate the possibility. Not allowing those traps to be caught and handled could result in a situation where such trivial coding errors becomes hours of tedious debugging. It's 2019 for goodness sake.

@bitwalker
Copy link

There was some discussion about stack traces with regards to traps, which I would expect to make debugging straightforward, so hours of tedious debugging as you put it would probably be overstating the problem. Without traces though, I agree, debugging would be a pain.

In general though, my feeling is that traps should at least have some similarity to POSIX signals, in that some of them cannot be caught (resulting in a panic/abort), and some can (defaulting to a panic/abort if unhandled, but if a program chooses to handle them, allows it to catch the trap and recover), and there is some kind of standardized mechanism for doing so.

To be clear though, I don't necessarily think the interface for handling traps needs to be the same as POSIX signals, though I think it has potential as a model; but signals do provide a good baseline for the kind of traps that can occur, and/or are useful to catch, and illustrate that not all of them can or need to be recoverable. I think I'd want to see something similar in Wasm, where you can opt in to handling certain traps that are considered recoverable, and for the rest, a program simply panics with a backtrace to the instruction which trapped.

I'm not sure how I feel about the idea of traps being converted to exceptions, it should be the job of the language compiling to Wasm to deal with things like division by zero, null pointer dereferencing, stack overflow, etc. As Andreas said, some languages can either guarantee that certain trappable behaviors will never occur as part of their compilation process, or choose to skip checks which add overhead at runtime as an optimization opportunity, and I don't think we should be forcing them to deal with that overhead if it is not needed.

If those kind of checks need to be part of Wasm, the best option for that in my mind would be adding checked/unchecked forms instructions which may trap, which would raise an exception rather than trap - this would address to some degree the potential for code size issues for languages which need to ensure that traps are surfaced as exceptions in the source language, since those checks would not need to be duplicated everywhere.

@rossberg
Copy link
Member

@andrewackerman, there are implementation-level type errors and language-level (i.e., user) type errors. Traps signify the former, while the latter will typically have to be implemented by a compiler. I think it's best not to conflate the two. Even user-observable null checks should best be explicit in the code, to avoid as much overhead as possible around implementation-level checks.

@Horcrux7
Copy link

@rossberg If the compiler need to check to prevent runtime traps then it can be required to add some more instructions. For example in the complex range of the GC spec. I have not check this if this really required.

The more I think about it the better I like it. Each language can precisely prevent the traps that are defined in its language.

To finish this discussion this should be a topic in one of the next community/working group meetings.

@binji
Copy link
Member

binji commented Mar 11, 2019

To finish this discussion this should be a topic in one of the next community/working group meetings.

Added to the March 19 agenda. Does that work?

@aheejin
Copy link
Member

aheejin commented Mar 11, 2019

I am OOO until 3/22, and in a different time zone, so I don't think I can attend it. Can we do it later?

@aheejin
Copy link
Member

aheejin commented Mar 11, 2019

I've been OOO (and will be for some more days) so I haven't fully caught up with the comments so far, but one thing I'd like to make sure is even if we decide to catch traps with catch, that does NOT mean C++'s catch clause will catch them. See #1 (comment) for reasons why it's not possible.

@binji
Copy link
Member

binji commented Mar 12, 2019

OK, no problem, let's postpone.

@aheejin
Copy link
Member

aheejin commented Oct 26, 2019

We discussed the topic in the in-person CG meeting in June. It's been a while, but I'd like to reopen the discussions and make decisions on this issue to proceed to Stage 2 for the EH proposal.

I'd like to propose that we don't catch traps with wasm catch instruction. After many discussions, I feel that traps have characteristics that more resemble signals than exceptions, meaning that 1. at least some of them should not be catchable (i.e., should result in abort) and 2. they usually don't need to be handled in local scopes like try-catch. I'm not saying the interface for handling traps should be necessarily similar to that of POSIX signals; what I mean is that it can be a separate proposal from the EH proposal if that functionality becomes necessary, in which traps can be handled in more broader scope, such as per function or per module.

As I mentioned above, for a practical reason, we need a way to abort, to implement functions like C abort or C++ std::terminate. In JS embedding, they are currently implemented in JS as throwing a JS exception, which is catchable by wasm catch instruction. I'd say that we implement those functions using wasm traps, and make those traps uncatchable.

I also think whether hardware traps should be handled like or converted to exceptions is the job of languages themselves. For example, Rust compiler inserts code that checks for divide by zero errors and throws exceptions. C++ treat them as UB and doesn't pay the performance cost. Java VM handles arithmetic errors using signal-handler-like routines and creates and throws Java exceptions.

OTOH, C++, the important language wasm currently supports, is not gonna benefit from making catch instruction catch traps; they would rather be likely to suffer from that. Wasm catch instructions are used for cleanup landing pads, such as ones for calling destructors, and those C++'s cleanup landing pads don't catch traps, which means we should insert bypassing code for traps in every cleanup landing pad. Cleanup landing pads are much more common than landing pads generated by C++'s catch clauses, and this may incur unnecessary code size increases. (One may argue we don't need to care because it's UB in C++ anyway, but then it will run random functions and cause even more traps, making debugging nearly impossible.)

Some people were concerned about debuggability if traps are not catchable; I don't think that will be a problem. If the embedder has stack trace support, users will be able to see stack traces when a program crashes after a trap just fine, provided that we have appropriate JS (or other embedder) API support.


This scheme will require some changes to the JS API, because traps should not be catchable even after they hit a JS frame. In the current JS API, if a trap hits a JS frame, it becomes a WebAssembly.RuntimeError. WebAssembly.RuntimeError contains a few more situations other than traps, I think it makes sense to make catch instruction not catch all WebAssembly.RuntimeErrors. This way traps can be handled consistently before and after they hit JS frames.

@aheejin
Copy link
Member

aheejin commented Oct 26, 2019

cc @titzer, in case he has more insights on support for languages like Java. (We talked a little on that at the CG meeting, but the it was more like a passing conversation, so)

@dschuff
Copy link
Member

dschuff commented Oct 31, 2019

I also think it makes sense not to have traps caught by wasm catch.
I think it makes sense to say that during unwinding a trap propagates into JS frames as a thrown RuntimeError, and unwinds through wasm frames as traps to today; and an exception thrown from wasm would propagate into JS frames as some JS reflection of the exception (if the event type is exported, it seems like you could even potentially get the thrown object out of it).

It's slightly unclear to me (as someone not accustomed to JS spec-ese) whether there could currently ever be a RuntimeError generated other than as the result of a wasm trap. If so, that would be weird if those 2 different kinds of events would be indistinguishable in JS.

@aheejin
Copy link
Member

aheejin commented Oct 31, 2019

@dschuff

It's slightly unclear to me (as someone not accustomed to JS spec-ese) whether there could currently ever be a RuntimeError generated other than as the result of a wasm trap. If so, that would be weird if those 2 different kinds of events would be indistinguishable in JS.

I opened #89 to discuss this.

aheejin added a commit to aheejin/exception-handling that referenced this issue Dec 30, 2019
This adds overview on traps not being caught by the `catch` instruction
and its relationship with the JS API.
Closes WebAssembly#1 and closes WebAssembly#89.
aheejin added a commit that referenced this issue Jan 10, 2020
This adds overview on traps not being caught by the `catch` instruction
and its relationship with the JS API.
Closes #1 and closes #89.
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Jun 6, 2020
This adds overview on traps not being caught by the `catch` instruction
and its relationship with the JS API.
Closes WebAssembly#1 and closes WebAssembly#89.
ioannad pushed a commit to ioannad/exception-handling that referenced this issue Feb 23, 2021
This adds overview on traps not being caught by the `catch` instruction
and its relationship with the JS API.
Closes WebAssembly#1 and closes WebAssembly#89.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.