[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