Skip to content

[SemaHLSL] Verify assignment of local resources#176793

Closed
inbelic wants to merge 15 commits into
llvm:mainfrom
inbelic:inbelic/verify-local-resources
Closed

[SemaHLSL] Verify assignment of local resources#176793
inbelic wants to merge 15 commits into
llvm:mainfrom
inbelic:inbelic/verify-local-resources

Conversation

@inbelic

@inbelic inbelic commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

This change adds the equivalent verification from DXC to ensure that a local resource is only assigned to a unique global resource.

It corresponds to the 'local resource not guaranteed to map to unique global resource' error demonstrated in cases B/C here.

This is orthogonal, but a pre-requisite, to resolving #165288. As it limits the kinds of ptr/handle usages are required to be handled during dxil-resource-access.

@inbelic inbelic marked this pull request as ready for review January 19, 2026 18:28
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" HLSL HLSL Language Support labels Jan 19, 2026
@llvmbot

llvmbot commented Jan 19, 2026

Copy link
Copy Markdown
Member

@llvm/pr-subscribers-hlsl

@llvm/pr-subscribers-clang

Author: Finn Plummer (inbelic)

Changes

This change adds the equivalent verification from DXC to ensure that a local resource is only assigned to a unique global resource.

It corresponds to the 'local resource not guaranteed to map to unique global resource' error demonstrated here.

This is orthogonal, but a pre-requisite, to resolving #165288. As it limits the kinds of ptr/handle usages are required to be handled during dxil-resource-access.


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

5 Files Affected:

  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+3)
  • (modified) clang/include/clang/Sema/SemaHLSL.h (+15)
  • (modified) clang/lib/Sema/SemaHLSL.cpp (+70-6)
  • (added) clang/test/SemaHLSL/local_resource_binding.hlsl (+56)
  • (added) clang/test/SemaHLSL/local_resource_binding_errs.hlsl (+41)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index eb7a608f798b8..87673e944fc0d 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -13431,6 +13431,9 @@ def err_hlsl_incomplete_resource_array_in_function_param: Error<
   "incomplete resource array in a function parameter">;
 def err_hlsl_assign_to_global_resource: Error<
   "assignment to global resource variable %0 is not allowed">;
+def err_hlsl_assigning_local_resource_is_not_unique
+    : Error<"assignment to local resource %0 is not to same unique global "
+            "resource">;
 
 def err_hlsl_push_constant_unique
     : Error<"cannot have more than one push constant block">;
