-
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
Forwards-compatibility with low-level primitives #108
Comments
While I have many other questions to your proposal, I'd like to ask a few things first:
|
Sounds good! It's a big write-up, so I imagine there are a ton of questions.
If at some point in time B is added, for programs using both A and B there is a "good" way for the features of A and B to interact, and for programs using B there is a "good" way to interact with programs written using A.
A (stack) mark in my writeup is a tag (e.g.
#105 has no references until it gets to first-class stacks. It's written up to be fairly independent of (engine-managed) GC. Since most of the terminology is new, I tried to demonstrate the meaning of new terms through usage rather than through description. Let me know at any time if there's a term you'd appreciate a descriptive definition of.
Unwinding code is code that is ideally run when the stack is unwound for whatever reason. The most common cause is an exception being thrown, but that may not be the only cause. (As another example, a finalizer for a first-class stack might unwind the stack to free up resources when the GC realizes the stack is no longer reachable. Similarly, a lightweight thread manager might unwind the stacks of worker threads whose work is no longer needed.) In the case of C++, destructors are unwinding code, whereas C++ try-catch clauses are exception-handling code.
My suggestion was to add With this separation, #105 would treat Hope I managed to be clearer this time! |
Sorry, it is not very easy to understand what you are suggesting. Also, unless you want to replace the whole proposal with #105, I think the details of that can be discussed in the new proposal repo dedicated to #105. It is not my intention to ask all those questions about #105 here. While it is hard to keep track of how many changes you requested for the proposal in the last three months in this repo and all those emails you sent me, could you explain this change without referring to other primitives in #105? I think the semantics of Also, what is the difference between the instructions in |
For
With the separation I'm suggesting, |
If I understand correctly, it sounds like:
I think this makes sense and is generally encouraging (at least to me, since I actually do kind of like the general idea behind the low-level stack-usage ideas in #105, but I also don't want to delay an MVP EH scheme). I'm also not opposed in principle to adding something on to the MVP scheme in order to get better compatibility with a low-level scheme. But I also think that would end being used in a "V2" C++ exception ABI. For example, even if we could magically agree on exactly the right design for Accordingly I think it probably makes the most sense not to tack anything onto the MVP EH just yet, but to move along the usual process with #105 (e.g. the early CG stages where we consider the problem we are trying to solve and get consensus that we want to solve it), and keep MVP compatibility as a consideration there. That proposal can include an addition to MVP EH such as |
Your summary of my perspective sounds accurate. Thanks for the thorough analysis! I think I understand your reasoning as well. There's one concern I want to focus on that you might be intending to be addressed by your plan, but I just want to make it explicit. I'll use V1 for this proposal, V2 for this proposal plus some My concern is that by that the time V3 comes around, there might be a lot of V1 programs out there that V3 programs need to play nice with. When a V3 module A calls some other module B and gives B a callback, module A doesn't know whether B is V1 or not, yet A is expected to be responsible for cleaning up B's stack. So if the callback throws an exception, it will need to walk up the stack and look for both V3-style handlers and V1-style handlers (and So I think what you are suggesting in your plan is that, with appropriate signaling to the community, we can expect enough V1 programs to have been recompiled into V2 programs such that V3 programs won't feel pressured to jump through the above compatibility hoops. You know the C++ WebAssembly community much better than me, so I'm happy to defer to your judgement on that matter. Can you confirm that that's the expectation you had mind, or clarify if otherwise? |
@RossTate Because, in general, module B could have a legitimate This "wrap every call to an import" situation is similar to a long-standing concern I've had that some modules/languages will simply not be compiled with exceptions enabled, and they will inevitably be composed with moduels that do and then bad things will happen (infrequently, in subtle ways). After talking about this in #68, it seems like we can't solve this in pure wasm b/c we don't have enough context to know what's a boundary between languages vs. just different DLLs in the same language. So, true to my idiom, my current thinking is that we should mitigate this problem in Interface Types by having any exception that unwinds into an adapter call turn into a trap. This puts the burden on modules that do use exceptions to catch those exceptions and convert them into something explicit in their interface (say a variant return in the shape of a Result). In the absence of such a design, every non-exception-safe module would need to defensively wrap all calls to imports with a |
@lukewagner Can you clarify your example? The filtered catch is not expressible in V1/V2, and in a V1/V2-only world In my example, module A knows that the exception thrown by the callback cannot be understood by module B (say because it pertains to some unexported stack mark). That's why if A knew that B were V2+, in which I see what you're saying about Interface Types, but research on exceptions (or more generally algebraic effects) has found that this callback pattern respects the "share nothing" principle behind Interface Types. Here is a paper that comes to mind on this topic. I'm happy to discuss that paper and Interface Types, but it probably merits a separate thread. Also, V3 does not have an explicit notion of exception throwing, since that's boiled down into a bunch of other primitive steps, only the last of which is unwinding. I think the construct you'd want in #105 is |
Well I certainly expect that there will still be V1 programs around for much longer than we'd prefer (certainly that's been my experience in past platforms). The emscripten community has actually been pretty good about uptake of new tools generally, but as the platform matures and more users trust us with giant cross-platform codebases that fund their businesses, I'd expect them to get more conservative on average.
So I think we will be able to give our users some kind of help on this when they need it, whether that's IT, another C++ ABI, etc. |
@RossTate That paper looks promising; I'll take a look! My |
@lukewagner Supposing V3 is willing to assume B is V2+, then V2's @dschuff I also definitely expect there to be V1 programs still around. It's the dynamics of community pressure that I'm concerned about. It sounds like you think the dynamics will be that V1 will be pressured to upgrade to V2+ rather than V3 be pressured to jump through hoops to unwind V1 stacks. Your reasoning sounds good to me, and anyways y'all have much more insight into the community with which to make that assessment. So supposing @aheejin is fine with this plan, I'm happy to close this issue. Given that, I'm wondering how y'all would like to proceed with #105 then, but it's probably better to discuss that in #105 instead of here. |
* Change element segment encoding * Change table index letter
Remove type annotation on ref.is_null in overview
In order to discuss forwards-compatibility, it's probably best to first get us all on the same page as to how the primitives in #105 would likely express the constructs in this proposal.
The first thing we need is a "universally understood" mark:
mark try-catch-exnref : [exnref] -> unreachable
.Next, translate
try ([ti*] -> [to*]) instr* catch instr* end
to the following:Then translate
throw $event
to the following:where
and similarly translate
rethrow
to simplycall $throw_exnref
.br_on_exn
remains as is, since that's more about reference types than about stacks or control.One takeaway from this translation is that, theoretically speaking, this proposal is already forwards-compatible with the primitives in #105. However, practically speaking, we care about more than just getting programs to run; we want programs to run correctly, including programs that call other programs or are called by other programs. This is where the importance of stack conventions comes in.
As an example, suppose module A is compiled in the style of #105 whereas module B is compiled in the style of this proposal. If module A calls B, providing B with a callback into A, and B calls that callback that happens to throw an exception (in A), then we have a situation where B's stack frames are sandwiched between A's throw and (presumably) A's catch but where A's exceptions are not implemented using
try-catch
. Module A would like to let module B clean up its stack, but module A has its own unwinding state it wants to maintain (e.g. Python building the stack trace as it unwinds the stack). How should module A proceed?In the current proposal, unwinding code always usurps control and then typically rethrows control to the next
try-catch-exnref
mark. That is, it assumestry-catch-exnref
is the sole way to unwind the stack. If, on the other hand, the current proposal were revised to separate unwinding code (e.g.on_unwind instr* do *instr* end
) from exception-handling code (stilltry/catch
), then #105 could translateon_unwind
to a "universally understood" markunwinder : [] -> []
and module A's unwinding code could choose to executeunwinder
marks it sees and ignore anytry-catch-exnref
marks. As an added benefit, this would work regardless of how B were compiled (assuming B chose to abide by theunwinder
convention), so module A would not need to adjust its implementation strategy to account for B's specific choice of implementation strategy.Hopefully that gives a since of where the compatibility problem really lies and how the current proposal might be changed to help with forwards-compatibility. It all comes down to what kind of conventions we want to support. More conventions means better compatibility between newer wasm programs and older wasm programs. Fewer conventions means fewer changes, including even no changes. It's possible that the
on_unwind
separation above is the sweet spot or that the sweet spot is to simply leave the proposal as is. Regardless of what we decide to do, the current proposal is optimized for a particularly common kind of exception semantics, and I think we should and can maintain that optimization for a common case.The text was updated successfully, but these errors were encountered: