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

How to implement delegate's target #3497

Closed
aheejin opened this issue Jan 15, 2021 · 9 comments
Closed

How to implement delegate's target #3497

aheejin opened this issue Jan 15, 2021 · 9 comments

Comments

@aheejin
Copy link
Member

aheejin commented Jan 15, 2021

The following is the new instruction delegate's semantics, taken from my LLVM CL's comment:

Linearizing the control flow by placing TRY / END_TRY markers can create
mismatches in unwind destinations for throwing instructions, such as calls.

We use the 'delegate' instruction to fix the unwind mismatches. 'delegate'
instruction delegates an exception to an outer 'catch'. It can target not
only 'catch' but all block-like structures including another 'delegate',
but with slightly different semantics than branches. When it targets a
'catch', it will delegate the exception to that catch. It is being
discussed how to define the semantics when 'delegate''s target is a non-try
block: it will either be a validation failure or it will target the next
outer try-catch. But anyway our LLVM backend currently does not generate
such code. The example below illustrates where the 'delegate' instruction
in the middle will delegate the exception to, depending on the value of N.
try
  try
    block
      try
        try
          call @foo
        delegate N    ;; Where will this delegate to?
      catch           ;; N == 0
      end
    end               ;; N == 1 (invalid; will not be generated)
  delegate            ;; N == 2
catch                 ;; N == 3
end
                      ;; N == 4 (to caller)

So delegate's immediate field can target all control flow structures including blocks and loops, like brs. But if the target is block or loop, it will be a validation failure. When it targets a try, it is really targetting not try but its catch.

Then here comes the catch: Currently if and try do not take label names; they use wrapping blocks or inner blocks to achieve the same semantics, mostly because there are too many places to handle if we add a new control flow structure that can have a label. But if we want to give delegate a label, it should target a try (more precisely its catch), and if it targets a block, it will be a validation failure. But I'm not sure if we want to give try a label and add its handling in a bunch of places, which is exactly what we wanted to avoid when we implemented if and try this way.