diff --git a/clang/include/clang/Sema/SemaHLSL.h b/clang/include/clang/Sema/SemaHLSL.h
index 99d8ed137b0c2..48daebbf61c90 100644
--- a/clang/include/clang/Sema/SemaHLSL.h
+++ b/clang/include/clang/Sema/SemaHLSL.h
@@ -132,6 +132,14 @@ class SemaHLSL : public SemaBase {
   bool ActOnUninitializedVarDecl(VarDecl *D);
   void ActOnEndOfTranslationUnit(TranslationUnitDecl *TU);
   void CheckEntryPoint(FunctionDecl *FD);
+
+  // Return an info if the expr has common global binding info; returns
+  // std::nullopt if the expr refers to non-unique global bindings, returns
+  // nullptr if it isn't refer to any global binding, otherwise it returns
+  // a reference to the global binding info.
+  std::optional<const DeclBindingInfo *> GetGlobalBinding(Expr *E);
+
+  // Return true if everything is ok; returns false if there was an error.
   bool CheckResourceBinOp(BinaryOperatorKind Opc, Expr *LHSExpr, Expr *RHSExpr,
                           SourceLocation Loc);
 
@@ -230,6 +238,13 @@ class SemaHLSL : public SemaBase {
   // List of all resource bindings
   ResourceBindings Bindings;
 
+  // Map of local resource variables to their used global resource.
+  //
+  // The Binding can be a nullptr, in which case, the variable has not be
+  // initialized or assigned to yet.
+  llvm::DenseMap<const VarDecl *, const DeclBindingInfo *>
+      LocalResourceBindings;
+
   // Global declaration collected for the $Globals default constant
   // buffer which will be created at the end of the translation unit.
   llvm::SmallVector<Decl *> DefaultCBufferDecls;
diff --git a/clang/lib/Sema/SemaHLSL.cpp b/clang/lib/Sema/SemaHLSL.cpp
index f15b274a65a53..cd00167ceb72f 100644
--- a/clang/lib/Sema/SemaHLSL.cpp
+++ b/clang/lib/Sema/SemaHLSL.cpp
@@ -4398,7 +4398,35 @@ bool SemaHLSL::ActOnUninitializedVarDecl(VarDecl *VD) {
   return false;
 }
 
-// Return true if everything is ok; returns false if there was an error.
+std::optional<const DeclBindingInfo *> SemaHLSL::GetGlobalBinding(Expr *E) {
+  if (auto *Ternary = dyn_cast<ConditionalOperator>(E)) {
+    auto TrueInfo = GetGlobalBinding(Ternary->getTrueExpr());
+    auto FalseInfo = GetGlobalBinding(Ternary->getFalseExpr());
+    if (!TrueInfo || !FalseInfo)
+      return std::nullopt;
+    if (*TrueInfo != *FalseInfo)
+      return std::nullopt;
+    return TrueInfo;
+  }
+
+  if (auto *ASE = dyn_cast<ArraySubscriptExpr>(E))
+    E = ASE->getBase()->IgnoreParenImpCasts();
+
+  if (DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(E->IgnoreParens()))
+    if (VarDecl *VD = dyn_cast<VarDecl>(DRE->getDecl())) {
+      const Type *Ty = VD->getType()->getUnqualifiedDesugaredType();
+      if (Ty->isArrayType())
+        Ty = Ty->getArrayElementTypeNoTypeQual();
+      if (const auto *AttrResType =
+              HLSLAttributedResourceType::findHandleTypeOnResource(Ty)) {
+        ResourceClass RC = AttrResType->getAttrs().ResourceClass;
+        return Bindings.getDeclBindingInfo(VD, RC);
+      }
+    }
+
+  return nullptr;
+}
+
 bool SemaHLSL::CheckResourceBinOp(BinaryOperatorKind Opc, Expr *LHSExpr,
                                   Expr *RHSExpr, SourceLocation Loc) {
   assert((LHSExpr->getType()->isHLSLResourceRecord() ||
@@ -4412,14 +4440,37 @@ bool SemaHLSL::CheckResourceBinOp(BinaryOperatorKind Opc, Expr *LHSExpr,
   while (auto *ASE = dyn_cast<ArraySubscriptExpr>(E))
     E = ASE->getBase()->IgnoreParenImpCasts();
 
+  auto RHSBinding = GetGlobalBinding(RHSExpr);
+  if (!RHSBinding) {
+    SemaRef.Diag(Loc, diag::err_hlsl_assigning_local_resource_is_not_unique)
+        << RHSExpr;
+    return false;
+  }
+
   // Report error if LHS is a non-static resource declared at a global scope.
   if (DeclRefExpr *DRE = dyn_cast<DeclRefExpr>(E->IgnoreParens())) {
     if (VarDecl *VD = dyn_cast<VarDecl>(DRE->getDecl())) {
-      if (VD->hasGlobalStorage() && VD->getStorageClass() != SC_Static) {
-        // assignment to global resource is not allowed
-        SemaRef.Diag(Loc, diag::err_hlsl_assign_to_global_resource) << VD;
-        SemaRef.Diag(VD->getLocation(), diag::note_var_declared_here) << VD;
-        return false;
+      if (VD->getStorageClass() != SC_Static) {
+        if (VD->hasGlobalStorage()) {
+          // assignment to global resource is not allowed
+          SemaRef.Diag(Loc, diag::err_hlsl_assign_to_global_resource) << VD;
+          SemaRef.Diag(VD->getLocation(), diag::note_var_declared_here) << VD;
+          return false;
+        }
+
+        // Ensure assignment to a non-static local resource does not conflict
+        // with previous assignments to global resources
+        const DeclBindingInfo *LHSBinding = LocalResourceBindings[VD];
+        if (!LHSBinding) {
+          // occurs when local resource was instantiated without an expression
+          LocalResourceBindings[VD] = *RHSBinding;
+        } else if (LHSBinding != *RHSBinding) {
+          SemaRef.Diag(Loc,
+                       diag::err_hlsl_assigning_local_resource_is_not_unique)
+              << RHSExpr;
+          SemaRef.Diag(VD->getLocation(), diag::note_var_declared_here) << VD;
+          return false;
+        }
       }
     }
   }
@@ -4797,6 +4848,19 @@ bool SemaHLSL::transformInitList(const InitializedEntity &Entity,
 }
 
 bool SemaHLSL::handleInitialization(VarDecl *VDecl, Expr *&Init) {
+  // If initializing a local resource, register the binding it is using
+  if (VDecl->getType()->isHLSLResourceRecord() && !VDecl->hasGlobalStorage())
+    if (auto *InitExpr = Init) {
+      if (auto Binding = GetGlobalBinding(InitExpr)) {
+        LocalResourceBindings.insert({VDecl, *Binding});
+      } else {
+        SemaRef.Diag(Init->getBeginLoc(),
+                     diag::err_hlsl_assigning_local_resource_is_not_unique)
+            << Init;
+        return false;
+      }
+    }
+
   const HLSLVkConstantIdAttr *ConstIdAttr =
       VDecl->getAttr<HLSLVkConstantIdAttr>();
   if (!ConstIdAttr)
diff --git a/clang/test/SemaHLSL/local_resource_binding.hlsl b/clang/test/SemaHLSL/local_resource_binding.hlsl
new file mode 100644
index 0000000000000..3059452b49ef0
--- /dev/null
+++ b/clang/test/SemaHLSL/local_resource_binding.hlsl
@@ -0,0 +1,56 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -verify %s
+
+// expected-no-diagnostics
+
+RWBuffer<int> In : register(u0);
+RWStructuredBuffer<int> Out0 : register(u1);
+RWStructuredBuffer<int> Out1 : register(u2);
+RWStructuredBuffer<int> OutArr[];
+
+cbuffer c {
+    bool cond;
+};
+
+void no_initial_assignment(int idx) {
+    RWStructuredBuffer<int> Out;
+    if (cond) {
+        Out = Out1;
+    }
+    Out[idx] = In[idx];
+}
+
+void same_assignment(int idx) {
+    RWStructuredBuffer<int> Out = Out1;
+    if (cond) {
+        Out = Out1;
+    }
+    Out[idx] = In[idx];
+}
+
+void conditional_initialization_with_index(int idx) {
+    RWStructuredBuffer<int> Out = cond ? OutArr[0] : OutArr[1];
+    Out[idx] = In[idx];
+}
+
+void conditional_assignment_with_index(int idx) {
+    RWStructuredBuffer<int> Out;
+	if (cond) {
+		Out = OutArr[0];
+	} else {
+		Out = OutArr[1];
+	}
+    Out[idx] = In[idx];
+}
+
+void reassignment(int idx) {
+    RWStructuredBuffer<int> Out = Out0;
+	if (cond) {
+		Out = Out0;
+	}
+	Out[idx] = In[idx];
+}
+
+void conditional_result_in_same(int idx) {
+    RWStructuredBuffer<int> Out = cond ? Out0 : Out0;
+	Out[idx] = In[idx];
+}
diff --git a/clang/test/SemaHLSL/local_resource_binding_errs.hlsl b/clang/test/SemaHLSL/local_resource_binding_errs.hlsl
new file mode 100644
index 0000000000000..163d3c4493ca2
--- /dev/null
+++ b/clang/test/SemaHLSL/local_resource_binding_errs.hlsl
@@ -0,0 +1,41 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -verify %s
+
+RWBuffer<int> In : register(u0);
+RWStructuredBuffer<int> Out0 : register(u1);
+RWStructuredBuffer<int> Out1 : register(u2);
+RWStructuredBuffer<int> OutArr[];
+
+cbuffer c {
+    bool cond;
+};
+
+void conditional_initialization(int idx) {
+    // expected-error@+1 {{assignment to local resource 'cond ? Out0 : Out1' is not to same unique global resource}}
+    RWStructuredBuffer<int> Out = cond ? Out0 : Out1;
+    Out[idx] = In[idx];
+}
+
+void branched_assignment(int idx) {
+    RWStructuredBuffer<int> Out = Out0; // expected-note {{variable 'Out' is declared here}}
+    if (cond) {
+        // expected-error@+1 {{assignment to local resource 'Out1' is not to same unique global resource}}
+        Out = Out1;
+    }
+    Out[idx] = In[idx];
+}
+
+void branched_assignment_with_array(int idx) {
+    RWStructuredBuffer<int> Out = Out0; // expected-note {{variable 'Out' is declared here}}
+    if (cond) {
+        // expected-error@+1 {{assignment to local resource 'OutArr[0]' is not to same unique global resource}}
+        Out = OutArr[0];
+    }
+    Out[idx] = In[idx];
+}
+
+void conditional_assignment(int idx) {
+    RWStructuredBuffer<int> Out;
+    // expected-error@+1 {{assignment to local resource 'cond ? Out0 : Out1' is not to same unique global resource}}
+    Out = cond ? Out0 : Out1;
+    Out[idx] = In[idx];
+}

Comment thread clang/include/clang/Basic/DiagnosticSemaKinds.td Outdated
Comment thread clang/include/clang/Sema/SemaHLSL.h Outdated

@Icohedron Icohedron left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Just some nits.

Comment thread clang/lib/Sema/SemaHLSL.cpp
Comment thread clang/lib/Sema/SemaHLSL.cpp Outdated
Comment thread clang/lib/Sema/SemaHLSL.cpp Outdated
Comment thread clang/lib/Sema/SemaHLSL.cpp Outdated
Co-authored-by: Deric C. <cheung.deric@gmail.com>
@github-actions

github-actions Bot commented Jan 19, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@github-actions

github-actions Bot commented Jan 19, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 112667 tests passed
  • 4613 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Jan 19, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 53486 tests passed
  • 2229 tests skipped

✅ The build succeeded and all tests passed.

@joaosaffran joaosaffran left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The logic seems correct, but I am not familiar with the resource bindings' inner details. I think we could do some improvements in the error handling; the messages being emitted seem confusing, IMHO.

Comment thread clang/test/SemaHLSL/local_resource_binding_errs.hlsl Outdated
Comment thread clang/test/SemaHLSL/local_resource_binding_errs.hlsl Outdated
@hekota

hekota commented Jan 20, 2026

Copy link
Copy Markdown
Member

I wonder if this is the only way we can resolve the convergence issue. Is there really no way how to deal with this in the backend?

this complys with dxc error messaging
@inbelic inbelic marked this pull request as draft January 20, 2026 23:10
@inbelic

inbelic commented Jan 20, 2026

Copy link
Copy Markdown
Contributor Author

From offline discussion, it is noted that the current implementation misses the case when using a local array of resources and that we are over-eagerly marking uses as invalid wrt DXC. Marking as draft to resolve these issues before review.

  1. Using local resource arrays:
RWStructuredBuffer<int> Outs[] = {Out0, Out1};
Out[cond ? 1 : 0][GI] = In[GI]; // <- generates error here in DXC, and we need to match this
Out[0][GI] = In[GI]; // <- valid
  1. Reassigning without divergent control flow:
RWStructuredBuffer<int> Out = Out0;
if (cond) {
  Out[GI] = In[0]
  Out = Out1;
} else {
  Out[GI] = In[1];
  Out = Out1;
}
Out[GI] = In[GI]; // <- is valid because it is guaranteed to be equivalent

DXC does some static analysis to determine if the resource will actually be from a different global resource, which we must match.

@inbelic

inbelic commented Jan 29, 2026

Copy link
Copy Markdown
Contributor Author

I have pushed some demonstrative changes that would largely match DXC behaviour of assignment tracking for local resources. However, there are still remaining concerns:

  1. DXC does this analysis after DCE and constant folding. So if we wish to match the behaviour exactly, we are required to fully evaluate any constant conditions.
  2. DXC allows having an uninitialized local resource and assigning to, which seems like a case we should disallow.
  3. As noted ahove, this behaviour is also present for local resource arrays. So to extend this further gets tricky as we need to track the assigns of each resource in an array and only generate an error if the index into the array is dynamic and the elements are not.

I had intended this pr to be a simple pre-requisite for the depending pr, however, I think it warrants having its own issue to track and properly determine the right approach here.

@inbelic inbelic closed this Jan 29, 2026
@inbelic inbelic deleted the inbelic/verify-local-resources branch March 3, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category HLSL HLSL Language Support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants