Closed sys5867 closed 3 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:
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.
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:
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)))
)
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)
)
Compile a source-level throw E
to (call $throw (E))
.
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)
)
)
...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.
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.
- 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".
- 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.)
- 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.
- ...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.
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 ofdynamic-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 whereaslongjmp
is more akin to lexical scoping
I don't see that. A longjmp's target is first-class and selected completely dynamically, not lexically.
- 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.
- 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.
- 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
. Usingtry
/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.
- ...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 acatch_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.
@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.
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)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.@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 separateresume
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 thethrow
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).
@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.
@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.
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.
@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 givencatch
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 asresume
, which is confusing. In my understanding,rethrow
initiates a new two-phase search, andresume
resumes the (already ongoing) second phase search. If we changerethrow
to meanresume
, can other languages, that hasrethrow
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.
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.
In your sketch, every handler has a corresponding
catch
, but your sketch does not have the property that thethrow
in a handler gets caught by its correspondingcatch
. 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.
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
.
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.
Thanks for helping me find the right illustrative example. Did that also help convey to you the rationale of unwind
then?
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.
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.
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.
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 addingunwind
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.
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?
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.
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 withthrow
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?