[polly] [Polly] Materialize escaping scalars from empty-domain statements as poison (PR #220008)
Shikhar Jain via llvm-commits
llvm-commits at lists.llvm.org
Wed Sep 23 02:07:46 PDT 2026
https://github.com/ShikharJ-Corp updated https://github.com/llvm/llvm-project/pull/220008
>From 31e28dff5d11ec9dd1716c638e2ff5e1da218663 Mon Sep 17 00:00:00 2001
From: ShikharJain <shikharj at qti.qualcomm.com>
Date: Mon, 31 Aug 2026 08:28:29 -0700
Subject: [PATCH 1/2] [Polly] Materialize escaping scalars from empty-domain
statements as poison
Problem summary: In a versioned SCoP, Polly emits an optimized copy and the original copy
joined at polly.merge_new_and_old. When an escaping scalar is defined in a
statement whose iteration domain is empty in the optimized range, buildScop
prunes that statement in removeStmtNotInDomainMap. This destroys the scalar's
escaping MemoryKind::Value MUST_WRITE access, so it never receives a Value
ScopArrayInfo. Code generation then leaves the merge PHI referring to the raw
value, which is only defined in the original copy and does not dominate the
merge edge, tripping the verifier.
Fix: When removeStmtNotInDomainMap() is about to prune the empty domain
stmt, then register the ScopArrayInfo of all the MAs/Values in this stmt
such that they are 1) MEMORY_KIND:Value 2) MUST_WRITE 3)Escaping.
Code generation then allocates a .s2a slot and builds a proper .merge PHI
whose optimized edge reloads that slot.
Because the defining statement is never emitted on the optimized path,
the slot is never stored and the reload is poison.
-> This is sound: the poison only reaches a live use on the exact parameter ranges
where the original program was already undefined at that point, so no new
undefined behavior is introduced.
The new .merge PHI provides a dominating definition on the optimized edge,
which resolves the verifier failure while keeping the region optimized.
Fixes #206551
---
polly/lib/Analysis/ScopBuilder.cpp | 10 +++
polly/lib/Analysis/ScopInfo.cpp | 23 ++++++-
.../broken-dominance-escaping-scalar.ll | 62 +++++++++++++++++++
3 files changed, 92 insertions(+), 3 deletions(-)
create mode 100644 polly/test/CodeGen/broken-dominance-escaping-scalar.ll
diff --git a/polly/lib/Analysis/ScopBuilder.cpp b/polly/lib/Analysis/ScopBuilder.cpp
index 762a930dfea6f2..ba3c7a9f0bdfb4 100644
--- a/polly/lib/Analysis/ScopBuilder.cpp
+++ b/polly/lib/Analysis/ScopBuilder.cpp
@@ -1328,6 +1328,16 @@ void ScopBuilder::buildEscapingDependences(Instruction *Inst) {
// Check for uses of this instruction outside the scop. Because we do not
// iterate over such instructions and therefore did not "ensure" the existence
// of a write, we must determine such use here.
+ // ------------------- TODO -------------------------
+ // If domain information for an instruction (via its containing ScopStmt)
+ // is available at this point, then we can perform SAI registration for
+ // escaping scalars that are also doomed (i.e., belong to a ScopStmt with
+ // an invalid domain) and have a must-write access.
+ // However, for this necessary domain information to be available, we need
+ // to reshuffle the ScopBuilder pipeline such that buildDomains() and the
+ // functions that depend only on it are moved before/above
+ // buildAccessFunctions(), since domain calculation and determining the
+ // MAs of an instruction are logically unrelated.
if (scop->isEscaping(Inst))
ensureValueWrite(Inst);
}
diff --git a/polly/lib/Analysis/ScopInfo.cpp b/polly/lib/Analysis/ScopInfo.cpp
index 38943f2557cf21..88d445fda54b06 100644
--- a/polly/lib/Analysis/ScopInfo.cpp
+++ b/polly/lib/Analysis/ScopInfo.cpp
@@ -1686,9 +1686,26 @@ void Scop::removeStmts(function_ref<bool(ScopStmt &)> ShouldDelete,
void Scop::removeStmtNotInDomainMap() {
removeStmts([this](ScopStmt &Stmt) -> bool {
isl::set Domain = DomainMap.lookup(Stmt.getEntryBlock());
- if (Domain.is_null())
- return true;
- return Domain.is_empty();
+ if (!Domain.is_null() && !Domain.is_empty())
+ return false;
+
+ // This ScopStmt is being removed. For all the MAs belonging to this
+ // ScopStmt if it is 1) a scalar (MemoryKind::Value) 2) escaping 3) a
+ // must-write access 4) empty domain ScopStmt, must therefore be preserved
+ // via SAI registration. This allows code generation to create the required
+ // merge PHIs and repair use sites after versioning has pruned the defining
+ // statement from optimized copy (due to its null/empty domain). Without
+ // this, Polly may generate invalid IR with broken dominance.
+ for (MemoryAccess *MA : Stmt) {
+ if (!MA->isMustWrite() || !MA->isOriginalValueKind())
+ continue;
+ auto *Inst = dyn_cast_or_null<Instruction>(MA->getAccessValue());
+ if (!Inst || !contains(Inst) || !isEscaping(Inst))
+ continue;
+ getOrCreateScopArrayInfo(Inst, Inst->getType(), {}, MemoryKind::Value);
+ }
+
+ return true;
});
}
diff --git a/polly/test/CodeGen/broken-dominance-escaping-scalar.ll b/polly/test/CodeGen/broken-dominance-escaping-scalar.ll
new file mode 100644
index 00000000000000..a473197b7ab123
--- /dev/null
+++ b/polly/test/CodeGen/broken-dominance-escaping-scalar.ll
@@ -0,0 +1,62 @@
+; RUN: opt %loadNPMPolly '-passes=polly-custom<codegen>' -S %s | FileCheck %s
+;
+; https://github.com/llvm/llvm-project/issues/206551
+;
+; Regression test for a verifier crash ("Instruction does not dominate all
+; uses!") triggered when an escaping scalar (%cond47.us.us.us) is defined in a
+; statement whose iteration domain is empty. The inner loop trip count depends
+; on a parameter that the runtime check restricts to a range where the loop
+; body never executes, making the defining statement's domain empty. Polly
+; prunes it, but the scalar still escapes via the outer-loop header PHI.
+;
+; Without the fix the generated merge block refers to a definition that only
+; exists in the original (unoptimized) copy, breaking dominance. With the fix
+; the region stays versioned and a proper .merge PHI is built, so we check that
+; the versioning infrastructure (.s2a alloca, .merge PHI, .final_reload) is
+; present and well-formed.
+;
+; CHECK: %cond47.us.us.us.s2a = alloca
+; CHECK: polly.merge_new_and_old:
+; CHECK: %cond47.us.us.us.merge = phi i32 [ %cond47.us.us.us.final_reload, %polly.exiting ], [ %cond47.us.us.us, %for.cond.cleanup6.us.us ]
+; CHECK: %cond47.us.us116.us = phi i32 [ 0, %entry ], [ %cond47.us.us.us.merge, %polly.merge_new_and_old ]
+; CHECK: polly.exiting:
+; CHECK: %cond47.us.us.us.final_reload = load i32, ptr %cond47.us.us.us.s2a
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+define void @_Z1iiPA4_A4_iiPA4_A4_cS4_S1_(ptr %j, i32 %k, ptr %0) {
+entry:
+ %sext = shl i32 %k, 24
+ %conv10 = ashr i32 %sext, 24
+ %sub11 = add i32 %conv10, -127
+ %wide.trip.count = zext i32 %sub11 to i64
+ br label %for.cond4.preheader.us.us
+
+for.cond4.preheader.us.us: ; preds = %for.cond.cleanup6.us.us, %entry
+ %cond47.us.us116.us = phi i32 [ 0, %entry ], [ %cond47.us.us.us, %for.cond.cleanup6.us.us ]
+ br label %for.body7.us.us.us
+
+for.cond.cleanup6.us.us: ; preds = %for.cond9.for.cond.cleanup13_crit_edge.us.us.us
+ br label %for.cond4.preheader.us.us
+
+for.body7.us.us.us: ; preds = %for.cond9.for.cond.cleanup13_crit_edge.us.us.us, %for.cond4.preheader.us.us
+ br label %for.cond15.preheader.us.us.us
+
+cond.false.us.us.us.1: ; preds = %for.cond15.preheader.us.us.us
+ %cond47.us.us.us = select i1 false, i32 0, i32 0
+ store i32 0, ptr null, align 4
+ %indvars.iv.next = add nsw i64 %indvars.iv, 1
+ %exitcond.not = icmp eq i64 %indvars.iv.next, %wide.trip.count
+ br i1 %exitcond.not, label %for.cond9.for.cond.cleanup13_crit_edge.us.us.us, label %for.cond15.preheader.us.us.us
+
+for.cond15.preheader.us.us.us: ; preds = %cond.false.us.us.us.1, %for.body7.us.us.us
+ %indvars.iv = phi i64 [ %indvars.iv.next, %cond.false.us.us.us.1 ], [ 0, %for.body7.us.us.us ]
+ %cond478486.us.us.us = phi i32 [ 1, %cond.false.us.us.us.1 ], [ 0, %for.body7.us.us.us ]
+ %1 = load i32, ptr %0, align 4
+ store i32 0, ptr %j, align 8
+ br label %cond.false.us.us.us.1
+
+for.cond9.for.cond.cleanup13_crit_edge.us.us.us: ; preds = %cond.false.us.us.us.1
+ br i1 true, label %for.cond.cleanup6.us.us, label %for.body7.us.us.us
+}
>From 43a4f2a46507fa4e8c6ad153de99eafde5bcf420 Mon Sep 17 00:00:00 2001
From: ShikharJain <shikharj at qti.qualcomm.com>
Date: Mon, 31 Aug 2026 08:28:29 -0700
Subject: [PATCH 2/2] [Polly] Materialize escaping scalars from empty-domain
statements as poison
Problem summary: In a versioned SCoP, Polly emits an optimized copy and the original copy
joined at polly.merge_new_and_old. When an escaping scalar is defined in a
statement whose iteration domain is empty in the optimized range, buildScop
prunes that statement in removeStmtNotInDomainMap. This destroys the scalar's
escaping MemoryKind::Value MUST_WRITE access, so it never receives a Value
ScopArrayInfo. Code generation then leaves the merge PHI referring to the raw
value, which is only defined in the original copy and does not dominate the
merge edge, tripping the verifier.
Fix: When removeStmtNotInDomainMap() is about to prune the empty domain
stmt, then register the ScopArrayInfo of all the MAs/Values in this stmt
such that they are 1) MEMORY_KIND:Value 2) MUST_WRITE 3)Escaping.
Code generation then allocates a .s2a slot and builds a proper .merge PHI
whose optimized edge reloads that slot.
Because the defining statement is never emitted on the optimized path,
the slot is never stored and the reload is poison.
-> This is sound: the poison only reaches a live use on the exact parameter ranges
where the original program was already undefined at that point, so no new
undefined behavior is introduced.
The new .merge PHI provides a dominating definition on the optimized edge,
which resolves the verifier failure while keeping the region optimized.
Fixes #206551
---
polly/lib/Analysis/ScopInfo.cpp | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/polly/lib/Analysis/ScopInfo.cpp b/polly/lib/Analysis/ScopInfo.cpp
index 88d445fda54b06..75a9ad08b540c0 100644
--- a/polly/lib/Analysis/ScopInfo.cpp
+++ b/polly/lib/Analysis/ScopInfo.cpp
@@ -1696,6 +1696,13 @@ void Scop::removeStmtNotInDomainMap() {
// merge PHIs and repair use sites after versioning has pruned the defining
// statement from optimized copy (due to its null/empty domain). Without
// this, Polly may generate invalid IR with broken dominance.
+ // ----------- TODO ----------
+ // If domain information is available before memory accesses are
+ // determined for a ScopStmt, then we can remove this SAI registration
+ // for escaping scalars whose containing ScopStmt's domain is actually
+ // invalid/null, and instead do this in
+ // ScopBuilder::buildEscapingDependences. A related TODO is also mentioned
+ // there.
for (MemoryAccess *MA : Stmt) {
if (!MA->isMustWrite() || !MA->isOriginalValueKind())
continue;
More information about the llvm-commits
mailing list