Skip to content

Conversation

abidh
Copy link
Contributor

@abidh abidh commented Aug 22, 2025

Calling BasicBlock::splice in spliceBB when both Old and New are empty is a nop currently but it can cause a crash once debug records are used instead of debug intrinsics. This PR makes the call conditional on at least one of Old or New being non-empty.

Consider the following mlir:

omp.target map_entries() {
  llvm.intr.dbg.declare ...
  llvm.intr.dbg.declare ...
  omp.teams ...
  ...
}

Current code would translate llvm.intr Ops to llvm intrinsics. Old is the BasicBlock where they were get inserted and it will have 2 llvm debug intrinsics by the time the implementation of omp.teams starts. This implementation creates many BasicBlocks by calling splitBB. The New is the just created BasicBlock which is empty.

In the new scheme (using debug records), there will be no instruction in the Old BB after llvm.intr Ops get translated but just 2 trailing debug records. So both Old and New are empty. When control reaches BasicBlock::splice, it calls spliceDebugInfoEmptyBlock. This function expects that in this case (Src is empty but has trailing debug records), the ToIt is valid and it can call adoptDbgRecords on it. This assumption is not true in this case as New is empty and ToIt is pointing to end(). The fix is to only call BasicBlock::splice when at least of Old or New is not empty.

@abidh abidh requested review from Meinersbur, skatrak and tblah August 22, 2025 16:58
@llvmbot llvmbot added flang:openmp clang:openmp OpenMP related changes to Clang labels Aug 22, 2025
@llvmbot
Copy link
Member

llvmbot commented Aug 22, 2025

@llvm/pr-subscribers-flang-openmp

Author: Abid Qadeer (abidh)

Changes

Calling BasicBlock::splice in spliceBB when both Old and New are empty is a nop currently but it can cause a crash once debug records are used instead of debug intrinsics. This PR makes the call conditional on at least one of Old or New being non-empty.

Consider the following mlir:

omp.target map_entries() {
  llvm.intr.dbg.declare ...
  llvm.intr.dbg.declare ...
  omp.teams ...
  ...
}

Current code would translate llvm.intr Ops to llvm intrinsics. Old is the BasicBlock where they were get inserted and it will have 2 llvm debug intrinsics by the time the implementation of omp.teams starts. This implementation creates many BasicBlocks by calling splitBB. The New is the just created BasicBlock which is empty.

In the new scheme (using debug records), there will be no instruction in the Old BB after llvm.intr Ops get translated but just 2 trailing debug records. So both Old and New are empty. When control reaches BasicBlock::splice, it calls spliceDebugInfoEmptyBlock. This function expects that in this case (Src is empty but has trailing debug records), the ToIt is valid and it can call adoptDbgRecords on it. This assumption is not true in this case as New is empty and ToIt is pointing to end(). The fix is to only call BasicBlock::splice when at least of Old or New is not empty.


Full diff: https://github.com/llvm/llvm-project/pull/154987.diff

1 Files Affected:

  • (modified) llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp (+2-1)
