WebAssembly / exception-handling

Proposal to add exception handling to WebAssembly
https://webassembly.github.io/exception-handling/
Other
162 stars 35 forks source link

Any formal execution definition of exception handling? #87

Closed sys5867 closed 3 years ago

sys5867 commented 5 years ago

Hello.

I'm interested in formal execution definition of WASM with exception handling proposal included.

What I now think is that the try catch end block semantics with throw instruction can be described in reduction rules, similar to PLDI'17 MVP spec paper.

Also, I heard from @aheejin that @rossberg has done some work on the formal spec of the proposal.

Are there in-progress works related to formal definition of exception handling?

rossberg commented 4 years ago

Thanks for the clarifications, @aheejin. I can see how this all came about. But I still feel rather uncomfortable about the general turn we have taken wrt hypothetical two-phase EH. My impression is that the current direction is based on multiple implicit assumptions that all are somewhat questionable, especially:

  1. Languages that expose two-phase EH (or other use cases) all share a sufficiently similar semantics that we can bake into Wasm. -- There hasn't been much evidence for that. @phoe's explanations about CL may suggest the opposite.

  2. Two-phase EH can only be implemented by the engine. -- I don't think that's correct. If you interpret throw as "unwind-to" and exception tags as target ids, then you can employ the current mechanism as the unwind phase and implement the handler search phase completely in user space with a simple shadow handler stack (see below).

The current direction OTOH seems to take us down a road where we end up building in special support for specific ad-hoc high-level language features. That is not aligned well with Wasm's goal of being a low-level and language-neutral engine.

Taking a step back, obviously, there has to be a primitive mechanism for non-local control transfer/abort, and basic throw/catch as in the current proposal can be viewed as just that. In a way, it's just a glorified and well-behaved longjmp with unwind. But given that, everything more complex seems better suited to be expressed in user space.

To wit, let me sketch how one could, quite easily, compile 2PEH with filters under the current proposal, without any use of try-unwind:

  1. Have the language's RTS define a (per-thread) table that represents its handler stack:

    (type $handler-func (func (param $exn i32)))
    (table $handlers (ref $handler-func))  ;; plain funcref would work as well
    (global $top+1 (mut i32) (i32.const 0))
    
    ;; These helpers would probably be inlined by the producer
    (func $push-handler (param $h (ref $handler-func))
     ;; omitting an overflow check here
     (table.set $handlers (global.get $top+1) (local.get $h))
     (global.set $top+1 (i32.add (global.get $top+1) (i32.const 1)))
    )
    (func $pop-handler
     (global.set $top+1 (i32.sub (global.get $top+1) (i32.const 1)))
    )
  2. And a helper function in the RTS to perform throws:

    (func $throw (param $exn i32)
     (local $i i32)
     (for $i in $top+1 - 1 to 0
       (call $pop-handler)
       ;; a handler that matches will not return
       (call_indirect $handlers (local.get $exn) (local.get $i))
     )
     (report uncaught exception)
    )
  3. Compile a source-level throw E to (call $throw (E)).

  4. Compile a source-level try A catch (e) when (C) B (with the catch, say, on line XYZ) to:

    (try
     (do
       (call $push-handler (ref.func $catch-XYZ))
       (A)
       (call $pop-handler)
     )
     (catch $unwind-to-XYZ
       (B)
     )
    )
  5. ...where a distinct exception tag and a handler function are generated for the catch:

    (exception $unwind-to-XYZ (param i32))  ;; assume i32 is representation of language exception
    (func $catch-XYZ (param $exn i32)
     (if (C)
       (then (throw $unwind-to-XYZ (local.get $exn))
     )
    )

This can easily be extended to multiple catch clauses etc. There is a small overhead here in that each try has to push/pop the shadow stack. But before we plan for lowering (potentially multiple semantic variations of) similar machinery into Wasm, my take is that there should be sufficient evidence that this overhead is practically relevant, for the minority of languages that need it.

RossTate commented 4 years ago

I very much agree that point 1 is a misconception in the current proposal, but I disagree with point 2 (or more precisely, it's more nuanced, as I'll discuss below).

there has to be a primitive mechanism for non-local control transfer/abort, and basic throw/catch as in the current proposal can be viewed as just that.

I agree with this, though there's a missing piece, which is that you need a way to unwind, e.g. destructors, (part of) finally, fault, unwind-protect (and part of dynamic-wind). All of the latter are conceptually blocks/functions of type [] -> [].

In a way, it's just a glorified and well-behaved longjmp with unwind.

This is not true. throw/catch uses dynamic scoping whereas longjmp is more akin to lexical scoping (e.g. block/return-from in Common Lisp). Of course we can emulate the latter with the former provided we have a way to dynamically generate fresh identities, but it is important to recognize this distinction and that for probably a good while the latter will be emulated with the former. In particular, it means that not all uses of throw are for exceptions, which for example means compiling a C++ catch (...) to something (like catch_all) that intercepts them is problematic. In the meanwhile, we want both forms of non-local control flow to run unwinding code in the middle.


To wit, let me sketch how one could, quite easily, compile 2PEH with filters under the current proposal, without any use of try-unwind

There are multiple problems with this suggestion, many of which line up with the reasons why we aren't telling programs to implement exception handling with shadow stacks.

  1. Have the language's RTS define a (per-thread) table that represents its handler stack:

Here you're using a global table to encode thread-local state. According to https://github.com/WebAssembly/design/issues/1375#issuecomment-691324760, this is "the (somewhat embarrassing) current hack we use for web workers, but it isn't the plan for a first-class (pure wasm) threads".

  1. And a helper function in the RTS to perform throws:

This helper function has a bug. It pops handlers during the search. Those handlers need to stay on in case, during unwinding after the search has completed, an unwinder throws an exception that should be handled by one of those handlers. (There is Common Lisp code that specifically relies on this behavior.)

  1. Compile a source-level try A catch (e) when (C) B (with the catch, say, on line XYZ) to:

This has two bugs. For the first bug, you need to pop the handler regardless of how control exits. That should include when foreign code causes non-local control to exit the block, which is covered by using try/unwind. Using try/unwind also fixes the bug above.

The second bug is that, if the handler function matches, then you need to make sure to go to this specific call frame's catcher (as there might be multiple frames for the same function on the stack). So you need to dynamically generate a unique identifier for the call frame and somehow store that in/with the pushed handler (so that it can be put in the event's payload), and similarly the catch needs to check that the identifier in the payload matches the current call frame's generated identifier. (This is not a problem for, say, C++ handlers. Though even then the payload should probably specify which of the compiled C++-catch clauses should be branched to.)

