Skip to content

[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

Merged

Conversation

slackito
Copy link
Collaborator

@slackito slackito commented Nov 9, 2024

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.

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.
unsigned NumOps = Switch->getNumOperands();
auto ValUseB = Switch->getOperandUse(NumOps - 2);
auto SucUseB = Switch->getOperandUse(NumOps - 2 + 1);
ValUseA.swap(ValUseB);
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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?

Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@tschuett
Copy link

tschuett commented Nov 9, 2024

Could please add a SandBoxIR label to
https://github.com/llvm/llvm-project/blob/main/.github/new-prs-labeler.yml

The new test also tests a couple more situations, as requested during
review.
Copy link

github-actions bot commented Nov 11, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@slackito
Copy link
Collaborator Author

Could please add a SandBoxIR label to https://github.com/llvm/llvm-project/blob/main/.github/new-prs-labeler.yml

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);
Copy link
Contributor

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.

Copy link
Collaborator Author

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

@vporpo
Copy link
Contributor

vporpo commented Nov 12, 2024

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.

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

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.

Copy link
Collaborator Author

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.

// 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) {
Copy link
Contributor

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. Thanks!

// 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.
Copy link
Contributor

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?

@slackito
Copy link
Collaborator Author

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.

I'm not super happy about it either, but I think this one is more robust against potential (if unlikely) removeCase changes.

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.

@slackito slackito merged commit 9d85ba5 into llvm:main Nov 12, 2024
6 of 7 checks passed
@slackito slackito deleted the sandboxir-preserve-switchinst-operand-order branch November 12, 2024 23:02
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.

3 participants