-
Notifications
You must be signed in to change notification settings - Fork 745
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
[EH] Add translator from old to new EH instructions #6210
Conversation
This translates the old Phase 3 EH instructions, which include `try`, `catch`, `catch_all`, `delegate`, and `rethrow`, into the new EH instructions, which include `try_table` (with `catch` / `catch_ref` / `catch_all` / `catch_all_ref`) and `throw_ref`, passed at the Oct 2023 CG meeting. This translator can be used as a standalone tool by users of the previous EH toolchain to generate binaries for the new spec without recompiling, and also can be used at the end of the Binaryen pipeline to produce binaries for the new spec while the end-to-end toolchain implementation for the new spec is in progress. While the goal of this pass is not optimization, this tries to a little better than the most naive implementation, namely by omitting a few instructions where possible and trying to minimize the number of additional locals, because this can be used as a standalone translator or the last stage of the pipeline while we can't post-optimize the results because the whole pipeline (-On) is not ready for the new EH.
src/wasm-builder.h
Outdated
} | ||
block->finalize(type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive-by fix. We should finalize even if append
is nullptr
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that true? If we aren't appending anything, then should the block already exist and be finalized or be brand new and finalized by makeBlock
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the old logic here makes sense in that it assumes that if there is a previous block then that block is valid (i.e. finalized), but the comment for the function was not that clear (probably my fault).
@aheejin is this change needed for the new pass for some reason? If so perhaps we can add a new function or rethink this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right, but things are a little tricky.
So first off, I think we should've passed name
to makeBlock
in the first place, because makeBlock
uses BranchSeeker
when finalizing only when the block has a name, and we need that BranchSeeker
to figure out the inner try_table (catch_ref $label)
is targeting $label
and its return type is exnref
.
Lines 197 to 209 in 1850199
// The default type is according to the value that flows out. | |
BranchUtils::BranchSeeker seeker(this->name); | |
Expression* temp = this; | |
seeker.walk(temp); | |
if (seeker.found) { | |
// Calculate the LUB of the branch types and the flowed-out type. | |
seeker.types.insert(type); | |
type = Type::getLeastUpperBound(seeker.types); | |
} else { | |
// There are no branches, so this block may be unreachable. | |
handleUnreachable(this, NoBreak); | |
} | |
} |
But there's another problem. If the passed expression is already a block, blockifyWithName
reuses it, in which case the passed type
parameter (in case it is not null) doesn't get used. This doesn't mean the existing block is invalid. For example, in processDelegateTarget
, the input to blockfiyWithName
is
(block
(try_table (catch_all_ref $label)
...
)
)
The block
here is the body of the outer try
(which is targeted by the inner delegate
, which has been converted to try_table
). The naive output we would want is this:
(block $label (result exnref)
(block
(try_table (catch_all_ref $label)
...
)
)
)
(block $label)
's result type is exnref
because it is targeted by catch_all_ref
. But with blockifyWithName
, we can reuse the input block if it is already a block. So we can do
(block $label (result exnref)
(try_table (catch_all_ref $label)
...
)
)
But in this case we need a refinalization of the block
with the given type type
, even if the given block is not invalid, because we are reusing it.
I updated blockifyWithName
so that it satisfies this, PTAL. So now we pass name
to makeBlock
, and also do finalization when type
is explicitly given.
--- a/src/wasm-builder.h
+++ b/src/wasm-builder.h
@@ -1323,11 +1323,14 @@ public:
block = any->dynCast<Block>();
}
if (!block || block->name.is()) {
- block = makeBlock(any);
+ block = makeBlock(name, any);
+ } else {
+ block->name = name;
}
- block->name = name;
if (append) {
block->list.push_back(append);
+ }
+ if (append || type) {
block->finalize(type);
}
return block;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points. I had not considered the situation when the appended item branches to the new name.
I think your diff there looks right. But as this is non-trivial, what do you think about putting that in another PR? Also, I see that in the current codebase, only one file (SimplifyLocals) uses blockifyWithName
, and it always does so with an empty Name()
so perhaps there is an opportunity to simplify things in that PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in #6223, even though I'm not sure how to add a standalone test there... (Will rebase this PR onto that after that lands, because I'm not using any fancy tools supporting stacked PRs)
src/passes/passes.h
Outdated
@@ -164,6 +164,8 @@ Pass* createStripEHPass(); | |||
Pass* createStubUnsupportedJSOpsPass(); | |||
Pass* createSSAifyPass(); | |||
Pass* createSSAifyNoMergePass(); | |||
Pass* createTranslateEHOldToNewPass(); | |||
Pass* createTranslateEHNewToOldPass(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have both directions here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second was a TODO plan; see the comments at the top of TranslateEH.cpp
. But I will delete this for now given that this has not been implemented anyway.
;; CHECK-NEXT: (throw_ref | ||
;; CHECK-NEXT: (block $l01 (result exnref) | ||
;; CHECK-NEXT: (br $outer2 | ||
;; CHECK-NEXT: (try_table (result i32) (catch_all_ref $l01) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised to see a single try_table
for a testcase that has two try
s. Is that an optimization or must it be like this for some reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it was a bug. Pushed a fix, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/wasm-builder.h
Outdated
} | ||
block->finalize(type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the old logic here makes sense in that it assumes that if there is a previous block then that block is valid (i.e. finalized), but the comment for the function was not that clear (probably my fault).
@aheejin is this change needed for the new pass for some reason? If so perhaps we can add a new function or rethink this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't taken a look at the tests yet, but here are comments on the code.
src/passes/TranslateEH.cpp
Outdated
|
||
std::unordered_set<Name> delegateTargets; | ||
std::unordered_set<Name> rethrowTargets; | ||
bool isTargettedByDelegates(Try* curr) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be spelled "targeted" with one 't' between the 'e's.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh really... Didn't know that. Will fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: c44a66d
src/passes/TranslateEH.cpp
Outdated
return rethrowTargetToExnrefLocal.find(rethrowTarget) != | ||
rethrowTargetToExnrefLocal.end(); | ||
} | ||
Index getExnrefLocal(Name rethrowTarget) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think the code would be any nicer if you combined has
and get
methods by either returning empty Name
s in the "not present" case or using std::optional
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: 0460656
src/passes/TranslateEH.cpp
Outdated
std::unique_ptr<LabelUtils::LabelManager> labels; | ||
std::unique_ptr<TargetTryLabelScanner> labelScanner; | ||
std::unique_ptr<ExnrefLocalAssigner> localAssigner; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do these need to be behind std::unique_ptr
? If they need to be lazily initialized, could they use std::optional
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used unique_ptr
mainly to avoid manually deleting those instances. Does std::optional
delete the pointers for me? I don't think so... Would there be a better way to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean you could use e.g. std::optional<LabelUtils::LabelManager> labels;
. It would still manage the lifetime of the object for you automatically and let you initialize it whenever you want, it just wouldn't require a heap allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, std::optional
also deletes the pointer when it is deleted. But in this case they are not strictly optional; they are just initialized later. Given that std::optional
also deletes the pointer, I will change the code to use it, but what's a good practice, or criteria, on how to use std::unique_ptr
vs. std::optional
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that std::optional is always better unless there’s a specific reason you need to do a heap allocation, e.g. very large data that would overflow the stack if stored in a std::optional or recursive data that requires an indirection to avoid having infinite size.
// (use_inst | ||
// (local.get $scratch-i32) | ||
// ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid the scratch local entirely by replacing the pop
with the entire block $catch
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of catch
, we can, because pop
is always the first instruction in the catch body in the execution order. So if there's a single catch
, it can be like
(block $outer
(use_inst
(block $l0 (result i32)
(try_table (catch $l0 $tag)
...
)
(br $outer)
)
)
)
But in case of catch_all
(used for destructors in C++), it's hard, because there would be another instruction (calls to a destructor) before a throw_ref
, so throw_ref
is not the first instruction within a catch_all
body. For example, in case of a single catch_all
,
(block $outer
(call $destructor) ;; We can't do this!
(throw_ref
(block $l0 (result exnref)
(try_table (catch $l0 $tag)
...
)
(br $outer)
)
)
)
We can't do this because we can't bypass the (call $destructor)
.
And really the only thing that matters to the code size for C++ programs is destructor calls with try
-catch_all
, because there are so many of them. The number of try
-catch
, which is generated from C++ try
-catch
clauses, is so small compared to that of try
-catch_all
. (J2Wasm currently doesn't even use catch_all
and a very small number of catch
es, so we don't need to worry about it)
So if we can't transform try
-catch_all
, I think keeping the current transformation is simpler, because we can apply the same way of transformation to all catch
variants. If we do one thing for catch
and something else for catch_all_ref
that will be more complicated and that doesn't decrease code size much anyway. Also after implementing Binaryen support for optimization pipeline, I think it can take care of those catch
es later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, that sounds reasonable.
// (throw_ref | ||
// (local.get $exn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we eliminate these scratch locals as well by making the block $catch_all
a child of the throw_ref
? For the tuple case below we could use a (tuple.extract 2 1 (local.tee $tuple) ...
to make the same pattern work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #6210 (comment). Please let me know if I'm missing something.
src/passes/TranslateEH.cpp
Outdated
// If sometype (func's type) is none: | ||
// INNER_BODY = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stale?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, removed.
;; CHECK-NEXT: (block $outer3 | ||
;; CHECK-NEXT: (block $catch_all4 | ||
;; CHECK-NEXT: (try_table (catch_all $catch_all4) | ||
;; CHECK-NEXT: (throw_ref |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading testcases like this, I get the impression that the new code is less efficient than the old. Is that right? Specifically it looks like to implement a delegate we do a branch and a throw, and before we didn't have this manual throw operation, so the VM could have just jumped along the delegate path.
If that makes sense, a followup question, would new compiler output be able to avoid the extra overhead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I also (was aware but realize again) that the old spec is very concise.
I ran the translator on an Adobe binary hanging around somewhere in my local machine and the code size increase is 4.2%. We made the switch knowing this will make the code size worse. But one thing I realized was my estimation was incorrect. As you may know, I once estimated the code size increase (for Adobe) to be around 2.1~2.2%, given that we switch to the option that is similar to what we chose and we remove delegate
.
The most important case, and actually the only case that matters, a simple try
-catch_all
-rethrow
, which is the most common because it is used for destructors. The generated code for other cases are a lot worse due to tuples and such but they are a lot less frequent so they don't affect the code size much.
try $l
...
catch_all
... ;; cleanup
rethrow $l
end
My (incorrect) estimation (in the doc) was
block $label0
try_table (catch_all_ref $label0)
...
end
end
local.set $exn
... ;; cleanup
local.get $exn
throw_ref
But this is missing some instructions. The correct version should be
block $outer
block $label0
try_table (catch_all_ref $label0)
...
end
br $outer
end
local.set $exn
... ;; cleanup
local.get $exn
throw_ref
end
So I missed that we need an outer block
and end
and a br
to it in case exceptions are not thrown. Anyway this seems to make the code size increase double. delegate
transformation seems to be as I expected it to be, so I don't think that estimation was the culprit...
Anyway, this number might go down after optimizations, but at this point, it's worse than I estimated. Not sure if there is any way we can mitigate this... If we gzip the binaries the size increase goes down to around 2%.
If that makes sense, a followup question, would new compiler output be able to avoid the extra overhead?
I doubt it. How I translate the code here is exactly how I would generate code in LLVM. Not sure if there is a better sequence, but I can't find one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, thanks for the details!
Personally I feel that 2% or so worse (than the 2% we expected) is not terrible since we are getting advantages in return for this change.
However, the delegate stuff worries me, because my naive expectation is that VMs might emit slower code. Specifically delegates seem like they statically told the VM where to go - "if this try catches it, just go to this other try" - but now we need an extra rethrow - "if this try catches it, rethrow right before the try whose catch I want to enter". The VM would need to pattern-match a rethrow that goes straight into a catch to make that efficient, I guess?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a possibility, but exception code is executed in rare circumstances in the first place, so I wonder if they consist of a significant portion of the runtime. (Destructors too; try
-catch_all
destructors run only when an exception is thrown elsewhere and we need to run destructors before we rethrow the exception to the enclosing scope. Running destructors in normal circumstances doesn't use try
-catch_all
paths.)
cc @thibaudmichaud ... Would throw_ref
be slower than try
-delegate
, and if so, how much?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is fast enough in the VM then sorry for the noise here, btw. My understanding of the work the VM does for exceptions is very limited 😄
Re-reading the output of this test here (starting on line 1313) I wonder if the delegate which goes to $l00
, and then throws to be caught immediately again, could avoid the second throw by going directly to $catch_all4
? We do know this statically so it seems possible. OTOH there is a mismatch because one is a catch_all
and the other a catch_all_ref
, but even so, can we jump to a place that does another jump rather than a throw? (Again, all of this is based on my intuition that jumps are faster than throws - if that's inaccurate, feel free to ignore...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make sure I follow, that is in the example that has
catch_all
, but if it were a catch of a specific tag, that would need to match between the two trys? (But maybe we can optimize in some cases even without a match?)
To be more precise, I think this optimization is possible only when the targeted try
has a single catch_all
; try
-catch
-catch_all
wouldn't count, given that we don't know whether this exception is gonna be caught in the catch
or catch_all
. We can only branch to somewhere when we are certain the exception will be caught there.
I'm not sure what you mean by a match between two try
s. The inner one is a try
-delegate
here, so it can't have tags I think..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I think we can optimize an inner try
-delegate
targeting an outer try
-delegate
too... because the destination for the inner delegate
should be the same as the destination for the outer delegate
, wherever that is. In case the outer delegate
targets another try
-catch_all
, the both inner and outer delegate
can be optimized to target the third catch_all
... which gets complicated 🫠
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, sorry, I always found the delegate stuff confusing... I was confused because I thought for some reason a delegate was for a particular tag or such. Yeah, sounds like we need a catch-all in the outer one then, as you said.
Btw, one possible option for later optimization work here is to do less in this pass and add general TryTable
optimizations elsewhere. For example RemoveUnsedBrs
has a jump-threading section which might be adapted to also thread exceptions perhaps (since exception control flow is just branches, it could fit there, I hope). Anyhow, just a thought for later maybe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was under the impression that it'd be easier to do things here because this pass has the access to the original try
-delegate
structure, which I thought was simpler and didn't involve other instructions such as block
s. But maybe things are easier to maintain if we use something like jump threading after all..? But even if we use jump threading I guess we should do at least certain things here because jump threading itself wouldn't take care of throw_ref
s and catch_all_ref
s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, yeah, probably doing some stuff here and some elsewhere is best. This pass should definitely use the delegate-specific info it knows, as you said.
But in general I was thinking that in the long term we will need this pass less, while any general-purpose optimizations on TryTable would be helpful forever. So I'd lean in that direction where possible (but I don't know where that line is).
- This passes `name` to `makeBlock` call, because `makeBlock` uses `BranchSeeker` when finalizing only when the block has a `name`. - This also refinalizes the block when an optional `type` is given. This was spun off from WebAssembly#6210, but I'm not sure how to add a standalone test for this.
|
||
// Depth of the current try nest, when only counting trys targeted with | ||
// rethrows. | ||
size_t rethrowTryDepth = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC tracking the depth is an optimization? If so then we could more simply allocate a unique local for each try target, and rely on other passes to coalesce and remove unneeded ones. (If we want this pass to emit efficient code we can still do that, if we run coalesce-locals,reorder-locals
internally.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but I'm not sure we can run coalesce-locals
or reorder-locals
with the new EH at this point yet... Supporting it in them wouldn't be a ton of work though. I wanted to make the translator emit maybe not the best but ok code at this point without spending more time on the optimization pipeline.
Also if we assign a local to each try
the number of locals can be prohibitive. CoalesceLocal.cpp
says
binaryen/src/passes/CoalesceLocals.cpp
Lines 22 to 25 in 2dfa01f
// NB: This pass is nonlinear in the number of locals. It is best to run it | |
// after the number of locals has been somewhat reduced by other passes, | |
// for example by simplify-locals (to remove unneeded uses of locals) and | |
// reorder-locals (to sort them by # of uses and remove all unneeded ones). |
simplify-locals
too?
Anyway, given that the optimization pipeline is not ready for the new EH yet, and also given that this local assignment in this pass is not very complicated, I am inclined to keep this at least for the moment, even though maybe we can delete that later after the opt pipeline is ready. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sgtm, this seems fine for now.
About that comment, in general we'd need simplify-locals
, but I think the locals added in this pass will not benefit from that particular pass just because of the form they are in. I could be wrong though...
src/passes/TranslateEH.cpp
Outdated
if (localAssigner->hasExnrefLocal(curr->name)) { | ||
Index local = localAssigner->getExnrefLocal(curr->name); | ||
for (auto* throwRef : FindAll<ThrowRef>(catchBody).list) { | ||
if (auto* localGet = throwRef->exnref->cast<LocalGet>()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case I think the if is unneeded? (cast
always succeeds)
- This passes `name` to `makeBlock` call, because `makeBlock` uses `BranchSeeker` when finalizing only when the block has a `name`. - This also refinalizes the block when an optional `type` is given. This was spun off from #6210, but I'm not sure how to add a standalone test for this.
Co-authored-by: Alon Zakai <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm to land this and iterate later as needed.
@tlively Any more concerns on the PR? |
I added at last one instruction that can throw, like a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No concerns, LGTM!
- This passes `name` to `makeBlock` call, because `makeBlock` uses `BranchSeeker` when finalizing only when the block has a `name`. - This also refinalizes the block when an optional `type` is given. This was spun off from WebAssembly#6210, but I'm not sure how to add a standalone test for this.
This translates the old Phase 3 EH instructions, which include `try`, `catch`, `catch_all`, `delegate`, and `rethrow`, into the new EH instructions, which include `try_table` (with `catch` / `catch_ref` / `catch_all` / `catch_all_ref`) and `throw_ref`, passed at the Oct 2023 CG meeting. This translator can be used as a standalone tool by users of the previous EH toolchain to generate binaries for the new spec without recompiling, and also can be used at the end of the Binaryen pipeline to produce binaries for the new spec while the end-to-end toolchain implementation for the new spec is in progress. While the goal of this pass is not optimization, this tries to a little better than the most naive implementation, namely by omitting a few instructions where possible and trying to minimize the number of additional locals, because this can be used as a standalone translator or the last stage of the pipeline while we can't post-optimize the results because the whole pipeline (-On) is not ready for the new EH.
This translates the old Phase 3 EH instructions, which include
try
,catch
,catch_all
,delegate
, andrethrow
, into the new EH instructions, which includetry_table
(withcatch
/catch_ref
/catch_all
/catch_all_ref
) andthrow_ref
, passed at the Oct 2023 CG meeting.This translator can be used as a standalone tool by users of the previous EH toolchain to generate binaries for the new spec without recompiling, and also can be used at the end of the Binaryen pipeline to produce binaries for the new spec while the end-to-end toolchain implementation for the new spec is in progress.
While the goal of this pass is not optimization, this tries to a little better than the most naive implementation, namely by omitting a few instructions where possible and trying to minimize the number of additional locals, because this can be used as a standalone translator or the last stage of the pipeline while we can't post-optimize the results because the whole pipeline (-On) is not ready for the new EH.