Note that in more advanced 2PEH languages/implementations, the handler would normally reference and even mutate values in the corresponding call frame. So these languages will require allocating a closure each time you push the handler, updating the closure every time a relevant value is mutated, and pulling changes from the closure when the handler is popped. This work is wasted if the handler is never invoked.

  1. ...where a distinct exception tag and a handler function are generated for the catch:

Note that a foreign catch_all while mistake these thrown events for unhandled exceptions.


Reviewing the above exercise, I see that the above implementation strategy needs try/unwind to interact correctly with foreign exceptions (and at the same time interact well with its own exceptions). I also see that a catch_all in foreign code (say representing a C++ catch (...)) would mistake a non-local transfer of control for an exception.

There are also issues with how it composes with other features in the pipeline. In particular, algebraic effects or first-class stacks provide ways to change the current stack and consequently change the handlers on the stack. This means that the global table can become out of touch with reality. I also identified a number of inefficiencies for more advanced 2PEH language implementations caused by trying to maintain this table as a shadow of the real stack. All these problems would be addressed by actually using the real stack to implement 2PEH rather than some attempted portrait of the stack.

rossberg commented 4 years ago

I agree with this, though there's a missing piece, which is that you need a way to unwind, e.g. destructors, (part of) finally, fault, unwind-protect (and part of dynamic-wind).

My whole point is that these might not be "missing", because they do not necessarily need to be primitive in a low-level VM. Anything that can be done in user space without significant loss ought to be done there.

throw/catch uses dynamic scoping whereas longjmp is more akin to lexical scoping

I don't see that. A longjmp's target is first-class and selected completely dynamically, not lexically.

  1. Have the language's RTS define a (per-thread) table that represents its handler stack:

Here you're using a global table to encode thread-local state. According to WebAssembly/design#1375 (comment), this is "the (somewhat embarrassing) current hack we use for web workers, but it isn't the plan for a first-class (pure wasm) threads".

I think you are misrepresenting @lukewagner's comment. He was referring to thread-local state being implicitly identified with instance state right now, because there's no fork and no explicit sharing annotation, not to using thread-local state at all -- of course RTSs will need TLS for certain things, e.g., to maintain the shadow stack for C. In any case, this question seems completely orthogonal.

  1. And a helper function in the RTS to perform throws:

