-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[move-function] Implement move checking for address only lets #40423
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
Conversation
@swift-ci test |
Build failed |
Build failed |
7e24fe2
to
8efa174
Compare
@swift-ci test |
This instruction is similar to a copy_addr except that it marks a move of an address that has to be checked. In order to keep the memory lifetime verifier happy, the semantics before the checker runs are the mark_unresolved_move_addr is equivalent to copy_addr [init] (not copy_addr [take][init]). The use of this instruction is that Mandatory Inlining converts builtin "move" to a mark_unresolved_move_addr when inlining the function "_move" (the only place said builtin is invoked). This is then run through a special checker (that is later in this PR) that either proves that the mark_unresolved_move_addr can actually be a move in which case it converts it to copy_addr [take][init] or if it can not be a move, emit an error and convert the instruction to a copy_addr [init]. After this is done for all instructions, we loop back through again and emit an error on any mark_unresolved_move_addr that were not processed earlier allowing for us to know that we have completeness. NOTE: The move kills checker for addresses is going to run after Mandatory Inlining, but before predictable memory opts and friends.
…._move in a generic context. This turns off the diagnostic that prevented the user from calling _move in such contexts. I left that in for _copy.
Previously, I wasn't supporting address only values at all. We support it now in certain cases. So remove this test.
8efa174
to
6f7f235
Compare
@swift-ci test |
This seems like the best place to put a review of this pass even though it's based on the latest iteration of the code. Notes on the design descriptionThe generally looks like a great approach from the high level description. It's should be as similar to the value-based algorithm as possible. The same diagnostic algorithm should be used for moveonly variables and copyable variables that are explicitly moved. The name indicates otherwise. I expected simply a As discussed offline, the complete form of this diagnostic is equivalent to DefiniteInitialization. DI can insert mark_initialized markers to help you, but that's a pretty big FIXME to call out. Please check that the design handles all the variations on indirect call arguments. I'm a little concerned that SILGen might emit temporary alloc_stack copies for some things.
Why is
It all be simpler to understand if you partition uses into two sets: live uses vs. destroys. Then say that some subset of those live uses are "moves".
This is hard to parse. Isn't it just this?
I don't understand what "see an init without seeing a destroy_addr" means. But It seems unfortunate to have an algorithm for addressable variables that doesn't support reinitialization.
For moveonly variables, consuming operations will need to be handled just like moves. I'm not sure why the algorithm isn't just designed that way from the start. This whole discussion and design is centered on the
When you say "continue" here I think you mean "stop". Notes on the implementationThere are several different data flow algorithms and dozens of data structures, without clear boundaries between them. Each aspect of the pass should be encapsulated into its own internal state distinct from its result that feeds into other aspects:
For each data flow, clearly separate:
Most of the large functions in this pass are trying to do multiple unrelated things at once and have access to all different pieces of state. These are a ton of data structures across this code. They should all be encapsulated into objects that are each the output of one stage and the input of following stages.
This is easy to understand. But Please clarify somehow that UseState is the output of GatherClosureUseVisitor. The only modification to that output is that uses are "cleared" later as they are processed. The UseState and GatherClosureUseVisitor are currently separated by 500 lines of code.
Should look like
Why do you have both SmallBlotSetVector/SmallMapVector and DenseMap for the same thing? You can write nullptr into a vector instead of using Blot/MapVector. The null-ness can be hidden behind the API. I don't know why you need bitsets. But, if you do map instructions to integers, why aren't you just using a single instruction map, a single vector for all uses, and 6 bitsets to represent the sets? Instead of 9 vectors, 9 hashmaps, and dynamically populated bitsets? None of these data structure details should be exposed. You can add an API inside
APIs like this are hard to make sense of. An analysis function takes some input and produces a result.
I could not figure out how the closure analysis fits with the rest of the pass. It seems like exactly the same analysis should run on the main function and on the closure, then two separate pieces of information need to be summarized for the closure: (1) upward liveness (2) downward exposed consumes
Code like this makes the function hard to read. This function doesn't care how the data structure is implemented. It should just ask
It might be simpler and more efficient to have a single BasicBlockSet for all blocks with uses (from GatherUses). And have a single
performSingleBasicBlockAnalysis conceptually has nothing to do with global data flow propagation. DataflowState should not be passed to it.
This upward scan here seems unrelated to the purpose of performSingleBasicBlockAnalysis. All the per-block data flow initialization should be encapsulated in a single place. Generally, try to avoid conflating the move-as-use vs. the move-as-lifetime-end. Just handle move-as-use as part of the regular use set.
Can you centralize all the diagnostics in a single object? Then, instead of passing in
In
I don't understand why cleanupAllDestroyAddr is a member of DataflowState. It takes 8 arguments. Most of them data structures defined locally then passed around. These should all be encapsulated in a separate data flow algorithm for destroy placement. It would be really great if each data flow was a separate class that only contained data flow state and data flow logic and produced a data flow result. Nothing else whatsoever. Otherwise I think it's practically impossible to relate the code to the file-level comment that explains the design. |
This new pass works as follows:
Mandatory Inlining converts builtin "move" to a mark_unresolved_move_addr when inlining the function "_move" (the only place said builtin is invoked). mark_unresolved_move_addr is semantically a copy_addr [init] so we have valid IR.
This is then run through a special checker that
either proves that the mark_unresolved_move_addr can actually be a move in which
case it converts it to copy_addr [take][init] or if it can not be a move, emit
an error and convert the instruction to a copy_addr [init]. After this is done
for all instructions, we loop back through again and emit an error on any
mark_unresolved_move_addr that were not processed earlier allowing for us to
know that we have completeness.
NOTE: The move kills checker for addresses runs after Mandatory
Inlining, but before predictable memory opts and any other memory optimizations on purpose.