diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td index 67ddaa73d9184..115031529348a 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIROps.td +++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td @@ -4670,6 +4670,84 @@ def CIR_StackRestoreOp : CIR_Op<"stackrestore"> { let assemblyFormat = "$ptr attr-dict `:` qualified(type($ptr))"; } +//===----------------------------------------------------------------------===// +// LifetimeStartOp & LifetimeEndOp +//===----------------------------------------------------------------------===// + +def CIR_LifetimeStartOp : CIR_Op<"lifetime.start"> { + let summary = "Marks the start of the lifetime of a variable produced by a cir.alloca operation"; + let description = [{ + The `cir.lifetime.start` operation marks the beginning of the lifetime + of the storage pointed to by `$ptr`. Between this operation and a + matching `cir.lifetime.end` on the same pointer, the underlying storage + is considered live; outside that range it is considered dead, and the + optimizer is free to reuse the storage for other purposes. + + The `cir.scope` is the operation that models the block scope of the C/C++ + source. Once ClangIR is no longer structured, `cir.scope` can no longer express + the lifetime of a local variable. This happens, for example, after `FlattenCFG`, + where `cir.scope` regions are dissolved into plain basic blocks, or after + `HoistAllocas`, where an alloca is moved to the function entry so that its + position no longer reflects the scope it was declared in. In that form, these + lifetime markers are what delimit the beginning and end of a variable's + lifetime. + + The verifier requires `$ptr` to be produced by a `cir.alloca`. For the + lifetime to be meaningful, a matching `cir.lifetime.end` on the same + pointer should follow on every control-flow path. This is different from + LLVM, where an `llvm.lifetime.start` may appear without a matching + `llvm.lifetime.end` -- there the storage is also implicitly marked dead + when the function returns (see the + [LLVM LangRef](https://llvm.org/docs/LangRef.html#int-lifestart)). + + This operation corresponds to the LLVM intrinsic `llvm.lifetime.start`. + + Example: + ``` + cir.lifetime.start %ptr : !cir.ptr + ``` + }]; + + let arguments = (ins CIR_PointerType:$ptr); + let assemblyFormat = "$ptr attr-dict `:` qualified(type($ptr))"; + let hasVerifier = 1; +} + +def CIR_LifetimeEndOp : CIR_Op<"lifetime.end"> { + let summary = "Marks the end of the lifetime of a variable produced by a cir.alloca operation"; + let description = [{ + The `cir.lifetime.end` operation marks the end of the lifetime of the + storage pointed to by `$ptr`. After this operation the underlying storage + is considered dead, and the optimizer is free to reuse the storage for + other purposes, until a subsequent `cir.lifetime.start` on the same + pointer revives it. + + The `cir.scope` is the operation that models the block scope of the C/C++ + source. Once ClangIR is no longer structured, `cir.scope` can no longer express + the lifetime of a local variable. This happens, for example, after `FlattenCFG`, + where `cir.scope` regions are dissolved into plain basic blocks, or after + `HoistAllocas`, where an alloca is moved to the function entry so that its + position no longer reflects the scope it was declared in. In that form, these + lifetime markers are what delimit the beginning and end of a variable's + lifetime. + + The verifier requires `$ptr` to be produced by a `cir.alloca`. + `cir.lifetime.end` should be preceded by a matching `cir.lifetime.start` on + the same pointer on every control-flow path that reaches it. + + This operation corresponds to the LLVM intrinsic `llvm.lifetime.end`. + + Example: + ``` + cir.lifetime.end %ptr : !cir.ptr + ``` + }]; + + let arguments = (ins CIR_PointerType:$ptr); + let assemblyFormat = "$ptr attr-dict `:` qualified(type($ptr))"; + let hasVerifier = 1; +} + //===----------------------------------------------------------------------===// // InlineAsmOp //===----------------------------------------------------------------------===// diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp index c23a02d6f49fb..92cedea28cfca 100644 --- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp @@ -19,6 +19,7 @@ #include "mlir/IR/Attributes.h" #include "mlir/IR/DialectImplementation.h" #include "mlir/IR/PatternMatch.h" +#include "mlir/IR/Value.h" #include "mlir/Interfaces/ControlFlowInterfaces.h" #include "mlir/Interfaces/FunctionImplementation.h" #include "mlir/Support/LLVM.h" @@ -208,6 +209,19 @@ static bool omitRegionTerm(mlir::Region &r) { return singleNonEmptyBlock && yieldsNothing(); } +// Verifies that the given operand is produced by an operation of type +// ExpectedProducerOp. +template +static LogicalResult verifyProducedBy(Operation *op, Value operand, + StringRef operandName) { + Operation *producer = operand.getDefiningOp(); + if (!producer || !isa(producer)) + return op->emitOpError() + << "operand '" << operandName << "' must be produced by '" + << ExpectedProducerOp::getOperationName() << "'"; + return success(); +} + //===----------------------------------------------------------------------===// // InlineKindAttr (FIXME: remove once FuncOp uses assembly format) //===----------------------------------------------------------------------===// @@ -4377,6 +4391,18 @@ cir::EhTypeIdOp::verifySymbolUses(SymbolTableCollection &symbolTable) { return success(); } +//===----------------------------------------------------------------------===// +// LifetimeStartOp & LifetimeEndOp +//===----------------------------------------------------------------------===// + +LogicalResult cir::LifetimeStartOp::verify() { + return verifyProducedBy(*this, getPtr(), "ptr"); +} + +LogicalResult cir::LifetimeEndOp::verify() { + return verifyProducedBy(*this, getPtr(), "ptr"); +} + //===----------------------------------------------------------------------===// // ConstructCatchParamOp //===----------------------------------------------------------------------===// diff --git a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp index c4e98e299dfc1..7fdf6ce101303 100644 --- a/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp +++ b/clang/lib/CIR/Lowering/DirectToLLVM/LowerToLLVM.cpp @@ -4193,6 +4193,21 @@ mlir::LogicalResult CIRToLLVMStackRestoreOpLowering::matchAndRewrite( return mlir::success(); } +mlir::LogicalResult CIRToLLVMLifetimeStartOpLowering::matchAndRewrite( + cir::LifetimeStartOp op, OpAdaptor adaptor, + mlir::ConversionPatternRewriter &rewriter) const { + rewriter.replaceOpWithNewOp(op, + adaptor.getPtr()); + return mlir::success(); +} + +mlir::LogicalResult CIRToLLVMLifetimeEndOpLowering::matchAndRewrite( + cir::LifetimeEndOp op, OpAdaptor adaptor, + mlir::ConversionPatternRewriter &rewriter) const { + rewriter.replaceOpWithNewOp(op, adaptor.getPtr()); + return mlir::success(); +} + mlir::LogicalResult CIRToLLVMVecCreateOpLowering::matchAndRewrite( cir::VecCreateOp op, OpAdaptor adaptor, mlir::ConversionPatternRewriter &rewriter) const { diff --git a/clang/test/CIR/IR/invalid-lifetime.cir b/clang/test/CIR/IR/invalid-lifetime.cir new file mode 100644 index 0000000000000..6462a20ca06d3 --- /dev/null +++ b/clang/test/CIR/IR/invalid-lifetime.cir @@ -0,0 +1,36 @@ +// RUN: cir-opt %s -verify-diagnostics -split-input-file + +!s32i = !cir.int + +// `cir.lifetime.start` requires its pointer to be produced by a `cir.alloca`. +cir.func @start_not_alloca(%arg0: !cir.ptr) { +// expected-error @below {{operand 'ptr' must be produced by 'cir.alloca'}} +cir.lifetime.start %arg0 : !cir.ptr +cir.return +} + +// ----- + +!s32i = !cir.int + +// `cir.lifetime.end` requires its pointer to be produced by a `cir.alloca`. +cir.func @end_not_alloca(%arg0: !cir.ptr) { +// expected-error @below {{operand 'ptr' must be produced by 'cir.alloca'}} +cir.lifetime.end %arg0 : !cir.ptr +cir.return +} + +// ----- + +!s32i = !cir.int + +cir.global external @g = #cir.int<0> : !s32i + +// The pointer has a defining op, but it is a `cir.get_global`, not a +// `cir.alloca`, so the verifier still rejects it. +cir.func @start_from_global() { +%0 = cir.get_global @g : !cir.ptr +// expected-error @below {{operand 'ptr' must be produced by 'cir.alloca'}} +cir.lifetime.start %0 : !cir.ptr +cir.return +} diff --git a/clang/test/CIR/IR/lifetime.cir b/clang/test/CIR/IR/lifetime.cir new file mode 100644 index 0000000000000..5b310d93679d1 --- /dev/null +++ b/clang/test/CIR/IR/lifetime.cir @@ -0,0 +1,14 @@ +// Test the CIR operations can parse and print correctly (roundtrip) + +// RUN: cir-opt %s --verify-roundtrip | FileCheck %s + +!s32i = !cir.int + +cir.func @lifetime_start_end() { + %0 = cir.alloca !s32i, !cir.ptr, ["x"] {alignment = 4 : i64} + // CHECK: cir.lifetime.start %0 : !cir.ptr + cir.lifetime.start %0 : !cir.ptr + // CHECK: cir.lifetime.end %0 : !cir.ptr + cir.lifetime.end %0 : !cir.ptr + cir.return +} diff --git a/clang/test/CIR/Lowering/lifetime.cir b/clang/test/CIR/Lowering/lifetime.cir new file mode 100644 index 0000000000000..70db17eae76bf --- /dev/null +++ b/clang/test/CIR/Lowering/lifetime.cir @@ -0,0 +1,12 @@ +// RUN: cir-opt %s -cir-to-llvm -o - | mlir-translate -mlir-to-llvmir | FileCheck %s + +!s32i = !cir.int + +cir.func @lifetime_markers() { + %0 = cir.alloca !s32i, !cir.ptr, ["x"] {alignment = 4 : i64} + // CHECK: call void @llvm.lifetime.start.p0(ptr %[[X:.*]]) + cir.lifetime.start %0 : !cir.ptr + // CHECK: call void @llvm.lifetime.end.p0(ptr %[[X]]) + cir.lifetime.end %0 : !cir.ptr + cir.return +}