This helper function has a bug. It pops handlers during the search. Those handlers need to stay on in case, during unwinding after the search has completed, an unwinder throws an exception that should be handled by one of those handlers. (There is Common Lisp code that specifically relies on this behavior.)

You are right, I was prematurely optimising code size by moving the pop from the catch to $throw while writing down the sketch. I hope we can agree that this is easily fixed.

  1. Compile a source-level try A catch (e) when (C) B (with the catch, say, on line XYZ) to:

This has two bugs. For the first bug, you need to pop the handler regardless of how control exits. That should include when foreign code causes non-local control to exit the block, which is covered by using try/unwind. Using try/unwind also fixes the bug above.

Remember this was just a sketch. I agree that to interact cleanly with foreign code you'll need to run pop in a finalizer, but catch-all/rethrow will do just fine.

The second bug is that, if the handler function matches, then you need to make sure to go to this specific call frame's catcher (as there might be multiple frames for the same function on the stack). So you need to dynamically generate a unique identifier for the call frame and somehow store that in/with the pushed handler (so that it can be put in the event's payload), and similarly the catch needs to check that the identifier in the payload matches the current call frame's generated identifier.

I think that's not true. For a given catch, the innermost instance of handler in the shadow stack will correspond to the innermost handler on the Wasm stack by construction. I don't see why anything else would be needed.

Note that in more advanced 2PEH languages/implementations, the handler would normally reference and even mutate values in the corresponding call frame. So these languages will require allocating a closure each time you push the handler, updating the closure every time a relevant value is mutated, and pulling changes from the closure when the handler is popped. This work is wasted if the handler is never invoked.

Well, this relates directly to the problem I keep pointing out with your vision for an engine solution: the implicit assumption that an engine could execute code in a call frame that is not on top of the stack. In most engines I have seen that won't work without implementing fragmented stack frames, for reasons I have pointed out several times. I highly doubt fragmented stack frames are something engine makers would be willing to accept -- it seems like a bad idea for Wasm. So I believe that any built-in solution will almost inevitably have the exact same limitation.

  1. ...where a distinct exception tag and a handler function are generated for the catch:

Note that a foreign catch_all while mistake these thrown events for unhandled exceptions.

Such a catch-all will run in the 2nd phase, where it seems like it would evoke exactly the behaviour it should? If it rethrows, e.g., because it encodes a finally, then unwinding continues as one would expect. If it swallows (a.k.a. handles) the exception, that seems fine as well, the outer handlers will remain on the shadow stack and thus active (provided my mistake above is corrected).

Reviewing the above exercise, I see that the above implementation strategy needs try/unwind to interact correctly with foreign exceptions (and at the same time interact well with its own exceptions). I also see that a catch_all in foreign code (say representing a C++ catch (...)) would mistake a non-local transfer of control for an exception.

See above, there may well be other potential issues I'm missing right now, but this probably isn't one of them.

There are also issues with how it composes with other features in the pipeline. In particular, algebraic effects or first-class stacks provide ways to change the current stack and consequently change the handlers on the stack. This means that the global table can become out of touch with reality. I also identified a number of inefficiencies for more advanced 2PEH language implementations caused by trying to maintain this table as a shadow of the real stack. All these problems would be addressed by actually using the real stack to implement 2PEH rather than some attempted portrait of the stack.

Interaction with continuations is the main concern here, I agree. This one I don't have a good answer for right now -- though I do point out that this doesn't represent a new problem, we'll already need to solve that somehow for other existing uses of shadow stack, as in C.

OTOH, this mirrors my dual concern with baking ad-hoc mechanisms into the language that risk ending up with a hacky combined semantics if they lack a proper foundation.

aheejin commented 4 years ago

@rossberg Thanks for your writeup. What you wrote is very much like what I thought a VM would do for two-phase internally, and I don't really have a preference whether it should be done in the VM side or in the user side, as long as it shows reasonable performance. But I'm still not sure how that handles cleanup code.

rossberg commented 4 years ago

@aheejin:

  • I might have missed this, but I don't see how your scheme distinguishes user catch handler and cleanup code. Is it via a different tag? Is there a special "cleanup" tag or something so we ignore that tag when searching?

