Skip to content
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

Merged
merged 19 commits into from
Jan 24, 2024

Conversation

aheejin
Copy link
Member

@aheejin aheejin commented Jan 8, 2024

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`, 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.
}
block->finalize(type);
Copy link
Member Author

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

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member Author

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.

binaryen/src/wasm/wasm.cpp

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;

Copy link
Member

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.

Copy link
Member Author

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)

@@ -164,6 +164,8 @@ Pass* createStripEHPass();
Pass* createStubUnsupportedJSOpsPass();
Pass* createSSAifyPass();
Pass* createSSAifyNoMergePass();
Pass* createTranslateEHOldToNewPass();
Pass* createTranslateEHNewToOldPass();
Copy link
Member

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?

Copy link
Member Author

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)
Copy link
Member

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 trys. Is that an optimization or must it be like this for some reason?

Copy link
Member Author

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
block->finalize(type);
Copy link
Member

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.

Copy link
Member

@tlively tlively left a 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.


std::unordered_set<Name> delegateTargets;
std::unordered_set<Name> rethrowTargets;
bool isTargettedByDelegates(Try* curr) const {
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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 Show resolved Hide resolved
return rethrowTargetToExnrefLocal.find(rethrowTarget) !=
rethrowTargetToExnrefLocal.end();
}
Index getExnrefLocal(Name rethrowTarget) const {
Copy link
Member

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 Names in the "not present" case or using std::optional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: 0460656

Comment on lines 196 to 198
std::unique_ptr<LabelUtils::LabelManager> labels;
std::unique_ptr<TargetTryLabelScanner> labelScanner;
std::unique_ptr<ExnrefLocalAssigner> localAssigner;
Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

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.

src/passes/TranslateEH.cpp Outdated Show resolved Hide resolved
Comment on lines +482 to +484
// (use_inst
// (local.get $scratch-i32)
// )
Copy link
Member

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?

Copy link
Member Author

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 catches, 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 catches later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that sounds reasonable.

src/passes/TranslateEH.cpp Outdated Show resolved Hide resolved
Comment on lines +512 to +513
// (throw_ref
// (local.get $exn)
Copy link
Member

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.

Copy link
Member Author

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 Show resolved Hide resolved
Comment on lines 761 to 762
// If sometype (func's type) is none:
// INNER_BODY =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale?

Copy link
Member Author

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
Copy link
Member

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?

Copy link
Member Author

@aheejin aheejin Jan 10, 2024

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.

Copy link
Member

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?

Copy link
Member Author

@aheejin aheejin Jan 10, 2024

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?

Copy link
Member

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...)

Copy link
Member Author

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 trys. The inner one is a try-delegate here, so it can't have tags I think..?

Copy link
Member Author

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 🫠

Copy link
Member

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.

Copy link
Member Author

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 blocks. 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_refs and catch_all_refs.

Copy link
Member

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).

aheejin added a commit to aheejin/binaryen that referenced this pull request Jan 12, 2024
- 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.
src/passes/TranslateEH.cpp Outdated Show resolved Hide resolved

// Depth of the current try nest, when only counting trys targeted with
// rethrows.
size_t rethrowTryDepth = 0;
Copy link
Member

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.)

Copy link
Member Author

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

// 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).
, which means we should run 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?

Copy link
Member

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...

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>()) {
Copy link
Member

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)

aheejin added a commit that referenced this pull request Jan 16, 2024
- 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.
Copy link
Member

@kripken kripken left a 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.

@aheejin
Copy link
Member Author

aheejin commented Jan 22, 2024

@tlively Any more concerns on the PR?

@aheejin
Copy link
Member Author

aheejin commented Jan 23, 2024

I added at last one instruction that can throw, like a call, within try bodies in the tests. It is more realistic, and as we will see later with other optimizations, without any throwable instructions within a try body, CFG doesn't show the all possible paths.

Copy link
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No concerns, LGTM!

@aheejin aheejin merged commit 1ce851d into WebAssembly:main Jan 24, 2024
14 checks passed
@aheejin aheejin deleted the eh_old_new_translator branch January 24, 2024 02:52
radekdoulik pushed a commit to dotnet/binaryen that referenced this pull request Jul 12, 2024
- 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.
radekdoulik pushed a commit to dotnet/binaryen that referenced this pull request Jul 12, 2024
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.
@gkdn gkdn mentioned this pull request Aug 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants