[clang] [Clang] [CodeGen] Avoid constant folding when DeclRefExpr references a captured variable (PR #192704)
via cfe-commits
cfe-commits at lists.llvm.org
Fri Apr 17 10:16:07 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang
Author: ross.codes (kilgariff)
<details>
<summary>Changes</summary>
Codegen behaves incorrectly when a DeclRefExpr refers to a variable that is named in a lambda capture list if:
- The captured variable is a reference to a global.
- The variable is captured by value (not by reference).
In this case, the value of the global at the time of capture should be copied into lambda field storage and any DeclRefExpr that refers to the variable should now instead refer to the captured copy.
This was discovered with C++ lambda captures, but the fix also guards against similar issues with Blocks, Objective-C `@<!-- -->finally`, and OpenMP.
In this situation, Clang 8 shows the correct result whereas Clang 9 doesn't. This appears to be due to a regression in 36bd1c90d0e0.
Some of the removed code from that commit states:
"FIXME: This should be handled in odr-use marking, not here."
I am new to the Clang codebase, but from what I gather this is suggesting that Sema should mark the DeclRefExpr as being an odr-use if it refers to a captured copy because that copy has definite storage. This would sidestep constant folding and would always handle captured values correctly. I am not currently confident in making that change myself, but would welcome clarification if that solution is preferred over the one implemented here.
---
References and worked examples of the issue:
>From cppreference: https://en.cppreference.com/cpp/language/lambda
_The type of each data member is the type of the corresponding captured entity, except if the entity has reference type (in that case, references to functions are captured as lvalue references to the referenced functions, and references to objects are captured as copies of the referenced objects)._
Example of issue: https://godbolt.org/z/T678KT9eM
```
#include <cstdio>
int global = 5;
int main() {
int & local = global;
printf("outside: 0x%p\n", &local);
auto cb = [local]() mutable {
printf("inside: 0x%p\n", &local);
local = 10;
};
cb();
printf("result: %d\n", local);
/*
clang 22.1.0 output:
outside: 0x0x58c5be87a018
inside: 0x0x58c5be87a018
result: 10
msvc 19 cl output:
outside: 0x00007FF74E028000
inside: 0x000000ECACD7FA50
result: 5
gcc 15.2 output:
outside: 0x0x404018
inside: 0x0x7ffd54134934
result: 5
*/
}
```
---
Full diff: https://github.com/llvm/llvm-project/pull/192704.diff
3 Files Affected:
- (modified) clang/lib/CodeGen/CGExpr.cpp (+34-1)
- (modified) clang/lib/CodeGen/CodeGenFunction.h (+5)
- (added) clang/test/CodeGenCXX/captured-reference-declref-contexts.mm (+93)
``````````diff
diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp
index ab08f3a238ef6..3435aee987859 100644
--- a/clang/lib/CodeGen/CGExpr.cpp
+++ b/clang/lib/CodeGen/CGExpr.cpp
@@ -3555,6 +3555,37 @@ static bool canEmitSpuriousReferenceToVariable(CodeGenFunction &CGF,
}
}
+/// Returns true if \p E refers to an enclosing variable whose value/object is
+/// represented by capture storage in the current function (lambda field,
+/// captured statement, block).
+static bool declRefExprNamesCaptureStorage(CodeGenFunction &CGF,
+ const DeclRefExpr *E,
+ const VarDecl *VD) {
+
+ if (!E->refersToEnclosingVariableOrCapture())
+ return false;
+
+ // Captured in an Objective-C block (or C with language extension):
+ const auto *BD = dyn_cast<BlockDecl>(CGF.CurCodeDecl);
+ if (BD && BD->capturesVariable(VD))
+ return true;
+
+ const VarDecl *CanonicalDecl = VD->getCanonicalDecl();
+
+ // Captured in a C++ lambda:
+ if (CGF.LambdaCaptureFields.lookup(CanonicalDecl))
+ return true;
+
+ // Captured in OpenMP regions (CR_OpenMP), Objective-C @finally
+ // (CR_ObjCAtFinally), or other default captured regions (CR_Default).
+ if (CGF.CapturedStmtInfo &&
+ (CGF.hasLocalDeclEntry(CanonicalDecl) ||
+ CGF.CapturedStmtInfo->lookup(CanonicalDecl)))
+ return true;
+
+ return false;
+}
+
LValue CodeGenFunction::EmitDeclRefLValue(const DeclRefExpr *E) {
const NamedDecl *ND = E->getDecl();
QualType T = E->getType();
@@ -3574,7 +3605,9 @@ LValue CodeGenFunction::EmitDeclRefLValue(const DeclRefExpr *E) {
// constant value directly instead.
if (E->isNonOdrUse() == NOUR_Constant &&
(VD->getType()->isReferenceType() ||
- !canEmitSpuriousReferenceToVariable(*this, E, VD))) {
+ !canEmitSpuriousReferenceToVariable(*this, E, VD)) &&
+ !declRefExprNamesCaptureStorage(*this, E, VD)) {
+
VD->getAnyInitializer(VD);
llvm::Constant *Val = ConstantEmitter(*this).emitAbstract(
E->getLocation(), *VD->evaluateValue(), VD->getType());
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index 29b87a0616992..7bef437c76e38 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -3086,6 +3086,11 @@ class CodeGenFunction : public CodeGenTypeCache {
return it->second;
}
+ /// Return true if \p VD has an entry in LocalDeclMap for this function.
+ bool hasLocalDeclEntry(const VarDecl *VD) const {
+ return LocalDeclMap.find(VD) != LocalDeclMap.end();
+ }
+
/// Given an opaque value expression, return its LValue mapping if it exists,
/// otherwise create one.
LValue getOrCreateOpaqueLValueMapping(const OpaqueValueExpr *e);
diff --git a/clang/test/CodeGenCXX/captured-reference-declref-contexts.mm b/clang/test/CodeGenCXX/captured-reference-declref-contexts.mm
new file mode 100644
index 0000000000000..16b866313b5b5
--- /dev/null
+++ b/clang/test/CodeGenCXX/captured-reference-declref-contexts.mm
@@ -0,0 +1,93 @@
+// RUN: %clang_cc1 -std=gnu++17 -fopenmp -fopenmp-version=45 -fblocks -fexceptions -fobjc-exceptions -triple x86_64-apple-darwin12 -disable-O0-optnone -emit-llvm %s -o - | FileCheck %s
+
+/// Regression tests for `EmitDeclRefLValue`: a `DeclRefExpr` to a local reference
+/// variable must not take the `NOUR_Constant` fast path through to the referee
+/// when that name is implemented via capture storage (lambda field, OpenMP
+/// outlined region, block literal, ObjC @finally capture, etc.).
+///
+/// Contexts below are intentionally distinct:
+/// - **Lambda `[r]`**: by-copy capture holds a copy of the referenced object; the
+/// body must not mutate `global` when the outer binding was `int &r = global`.
+/// - **Lambda `[&r]`**: contrast — capture holds the address of `r`; mutation
+/// still reaches `global`.
+/// - **OpenMP `firstprivate`**: each thread gets private storage initialized from
+/// the outer `r`; the outlined body must not store through `@global`.
+/// - **Block**: Clang stores the *address* of the referee in the block literal; the
+/// invoke must still load that pointer from the capture field. Using
+/// `int *p = &global; int &r = *p` avoids a global block with no captures that
+/// would not exercise this path.
+/// - **`@finally`**: the body is a captured statement (`CR_ObjCAtFinally`). Use
+/// `int *p = &global; int &r = *p` like the block case: plain `int &r = global`
+/// can be lowered as a direct store to `@global` in the `@finally` block, which
+/// does not exercise the capture path; indirection keeps the `DeclRef` for `r`
+/// tied to capture lowering.
+
+int global;
+
+void f1() {
+ int &r = global;
+ r = 1;
+ auto L = [r]() mutable { r = 99; };
+ L();
+}
+
+void f2() {
+ int &r = global;
+ r = 2;
+ auto L = [&r]() mutable { r = 88; };
+ L();
+}
+
+void omp_firstprivate_ref_to_global(void) {
+ int &r = global;
+ r = 7;
+#pragma omp parallel num_threads(1) default(none) firstprivate(r)
+ {
+ r = 8421;
+ }
+}
+
+void block_ref_to_global(void) {
+ int *p = &global;
+ int &r = *p;
+ r = 1;
+ void (^b)(void) = ^{
+ r = 8423;
+ };
+ b();
+}
+
+void finally_ref_to_global(void) {
+ int *p = &global;
+ int &r = *p;
+ r = 7;
+ @try {
+ } @finally {
+ r = 8424;
+ }
+}
+
+// --- Lambda by-copy: must not store into @global inside the closure.
+// CHECK-LABEL: define internal void @"{{_ZZ2f1vE.+clEv}}"
+// CHECK-NOT: store i32 99, ptr @global
+// CHECK: store i32 99
+
+// --- Lambda by-reference: write still reaches the referee (value 88).
+// CHECK-LABEL: define internal void @"{{_ZZ2f2vE.+clEv}}"
+// CHECK: store i32 88
+
+// --- OpenMP firstprivate: outlined region must not store into @global.
+// CHECK-LABEL: define {{.*}}@{{.*}}omp_firstprivate_ref_to_global
+// CHECK: define internal {{.*}}@{{.*}}.omp_outlined
+// CHECK-NOT: store i32 8421, ptr @global
+
+// --- Block: invoke must not fold to a direct store to @global.
+// CHECK-LABEL: define {{.*}}@_Z{{.*}}block_ref_to_global
+// CHECK: define internal {{.*}} @{{.*}}_block_invoke
+// CHECK-NOT: store i32 8423, ptr @global
+// CHECK: store i32 8423
+
+// --- ObjC @finally: must not fold to a direct store to @global (see header).
+// CHECK-LABEL: define {{.*}}@_Z{{.*}}finally_ref_to_global
+// CHECK-NOT: store i32 8424, ptr @global
+// CHECK: store i32 8424
``````````
</details>
https://github.com/llvm/llvm-project/pull/192704
More information about the cfe-commits
mailing list