Clean-up code would compile to a plain try-catch_all-rethrow without a counterpart on the shadow handler stack. So it runs as expected when a throw $unwind-to-XYZ is invoked from that stack.

  • In your scheme, let's say we searched the handler stack and found a user catch handler somewhere. Then we have to actually unwind the stack (= second phase), while running cleanup code. After running a part of cleanup code, how do we resume the second phase? As I said, rethrow initiates a new search, which we don't want. We want resuming functionality. (We can have a separate resume instruction, which I also mentioned)

The observation underlying this scheme is that 2PEH should not be understood as 1PEH + a separate unwind phase afterwards, but as 1PEH + a separate search phase before. Throw/catch becomes the unwinding primitive, not the search primitive.

So, rethrow does exactly what you want here, because throw/catch is only used to implement the unwinding phase (after search has already been completed). And rethrow resumes that. Actual source-level throw is implemented as a more high-level operation via the $throw function I showed. And if the source language had something akin to rethrow itself, then that would equally be implemented by a higher-level abstraction that invokes the search phase first explicitly.

  • Your scheme lacks the functionality of delegate, but I think this can be added by tweaking the throw function code, such as, not decrementing the loop index by 1 or by N.

Hm, I believe that's orthogonal. While I only showed a source-level try compiling to try-catch, it can likewise be compiled to try-delegate when needed. Whether it's catch or delegate is a handler-side detail that does not affect the search and unwind mechanism leading up to it (or being resumed from it). You only have to ensure that you pop the necessary shadow handlers if delegation skips over respective handlers (which would be simpler if we replaced try-delegate with a more low-level rethrow_in, as suggested earlier).

RossTate commented 4 years ago

@rossberg All high-level and low-level systems with 2PEH (that I know of) have an unwind-like construct. But you claimed that this feature that was added to make programs compose better with each other and with future programs using future extensions is unnecessary. To back this claim, you gave an implementation strategy for 2PEH. But that implementation strategy does not compose well with other programs or with future extensions (specifically extensions that you are advocating for). Regardless of your concerns about how I have suggested we might support (composable) 2PEH in the future, there seems to be significant evidence that unwind will be helpful for any way we might support (composable) 2PEH in the future.

aheejin commented 4 years ago

@rossberg

So, rethrow does exactly what you want here, because throw/catch is only used to implement the unwinding phase (after search has already been completed). And rethrow resumes that. Actual source-level throw is implemented as a more high-level operation via the $throw function I showed. And if the source language had something akin to rethrow itself, then that would equally be implemented by a higher-level abstraction that invokes the search phase first explicitly.

I still don't understand how your scheme support cleanup code in the first phase. In the first phase, we should search for a matching handler. This shouldn't include cleanup code. If there's no matching handler, the program should crash without unwinding the stack.

You also don't like an option that filter functions in catch requires three possible values (1. catch handler + my exception, 2. catch handler + not my exception, 3. cleanup). Then how is the first search phase supposed to figure out a given catch is cleanup code or not? If it is cleanup code, the first phase should just skip it. We shouldn't catch an exception and unwind the stack to the point of the cleanup code, if there's no user catch handler somewhere up in the stack.

Or, do you think we should delegate everything to user space so each handler does its own thing, including saying "I'm cleanup code" "I'm a user handler" or something?


Also you are suggesting rethrow should be the same as resume, which is confusing. In my understanding, rethrow initiates a new two-phase search, and resume resumes the (already ongoing) second phase search. If we change rethrow to mean resume, can other languages, that has rethrow like construct, use it for their purpose?


If your scheme can answer both of these problems, I actually support its removal.

RossTate commented 4 years ago

For a given catch, the innermost instance of handler in the shadow stack will correspond to the innermost handler on the Wasm stack by construction. I don't see why anything else would be needed.

@rossberg It is important to be aware that this is not true. Only languages that do not strictly speak need 2PEH can have this property, and even then not all such languages do. As an example of this latter subset, in Python the kind of exception that is caught by a given except can be a dynamically determined value, and so different call frames for the same function can catch different exceptions. And for languages that do need 2PEH, the "filter" function (which is a misnomer) can have context/state-sensitive code.

This is why the macro-implementation of Common Lisp's handler-case (its version of filter-by-exception-tag handling) uses block/return-from, i.e. lexically scoped non-local control. The use of lexical scoping makes sure the unwinding phase proceeds up to the specific call frame on the stack already determined by the search phase, with no need to distinguish the chosen stack frame from other call frames of the same function.

