diff --git a/mlir/include/mlir/Analysis/DataFlow/LivenessAnalysis.h b/mlir/include/mlir/Analysis/DataFlow/LivenessAnalysis.h new file mode 100644 --- /dev/null +++ b/mlir/include/mlir/Analysis/DataFlow/LivenessAnalysis.h @@ -0,0 +1,109 @@ +//===- LivenessAnalysis.h - Liveness analysis -------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// This file implements liveness analysis using the sparse backward data-flow +// analysis framework. Theoretically, liveness analysis assigns liveness to each +// (value, program point) pair in the program and it is thus a dense analysis. +// However, since values are immutable in MLIR, a sparse analysis, which will +// assign liveness to each value in the program, suffices here. +// +// Liveness analysis has many applications. It can be used to avoid the +// computation of extraneous operations that have no effect on the memory or the +// final output of a program. It can also be used to optimize register +// allocation. Both of these applications help achieve one very important goal: +// reducing runtime. +// +//===----------------------------------------------------------------------===// + +#ifndef MLIR_ANALYSIS_DATAFLOW_LIVENESSANALYSIS_H +#define MLIR_ANALYSIS_DATAFLOW_LIVENESSANALYSIS_H + +#include +#include + +namespace mlir::dataflow { + +//===----------------------------------------------------------------------===// +// Liveness +//===----------------------------------------------------------------------===// + +/// This lattice represents, for a given value, whether or not it is "live". +/// +/// A value is considered "live" iff it: +/// (1) has memory effects OR +/// (2) is returned by a public function OR +/// (3) is used to compute a value of type (1) or (2). +/// It is also to be noted that a value could be of multiple types (1/2/3) at +/// the same time. +/// +/// A value "has memory effects" iff it: +/// (1.a) is an operand of an op with memory effects OR +/// (1.b) is a non-forwarded branch operand and a block where its op could +/// take the control has an op with memory effects. +/// +/// A value `A` is said to be "used to compute" value `B` iff `B` cannot be +/// computed in the absence of `A`. Thus, in this implementation, we say that +/// value `A` is used to compute value `B` iff: +/// (3.a) `B` is a result of an op with operand `A` OR +/// (3.b) `A` is used to compute some value `C` and `C` is used to compute +/// `B`. +struct Liveness : public AbstractSparseLattice { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(Liveness) + using AbstractSparseLattice::AbstractSparseLattice; + + void print(raw_ostream &os) const override; + + ChangeResult markLive(); + + ChangeResult meet(const AbstractSparseLattice &other) override; + + // At the beginning of the analysis, everything is marked "not live" and as + // the analysis progresses, values are marked "live" if they are found to be + // live. + bool isLive = false; +}; + +//===----------------------------------------------------------------------===// +// LivenessAnalysis +//===----------------------------------------------------------------------===// + +/// An analysis that, by going backwards along the dataflow graph, annotates +/// each value with a boolean storing true iff it is "live". +class LivenessAnalysis : public SparseBackwardDataFlowAnalysis { +public: + using SparseBackwardDataFlowAnalysis::SparseBackwardDataFlowAnalysis; + + void visitOperation(Operation *op, ArrayRef operands, + ArrayRef results) override; + + void visitBranchOperand(OpOperand &operand) override; + + void setToExitState(Liveness *lattice) override; +}; + +//===----------------------------------------------------------------------===// +// RunLivenessAnalysis +//===----------------------------------------------------------------------===// + +/// Runs liveness analysis on the IR defined by `op`. +struct RunLivenessAnalysis { +public: + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(RunLivenessAnalysis) + + RunLivenessAnalysis(Operation *op); + + const Liveness *getLiveness(Value val); + +private: + /// Stores the result of the liveness analysis that was run. + DataFlowSolver solver; +}; + +} // end namespace mlir::dataflow + +#endif // MLIR_ANALYSIS_DATAFLOW_LIVENESSANALYSIS_H diff --git a/mlir/lib/Analysis/CMakeLists.txt b/mlir/lib/Analysis/CMakeLists.txt --- a/mlir/lib/Analysis/CMakeLists.txt +++ b/mlir/lib/Analysis/CMakeLists.txt @@ -13,6 +13,7 @@ DataFlow/DeadCodeAnalysis.cpp DataFlow/DenseAnalysis.cpp DataFlow/IntegerRangeAnalysis.cpp + DataFlow/LivenessAnalysis.cpp DataFlow/SparseAnalysis.cpp ) @@ -34,6 +35,7 @@ DataFlow/DeadCodeAnalysis.cpp DataFlow/DenseAnalysis.cpp DataFlow/IntegerRangeAnalysis.cpp + DataFlow/LivenessAnalysis.cpp DataFlow/SparseAnalysis.cpp ADDITIONAL_HEADER_DIRS diff --git a/mlir/lib/Analysis/DataFlow/LivenessAnalysis.cpp b/mlir/lib/Analysis/DataFlow/LivenessAnalysis.cpp new file mode 100644 --- /dev/null +++ b/mlir/lib/Analysis/DataFlow/LivenessAnalysis.cpp @@ -0,0 +1,190 @@ +//===- LivenessAnalysis.cpp - Liveness analysis ---------------------------===// +// +// 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 + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::dataflow; + +//===----------------------------------------------------------------------===// +// Liveness +//===----------------------------------------------------------------------===// + +void Liveness::print(raw_ostream &os) const { + os << (isLive ? "live" : "not live"); +} + +ChangeResult Liveness::markLive() { + bool wasLive = isLive; + isLive = true; + return wasLive ? ChangeResult::NoChange : ChangeResult::Change; +} + +ChangeResult Liveness::meet(const AbstractSparseLattice &other) { + const auto *otherLiveness = reinterpret_cast(&other); + return otherLiveness->isLive ? markLive() : ChangeResult::NoChange; +} + +//===----------------------------------------------------------------------===// +// LivenessAnalysis +//===----------------------------------------------------------------------===// + +/// For every value, liveness analysis determines whether or not it is "live". +/// +/// A value is considered "live" iff it: +/// (1) has memory effects OR +/// (2) is returned by a public function OR +/// (3) is used to compute a value of type (1) or (2). +/// It is also to be noted that a value could be of multiple types (1/2/3) at +/// the same time. +/// +/// A value "has memory effects" iff it: +/// (1.a) is an operand of an op with memory effects OR +/// (1.b) is a non-forwarded branch operand and a block where its op could +/// take the control has an op with memory effects. +/// +/// A value `A` is said to be "used to compute" value `B` iff `B` cannot be +/// computed in the absence of `A`. Thus, in this implementation, we say that +/// value `A` is used to compute value `B` iff: +/// (3.a) `B` is a result of an op with operand `A` OR +/// (3.b) `A` is used to compute some value `C` and `C` is used to compute +/// `B`. + +void LivenessAnalysis::visitOperation(Operation *op, + ArrayRef operands, + ArrayRef results) { + // This marks values of type (1.a) liveness as "live". + if (!isMemoryEffectFree(op)) { + for (auto *operand : operands) + propagateIfChanged(operand, operand->markLive()); + } + + // This marks values of type (3) liveness as "live". + bool foundLiveResult = false; + for (const Liveness *r : results) { + if (r->isLive && !foundLiveResult) { + // It is assumed that each operand is used to compute each result of an + // op. Thus, if at least one result is live, each operand is live. + for (Liveness *operand : operands) + meet(operand, *r); + foundLiveResult = true; + } + addDependency(const_cast(r), op); + } +} + +void LivenessAnalysis::visitBranchOperand(OpOperand &operand) { + // We know (at the moment) and assume (for the future) that `operand` is a + // non-forwarded branch operand of an op of type `RegionBranchOpInterface`, + // `BranchOpInterface`, or `RegionBranchTerminatorOpInterface`. + Operation *op = operand.getOwner(); + assert((isa(op) || isa(op) || + isa(op)) && + "expected the op to be `RegionBranchOpInterface`, " + "`BranchOpInterface`, or `RegionBranchTerminatorOpInterface`"); + + // The lattices of the non-forwarded branch operands don't get updated like + // the forwarded branch operands or the non-branch operands. Thus they need + // to be handled separately. This is where we handle them. + + // This marks values of type (1.b) liveness as "live". A non-forwarded + // branch operand will be live if a block where its op could take the control + // has an op with memory effects. + // Populating such blocks in `blocks`. + SmallVector blocks; + if (isa(op)) { + // When the op is a `RegionBranchOpInterface`, like an `scf.for` or an + // `scf.index_switch` op, its branch operand controls the flow into this + // op's regions. + for (Region ®ion : op->getRegions()) { + for (Block &block : region) + blocks.push_back(&block); + } + } else if (isa(op)) { + // When the op is a `BranchOpInterface`, like a `cf.cond_br` or a + // `cf.switch` op, its branch operand controls the flow into this op's + // successors. + blocks = op->getSuccessors(); + } else { + // When the op is a `RegionBranchTerminatorOpInterface`, like a + // `scf.condition` op, its branch operand controls the flow into this op's + // parent's (which is a `RegionBranchOpInterface`'s) regions. + for (Region ®ion : op->getParentOp()->getRegions()) { + for (Block &block : region) + blocks.push_back(&block); + } + } + bool foundMemoryEffectingOp = false; + for (Block *block : blocks) { + if (foundMemoryEffectingOp) + break; + for (Operation &nestedOp : *block) { + if (!isMemoryEffectFree(&nestedOp)) { + Liveness *operandLiveness = getLatticeElement(operand.get()); + propagateIfChanged(operandLiveness, operandLiveness->markLive()); + foundMemoryEffectingOp = true; + break; + } + } + } + + // Now that we have checked for memory-effecting ops in the blocks of concern, + // we will simply visit the op with this non-forwarded operand to potentially + // mark it "live" due to type (1.a/3) liveness. + if (operand.getOperandNumber() > 0) + return; + SmallVector operandLiveness; + operandLiveness.push_back(getLatticeElement(operand.get())); + SmallVector resultsLiveness; + for (const Value result : op->getResults()) + resultsLiveness.push_back(getLatticeElement(result)); + visitOperation(op, operandLiveness, resultsLiveness); + + // We also visit the parent op with the parent's results and this operand if + // `op` is a `RegionBranchTerminatorOpInterface` because its non-forwarded + // operand depends on not only its memory effects/results but also on those of + // its parent's. + if (!isa(op)) + return; + Operation *parentOp = op->getParentOp(); + SmallVector parentResultsLiveness; + for (const Value parentResult : parentOp->getResults()) + parentResultsLiveness.push_back(getLatticeElement(parentResult)); + visitOperation(parentOp, operandLiveness, parentResultsLiveness); +} + +void LivenessAnalysis::setToExitState(Liveness *lattice) { + // This marks values of type (2) liveness as "live". + lattice->markLive(); +} + +//===----------------------------------------------------------------------===// +// RunLivenessAnalysis +//===----------------------------------------------------------------------===// + +RunLivenessAnalysis::RunLivenessAnalysis(Operation *op) { + SymbolTableCollection symbolTable; + + solver.load(); + solver.load(); + solver.load(symbolTable); + (void)solver.initializeAndRun(op); +} + +const Liveness *RunLivenessAnalysis::getLiveness(Value val) { + return solver.lookupState(val); +} diff --git a/mlir/test/Analysis/DataFlow/test-liveness-analysis.mlir b/mlir/test/Analysis/DataFlow/test-liveness-analysis.mlir new file mode 100644 --- /dev/null +++ b/mlir/test/Analysis/DataFlow/test-liveness-analysis.mlir @@ -0,0 +1,233 @@ +// RUN: mlir-opt -split-input-file -test-liveness-analysis %s 2>&1 | FileCheck %s + +// Positive test: Type (1.a) "is an operand of an op with memory effects" +// zero is live because it is stored in memory. +// CHECK-LABEL: test_tag: zero: +// CHECK-NEXT: result #0: live +func.func @test_1_type_1.a(%arg0: memref) { + %c0_i32 = arith.constant {tag = "zero"} 0 : i32 + memref.store %c0_i32, %arg0[] : memref + return +} + +// ----- + +// Positive test: Type (1.b) "is a non-forwarded branch operand and a block +// where its op could take the control has an op with memory effects" +// %arg2 is live because it can make the control go into a block with a memory +// effecting op. +// Note that if `visitBranchOperand()` was left empty, it would have been +// incorrectly marked as "not live". +// CHECK-LABEL: test_tag: br: +// CHECK-NEXT: operand #0: live +// CHECK-NEXT: operand #1: live +// CHECK-NEXT: operand #2: live +func.func @test_2_RegionBranchOpInterface_type_1.b(%arg0: memref, %arg1: memref, %arg2: i1) { + %c0_i32 = arith.constant 0 : i32 + cf.cond_br %arg2, ^bb1(%c0_i32 : i32), ^bb2(%c0_i32 : i32) {tag = "br"} +^bb1(%0 : i32): + memref.store %0, %arg0[] : memref + cf.br ^bb3 +^bb2(%1 : i32): + memref.store %1, %arg1[] : memref + cf.br ^bb3 +^bb3: + return +} + +// ----- + +// Positive test: Type (1.b) "is a non-forwarded branch operand and a block +// where its op could take the control has an op with memory effects" +// %arg0 is live because it can make the control go into a block with a memory +// effecting op. +// Note that if `visitBranchOperand()` was left empty, it would have been +// incorrectly marked as "not live". +// CHECK-LABEL: test_tag: flag: +// CHECK-NEXT: operand #0: live +func.func @test_3_BranchOpInterface_type_1.b(%arg0: i32, %arg1: memref, %arg2: memref) { + %c0_i32 = arith.constant 0 : i32 + cf.switch %arg0 : i32, [ + default: ^bb1, + 42: ^bb2 + ] {tag = "flag"} +^bb1: + memref.store %c0_i32, %arg1[] : memref + cf.br ^bb3 +^bb2: + memref.store %c0_i32, %arg2[] : memref + cf.br ^bb3 +^bb3: + return +} + +// ----- + +// Positive test: Type (2) "is returned by a public function" +// zero is live because it is returned by a public function. +// CHECK-LABEL: test_tag: zero: +// CHECK-NEXT: result #0: live +func.func @test_4_type_2() -> (f32){ + %0 = arith.constant {tag = "zero"} 0.0 : f32 + return %0 : f32 +} + +// ----- + +// Positive test: Type (3) "is used to compute a value of type (1) or (2)" +// %arg1 is live because the scf.while has a live result and %arg1 is a +// non-forwarded branch operand. +// Note that if `visitBranchOperand()` was left empty, it would have been +// incorrectly marked as "not live". +// %arg2 is live because it is forwarded to the live result of the scf.while +// op. +// Negative test: %arg3 is not live even though %arg1 and %arg2 are live +// because it is neither a non-forwarded branch operand nor a forwarded +// operand that forwards to a live value. It actually is a forwarded operand +// that forwards to a non-live value. +// CHECK-LABEL: test_tag: condition: +// CHECK-NEXT: operand #0: live +// CHECK-NEXT: operand #1: live +// CHECK-NEXT: operand #2: not live +func.func @test_5_RegionBranchTerminatorOpInterface_type_3(%arg0: memref, %arg1: i1) -> (i32) { + %c0_i32 = arith.constant 0 : i32 + %c1_i32 = arith.constant 1 : i32 + %0:2 = scf.while (%arg2 = %c0_i32, %arg3 = %c1_i32) : (i32, i32) -> (i32, i32) { + scf.condition(%arg1) {tag = "condition"} %arg2, %arg3 : i32, i32 + } do { + ^bb0(%arg2: i32, %arg3: i32): + scf.yield %arg2, %arg3 : i32, i32 + } + return %0#0 : i32 +} + +// ----- + +func.func private @private0(%0 : i32) -> i32 { + %1 = arith.addi %0, %0 {tag = "in_private0"} : i32 + func.return %1 : i32 +} + +// Positive test: Type (3) "is used to compute a value of type (1) or (2)" +// zero, ten, and one are live because they are used to decide the number of +// times the `for` loop executes, which in turn decides the value stored in +// memory. +// Note that if `visitBranchOperand()` was left empty, they would have been +// incorrectly marked as "not live". +// in_private0 and x are also live because they decide the value stored in +// memory. +// Negative test: y is not live even though the non-forwarded branch operand +// and x are live. +// CHECK-LABEL: test_tag: in_private0: +// CHECK-NEXT: operand #0: live +// CHECK-NEXT: operand #1: live +// CHECK-NEXT: result #0: live +// CHECK-LABEL: test_tag: zero: +// CHECK-NEXT: result #0: live +// CHECK-LABEL: test_tag: ten: +// CHECK-NEXT: result #0: live +// CHECK-LABEL: test_tag: one: +// CHECK-NEXT: result #0: live +// CHECK-LABEL: test_tag: x: +// CHECK-NEXT: result #0: live +// CHECK-LABEL: test_tag: y: +// CHECK-NEXT: result #0: not live +func.func @test_6_type_3(%arg0: memref) { + %c0 = arith.constant {tag = "zero"} 0 : index + %c10 = arith.constant {tag = "ten"} 10 : index + %c1 = arith.constant {tag = "one"} 1 : index + %x = arith.constant {tag = "x"} 0 : i32 + %y = arith.constant {tag = "y"} 1 : i32 + %0:2 = scf.for %arg1 = %c0 to %c10 step %c1 iter_args(%arg2 = %x, %arg3 = %y) -> (i32, i32) { + %1 = arith.addi %x, %x : i32 + %2 = func.call @private0(%1) : (i32) -> i32 + scf.yield %2, %arg3 : i32, i32 + } + memref.store %0#0, %arg0[] : memref + return +} + +// ----- + +func.func private @private1(%0 : i32) -> i32 { + %1 = func.call @private2(%0) : (i32) -> i32 + %2 = arith.muli %0, %1 {tag = "in_private1"} : i32 + func.return %2 : i32 +} + +func.func private @private2(%0 : i32) -> i32 { + %cond = arith.index_cast %0 {tag = "in_private2"} : i32 to index + %1 = scf.index_switch %cond -> i32 + case 1 { + %ten = arith.constant 10 : i32 + scf.yield %ten : i32 + } + case 2 { + %twenty = arith.constant 20 : i32 + scf.yield %twenty : i32 + } + default { + %thirty = arith.constant 30 : i32 + scf.yield %thirty : i32 + } + func.return %1 : i32 +} + +// Positive test: Type (3) "is used to compute a value of type (1) or (2)" +// in_private1, in_private2, and final are live because they are used to compute +// the value returned by this public function. +// CHECK-LABEL: test_tag: in_private1: +// CHECK-NEXT: operand #0: live +// CHECK-NEXT: operand #1: live +// CHECK-NEXT: result #0: live +// CHECK-LABEL: test_tag: in_private2: +// CHECK-NEXT: operand #0: live +// CHECK-NEXT: result #0: live +// CHECK-LABEL: test_tag: final: +// CHECK-NEXT: operand #0: live +// CHECK-NEXT: operand #1: live +// CHECK-NEXT: result #0: live +func.func @test_7_type_3(%arg: i32) -> (i32) { + %0 = func.call @private1(%arg) : (i32) -> i32 + %final = arith.muli %0, %arg {tag = "final"} : i32 + return %final : i32 +} + +// ----- + +// Negative test: None of the types (1), (2), or (3) +// zero is not live because it has no effect outside the program: it doesn't +// affect the memory or the program output. +// CHECK-LABEL: test_tag: zero: +// CHECK-NEXT: result #0: not live +// CHECK-LABEL: test_tag: one: +// CHECK-NEXT: result #0: live +func.func @test_8_negative() -> (f32){ + %0 = arith.constant {tag = "zero"} 0.0 : f32 + %1 = arith.constant {tag = "one"} 1.0 : f32 + return %1 : f32 +} + +// ----- + +// Negative test: None of the types (1), (2), or (3) +// %1 is not live because it has no effect outside the program: it doesn't +// affect the memory or the program output. Even though it is returned by the +// function `@private_1`, it is never used by the caller. +// Note that this test clearly shows how this liveness analysis utility differs +// from the existing liveness utility present at +// llvm-project/mlir/include/mlir/Analysis/Liveness.h. The latter marks %1 as +// live as it exists the block of function `@private_1`, simply because it is +// computed inside and returned by the block, irrespective of whether or not it +// is used by the caller. +// CHECK-LABEL: test_tag: one: +// CHECK: result #0: not live +func.func private @private_1() -> (i32, i32) { + %0 = arith.constant 0 : i32 + %1 = arith.addi %0, %0 {tag = "one"} : i32 + return %0, %1 : i32, i32 +} +func.func @test_9_negative() -> (i32) { + %0:2 = func.call @private_1() : () -> (i32, i32) + return %0#0 : i32 +} diff --git a/mlir/test/lib/Analysis/CMakeLists.txt b/mlir/test/lib/Analysis/CMakeLists.txt --- a/mlir/test/lib/Analysis/CMakeLists.txt +++ b/mlir/test/lib/Analysis/CMakeLists.txt @@ -15,6 +15,7 @@ DataFlow/TestDenseDataFlowAnalysis.cpp DataFlow/TestBackwardDataFlowAnalysis.cpp DataFlow/TestDenseBackwardDataFlowAnalysis.cpp + DataFlow/TestLivenessAnalysis.cpp EXCLUDE_FROM_LIBMLIR diff --git a/mlir/test/lib/Analysis/DataFlow/TestLivenessAnalysis.cpp b/mlir/test/lib/Analysis/DataFlow/TestLivenessAnalysis.cpp new file mode 100644 --- /dev/null +++ b/mlir/test/lib/Analysis/DataFlow/TestLivenessAnalysis.cpp @@ -0,0 +1,72 @@ +//===- TestLivenessAnalysis.cpp - Test liveness analysis ------------------===// +// +// 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::dataflow; + +namespace { + +struct TestLivenessAnalysisPass + : public PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestLivenessAnalysisPass) + + StringRef getArgument() const override { return "test-liveness-analysis"; } + + void runOnOperation() override { + auto &livenessAnalysis = getAnalysis(); + + Operation *op = getOperation(); + + raw_ostream &os = llvm::outs(); + + op->walk([&](Operation *op) { + auto tag = op->getAttrOfType("tag"); + if (!tag) + return; + os << "test_tag: " << tag.getValue() << ":\n"; + for (auto [index, operand] : llvm::enumerate(op->getOperands())) { + const Liveness *liveness = livenessAnalysis.getLiveness(operand); + assert(liveness && "expected a sparse lattice"); + os << " operand #" << index << ": "; + liveness->print(os); + os << "\n"; + } + for (auto [index, operand] : llvm::enumerate(op->getResults())) { + const Liveness *liveness = livenessAnalysis.getLiveness(operand); + assert(liveness && "expected a sparse lattice"); + os << " result #" << index << ": "; + liveness->print(os); + os << "\n"; + } + }); + } +}; +} // end anonymous namespace + +namespace mlir { +namespace test { +void registerTestLivenessAnalysisPass() { + PassRegistration(); +} +} // end namespace test +} // end namespace mlir diff --git a/mlir/tools/mlir-opt/mlir-opt.cpp b/mlir/tools/mlir-opt/mlir-opt.cpp --- a/mlir/tools/mlir-opt/mlir-opt.cpp +++ b/mlir/tools/mlir-opt/mlir-opt.cpp @@ -104,6 +104,7 @@ void registerTestLinalgElementwiseFusion(); void registerTestLinalgGreedyFusion(); void registerTestLinalgTransforms(); +void registerTestLivenessAnalysisPass(); void registerTestLivenessPass(); void registerTestLoopFusion(); void registerTestCFGLoopInfoPass(); @@ -227,6 +228,7 @@ mlir::test::registerTestLinalgElementwiseFusion(); mlir::test::registerTestLinalgGreedyFusion(); mlir::test::registerTestLinalgTransforms(); + mlir::test::registerTestLivenessAnalysisPass(); mlir::test::registerTestLivenessPass(); mlir::test::registerTestLoopFusion(); mlir::test::registerTestCFGLoopInfoPass();