-
Notifications
You must be signed in to change notification settings - Fork 14.3k
[SandboxIR] Preserve the order of switch cases after revert. #115577
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
[SandboxIR] Preserve the order of switch cases after revert. #115577
Conversation
Preserving the case order is not strictly necessary to preserve semantics (for example, operations like SwitchInst::removeCase will happily swap cases around). However, I'm planning to introduce an optional verification step for SandboxIR that will use StructuralHash to compare IR after a revert to the original IR to help catch tracker bugs, and the order difference triggers a difference there.
llvm/lib/SandboxIR/Tracker.cpp
Outdated
unsigned NumOps = Switch->getNumOperands(); | ||
auto ValUseB = Switch->getOperandUse(NumOps - 2); | ||
auto SucUseB = Switch->getOperandUse(NumOps - 2 + 1); | ||
ValUseA.swap(ValUseB); |
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 this would require a loop that swaps the last 'case' one position to the left at a time until we reach Index. Because if the switch contains 3 cases (Case0, Case1, Case2) and we remove case 0, then when we revert we are placing the removed one at the end (Case1, Case2, Case0). Then we would need to bring Case0 back to position 0 without changing the order of 1 and 2.
Also I think we could use swapOperands()
instead of swap(Use)
, which may be a bit more compact.
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 I understand the implementation correctly, if you remove Case0 from (Case0, Case1, Case2), it swaps the element to be deleted with the last one, and then shrinks the list. So you actually get (Case2, Case1), which you can undo with a single swap after adding the case back at the end of the list.
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 I hadn't realized that this is how it works, makes sense though. Hmm but should we rely on this behavior ? it seems too implementation specific, so if someone changes the implementation in LLVM IR, our tests will break. I think a comment that explains how this works would be really helpful.
The alternative would be to record the order of all cases when we create the change object. Then when we want to revert we remove them all and add them in again. 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.
I think a comment that explains how this works would be really helpful.
That's what this comment a couple of lines above was supposed to be about:
// removeCase swaps the last case with the deleted one. To revert it, we use
// addCase (which adds the new case to the end), and swap the newly-added
// value and successor operands to the positions for the original case index.
I can try to rephrase/expand it if it wasn't clear enough.
Hmm but should we rely on this behavior ? it seems too implementation specific, so if someone changes the implementation in LLVM IR, our tests will break.
Yeah, I was assuming the implementation wasn't very likely to change and that we would be fine updating this logic if it ever happens. If we don't feel comfortable with that option I can go with the alternative you suggested (recording all cases, then adding them again on revert) instead. Which one do you prefer?
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 can try to rephrase/expand it if it wasn't clear enough.
Sorry, what I wanted to say is that it would be nice to show the example with the ordering of cases by actually showing them too like with (case0, case1, case2)
.
Which one do you prefer?
I am fine with either, but if we stick with this one, we should probably be very specific about the fact that this is implementation dependent and that it could break.
EXPECT_EQ(Switch->findCaseDest(BB1), One); | ||
EXPECT_EQ(Switch->getSuccessor(0), OrigDefaultDest); | ||
EXPECT_EQ(Switch->getSuccessor(1), BB0); | ||
EXPECT_EQ(Switch->getSuccessor(2), BB1); |
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.
Please add a test where we remove case 0 of a 3-case switch, to check that we revert correctly in that case too.
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.
Could please add a SandBoxIR label to |
The new test also tests a couple more situations, as requested during review.
✅ With the latest revision this PR passed the C/C++ code formatter. |
Sure, I'll send you a separate PR. Thanks for pointing it out. |
EXPECT_EQ(Switch->getNumCases(), 0u); | ||
Ctx.revert(); | ||
EXPECT_EQ(Switch->getNumCases(), 3u); | ||
EXPECT_EQ(Switch->findCaseDest(BB0), Zero); |
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.
Perhaps this would be a good place to add a comment that if these checks fail, then it might be that the implementation in LLVM changed and that we should update the way revert() works.
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.
Changed the implementation so it doesn't rely on the specific ordering changes introduced by removeCase()
This solution is still implementation dependent given the lack of an addCase() API call with index. Not sure if I prefer this one over the previous one, given that the previous one was a constant-time operation. |
llvm/lib/SandboxIR/Tracker.cpp
Outdated
auto SucUseB = Switch->getOperandUse(NumOps - 2 + 1); | ||
ValUseA.swap(ValUseB); | ||
SucUseA.swap(SucUseB); | ||
// llvm::SwitchInst doesn't give us any API to insert cases at a specific |
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.
Please also mention that removeCase() provides no guarantees about the order.
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, even if we had APIs to insert cases at specific indices we'd still need to save everything to account for the lack of ordering guarantees in removeCase, so I've changed it to say just that.
llvm/lib/SandboxIR/Tracker.cpp
Outdated
// still relies on the fact that `addCase` will insert them at the end, but it | ||
// is documented to invalidate `case_end()` so it's probably okay. | ||
unsigned NumCases = Switch->getNumCases(); | ||
for (unsigned I = 0; I < NumCases; ++I) { |
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.
nit: drop the curly braces?
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. Thanks!
llvm/lib/SandboxIR/Tracker.cpp
Outdated
// index. In order to preserve the original ordering, we save all of them and, | ||
// when reverting, clear them all then insert them in the desired order. This | ||
// still relies on the fact that `addCase` will insert them at the end, but it | ||
// is documented to invalidate `case_end()` so it's probably okay. |
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.
Hmm so this too is implementation dependent, but I guess less so?
I'm not super happy about it either, but I think this one is more robust against potential (if unlikely) If this turns out to be performance-sensitive we can go back to the other solution, and maybe open an RFC to document the existing behavior so we can rely on it. |
Preserving the case order is not strictly necessary to preserve semantics (for example, operations like SwitchInst::removeCase will happily swap cases around). However, I'm planning to introduce an optional verification step for SandboxIR that will use StructuralHash to compare IR after a revert to the original IR to help catch tracker bugs, and the order difference triggers a difference there.