Of course, we are not adding such a construct at the moment, and maybe we never will. Or maybe we will design a completely different way to do 2PEH. We just wanted to make room for such extensions, and the unwind concept has been demonstrated to compose well with a wide range of control constructs.

rossberg commented 4 years ago

@aheejin:

I still don't understand how your scheme support cleanup code in the first phase. In the first phase, we should search for a matching handler. This shouldn't include cleanup code. If there's no matching handler, the program should crash without unwinding the stack.

The first phase searches the shadow handler table and does not execute anything. Clean-up code is not in that table, so isn't taken into account.

You also don't like an option that filter functions in catch requires three possible values (1. catch handler + my exception, 2. catch handler + not my exception, 3. cleanup). Then how is the first search phase supposed to figure out a given catch is cleanup code or not?

As I said, clean-up code isn't even considered in the search. IOW, it is implemented differently from handlers (by the compiler) and no dynamic differentiation is needed. So catch and unwind are different just as they would be as Wasm constructs, except that the distinction is fully implemented in user code.

Also you are suggesting rethrow should be the same as resume, which is confusing. In my understanding, rethrow initiates a new two-phase search, and resume resumes the (already ongoing) second phase search. If we change rethrow to mean resume, can other languages, that has rethrow like construct, use it for their purpose?

Oh, I am not proposing to change anything about rethrow. I am demonstrating that we would not need to change anything to implement 2PEH. Rethrow already does what it takes. I am showing a producer-side implementation technique that would use it in a certain way (and that may be non-obvious if one only thinks about 2PEH in a particular way). And interacts correctly with another 1PEH language that uses rethrow in the conventional way.

There is one caveat, though, and that might be what you are getting at: if the 2PEH source language has a source-level rethrow itself, then that would compile to a regular Wasm-level throw under this scheme. So it would only include the stack trace from the rethrow point. But the combination of 2PEH and rethrow is rare enough and the loss small enough that it might be considered okay?

@RossTate:

For a given catch, the innermost instance of handler in the shadow stack will correspond to the innermost handler on the Wasm stack by construction. I don't see why anything else would be needed.

@rossberg It is important to be aware that this is not true.

Huh? Handlers on the real stack have a 1-to-1 correspondence with handlers on the shadow stack. That's the very definition of a shadow stack. If there are multiple invocations of the same function with a handler then there are multiple entries for that handler on the shadow stack, in the same order. That is all you need.

You seem to be conflating this with a completely separate problem, which is that some languages might require those entries to have access to some context from the handler's enclosing function. In such a setting, you have to program up the context closure, either by using Wasm closures (func.bind) as filters, or short of that, the good old way of threading through a context parameter manually. The same would be true for built-in 2PEH filters, because even then a filter cannot (realistically) be expected to be runnable in the function's frame, as I have pointed out various times already.

RossTate commented 4 years ago

Handlers on the real stack have a 1-to-1 correspondence with handlers on the shadow stack. That's the very definition of a shadow stack. If there are multiple invocations of the same function with a handler then there are multiple entries for that handler on the shadow stack, in the same order. That is all you need.

In your sketch, every handler has a corresponding catch, but your sketch does not have the property that the throw in a handler gets caught by its corresponding catch. That is, your sketch has a bug caused by using dynamically scoped non-local control rather than lexically scoped non-local control as other implementations of 2PEH use. I understand that it is a just a sketch, but the issue is that the sketch oversimplifies the issues and therefore makes particular solutions seem viable even though those solutions would not scale to real uses of 2PEH.

because even then a filter cannot (realistically) be expected to be runnable in the function's frame, as I have pointed out various times already.

You have said this many times, and yet it is not in line with what implementers said in other discussions. Regardless, unwind does not force us to commit to anything, whereas you seem to be arguing that we should commit to not supporting any extensions.

rossberg commented 4 years ago

In your sketch, every handler has a corresponding catch, but your sketch does not have the property that the throw in a handler gets caught by its corresponding catch. That is, your sketch has a bug caused by using dynamically scoped non-local control rather than lexically scoped non-local control as other implementations of 2PEH use.

EH control transfer is non-lexical by nature. Sorry, I don't see how there is a bug of the kind you are claiming. Can you provide an example of a throw that you think would go wrong?

because even then a filter cannot (realistically) be expected to be runnable in the function's frame, as I have pointed out various times already.

You have said this many times, and yet it is not in line with what implementers said in other discussions.

