[llvm] [WinEH] Preserve asynchronous SEH regions during IR sinking (PR #224425)
Yassine Missoum via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 17 13:52:59 PDT 2026
https://github.com/yasster created https://github.com/llvm/llvm-project/pull/224425
Add SEHTryRegionInfo for the existing asynchronous table-SEH contract and use it in InstCombine and CodeGenPrepare sinking decisions. Keep unknown or ambiguous regions conservative, with coverage for inward and outward sinking, same-region motion, casts, comparisons, and masks.
This patch is independent of the machine-level region preservation changes.
Assisted-by: Claude Opus 5 (via VS Code)
>From d4b31f51dbeb454b1d1f91bfb5b62a41b509d41a Mon Sep 17 00:00:00 2001
From: Yassine Missoum <mmissoum at microsoft.com>
Date: Wed, 16 Sep 2026 16:45:53 -0700
Subject: [PATCH] [WinEH] Preserve asynchronous SEH regions during IR sinking
Add SEHTryRegionInfo for the existing asynchronous table-SEH contract and use it in InstCombine and CodeGenPrepare sinking decisions. Keep unknown or ambiguous regions conservative, with coverage for inward and outward sinking, same-region motion, casts, comparisons, and masks.
This patch is independent of the machine-level region preservation changes.
Assisted-by: GitHub Copilot (via VS Code)
---
llvm/docs/ExceptionHandling.md | 19 +++
llvm/include/llvm/IR/EHPersonalities.h | 20 +++
llvm/lib/CodeGen/CodeGenPrepare.cpp | 18 +++
llvm/lib/IR/EHPersonalities.cpp | 92 ++++++++++++
.../InstCombine/InstructionCombining.cpp | 7 +
.../InstCombine/seh-region-sinking.ll | 140 ++++++++++++++++++
6 files changed, 296 insertions(+)
create mode 100644 llvm/test/Transforms/InstCombine/seh-region-sinking.ll
diff --git a/llvm/docs/ExceptionHandling.md b/llvm/docs/ExceptionHandling.md
index 4b6cf835bac7f3..4f2774e9ee0d8a 100644
--- a/llvm/docs/ExceptionHandling.md
+++ b/llvm/docs/ExceptionHandling.md
@@ -516,6 +516,25 @@ expressions, and frontends must outline them ahead of time. Local variables of
the parent function can be escaped and accessed using the `llvm.localescape`
and `llvm.localrecover` intrinsics.
+### Preserving asynchronous SEH regions during IR optimization
+
+For table-based SEH with the `eh-asynch` module flag, invokes of
+`llvm.seh.try.begin` and `llvm.seh.try.end` describe protected regions.
+The handler selected for a fault depends on the instruction address in the
+emitted scope table, so sinking instructions between regions can change which
+handler observes a fault.
+
+`SEHTryRegionInfo` associates IR blocks with their protecting EH pad. Its
+region comparison is conservative for unreachable blocks or ambiguous
+nesting. The result is a snapshot: consumers must recompute it after changing
+the CFG or region markers. InstCombine and CodeGenPrepare use it when deciding
+whether to sink instructions between blocks. The query does not enable new
+behavior for other personalities or for modules without `eh-asynch`.
+
+This analysis consumes existing IR. It does not change Clang's marker emission,
+introduce new intrinsics, or change the language rules for undefined behavior,
+volatile accesses, or `nounwind`.
+
### New exception handling instructions
The primary design goal of the new EH instructions is to support funclet
diff --git a/llvm/include/llvm/IR/EHPersonalities.h b/llvm/include/llvm/IR/EHPersonalities.h
index 39f6817b3dc387..c42329feb032ec 100644
--- a/llvm/include/llvm/IR/EHPersonalities.h
+++ b/llvm/include/llvm/IR/EHPersonalities.h
@@ -12,6 +12,7 @@
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/TinyPtrVector.h"
#include "llvm/Support/Compiler.h"
+#include <optional>
namespace llvm {
class BasicBlock;
@@ -107,6 +108,25 @@ inline bool isNoOpWithoutInvoke(EHPersonality Pers) {
LLVM_ABI bool canSimplifyInvokeNoUnwind(const Function *F);
+/// Snapshot of the protecting EH pad for blocks in table-based asynchronous
+/// SEH. Scope-table dispatch depends on where an instruction executes, so
+/// moving it between regions can change the handler that observes a fault.
+/// Recompute this information after changing the CFG or the try markers.
+class LLVM_ABI SEHTryRegionInfo {
+public:
+ explicit SEHTryRegionInfo(const Function &F);
+
+ /// Whether region membership permits the move, not a complete legality check.
+ /// With active region tracking, different blocks must have known, equal
+ /// regions. Without applicable markers, this query imposes no restriction.
+ bool isSameRegion(const BasicBlock *From, const BasicBlock *To) const;
+
+private:
+ // An empty map disables this analysis. Otherwise, an absent key is unvisited,
+ // nullopt is ambiguous, and an engaged null pointer means unwind to caller.
+ DenseMap<const BasicBlock *, std::optional<const BasicBlock *>> RegionOf;
+};
+
typedef TinyPtrVector<BasicBlock *> ColorVector;
/// If an EH funclet personality is in use (see isFuncletEHPersonality),
diff --git a/llvm/lib/CodeGen/CodeGenPrepare.cpp b/llvm/lib/CodeGen/CodeGenPrepare.cpp
index 3e0b8a956ca813..b200340bc3cb63 100644
--- a/llvm/lib/CodeGen/CodeGenPrepare.cpp
+++ b/llvm/lib/CodeGen/CodeGenPrepare.cpp
@@ -55,6 +55,7 @@
#include "llvm/IR/DebugInfo.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/IR/Dominators.h"
+#include "llvm/IR/EHPersonalities.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/GetElementPtrTypeIterator.h"
#include "llvm/IR/GlobalValue.h"
@@ -1431,6 +1432,9 @@ bool CodeGenPrepare::simplifyOffsetableRelocate(GCStatepointInst &I) {
/// Sink the specified cast instruction into its user blocks.
static bool SinkCast(CastInst *CI) {
BasicBlock *DefBB = CI->getParent();
+ // This rewrite changes instructions, not edges, so one snapshot suffices for
+ // its uses. Do not cache it across other CodeGenPrepare rewrites of the CFG.
+ SEHTryRegionInfo SEHRegions(*CI->getFunction());
/// InsertedCasts - Only insert a cast in each block once.
DenseMap<BasicBlock *, CastInst *> InsertedCasts;
@@ -1466,6 +1470,11 @@ static bool SinkCast(CastInst *CI) {
if (UserBB == DefBB)
continue;
+ // Preserve protection at the execution point, including the incoming edge
+ // chosen above for a PHI use, not just at the block containing the user.
+ if (!SEHRegions.isSameRegion(DefBB, UserBB))
+ continue;
+
// If we have already inserted a cast into this block, use it.
CastInst *&InsertedCast = InsertedCasts[UserBB];
@@ -1944,6 +1953,7 @@ static bool sinkCmpExpression(CmpInst *Cmp, const TargetLowering &TLI,
// Only insert a cmp in each block once.
DenseMap<BasicBlock *, CmpInst *> InsertedCmps;
+ SEHTryRegionInfo SEHRegions(*Cmp->getFunction());
bool MadeChange = false;
for (Instruction::user_iterator UI = Cmp->user_begin(), E = Cmp->user_end();
@@ -1966,6 +1976,10 @@ static bool sinkCmpExpression(CmpInst *Cmp, const TargetLowering &TLI,
if (UserBB == DefBB)
continue;
+ // Cloning a comparison into its user's block must not change its region.
+ if (!SEHRegions.isSameRegion(DefBB, UserBB))
+ continue;
+
// If we have already inserted a cmp into this block, use it.
CmpInst *&InsertedCmp = InsertedCmps[UserBB];
@@ -2358,6 +2372,7 @@ static bool sinkAndCmp0Expression(Instruction *AndI, const TargetLowering &TLI,
AndI->getOperand(0)->hasOneUse() && AndI->getOperand(1)->hasOneUse())
return false;
+ SEHTryRegionInfo SEHRegions(*AndI->getFunction());
for (auto *U : AndI->users()) {
Instruction *User = cast<Instruction>(U);
@@ -2365,6 +2380,9 @@ static bool sinkAndCmp0Expression(Instruction *AndI, const TargetLowering &TLI,
if (!isa<ICmpInst>(User))
return false;
+ if (!SEHRegions.isSameRegion(AndI->getParent(), User->getParent()))
+ return false;
+
auto *CmpC = dyn_cast<ConstantInt>(User->getOperand(1));
if (!CmpC || !CmpC->isZero())
return false;
diff --git a/llvm/lib/IR/EHPersonalities.cpp b/llvm/lib/IR/EHPersonalities.cpp
index 12ae4748e1f4ae..a6f2d3a950c6db 100644
--- a/llvm/lib/IR/EHPersonalities.cpp
+++ b/llvm/lib/IR/EHPersonalities.cpp
@@ -12,6 +12,7 @@
#include "llvm/IR/Constants.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
+#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/raw_ostream.h"
@@ -112,6 +113,97 @@ bool llvm::canSimplifyInvokeNoUnwind(const Function *F) {
return !EHa && !isAsynchronousEHPersonality(Personality);
}
+static const InvokeInst *getSEHTryMarker(const BasicBlock *BB) {
+ const auto *Invoke = dyn_cast<InvokeInst>(BB->getTerminator());
+ if (!Invoke)
+ return nullptr;
+ Intrinsic::ID ID = Invoke->getIntrinsicID();
+ return ID == Intrinsic::seh_try_begin || ID == Intrinsic::seh_try_end
+ ? Invoke
+ : nullptr;
+}
+
+SEHTryRegionInfo::SEHTryRegionInfo(const Function &F) {
+ // Do not impose SEH region rules on C++ personalities or infer this mode from
+ // markers alone. Its interpretation requires the asynchronous table-SEH
+ // contract as well as explicit try markers.
+ if (!F.getParent()->getModuleFlag("eh-asynch") || !F.hasPersonalityFn() ||
+ classifyEHPersonality(F.getPersonalityFn()) !=
+ EHPersonality::MSVC_TableSEH)
+ return;
+ if (none_of(F, [](const BasicBlock &BB) {
+ const InvokeInst *Marker = getSEHTryMarker(&BB);
+ return Marker && Marker->getIntrinsicID() == Intrinsic::seh_try_begin;
+ }))
+ return;
+
+ using Region = std::optional<const BasicBlock *>;
+ // The normal edge of try.end restores the region enclosing its matching try.
+ DenseMap<const BasicBlock *, Region> Parents;
+ SmallVector<const BasicBlock *, 32> Worklist;
+ auto Assign = [&](const BasicBlock *BB, Region NewRegion) {
+ auto [Position, Inserted] = RegionOf.try_emplace(BB, NewRegion);
+ if (Inserted) {
+ Worklist.push_back(BB);
+ } else if (Position->second && Position->second != NewRegion) {
+ // Conflicting paths lose precision permanently; never pick one handler
+ // according to traversal order and authorize a cross-region move.
+ Position->second = std::nullopt;
+ Worklist.push_back(BB);
+ }
+ };
+
+ Assign(&F.getEntryBlock(), nullptr);
+ while (!Worklist.empty()) {
+ const BasicBlock *BB = Worklist.pop_back_val();
+ Region Current = RegionOf[BB];
+ Region Normal = Current;
+ if (const InvokeInst *Marker = getSEHTryMarker(BB)) {
+ if (Marker->getIntrinsicID() == Intrinsic::seh_try_begin) {
+ const BasicBlock *Pad = Marker->getUnwindDest();
+ // Only the normal successor enters the protected region. The handler
+ // itself runs under the enclosing region, not under its own protection.
+ Normal = Pad;
+ auto [Parent, Inserted] = Parents.try_emplace(Pad, Current);
+ if (!Inserted && Parent->second != Current) {
+ // Earlier try.end results may depend on this parent assignment.
+ // Invalidate all answers rather than retaining traversal-order facts.
+ for (auto &KnownRegion : RegionOf)
+ KnownRegion.second = std::nullopt;
+ return;
+ }
+ Assign(Pad, Parent->second);
+ } else if (Current && *Current) {
+ auto Parent = Parents.find(*Current);
+ Normal = Parent == Parents.end() ? Region() : Parent->second;
+ }
+ }
+
+ for (const BasicBlock *Successor : successors(BB)) {
+ // Handlers are seeded from try.begin above. Propagating the current
+ // region down unwind edges would make a handler protect itself.
+ if (const auto *Invoke = dyn_cast<InvokeInst>(BB->getTerminator()))
+ if (Successor == Invoke->getUnwindDest())
+ continue;
+ if (const auto *Switch = dyn_cast<CatchSwitchInst>(BB->getTerminator()))
+ if (Successor == Switch->getUnwindDest())
+ continue;
+ Assign(Successor, Normal);
+ }
+ }
+}
+
+bool SEHTryRegionInfo::isSameRegion(const BasicBlock *From,
+ const BasicBlock *To) const {
+ if (RegionOf.empty() || From == To)
+ return true;
+ auto Source = RegionOf.find(From);
+ auto Destination = RegionOf.find(To);
+ return Source != RegionOf.end() && Destination != RegionOf.end() &&
+ Source->second && Destination->second &&
+ Source->second == Destination->second;
+}
+
DenseMap<BasicBlock *, ColorVector> llvm::colorEHFunclets(Function &F) {
SmallVector<std::pair<BasicBlock *, BasicBlock *>, 16> Worklist;
BasicBlock *EntryBlock = &F.getEntryBlock();
diff --git a/llvm/lib/Transforms/InstCombine/InstructionCombining.cpp b/llvm/lib/Transforms/InstCombine/InstructionCombining.cpp
index 4b09510933d0cb..5aca575e2bb967 100644
--- a/llvm/lib/Transforms/InstCombine/InstructionCombining.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstructionCombining.cpp
@@ -5565,6 +5565,13 @@ bool InstCombinerImpl::tryToSinkInstruction(Instruction *I,
if (isa<CatchSwitchInst>(DestBlock->getTerminator()))
return false;
+ // Keep the instruction under the same handler even if ordinary use/dominance
+ // checks allow sinking. InstCombine can edit the CFG and markers between
+ // attempts, so do not reuse a region snapshot from an earlier attempt.
+ if (F.getParent()->getModuleFlag("eh-asynch") &&
+ !SEHTryRegionInfo(F).isSameRegion(SrcBlock, DestBlock))
+ return false;
+
// Do not sink convergent call instructions.
if (auto *CI = dyn_cast<CallInst>(I)) {
if (CI->isConvergent())
diff --git a/llvm/test/Transforms/InstCombine/seh-region-sinking.ll b/llvm/test/Transforms/InstCombine/seh-region-sinking.ll
new file mode 100644
index 00000000000000..c4b10d4feb57bc
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/seh-region-sinking.ll
@@ -0,0 +1,140 @@
+; RUN: opt -passes=instcombine -verify-each -S < %s | FileCheck %s
+; RUN: opt -mtriple=x86_64-pc-windows-msvc -passes='require<profile-summary>,function(codegenprepare)' -verify-each -S < %s | FileCheck %s --check-prefix=CGP
+; RUN: sed '/!llvm.module.flags/d' %s | opt -passes=instcombine -verify-each -S | FileCheck %s --check-prefix=NOASYNC
+
+declare void @llvm.seh.try.begin()
+declare void @llvm.seh.try.end()
+declare i32 @__C_specific_handler(...)
+
+define i32 @no_sink_into_try(ptr %address, i32 %number) personality ptr @__C_specific_handler {
+entry:
+ %multiply = mul i32 %number, 3
+ %add = add i32 %multiply, 7
+ invoke void @llvm.seh.try.begin() to label %body unwind label %dispatch
+body:
+ %value = load volatile i32, ptr %address
+ %sum = add i32 %add, %value
+ invoke void @llvm.seh.try.end() to label %join unwind label %dispatch
+dispatch:
+ %switch = catchswitch within none [label %handler] unwind to caller
+handler:
+ %pad = catchpad within %switch [ptr null]
+ catchret from %pad to label %join
+join:
+ %result = phi i32 [ %sum, %body ], [ -1, %handler ]
+ ret i32 %result
+}
+
+; CHECK-LABEL: define i32 @no_sink_into_try(
+; CHECK: entry:
+; CHECK-NEXT: [[MULTIPLY:%.*]] = mul i32 %number, 3
+; CHECK-NEXT: [[ADD:%.*]] = add i32 [[MULTIPLY]], 7
+; CHECK-NEXT: invoke void @llvm.seh.try.begin()
+
+; NOASYNC-LABEL: define i32 @no_sink_into_try(
+; NOASYNC: entry:
+; NOASYNC-NEXT: invoke void @llvm.seh.try.begin()
+; NOASYNC: body:
+; NOASYNC-NEXT: [[UNGUARDED_MUL:%.*]] = mul i32 %number, 3
+; NOASYNC-NEXT: [[UNGUARDED_ADD:%.*]] = add i32 [[UNGUARDED_MUL]], 7
+
+define i32 @sink_within_try(ptr %address, i32 %number, i1 %condition) personality ptr @__C_specific_handler {
+entry:
+ invoke void @llvm.seh.try.begin() to label %body unwind label %dispatch
+body:
+ %multiply = mul i32 %number, 3
+ br i1 %condition, label %use, label %end
+use:
+ %value = load volatile i32, ptr %address
+ %sum = add i32 %multiply, %value
+ br label %end
+end:
+ %value.end = phi i32 [ %sum, %use ], [ 0, %body ]
+ invoke void @llvm.seh.try.end() to label %join unwind label %dispatch
+dispatch:
+ %switch = catchswitch within none [label %handler] unwind to caller
+handler:
+ %pad = catchpad within %switch [ptr null]
+ catchret from %pad to label %join
+join:
+ %result = phi i32 [ %value.end, %end ], [ -1, %handler ]
+ ret i32 %result
+}
+
+; CHECK-LABEL: define i32 @sink_within_try(
+; CHECK: use:
+; CHECK-NEXT: [[SAME:%.*]] = mul i32 %number, 3
+
+define i32 @no_sink_out_of_try(i32 %number) personality ptr @__C_specific_handler {
+entry:
+ invoke void @llvm.seh.try.begin() to label %body unwind label %dispatch
+body:
+ %multiply = mul i32 %number, 5
+ invoke void @llvm.seh.try.end() to label %join unwind label %dispatch
+dispatch:
+ %switch = catchswitch within none [label %handler] unwind to caller
+handler:
+ %pad = catchpad within %switch [ptr null]
+ catchret from %pad to label %fail
+join:
+ %result = add i32 %multiply, 7
+ ret i32 %result
+fail:
+ ret i32 -1
+}
+
+; CHECK-LABEL: define i32 @no_sink_out_of_try(
+; CHECK: body:
+; CHECK-NEXT: %multiply = mul i32 %number, 5
+; CHECK-NEXT: invoke void @llvm.seh.try.end()
+
+define void @no_sink_cast_or_compare(ptr %address, i64 %wide, i32 %number) personality ptr @__C_specific_handler {
+entry:
+ %truncate = trunc i64 %wide to i32
+ %compare = icmp eq i32 %number, 0
+ invoke void @llvm.seh.try.begin() to label %body unwind label %dispatch
+body:
+ store volatile i32 %truncate, ptr %address
+ %select = select i1 %compare, i32 3, i32 4
+ store volatile i32 %select, ptr %address
+ invoke void @llvm.seh.try.end() to label %exit unwind label %dispatch
+dispatch:
+ %switch = catchswitch within none [label %handler] unwind to caller
+handler:
+ %pad = catchpad within %switch [ptr null]
+ catchret from %pad to label %exit
+exit:
+ ret void
+}
+
+; CGP-LABEL: define void @no_sink_cast_or_compare(
+; CGP: entry:
+; CGP-NEXT: %truncate = trunc i64 %wide to i32
+; CGP-NEXT: %compare = icmp eq i32 %number, 0
+; CGP-NEXT: invoke void @llvm.seh.try.begin()
+
+define void @no_sink_mask(ptr %address, i32 %number) personality ptr @__C_specific_handler {
+entry:
+ %mask = and i32 %number, 7
+ invoke void @llvm.seh.try.begin() to label %body unwind label %dispatch
+body:
+ %compare = icmp eq i32 %mask, 0
+ %select = select i1 %compare, i32 3, i32 4
+ store volatile i32 %select, ptr %address
+ invoke void @llvm.seh.try.end() to label %exit unwind label %dispatch
+dispatch:
+ %switch = catchswitch within none [label %handler] unwind to caller
+handler:
+ %pad = catchpad within %switch [ptr null]
+ catchret from %pad to label %exit
+exit:
+ ret void
+}
+
+; CGP-LABEL: define void @no_sink_mask(
+; CGP: entry:
+; CGP-NEXT: %mask = and i32 %number, 7
+; CGP-NEXT: invoke void @llvm.seh.try.begin()
+
+!llvm.module.flags = !{!0}
+!0 = !{i32 1, !"eh-asynch", i32 1}
\ No newline at end of file
More information about the llvm-commits
mailing list