[clang] [HLSL] Emit lifetime.start before copy-in for inout parameters (PR #191917)
Alexandre Isoard via cfe-commits
cfe-commits at lists.llvm.org
Mon Apr 13 17:54:51 PDT 2026
https://github.com/isoard-amd created https://github.com/llvm/llvm-project/pull/191917
For inout parameters, Clang was emitting lifetime.start after the copy-in store that initializes the temporary. Per LLVM's lifetime semantics, any access to memory outside its lifetime is undefined behavior, so the copy-in store was technically UB and the value was undefined after lifetime.start.
Move EmitLifetimeStart into EmitHLSLOutArgLValues so that it is emitted before EmitInitializationToLValue, putting the copy-in store within the lifetime of the temporary.
>From 03d612c98c2fb3c4cd0cabcb059da66b98cbe884 Mon Sep 17 00:00:00 2001
From: Alexandre Isoard <alexandre.isoard at amd.com>
Date: Mon, 13 Apr 2026 17:38:39 -0600
Subject: [PATCH] [HLSL] Emit lifetime.start before copy-in for inout
parameters
For inout parameters, Clang was emitting lifetime.start after the
copy-in store that initializes the temporary. Per LLVM's lifetime
semantics, any access to memory outside its lifetime is undefined
behavior, so the copy-in store was technically UB and the value was
undefined after lifetime.start.
Move EmitLifetimeStart into EmitHLSLOutArgLValues so that it is
emitted before EmitInitializationToLValue, putting the copy-in store
within the lifetime of the temporary.
---
clang/lib/CodeGen/CGExpr.cpp | 7 ++--
.../BasicFeatures/OutArgLifetime.hlsl | 36 +++++++++++++++++++
2 files changed, 41 insertions(+), 2 deletions(-)
create mode 100644 clang/test/CodeGenHLSL/BasicFeatures/OutArgLifetime.hlsl
diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp
index 2ef399a7e7309..b29b791efb50f 100644
--- a/clang/lib/CodeGen/CGExpr.cpp
+++ b/clang/lib/CodeGen/CGExpr.cpp
@@ -6317,6 +6317,11 @@ CodeGenFunction::EmitHLSLOutArgLValues(const HLSLOutArgExpr *E, QualType Ty) {
Address OutTemp = CreateIRTempWithoutCast(ExprTy);
LValue TempLV = MakeAddrLValue(OutTemp, ExprTy);
+ // Start the lifetime before the copy-in so that the temporary is live when
+ // the initial value is written. This ensures the store is within the
+ // lifetime and is not killed by a store undef inserted at lifetime.start.
+ EmitLifetimeStart(OutTemp.getBasePointer());
+
if (E->isInOut())
EmitInitializationToLValue(E->getCastedTemporary()->getSourceExpr(),
TempLV);
@@ -6333,8 +6338,6 @@ LValue CodeGenFunction::EmitHLSLOutArgExpr(const HLSLOutArgExpr *E,
llvm::Value *Addr = TempLV.getAddress().getBasePointer();
llvm::Type *ElTy = ConvertTypeForMem(TempLV.getType());
- EmitLifetimeStart(Addr);
-
Address TmpAddr(Addr, ElTy, TempLV.getAlignment());
Args.addWriteback(BaseLV, TmpAddr, nullptr, E->getWritebackCast());
Args.add(RValue::get(TmpAddr, *this), Ty);
diff --git a/clang/test/CodeGenHLSL/BasicFeatures/OutArgLifetime.hlsl b/clang/test/CodeGenHLSL/BasicFeatures/OutArgLifetime.hlsl
new file mode 100644
index 0000000000000..a604117be4dbf
--- /dev/null
+++ b/clang/test/CodeGenHLSL/BasicFeatures/OutArgLifetime.hlsl
@@ -0,0 +1,36 @@
+// RUN: %clang_cc1 -triple dxil-pc-shadermodel6.0-library -O3 -disable-llvm-passes -emit-llvm -finclude-default-header -o - %s | FileCheck %s
+
+// Check that lifetime.start for an inout argument temporary is emitted
+// *before* the copy-in store, so that the store is within the lifetime
+// and is not treated as undefined behavior.
+
+void increment(inout int I) { I += 1; }
+void reset(out int I) { I = 0; }
+
+// CHECK-LABEL: define noundef i32 {{.*}}inout_test
+// CHECK: alloca i32
+// CHECK: [[TMP:%.*]] = alloca i32
+// The lifetime.start must come before the copy-in load/store sequence.
+// CHECK: call void @llvm.lifetime.start.p0(ptr [[TMP]])
+// CHECK-NEXT: {{.*}} = load i32
+// CHECK-NEXT: store i32 {{.*}}, ptr [[TMP]]
+// CHECK: call void {{.*}}increment{{.*}}(ptr {{.*}} [[TMP]])
+// CHECK: call void @llvm.lifetime.end.p0(ptr [[TMP]])
+export int inout_test(int X) {
+ increment(X);
+ return X;
+}
+
+// For `out` parameters there is no copy-in, so lifetime.start just needs
+// to appear before the call with no intervening store to the temporary.
+// CHECK-LABEL: define noundef i32 {{.*}}out_test
+// CHECK: alloca i32
+// CHECK: [[TMP2:%.*]] = alloca i32
+// CHECK: call void @llvm.lifetime.start.p0(ptr [[TMP2]])
+// CHECK-NEXT: call void {{.*}}reset{{.*}}(ptr {{.*}} [[TMP2]])
+// CHECK: call void @llvm.lifetime.end.p0(ptr [[TMP2]])
+export int out_test() {
+ int X;
+ reset(X);
+ return X;
+}
More information about the cfe-commits
mailing list