I don't know who you talked to and what you asked them, but I have worked on some of V8's calling conventions myself, and hence can tell you with some confidence that this will not fly without fundamental changes to its stack usage and assumptions about it, and likely affecting performance. I can't speak for other engines, but I would be very surprised if it worked seamlessly for them.

Regardless, unwind does not force us to commit to anything, whereas you seem to be arguing that we should commit to not supporting any extensions.

I'm arguing that we need to support features in a non-additive manner and that there are too many premature assumptions right now.

RossTate commented 4 years ago

EH control transfer is non-lexical by nature.

2PEH has two phases. The search phase is non-lexical by nature—you are searching the stack for a matching handler. But the unwinding phase is lexical—the matching handler indicates where to transfer control to (after unwinding). As I mentioned, this is explicit in Common Lisp, which desugars handler-case to a handler-bind where the handler function checks the appropriate tags on the exception and then, upon finding a match, uses return-from to jump (after unwinding) to the appropriate catch body.

Sorry, I don't see how there is a bug of the kind you are claiming. Can you provide an example of a throw that you think would go wrong?

(exception $unwind-to-XYZ (param i32))  ;; assume i32 is representation of language exception
(func $catch-XYZ (param $exn i32)
  (if (C)
    (then (throw $unwind-to-XYZ (local.get $exn))
  )
)

Suppose C is stateful. For example, it mutably flips the bit of some global boolean variable and checks if the result is true. If we have two such handlers on the shadow stack and the global boolean variable is initially true, the innermost handler will flip the bit and not throw, and then the outermost handler will flip the bit and throw. But then your innermost catch will catch the exception, whereas control was supposed to go to the outermost catch.

rossberg commented 4 years ago

Ah, okay, excellent point. Thanks! I stand corrected.

Of course, it wouldn't be hard to work around that by using a secondary dynamic token and compare that in the handler. But I admit that reduces the appeal of that solution. A more elegant solution would be possible if we removed the (somewhat artificial) limitation of static exception tag allocation in the EH proposal, which might be desirable for other things as well.

RossTate commented 3 years ago

Thanks for helping me find the right illustrative example. Did that also help convey to you the rationale of unwind then?

rossberg commented 3 years ago

Yeah, I'm still uncomfortable with committing to ad-hoc solutions prematurely and without a strategy that sound sounds different from "let's built in every feature". Dynamic tag generation would be a more orthogonal and principled solution to the particular problem you pointed out. OTOH, the potential harm of unwind is hopefully limited, so I won't die on this hill. But I do remain worried about the path this approach puts us on.

RossTate commented 3 years ago

At present the proposal has dynamically scoped local and non-local control but only lexically scoped local, so it seems natural to complete the square with lexically scoped non-local control. Regarding principles, lexically scoped non-local control has stronger parametricity properties. It also supports more efficient (in terms of order of complexity) non-local control transfers because there is no need to search. Dynamic tag generation can emulate lexically scoped non-local control but with weaker guarantees and poorer performance, and the current proposal can already emulate dynamic tag generation. As for the path, completing the square seems to cover the single-stack control space very well.

aheejin commented 3 years ago

I'm not very sure why we are discussing lexical vs. dynamically scoping here. Do we plan to support any dynamically scoped language? Is wasm a dynamically scoped model? (I think no)

I share @rossberg's concern that, even if we need a functionality similar to unwind in the follow-on 2PEH proposal, given that it does not have a meaningful difference with catch_all now, adding it in the current proposal seems premature. We don't know what the follow-on 2PEH proposal will look like, and adding unwind there looks OK to me.

ioannad commented 3 years ago

given that it [unwind] does not have a meaningful difference with catch_all now, adding it in the current proposal seems premature. We don't know what the follow-on 2PEH proposal will look like, and adding unwind there looks OK to me.

I agree with this. And as I feared in a comment to PR#137, trying to specify unwind is already leading to long discussions which probably belong in a follow-on 2PEH proposal. I think the point of changing the 2nd EH proposal to the current, was just to have it be compatible with a 2PEH follow-on proposal, not more.

ioannad commented 3 years ago

I just made PR #143 with a document attempting to describe the current spec, as @tlively requested.

@aheejin if you like the idea of keeping such a formal-overview.md document, perhaps we could close this issue by pointing to that PR? Or to the document when everyone agrees with it?

aheejin commented 3 years ago

Yes, I think continuing discussions in a dedicated issue or PR is more desirable. This issue started as a question from someone else that if we had a formal spec at all, and got expanded to include a number of topics I can't even count.