-
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
Consolidate throw and rethrow #113
Comments
No, they haven't. Rethrow operates on an exnpackage, which generally contains more information than just the exception value. In practice, e.g. a stack trace that is copied over on rethrow, but newly generated on throw. More interestingly, if you were to add resumption, then the exnpackage would carry a continuation, and that is also only captured by throw, not by rethrow. So they are fundamentally different operations. |
WebAssembly does not have a stack-trace semantics, nor does C++, nor does JavaScript. Java, C#, and Python do have stack-trace semantics, and all of them are different. There is no practice here; it's all ad hoc. Regardless, there's nothing preventing my revised
As you also noted twice, you can't use this design for resumable exceptions. The C++ destructors on the stack can already have executed by the time the handler is reached. But now I realize it doesn't matter anyways. My suggestion is equivalent to the current proposal, as in the two can encode the other. But it has a clearer separation of functionality: one instruction for creating the exception, and one exception for raising the exception. |
I think this is mostly a choice of flavor. We considered splitting those instructions too, and ended up with the current |
Agreed it's all ad-hoc, but it still is rather relevant practice. This has been an actual pain point in JS engines, for example, where the language fails to make this distinction, and there is no viable implementation strategy that does not loose stack traces in some cases.
I think you could only do this by either mutating the exception value, i.e., expose the same hacky behaviour as JS, or worse, by mutating some global state.
Probably true, but consider these two as examples of why it's better to assume that there is a conceptual difference between the two operations. And hence that it is more conservative to separate them. |
@aheejin, your parenthetical makes me think there's a misunderstanding. I'm suggesting replacing two instructions,
@rossberg, you can collect the trace when you execute I do not follow your last argument. |
What I meant by fewer instructions is in terms of the number of instructions used in real code. When you throw an exception, now we can do it with a single This difference may not be very significant in terms of final code size given that throwing exceptions is rare, but it's still simpler. That's the reason why I said this is mostly a choice of flavor. |
Sorry I was going to click "Comment" button I accidentally clicked "Close and comment" button. Reopened the issue. |
Ah, thanks for the clarification! |
But the stack trace ought to be captures at the throw point, not the point where you create the value, which might be somewhere else entirely. Likewise, you may want to amend the trace upon rethrow, because again that may happen somewhere else entirely than where the catch was.
We can think of two examples of meta data that one might include into exception packages (traces, continuations). For both we already know that the suggested approach cannot generally produce the correct behaviour. That's a strong indication to be careful and not conflate two operations that behave differently. |
I don't understand how you can make statements about what stack-trace semantics ought to be. Different languages have different semantics (including collect the stack trace at the point of allocation). WebAssembly should not be prescribing which of these is correct; it should be providing infrastructure to support each of these semantics and let the language implementer choose which to use. (And I doubt either of us have enough knowledge of the debugging literature to know which semantics is best anyways, supposing any best practice has even been established.)
As mentioned above, neither of these pan out. If a host chooses to associate a stack trace for some internal purpose, however it does so is guaranteed to be arbitrary. If you were to associate a continuation, you wouldn't want to use it because a number of the destructors on the stack have generally been executed already. And regardless, even if you did do these two things, the translation I gave above is still semantics preserving (assuming program equivalences that I think we both agreed we expected to hold in other conversations).
@aheejin gave an observable difference between the suggestion above and the status quo, and an argument based on that difference as to why to prefer the status quo. @rossberg, as I believe you are the originator of this design, can you give a similar observable difference (or a useful concrete extension in which this difference matters) to justify this claim? |
@RossTate, one main motivation for the current design was that we wanted to keep the door open for resumption. As mentioned, throw and rethrow are very different primitives in the presence of resumption. Moreover, the return type of a throw depends on the exception constructor. It is more natural to have throw applied to a constructor, symmetrically determining both in and out type (this is standard in effect systems, too). You could still separate exception construction and throw, but that requires more tedious typing machinery to track the resumption type. In short, if you ever want resumption, then separating rethrow is a must, and not separating exn creation is more natural. I think we also wanted to avoid introducing an extra type of exception values, which again raises issues about life time management etc.
No, as I pointed out before, that's not how destructors (or any finalizers) can work in the presence of resumable exceptions. They must not run when a resumable exception is thrown, that would be a broken semantics. Instead, a separate abort/finalize primitive is needed (which could be provided by means of injecting an unresumable exception, as in our design). |
Okay, it seems it will help clarify things if you explain how you plan to address the following:
Suppose the |
Refactor segment representation in AST (in both spec and interpreter) by separating out a `segment_mode`. Other changes in Spec: - Various fixes to text format grammar of segments. - Factor out elemkind in binary format. - Add note about possible future extension of element kinds. - Add note about interpretation of segment kinds as bitfields. - Fix some cross references. Other changes in Interpreter: - Rename {table,memory}_segment to {elem,data}_segment. - Some rename elem to elem_expr and global.value to global.ginit for consistency with spec. - Decode the elem segment kind as vu32 to maintain backwards compat. - Some code simplifications / beautifications.
As I pointed out before, you have to distinguish a try instruction with resumption and try without. Each can only catch exceptions of the corresponding kind. They also have different types, because one provides a continuation, while the other doesn't. So you'll need to be more specific which try is which in your example. A (correct) handler for resumable exceptions ought to be linear in the exnref, i.e., it either resumes it or explicitly aborts it. |
Okay, so it sounds like your plan is to effectively have a new kind of throw that ignores existing catches and instead only uses a new kind of catch, and yet you're saying this new kind of throw and new kind of catch should still use the existing This is a lot of complexity and uncertainty just to claim that |
Actually, I'm not sure there's a way to make the abort phase work for my best guess as to what you have in mind. But rather than lay out the concern based on a guess, per your request elsewhere I'll first ask you to lay out in more detail what you have in mind. |
When throwing a resumable exception, anything on the stack is still live, so invoking destructors would obviously be unsound. There isn't much leeway. Ordinary exceptions must unwind the stack, resumable ones must not. Our original idea was to introduce the distinction simply by annotating exception definitions, throw and try instructions, and the exnref type with a The simplest way to "abort" a continuation and unwind the stack is to inject a regular exception at the resumption point. So there would be a But as you say, and as I pointed out during my Feb presentation, this ultimately leads to two almost disjoint sets of primitives. Which is why we don't propose this design anymore. Yet I wouldn't disregard the insights gained from this design experiment about the nature of exceptions, esp that initiating an exception and forwarding an exception are different operations (which also shows up in implementations, FWIW; C++ makes the distinction explicit as well). |
It sounds like insisting these are different operations accidentally misled you and others to think this design was more extensible than it is. As a consequence, we're now in a position where we need something as heavyweight as continuations just to implement exception filtering and various other features that without this backwards-compatibility constraint would otherwise only need two-phase exception handling.
Actually, the only distinction in C++ between |
I don't see how the throw/rethrow distinction implies anything like that. |
I'm wondering how this discussion might change in light of recent developments. The new version of resumable EH proposal (WebAssembly/design#1359) does not have And also in #123 we discussed removal of In the first proposal, it used to take an immediate argument, which specifies which exception in the current EH stack to rethrow. For example,
Then we have two questions to answer:
I think the original rationale of
I'm little unsure where we are going to use this functionality, and I'm not sure if this will be compatible with two-phase unwinding. But I might be mistaken. What do you think? I also considered a possibility of changing
which means, if an exception But as @rossberg pointed out, this has two problems:
I think we can make a rule such as the (optional) last label as
I don't know an answer to this. If we keep |
For designing |
The spec says IIRC Also it was used to run destructors too. But if we add Not sure what you mean by |
So the issue is that the surface language needs some catch-all construct for this to be generated in the first place. That's why I brought up
Technically, that spec is about a particular ABI, not C++ in general. Googling around, it seems that different compilers/systems give different semantics to
Ah, useful information. Thanks! (Though, again, this might be specific to Itanium.) Just to clarify, I'm not opposed to As a separate thought, I'm wondering if it would be worthwhile to develop a WebAssembly-specific ABI for C++. |
Then it is up to our tool convention whether we should catch foreign exceptions with
Traps should not be caught by
I'm nervous about removing one of the functionalities we had from the beginning. And as I said, I don't think the argument about its uses (handling foreign exceptions appropriately or gracefully) has changed. Also given that we are already likely to divide the EH proposal into two steps if we decide to support two-phase unwinding, I'm not sure if I want to divide the EH proposal further. I wouldn't want something like 4 different steps for the EH proposal. Also, even though we don't consider that as a part of this proposal, we are likely to have a related proposal on resumable exceptions (or stack switching or anything in that vein).
We have some of that in https://github.com/WebAssembly/tool-conventions. EH scheme doc there is not very up-to-date; it was written like 2-3 years ago and it does not reflect the current status of implementation, so please don't consult it. But this is the repo we should put any ABI stuff specific to wasm. |
Oh wait, I just remembered that I had thought through this. You can implement |
I think the original reasons for keeping these as distinct concepts have sort of dissolved(?). (I have no idea what the right word to use is.)
We should have just
new_exn
oralloc_exn
or something of the like, andthrow
but taking anexnref
.If it helps prove the point here's how you can implement
new_exn $exception
now:The text was updated successfully, but these errors were encountered: