[llvm] [SimplifyCFG] Drop UB-implying metadata when hoisting past side effects (PR #226746)
Mian Miftah via llvm-commits
llvm-commits at lists.llvm.org
Sun Sep 27 09:46:47 PDT 2026
https://github.com/mmiftahx updated https://github.com/llvm/llvm-project/pull/226746
>From be3ce3ad51fbffe60a9730eb098ab081943841bb Mon Sep 17 00:00:00 2001
From: mmiftahx <mmiftah.duna at gmail.com>
Date: Sat, 26 Sep 2026 21:41:58 -0500
Subject: [PATCH 1/3] [SimplifyCFG] Precommit tests for hoisting past side
effects (NFC)
Add tests for hoisting an inttoptr with !dereferenceable and calls with
noundef past skipped instructions.
---
.../SimplifyCFG/hoist-common-skip.ll | 136 +++++++++++++++++-
1 file changed, 135 insertions(+), 1 deletion(-)
diff --git a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
index 3e6f31176315b..bbd543fb2b2fb 100644
--- a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
+++ b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
@@ -1,5 +1,5 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
-; RUN: opt -S --passes='simplifycfg<hoist-common-insts>' %s | FileCheck %s
+; RUN: opt -S --passes='simplifycfg<hoist-common-insts>' %s | FileCheck %s --implicit-check-not='!dereferenceable'
;; Check that the two loads are hoisted to the common predecessor, skipping
;; over the add/sub instructions.
@@ -1150,3 +1150,137 @@ j:
%p = phi ptr [ %call, %t ], [ %call2, %e ]
ret ptr %p
}
+
+declare void @map(i64) nounwind willreturn
+
+; The skipped calls may allocate the memory at %i, so !dereferenceable and
+; !dereferenceable_or_null may not hold before them.
+define ptr @hoist_deref_past_call(i1 %c, i64 %i) {
+; CHECK-LABEL: @hoist_deref_past_call(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr, !dereferenceable [[META0:![0-9]+]], !dereferenceable_or_null [[META0]]
+; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK: if:
+; CHECK-NEXT: call void @map(i64 [[I]])
+; CHECK-NEXT: br label [[END:%.*]]
+; CHECK: else:
+; CHECK-NEXT: call void @map(i64 0)
+; CHECK-NEXT: br label [[END]]
+; CHECK: end:
+; CHECK-NEXT: ret ptr [[P]]
+;
+entry:
+ br i1 %c, label %if, label %else
+if:
+ call void @map(i64 %i)
+ %p = inttoptr i64 %i to ptr, !dereferenceable !0, !dereferenceable_or_null !0
+ br label %end
+else:
+ call void @map(i64 0)
+ %q = inttoptr i64 %i to ptr, !dereferenceable !0, !dereferenceable_or_null !0
+ br label %end
+end:
+ %r = phi ptr [ %p, %if ], [ %q, %else ]
+ ret ptr %r
+}
+
+; Skipped loads do not affect !dereferenceable.
+define ptr @hoist_deref_past_load(i1 %c, i64 %i, ptr %x, ptr %y, ptr %out) {
+; CHECK-LABEL: @hoist_deref_past_load(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr, !dereferenceable [[META0]]
+; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK: if:
+; CHECK-NEXT: [[A:%.*]] = load i32, ptr [[X:%.*]], align 4
+; CHECK-NEXT: br label [[END:%.*]]
+; CHECK: else:
+; CHECK-NEXT: [[B:%.*]] = load i32, ptr [[Y:%.*]], align 4
+; CHECK-NEXT: br label [[END]]
+; CHECK: end:
+; CHECK-NEXT: [[V:%.*]] = phi i32 [ [[A]], [[IF]] ], [ [[B]], [[ELSE]] ]
+; CHECK-NEXT: [[R:%.*]] = phi ptr [ [[P]], [[IF]] ], [ [[P]], [[ELSE]] ]
+; CHECK-NEXT: store i32 [[V]], ptr [[OUT:%.*]], align 4
+; CHECK-NEXT: ret ptr [[R]]
+;
+entry:
+ br i1 %c, label %if, label %else
+if:
+ %a = load i32, ptr %x
+ %p = inttoptr i64 %i to ptr, !dereferenceable !0
+ br label %end
+else:
+ %b = load i32, ptr %y
+ %q = inttoptr i64 %i to ptr, !dereferenceable !0
+ br label %end
+end:
+ %v = phi i32 [ %a, %if ], [ %b, %else ]
+ %r = phi ptr [ %p, %if ], [ %q, %else ]
+ store i32 %v, ptr %out
+ ret ptr %r
+}
+
+declare i32 @pure(i32) memory(none) nounwind willreturn
+declare i32 @pure_speculatable(i32) memory(none) nounwind willreturn speculatable
+
+; The skipped calls may write memory, but noundef does not depend on it.
+define i32 @hoist_noundef_past_call(i1 %c, i64 %i, i32 %x) {
+; CHECK-LABEL: @hoist_noundef_past_call(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[A:%.*]] = call noundef i32 @pure(i32 noundef [[X:%.*]])
+; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK: if:
+; CHECK-NEXT: call void @map(i64 [[I:%.*]])
+; CHECK-NEXT: br label [[END:%.*]]
+; CHECK: else:
+; CHECK-NEXT: call void @map(i64 0)
+; CHECK-NEXT: br label [[END]]
+; CHECK: end:
+; CHECK-NEXT: ret i32 [[A]]
+;
+entry:
+ br i1 %c, label %if, label %else
+if:
+ call void @map(i64 %i)
+ %a = call noundef i32 @pure(i32 noundef %x)
+ br label %end
+else:
+ call void @map(i64 0)
+ %b = call noundef i32 @pure(i32 noundef %x)
+ br label %end
+end:
+ %r = phi i32 [ %a, %if ], [ %b, %else ]
+ ret i32 %r
+}
+
+; The skipped calls may throw, so the hoisted call is speculated and noundef
+; may not hold.
+define i32 @hoist_noundef_past_throwing_call(i1 %c, i32 %x) {
+; CHECK-LABEL: @hoist_noundef_past_throwing_call(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[A:%.*]] = call noundef i32 @pure_speculatable(i32 noundef [[X:%.*]])
+; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK: if:
+; CHECK-NEXT: call void @side_effects0()
+; CHECK-NEXT: br label [[END:%.*]]
+; CHECK: else:
+; CHECK-NEXT: call void @side_effects1()
+; CHECK-NEXT: br label [[END]]
+; CHECK: end:
+; CHECK-NEXT: ret i32 [[A]]
+;
+entry:
+ br i1 %c, label %if, label %else
+if:
+ call void @side_effects0()
+ %a = call noundef i32 @pure_speculatable(i32 noundef %x)
+ br label %end
+else:
+ call void @side_effects1()
+ %b = call noundef i32 @pure_speculatable(i32 noundef %x)
+ br label %end
+end:
+ %r = phi i32 [ %a, %if ], [ %b, %else ]
+ ret i32 %r
+}
+
+!0 = !{i64 4}
>From f89e7a531492db36b8ece4cf17877d817b62643c Mon Sep 17 00:00:00 2001
From: mmiftahx <mmiftah.duna at gmail.com>
Date: Sat, 26 Sep 2026 21:43:21 -0500
Subject: [PATCH 2/3] [SimplifyCFG] Drop UB-implying metadata when hoisting
past side effects
hoistCommonCodeFromSuccessors() can hoist identical instructions past
non-identical ones that it skips, but the hoisted instruction keeps its
attributes and metadata.
If a skipped instruction may write memory, metadata that only holds at
the hoisted instruction's old position may not hold before the write.
For example, an inttoptr with !dereferenceable is hoisted above the call
that maps the memory. Drop !dereferenceable, !dereferenceable_or_null
and !nofreeobj in that case. Nothing is speculated then, so noundef is
kept.
If a skipped instruction may throw or not return, the hoisted
instruction is speculated, so drop UB-implying attributes and
metadata, as SimplifyCFG already does when it speculates.
---
llvm/lib/Transforms/Utils/SimplifyCFG.cpp | 14 ++++++++++++++
.../Transforms/SimplifyCFG/hoist-common-skip.ll | 6 +++---
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
index 7134ab2a7f40c..dd0fca91f2863 100644
--- a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
+++ b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
@@ -2053,6 +2053,20 @@ bool SimplifyCFGOpt::hoistCommonCodeFromSuccessors(Instruction *TI,
I1->applyMergedLocation(I1->getDebugLoc(), I2->getDebugLoc());
I2->eraseFromParent();
}
+ // I1 now executes before the instructions we skipped.
+ unsigned SkippedFlags = 0;
+ for (const SuccIterPair &P : SuccIterPairs)
+ SkippedFlags |= P.second;
+ if (SkippedFlags & SkipImplicitControlFlow) {
+ // One of them may throw or not return, so I1 is speculated.
+ I1->dropUBImplyingAttrsAndMetadata();
+ } else if (SkippedFlags & SkipSideEffect) {
+ // One of them may write memory, for example allocate or free it, so
+ // metadata that only holds at I1's old position may not hold here.
+ I1->setMetadata(LLVMContext::MD_dereferenceable, nullptr);
+ I1->setMetadata(LLVMContext::MD_dereferenceable_or_null, nullptr);
+ I1->setMetadata(LLVMContext::MD_nofreeobj, nullptr);
+ }
if (!Changed)
NumHoistCommonCode += SuccIterPairs.size();
Changed = true;
diff --git a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
index bbd543fb2b2fb..cfb67e5a2c73a 100644
--- a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
+++ b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
@@ -1158,7 +1158,7 @@ declare void @map(i64) nounwind willreturn
define ptr @hoist_deref_past_call(i1 %c, i64 %i) {
; CHECK-LABEL: @hoist_deref_past_call(
; CHECK-NEXT: entry:
-; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr, !dereferenceable [[META0:![0-9]+]], !dereferenceable_or_null [[META0]]
+; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr
; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
; CHECK: if:
; CHECK-NEXT: call void @map(i64 [[I]])
@@ -1188,7 +1188,7 @@ end:
define ptr @hoist_deref_past_load(i1 %c, i64 %i, ptr %x, ptr %y, ptr %out) {
; CHECK-LABEL: @hoist_deref_past_load(
; CHECK-NEXT: entry:
-; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr, !dereferenceable [[META0]]
+; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr, !dereferenceable [[META0:![0-9]+]]
; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
; CHECK: if:
; CHECK-NEXT: [[A:%.*]] = load i32, ptr [[X:%.*]], align 4
@@ -1257,7 +1257,7 @@ end:
define i32 @hoist_noundef_past_throwing_call(i1 %c, i32 %x) {
; CHECK-LABEL: @hoist_noundef_past_throwing_call(
; CHECK-NEXT: entry:
-; CHECK-NEXT: [[A:%.*]] = call noundef i32 @pure_speculatable(i32 noundef [[X:%.*]])
+; CHECK-NEXT: [[A:%.*]] = call i32 @pure_speculatable(i32 [[X:%.*]])
; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
; CHECK: if:
; CHECK-NEXT: call void @side_effects0()
>From aa76ef660b046247c0b771f7c7b344576071952f Mon Sep 17 00:00:00 2001
From: mmiftahx <mmiftah.duna at gmail.com>
Date: Sun, 27 Sep 2026 11:46:24 -0500
Subject: [PATCH 3/3] Leave !dereferenceable and !nofreeobj alone
---
llvm/lib/Transforms/Utils/SimplifyCFG.cpp | 6 --
.../SimplifyCFG/hoist-common-skip.ll | 71 +------------------
2 files changed, 1 insertion(+), 76 deletions(-)
diff --git a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
index dd0fca91f2863..ded2586d5aa1d 100644
--- a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
+++ b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
@@ -2060,12 +2060,6 @@ bool SimplifyCFGOpt::hoistCommonCodeFromSuccessors(Instruction *TI,
if (SkippedFlags & SkipImplicitControlFlow) {
// One of them may throw or not return, so I1 is speculated.
I1->dropUBImplyingAttrsAndMetadata();
- } else if (SkippedFlags & SkipSideEffect) {
- // One of them may write memory, for example allocate or free it, so
- // metadata that only holds at I1's old position may not hold here.
- I1->setMetadata(LLVMContext::MD_dereferenceable, nullptr);
- I1->setMetadata(LLVMContext::MD_dereferenceable_or_null, nullptr);
- I1->setMetadata(LLVMContext::MD_nofreeobj, nullptr);
}
if (!Changed)
NumHoistCommonCode += SuccIterPairs.size();
diff --git a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
index cfb67e5a2c73a..31a59ab83285e 100644
--- a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
+++ b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
@@ -1,5 +1,5 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
-; RUN: opt -S --passes='simplifycfg<hoist-common-insts>' %s | FileCheck %s --implicit-check-not='!dereferenceable'
+; RUN: opt -S --passes='simplifycfg<hoist-common-insts>' %s | FileCheck %s
;; Check that the two loads are hoisted to the common predecessor, skipping
;; over the add/sub instructions.
@@ -1152,73 +1152,6 @@ j:
}
declare void @map(i64) nounwind willreturn
-
-; The skipped calls may allocate the memory at %i, so !dereferenceable and
-; !dereferenceable_or_null may not hold before them.
-define ptr @hoist_deref_past_call(i1 %c, i64 %i) {
-; CHECK-LABEL: @hoist_deref_past_call(
-; CHECK-NEXT: entry:
-; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr
-; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
-; CHECK: if:
-; CHECK-NEXT: call void @map(i64 [[I]])
-; CHECK-NEXT: br label [[END:%.*]]
-; CHECK: else:
-; CHECK-NEXT: call void @map(i64 0)
-; CHECK-NEXT: br label [[END]]
-; CHECK: end:
-; CHECK-NEXT: ret ptr [[P]]
-;
-entry:
- br i1 %c, label %if, label %else
-if:
- call void @map(i64 %i)
- %p = inttoptr i64 %i to ptr, !dereferenceable !0, !dereferenceable_or_null !0
- br label %end
-else:
- call void @map(i64 0)
- %q = inttoptr i64 %i to ptr, !dereferenceable !0, !dereferenceable_or_null !0
- br label %end
-end:
- %r = phi ptr [ %p, %if ], [ %q, %else ]
- ret ptr %r
-}
-
-; Skipped loads do not affect !dereferenceable.
-define ptr @hoist_deref_past_load(i1 %c, i64 %i, ptr %x, ptr %y, ptr %out) {
-; CHECK-LABEL: @hoist_deref_past_load(
-; CHECK-NEXT: entry:
-; CHECK-NEXT: [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr, !dereferenceable [[META0:![0-9]+]]
-; CHECK-NEXT: br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
-; CHECK: if:
-; CHECK-NEXT: [[A:%.*]] = load i32, ptr [[X:%.*]], align 4
-; CHECK-NEXT: br label [[END:%.*]]
-; CHECK: else:
-; CHECK-NEXT: [[B:%.*]] = load i32, ptr [[Y:%.*]], align 4
-; CHECK-NEXT: br label [[END]]
-; CHECK: end:
-; CHECK-NEXT: [[V:%.*]] = phi i32 [ [[A]], [[IF]] ], [ [[B]], [[ELSE]] ]
-; CHECK-NEXT: [[R:%.*]] = phi ptr [ [[P]], [[IF]] ], [ [[P]], [[ELSE]] ]
-; CHECK-NEXT: store i32 [[V]], ptr [[OUT:%.*]], align 4
-; CHECK-NEXT: ret ptr [[R]]
-;
-entry:
- br i1 %c, label %if, label %else
-if:
- %a = load i32, ptr %x
- %p = inttoptr i64 %i to ptr, !dereferenceable !0
- br label %end
-else:
- %b = load i32, ptr %y
- %q = inttoptr i64 %i to ptr, !dereferenceable !0
- br label %end
-end:
- %v = phi i32 [ %a, %if ], [ %b, %else ]
- %r = phi ptr [ %p, %if ], [ %q, %else ]
- store i32 %v, ptr %out
- ret ptr %r
-}
-
declare i32 @pure(i32) memory(none) nounwind willreturn
declare i32 @pure_speculatable(i32) memory(none) nounwind willreturn speculatable
@@ -1282,5 +1215,3 @@ end:
%r = phi i32 [ %a, %if ], [ %b, %else ]
ret i32 %r
}
-
-!0 = !{i64 4}
More information about the llvm-commits
mailing list