Skip to content

[llvm][ConstraintElimination]Insert ConditionFact into loop header in case of monotonic induction variables #112080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 105 additions & 23 deletions llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,12 @@ struct State {
LoopInfo &LI;
ScalarEvolution &SE;
SmallVector<FactOrCheck, 64> WorkList;
bool AddInductionInfoIntoHeader = false;

State(DominatorTree &DT, LoopInfo &LI, ScalarEvolution &SE)
: DT(DT), LI(LI), SE(SE) {}
State(DominatorTree &DT, LoopInfo &LI, ScalarEvolution &SE,
bool AddInductionInfoIntoHeader = false)
: DT(DT), LI(LI), SE(SE),
AddInductionInfoIntoHeader(AddInductionInfoIntoHeader) {}

/// Process block \p BB and add known facts to work-list.
void addInfoFor(BasicBlock &BB);
Expand All @@ -197,6 +200,8 @@ struct State {
/// controlling the loop header.
void addInfoForInductions(BasicBlock &BB);

void addConditionFactsIntoLoopHeader(Loop &L);

/// Returns true if we can add a known condition from BB to its successor
/// block Succ.
bool canAddSuccessor(BasicBlock &BB, BasicBlock *Succ) const {
Expand Down Expand Up @@ -900,10 +905,87 @@ static void dumpConstraint(ArrayRef<int64_t> C,
}
#endif

static Value *getStartValueFromAddRec(const SCEVAddRecExpr &AR, PHINode &PN,
Loop &L, ScalarEvolution &SE) {
const SCEV *StartSCEV = AR.getStart();
Value *StartValue = nullptr;
BasicBlock *LoopPred = L.getLoopPredecessor();
if (auto *C = dyn_cast<SCEVConstant>(StartSCEV)) {
StartValue = C->getValue();
} else {
StartValue = PN.getIncomingValueForBlock(LoopPred);
assert(SE.getSCEV(StartValue) == StartSCEV && "inconsistent start value");
}
return StartValue;
}

struct MonotonicityInfo {
std::optional<ScalarEvolution::MonotonicPredicateType> UnsignedPredicateType;
std::optional<ScalarEvolution::MonotonicPredicateType> SignedPredicateType;
bool isUnsignedIncreasing() const {
return UnsignedPredicateType &&
*UnsignedPredicateType == ScalarEvolution::MonotonicallyIncreasing;
}
bool isUnsignedDecreasing() const {
return UnsignedPredicateType &&
*UnsignedPredicateType == ScalarEvolution::MonotonicallyDecreasing;
}
bool isSignedIncreasing() const {
return SignedPredicateType &&
*SignedPredicateType == ScalarEvolution::MonotonicallyIncreasing;
}
bool isSignedDecreasing() const {
return SignedPredicateType &&
*SignedPredicateType == ScalarEvolution::MonotonicallyDecreasing;
}
};

static MonotonicityInfo getMonotonicityInfo(const SCEVAddRecExpr *AR,
ScalarEvolution &SE) {
MonotonicityInfo MI;
MI.UnsignedPredicateType =
SE.getMonotonicPredicateType(AR, CmpInst::ICMP_UGT);
MI.SignedPredicateType = SE.getMonotonicPredicateType(AR, CmpInst::ICMP_SGT);
return MI;
}

// For monotonically decreasing/increasing variables in the loop,
// this will insert ConditionFact PN >= StartingValue or PN <= StartingValue
// associated with the loop header, where PN is the corresponding PHi node.
void State::addConditionFactsIntoLoopHeader(Loop &L) {
BasicBlock &BB = *L.getHeader();
DomTreeNode *DTN = DT.getNode(&BB);
for (PHINode &PN : BB.phis()) {
if (PN.getNumIncomingValues() != 2 || !SE.isSCEVable(PN.getType()))
continue;
auto *AR = dyn_cast_or_null<SCEVAddRecExpr>(SE.getSCEV(&PN));
BasicBlock *LoopPred = L.getLoopPredecessor();
if (!AR || AR->getLoop() != &L || !LoopPred)
continue;
Value *StartValue = getStartValueFromAddRec(*AR, PN, L, SE);
auto MI = getMonotonicityInfo(AR, SE);

if (MI.isUnsignedIncreasing())
WorkList.push_back(FactOrCheck::getConditionFact(DTN, CmpInst::ICMP_UGE,
&PN, StartValue));
if (MI.isSignedIncreasing())
WorkList.push_back(FactOrCheck::getConditionFact(DTN, CmpInst::ICMP_SGE,
&PN, StartValue));
if (MI.isUnsignedDecreasing())
WorkList.push_back(FactOrCheck::getConditionFact(DTN, CmpInst::ICMP_ULE,
&PN, StartValue));
if (MI.isSignedDecreasing())
WorkList.push_back(FactOrCheck::getConditionFact(DTN, CmpInst::ICMP_SLE,
&PN, StartValue));
}
}

void State::addInfoForInductions(BasicBlock &BB) {
auto *L = LI.getLoopFor(&BB);
if (!L || L->getHeader() != &BB)
return;
if (AddInductionInfoIntoHeader)
addConditionFactsIntoLoopHeader(*L);

Value *A;
Value *B;
Expand Down Expand Up @@ -939,28 +1021,16 @@ void State::addInfoForInductions(BasicBlock &BB) {
if (!AR || AR->getLoop() != L || !LoopPred)
return;

const SCEV *StartSCEV = AR->getStart();
Value *StartValue = nullptr;
if (auto *C = dyn_cast<SCEVConstant>(StartSCEV)) {
StartValue = C->getValue();
} else {
StartValue = PN->getIncomingValueForBlock(LoopPred);
assert(SE.getSCEV(StartValue) == StartSCEV && "inconsistent start value");
}
Value *StartValue = getStartValueFromAddRec(*AR, *PN, *L, SE);
auto MI = getMonotonicityInfo(AR, SE);

DomTreeNode *DTN = DT.getNode(InLoopSucc);
auto IncUnsigned = SE.getMonotonicPredicateType(AR, CmpInst::ICMP_UGT);
auto IncSigned = SE.getMonotonicPredicateType(AR, CmpInst::ICMP_SGT);
bool MonotonicallyIncreasingUnsigned =
IncUnsigned && *IncUnsigned == ScalarEvolution::MonotonicallyIncreasing;
bool MonotonicallyIncreasingSigned =
IncSigned && *IncSigned == ScalarEvolution::MonotonicallyIncreasing;
// If SCEV guarantees that AR does not wrap, PN >= StartValue can be added
// unconditionally.
if (MonotonicallyIncreasingUnsigned)
if (MI.isUnsignedIncreasing())
WorkList.push_back(
FactOrCheck::getConditionFact(DTN, CmpInst::ICMP_UGE, PN, StartValue));
if (MonotonicallyIncreasingSigned)
if (MI.isSignedIncreasing())
WorkList.push_back(
FactOrCheck::getConditionFact(DTN, CmpInst::ICMP_SGE, PN, StartValue));

Expand Down Expand Up @@ -1008,7 +1078,7 @@ void State::addInfoForInductions(BasicBlock &BB) {

if (!StepOffset.isOne()) {
// Check whether B-Start is known to be a multiple of StepOffset.
const SCEV *BMinusStart = SE.getMinusSCEV(SE.getSCEV(B), StartSCEV);
const SCEV *BMinusStart = SE.getMinusSCEV(SE.getSCEV(B), AR->getStart());
if (isa<SCEVCouldNotCompute>(BMinusStart) ||
!SE.getConstantMultiple(BMinusStart).urem(StepOffset).isZero())
return;
Expand All @@ -1017,11 +1087,11 @@ void State::addInfoForInductions(BasicBlock &BB) {
// AR may wrap. Add PN >= StartValue conditional on StartValue <= B which
// guarantees that the loop exits before wrapping in combination with the
// restrictions on B and the step above.
if (!MonotonicallyIncreasingUnsigned)
if (!MI.isUnsignedIncreasing())
WorkList.push_back(FactOrCheck::getConditionFact(
DTN, CmpInst::ICMP_UGE, PN, StartValue,
ConditionTy(CmpInst::ICMP_ULE, StartValue, B)));
if (!MonotonicallyIncreasingSigned)
if (!MI.isSignedIncreasing())
WorkList.push_back(FactOrCheck::getConditionFact(
DTN, CmpInst::ICMP_SGE, PN, StartValue,
ConditionTy(CmpInst::ICMP_SLE, StartValue, B)));
Expand Down Expand Up @@ -1358,7 +1428,7 @@ static std::optional<bool> checkCondition(CmpInst::Predicate Pred, Value *A,
LLVM_DEBUG(dbgs() << "Checking " << *CheckInst << "\n");

auto R = Info.getConstraintForSolving(Pred, A, B);
if (R.empty() || !R.isValid(Info)){
if (R.empty() || !R.isValid(Info)) {
LLVM_DEBUG(dbgs() << " failed to decompose condition\n");
return std::nullopt;
}
Expand Down Expand Up @@ -1671,6 +1741,16 @@ tryToSimplifyOverflowMath(IntrinsicInst *II, ConstraintInfo &Info,
return Changed;
}

static unsigned int getNumConditionalBranches(Function &F) {
unsigned int NumCondBranches = 0;
for (BasicBlock &BB : F) {
BranchInst *BranchInstr = dyn_cast_or_null<BranchInst>(BB.getTerminator());
if (BranchInstr && BranchInstr->isConditional())
NumCondBranches++;
}
return NumCondBranches;
}

static bool eliminateConstraints(Function &F, DominatorTree &DT, LoopInfo &LI,
ScalarEvolution &SE,
OptimizationRemarkEmitter &ORE) {
Expand All @@ -1680,7 +1760,9 @@ static bool eliminateConstraints(Function &F, DominatorTree &DT, LoopInfo &LI,
for (Value &Arg : F.args())
FunctionArgs.push_back(&Arg);
ConstraintInfo Info(F.getDataLayout(), FunctionArgs);
State S(DT, LI, SE);
unsigned int NumCondBranches = getNumConditionalBranches(F);
State S(DT, LI, SE,
/* AddInductionInfoIntoHeader= */ NumCondBranches < MaxRows / 5);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not aware of the existing MaxRows mechanism during my earlier review, so that seems to be already enough to avoid compile-time explosion.

Doing a whole walk over the function to count conditional branches feels somewhat heavy and we potentially get an amount of facts in the number of Phi instructions (which isn't necessarily correlated to number of condjumps). Anyway just trying to make a case that maybe we should just do the simpler thing and rely on MaxRows limiting things without adding extra conditions here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I added this condition is to avoid regression in some edge cases (see analysis by @dtcxzyw). If we add extra constrains in loop headers and reach MaxRows limit, some constraints are dropped that are not otherwise. I am happy to remove it if we are OK with those regressions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just feels like a lot of trouble (or rather a lot of memory read) to go through the whole function just to count the number of conditional branches... So I am wondering if there is ways to limit this without having an extra pass over the whole function...

std::unique_ptr<Module> ReproducerModule(
DumpReproducers ? new Module(F.getName(), F.getContext()) : nullptr);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,8 @@ define void @test_monotonic_ptr_iv_inc_1_eq_to_uge(ptr %start, i16 %len) {
; CHECK-NEXT: [[AND_0:%.*]] = and i1 [[LEN_NEG]], [[C]]
; CHECK-NEXT: br i1 [[AND_0]], label [[FOR_BODY:%.*]], label [[EXIT:%.*]]
; CHECK: for.body:
; CHECK-NEXT: [[T_1:%.*]] = icmp uge ptr [[PTR_IV]], [[START]]
; CHECK-NEXT: [[T_2:%.*]] = icmp ult ptr [[PTR_IV]], [[UPPER]]
; CHECK-NEXT: [[AND:%.*]] = and i1 [[T_1]], [[T_2]]
; CHECK-NEXT: [[AND:%.*]] = and i1 true, [[T_2]]
; CHECK-NEXT: br i1 [[AND]], label [[LOOP_LATCH]], label [[EXIT]]
; CHECK: loop.latch:
; CHECK-NEXT: call void @use(ptr [[PTR_IV]])
Expand Down
98 changes: 98 additions & 0 deletions llvm/test/Transforms/ConstraintElimination/loop-removal.ll
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
; RUN: opt < %s -passes=constraint-elimination -S | FileCheck %s

define i32 @foo() {
; CHECK-LABEL: define i32 @foo() {
; CHECK-NEXT: init:
; CHECK-NEXT: br label %[[OUTER_LOOP_CONTROL:.*]]
; CHECK: outer.loop.control:
; CHECK-NEXT: [[X_0:%.*]] = phi i32 [ 0, [[INIT:%.*]] ], [ [[X_OUTER:%.*]], %[[OUTER_LOOP_INC:.*]] ]
; CHECK-NEXT: [[TMP0:%.*]] = icmp slt i32 [[X_0]], 10
; CHECK-NEXT: br i1 [[TMP0]], label %[[INNER_LOOP_CONTROL:.*]], label %[[EXIT:.*]]
; CHECK: inner.loop.control:
; CHECK-NEXT: [[X_1:%.*]] = phi i32 [ [[X_0]], %[[OUTER_LOOP_CONTROL]] ], [ [[X_INNER:%.*]], %[[INNER_LOOP_BODY:.*]] ]
; CHECK-NEXT: br i1 false, label %[[INNER_LOOP_BODY]], label %[[OUTER_LOOP_INC]]
; CHECK: inner.loop.body:
; CHECK-NEXT: [[X_INNER]] = add nsw i32 [[X_1]], -1
; CHECK-NEXT: br label %[[INNER_LOOP_CONTROL]]
; CHECK: outer.loop.inc:
; CHECK-NEXT: [[X_OUTER]] = add nsw i32 [[X_1]], 2
; CHECK-NEXT: br label %[[OUTER_LOOP_CONTROL]]
; CHECK: exit:
; CHECK-NEXT: ret i32 [[X_0]]
;
init:
br label %outer.loop.control

outer.loop.control: ; preds = %init, %outer.loop.inc
%x.0 = phi i32 [ 0, %init ], [ %x.outer, %outer.loop.inc ]
%0 = icmp slt i32 %x.0, 10
br i1 %0, label %inner.loop.control, label %exit

inner.loop.control: ; preds = %outer.loop.control, %inner.loop.body
%x.1 = phi i32 [ %x.0, %outer.loop.control ], [ %x.inner, %inner.loop.body ]
%1 = icmp sgt i32 %x.1, 20
br i1 %1, label %inner.loop.body, label %outer.loop.inc

inner.loop.body: ; preds = %inner.loop.control
%x.inner = add nsw i32 %x.1, -1
br label %inner.loop.control

outer.loop.inc: ; preds = %inner.loop.control
%x.outer = add nsw i32 %x.1, 2
br label %outer.loop.control

exit: ; preds = %1
ret i32 %x.0
}

; int foo_with_overflow(unsigned x, int y){
; unsigned i = x;
; while(i < 10 || i > 20){
; if(i > x){
; y -= 23;
; }
; i -= 1;
; }
; return y;
; }
define dso_local noundef i32 @foo_with_overflow(i32 noundef %x, i32 noundef %y) local_unnamed_addr #0 {
; CHECK-LABEL: i32 @foo_with_overflow(
; CHECK-NEXT: entry:
; CHECK-NEXT: [[TMP0:%.*]] = add i32 [[X:%.*]], -21
; CHECK-NEXT: [[TMP1:%.*]] = icmp ult i32 [[TMP0]], -11
; CHECK-NEXT: br i1 [[TMP1]], label %[[WHILE_BODY:.*]], label %[[WHILE_END:.*]]
; CHECK: while.body:
; CHECK-NEXT: [[I_02:%.*]] = phi i32 [ [[SUB3:%.*]], %[[WHILE_BODY]] ], [ [[X]], %[[ENTRY:.*]] ]
; CHECK-NEXT: [[Y_ADDR_01:%.*]] = phi i32 [ [[SPEC_SELECT:%.*]], %[[WHILE_BODY]] ], [ [[Y:%.*]], %[[ENTRY]] ]
; CHECK-NEXT: [[CMP2:%.*]] = icmp ugt i32 [[I_02]], [[X]]
; CHECK-NEXT: [[SUB:%.*]] = add nsw i32 [[Y_ADDR_01]], -23
; CHECK-NEXT: [[SPEC_SELECT]] = select i1 [[CMP2]], i32 [[SUB]], i32 [[Y_ADDR_01]]
; CHECK-NEXT: [[SUB3]] = add i32 [[I_02]], -1
; CHECK-NEXT: [[DOTREASS:%.*]] = add i32 [[I_02]], -22
; CHECK-NEXT: [[TMP2:%.*]] = icmp ult i32 [[DOTREASS]], -11
; CHECK-NEXT: br i1 [[TMP2]], label %[[WHILE_BODY]], label %[[WHILE_END]]
; CHECK: while.end:
; CHECK-NEXT: [[Y_ADDR_0_LCSSA:%.*]] = phi i32 [ [[Y]], %[[ENTRY]] ], [ [[SPEC_SELECT]], %[[WHILE_BODY]] ]
; CHECK-NEXT: ret i32 [[Y_ADDR_0_LCSSA]]
;
entry:
%0 = add i32 %x, -21
%1 = icmp ult i32 %0, -11
br i1 %1, label %while.body, label %while.end

while.body: ; preds = %entry, %while.body
%i.02 = phi i32 [ %sub3, %while.body ], [ %x, %entry ]
%y.addr.01 = phi i32 [ %spec.select, %while.body ], [ %y, %entry ]
%cmp2 = icmp ugt i32 %i.02, %x
%sub = add nsw i32 %y.addr.01, -23
%spec.select = select i1 %cmp2, i32 %sub, i32 %y.addr.01
%sub3 = add i32 %i.02, -1
%.reass = add i32 %i.02, -22
%2 = icmp ult i32 %.reass, -11
br i1 %2, label %while.body, label %while.end

while.end: ; preds = %while.body, %entry
%y.addr.0.lcssa = phi i32 [ %y, %entry ], [ %spec.select, %while.body ]
ret i32 %y.addr.0.lcssa
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ define void @loop_iv_cond_variable_bound(i32 %n) {
; CHECK-NEXT: [[IV:%.*]] = phi i32 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
; CHECK-NEXT: [[T_1:%.*]] = icmp ule i32 [[IV]], [[N:%.*]]
; CHECK-NEXT: call void @use(i1 [[T_1]])
; CHECK-NEXT: [[T_2:%.*]] = icmp sge i32 [[IV]], 0
; CHECK-NEXT: call void @use(i1 [[T_2]])
; CHECK-NEXT: [[T_3:%.*]] = icmp sge i32 [[IV]], -1
; CHECK-NEXT: call void @use(i1 [[T_3]])
; CHECK-NEXT: call void @use(i1 true)
; CHECK-NEXT: call void @use(i1 true)
; CHECK-NEXT: [[C_1:%.*]] = icmp ult i32 [[IV]], [[N]]
; CHECK-NEXT: call void @use(i1 [[C_1]])
; CHECK-NEXT: [[C_2:%.*]] = icmp ugt i32 [[IV]], 1
Expand Down Expand Up @@ -58,10 +56,8 @@ define void @loop_iv_cond_constant_bound() {
; CHECK-NEXT: [[IV:%.*]] = phi i32 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
; CHECK-NEXT: [[T_1:%.*]] = icmp ule i32 [[IV]], 2
; CHECK-NEXT: call void @use(i1 [[T_1]])
; CHECK-NEXT: [[T_2:%.*]] = icmp sge i32 [[IV]], 0
; CHECK-NEXT: call void @use(i1 [[T_2]])
; CHECK-NEXT: [[T_3:%.*]] = icmp sge i32 [[IV]], -1
; CHECK-NEXT: call void @use(i1 [[T_3]])
; CHECK-NEXT: call void @use(i1 true)
; CHECK-NEXT: call void @use(i1 true)
; CHECK-NEXT: [[C_1:%.*]] = icmp ult i32 [[IV]], 2
; CHECK-NEXT: call void @use(i1 [[C_1]])
; CHECK-NEXT: [[C_2:%.*]] = icmp ugt i32 [[IV]], 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ define void @checks_in_loops_removable(ptr %ptr, ptr %lower, ptr %upper, i8 %n)
; CHECK: loop.header:
; CHECK-NEXT: [[IV:%.*]] = phi i16 [ 0, [[PRE_2]] ], [ [[IV_NEXT:%.*]], [[LOOP_LATCH:%.*]] ]
; CHECK-NEXT: [[PTR_IV:%.*]] = getelementptr inbounds i8, ptr [[PTR]], i16 [[IV]]
; CHECK-NEXT: [[CMP_PTR_IV_LOWER:%.*]] = icmp ugt ptr [[LOWER]], [[PTR_IV]]
; CHECK-NEXT: [[CMP_PTR_IV_UPPER:%.*]] = icmp ule ptr [[UPPER]], [[PTR_IV]]
; CHECK-NEXT: [[OR:%.*]] = or i1 false, [[CMP_PTR_IV_UPPER]]
; CHECK-NEXT: br i1 [[OR]], label [[TRAP]], label [[LOOP_LATCH]]
Expand Down Expand Up @@ -86,7 +85,6 @@ define void @some_checks_in_loops_removable(ptr %ptr, ptr %lower, ptr %upper, i8
; CHECK: loop.header:
; CHECK-NEXT: [[IV:%.*]] = phi i16 [ 0, [[PRE_2]] ], [ [[IV_NEXT:%.*]], [[LOOP_LATCH:%.*]] ]
; CHECK-NEXT: [[PTR_IV:%.*]] = getelementptr inbounds i8, ptr [[PTR]], i16 [[IV]]
; CHECK-NEXT: [[CMP_PTR_IV_LOWER:%.*]] = icmp ugt ptr [[LOWER]], [[PTR_IV]]
; CHECK-NEXT: [[CMP_PTR_IV_UPPER:%.*]] = icmp ule ptr [[UPPER]], [[PTR_IV]]
; CHECK-NEXT: [[OR:%.*]] = or i1 false, [[CMP_PTR_IV_UPPER]]
; CHECK-NEXT: br i1 [[OR]], label [[TRAP]], label [[LOOP_BODY:%.*]]
Expand Down Expand Up @@ -163,7 +161,6 @@ define void @no_checks_in_loops_removable(ptr %ptr, ptr %lower, ptr %upper, i8 %
; CHECK: loop.header:
; CHECK-NEXT: [[IV:%.*]] = phi i16 [ 0, [[PRE_1]] ], [ [[IV_NEXT:%.*]], [[LOOP_LATCH:%.*]] ]
; CHECK-NEXT: [[PTR_IV:%.*]] = getelementptr inbounds i8, ptr [[PTR]], i16 [[IV]]
; CHECK-NEXT: [[CMP_PTR_IV_LOWER:%.*]] = icmp ugt ptr [[LOWER]], [[PTR_IV]]
; CHECK-NEXT: [[CMP_PTR_IV_UPPER:%.*]] = icmp ule ptr [[UPPER]], [[PTR_IV]]
; CHECK-NEXT: [[OR:%.*]] = or i1 false, [[CMP_PTR_IV_UPPER]]
; CHECK-NEXT: br i1 [[OR]], label [[TRAP]], label [[LOOP_BODY:%.*]]
Expand Down
Loading
Loading