[Mlir-commits] [mlir] [mlir][SPIR-V] Add LifetimeStart and LifetimeStop operations (PR #232191)

Alexander Shaposhnikov llvmlistbot at llvm.org
Wed Oct 14 23:17:28 PDT 2026


https://github.com/alexander-shaposhnikov created https://github.com/llvm/llvm-project/pull/232191

Add spirv.LifetimeStart and spirv.LifetimeStop with assembly syntax, verification, and generated binary serialization and deserialization. Both operations require function scope and the Kernel capability.



>From 8e7c87a8a75f4965202ccba9103be940ac3023f2 Mon Sep 17 00:00:00 2001
From: Alexander Shaposhnikov <alexander.v.shaposhnikov at gmail.com>
Date: Wed, 14 Oct 2026 23:16:48 -0700
Subject: [PATCH] [mlir][SPIR-V] Add LifetimeStart and LifetimeStop operations

Add spirv.LifetimeStart and spirv.LifetimeStop with assembly syntax,
verification, and generated binary serialization and deserialization.
Both operations require function scope and the Kernel capability.

Share verification of the Function storage class and zero-size requirement.
Only zero sizes are supported because the dialect cannot represent void
pointee types; pointers to i8 follow the same rule as other typed pointers.

Add tests for textual assembly, invalid storage classes and nonzero sizes,
and binary round trips for both operations.
---
 .../mlir/Dialect/SPIRV/IR/SPIRVBase.td        |  5 +-
 .../Dialect/SPIRV/IR/SPIRVControlFlowOps.td   | 88 +++++++++++++++++++
 mlir/lib/Dialect/SPIRV/IR/ControlFlowOps.cpp  | 20 +++++
 .../Dialect/SPIRV/IR/control-flow-ops.mlir    | 72 +++++++++++++++
 mlir/test/Target/SPIRV/lifetime.mlir          | 23 +++++
 5 files changed, 207 insertions(+), 1 deletion(-)
 create mode 100644 mlir/test/Target/SPIRV/lifetime.mlir

diff --git a/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVBase.td b/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVBase.td
index 7e799fc0191076..54934572860b9b 100644
--- a/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVBase.td
+++ b/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVBase.td
@@ -4679,6 +4679,8 @@ def SPIRV_OC_OpKill                           : I32EnumAttrCase<"OpKill", 252>;
 def SPIRV_OC_OpReturn                         : I32EnumAttrCase<"OpReturn", 253>;
 def SPIRV_OC_OpReturnValue                    : I32EnumAttrCase<"OpReturnValue", 254>;
 def SPIRV_OC_OpUnreachable                    : I32EnumAttrCase<"OpUnreachable", 255>;
+def SPIRV_OC_OpLifetimeStart                  : I32EnumAttrCase<"OpLifetimeStart", 256>;
+def SPIRV_OC_OpLifetimeStop                   : I32EnumAttrCase<"OpLifetimeStop", 257>;
 def SPIRV_OC_OpGroupBroadcast                 : I32EnumAttrCase<"OpGroupBroadcast", 263>;
 def SPIRV_OC_OpGroupIAdd                      : I32EnumAttrCase<"OpGroupIAdd", 264>;
 def SPIRV_OC_OpGroupFAdd                      : I32EnumAttrCase<"OpGroupFAdd", 265>;
@@ -4846,7 +4848,8 @@ def SPIRV_OpcodeAttr :
       SPIRV_OC_OpPhi, SPIRV_OC_OpLoopMerge, SPIRV_OC_OpSelectionMerge,
       SPIRV_OC_OpLabel, SPIRV_OC_OpBranch, SPIRV_OC_OpBranchConditional,
       SPIRV_OC_OpSwitch, SPIRV_OC_OpKill, SPIRV_OC_OpReturn, SPIRV_OC_OpReturnValue,
-      SPIRV_OC_OpUnreachable, SPIRV_OC_OpGroupBroadcast, SPIRV_OC_OpGroupIAdd,
+      SPIRV_OC_OpUnreachable, SPIRV_OC_OpLifetimeStart, SPIRV_OC_OpLifetimeStop,
+      SPIRV_OC_OpGroupBroadcast, SPIRV_OC_OpGroupIAdd,
       SPIRV_OC_OpGroupFAdd, SPIRV_OC_OpGroupFMin, SPIRV_OC_OpGroupUMin,
       SPIRV_OC_OpGroupSMin, SPIRV_OC_OpGroupFMax, SPIRV_OC_OpGroupUMax,
       SPIRV_OC_OpGroupSMax, SPIRV_OC_OpNoLine, SPIRV_OC_OpModuleProcessed,
diff --git a/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVControlFlowOps.td b/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVControlFlowOps.td
index bc2bad2d24dbb8..b29472271c1ecd 100644
--- a/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVControlFlowOps.td
+++ b/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVControlFlowOps.td
@@ -395,6 +395,94 @@ def SPIRV_KillOp : SPIRV_Op<"Kill", [Terminator]> {
 
 // -----
 
+def SPIRV_LifetimeStartOp : SPIRV_Op<"LifetimeStart", [InFunctionScope]> {
+  let summary = "Declare that an object was not defined before this instruction.";
+
+  let description = [{
+    Pointer is a pointer to the object whose lifetime is starting. Its type
+    must be an OpTypePointer with Storage Class Function.
+
+    Size is an unsigned 32-bit integer. Size must be 0 if Pointer is a
+    pointer to a non-void type or the Addresses capability is not declared.
+    If Size is non-zero, it is the number of bytes of memory whose lifetime
+    is starting.
+
+    <!-- End of AutoGen section -->
+
+    Only zero sizes are currently supported because SPIR-V dialect pointers
+    cannot point to void.
+
+    #### Example:
+
+    ```mlir
+    %0 = spirv.Variable : !spirv.ptr<f32, Function>
+    spirv.LifetimeStart %0, 0 : !spirv.ptr<f32, Function>
+    ```
+  }];
+
+  let availability = [
+    MinVersion<SPIRV_V_1_0>,
+    MaxVersion<SPIRV_V_1_6>,
+    Extension<[]>,
+    Capability<[SPIRV_C_Kernel]>
+  ];
+
+  let arguments = (ins
+    SPIRV_AnyPtr:$pointer,
+    I32Attr:$size
+  );
+
+  let results = (outs);
+
+  let assemblyFormat = "$pointer `,` $size attr-dict `:` type($pointer)";
+}
+
+// -----
+
+def SPIRV_LifetimeStopOp : SPIRV_Op<"LifetimeStop", [InFunctionScope]> {
+  let summary = "Declare that an object is dead after this instruction.";
+
+  let description = [{
+    Pointer is a pointer to the object whose lifetime is ending. Its type
+    must be an OpTypePointer with Storage Class Function.
+
+    Size is an unsigned 32-bit integer. Size must be 0 if Pointer is a
+    pointer to a non-void type or the Addresses capability is not declared.
+    If Size is non-zero, it is the number of bytes of memory whose lifetime
+    is ending.
+
+    <!-- End of AutoGen section -->
+
+    Only zero sizes are currently supported because SPIR-V dialect pointers
+    cannot point to void.
+
+    #### Example:
+
+    ```mlir
+    %0 = spirv.Variable : !spirv.ptr<f32, Function>
+    spirv.LifetimeStop %0, 0 : !spirv.ptr<f32, Function>
+    ```
+  }];
+
+  let availability = [
+    MinVersion<SPIRV_V_1_0>,
+    MaxVersion<SPIRV_V_1_6>,
+    Extension<[]>,
+    Capability<[SPIRV_C_Kernel]>
+  ];
+
+  let arguments = (ins
+    SPIRV_AnyPtr:$pointer,
+    I32Attr:$size
+  );
+
+  let results = (outs);
+
+  let assemblyFormat = "$pointer `,` $size attr-dict `:` type($pointer)";
+}
+
+// -----
+
 def SPIRV_LoopOp : SPIRV_Op<"mlir.loop", [InFunctionScope]> {
   let summary = "Define a structured loop.";
 
diff --git a/mlir/lib/Dialect/SPIRV/IR/ControlFlowOps.cpp b/mlir/lib/Dialect/SPIRV/IR/ControlFlowOps.cpp
index 44cee60decdbd3..685a2bcc711692 100644
--- a/mlir/lib/Dialect/SPIRV/IR/ControlFlowOps.cpp
+++ b/mlir/lib/Dialect/SPIRV/IR/ControlFlowOps.cpp
@@ -304,6 +304,26 @@ Block *SwitchOp::getSuccessorForOperands(ArrayRef<Attribute> operands) {
   return nullptr;
 }
 
+//===----------------------------------------------------------------------===//
+// spirv.LifetimeStart / spirv.LifetimeStop
+//===----------------------------------------------------------------------===//
+
+template <typename LifetimeOpTy>
+static LogicalResult verifyLifetimeOp(LifetimeOpTy op) {
+  auto ptrType = cast<spirv::PointerType>(op.getPointer().getType());
+  if (ptrType.getStorageClass() != spirv::StorageClass::Function)
+    return op.emitOpError("pointer must have the Function storage class");
+
+  if (op.getSize() != 0)
+    return op.emitOpError("size must be 0 for a pointer to a non-void type");
+
+  return success();
+}
+
+LogicalResult LifetimeStartOp::verify() { return verifyLifetimeOp(*this); }
+
+LogicalResult LifetimeStopOp::verify() { return verifyLifetimeOp(*this); }
+
 //===----------------------------------------------------------------------===//
 // spirv.mlir.loop
 //===----------------------------------------------------------------------===//
diff --git a/mlir/test/Dialect/SPIRV/IR/control-flow-ops.mlir b/mlir/test/Dialect/SPIRV/IR/control-flow-ops.mlir
index 374d6ca134de51..4259e66615035f 100644
--- a/mlir/test/Dialect/SPIRV/IR/control-flow-ops.mlir
+++ b/mlir/test/Dialect/SPIRV/IR/control-flow-ops.mlir
@@ -1080,6 +1080,78 @@ func.func @kill() {
 
 // -----
 
+//===----------------------------------------------------------------------===//
+// spirv.LifetimeStart / spirv.LifetimeStop
+//===----------------------------------------------------------------------===//
+
+// CHECK-LABEL: func @lifetime
+func.func @lifetime() {
+  %0 = spirv.Variable : !spirv.ptr<f32, Function>
+  // CHECK: spirv.LifetimeStart %{{.*}}, 0 : !spirv.ptr<f32, Function>
+  spirv.LifetimeStart %0, 0 : !spirv.ptr<f32, Function>
+  // CHECK: spirv.LifetimeStop %{{.*}}, 0 : !spirv.ptr<f32, Function>
+  spirv.LifetimeStop %0, 0 : !spirv.ptr<f32, Function>
+
+  %1 = spirv.Bitcast %0 : !spirv.ptr<f32, Function> to !spirv.ptr<i8, Function>
+  // CHECK: spirv.LifetimeStart %{{.*}}, 0 : !spirv.ptr<i8, Function>
+  spirv.LifetimeStart %1, 0 : !spirv.ptr<i8, Function>
+  // CHECK: spirv.LifetimeStop %{{.*}}, 0 : !spirv.ptr<i8, Function>
+  spirv.LifetimeStop %1, 0 : !spirv.ptr<i8, Function>
+  spirv.Return
+}
+
+// -----
+
+func.func @lifetime_start_wrong_storage_class(%arg0: !spirv.ptr<f32, CrossWorkgroup>) {
+  // expected-error @+1 {{pointer must have the Function storage class}}
+  spirv.LifetimeStart %arg0, 0 : !spirv.ptr<f32, CrossWorkgroup>
+  spirv.Return
+}
+
+// -----
+
+func.func @lifetime_stop_wrong_storage_class(%arg0: !spirv.ptr<f32, CrossWorkgroup>) {
+  // expected-error @+1 {{pointer must have the Function storage class}}
+  spirv.LifetimeStop %arg0, 0 : !spirv.ptr<f32, CrossWorkgroup>
+  spirv.Return
+}
+
+// -----
+
+func.func @lifetime_start_size_on_typed_pointer() {
+  %0 = spirv.Variable : !spirv.ptr<f32, Function>
+  // expected-error @+1 {{size must be 0 for a pointer to a non-void type}}
+  spirv.LifetimeStart %0, 4 : !spirv.ptr<f32, Function>
+  spirv.Return
+}
+
+// -----
+
+func.func @lifetime_stop_size_on_typed_pointer() {
+  %0 = spirv.Variable : !spirv.ptr<f32, Function>
+  // expected-error @+1 {{size must be 0 for a pointer to a non-void type}}
+  spirv.LifetimeStop %0, 4 : !spirv.ptr<f32, Function>
+  spirv.Return
+}
+
+// -----
+
+func.func @lifetime_start_size_on_byte_pointer(%ptr: !spirv.ptr<i8, Function>) {
+  // expected-error @+1 {{size must be 0 for a pointer to a non-void type}}
+  spirv.LifetimeStart %ptr, 4 : !spirv.ptr<i8, Function>
+  spirv.Return
+}
+
+// -----
+
+func.func @lifetime_stop_size_on_byte_pointer(%ptr: !spirv.ptr<i8, Function>) {
+  // expected-error @+1 {{size must be 0 for a pointer to a non-void type}}
+  spirv.LifetimeStop %ptr, 4 : !spirv.ptr<i8, Function>
+  spirv.Return
+}
+
+// -----
+
 //===----------------------------------------------------------------------===//
 // spirv.Switch
 //===----------------------------------------------------------------------===//
diff --git a/mlir/test/Target/SPIRV/lifetime.mlir b/mlir/test/Target/SPIRV/lifetime.mlir
new file mode 100644
index 00000000000000..cbb0c9b69d3c83
--- /dev/null
+++ b/mlir/test/Target/SPIRV/lifetime.mlir
@@ -0,0 +1,23 @@
+// RUN: mlir-translate -no-implicit-module -test-spirv-roundtrip %s | FileCheck %s
+
+// RUN: %if spirv-tools %{ rm -rf %t %}
+// RUN: %if spirv-tools %{ mkdir %t %}
+// RUN: %if spirv-tools %{ mlir-translate --no-implicit-module --serialize-spirv --split-input-file --spirv-save-validation-files-with-prefix=%t/module %s %}
+// RUN: %if spirv-tools %{ spirv-val %t %}
+
+spirv.module Physical64 OpenCL requires #spirv.vce<v1.0, [Kernel, Addresses, Int8, Linkage], []> {
+  spirv.func @lifetime() "None" {
+    %0 = spirv.Variable : !spirv.ptr<f32, Function>
+    // CHECK: spirv.LifetimeStart %{{.*}}, 0 : !spirv.ptr<f32, Function>
+    spirv.LifetimeStart %0, 0 : !spirv.ptr<f32, Function>
+    // CHECK: spirv.LifetimeStop %{{.*}}, 0 : !spirv.ptr<f32, Function>
+    spirv.LifetimeStop %0, 0 : !spirv.ptr<f32, Function>
+
+    %1 = spirv.Bitcast %0 : !spirv.ptr<f32, Function> to !spirv.ptr<i8, Function>
+    // CHECK: spirv.LifetimeStart %{{.*}}, 0 : !spirv.ptr<i8, Function>
+    spirv.LifetimeStart %1, 0 : !spirv.ptr<i8, Function>
+    // CHECK: spirv.LifetimeStop %{{.*}}, 0 : !spirv.ptr<i8, Function>
+    spirv.LifetimeStop %1, 0 : !spirv.ptr<i8, Function>
+    spirv.Return
+  }
+}



More information about the Mlir-commits mailing list