-
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
Provide Module& parameter to operateOnScopeNameUsesAndSentTypes #6096
Conversation
void operateOnScopeNameUsesAndSentTypes(Expression* expr, T func) { | ||
void operateOnScopeNameUsesAndSentTypes(Module& wasm, | ||
Expression* expr, | ||
T func) { |
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 root of this sequence of changes appears to be adding Module& wasm
here. That leads to needing the module in the new BranchTypeSeeker
which leads to Block::finalize
needing it, which leads to lots of changes.
However, I do not actually see Module& wasm
used here in operateOnScopeNameUsesAndSentTypes
, so I don't yet understand the motivation. Reading the code in #6083 I don't see wasm
used there. I do see some discussion in https://github.com/WebAssembly/binaryen/pull/6083/files#r1382658351 but I can't understand it 😄 Probably because it speaks about wasm proposals I am not familiar with.
What I am trying to understand is: what would the code look like, that uses Module& wasm
here?
I ask because this PR looks like a very large refactoring of the sort we've tried to avoid in general. For example, RefFunc
could in theory read the type of the function from the Module, but we purposefully do not do that in ReFinalize::visitRefFunc
, and instead rely on it being set initially in a correct manner, and then we just preserve it there (and anyone that changes a Function's type is responsible for updating RefFunc
s). In that case it avoids parallelism risks of modifying one function's type while modifying the contents of another, for example - in general, we try to avoid reading global state like this for Expressions.
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 core problem is that the new resume
instruction from typed continuations and the new try_table
instruction from the redesigned EH proposal act as a branches where the sent types depends on tag types. We currently depend on being able to determine the sent type of branches just by inspecting the branching Expression
, but the new dependencies on tag types means that determining the types of branches requires being able to look up the tags on the module as well.
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 root of this sequence of changes appears to be adding
Module& wasm
here. That leads to needing the module in the newBranchTypeSeeker
which leads toBlock::finalize
needing it, which leads to lots of changes.
Yes, that's right.
Sorry, I realize that this PR and the linked one do a bad job at showing what the Module&
is actually needed for.
From a high-level perspective, the typed continuations proposal allows writing something like this:
(type $ct (cont ...))
(tag $mytag (params ...) (results ...))
...
(func ...
...
(block $myblock (results ...)
(resume $ct (tag $mytag $myblock) (local.get $mycont))
)
...
)
This means that if you suspend
to the tag $mytag
while running the continuation $mycont
, then execution continues after $myblock. The type of the data that is provided to $myblock
depends on the types of the tag used and the type of the continuation (the latter is $ct
here).
Something very similar will happen for the new EH proposal, where exception handlers are just ordinary blocks, and throw
jumps to them, with additional data.
Practically speaking, for operateOnScopeNameUsesAndSentTypes
this means that we need to do something like this:
else if (auto* res = expr->dynCast<Resume>) {
...
Type tag_sig = module.getTags(res->handlerTags[i])->sig;
<type send to label is calculated from `tag_sig`>
...
}
I agree that this is a rather large refactoring. Id be happy to hear if there are ways of decreasing its footprint somehow.
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 for the explanation!
Can we simply duplicate the type into the resume? This is what we do in practice for e.g. RefFunc today: the type of each RefFunc mirrors the module's information about the function's signature. Likewise a Call mirrors the returns of the signature, etc. While in general such duplication is not great, it allows us to avoid reading global state constantly.
If that can't work, maybe elaborate on what i
is in that example, and what type send to label is calculated
means in practice? (if those things are referring to some kind of "dynamic" read from the module, then simple mirroring of a single value might not work, but I hope that's not the case here...) For the new EH proposal, at least, I think this should 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.
Both of the new instructions take a vector of (tag, label) pairs, so they're similar to br_table
except that they can potentially send different types to each of the labels. If we want to store the sent types directly in the new Expression
s, they would have to be ArenaVector<Type>
rather than just a single 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.
Can we simply duplicate the type into the resume?
There is nothing prohibiting this from a technical perspective. Note that in general, each resume
instruction has a list of (tag $sometag $someblock)
clauses, where my example above only used one. This means that we would have to duplicate the Signature
s of all of the tags into the Resume
object.
If that can't work, maybe elaborate on what i is in that example,
I just used the i
to reflect that the Resume
node actually contains a list of tags and a list of blocks, together representing a (tag $sometag $someblock)
entry. I was trying to hand-wave this away, because operateOnScopeNameUsesAndSentTypes
needs some unrelated adapation to avoid a quadratic blowup because of this, but that is independent from the need to have access to the Module
.
But coming back to your actual question: The information we need from the Module
is entirely static. We just need access to the param and results types that were given when defining the tags in the tags section of the module.
More concretely, the types of the values that flow into a handler block (such as $myblock
in my example above) are then a) the param types of the tag used, followed by b) a continuation type. The latter continuation type is determined entirely by the result types of the tag and the type of the resume
d continuation (which can be read off from the instruction itself).
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 we want to store the sent types directly in the new Expressions, they would have to be ArenaVector rather than just a single type.
Right, in my previous comment I mentioned storing the signtatures of each of the tag in the Resume
node for each (tag ...)
clause, but we may equally store the sent types directly. Note that these are often tuples, because the types sent to the handler are the param types of the corresponding tag, followed by a continuation 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 see, thanks, that it's a list of types is the tricky part then.
An ArenaVector<Type>
seems like a reasonable way to proceed here, since I don't think we will have very many such nodes and not very long lists in them? For EH at least for C++ I believe the list will always be of length 1 (the single standard C++ exception tag with a pointer in it).
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 general, the size of the ArenaVector<Type>
itself will be equal to the number of (tag ...)
clauses of the resume
instruction. Each Type
will then be a tuple type with n_i + 1 elements, where n_i is the number of parameters of the tag used by the i-th clause.
I think it's difficult to give a meaningful estimate of how many (tag ...)
clauses a resume
instruction will usually have, and how many params
the involved tags will have. But I would be surprised to see larger numbers of each.
Going back to your original concerns about this refactoring, I don't quite understand what you meant by the following:
In that case it avoids parallelism risks of modifying one function's type while modifying the contents of another, for example - in general, we try to avoid reading global state like this for Expressions.
Were you concerned that we are relying on some kind of global mutable state here? Luckily, this is not the case. Or was your concern more generally about the fact that in, say, Block::finalize(Module& wasm)
we would be relying on information that is maintained outside of the block in question? That is definitely true.
During the refactoring, it seemed like the Module
was usually in reach and just needed to be passed on to additional places (which arguably then sums up to: quite a lot of additional places).
I don't mind discarding this PR and adding the ArenaVector
to the Resume
nodes in #6083 if that is your preferred option.
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.
Sorry if I wasn't clear about the parallelism. Yes, in this case it is safe. I was trying to give an example of why we avoid reading global state in general, one reason for which is that in some situations it is unsafe.
I do prefer to add an ArenaVector. It's less of a change, and it is more consistent with our existing coding style, I think. If we see it adds significant overhead in practice we can always optimize it later.
Closing this in favor of storing the required information about types sent to blocks in |
This adds basic support for the new instructions in the new EH proposal passed at the Oct CG hybrid CG meeting: https://github.com/WebAssembly/meetings/blob/main/main/2023/CG-10.md https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md This mainly adds two instructions: `try_table` and `throw_ref`. This is the bare minimum required to read and write text and binary format, and does not include analyses or optimizations. (It includes some analysis required for validation of existing instructions.) Validation for the new instructions is not yet included. `try_table` faces the same problem with the `resume` instruction in WebAssembly#6083 that without the module-level tag info, we are unable to know the 'sent types' of `try_table`. This solves it with a similar approach taken in WebAssembly#6083: this adds `Module*` parameter to `finalize` methods, which defaults to `nullptr` when not given. The `Module*` parameter is given when called from the binary and text parser, and we cache those tag types in `sentTypes` array within `TryTable` class. In later optimization passes, as long as they don't touch tags, it is fine to call `finalize` without the `Module*`. Refer to WebAssembly#6083 (comment) and WebAssembly#6096 for related discussions when `resume` was added.
This adds basic support for the new instructions in the new EH proposal passed at the Oct CG hybrid CG meeting: https://github.com/WebAssembly/meetings/blob/main/main/2023/CG-10.md https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md This mainly adds two instructions: `try_table` and `throw_ref`. This is the bare minimum required to read and write text and binary format, and does not include analyses or optimizations. (It includes some analysis required for validation of existing instructions.) Validation for the new instructions is not yet included. `try_table` faces the same problem with the `resume` instruction in #6083 that without the module-level tag info, we are unable to know the 'sent types' of `try_table`. This solves it with a similar approach taken in #6083: this adds `Module*` parameter to `finalize` methods, which defaults to `nullptr` when not given. The `Module*` parameter is given when called from the binary and text parser, and we cache those tag types in `sentTypes` array within `TryTable` class. In later optimization passes, as long as they don't touch tags, it is fine to call `finalize` without the `Module*`. Refer to #6083 (comment) and #6096 for related discussions when `resume` was added.
This adds basic support for the new instructions in the new EH proposal passed at the Oct CG hybrid CG meeting: https://github.com/WebAssembly/meetings/blob/main/main/2023/CG-10.md https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md This mainly adds two instructions: `try_table` and `throw_ref`. This is the bare minimum required to read and write text and binary format, and does not include analyses or optimizations. (It includes some analysis required for validation of existing instructions.) Validation for the new instructions is not yet included. `try_table` faces the same problem with the `resume` instruction in WebAssembly#6083 that without the module-level tag info, we are unable to know the 'sent types' of `try_table`. This solves it with a similar approach taken in WebAssembly#6083: this adds `Module*` parameter to `finalize` methods, which defaults to `nullptr` when not given. The `Module*` parameter is given when called from the binary and text parser, and we cache those tag types in `sentTypes` array within `TryTable` class. In later optimization passes, as long as they don't touch tags, it is fine to call `finalize` without the `Module*`. Refer to WebAssembly#6083 (comment) and WebAssembly#6096 for related discussions when `resume` was added.
This PR performs the necessary plumbing to address a problem plaguing #6083:
In
BranchUtils::operateOnScopeNameUsesAndSentTypes
,Resume
instructions need access to the module to obtain type information associated with tags in order to determine the types of values that may be sent to a block. The same will hold for exception handling instructions, once the updated proposal is implemented.The most notable user of
operateOnScopeNameUsesAndSentTypes
isBranchSeeker
, whose use sites potentially need to be updated. However, many existing users ofBranchSeeker
actually just callBranchSeeker::has
. The latter can be implemented in terms ofoperateOnScopeNameUses
, rather thanoperateOnScopeNameUsesAndSentTypes
.Thus, I have split
BranchSeeker
into two classes:BranchTypeSeeker
is like the oldBranchSeeker
, but withouthas
orcount
. It is implemented in terms ofoperateOnScopeNameUsesAndSentTypes
and therefore needs to be constructed with aModule&
.BranchSeeker
provideshas
andcount
as static members. It is now implemented in terms ofoperateOnScopeNameUses
, meaning that the two functions can keep their original signatures.Most changes in this PR then come from the fact that
Block::finalize()
uses (what is now called)BranchTypeSeeker
, and therefore needs to be changed toBlock::finalize(Module* wasm)
.I have also added Block::finalize()`, which currently does nothing. I have deliberately not tried to identify places where it may be okay to call Block::finalize() and avoid the computation of types that flow into the block.