We are already doing some ugly things to implement try-catch.

  1. We create an inner block within catch, but we assume that block is removed when we print the text/binary. The reason it should be removed is catch body should start with a pop instruction (or a value on the stack in binary), and block should not interfere in between. This is easily done when there's no branch that targets the inner block.
  2. When there is br 0 within a catch, we wrap the whole try-catch with a block and make the br target it, not the inner block within catch, for the reason I described in 1. (Fix inner block problem with 'catch' #3129)

We can probably do a similar thing for delegate's target... Like, wrap the target try-catch with a block and make delegate target that instead, and we delete that block when we write the binary/text...? But unlike the wrapping block we created for br, delegate targetting a block is a validation failure and it must be removed when we write that out. Also in this case we should maintain the relationship between the wrapping block and try for the whole optimization pipeline, i.e., no other instruction should interfere between them and the block should not be removed.

Not sure what is the cleaniest way. Any ideas?
@kripken @tlively

@kripken
Copy link
Member

kripken commented Jan 15, 2021

What happens in the case of N == 2 in that example, where the nested delegate targets the outer delegate? Does it simply continue to run from the outer delegate's position? And what does that outer delegate do when reached normally - is it the same as delegate 0? Or are delete N and delegate two fundamentally different instructions, one that sends and one that receives..?

(delegate is not in the EH overview so I'm not sure where to read about it?)

@tlively
Copy link
Member

tlively commented Jan 15, 2021

One option would be to give try expressions labels that don't participate in normal branching. So for normal branches to the end of a try, you would still need to wrap it in a block, just like we do with if-else. All code that only deals with normal branching would be able to ignore the try labels, which would be used only by code dealing with delegate. I don't know how much that would help in practice, but that's the best I've come up with so far.

@aheejin
Copy link
Member Author

aheejin commented Jan 17, 2021

@kripken

(delegate is not in the EH overview so I'm not sure where to read about it?)

Sorry I should've been clearer. This instruction was named catch_br when I presented it in the CG meeting but later renamed to delegate. Its informal semantics is written here: WebAssembly/exception-handling#137, which was not merged yet. But what I wrote in this issue description is actually more detailed.

What happens in the case of N == 2 in that example, where the nested delegate targets the outer delegate? Does it simply continue to run from the outer delegate's position?

It goes to the outer delegate. The outer delegate should also have its own immediate argument (though not written in this comment), so it will be transferred from there using the same rule.

And what does that outer delegate do when reached normally - is it the same as delegate 0? Or are delete N and delegate two fundamentally different instructions, one that sends and one that receives..?

(You mean not delete but delegate right?)
Sorry, delegate always has an immediate argument. I just didn't write that in the outer delegate, which made it confusing. And delegate cannot be reached normally. It is kind of an EH pad. In the MVP spec,

try
  ...
delegate N

is similar to

try
  ...
catch_all
  rethrow to somewhere (currently rethrow can only rethrow to the immediate outer scope, but delegate can bypass scope by its argument N)
end

@kripken
Copy link
Member

kripken commented Jan 19, 2021

Thanks!

Can a try have both catches and a delegate? That is, is this possible? (I don't see such an example here or in the links)

try
  ...
catch ...
  ...
delegate N

@aheejin
Copy link
Member Author

aheejin commented Jan 20, 2021

@kripken No a try can only have either catches or a delegate.

@kripken
Copy link
Member

kripken commented Jan 20, 2021

I see, thanks. So is it accurate to say that a delegate targets a try, really, and not a catch or a delegate? (As I think one cannot target a specific catch of a try's catches? So if an exception is delegated to a try, we go through the catches one by one as if it were thrown in that try originally?)

edit: to be clear, I'm not sure I follow

But if we want to give delegate a label, it should target a try (more precisely its catch),

can catch be replaced with catches?

@kripken
Copy link
Member

kripken commented Jan 20, 2021

If that's correct, then I think I understand the semantics. And then I think we should maybe add a label to try - not a normal block label, but a new kind of identifier, that is only used in try and in delegate, to connect them.

The other options worry me, in particular we have optimizations that want to optimize out blocks or move them around. Furthermore, adding a block that cannot be removed would not model the binary size as well, since it would not appear in the output.

@aheejin
Copy link
Member Author

aheejin commented Jan 20, 2021

I see, thanks. So is it accurate to say that a delegate targets a try, really, and not a catch or a delegate?

Semantically, delegate only targets catches or another delegate.
The reason I somewhat described like it targets try is when writing wast files and also in the internal representation of Binaryen in which branch-like instructions has Name as a target, only try (or a block wrapping try has a label) but not a catch. I don't think we can add a label in catch in the wast files...

The reason we made it also target block ends, which is a validation failure, is because VM people thought it would be convenient to not maintain a separate EH pad stack; this way they can use the stack they use to calculate branch depths the same.

(As I think one cannot target a specific catch of a try's catches? So if an exception is delegated to a try, we go through the catches one by one as if it were thrown in that try originally?)

Yes you're correct. It cannot target a specific catch within a group of catches within a try. It can only target a group of catches within a try as a whole.

edit: to be clear, I'm not sure I follow

But if we want to give delegate a label, it should target a try (more precisely its catch),

can catch be replaced with catches?

Yes. Sorry that my initial description was not very precise.

aheejin added a commit to aheejin/binaryen that referenced this issue Feb 8, 2021
This adds support for reading/writing of the new 'delegate' instruction
in the folded wast format, the stack IR format, and the binary format in
Binaryen. We don't have a format spec written down yet, but please refer
to WebAssembly/exception-handling#137 and
WebAssembly/exception-handling#146 for the informal semantics.
In the current version of spec `delegate` is basically a rethrow, but
with branch-like immediate argument so that it can bypass other
catches/delegates in between.

'delegate' is not represented a new `Expression`, but it is rather an
option within a `Try` class, like `catch`/`catch_all`.

`delegate` semantically targets an outer `catch` or `delegate`, but we
write `delegate` target as a `try` label because we only give labels to
block-like scoping expressions. So far we has not given `Try` a label
and used inner blocks or a wrapping block in case a branch targets the
`try`. But in case of `delegate`, it can syntactically only target `try`
and if it targets blocks or loops it is a validation failure.

So after discussions in WebAssembly#3497, we give `Try` a label but this label can
only be targeted by `delegate`s. Unfortunately this makes parsing and
writing of `Try` expression somewhat complicated. Also there is one
special case; if the immediate argument of `try` is the same as the
depth of control flow stack, this means the 'delegate' delegates to the
caller. To handle this case this adds a fake label
`DELEGATE_CALLER_TARGET`, and when writing it back to the wast format
writes it as an immediate value, unlike other cases in which we write
labels.

This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label
and `delegate`'s target. There are many cases that `try` and
`delegate`'s labels need to be treated in the same way as block and
branch labels, such as for hashing or comparing. But there are routines
in which we automatically assume all label uses are branches. I thought
about adding a new kind of defines such as
`DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some
duplication of existing routines or classes. So at the moment this PR
chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for
`try` and `delegate` labels and makes only necessary amount of changes
in branch-utils. We can revisit this decision later if necessary.

This only supports reading and writing and has not been tested against
any optimization passes yet.
aheejin added a commit to aheejin/binaryen that referenced this issue Feb 8, 2021
This adds support for reading/writing of the new 'delegate' instruction
in the folded wast format, the stack IR format, and the binary format in
Binaryen. We don't have a format spec written down yet, but please refer
to WebAssembly/exception-handling#137 and
WebAssembly/exception-handling#146 for the informal semantics.
In the current version of spec `delegate` is basically a rethrow, but
with branch-like immediate argument so that it can bypass other
catches/delegates in between.

'delegate' is not represented a new `Expression`, but it is rather an
option within a `Try` class, like `catch`/`catch_all`.

One special thing about `delegate` is, even though it is written
_within_ a `try` in the folded wat format, like
```wasm
(try
  (do
    ...
  )
  (delegate $l)
)
```
In the unfolded wat format or in the binary format, `delegate` serves as
a scope end instruction so there is no separate `end`:
```wasm
try
  ...
delegate $l
```

`delegate` semantically targets an outer `catch` or `delegate`, but we
write `delegate` target as a `try` label because we only give labels to
block-like scoping expressions. So far we has not given `Try` a label
and used inner blocks or a wrapping block in case a branch targets the
`try`. But in case of `delegate`, it can syntactically only target `try`
and if it targets blocks or loops it is a validation failure.

So after discussions in WebAssembly#3497, we give `Try` a label but this label can
only be targeted by `delegate`s. Unfortunately this makes parsing and
writing of `Try` expression somewhat complicated. Also there is one
special case; if the immediate argument of `try` is the same as the
depth of control flow stack, this means the 'delegate' delegates to the
caller. To handle this case this adds a fake label
`DELEGATE_CALLER_TARGET`, and when writing it back to the wast format
writes it as an immediate value, unlike other cases in which we write
labels.

This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label
and `delegate`'s target. There are many cases that `try` and
`delegate`'s labels need to be treated in the same way as block and
branch labels, such as for hashing or comparing. But there are routines
in which we automatically assume all label uses are branches. I thought
about adding a new kind of defines such as
`DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some
duplication of existing routines or classes. So at the moment this PR
chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for
`try` and `delegate` labels and makes only necessary amount of changes
in branch-utils. We can revisit this decision later if necessary.

Many of changes to the existing test cases are because now all `try`s
are automatically assigned a label. They will be removed in
`RemoveUnusedNames` pass in the same way as block labels if not targeted
by any delegates.

This only supports reading and writing and has not been tested against
any optimization passes yet.
aheejin added a commit to aheejin/binaryen that referenced this issue Feb 10, 2021
This adds support for reading/writing of the new 'delegate' instruction
in the folded wast format, the stack IR format, and the binary format in
Binaryen. We don't have a format spec written down yet, but please refer
to WebAssembly/exception-handling#137 and
WebAssembly/exception-handling#146 for the informal semantics.
In the current version of spec `delegate` is basically a rethrow, but
with branch-like immediate argument so that it can bypass other
catches/delegates in between.

'delegate' is not represented a new `Expression`, but it is rather an
option within a `Try` class, like `catch`/`catch_all`.

One special thing about `delegate` is, even though it is written
_within_ a `try` in the folded wat format, like
```wasm
(try
  (do
    ...
  )
  (delegate $l)
)
```
In the unfolded wat format or in the binary format, `delegate` serves as
a scope end instruction so there is no separate `end`:
```wasm
try
  ...
delegate $l
```

`delegate` semantically targets an outer `catch` or `delegate`, but we
write `delegate` target as a `try` label because we only give labels to
block-like scoping expressions. So far we has not given `Try` a label
and used inner blocks or a wrapping block in case a branch targets the
`try`. But in case of `delegate`, it can syntactically only target `try`
and if it targets blocks or loops it is a validation failure.

So after discussions in WebAssembly#3497, we give `Try` a label but this label can
only be targeted by `delegate`s. Unfortunately this makes parsing and
writing of `Try` expression somewhat complicated. Also there is one
special case; if the immediate argument of `try` is the same as the
depth of control flow stack, this means the 'delegate' delegates to the
caller. To handle this case this adds a fake label
`DELEGATE_CALLER_TARGET`, and when writing it back to the wast format
writes it as an immediate value, unlike other cases in which we write
labels.

This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label
and `delegate`'s target. There are many cases that `try` and
`delegate`'s labels need to be treated in the same way as block and
branch labels, such as for hashing or comparing. But there are routines
in which we automatically assume all label uses are branches. I thought
about adding a new kind of defines such as
`DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some
duplication of existing routines or classes. So at the moment this PR
chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for
`try` and `delegate` labels and makes only necessary amount of changes
in branch-utils. We can revisit this decision later if necessary.

Many of changes to the existing test cases are because now all `try`s
are automatically assigned a label. They will be removed in
`RemoveUnusedNames` pass in the same way as block labels if not targeted
by any delegates.

This only supports reading and writing and has not been tested against
any optimization passes yet.
aheejin added a commit to aheejin/binaryen that referenced this issue Feb 10, 2021
This adds support for reading/writing of the new 'delegate' instruction
in the folded wast format, the stack IR format, and the binary format in
Binaryen. We don't have a format spec written down yet, but please refer
to WebAssembly/exception-handling#137 and
WebAssembly/exception-handling#146 for the informal semantics.
In the current version of spec `delegate` is basically a rethrow, but
with branch-like immediate argument so that it can bypass other
catches/delegates in between.

'delegate' is not represented a new `Expression`, but it is rather an
option within a `Try` class, like `catch`/`catch_all`.

One special thing about `delegate` is, even though it is written
_within_ a `try` in the folded wat format, like
```wasm
(try
  (do
    ...
  )
  (delegate $l)
)
```
In the unfolded wat format or in the binary format, `delegate` serves as
a scope end instruction so there is no separate `end`:
```wasm
try
  ...
delegate $l
```

`delegate` semantically targets an outer `catch` or `delegate`, but we
write `delegate` target as a `try` label because we only give labels to
block-like scoping expressions. So far we has not given `Try` a label
and used inner blocks or a wrapping block in case a branch targets the
`try`. But in case of `delegate`, it can syntactically only target `try`
and if it targets blocks or loops it is a validation failure.

So after discussions in WebAssembly#3497, we give `Try` a label but this label can
only be targeted by `delegate`s. Unfortunately this makes parsing and
writing of `Try` expression somewhat complicated. Also there is one
special case; if the immediate argument of `try` is the same as the
depth of control flow stack, this means the 'delegate' delegates to the
caller. To handle this case this adds a fake label
`DELEGATE_CALLER_TARGET`, and when writing it back to the wast format
writes it as an immediate value, unlike other cases in which we write
labels.

This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label
and `delegate`'s target. There are many cases that `try` and
`delegate`'s labels need to be treated in the same way as block and
branch labels, such as for hashing or comparing. But there are routines
in which we automatically assume all label uses are branches. I thought
about adding a new kind of defines such as
`DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some
duplication of existing routines or classes. So at the moment this PR
chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for
`try` and `delegate` labels and makes only necessary amount of changes
in branch-utils. We can revisit this decision later if necessary.

Many of changes to the existing test cases are because now all `try`s
are automatically assigned a label. They will be removed in
`RemoveUnusedNames` pass in the same way as block labels if not targeted
by any delegates.

This only supports reading and writing and has not been tested against
any optimization passes yet.
aheejin added a commit to aheejin/binaryen that referenced this issue Feb 10, 2021
This adds support for reading/writing of the new 'delegate' instruction
in the folded wast format, the stack IR format, the poppy IR format, and
the binary format in Binaryen. We don't have a format spec written down
yet, but please refer to WebAssembly/exception-handling#137 and
WebAssembly/exception-handling#146 for the informal semantics. In the
current version of spec `delegate` is basically a rethrow, but with
branch-like immediate argument so that it can bypass other
catches/delegates in between.

'delegate' is not represented a new `Expression`, but it is rather an
option within a `Try` class, like `catch`/`catch_all`.

One special thing about `delegate` is, even though it is written
_within_ a `try` in the folded wat format, like
```wasm
(try
  (do
    ...
  )
  (delegate $l)
)
```
In the unfolded wat format or in the binary format, `delegate` serves as
a scope end instruction so there is no separate `end`:
```wasm
try
  ...
delegate $l
```

`delegate` semantically targets an outer `catch` or `delegate`, but we
write `delegate` target as a `try` label because we only give labels to
block-like scoping expressions. So far we has not given `Try` a label
and used inner blocks or a wrapping block in case a branch targets the
`try`. But in case of `delegate`, it can syntactically only target `try`
and if it targets blocks or loops it is a validation failure.

So after discussions in WebAssembly#3497, we give `Try` a label but this label can
only be targeted by `delegate`s. Unfortunately this makes parsing and
writing of `Try` expression somewhat complicated. Also there is one
special case; if the immediate argument of `try` is the same as the
depth of control flow stack, this means the 'delegate' delegates to the
caller. To handle this case this adds a fake label
`DELEGATE_CALLER_TARGET`, and when writing it back to the wast format
writes it as an immediate value, unlike other cases in which we write
labels.

This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label
and `delegate`'s target. There are many cases that `try` and
`delegate`'s labels need to be treated in the same way as block and
branch labels, such as for hashing or comparing. But there are routines
in which we automatically assume all label uses are branches. I thought
about adding a new kind of defines such as
`DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some
duplication of existing routines or classes. So at the moment this PR
chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for
`try` and `delegate` labels and makes only necessary amount of changes
in branch-utils. We can revisit this decision later if necessary.

Many of changes to the existing test cases are because now all `try`s
are automatically assigned a label. They will be removed in
`RemoveUnusedNames` pass in the same way as block labels if not targeted
by any delegates.

This only supports reading and writing and has not been tested against
any optimization passes yet.
aheejin added a commit that referenced this issue Feb 12, 2021
This adds support for reading/writing of the new `delegate` instruction
in the folded wast format, the stack IR format, the poppy IR format, and
the binary format in Binaryen. We don't have a formal spec written down
yet, but please refer to WebAssembly/exception-handling#137 and
WebAssembly/exception-handling#146 for the informal semantics. In the
current version of spec `delegate` is basically a rethrow, but with
branch-like immediate argument so that it can bypass other
catches/delegates in between.

`delegate` is not represented as a new `Expression`, but it is rather
an option within a `Try` class, like `catch`/`catch_all`.

One special thing about `delegate` is, even though it is written
_within_ a `try` in the folded wat format, like
```wasm
(try
  (do
    ...
  )
  (delegate $l)
)
```
In the unfolded wat format or in the binary format, `delegate` serves as
a scope end instruction so there is no separate `end`:
```wasm
try
  ...
delegate $l
```

`delegate` semantically targets an outer `catch` or `delegate`, but we
write `delegate` target as a `try` label because we only give labels to
block-like scoping expressions. So far we have not given `Try` a label
and used inner blocks or a wrapping block in case a branch targets the
`try`. But in case of `delegate`, it can syntactically only target `try`
and if it targets blocks or loops it is a validation failure.

So after discussions in #3497, we give `Try` a label but this label can
only be targeted by `delegate`s. Unfortunately this makes parsing and
writing of `Try` expression somewhat complicated. Also there is one
special case; if the immediate argument of `try` is the same as the
depth of control flow stack, this means the 'delegate' delegates to the
caller. To handle this case this adds a fake label
`DELEGATE_CALLER_TARGET`, and when writing it back to the wast format
writes it as an immediate value, unlike other cases in which we write
labels.

This uses `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` to represent `try`'s label
and `delegate`'s target. There are many cases that `try` and
`delegate`'s labels need to be treated in the same way as block and
branch labels, such as for hashing or comparing. But there are routines
in which we automatically assume all label uses are branches. I thought
about adding a new kind of defines such as
`DELEGATE_FIELD_TRY_NAME_DEF/USE`, but I think it will also involve some
duplication of existing routines or classes. So at the moment this PR
chooses to use the existing `DELEGATE_FIELD_SCOPE_NAME_DEF/USE` for
`try` and `delegate` labels and makes only necessary amount of changes
in branch-utils. We can revisit this decision later if necessary.

Many of changes to the existing test cases are because now all `try`s
are automatically assigned a label. They will be removed in
`RemoveUnusedNames` pass in the same way as block labels if not targeted
by any delegates.

This only supports reading and writing and has not been tested against
any optimization passes yet.

---

Original unfolded wat file to generate test/try-delegate.wasm:
```wasm
(module
  (event $e)
  (func
    try
      try
      delegate 0
    catch $e
    end)

  (func
    try
      try
      catch $e
        i32.const 0
        drop
        try
        delegate 1
      end
    catch $e
    end
  )
)
```
@aheejin
Copy link
Member Author

aheejin commented Feb 14, 2021

Thanks for the suggestions! Will close this because #3561 landed.

@aheejin aheejin closed this as completed Mar 8, 2021
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

No branches or pull requests

3 participants