168d69237SJean Perier //===-- RewriteLoop.cpp ---------------------------------------------------===//
268d69237SJean Perier //
368d69237SJean Perier // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
468d69237SJean Perier // See https://llvm.org/LICENSE.txt for license information.
568d69237SJean Perier // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
668d69237SJean Perier //
768d69237SJean Perier //===----------------------------------------------------------------------===//
868d69237SJean Perier
968d69237SJean Perier #include "PassDetail.h"
1068d69237SJean Perier #include "flang/Optimizer/Dialect/FIRDialect.h"
1168d69237SJean Perier #include "flang/Optimizer/Dialect/FIROps.h"
1268d69237SJean Perier #include "flang/Optimizer/Transforms/Passes.h"
1368d69237SJean Perier #include "mlir/Dialect/Affine/IR/AffineOps.h"
14ace01605SRiver Riddle #include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
1523aa5a74SRiver Riddle #include "mlir/Dialect/Func/IR/FuncOps.h"
1668d69237SJean Perier #include "mlir/Pass/Pass.h"
1768d69237SJean Perier #include "mlir/Transforms/DialectConversion.h"
1868d69237SJean Perier #include "llvm/Support/CommandLine.h"
1968d69237SJean Perier
2068d69237SJean Perier using namespace fir;
21092601d4SAndrzej Warzynski using namespace mlir;
2268d69237SJean Perier
2368d69237SJean Perier namespace {
2468d69237SJean Perier
2568d69237SJean Perier // Conversion of fir control ops to more primitive control-flow.
2668d69237SJean Perier //
2768d69237SJean Perier // FIR loops that cannot be converted to the affine dialect will remain as
2868d69237SJean Perier // `fir.do_loop` operations. These can be converted to control-flow operations.
2968d69237SJean Perier
3068d69237SJean Perier /// Convert `fir.do_loop` to CFG
3168d69237SJean Perier class CfgLoopConv : public mlir::OpRewritePattern<fir::DoLoopOp> {
3268d69237SJean Perier public:
3368d69237SJean Perier using OpRewritePattern::OpRewritePattern;
3468d69237SJean Perier
CfgLoopConv(mlir::MLIRContext * ctx,bool forceLoopToExecuteOnce)3568d69237SJean Perier CfgLoopConv(mlir::MLIRContext *ctx, bool forceLoopToExecuteOnce)
3668d69237SJean Perier : mlir::OpRewritePattern<fir::DoLoopOp>(ctx),
3768d69237SJean Perier forceLoopToExecuteOnce(forceLoopToExecuteOnce) {}
3868d69237SJean Perier
3968d69237SJean Perier mlir::LogicalResult
matchAndRewrite(DoLoopOp loop,mlir::PatternRewriter & rewriter) const4068d69237SJean Perier matchAndRewrite(DoLoopOp loop,
4168d69237SJean Perier mlir::PatternRewriter &rewriter) const override {
4268d69237SJean Perier auto loc = loop.getLoc();
4368d69237SJean Perier
4468d69237SJean Perier // Create the start and end blocks that will wrap the DoLoopOp with an
4568d69237SJean Perier // initalizer and an end point
4668d69237SJean Perier auto *initBlock = rewriter.getInsertionBlock();
4768d69237SJean Perier auto initPos = rewriter.getInsertionPoint();
4868d69237SJean Perier auto *endBlock = rewriter.splitBlock(initBlock, initPos);
4968d69237SJean Perier
5068d69237SJean Perier // Split the first DoLoopOp block in two parts. The part before will be the
5168d69237SJean Perier // conditional block since it already has the induction variable and
5268d69237SJean Perier // loop-carried values as arguments.
53149ad3d5SShraiysh Vaishay auto *conditionalBlock = &loop.getRegion().front();
54e084679fSRiver Riddle conditionalBlock->addArgument(rewriter.getIndexType(), loc);
5568d69237SJean Perier auto *firstBlock =
5668d69237SJean Perier rewriter.splitBlock(conditionalBlock, conditionalBlock->begin());
57149ad3d5SShraiysh Vaishay auto *lastBlock = &loop.getRegion().back();
5868d69237SJean Perier
5968d69237SJean Perier // Move the blocks from the DoLoopOp between initBlock and endBlock
60149ad3d5SShraiysh Vaishay rewriter.inlineRegionBefore(loop.getRegion(), endBlock);
6168d69237SJean Perier
6268d69237SJean Perier // Get loop values from the DoLoopOp
63149ad3d5SShraiysh Vaishay auto low = loop.getLowerBound();
64149ad3d5SShraiysh Vaishay auto high = loop.getUpperBound();
6568d69237SJean Perier assert(low && high && "must be a Value");
66149ad3d5SShraiysh Vaishay auto step = loop.getStep();
6768d69237SJean Perier
6868d69237SJean Perier // Initalization block
6968d69237SJean Perier rewriter.setInsertionPointToEnd(initBlock);
70a54f4eaeSMogball auto diff = rewriter.create<mlir::arith::SubIOp>(loc, high, low);
71a54f4eaeSMogball auto distance = rewriter.create<mlir::arith::AddIOp>(loc, diff, step);
7268d69237SJean Perier mlir::Value iters =
73a54f4eaeSMogball rewriter.create<mlir::arith::DivSIOp>(loc, distance, step);
7468d69237SJean Perier
7568d69237SJean Perier if (forceLoopToExecuteOnce) {
76a54f4eaeSMogball auto zero = rewriter.create<mlir::arith::ConstantIndexOp>(loc, 0);
77a54f4eaeSMogball auto cond = rewriter.create<mlir::arith::CmpIOp>(
78a54f4eaeSMogball loc, arith::CmpIPredicate::sle, iters, zero);
79a54f4eaeSMogball auto one = rewriter.create<mlir::arith::ConstantIndexOp>(loc, 1);
80dec8af70SRiver Riddle iters = rewriter.create<mlir::arith::SelectOp>(loc, cond, one, iters);
8168d69237SJean Perier }
8268d69237SJean Perier
8368d69237SJean Perier llvm::SmallVector<mlir::Value> loopOperands;
8468d69237SJean Perier loopOperands.push_back(low);
8568d69237SJean Perier auto operands = loop.getIterOperands();
8668d69237SJean Perier loopOperands.append(operands.begin(), operands.end());
8768d69237SJean Perier loopOperands.push_back(iters);
8868d69237SJean Perier
89ace01605SRiver Riddle rewriter.create<mlir::cf::BranchOp>(loc, conditionalBlock, loopOperands);
9068d69237SJean Perier
9168d69237SJean Perier // Last loop block
9268d69237SJean Perier auto *terminator = lastBlock->getTerminator();
9368d69237SJean Perier rewriter.setInsertionPointToEnd(lastBlock);
9468d69237SJean Perier auto iv = conditionalBlock->getArgument(0);
95a54f4eaeSMogball mlir::Value steppedIndex =
96a54f4eaeSMogball rewriter.create<mlir::arith::AddIOp>(loc, iv, step);
9768d69237SJean Perier assert(steppedIndex && "must be a Value");
9868d69237SJean Perier auto lastArg = conditionalBlock->getNumArguments() - 1;
9968d69237SJean Perier auto itersLeft = conditionalBlock->getArgument(lastArg);
100a54f4eaeSMogball auto one = rewriter.create<mlir::arith::ConstantIndexOp>(loc, 1);
10168d69237SJean Perier mlir::Value itersMinusOne =
102a54f4eaeSMogball rewriter.create<mlir::arith::SubIOp>(loc, itersLeft, one);
10368d69237SJean Perier
10468d69237SJean Perier llvm::SmallVector<mlir::Value> loopCarried;
10568d69237SJean Perier loopCarried.push_back(steppedIndex);
106149ad3d5SShraiysh Vaishay auto begin = loop.getFinalValue() ? std::next(terminator->operand_begin())
10768d69237SJean Perier : terminator->operand_begin();
10868d69237SJean Perier loopCarried.append(begin, terminator->operand_end());
10968d69237SJean Perier loopCarried.push_back(itersMinusOne);
110ace01605SRiver Riddle rewriter.create<mlir::cf::BranchOp>(loc, conditionalBlock, loopCarried);
11168d69237SJean Perier rewriter.eraseOp(terminator);
11268d69237SJean Perier
11368d69237SJean Perier // Conditional block
11468d69237SJean Perier rewriter.setInsertionPointToEnd(conditionalBlock);
115a54f4eaeSMogball auto zero = rewriter.create<mlir::arith::ConstantIndexOp>(loc, 0);
116a54f4eaeSMogball auto comparison = rewriter.create<mlir::arith::CmpIOp>(
117a54f4eaeSMogball loc, arith::CmpIPredicate::sgt, itersLeft, zero);
11868d69237SJean Perier
119ace01605SRiver Riddle rewriter.create<mlir::cf::CondBranchOp>(
120ace01605SRiver Riddle loc, comparison, firstBlock, llvm::ArrayRef<mlir::Value>(), endBlock,
12168d69237SJean Perier llvm::ArrayRef<mlir::Value>());
12268d69237SJean Perier
12368d69237SJean Perier // The result of the loop operation is the values of the condition block
12468d69237SJean Perier // arguments except the induction variable on the last iteration.
125149ad3d5SShraiysh Vaishay auto args = loop.getFinalValue()
12668d69237SJean Perier ? conditionalBlock->getArguments()
12768d69237SJean Perier : conditionalBlock->getArguments().drop_front();
12868d69237SJean Perier rewriter.replaceOp(loop, args.drop_back());
12968d69237SJean Perier return success();
13068d69237SJean Perier }
13168d69237SJean Perier
13268d69237SJean Perier private:
13368d69237SJean Perier bool forceLoopToExecuteOnce;
13468d69237SJean Perier };
13568d69237SJean Perier
13668d69237SJean Perier /// Convert `fir.if` to control-flow
13768d69237SJean Perier class CfgIfConv : public mlir::OpRewritePattern<fir::IfOp> {
13868d69237SJean Perier public:
13968d69237SJean Perier using OpRewritePattern::OpRewritePattern;
14068d69237SJean Perier
CfgIfConv(mlir::MLIRContext * ctx,bool forceLoopToExecuteOnce)14168d69237SJean Perier CfgIfConv(mlir::MLIRContext *ctx, bool forceLoopToExecuteOnce)
142665970d4SValentin Clement : mlir::OpRewritePattern<fir::IfOp>(ctx) {}
14368d69237SJean Perier
14468d69237SJean Perier mlir::LogicalResult
matchAndRewrite(IfOp ifOp,mlir::PatternRewriter & rewriter) const14568d69237SJean Perier matchAndRewrite(IfOp ifOp, mlir::PatternRewriter &rewriter) const override {
14668d69237SJean Perier auto loc = ifOp.getLoc();
14768d69237SJean Perier
14868d69237SJean Perier // Split the block containing the 'fir.if' into two parts. The part before
14968d69237SJean Perier // will contain the condition, the part after will be the continuation
15068d69237SJean Perier // point.
15168d69237SJean Perier auto *condBlock = rewriter.getInsertionBlock();
15268d69237SJean Perier auto opPosition = rewriter.getInsertionPoint();
15368d69237SJean Perier auto *remainingOpsBlock = rewriter.splitBlock(condBlock, opPosition);
15468d69237SJean Perier mlir::Block *continueBlock;
15568d69237SJean Perier if (ifOp.getNumResults() == 0) {
15668d69237SJean Perier continueBlock = remainingOpsBlock;
15768d69237SJean Perier } else {
158*9aeb7f03SValentin Clement continueBlock = rewriter.createBlock(
159*9aeb7f03SValentin Clement remainingOpsBlock, ifOp.getResultTypes(),
160*9aeb7f03SValentin Clement llvm::SmallVector<mlir::Location>(ifOp.getNumResults(), loc));
161ace01605SRiver Riddle rewriter.create<mlir::cf::BranchOp>(loc, remainingOpsBlock);
16268d69237SJean Perier }
16368d69237SJean Perier
16468d69237SJean Perier // Move blocks from the "then" region to the region containing 'fir.if',
16568d69237SJean Perier // place it before the continuation block, and branch to it.
166149ad3d5SShraiysh Vaishay auto &ifOpRegion = ifOp.getThenRegion();
16768d69237SJean Perier auto *ifOpBlock = &ifOpRegion.front();
16868d69237SJean Perier auto *ifOpTerminator = ifOpRegion.back().getTerminator();
16968d69237SJean Perier auto ifOpTerminatorOperands = ifOpTerminator->getOperands();
17068d69237SJean Perier rewriter.setInsertionPointToEnd(&ifOpRegion.back());
171ace01605SRiver Riddle rewriter.create<mlir::cf::BranchOp>(loc, continueBlock,
172ace01605SRiver Riddle ifOpTerminatorOperands);
17368d69237SJean Perier rewriter.eraseOp(ifOpTerminator);
17468d69237SJean Perier rewriter.inlineRegionBefore(ifOpRegion, continueBlock);
17568d69237SJean Perier
17668d69237SJean Perier // Move blocks from the "else" region (if present) to the region containing
17768d69237SJean Perier // 'fir.if', place it before the continuation block and branch to it. It
17868d69237SJean Perier // will be placed after the "then" regions.
17968d69237SJean Perier auto *otherwiseBlock = continueBlock;
180149ad3d5SShraiysh Vaishay auto &otherwiseRegion = ifOp.getElseRegion();
18168d69237SJean Perier if (!otherwiseRegion.empty()) {
18268d69237SJean Perier otherwiseBlock = &otherwiseRegion.front();
18368d69237SJean Perier auto *otherwiseTerm = otherwiseRegion.back().getTerminator();
18468d69237SJean Perier auto otherwiseTermOperands = otherwiseTerm->getOperands();
18568d69237SJean Perier rewriter.setInsertionPointToEnd(&otherwiseRegion.back());
186ace01605SRiver Riddle rewriter.create<mlir::cf::BranchOp>(loc, continueBlock,
18768d69237SJean Perier otherwiseTermOperands);
18868d69237SJean Perier rewriter.eraseOp(otherwiseTerm);
18968d69237SJean Perier rewriter.inlineRegionBefore(otherwiseRegion, continueBlock);
19068d69237SJean Perier }
19168d69237SJean Perier
19268d69237SJean Perier rewriter.setInsertionPointToEnd(condBlock);
193ace01605SRiver Riddle rewriter.create<mlir::cf::CondBranchOp>(
194149ad3d5SShraiysh Vaishay loc, ifOp.getCondition(), ifOpBlock, llvm::ArrayRef<mlir::Value>(),
19568d69237SJean Perier otherwiseBlock, llvm::ArrayRef<mlir::Value>());
19668d69237SJean Perier rewriter.replaceOp(ifOp, continueBlock->getArguments());
19768d69237SJean Perier return success();
19868d69237SJean Perier }
19968d69237SJean Perier };
20068d69237SJean Perier
20168d69237SJean Perier /// Convert `fir.iter_while` to control-flow.
20268d69237SJean Perier class CfgIterWhileConv : public mlir::OpRewritePattern<fir::IterWhileOp> {
20368d69237SJean Perier public:
20468d69237SJean Perier using OpRewritePattern::OpRewritePattern;
20568d69237SJean Perier
CfgIterWhileConv(mlir::MLIRContext * ctx,bool forceLoopToExecuteOnce)20668d69237SJean Perier CfgIterWhileConv(mlir::MLIRContext *ctx, bool forceLoopToExecuteOnce)
207665970d4SValentin Clement : mlir::OpRewritePattern<fir::IterWhileOp>(ctx) {}
20868d69237SJean Perier
20968d69237SJean Perier mlir::LogicalResult
matchAndRewrite(fir::IterWhileOp whileOp,mlir::PatternRewriter & rewriter) const21068d69237SJean Perier matchAndRewrite(fir::IterWhileOp whileOp,
21168d69237SJean Perier mlir::PatternRewriter &rewriter) const override {
21268d69237SJean Perier auto loc = whileOp.getLoc();
21368d69237SJean Perier
21468d69237SJean Perier // Start by splitting the block containing the 'fir.do_loop' into two parts.
21568d69237SJean Perier // The part before will get the init code, the part after will be the end
21668d69237SJean Perier // point.
21768d69237SJean Perier auto *initBlock = rewriter.getInsertionBlock();
21868d69237SJean Perier auto initPosition = rewriter.getInsertionPoint();
21968d69237SJean Perier auto *endBlock = rewriter.splitBlock(initBlock, initPosition);
22068d69237SJean Perier
22168d69237SJean Perier // Use the first block of the loop body as the condition block since it is
22268d69237SJean Perier // the block that has the induction variable and loop-carried values as
22368d69237SJean Perier // arguments. Split out all operations from the first block into a new
22468d69237SJean Perier // block. Move all body blocks from the loop body region to the region
22568d69237SJean Perier // containing the loop.
226149ad3d5SShraiysh Vaishay auto *conditionBlock = &whileOp.getRegion().front();
22768d69237SJean Perier auto *firstBodyBlock =
22868d69237SJean Perier rewriter.splitBlock(conditionBlock, conditionBlock->begin());
229149ad3d5SShraiysh Vaishay auto *lastBodyBlock = &whileOp.getRegion().back();
230149ad3d5SShraiysh Vaishay rewriter.inlineRegionBefore(whileOp.getRegion(), endBlock);
23168d69237SJean Perier auto iv = conditionBlock->getArgument(0);
23268d69237SJean Perier auto iterateVar = conditionBlock->getArgument(1);
23368d69237SJean Perier
23468d69237SJean Perier // Append the induction variable stepping logic to the last body block and
23568d69237SJean Perier // branch back to the condition block. Loop-carried values are taken from
23668d69237SJean Perier // operands of the loop terminator.
23768d69237SJean Perier auto *terminator = lastBodyBlock->getTerminator();
23868d69237SJean Perier rewriter.setInsertionPointToEnd(lastBodyBlock);
239149ad3d5SShraiysh Vaishay auto step = whileOp.getStep();
240a54f4eaeSMogball mlir::Value stepped = rewriter.create<mlir::arith::AddIOp>(loc, iv, step);
24168d69237SJean Perier assert(stepped && "must be a Value");
24268d69237SJean Perier
24368d69237SJean Perier llvm::SmallVector<mlir::Value> loopCarried;
24468d69237SJean Perier loopCarried.push_back(stepped);
245149ad3d5SShraiysh Vaishay auto begin = whileOp.getFinalValue()
246149ad3d5SShraiysh Vaishay ? std::next(terminator->operand_begin())
24768d69237SJean Perier : terminator->operand_begin();
24868d69237SJean Perier loopCarried.append(begin, terminator->operand_end());
249ace01605SRiver Riddle rewriter.create<mlir::cf::BranchOp>(loc, conditionBlock, loopCarried);
25068d69237SJean Perier rewriter.eraseOp(terminator);
25168d69237SJean Perier
25268d69237SJean Perier // Compute loop bounds before branching to the condition.
25368d69237SJean Perier rewriter.setInsertionPointToEnd(initBlock);
254149ad3d5SShraiysh Vaishay auto lowerBound = whileOp.getLowerBound();
255149ad3d5SShraiysh Vaishay auto upperBound = whileOp.getUpperBound();
25668d69237SJean Perier assert(lowerBound && upperBound && "must be a Value");
25768d69237SJean Perier
25868d69237SJean Perier // The initial values of loop-carried values is obtained from the operands
25968d69237SJean Perier // of the loop operation.
26068d69237SJean Perier llvm::SmallVector<mlir::Value> destOperands;
26168d69237SJean Perier destOperands.push_back(lowerBound);
26268d69237SJean Perier auto iterOperands = whileOp.getIterOperands();
26368d69237SJean Perier destOperands.append(iterOperands.begin(), iterOperands.end());
264ace01605SRiver Riddle rewriter.create<mlir::cf::BranchOp>(loc, conditionBlock, destOperands);
26568d69237SJean Perier
26668d69237SJean Perier // With the body block done, we can fill in the condition block.
26768d69237SJean Perier rewriter.setInsertionPointToEnd(conditionBlock);
26868d69237SJean Perier // The comparison depends on the sign of the step value. We fully expect
26968d69237SJean Perier // this expression to be folded by the optimizer or LLVM. This expression
27068d69237SJean Perier // is written this way so that `step == 0` always returns `false`.
271a54f4eaeSMogball auto zero = rewriter.create<mlir::arith::ConstantIndexOp>(loc, 0);
272a54f4eaeSMogball auto compl0 = rewriter.create<mlir::arith::CmpIOp>(
273a54f4eaeSMogball loc, arith::CmpIPredicate::slt, zero, step);
274a54f4eaeSMogball auto compl1 = rewriter.create<mlir::arith::CmpIOp>(
275a54f4eaeSMogball loc, arith::CmpIPredicate::sle, iv, upperBound);
276a54f4eaeSMogball auto compl2 = rewriter.create<mlir::arith::CmpIOp>(
277a54f4eaeSMogball loc, arith::CmpIPredicate::slt, step, zero);
278a54f4eaeSMogball auto compl3 = rewriter.create<mlir::arith::CmpIOp>(
279a54f4eaeSMogball loc, arith::CmpIPredicate::sle, upperBound, iv);
280a54f4eaeSMogball auto cmp0 = rewriter.create<mlir::arith::AndIOp>(loc, compl0, compl1);
281a54f4eaeSMogball auto cmp1 = rewriter.create<mlir::arith::AndIOp>(loc, compl2, compl3);
282a54f4eaeSMogball auto cmp2 = rewriter.create<mlir::arith::OrIOp>(loc, cmp0, cmp1);
28368d69237SJean Perier // Remember to AND in the early-exit bool.
284a54f4eaeSMogball auto comparison =
285a54f4eaeSMogball rewriter.create<mlir::arith::AndIOp>(loc, iterateVar, cmp2);
286ace01605SRiver Riddle rewriter.create<mlir::cf::CondBranchOp>(
287ace01605SRiver Riddle loc, comparison, firstBodyBlock, llvm::ArrayRef<mlir::Value>(),
288ace01605SRiver Riddle endBlock, llvm::ArrayRef<mlir::Value>());
28968d69237SJean Perier // The result of the loop operation is the values of the condition block
29068d69237SJean Perier // arguments except the induction variable on the last iteration.
291149ad3d5SShraiysh Vaishay auto args = whileOp.getFinalValue()
29268d69237SJean Perier ? conditionBlock->getArguments()
29368d69237SJean Perier : conditionBlock->getArguments().drop_front();
29468d69237SJean Perier rewriter.replaceOp(whileOp, args);
29568d69237SJean Perier return success();
29668d69237SJean Perier }
29768d69237SJean Perier };
29868d69237SJean Perier
29968d69237SJean Perier /// Convert FIR structured control flow ops to CFG ops.
30068d69237SJean Perier class CfgConversion : public CFGConversionBase<CfgConversion> {
30168d69237SJean Perier public:
runOnOperation()302196c4279SRiver Riddle void runOnOperation() override {
30368d69237SJean Perier auto *context = &getContext();
3049f85c198SRiver Riddle mlir::RewritePatternSet patterns(context);
30568d69237SJean Perier patterns.insert<CfgLoopConv, CfgIfConv, CfgIterWhileConv>(
30668d69237SJean Perier context, forceLoopToExecuteOnce);
30768d69237SJean Perier mlir::ConversionTarget target(*context);
308ace01605SRiver Riddle target.addLegalDialect<mlir::AffineDialect, mlir::cf::ControlFlowDialect,
30923aa5a74SRiver Riddle FIROpsDialect, mlir::func::FuncDialect>();
31068d69237SJean Perier
31168d69237SJean Perier // apply the patterns
31268d69237SJean Perier target.addIllegalOp<ResultOp, DoLoopOp, IfOp, IterWhileOp>();
31368d69237SJean Perier target.markUnknownOpDynamicallyLegal([](Operation *) { return true; });
314196c4279SRiver Riddle if (mlir::failed(mlir::applyPartialConversion(getOperation(), target,
31568d69237SJean Perier std::move(patterns)))) {
31668d69237SJean Perier mlir::emitError(mlir::UnknownLoc::get(context),
31768d69237SJean Perier "error in converting to CFG\n");
31868d69237SJean Perier signalPassFailure();
31968d69237SJean Perier }
32068d69237SJean Perier }
32168d69237SJean Perier };
32268d69237SJean Perier } // namespace
32368d69237SJean Perier
32468d69237SJean Perier /// Convert FIR's structured control flow ops to CFG ops. This
32568d69237SJean Perier /// conversion enables the `createLowerToCFGPass` to transform these to CFG
32668d69237SJean Perier /// form.
createFirToCfgPass()32768d69237SJean Perier std::unique_ptr<mlir::Pass> fir::createFirToCfgPass() {
32868d69237SJean Perier return std::make_unique<CfgConversion>();
32968d69237SJean Perier }
330