diff --git a/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp b/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp
index 27a4fcfb303c4..2ff6d3a8ddd21 100644
--- a/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp
+++ b/llvm/lib/Frontend/OpenMP/OMPIRBuilder.cpp
@@ -308,7 +308,8 @@ void llvm::spliceBB(IRBuilderBase::InsertPoint IP, BasicBlock *New,
 
   // Move instructions to new block.
   BasicBlock *Old = IP.getBlock();
-  New->splice(New->begin(), Old, IP.getPoint(), Old->end());
+  if (!Old->empty() || !New->empty())
+    New->splice(New->begin(), Old, IP.getPoint(), Old->end());
 
   if (CreateBranch) {
     auto *NewBr = BranchInst::Create(New, Old);

@Meinersbur
Copy link
Member

I don't follow what is happening. Why doesn't BasicBlock::splitBasicBlock (where llvm::spliceBB was inspired by) need this check?

Can you add a unittest for illustration? I don't mean a translation from MLIR, but some IRBuilder-generated LLVM-IR on which llvm::spliceBB is called in OpenMPIRBuilderTest.cpp

@abidh
Copy link
Contributor Author

abidh commented Aug 26, 2025

I don't follow what is happening. Why doesn't BasicBlock::splitBasicBlock (where llvm::spliceBB was inspired by) need this check?

Can you add a unittest for illustration? I don't mean a translation from MLIR, but some IRBuilder-generated LLVM-IR on which llvm::spliceBB is called in OpenMPIRBuilderTest.cpp

The BasicBlock::splitBasicBlock already expects that BasicBlock being split is not empty. Please see the asserts here and here.

I added a unit test that show the scenario where the spliceBB will crash without the change in this PR.

abidh added 2 commits August 27, 2025 11:02
Calling splice when both Old and New are empty is a nop currently but
it can cause a crash once debug records are used instead of debug
intrinsics. This PR makes the call conditional on at least one of
Old or New being non-empty.

Consider the following mlir:
omp.target map_entries() {
  llvm.intr.dbg.declare ...
  llvm.intr.dbg.declare ...
  omp.teams ...
  ...
}

Current code would translate llvm.intr to llvm intrinsics and we will
have 2 of them in the Old BB by the time omp.teams implementation starts.
This implementation creates many BasicBlocks by calling splitBB.

In the new scheme (using debug records), there will be no instruction
in the Old BB after llvm.intr get translated but just 2 trailing debug
records. So effectively both Old and New are empty. When control reaches
BasicBlock::splice, it calls spliceDebugInfoEmptyBlock. This funtion
expects that in this case (Src is empty but has trailing debug records),
the ToIt is valid and it can call adoptDbgRecords on it. This
assumption is not true in this case as New is empty and ToIt is pointing
to end().
@Meinersbur
Copy link
Member

Thank you for the unittest, it helps me understand the issue.

It seems that spliceDebugInfoEmptyBlock wants to move "trailing DbgRecords" to the last instruction of the destination block. Since it is degenerate, such last instruction does not exist.

I think that spliceDebugInfoEmptyBlock is at fault here. If it wants to move "trailing DbgRecords" to an empty Dest, then it should append those DebugRecords also to the trailing DbgRecords of Dest (setTrailingDbgRecords).

@abidh
Copy link
Contributor Author

abidh commented Aug 27, 2025

Thank you for the unittest, it helps me understand the issue.

It seems that spliceDebugInfoEmptyBlock wants to move "trailing DbgRecords" to the last instruction of the destination block. Since it is degenerate, such last instruction does not exist.

I think that spliceDebugInfoEmptyBlock is at fault here. If it wants to move "trailing DbgRecords" to an empty Dest, then it should append those DebugRecords also to the trailing DbgRecords of Dest (setTrailingDbgRecords).

I agree that spliceDebugInfoEmptyBlock probably needs to improve but we also have a llvm::spliceBB specific behavior that we want to preserve. If the unit test code was run with old debug info format, the Old block will look like this before spliceBB is called.

Old:
      call void @llvm.dbg.declare(...)

The call to spliceBB would not move the llvm.dbg.declare into the New BasicBlock.

In the new scheme, there are no intrinsics so the the Old is empty but it has trailing debug records. We want those records to remain in the Old to match the debug information that get generated. So not calling BasicBlock::splice in this case seems the right thing to me.

@Meinersbur
Copy link
Member

The changed behavior comes from the addition of "trailing DbgRecords" with the new scheme. If they are meant to stay at the beginning of the BB they should be called "leading DbgRecords". If the were meant to stay in the source BB then BasicBlock::spliceBlock should never move them. Not moving these the DbgRecords because Old and New both happen to be empty feels like an inconsistency. spliceBB will move them if the New block is not empty, and so does llvm::SplitBlock. Instead of using spliceBB, OpenMPIRBuilder often adds a temporary UnrecheableInst that eventually is removed again1 to cirvcumvent that BasicBlock::splice/BasicBlock::split do not work with degenerate blocks. This would move the trailing DbgRecords as well.

Footnotes

  1. This is why I created spliceBB/splitBB in the first place, which unfortuntately seems to not have caught on

@abidh
Copy link
Contributor Author

abidh commented Aug 28, 2025

I agree with your point that moving dbg records from Old to New only when New is not empty is inconsistent. Would it work if we only checked that Old is empty. That will remove the inconsistency.

I am a bit hesitant about adding UnreachableInst as change may be required in multiple places and lifetime may be long. So I was a bit concerned with any side effects.

This removes the inconsistent behavior in the PR. Also added comments to describe the rationale.
Copy link
Member

@Meinersbur Meinersbur left a comment

Choose a reason for hiding this comment

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

LGTM

// If the Old block is empty then there are no instructions to move. But in
// the new debug scheme, it could have trailing debug records which will be
// moved to New in spliceDebugInfoEmptyBlock. We dont want that for 2 reasons:
// 1. If New is also empty, it could cause a crash.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 1. If New is also empty, it could cause a crash.
// 1. If New is also empty, `BasicBlock::splice` crashes

[nit] to be specific. If it doesn't crash anymore, we know it has been fixed.

Comment on lines 314 to 315
// 2. Even if New is not empty, we want to keep those debug records in the
// Old as that was the behavior with the old scheme (debug intrinsics).
Copy link
Member

@Meinersbur Meinersbur Aug 29, 2025

Choose a reason for hiding this comment

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

Suggested change
// 2. Even if New is not empty, we want to keep those debug records in the
// Old as that was the behavior with the old scheme (debug intrinsics).
// 2. Even if New is not empty, we want to keep those debug records in the
// Old as that was the behavior with the old scheme (debug intrinsics).

[nit] Just being the legacy scheme doesn't mean it is more correct. Consider something that mentions the rationale from BasicBlock::spliceDebugInfoEmptyBlock:

  // If the source block is completely empty, including no terminator, then
  // transfer any trailing DbgRecords that are still hanging around. This can
  // occur when a block is optimised away and the terminator has been moved
  // somewhere else.

which does not apply here since we still want to use Old BB (like adding a BranchInst with CreateBranch). And/or that those trailing DbgRecords should only exist if Old is empty since there is no other place to attach them.

@abidh abidh merged commit 4159fd8 into llvm:main Aug 29, 2025
9 checks passed
@abidh abidh deleted the empty_check branch September 2, 2025 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:openmp OpenMP related changes to Clang flang:openmp
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants