[clang-tools-extra] Add llvm-mlir-use-after-erase check (PR #210727)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Jul 20 07:14:25 PDT 2026
https://github.com/Harald-R created https://github.com/llvm/llvm-project/pull/210727
As proposed in [this thread](https://discourse.llvm.org/t/clang-tidy-checks-for-common-mlir-anti-patterns/91179), a clang-tidy check could be introduced which detects cases where an operation is erased, but it still has uses after. The check targets the following calls for erasing an operation:
- `mlir::Operation::erase`
- `mlir::Operation::destroy`
- `mlir::RewriterBase::eraseOp`
- `mlir::RewriterBase::replaceOp`
- through derived operations of `mlir::OpState` that are wrappers around `mlir::Operation` (e.g. `mlir::func::FuncOp` and all others that inherit from `mlir::Op`)
Cases where an operation's declaration variable is reinitialized are also taken into account. Additionally, cases where an operation is erased in a loop's iteration are covered as well.
The implementation of this check is based on the existing [bugprone-use-after-move](https://clang.llvm.org/extra/clang-tidy/checks/bugprone/use-after-move.html).
>From 4c4a2c2e0a926114d5dccdcf91375e7c3d1926d7 Mon Sep 17 00:00:00 2001
From: Harald-R <rotuna.razvan at gmail.com>
Date: Mon, 13 Jul 2026 14:17:39 +0300
Subject: [PATCH] Add llvm-mlir-use-after-erase check
---
.../clang-tidy/llvm/CMakeLists.txt | 1 +
.../clang-tidy/llvm/LLVMTidyModule.cpp | 3 +
.../llvm/MlirUseAfterEraseCheck.cpp | 365 ++++++++++++++
.../clang-tidy/llvm/MlirUseAfterEraseCheck.h | 37 ++
clang-tools-extra/docs/ReleaseNotes.rst | 9 +
.../docs/clang-tidy/checks/list.rst | 1 +
.../checks/llvm/mlir-use-after-erase.rst | 39 ++
.../checkers/llvm/mlir-use-after-erase.cpp | 476 ++++++++++++++++++
8 files changed, 931 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp
create mode 100644 clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h
create mode 100644 clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp
diff --git a/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt b/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt
index bec3ba50c81c5..847695c853cf5 100644
--- a/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt
@@ -8,6 +8,7 @@ add_clang_library(clangTidyLLVMModule STATIC
HeaderGuardCheck.cpp
IncludeOrderCheck.cpp
LLVMTidyModule.cpp
+ MlirUseAfterEraseCheck.cpp
PreferIsaOrDynCastInConditionalsCheck.cpp
PreferRegisterOverUnsignedCheck.cpp
PreferStaticOverAnonymousNamespaceCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp b/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp
index 918af88c979e0..1f42cce26a0b8 100644
--- a/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp
@@ -14,6 +14,7 @@
#include "FormatvStringCheck.h"
#include "HeaderGuardCheck.h"
#include "IncludeOrderCheck.h"
+#include "MlirUseAfterEraseCheck.h"
#include "PreferIsaOrDynCastInConditionalsCheck.h"
#include "PreferRegisterOverUnsignedCheck.h"
#include "PreferStaticOverAnonymousNamespaceCheck.h"
@@ -36,6 +37,8 @@ class LLVMModule : public ClangTidyModule {
CheckFactories.registerCheck<FormatvStringCheck>("llvm-formatv-string");
CheckFactories.registerCheck<LLVMHeaderGuardCheck>("llvm-header-guard");
CheckFactories.registerCheck<IncludeOrderCheck>("llvm-include-order");
+ CheckFactories.registerCheck<MlirUseAfterEraseCheck>(
+ "llvm-mlir-use-after-erase");
CheckFactories.registerCheck<readability::NamespaceCommentCheck>(
"llvm-namespace-comment");
CheckFactories.registerCheck<PreferIsaOrDynCastInConditionalsCheck>(
diff --git a/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp
new file mode 100644
index 0000000000000..6e5b6707cbd11
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.cpp
@@ -0,0 +1,365 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "MlirUseAfterEraseCheck.h"
+#include "../utils/ExprSequence.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/Analyses/CFGReachabilityAnalysis.h"
+#include "clang/Analysis/CFG.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include <optional>
+
+using namespace clang::ast_matchers;
+using namespace clang::tidy::utils;
+
+namespace clang::tidy::llvm_check {
+
+namespace {
+
+/// Contains information about a use-after-erase.
+struct UseAfterErase {
+ // The DeclRefExpr that constituted the use of the operation.
+ const DeclRefExpr *DeclRef;
+
+ // Is the order in which the erase and the use are evaluated undefined?
+ bool EvaluationOrderUndefined = false;
+
+ // Does the use happen in a later loop iteration than the erase?
+ //
+ // We default to false and change it to true if required in find().
+ bool UseHappensInLaterLoopIteration = false;
+};
+
+/// Finds uses of an `mlir::Operation` variable that are reachable after the
+/// operation was erased (and maintains state required by the various internal
+/// helper functions).
+class UseAfterEraseFinder {
+public:
+ UseAfterEraseFinder(ASTContext *TheContext);
+
+ // Within the given code block, finds the first use of 'ErasedVariable' that
+ // occurs after 'EraseCall' (the expression that erases the operation). If a
+ // use-after-erase is found, returns information about it.
+ std::optional<UseAfterErase> find(Stmt *CodeBlock, const Expr *EraseCall,
+ const DeclRefExpr *ErasedVariable);
+
+private:
+ std::optional<UseAfterErase> findInternal(const CFGBlock *Block,
+ const Expr *EraseCall,
+ const ValueDecl *ErasedVariable);
+ void getUsesAndReinits(const CFGBlock *Block, const ValueDecl *ErasedVariable,
+ SmallVectorImpl<const DeclRefExpr *> *Uses,
+ llvm::SmallPtrSetImpl<const Stmt *> *Reinits);
+ void getDeclRefs(const CFGBlock *Block, const Decl *ErasedVariable,
+ llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs);
+ void getReinits(const CFGBlock *Block, const Decl *ErasedVariable,
+ llvm::SmallPtrSetImpl<const Stmt *> *Stmts,
+ llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs);
+
+ ASTContext *Context;
+ std::unique_ptr<ExprSequence> Sequence;
+ std::unique_ptr<StmtToBlockMap> BlockMap;
+ llvm::SmallPtrSet<const CFGBlock *, 8> Visited;
+};
+
+} // namespace
+
+UseAfterEraseFinder::UseAfterEraseFinder(ASTContext *TheContext)
+ : Context(TheContext) {}
+
+std::optional<UseAfterErase>
+UseAfterEraseFinder::find(Stmt *CodeBlock, const Expr *EraseCall,
+ const DeclRefExpr *ErasedVariable) {
+ // We include implicit and temporary destructors in the CFG so that
+ // destructors marked [[noreturn]] are handled correctly in the control flow
+ // analysis. (These are used in some styles of assertion macros.)
+ CFG::BuildOptions Options;
+ Options.AddImplicitDtors = true;
+ Options.AddTemporaryDtors = true;
+ const auto TheCFG = CFG::buildCFG(nullptr, CodeBlock, Context, Options);
+ if (!TheCFG)
+ return std::nullopt;
+
+ Sequence = std::make_unique<ExprSequence>(TheCFG.get(), CodeBlock, Context);
+ BlockMap = std::make_unique<StmtToBlockMap>(TheCFG.get(), Context);
+ Visited.clear();
+
+ const CFGBlock *EraseBlock = BlockMap->blockContainingStmt(EraseCall);
+ if (!EraseBlock)
+ EraseBlock = &TheCFG->getEntry();
+
+ auto TheUseAfterErase =
+ findInternal(EraseBlock, EraseCall, ErasedVariable->getDecl());
+
+ if (TheUseAfterErase) {
+ if (const CFGBlock *UseBlock =
+ BlockMap->blockContainingStmt(TheUseAfterErase->DeclRef)) {
+ // Does the use happen in a later loop iteration than the erase?
+ // - If they are in the same CFG block, we know the use happened in a
+ // later iteration if we visited that block a second time.
+ // - Otherwise, we know the use happened in a later iteration if the
+ // erase is reachable from the use.
+ CFGReverseBlockReachabilityAnalysis CFA(*TheCFG);
+ TheUseAfterErase->UseHappensInLaterLoopIteration =
+ UseBlock == EraseBlock ? Visited.contains(UseBlock)
+ : CFA.isReachable(UseBlock, EraseBlock);
+ }
+ }
+ return TheUseAfterErase;
+}
+
+std::optional<UseAfterErase>
+UseAfterEraseFinder::findInternal(const CFGBlock *Block, const Expr *EraseCall,
+ const ValueDecl *ErasedVariable) {
+ if (Visited.contains(Block))
+ return std::nullopt;
+
+ // Mark the block as visited (except if this is the block containing the erase
+ // call and it's being visited the first time).
+ if (!EraseCall)
+ Visited.insert(Block);
+
+ // Get all uses and reinits in the block.
+ SmallVector<const DeclRefExpr *, 1> Uses;
+ llvm::SmallPtrSet<const Stmt *, 1> Reinits;
+ getUsesAndReinits(Block, ErasedVariable, &Uses, &Reinits);
+
+ // Ignore all reinitializations where the erase potentially comes after the
+ // reinit.
+ SmallVector<const Stmt *, 1> ReinitsToDelete;
+ for (const auto *Reinit : Reinits)
+ if (EraseCall && Sequence->potentiallyAfter(EraseCall, Reinit))
+ ReinitsToDelete.push_back(Reinit);
+ for (const auto *Reinit : ReinitsToDelete)
+ Reinits.erase(Reinit);
+
+ // Find all uses that potentially come after the erase.
+ for (const auto *Use : Uses) {
+ if (!EraseCall || Sequence->potentiallyAfter(Use, EraseCall)) {
+ // Does the use have a saving reinit? A reinit is saving if it definitely
+ // comes before the use, i.e. if there's no potential that the reinit is
+ // after the use.
+ bool HaveSavingReinit = false;
+ for (const auto *Reinit : Reinits)
+ if (!Sequence->potentiallyAfter(Reinit, Use))
+ HaveSavingReinit = true;
+
+ if (!HaveSavingReinit) {
+ UseAfterErase TheUseAfterErase;
+ TheUseAfterErase.DeclRef = Use;
+
+ // Is this a use-after-erase that depends on order of evaluation?
+ // This is the case if the erase potentially comes after the use (and we
+ // already know that the use potentially comes after the erase, which
+ // taken together tells us that the ordering is unclear).
+ TheUseAfterErase.EvaluationOrderUndefined =
+ EraseCall != nullptr && Sequence->potentiallyAfter(EraseCall, Use);
+
+ return TheUseAfterErase;
+ }
+ }
+ }
+
+ // If the operation wasn't reinitialized, call ourselves recursively on all
+ // successors.
+ if (Reinits.empty()) {
+ for (const auto &Succ : Block->succs())
+ if (Succ)
+ if (auto Found = findInternal(Succ, nullptr, ErasedVariable))
+ return Found;
+ }
+
+ return std::nullopt;
+}
+
+void UseAfterEraseFinder::getUsesAndReinits(
+ const CFGBlock *Block, const ValueDecl *ErasedVariable,
+ SmallVectorImpl<const DeclRefExpr *> *Uses,
+ llvm::SmallPtrSetImpl<const Stmt *> *Reinits) {
+ llvm::SmallPtrSet<const DeclRefExpr *, 1> DeclRefs;
+ llvm::SmallPtrSet<const DeclRefExpr *, 1> ReinitDeclRefs;
+
+ getDeclRefs(Block, ErasedVariable, &DeclRefs);
+ getReinits(Block, ErasedVariable, Reinits, &ReinitDeclRefs);
+
+ // All references to the variable that aren't reinitializations are uses.
+ Uses->clear();
+ for (const DeclRefExpr *DeclRef : DeclRefs)
+ if (!ReinitDeclRefs.contains(DeclRef))
+ Uses->push_back(DeclRef);
+
+ // Sort the uses by their occurrence in the source code.
+ llvm::sort(*Uses, [](const DeclRefExpr *D1, const DeclRefExpr *D2) {
+ return D1->getExprLoc() < D2->getExprLoc();
+ });
+}
+
+void UseAfterEraseFinder::getDeclRefs(
+ const CFGBlock *Block, const Decl *ErasedVariable,
+ llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs) {
+ DeclRefs->clear();
+ const auto DeclRefMatcher =
+ declRefExpr(to(equalsNode(ErasedVariable))).bind("declref");
+
+ for (const auto &Elem : *Block) {
+ const auto S = Elem.getAs<CFGStmt>();
+ if (!S)
+ continue;
+
+ for (const auto &Match :
+ match(findAll(DeclRefMatcher), *S->getStmt(), *Context)) {
+ const auto *DeclRef = Match.getNodeAs<DeclRefExpr>("declref");
+ if (DeclRef && BlockMap->blockContainingStmt(DeclRef) == Block)
+ DeclRefs->insert(DeclRef);
+ }
+ }
+}
+
+void UseAfterEraseFinder::getReinits(
+ const CFGBlock *Block, const Decl *ErasedVariable,
+ llvm::SmallPtrSetImpl<const Stmt *> *Stmts,
+ llvm::SmallPtrSetImpl<const DeclRefExpr *> *DeclRefs) {
+ const auto DeclRefMatcher =
+ declRefExpr(to(equalsNode(ErasedVariable))).bind("declref");
+
+ // A reinitialization gives the variable a new, valid operation, which makes
+ // subsequent uses safe again. We treat plain assignments and
+ // (re-)declarations as reinitializations. For "derived" ops (e.g.
+ // `mlir::func::FuncOp`), the assignment is an overloaded `operator=`, which
+ // is represented as a `CXXOperatorCallExpr` rather than a `BinaryOperator`.
+ const auto ReinitMatcher =
+ stmt(anyOf(binaryOperator(hasOperatorName("="),
+ hasLHS(ignoringParenImpCasts(DeclRefMatcher))),
+ cxxOperatorCallExpr(
+ hasOverloadedOperatorName("="),
+ hasArgument(0, ignoringParenImpCasts(DeclRefMatcher))),
+ declStmt(hasDescendant(equalsNode(ErasedVariable)))))
+ .bind("reinit");
+
+ Stmts->clear();
+ DeclRefs->clear();
+ for (const CFGElement &Elem : *Block) {
+ const auto S = Elem.getAs<CFGStmt>();
+ if (!S)
+ continue;
+
+ for (const auto &Match :
+ match(findAll(ReinitMatcher), *S->getStmt(), *Context)) {
+ const auto *TheStmt = Match.getNodeAs<Stmt>("reinit");
+ const auto *DeclRef = Match.getNodeAs<DeclRefExpr>("declref");
+ if (TheStmt && BlockMap->blockContainingStmt(TheStmt) == Block) {
+ Stmts->insert(TheStmt);
+
+ // We count DeclStmts as reinitializations, but they don't have a
+ // DeclRefExpr associated with them -- so we need to check 'DeclRef'
+ // before adding it to the set.
+ if (DeclRef)
+ DeclRefs->insert(DeclRef);
+ }
+ }
+ }
+}
+
+static void emitDiagnostic(const Expr *EraseCall, const DeclRefExpr *EraseArg,
+ const UseAfterErase &Use, ClangTidyCheck *Check) {
+ const auto UseLoc = Use.DeclRef->getExprLoc();
+ const auto EraseLoc = EraseCall->getExprLoc();
+
+ Check->diag(UseLoc, "operation %0 is used after it was erased")
+ << EraseArg->getDecl();
+ Check->diag(EraseLoc, "operation erased here", DiagnosticIDs::Note);
+ if (Use.EvaluationOrderUndefined) {
+ Check->diag(UseLoc,
+ "the use and erase are unsequenced, i.e. there is no guarantee "
+ "about the order in which they are evaluated",
+ DiagnosticIDs::Note);
+ } else if (Use.UseHappensInLaterLoopIteration) {
+ Check->diag(UseLoc,
+ "the use happens in a later loop iteration than the erase",
+ DiagnosticIDs::Note);
+ }
+}
+
+void MlirUseAfterEraseCheck::registerMatchers(MatchFinder *Finder) {
+ // The reference to the operation that is being erased.
+ const auto Arg = declRefExpr(to(varDecl())).bind("arg");
+
+ // A "derived" op is a value of a subclass of `mlir::OpState` (e.g.
+ // `mlir::func::FuncOp`) that wraps an `mlir::Operation *`. The underlying
+ // operation can be obtained via the overloaded `operator->` or via
+ // `getOperation()`, both of which are declared in `mlir::OpState`.
+ const auto OfOpState = ofClass(isSameOrDerivedFrom("::mlir::OpState"));
+ const auto UnwrapDerivedOp = expr(anyOf(
+ cxxMemberCallExpr(
+ on(ignoringParenImpCasts(Arg)),
+ callee(cxxMethodDecl(hasName("getOperation"), OfOpState))),
+ cxxOperatorCallExpr(hasOverloadedOperatorName("->"),
+ hasArgument(0, ignoringParenImpCasts(Arg)),
+ callee(cxxMethodDecl(OfOpState)))));
+
+ // A reference to the operation, either directly (a variable of type
+ // `mlir::Operation *`) or through a derived op that is unwrapped to the
+ // underlying `mlir::Operation *`.
+ const auto OpRef = expr(ignoringParenImpCasts(anyOf(Arg, UnwrapDerivedOp)));
+
+ // Erasure through `mlir::Operation::erase` and `mlir::Operation::destroy`
+ // member functions.
+ const auto MemberErase = cxxMemberCallExpr(
+ on(OpRef), callee(cxxMethodDecl(hasAnyName("erase", "destroy"),
+ ofClass(hasName("::mlir::Operation")))));
+
+ // Erasure through `mlir::RewriterBase`. The operation that gets invalidated
+ // is always the first argument of these methods.
+ const auto RewriterErase = cxxMemberCallExpr(
+ callee(
+ cxxMethodDecl(hasAnyName("eraseOp", "eraseOpResults", "replaceOp",
+ "replaceOpWithNewOp"),
+ ofClass(isSameOrDerivedFrom("::mlir::RewriterBase")))),
+ hasArgument(0, OpRef));
+
+ Finder->addMatcher(
+ traverse(TK_AsIs,
+ expr(anyOf(MemberErase, RewriterErase),
+ anyOf(hasAncestor(compoundStmt(hasParent(
+ lambdaExpr().bind("containing-lambda")))),
+ hasAncestor(functionDecl().bind("containing-func"))))
+ .bind("erase-call")),
+ this);
+}
+
+void MlirUseAfterEraseCheck::check(const MatchFinder::MatchResult &Result) {
+ const auto *EraseCall = Result.Nodes.getNodeAs<Expr>("erase-call");
+ const auto *Arg = Result.Nodes.getNodeAs<DeclRefExpr>("arg");
+ const auto *ContainingLambda =
+ Result.Nodes.getNodeAs<LambdaExpr>("containing-lambda");
+ const auto *ContainingFunc =
+ Result.Nodes.getNodeAs<FunctionDecl>("containing-func");
+
+ // Only track operations that are stored in a local variable.
+ if (!Arg->getDecl()->getDeclContext()->isFunctionOrMethod())
+ return;
+
+ // Collect the code block that could use the operation after it was erased.
+ SmallVector<Stmt *> CodeBlocks;
+ if (ContainingLambda)
+ CodeBlocks.push_back(ContainingLambda->getBody());
+ else if (ContainingFunc)
+ CodeBlocks.push_back(ContainingFunc->getBody());
+
+ for (Stmt *CodeBlock : CodeBlocks) {
+ UseAfterEraseFinder Finder(Result.Context);
+ if (auto Use = Finder.find(CodeBlock, EraseCall, Arg))
+ emitDiagnostic(EraseCall, Arg, *Use, this);
+ }
+}
+
+} // namespace clang::tidy::llvm_check
diff --git a/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h
new file mode 100644
index 0000000000000..52574ac76fdf9
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/llvm/MlirUseAfterEraseCheck.h
@@ -0,0 +1,37 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_MLIRUSEAFTERERASECHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_MLIRUSEAFTERERASECHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::llvm_check {
+
+/// Detects uses of an ``mlir::Operation`` after it has been erased, either
+/// directly through ``Operation::erase``/``Operation::destroy``, through
+/// ``mlir::RewriterBase`` helpers such as ``eraseOp`` and ``replaceOp``,
+/// or through derived ops of ``mlir::OpState`` that provide are wrappers
+/// around an ``mlir::Operation``.
+///
+/// For the user-facing documentation see:
+/// https://clang.llvm.org/extra/clang-tidy/checks/llvm/mlir-use-after-erase.html
+class MlirUseAfterEraseCheck : public ClangTidyCheck {
+public:
+ MlirUseAfterEraseCheck(StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context) {}
+ void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+ void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+ return LangOpts.CPlusPlus;
+ }
+};
+
+} // namespace clang::tidy::llvm_check
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_MLIRUSEAFTERERASECHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 69c3bcf67b8db..efd5444633cd8 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -97,6 +97,15 @@ Improvements to clang-tidy
New checks
^^^^^^^^^^
+- New :doc:`llvm-mlir-use-after-erase
+ <clang-tidy/checks/llvm/mlir-use-after-erase>` check.
+
+ Detects uses of an ``mlir::Operation`` after it has been erased, either
+ directly through ``Operation::erase``/``Operation::destroy``, through
+ ``mlir::RewriterBase`` helpers such as ``eraseOp`` and ``replaceOp``,
+ or through derived ops of ``mlir::OpState`` that are wrappers around
+ an ``mlir::Operation``.
+
New check aliases
^^^^^^^^^^^^^^^^^
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index 2a44dc78fbc89..7013c5f5e7510 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -246,6 +246,7 @@ Clang-Tidy Checks
:doc:`llvm-formatv-string <llvm/formatv-string>`,
:doc:`llvm-header-guard <llvm/header-guard>`,
:doc:`llvm-include-order <llvm/include-order>`, "Yes"
+ :doc:`llvm-mlir-use-after-erase <llvm/mlir-use-after-erase>`,
:doc:`llvm-namespace-comment <llvm/namespace-comment>`,
:doc:`llvm-prefer-isa-or-dyn-cast-in-conditionals <llvm/prefer-isa-or-dyn-cast-in-conditionals>`, "Yes"
:doc:`llvm-prefer-register-over-unsigned <llvm/prefer-register-over-unsigned>`, "Yes"
diff --git a/clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst b/clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst
new file mode 100644
index 0000000000000..b0a05453600ea
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/llvm/mlir-use-after-erase.rst
@@ -0,0 +1,39 @@
+.. title:: clang-tidy - llvm-mlir-use-after-erase
+
+llvm-mlir-use-after-erase
+=========================
+
+Detects uses of an ``mlir::Operation`` after it has been erased.
+
+Once an operation has been erased its memory is no longer valid, so any
+subsequent access to it is a use-after-free. This check flags such uses when
+the operation is invalidated through:
+
+- ``mlir::Operation`` member functions ``erase()`` and ``destroy()``, or
+- ``mlir::RewriterBase`` helpers ``eraseOp()``, ``eraseOpResults()``,
+ ``replaceOp()`` and ``replaceOpWithNewOp()``.
+- ``mlir::OpState`` wrappers that provide ``operator->`` or ``getOperation()``.
+ This includes derived ops from ``mlir::Op``, which inherits ``mlir::OpState``.
+
+The analysis is control-flow aware: only uses that can actually be reached
+after the erasing call are reported, and reassigning the variable to a new
+operation clears the diagnostic.
+
+.. code-block:: c++
+
+ void example(mlir::RewriterBase &rewriter, mlir::Operation *op) {
+ rewriter.eraseOp(op);
+ op->dump(); // warning: operation 'op' is used after it was erased
+ }
+
+Uses that happen *before* the operation is erased, or after it has been reassigned, are not reported:
+
+.. code-block:: c++
+
+ void ok(mlir::Operation *op) {
+ op->dump(); // fine
+ op->erase();
+
+ op = ...;
+ op->dump(); // fine, op is reassigned to a new operation
+ }
diff --git a/clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp b/clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp
new file mode 100644
index 0000000000000..f4cfdb5873fe7
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/llvm/mlir-use-after-erase.cpp
@@ -0,0 +1,476 @@
+// RUN: %check_clang_tidy %s llvm-mlir-use-after-erase %t
+
+namespace mlir {
+
+class BitVector {};
+class ValueRange {};
+class Location {};
+
+class Operation {
+public:
+ void destroy();
+ void erase();
+
+ void dump();
+ Location getLoc() {
+ return Location{};
+ }
+};
+
+class OpState {
+public:
+ Operation *operator->() const { return state; }
+ Operation *getOperation() { return state; }
+
+private:
+ Operation *state;
+};
+class Op : public OpState {};
+
+class Builder {};
+class OpBuilder : public Builder {};
+class RewriterBase : public OpBuilder {
+public:
+ virtual ~RewriterBase() = default;
+
+ virtual void eraseOp(Operation *op);
+ Operation *eraseOpResults(Operation *op, const BitVector &eraseIndices);
+
+ virtual void replaceOp(Operation *op, ValueRange newValues);
+ virtual void replaceOp(Operation *op, Operation *newOp);
+ template <typename OpTy, typename... Args>
+ OpTy replaceOpWithNewOp(Operation *op, Args &&...args);
+};
+class PatternRewriter : public RewriterBase {};
+
+} // namespace mlir
+
+namespace test {
+class MyOp : public mlir::Op {
+};
+
+
+} // namespace test
+
+namespace {
+
+void consume([[maybe_unused]] mlir::Operation *op) {
+}
+
+struct NoReturnDestructor {
+ [[noreturn]] ~NoReturnDestructor();
+};
+#define FATAL() NoReturnDestructor()
+
+// A type that happens to have erase() / destroy() methods but is unrelated to mlir::Operation
+class NotAnOperation {
+public:
+ void erase() {}
+ void destroy() {}
+ void dump() {}
+};
+
+// A type that exposes operator-> / getOperation() returning mlir::Operation*
+// but is not derived from mlir::OpState
+class NotAnOp {
+public:
+ mlir::Operation* operator->() const { return op; }
+ mlir::Operation* getOperation() const { return op; }
+private:
+ mlir::Operation* op;
+};
+
+// A type that has RewriterBase-like method names but is not derived from mlir::RewriterBase
+class NotARewriter {
+public:
+ void eraseOp(mlir::Operation *op) {}
+ void replaceOp(mlir::Operation *op, mlir::Operation *newOp) {}
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////////////
+// General tests
+
+void useAfterDestroy() {
+ mlir::Operation* op{};
+ op->destroy();
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here
+}
+
+void useAfterErase() {
+ mlir::Operation* op{};
+ op->erase();
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here
+}
+
+void useAfterRewriterErase() {
+ mlir::RewriterBase rewriter{};
+ mlir::Operation* op{};
+ rewriter.eraseOp(op);
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here
+}
+
+void useAfterRewriterEraseOpResults() {
+ mlir::RewriterBase rewriter{};
+ mlir::Operation* op{};
+ rewriter.eraseOpResults(op, {});
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here
+}
+
+void useAfterRewriterReplaceOp() {
+ mlir::RewriterBase rewriter{};
+ {
+ mlir::Operation* op{};
+ rewriter.replaceOp(op, {});
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:18: note: operation erased here
+ }
+ {
+ mlir::Operation* op{};
+ mlir::Operation* op2{};
+ rewriter.replaceOp(op, op2);
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:18: note: operation erased here
+ }
+}
+
+void useAfterRewriterReplaceOpWithNewOp() {
+ mlir::RewriterBase rewriter{};
+ mlir::Operation* op{};
+ rewriter.replaceOpWithNewOp<mlir::Operation>(op);
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here
+}
+
+void useAfterPatternRewriterErase() {
+ mlir::PatternRewriter rewriter{};
+ mlir::Operation* op{};
+ rewriter.eraseOp(op);
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here
+}
+
+void useAfterDerivedArrowErase() {
+ test::MyOp op{};
+ op->erase();
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here
+}
+
+void useAfterDerivedGetOperationErase() {
+ test::MyOp op{};
+ op.getOperation()->erase();
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:24: note: operation erased here
+}
+
+void useAfterRewriterEraseDerived() {
+ mlir::RewriterBase rewriter{};
+ test::MyOp op{};
+ rewriter.eraseOp(op.getOperation());
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:14: note: operation erased here
+}
+
+void onlyFlagOneUseAfterErase() {
+ mlir::Operation* op{};
+ op->erase();
+ op->dump(); // A warning should only be emitted for one use-after-erase
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here
+ op->dump();
+}
+
+void eraseAfterErase() {
+ mlir::Operation* op{};
+ op->erase();
+ op->erase(); // Erase-after-erase also counts as a use
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here
+}
+
+void useInCall() {
+ mlir::Operation* op{};
+ op->erase();
+ consume(op);
+ // CHECK-MESSAGES: :[[@LINE-1]]:13: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here
+}
+
+void useParameterAfterErase(mlir::Operation* op) {
+ op->erase();
+ op->dump(); // The erased operation may be a function parameter
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: operation erased here
+}
+
+struct Container {
+ void useAfterEraseInMemberFunction() {
+ mlir::Operation* op{};
+ op->erase();
+ op->dump(); // The check also works in member functions
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:13: note: operation erased here
+ }
+};
+
+void useAfterEraseInLambda() {
+ [] {
+ mlir::Operation* op{};
+ op->erase();
+ op->dump(); // The check also works in lambdas
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-3]]:13: note: operation erased here
+ }();
+}
+
+// Using an operation before it is erased is fine
+void useBeforeErase() {
+ {
+ mlir::Operation* op{};
+ op->dump();
+ op->destroy();
+ }
+ {
+ mlir::PatternRewriter rewriter{};
+ mlir::Operation* op{};
+ op->dump();
+ rewriter.eraseOp(op);
+ }
+}
+
+void derivedUseBeforeErase() {
+ test::MyOp op{};
+ op->dump(); // Use before erase is fine
+ op->erase();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Tests involving control flow
+
+void useAndEraseInLoop(int size) {
+ mlir::Operation* op{};
+ for (int i = 0; i < size; ++i) {
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE+2]]:13: note: operation erased here
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: the use happens in a later loop iteration than the erase
+ op->erase();
+ }
+}
+
+void derivedUseAndEraseInLoop(int size) {
+ test::MyOp op{};
+ for (int i = 0; i < size; ++i) {
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE+2]]:13: note: operation erased here
+ // CHECK-MESSAGES: :[[@LINE-3]]:9: note: the use happens in a later loop iteration than the erase
+ op->erase();
+ }
+}
+
+void declaredInLoop() {
+ for (int i = 0; i < 10; ++i) {
+ mlir::Operation* op{};
+ op->dump();
+ op->erase(); // No warning if 'op' is declared inside the loop, because it is a fresh operation on every iteration
+ }
+}
+
+void returnAfterErase(int i) {
+ mlir::Operation* op{};
+ for (int j = 0; j < 10; ++j) {
+ op->dump();
+ if (i > 0) {
+ op->erase(); // Don't warn if we return after the erase
+ return;
+ }
+ }
+}
+
+void differentBranches(int i) {
+ mlir::Operation* op{};
+ if (i > 0) {
+ op->erase();
+ } else {
+ op->dump(); // Don't warn if the use is in a different branch from the erase
+ }
+}
+
+void differentBranchesRewriter(int i) {
+ mlir::PatternRewriter rewriter{};
+ mlir::Operation* op{};
+ if (i > 0) {
+ rewriter.eraseOp(op);
+ } else {
+ op->dump(); // Don't warn if the use is in a different branch from the erase
+ }
+}
+
+void switchFallthrough(int i) {
+ mlir::Operation* op{};
+ switch (i) {
+ case 1:
+ op->erase();
+ case 2: // A fallthrough in a switch statement causes a warning
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:9: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-4]]:13: note: operation erased here
+ break;
+ }
+}
+
+void noReturnDestructorAfterErase(bool cond, mlir::Operation* other) {
+ mlir::Operation* op{};
+ op->erase();
+ if (cond) {
+ FATAL(); // [[noreturn]] destructor
+ } else {
+ op = other;
+ }
+ op->dump(); // only reachable through the branch that reassigns 'op', so no warning should be emitted
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Tests for reinitializations
+
+void reassignAfterErase(mlir::Operation* other) {
+ mlir::Operation* op{};
+ op->erase();
+ op = other; // Reassignment to a new operation makes the later use safe
+ op->dump();
+}
+
+void derivedReassignAfterErase(test::MyOp other) {
+ test::MyOp op{};
+ op->erase();
+ op = other; // Reassignment to a new operation makes the later use safe
+ op->dump();
+}
+
+void conditionalReassign(int i, mlir::Operation* other) {
+ mlir::Operation* op{};
+ op->erase();
+ if (i > 0) {
+ op = other; // Reassignment is not guaranteed to happen before the use, so we still warn
+ }
+ op->dump();
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-6]]:9: note: operation erased here
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Tests related to order of evaluation within expressions
+
+void sequencingOfEraseAndUse() {
+ const auto fn = [](mlir::Location, mlir::Operation) {
+ };
+ mlir::RewriterBase rewriter{};
+ mlir::Operation* op{};
+ fn(op->getLoc(), rewriter.replaceOpWithNewOp<mlir::Operation>(op));
+ // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: operation 'op' is used after it was erased [llvm-mlir-use-after-erase]
+ // CHECK-MESSAGES: :[[@LINE-2]]:31: note: operation erased here
+ // CHECK-MESSAGES: :[[@LINE-3]]:8: note: the use and erase are unsequenced, i.e. there is no guarantee about the order in which they are evaluated
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Tests for types and operations that are not tracked
+
+void eraseOnNonOperation() {
+ NotAnOperation* obj{};
+ obj->erase(); // erase() on an unrelated type are not flagged
+ obj->dump();
+}
+
+void destroyOnNonOperation() {
+ NotAnOperation* obj{};
+ obj->destroy(); // destroy() on an unrelated type is not flagged
+ obj->dump();
+}
+
+void eraseOpOnNonRewriter() {
+ NotARewriter rewriter{};
+ mlir::Operation* op{};
+ rewriter.eraseOp(op); // eraseOp() on a type not derived from RewriterBase is not flagged
+ op->dump();
+}
+
+void replaceOpOnNonRewriter() {
+ NotARewriter rewriter{};
+ mlir::Operation* op{};
+ mlir::Operation* newOp{};
+ rewriter.replaceOp(op, newOp); // replaceOp() on a type not derived from RewriterBase is not flagged
+ op->dump();
+}
+
+void eraseOnNonOpState() {
+ NotAnOp op{};
+ op->erase(); // operator-> on a type not derived from mlir::OpState is not flagged
+ op->dump();
+}
+
+void eraseGetOperationOnNonOpState() {
+ NotAnOp op{};
+ op.getOperation()->erase(); // getOperation() on a type not derived from mlir::OpState is not flagged
+ op->dump();
+}
+
+mlir::Operation* globalOp{}; // Operations that are not local variables are not tracked
+void eraseGlobalOperation() {
+ globalOp->erase();
+ globalOp->dump();
+}
+
+struct OperationHolder {
+ mlir::Operation* op; // Operations stored in member fields are not tracked
+ void eraseAndUse() {
+ op->erase();
+ op->dump();
+ }
+};
+
+void differentOperations() {
+ mlir::Operation* op1{};
+ mlir::Operation* op2{};
+ op1->erase();
+ op2->dump(); // Erasing one operation doesn't flag the use of a different operation
+}
+
+void replacementOperationNotErased() {
+ mlir::RewriterBase rewriter{};
+ mlir::Operation* op{};
+ mlir::Operation* newOp{};
+ rewriter.replaceOp(op, newOp); // The replacement operation passed to replaceOp is not the erased one
+ newOp->dump();
+}
+
+void decltypeIsNotUse() {
+ mlir::Operation* op{};
+ op->erase();
+ using OpType = decltype(op); // Using decltype on an erased operation is not flagged
+ OpType op2{};
+ op2->dump();
+}
+
+void unevaluatedIsNotUse() {
+ mlir::Operation* op{};
+ op->erase();
+ static_assert(sizeof(op) == sizeof(void*)); // Unevaluated, so not a real use
+}
More information about the cfe-commits
mailing list