//===- OpenACCUtilsLoopTest.cpp - Unit tests for OpenACC loop utilities --===// // // 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 "mlir/Dialect/OpenACC/OpenACCUtilsLoop.h" #include "mlir/Dialect/Affine/IR/AffineOps.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" #include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/MemRef/IR/MemRef.h" #include "mlir/Dialect/OpenACC/OpenACC.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/IR/BuiltinOps.h" #include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/Diagnostics.h" #include "mlir/IR/MLIRContext.h" #include "mlir/IR/OwningOpRef.h" #include "mlir/IR/PatternMatch.h" #include "mlir/IR/Value.h" #include "gtest/gtest.h" using namespace mlir; using namespace mlir::acc; //===----------------------------------------------------------------------===// // Test Fixture //===----------------------------------------------------------------------===// class OpenACCUtilsLoopTest : public ::testing::Test { protected: OpenACCUtilsLoopTest() : b(&context), loc(UnknownLoc::get(&context)) { context.loadDialect(); } /// Helper to create an index constant Value createIndexConstant(int64_t value) { return arith::ConstantIndexOp::create(b, loc, value); } /// Helper to create an i32 constant Value createI32Constant(int32_t value) { return arith::ConstantIntOp::create(b, loc, b.getI32Type(), value); } /// Helper to create a simple acc.loop with the given bounds. /// Preserves the builder's insertion point. acc::LoopOp createLoopOp(ValueRange lbs, ValueRange ubs, ValueRange steps, bool inclusiveUpperbound = true) { OpBuilder::InsertionGuard guard(b); auto loopOp = acc::LoopOp::create(b, loc, lbs, ubs, steps, acc::LoopParMode::loop_independent); // Set inclusive upper bound attribute SmallVector inclusiveFlags(lbs.size(), inclusiveUpperbound); loopOp.setInclusiveUpperboundAttr(b.getDenseBoolArrayAttr(inclusiveFlags)); // Add body block with IV arguments and yield Region ®ion = loopOp.getRegion(); Block *block = b.createBlock(®ion, region.begin()); for (Value lb : lbs) block->addArgument(lb.getType(), loc); b.setInsertionPointToEnd(block); acc::YieldOp::create(b, loc); return loopOp; } /// Helper to create an unstructured acc.loop with multiple blocks and ops. /// Preserves the builder's insertion point. acc::LoopOp createUnstructuredLoopOp(ValueRange lbs, ValueRange ubs, ValueRange steps) { OpBuilder::InsertionGuard guard(b); auto loopOp = acc::LoopOp::create(b, loc, lbs, ubs, steps, acc::LoopParMode::loop_independent); loopOp.setInclusiveUpperboundAttr( b.getDenseBoolArrayAttr(SmallVector(lbs.size(), true))); loopOp.setUnstructuredAttr(b.getUnitAttr()); // Create 4 blocks with control flow to test proper replication Region ®ion = loopOp.getRegion(); Block *entry = b.createBlock(®ion, region.begin()); Block *thenBlock = b.createBlock(®ion, region.end()); Block *elseBlock = b.createBlock(®ion, region.end()); Block *exitBlock = b.createBlock(®ion, region.end()); // Entry block: create a condition and conditional branch b.setInsertionPointToEnd(entry); Value cond = arith::ConstantOp::create(b, loc, b.getI1Type(), b.getBoolAttr(true)); cf::CondBranchOp::create(b, loc, cond, thenBlock, elseBlock); // Then block: create an arith op and branch to exit b.setInsertionPointToEnd(thenBlock); Value c1 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(1)); Value c2 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(2)); arith::AddIOp::create(b, loc, c1, c2); cf::BranchOp::create(b, loc, exitBlock); // Else block: create a different arith op and branch to exit b.setInsertionPointToEnd(elseBlock); Value c3 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(3)); Value c4 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(4)); arith::MulIOp::create(b, loc, c3, c4); cf::BranchOp::create(b, loc, exitBlock); // Exit block: yield b.setInsertionPointToEnd(exitBlock); acc::YieldOp::create(b, loc); return loopOp; } /// Create a module with a function and set the insertion point in it std::pair, func::FuncOp> createModuleWithFunc() { OwningOpRef module = ModuleOp::create(loc); b.setInsertionPointToStart(module->getBody()); auto funcType = b.getFunctionType({}, {}); auto funcOp = func::FuncOp::create(b, loc, "test_func", funcType); Block *entryBlock = funcOp.addEntryBlock(); b.setInsertionPointToStart(entryBlock); return {std::move(module), funcOp}; } /// Create a module with a function that has arguments std::pair, func::FuncOp> createModuleWithFuncArgs(TypeRange argTypes) { OwningOpRef module = ModuleOp::create(loc); b.setInsertionPointToStart(module->getBody()); auto funcType = b.getFunctionType(argTypes, {}); auto funcOp = func::FuncOp::create(b, loc, "test_func", funcType); Block *entryBlock = funcOp.addEntryBlock(); b.setInsertionPointToStart(entryBlock); return {std::move(module), funcOp}; } /// Helper to extract constant index value from a Value std::optional getConstantIndex(Value v) { if (auto constOp = v.getDefiningOp()) return constOp.value(); if (auto constOp = v.getDefiningOp()) { if (auto intAttr = dyn_cast(constOp.getValue())) return intAttr.getInt(); } return std::nullopt; } MLIRContext context; IRRewriter b; Location loc; }; //===----------------------------------------------------------------------===// // convertACCLoopToSCFFor Tests //===----------------------------------------------------------------------===// TEST_F(OpenACCUtilsLoopTest, ConvertSimpleLoopToSCFFor) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0}, {c10}, {c1}); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/false); ASSERT_TRUE(forOp); // Verify IV type is index EXPECT_TRUE(forOp.getInductionVar().getType().isIndex()); // Verify bounds: lb=0, ub=11 (folded from 10+1), step=1 auto lbConst = getConstantIndex(forOp.getLowerBound()); ASSERT_TRUE(lbConst.has_value()); EXPECT_EQ(*lbConst, 0); auto ubConst = getConstantIndex(forOp.getUpperBound()); ASSERT_TRUE(ubConst.has_value()); EXPECT_EQ(*ubConst, 11); // inclusive 10 becomes exclusive 11 auto stepConst = getConstantIndex(forOp.getStep()); ASSERT_TRUE(stepConst.has_value()); EXPECT_EQ(*stepConst, 1); // Verify the body has a yield terminator EXPECT_TRUE(isa(forOp.getBody()->getTerminator())); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopWithI32Bounds) { auto [module, funcOp] = createModuleWithFunc(); Value lb = createI32Constant(0); Value ub = createI32Constant(100); Value step = createI32Constant(1); acc::LoopOp loopOp = createLoopOp({lb}, {ub}, {step}); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/false); ASSERT_TRUE(forOp); // IV type should be converted to index EXPECT_TRUE(forOp.getInductionVar().getType().isIndex()); // Bounds should be cast to index type EXPECT_TRUE(forOp.getLowerBound().getType().isIndex()); EXPECT_TRUE(forOp.getUpperBound().getType().isIndex()); EXPECT_TRUE(forOp.getStep().getType().isIndex()); // Verify the body has a yield terminator EXPECT_TRUE(isa(forOp.getBody()->getTerminator())); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopWithNonConstantBounds) { auto [module, funcOp] = createModuleWithFuncArgs({b.getIndexType(), b.getIndexType()}); Value lb = funcOp.getArgument(0); Value ub = funcOp.getArgument(1); Value step = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({lb}, {ub}, {step}); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/false); ASSERT_TRUE(forOp); // Normalized: lb=0, step=1, ub=tripCount (computed from dynamic bounds) auto lbConst = getConstantIndex(forOp.getLowerBound()); ASSERT_TRUE(lbConst.has_value()); EXPECT_EQ(*lbConst, 0); auto stepConst = getConstantIndex(forOp.getStep()); ASSERT_TRUE(stepConst.has_value()); EXPECT_EQ(*stepConst, 1); EXPECT_FALSE(getConstantIndex(forOp.getUpperBound()).has_value()); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopToSCFForWithCollapse) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0, c0}, {c10, c10}, {c1, c1}); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/true); ASSERT_TRUE(forOp); // With collapse, there should be NO nested for loops bool hasNestedFor = false; forOp.getBody()->walk([&](scf::ForOp) { hasNestedFor = true; }); EXPECT_FALSE(hasNestedFor); // The collapsed loop should iterate over the product of dimensions // lb=0, step=1 (after collapsing two 0..10 inclusive loops) auto lbConst = getConstantIndex(forOp.getLowerBound()); ASSERT_TRUE(lbConst.has_value()); EXPECT_EQ(*lbConst, 0); auto stepConst = getConstantIndex(forOp.getStep()); ASSERT_TRUE(stepConst.has_value()); EXPECT_EQ(*stepConst, 1); // Upper bound should be 11*11=121 (product of trip counts) // coalesceLoops normalizes the loops, so ub = totalTripCount EXPECT_TRUE(forOp.getUpperBound().getType().isIndex()); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopToSCFForNoCollapse) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0, c0}, {c10, c10}, {c1, c1}); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/false); ASSERT_TRUE(forOp); bool hasNestedFor = false; forOp.getBody()->walk([&](scf::ForOp) { hasNestedFor = true; }); EXPECT_TRUE(hasNestedFor); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopToSCFForWithCollapseAndDynamicBounds) { // Test with dynamic (non-constant) bounds to ensure all bounds are computed // before any ForOp is created. This is required for coalesceLoops to work // correctly (called when enableCollapse is true) - if inner loop bounds were // computed inside the outer loop, coalesceLoops would create ops that // reference values from a nested region. auto [module, funcOp] = createModuleWithFuncArgs( {b.getIndexType(), b.getIndexType(), b.getIndexType(), b.getIndexType()}); // Use function arguments as dynamic bounds Value lb0 = funcOp.getArgument(0); Value ub0 = funcOp.getArgument(1); Value lb1 = funcOp.getArgument(2); Value ub1 = funcOp.getArgument(3); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({lb0, lb1}, {ub0, ub1}, {c1, c1}); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/true); ASSERT_TRUE(forOp); // With collapse, there should be NO nested for loops bool hasNestedFor = false; forOp.getBody()->walk([&](scf::ForOp) { hasNestedFor = true; }); EXPECT_FALSE(hasNestedFor); // Verify the IR is valid EXPECT_TRUE(module->verify().succeeded()); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopToSCFForExclusiveUpperBound) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0}, {c10}, {c1}, /*inclusiveUpperbound=*/false); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/false); ASSERT_TRUE(forOp); // Normalized: lb=0, step=1, ub=tripCount // For exclusive [0, 10) with step 1: tripCount = (9 - 0 + 1) / 1 = 10 auto lbConst = getConstantIndex(forOp.getLowerBound()); ASSERT_TRUE(lbConst.has_value()); EXPECT_EQ(*lbConst, 0); auto ubConst = getConstantIndex(forOp.getUpperBound()); ASSERT_TRUE(ubConst.has_value()); EXPECT_EQ(*ubConst, 10); auto stepConst = getConstantIndex(forOp.getStep()); ASSERT_TRUE(stepConst.has_value()); EXPECT_EQ(*stepConst, 1); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopToSCFForNegativeStep) { auto [module, funcOp] = createModuleWithFunc(); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); Value cNeg1 = createIndexConstant(-1); // acc.loop from 10 to 1 step -1 (inclusive) acc::LoopOp loopOp = createLoopOp({c10}, {c1}, {cNeg1}); scf::ForOp forOp = convertACCLoopToSCFFor(loopOp, b, /*enableCollapse=*/false); ASSERT_TRUE(forOp); // Normalized: lb=0, step=1, ub=tripCount // tripCount = (1 - 10 + (-1)) / (-1) = (-10) / (-1) = 10 auto lbConst = getConstantIndex(forOp.getLowerBound()); ASSERT_TRUE(lbConst.has_value()); EXPECT_EQ(*lbConst, 0); auto ubConst = getConstantIndex(forOp.getUpperBound()); ASSERT_TRUE(ubConst.has_value()); EXPECT_EQ(*ubConst, 10); auto stepConst = getConstantIndex(forOp.getStep()); ASSERT_TRUE(stepConst.has_value()); EXPECT_EQ(*stepConst, 1); EXPECT_TRUE(isa(forOp.getBody()->getTerminator())); EXPECT_TRUE(module->verify().succeeded()); } //===----------------------------------------------------------------------===// // convertACCLoopToSCFParallel Tests //===----------------------------------------------------------------------===// TEST_F(OpenACCUtilsLoopTest, ConvertSimpleLoopToSCFParallel) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0}, {c10}, {c1}); scf::ParallelOp parallelOp = convertACCLoopToSCFParallel(loopOp, b); ASSERT_TRUE(parallelOp); EXPECT_EQ(parallelOp.getNumLoops(), 1u); // scf.parallel uses normalized bounds: lb=0, step=1, ub=tripCount auto lb = getConstantIndex(parallelOp.getLowerBound()[0]); auto step = getConstantIndex(parallelOp.getStep()[0]); auto ub = getConstantIndex(parallelOp.getUpperBound()[0]); ASSERT_TRUE(lb.has_value()); ASSERT_TRUE(step.has_value()); ASSERT_TRUE(ub.has_value()); EXPECT_EQ(*lb, 0); EXPECT_EQ(*step, 1); EXPECT_EQ(*ub, 11); // trip count for 0..10 inclusive with step 1 // Verify IVs are index type EXPECT_EQ(parallelOp.getInductionVars().size(), 1u); EXPECT_TRUE(parallelOp.getInductionVars()[0].getType().isIndex()); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopWithI32BoundsToSCFParallel) { auto [module, funcOp] = createModuleWithFunc(); Value lb = createI32Constant(5); Value ub = createI32Constant(15); Value step = createI32Constant(2); acc::LoopOp loopOp = createLoopOp({lb}, {ub}, {step}); scf::ParallelOp parallelOp = convertACCLoopToSCFParallel(loopOp, b); ASSERT_TRUE(parallelOp); EXPECT_EQ(parallelOp.getNumLoops(), 1u); // All bounds should be index type (converted from i32) EXPECT_TRUE(parallelOp.getLowerBound()[0].getType().isIndex()); EXPECT_TRUE(parallelOp.getUpperBound()[0].getType().isIndex()); EXPECT_TRUE(parallelOp.getStep()[0].getType().isIndex()); // Normalized: lb=0, step=1 // Note: ub is trip count but not folded because index_cast prevents folding auto lbConst = getConstantIndex(parallelOp.getLowerBound()[0]); auto stepConst = getConstantIndex(parallelOp.getStep()[0]); ASSERT_TRUE(lbConst.has_value()); ASSERT_TRUE(stepConst.has_value()); EXPECT_EQ(*lbConst, 0); EXPECT_EQ(*stepConst, 1); // Verify IVs are index type EXPECT_TRUE(parallelOp.getInductionVars()[0].getType().isIndex()); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopWithNonConstantBoundsToSCFParallel) { auto [module, funcOp] = createModuleWithFuncArgs( {b.getIndexType(), b.getIndexType(), b.getIndexType()}); Block &entryBlock = funcOp.getBody().front(); Value lb = entryBlock.getArgument(0); Value ub = entryBlock.getArgument(1); Value step = entryBlock.getArgument(2); acc::LoopOp loopOp = createLoopOp({lb}, {ub}, {step}); scf::ParallelOp parallelOp = convertACCLoopToSCFParallel(loopOp, b); ASSERT_TRUE(parallelOp); EXPECT_EQ(parallelOp.getNumLoops(), 1u); // Normalized: lb=0, step=1 auto lbConst = getConstantIndex(parallelOp.getLowerBound()[0]); auto stepConst = getConstantIndex(parallelOp.getStep()[0]); ASSERT_TRUE(lbConst.has_value()); ASSERT_TRUE(stepConst.has_value()); EXPECT_EQ(*lbConst, 0); EXPECT_EQ(*stepConst, 1); // Upper bound should be computed trip count (not a constant) // Verify it's not a simple constant (since bounds are dynamic) EXPECT_FALSE(getConstantIndex(parallelOp.getUpperBound()[0]).has_value()); } TEST_F(OpenACCUtilsLoopTest, ConvertMultiDimLoopToSCFParallel) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0, c0}, {c10, c10}, {c1, c1}); scf::ParallelOp parallelOp = convertACCLoopToSCFParallel(loopOp, b); ASSERT_TRUE(parallelOp); EXPECT_EQ(parallelOp.getNumLoops(), 2u); // Both dimensions should have normalized lb=0, step=1, ub=11 for (unsigned i = 0; i < 2; ++i) { auto lb = getConstantIndex(parallelOp.getLowerBound()[i]); auto step = getConstantIndex(parallelOp.getStep()[i]); auto ub = getConstantIndex(parallelOp.getUpperBound()[i]); ASSERT_TRUE(lb.has_value()); ASSERT_TRUE(step.has_value()); ASSERT_TRUE(ub.has_value()); EXPECT_EQ(*lb, 0); EXPECT_EQ(*step, 1); EXPECT_EQ(*ub, 11); // 0..10 inclusive = 11 iterations } // Should have 2 induction variables EXPECT_EQ(parallelOp.getInductionVars().size(), 2u); EXPECT_TRUE(parallelOp.getInductionVars()[0].getType().isIndex()); EXPECT_TRUE(parallelOp.getInductionVars()[1].getType().isIndex()); } TEST_F(OpenACCUtilsLoopTest, ConvertLoopWithLargeStepToSCFParallel) { auto [module, funcOp] = createModuleWithFunc(); Value lb = createIndexConstant(0); Value ub = createIndexConstant(100); Value step = createIndexConstant(10); acc::LoopOp loopOp = createLoopOp({lb}, {ub}, {step}); scf::ParallelOp parallelOp = convertACCLoopToSCFParallel(loopOp, b); ASSERT_TRUE(parallelOp); EXPECT_EQ(parallelOp.getNumLoops(), 1u); // Normalized: lb=0, step=1, ub=tripCount auto lbConst = getConstantIndex(parallelOp.getLowerBound()[0]); auto stepConst = getConstantIndex(parallelOp.getStep()[0]); auto ubConst = getConstantIndex(parallelOp.getUpperBound()[0]); ASSERT_TRUE(lbConst.has_value()); ASSERT_TRUE(stepConst.has_value()); ASSERT_TRUE(ubConst.has_value()); EXPECT_EQ(*lbConst, 0); EXPECT_EQ(*stepConst, 1); EXPECT_EQ(*ubConst, 11); // trip count for 0..100 inclusive with step 10 // Verify IV is index type EXPECT_TRUE(parallelOp.getInductionVars()[0].getType().isIndex()); } //===----------------------------------------------------------------------===// // convertUnstructuredACCLoopToSCFExecuteRegion Tests //===----------------------------------------------------------------------===// TEST_F(OpenACCUtilsLoopTest, ConvertUnstructuredLoopToExecuteRegion) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createUnstructuredLoopOp({c0}, {c10}, {c1}); // Verify the source loop has 4 blocks EXPECT_EQ(loopOp.getRegion().getBlocks().size(), 4u); scf::ExecuteRegionOp exeRegionOp = convertUnstructuredACCLoopToSCFExecuteRegion(loopOp, b); ASSERT_TRUE(exeRegionOp); // The execute_region should have 4 blocks replicated from the source EXPECT_EQ(exeRegionOp.getRegion().getBlocks().size(), 4u); // Verify that the control flow structure is preserved: Block &entryBlock = exeRegionOp.getRegion().front(); EXPECT_TRUE(isa(entryBlock.getTerminator())); Block &exitBlock = exeRegionOp.getRegion().back(); EXPECT_TRUE(isa(exitBlock.getTerminator())); // Count arith operations to verify body was cloned correctly unsigned addCount = 0; unsigned mulCount = 0; exeRegionOp.getRegion().walk([&](arith::AddIOp) { ++addCount; }); exeRegionOp.getRegion().walk([&](arith::MulIOp) { ++mulCount; }); EXPECT_EQ(addCount, 1u); EXPECT_EQ(mulCount, 1u); } TEST_F(OpenACCUtilsLoopTest, ConvertUnstructuredLoopPreservesSuccessors) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createUnstructuredLoopOp({c0}, {c10}, {c1}); scf::ExecuteRegionOp exeRegionOp = convertUnstructuredACCLoopToSCFExecuteRegion(loopOp, b); ASSERT_TRUE(exeRegionOp); Block &entryBlock = exeRegionOp.getRegion().front(); auto condBranch = dyn_cast(entryBlock.getTerminator()); ASSERT_TRUE(condBranch); // Both successors should exist in the region Block *trueDest = condBranch.getTrueDest(); Block *falseDest = condBranch.getFalseDest(); EXPECT_TRUE(trueDest->getParent() == &exeRegionOp.getRegion()); EXPECT_TRUE(falseDest->getParent() == &exeRegionOp.getRegion()); } //===----------------------------------------------------------------------===// // wrapMultiBlockRegionWithSCFExecuteRegion Tests //===----------------------------------------------------------------------===// TEST_F(OpenACCUtilsLoopTest, WrapMultiBlockRegionWithSCFExecuteRegionMultiBlock) { auto [module, funcOp] = createModuleWithFunc(); // Create a region with multi-block control flow: use acc.parallel // only to own the region, then build entry -> then/else -> exit with // acc.yield. OwningOpRef parallelOp = acc::ParallelOp::create(b, loc, TypeRange{}, ValueRange{}); Region ®ion = parallelOp->getRegion(); Block *entry = b.createBlock(®ion, region.begin()); Block *thenBlock = b.createBlock(®ion, region.end()); Block *elseBlock = b.createBlock(®ion, region.end()); Block *exitBlock = b.createBlock(®ion, region.end()); b.setInsertionPointToEnd(entry); Value cond = arith::ConstantOp::create(b, loc, b.getI1Type(), b.getBoolAttr(true)); cf::CondBranchOp::create(b, loc, cond, thenBlock, elseBlock); b.setInsertionPointToEnd(thenBlock); Value c1 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(1)); Value c2 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(2)); arith::AddIOp::create(b, loc, c1, c2); cf::BranchOp::create(b, loc, exitBlock); b.setInsertionPointToEnd(elseBlock); Value c3 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(3)); Value c4 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(4)); arith::MulIOp::create(b, loc, c3, c4); cf::BranchOp::create(b, loc, exitBlock); b.setInsertionPointToEnd(exitBlock); acc::YieldOp::create(b, loc); EXPECT_EQ(region.getBlocks().size(), 4u); b.setInsertionPointAfter(parallelOp.get()); IRMapping mapping; scf::ExecuteRegionOp exeRegionOp = wrapMultiBlockRegionWithSCFExecuteRegion(region, mapping, loc, b); ASSERT_TRUE(exeRegionOp); // The execute_region should have the same number of blocks as the source EXPECT_EQ(exeRegionOp.getRegion().getBlocks().size(), 4u); // Entry block should have cond_br (control flow preserved) Block &entryBlock = exeRegionOp.getRegion().front(); EXPECT_TRUE(isa(entryBlock.getTerminator())); // Last block should have scf.yield (acc.yield was replaced) Block &lastBlock = exeRegionOp.getRegion().back(); EXPECT_TRUE(isa(lastBlock.getTerminator())); // Body ops should be cloned (one addi, one muli from then/else blocks) unsigned addCount = 0; unsigned mulCount = 0; exeRegionOp.getRegion().walk([&](arith::AddIOp) { ++addCount; }); exeRegionOp.getRegion().walk([&](arith::MulIOp) { ++mulCount; }); EXPECT_EQ(addCount, 1u); EXPECT_EQ(mulCount, 1u); } TEST_F(OpenACCUtilsLoopTest, WrapMultiBlockWithEarlyExitRegionWithSCFExecuteRegion) { auto [module, funcOp] = createModuleWithFunc(); OwningOpRef parallelOp = acc::ParallelOp::create(b, loc, TypeRange{}, ValueRange{}); Region ®ion = parallelOp->getRegion(); // Block order as in all.mlir: ^bb0 entry, ^bb1 header, ^bb2 exit, ^bb3 body Block *entry = b.createBlock(®ion, region.begin()); Block *header = b.createBlock(®ion, region.end()); Block *exitBlock = b.createBlock(®ion, region.end()); Block *bodyBlock = b.createBlock(®ion, region.end()); // ^bb0 (entry): setup then cf.br ^bb1 b.setInsertionPointToEnd(entry); Value c1 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(1)); arith::AddIOp::create(b, loc, c1, c1); // some op like fir.store in all.mlir cf::BranchOp::create(b, loc, header); // ^bb1 (header): 2 preds (entry, body). cond_br to exit or body b.setInsertionPointToEnd(header); Value cond = arith::ConstantOp::create(b, loc, b.getI1Type(), b.getBoolAttr(false)); cf::CondBranchOp::create(b, loc, cond, exitBlock, bodyBlock); // ^bb2 (exit): return — use acc.yield (replaced with scf.yield by the util) b.setInsertionPointToEnd(exitBlock); acc::YieldOp::create(b, loc); // ^bb3 (body): body ops then cf.br ^bb1 b.setInsertionPointToEnd(bodyBlock); Value c2 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(2)); Value c3 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(3)); arith::MulIOp::create(b, loc, c2, c3); cf::BranchOp::create(b, loc, header); EXPECT_EQ(region.getBlocks().size(), 4u); b.setInsertionPointAfter(parallelOp.get()); IRMapping mapping; scf::ExecuteRegionOp exeRegionOp = wrapMultiBlockRegionWithSCFExecuteRegion(region, mapping, loc, b); ASSERT_TRUE(exeRegionOp); EXPECT_EQ(exeRegionOp.getRegion().getBlocks().size(), 4u); // First block (entry): branch to header Block &exeEntry = exeRegionOp.getRegion().front(); EXPECT_TRUE(isa(exeEntry.getTerminator())); // Second block (header): cond_br, 2 predecessors (entry and body) Block &exeHeader = *std::next(exeRegionOp.getRegion().begin()); EXPECT_TRUE(isa(exeHeader.getTerminator())); // Third block (exit): scf.yield Block &exeExit = *std::next(exeRegionOp.getRegion().begin(), 2); EXPECT_TRUE(isa(exeExit.getTerminator())); // Fourth block (body): branch back to header Block &exeBody = exeRegionOp.getRegion().back(); EXPECT_TRUE(isa(exeBody.getTerminator())); EXPECT_EQ(exeBody.getTerminator()->getSuccessor(0), &exeHeader); // Body ops preserved: one addi (entry), one muli (body) unsigned addCount = 0; unsigned mulCount = 0; exeRegionOp.getRegion().walk([&](arith::AddIOp) { ++addCount; }); exeRegionOp.getRegion().walk([&](arith::MulIOp) { ++mulCount; }); EXPECT_EQ(addCount, 1u); EXPECT_EQ(mulCount, 1u); } TEST_F(OpenACCUtilsLoopTest, WrapMultiBlockRegionWithSCFExecuteRegionConvertFuncReturn) { auto [module, funcOp] = createModuleWithFunc(); // Build a multi-block region that uses func.return (function body). // wrapMultiBlockRegionWithSCFExecuteRegion(..., true) should replace // func.return with scf.yield. Region ®ion = funcOp.getBody(); Block *entry = ®ion.front(); Block *thenBlock = b.createBlock(®ion, region.end()); Block *elseBlock = b.createBlock(®ion, region.end()); Block *exitBlock = b.createBlock(®ion, region.end()); Block *tailBlock = b.createBlock(®ion, region.end()); b.setInsertionPointToEnd(entry); Value cond = arith::ConstantOp::create(b, loc, b.getI1Type(), b.getBoolAttr(true)); cf::CondBranchOp::create(b, loc, cond, thenBlock, elseBlock); b.setInsertionPointToEnd(thenBlock); Value c1 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(1)); Value c2 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(2)); arith::AddIOp::create(b, loc, c1, c2); cf::BranchOp::create(b, loc, exitBlock); b.setInsertionPointToEnd(elseBlock); Value c3 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(3)); Value c4 = arith::ConstantOp::create(b, loc, b.getIndexType(), b.getIndexAttr(4)); arith::MulIOp::create(b, loc, c3, c4); cf::BranchOp::create(b, loc, exitBlock); b.setInsertionPointToEnd(exitBlock); func::ReturnOp::create(b, loc, ValueRange{}); b.setInsertionPointToEnd(tailBlock); cf::BranchOp::create(b, loc, exitBlock); EXPECT_EQ(region.getBlocks().size(), 5u); b.setInsertionPointAfter(funcOp); IRMapping mapping; scf::ExecuteRegionOp exeRegionOp = wrapMultiBlockRegionWithSCFExecuteRegion(region, mapping, loc, b); ASSERT_TRUE(exeRegionOp); EXPECT_EQ(exeRegionOp.getRegion().getBlocks().size(), 5u); // Only the block that had func.return is replaced with scf.yield; other // blocks keep their cf.cond_br / cf.br terminators. unsigned exeBlockIndex = 0; Block *exeExitBlock = nullptr; for (Block &block : exeRegionOp.getRegion().getBlocks()) { if (exeBlockIndex == 3) // exitBlock (entry=0, then=1, else=2, exit=3, tail=4) exeExitBlock = █ exeBlockIndex++; } ASSERT_TRUE(exeExitBlock); EXPECT_TRUE(isa(exeExitBlock->getTerminator())) << "block that had func.return should now have scf.yield"; // Control flow and body ops preserved Block &exeEntry = exeRegionOp.getRegion().front(); EXPECT_TRUE(isa(exeEntry.getTerminator())); unsigned addCount = 0; unsigned mulCount = 0; exeRegionOp.getRegion().walk([&](arith::AddIOp) { ++addCount; }); exeRegionOp.getRegion().walk([&](arith::MulIOp) { ++mulCount; }); EXPECT_EQ(addCount, 1u); EXPECT_EQ(mulCount, 1u); } TEST_F(OpenACCUtilsLoopTest, WrapMultiBlockRegionWithSCFExecuteRegionFuncMultipleReturnsWithResults) { // Create a function with multiple blocks that each end with func.return // with results. wrapMultiBlockRegionWithSCFExecuteRegion(..., true) should // replace each func.return with scf.yield and produce an execute_region // with the same result types. OwningOpRef module = ModuleOp::create(loc); b.setInsertionPointToStart(module->getBody()); Type i32 = b.getI32Type(); auto funcType = b.getFunctionType({}, {i32}); auto funcOp = func::FuncOp::create(b, loc, "test_func", funcType); Block *entry = funcOp.addEntryBlock(); Block *thenBlock = b.createBlock(&funcOp.getBody(), funcOp.getBody().end()); Block *elseBlock = b.createBlock(&funcOp.getBody(), funcOp.getBody().end()); b.setInsertionPointToEnd(entry); Value cond = arith::ConstantOp::create(b, loc, b.getI1Type(), b.getBoolAttr(true)); cf::CondBranchOp::create(b, loc, cond, thenBlock, elseBlock); b.setInsertionPointToEnd(thenBlock); Value thenV = createI32Constant(1); func::ReturnOp::create(b, loc, ValueRange{thenV}); b.setInsertionPointToEnd(elseBlock); Value elseV = createI32Constant(3); func::ReturnOp::create(b, loc, ValueRange{elseV}); Region ®ion = funcOp.getBody(); EXPECT_EQ(region.getBlocks().size(), 3u); b.setInsertionPointAfter(funcOp); IRMapping mapping; scf::ExecuteRegionOp exeRegionOp = wrapMultiBlockRegionWithSCFExecuteRegion(region, mapping, loc, b); ASSERT_TRUE(exeRegionOp); // execute_region should have one result (same as func.return operands) EXPECT_EQ(exeRegionOp.getNumResults(), 1u); EXPECT_TRUE(exeRegionOp.getResult(0).getType().isSignlessInteger(32)); // Cloned region should have 3 blocks EXPECT_EQ(exeRegionOp.getRegion().getBlocks().size(), 3u); // Entry block: cond_br unchanged Block &exeEntry = exeRegionOp.getRegion().front(); EXPECT_TRUE(isa(exeEntry.getTerminator())); // Both then and else blocks should now have scf.yield with one operand unsigned yieldCount = 0; for (Block &block : exeRegionOp.getRegion().getBlocks()) { if (isa(block.getTerminator())) { EXPECT_EQ(block.getTerminator()->getNumOperands(), 1u); yieldCount++; } } EXPECT_EQ(yieldCount, 2u) << "both return blocks should be scf.yield"; } TEST_F(OpenACCUtilsLoopTest, UnstructuredLoopWithYieldOperandsSucceeds) { auto [module, funcOp] = createModuleWithFunc(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); // Create an unstructured loop where the yield has operands (loop with // results); conversion should succeed and produce execute_region with // results. auto loopOp = acc::LoopOp::create(b, loc, {c0}, {c10}, {c1}, acc::LoopParMode::loop_independent); loopOp.setInclusiveUpperboundAttr(b.getDenseBoolArrayAttr({true})); loopOp.setUnstructuredAttr(b.getUnitAttr()); // Create multi-block body with yield that has operands { OpBuilder::InsertionGuard guard(b); Region ®ion = loopOp.getRegion(); Block *entry = b.createBlock(®ion, region.begin()); Block *exitBlock = b.createBlock(®ion, region.end()); b.setInsertionPointToEnd(entry); cf::BranchOp::create(b, loc, exitBlock); b.setInsertionPointToEnd(exitBlock); Value result = createI32Constant(42); acc::YieldOp::create(b, loc, ValueRange{result}); } scf::ExecuteRegionOp exeRegionOp = convertUnstructuredACCLoopToSCFExecuteRegion(loopOp, b); ASSERT_TRUE(exeRegionOp); EXPECT_EQ(exeRegionOp.getNumResults(), 1u); EXPECT_TRUE(exeRegionOp.getResult(0).getType().isSignlessInteger(32)); } //===----------------------------------------------------------------------===// // cloneACCRegionInto Tests //===----------------------------------------------------------------------===// TEST_F(OpenACCUtilsLoopTest, CloneACCRegionIntoWithYield) { auto [module, funcOp] = createModuleWithFunc(); Block *entry = &funcOp.getBody().front(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0}, {c10}, {c1}); // Add a constant to the loop body before the yield so the region has // something to clone besides the terminator. Block *loopBody = &loopOp.getRegion().front(); b.setInsertionPoint(loopBody->getTerminator()); arith::ConstantOp::create(b, loc, b.getI32IntegerAttr(42)); b.setInsertionPointToEnd(entry); func::ReturnOp::create(b, loc); IRMapping mapping; mapping.map(loopBody->getArgument(0), c0); auto [replacements, ip] = acc::cloneACCRegionInto( &loopOp.getRegion(), entry, entry->begin(), mapping, ValueRange{}); EXPECT_TRUE(replacements.empty()); // The cloned block should have been merged: constant 42 present, no acc.yield bool hasConst42 = false; bool hasAccYield = false; for (Operation &op : entry->getOperations()) { if (auto cst = dyn_cast(op)) hasConst42 = hasConst42 || (cst.getValue() == b.getI32IntegerAttr(42)); hasAccYield = hasAccYield || isa(op); } EXPECT_TRUE(hasConst42); EXPECT_FALSE(hasAccYield); } TEST_F(OpenACCUtilsLoopTest, CloneACCRegionIntoWithResultReplacement) { auto [module, funcOp] = createModuleWithFunc(); Block *entry = &funcOp.getBody().front(); // Value that will be replaced by the cloned region's yield operand Value origVal = arith::ConstantOp::create(b, loc, b.getI32IntegerAttr(0)).getResult(); Value c0 = createIndexConstant(0); Value c10 = createIndexConstant(10); Value c1 = createIndexConstant(1); acc::LoopOp loopOp = createLoopOp({c0}, {c10}, {c1}); Block *loopBody = &loopOp.getRegion().front(); b.setInsertionPoint(loopBody->getTerminator()); Value replacementVal = arith::ConstantOp::create(b, loc, b.getI32IntegerAttr(1)).getResult(); loopBody->getTerminator()->erase(); b.setInsertionPointToEnd(loopBody); acc::YieldOp::create(b, loc, ValueRange{replacementVal}); b.setInsertionPointToEnd(entry); Value c1value = arith::ConstantOp::create(b, loc, b.getI32IntegerAttr(1)).getResult(); Value addResult = arith::AddIOp::create(b, loc, origVal, c1value) .getResult(); // use of origVal (void)addResult; func::ReturnOp::create(b, loc); IRMapping mapping; mapping.map(loopBody->getArgument(0), c0); auto [replacements, ip] = acc::cloneACCRegionInto( &loopOp.getRegion(), entry, entry->begin(), mapping, ValueRange{origVal}); ASSERT_EQ(replacements.size(), 1u); // The addi should now use the replacement (constant 1), not origVal bool addiUsesReplacement = false; for (Operation &op : entry->getOperations()) { if (auto addi = dyn_cast(op)) addiUsesReplacement = (addi.getLhs() == replacements[0]); } EXPECT_TRUE(addiUsesReplacement); }