From fd3b556958a9deee7ced33ec18b38c030145160b Mon Sep 17 00:00:00 2001 From: Fraser Cormack Date: Mon, 31 May 2021 18:31:55 +0100 Subject: [PATCH] [Constants] Extend support for scalable-vector splats This patch extends the various "isXXX" functions of the `Constant` class to include scalable-vector splats. In several "isXXX" functions, code that was separately inspecting `ConstantVector` and `ConstantDataVector` was unified to use `getSplatValue`, which already includes support for said splats. In the varous "isNotXXX" functions, code was added to check whether the scalar splat value -- if any -- satisfies the predicate. An extra fix for `isNotMinSignedValue` was included, as it previously crashed when passed a scalable-vector type because it unconditionally cast to `FixedVectorType` These changes address numerous missed optimizations, a compiler crash mentioned above and -- perhaps most egregiously -- an infinite loop in InstCombine due to the compiler breaking canonical form when it failed to pick up on a splat in a select instruction. Test cases have been added to cover as many of these functions as possible, though existing coverage is slim; it doesn't appear that there are any in-tree uses of `Constant::isNegativeZeroValue`, for example. Reviewed By: RKSimon Differential Revision: https://reviews.llvm.org/D103421 --- llvm/lib/IR/Constants.cpp | 193 +++++++++++++-------------- llvm/test/Transforms/InstCombine/bitcast.ll | 8 ++ llvm/test/Transforms/InstCombine/div.ll | 42 ++++++ llvm/test/Transforms/InstCombine/fdiv.ll | 9 ++ llvm/test/Transforms/InstCombine/fmul.ll | 25 ++++ llvm/test/Transforms/InstCombine/icmp-vec.ll | 26 ++++ llvm/test/Transforms/InstCombine/sub.ll | 40 ++++++ 7 files changed, 245 insertions(+), 98 deletions(-) diff --git a/llvm/lib/IR/Constants.cpp b/llvm/lib/IR/Constants.cpp index c3134dc..147e96c 100644 --- a/llvm/lib/IR/Constants.cpp +++ b/llvm/lib/IR/Constants.cpp @@ -43,15 +43,9 @@ bool Constant::isNegativeZeroValue() const { return CFP->isZero() && CFP->isNegative(); // Equivalent for a vector of -0.0's. - if (const ConstantDataVector *CV = dyn_cast(this)) - if (CV->getElementType()->isFloatingPointTy() && CV->isSplat()) - if (CV->getElementAsAPFloat(0).isNegZero()) - return true; - - if (const ConstantVector *CV = dyn_cast(this)) - if (ConstantFP *SplatCFP = dyn_cast_or_null(CV->getSplatValue())) - if (SplatCFP && SplatCFP->isZero() && SplatCFP->isNegative()) - return true; + if (getType()->isVectorTy()) + if (const auto *SplatCFP = dyn_cast_or_null(getSplatValue())) + return SplatCFP->isNegativeZeroValue(); // We've already handled true FP case; any other FP vectors can't represent -0.0. if (getType()->isFPOrFPVectorTy()) @@ -68,16 +62,10 @@ bool Constant::isZeroValue() const { if (const ConstantFP *CFP = dyn_cast(this)) return CFP->isZero(); - // Equivalent for a vector of -0.0's. - if (const ConstantDataVector *CV = dyn_cast(this)) - if (CV->getElementType()->isFloatingPointTy() && CV->isSplat()) - if (CV->getElementAsAPFloat(0).isZero()) - return true; - - if (const ConstantVector *CV = dyn_cast(this)) - if (ConstantFP *SplatCFP = dyn_cast_or_null(CV->getSplatValue())) - if (SplatCFP && SplatCFP->isZero()) - return true; + // Check for constant splat vectors of 1 values. + if (getType()->isVectorTy()) + if (const auto *SplatCFP = dyn_cast_or_null(getSplatValue())) + return SplatCFP->isZero(); // Otherwise, just use +0.0. return isNullValue(); @@ -109,19 +97,10 @@ bool Constant::isAllOnesValue() const { if (const ConstantFP *CFP = dyn_cast(this)) return CFP->getValueAPF().bitcastToAPInt().isAllOnesValue(); - // Check for constant vectors which are splats of -1 values. - if (const ConstantVector *CV = dyn_cast(this)) - if (Constant *Splat = CV->getSplatValue()) - return Splat->isAllOnesValue(); - - // Check for constant vectors which are splats of -1 values. - if (const ConstantDataVector *CV = dyn_cast(this)) { - if (CV->isSplat()) { - if (CV->getElementType()->isFloatingPointTy()) - return CV->getElementAsAPFloat(0).bitcastToAPInt().isAllOnesValue(); - return CV->getElementAsAPInt(0).isAllOnesValue(); - } - } + // Check for constant splat vectors of 1 values. + if (getType()->isVectorTy()) + if (const auto *SplatVal = getSplatValue()) + return SplatVal->isAllOnesValue(); return false; } @@ -135,19 +114,10 @@ bool Constant::isOneValue() const { if (const ConstantFP *CFP = dyn_cast(this)) return CFP->getValueAPF().bitcastToAPInt().isOneValue(); - // Check for constant vectors which are splats of 1 values. - if (const ConstantVector *CV = dyn_cast(this)) - if (Constant *Splat = CV->getSplatValue()) - return Splat->isOneValue(); - - // Check for constant vectors which are splats of 1 values. - if (const ConstantDataVector *CV = dyn_cast(this)) { - if (CV->isSplat()) { - if (CV->getElementType()->isFloatingPointTy()) - return CV->getElementAsAPFloat(0).bitcastToAPInt().isOneValue(); - return CV->getElementAsAPInt(0).isOneValue(); - } - } + // Check for constant splat vectors of 1 values. + if (getType()->isVectorTy()) + if (const auto *SplatVal = getSplatValue()) + return SplatVal->isOneValue(); return false; } @@ -162,16 +132,20 @@ bool Constant::isNotOneValue() const { return !CFP->getValueAPF().bitcastToAPInt().isOneValue(); // Check that vectors don't contain 1 - if (auto *VTy = dyn_cast(this->getType())) { - unsigned NumElts = cast(VTy)->getNumElements(); - for (unsigned i = 0; i != NumElts; ++i) { - Constant *Elt = this->getAggregateElement(i); + if (auto *VTy = dyn_cast(getType())) { + for (unsigned I = 0, E = VTy->getNumElements(); I != E; ++I) { + Constant *Elt = getAggregateElement(I); if (!Elt || !Elt->isNotOneValue()) return false; } return true; } + // Check for splats that don't contain 1 + if (getType()->isVectorTy()) + if (const auto *SplatVal = getSplatValue()) + return SplatVal->isNotOneValue(); + // It *may* contain 1, we can't tell. return false; } @@ -185,19 +159,10 @@ bool Constant::isMinSignedValue() const { if (const ConstantFP *CFP = dyn_cast(this)) return CFP->getValueAPF().bitcastToAPInt().isMinSignedValue(); - // Check for constant vectors which are splats of INT_MIN values. - if (const ConstantVector *CV = dyn_cast(this)) - if (Constant *Splat = CV->getSplatValue()) - return Splat->isMinSignedValue(); - - // Check for constant vectors which are splats of INT_MIN values. - if (const ConstantDataVector *CV = dyn_cast(this)) { - if (CV->isSplat()) { - if (CV->getElementType()->isFloatingPointTy()) - return CV->getElementAsAPFloat(0).bitcastToAPInt().isMinSignedValue(); - return CV->getElementAsAPInt(0).isMinSignedValue(); - } - } + // Check for splats of INT_MIN values. + if (getType()->isVectorTy()) + if (const auto *SplatVal = getSplatValue()) + return SplatVal->isMinSignedValue(); return false; } @@ -212,16 +177,20 @@ bool Constant::isNotMinSignedValue() const { return !CFP->getValueAPF().bitcastToAPInt().isMinSignedValue(); // Check that vectors don't contain INT_MIN - if (auto *VTy = dyn_cast(this->getType())) { - unsigned NumElts = cast(VTy)->getNumElements(); - for (unsigned i = 0; i != NumElts; ++i) { - Constant *Elt = this->getAggregateElement(i); + if (auto *VTy = dyn_cast(getType())) { + for (unsigned I = 0, E = VTy->getNumElements(); I != E; ++I) { + Constant *Elt = getAggregateElement(I); if (!Elt || !Elt->isNotMinSignedValue()) return false; } return true; } + // Check for splats that aren't INT_MIN + if (getType()->isVectorTy()) + if (const auto *SplatVal = getSplatValue()) + return SplatVal->isNotMinSignedValue(); + // It *may* contain INT_MIN, we can't tell. return false; } @@ -229,57 +198,85 @@ bool Constant::isNotMinSignedValue() const { bool Constant::isFiniteNonZeroFP() const { if (auto *CFP = dyn_cast(this)) return CFP->getValueAPF().isFiniteNonZero(); - auto *VTy = dyn_cast(getType()); - if (!VTy) - return false; - for (unsigned i = 0, e = VTy->getNumElements(); i != e; ++i) { - auto *CFP = dyn_cast_or_null(this->getAggregateElement(i)); - if (!CFP || !CFP->getValueAPF().isFiniteNonZero()) - return false; + + if (auto *VTy = dyn_cast(getType())) { + for (unsigned I = 0, E = VTy->getNumElements(); I != E; ++I) { + auto *CFP = dyn_cast_or_null(getAggregateElement(I)); + if (!CFP || !CFP->getValueAPF().isFiniteNonZero()) + return false; + } + return true; } - return true; + + if (getType()->isVectorTy()) + if (const auto *SplatCFP = dyn_cast_or_null(getSplatValue())) + return SplatCFP->isFiniteNonZeroFP(); + + // It *may* contain finite non-zero, we can't tell. + return false; } bool Constant::isNormalFP() const { if (auto *CFP = dyn_cast(this)) return CFP->getValueAPF().isNormal(); - auto *VTy = dyn_cast(getType()); - if (!VTy) - return false; - for (unsigned i = 0, e = VTy->getNumElements(); i != e; ++i) { - auto *CFP = dyn_cast_or_null(this->getAggregateElement(i)); - if (!CFP || !CFP->getValueAPF().isNormal()) - return false; + + if (auto *VTy = dyn_cast(getType())) { + for (unsigned I = 0, E = VTy->getNumElements(); I != E; ++I) { + auto *CFP = dyn_cast_or_null(getAggregateElement(I)); + if (!CFP || !CFP->getValueAPF().isNormal()) + return false; + } + return true; } - return true; + + if (getType()->isVectorTy()) + if (const auto *SplatCFP = dyn_cast_or_null(getSplatValue())) + return SplatCFP->isNormalFP(); + + // It *may* contain a normal fp value, we can't tell. + return false; } bool Constant::hasExactInverseFP() const { if (auto *CFP = dyn_cast(this)) return CFP->getValueAPF().getExactInverse(nullptr); - auto *VTy = dyn_cast(getType()); - if (!VTy) - return false; - for (unsigned i = 0, e = VTy->getNumElements(); i != e; ++i) { - auto *CFP = dyn_cast_or_null(this->getAggregateElement(i)); - if (!CFP || !CFP->getValueAPF().getExactInverse(nullptr)) - return false; + + if (auto *VTy = dyn_cast(getType())) { + for (unsigned I = 0, E = VTy->getNumElements(); I != E; ++I) { + auto *CFP = dyn_cast_or_null(getAggregateElement(I)); + if (!CFP || !CFP->getValueAPF().getExactInverse(nullptr)) + return false; + } + return true; } - return true; + + if (getType()->isVectorTy()) + if (const auto *SplatCFP = dyn_cast_or_null(getSplatValue())) + return SplatCFP->hasExactInverseFP(); + + // It *may* have an exact inverse fp value, we can't tell. + return false; } bool Constant::isNaN() const { if (auto *CFP = dyn_cast(this)) return CFP->isNaN(); - auto *VTy = dyn_cast(getType()); - if (!VTy) - return false; - for (unsigned i = 0, e = VTy->getNumElements(); i != e; ++i) { - auto *CFP = dyn_cast_or_null(this->getAggregateElement(i)); - if (!CFP || !CFP->isNaN()) - return false; + + if (auto *VTy = dyn_cast(getType())) { + for (unsigned I = 0, E = VTy->getNumElements(); I != E; ++I) { + auto *CFP = dyn_cast_or_null(getAggregateElement(I)); + if (!CFP || !CFP->isNaN()) + return false; + } + return true; } - return true; + + if (getType()->isVectorTy()) + if (const auto *SplatCFP = dyn_cast_or_null(getSplatValue())) + return SplatCFP->isNaN(); + + // It *may* be NaN, we can't tell. + return false; } bool Constant::isElementWiseEqual(Value *Y) const { diff --git a/llvm/test/Transforms/InstCombine/bitcast.ll b/llvm/test/Transforms/InstCombine/bitcast.ll index 5f89bb9..61e0327 100644 --- a/llvm/test/Transforms/InstCombine/bitcast.ll +++ b/llvm/test/Transforms/InstCombine/bitcast.ll @@ -465,6 +465,14 @@ define i32 @All111(i32 %in) { ret i32 %out } +define @ScalableAll111( %in) { +; CHECK-LABEL: @ScalableAll111( +; CHECK-NEXT: ret [[IN:%.*]] +; + %out = and %in, bitcast ( shufflevector ( insertelement ( undef, i16 -1, i32 0), undef, zeroinitializer) to ) + ret %out +} + define <2 x i16> @BitcastInsert(i32 %a) { ; CHECK-LABEL: @BitcastInsert( ; CHECK-NEXT: [[R:%.*]] = bitcast i32 [[A:%.*]] to <2 x i16> diff --git a/llvm/test/Transforms/InstCombine/div.ll b/llvm/test/Transforms/InstCombine/div.ll index 835db5e2..485cadf 100644 --- a/llvm/test/Transforms/InstCombine/div.ll +++ b/llvm/test/Transforms/InstCombine/div.ll @@ -1045,3 +1045,45 @@ define <2 x i8> @sdiv_by_int_min_vec_splat_undef(<2 x i8> %x) { %d = sdiv <2 x i8> %x, ret <2 x i8> %d } + +define <2 x i8> @sdiv_by_negconst_v2i8(<2 x i8> %x) { +; CHECK-LABEL: @sdiv_by_negconst_v2i8( +; CHECK-NEXT: [[DIV_NEG:%.*]] = sdiv <2 x i8> [[X:%.*]], +; CHECK-NEXT: ret <2 x i8> [[DIV_NEG]] +; + %div = sdiv <2 x i8> %x, + %sub = sub <2 x i8> zeroinitializer, %div + ret <2 x i8> %sub +} + +define @sdiv_by_negconst_nxv2i8( %x) { +; CHECK-LABEL: @sdiv_by_negconst_nxv2i8( +; CHECK-NEXT: [[DIV_NEG:%.*]] = sdiv [[X:%.*]], shufflevector ( insertelement ( undef, i8 108, i32 0), undef, zeroinitializer) +; CHECK-NEXT: ret [[DIV_NEG]] +; + %div = sdiv %x, shufflevector ( insertelement ( undef, i8 -108, i32 0), undef, zeroinitializer) + %sub = sub zeroinitializer, %div + ret %sub +} + +define <2 x i8> @sdiv_by_minSigned_v2i8(<2 x i8> %x) { +; CHECK-LABEL: @sdiv_by_minSigned_v2i8( +; CHECK-NEXT: [[TMP1:%.*]] = icmp eq <2 x i8> [[X:%.*]], +; CHECK-NEXT: [[DIV_NEG:%.*]] = sext <2 x i1> [[TMP1]] to <2 x i8> +; CHECK-NEXT: ret <2 x i8> [[DIV_NEG]] +; + %div = sdiv <2 x i8> %x, + %sub = sub <2 x i8> zeroinitializer, %div + ret <2 x i8> %sub +} + +define @sdiv_by_minSigned_nxv2i8( %x) { +; CHECK-LABEL: @sdiv_by_minSigned_nxv2i8( +; CHECK-NEXT: [[TMP1:%.*]] = icmp eq [[X:%.*]], shufflevector ( insertelement ( undef, i8 -128, i32 0), undef, zeroinitializer) +; CHECK-NEXT: [[DIV_NEG:%.*]] = sext [[TMP1]] to +; CHECK-NEXT: ret [[DIV_NEG]] +; + %div = sdiv %x, shufflevector ( insertelement ( undef, i8 -128, i32 0), undef, zeroinitializer) + %sub = sub zeroinitializer, %div + ret %sub +} diff --git a/llvm/test/Transforms/InstCombine/fdiv.ll b/llvm/test/Transforms/InstCombine/fdiv.ll index ff4fa2b..72325f6 100644 --- a/llvm/test/Transforms/InstCombine/fdiv.ll +++ b/llvm/test/Transforms/InstCombine/fdiv.ll @@ -85,6 +85,15 @@ define <2 x float> @exact_inverse_splat(<2 x float> %x) { ret <2 x float> %div } +define @exact_inverse_scalable_splat( %x) { +; CHECK-LABEL: @exact_inverse_scalable_splat( +; CHECK-NEXT: [[DIV:%.*]] = fmul [[X:%.*]], shufflevector ( insertelement ( undef, float 2.500000e-01, i32 0), undef, zeroinitializer) +; CHECK-NEXT: ret [[DIV]] +; + %div = fdiv %x, shufflevector ( insertelement ( undef, float 4.0, i32 0), undef, zeroinitializer) + ret %div +} + ; Fast math allows us to replace this fdiv. define <2 x float> @not_exact_but_allow_recip_splat(<2 x float> %x) { diff --git a/llvm/test/Transforms/InstCombine/fmul.ll b/llvm/test/Transforms/InstCombine/fmul.ll index 4162973..fff6d26 100644 --- a/llvm/test/Transforms/InstCombine/fmul.ll +++ b/llvm/test/Transforms/InstCombine/fmul.ll @@ -781,6 +781,30 @@ define float @fmul_fadd_distribute(float %x) { ret float %t3 } +define <2 x float> @fmul_fadd_distribute_vec(<2 x float> %x) { +; CHECK-LABEL: @fmul_fadd_distribute_vec( +; CHECK-NEXT: [[TMP1:%.*]] = fmul reassoc <2 x float> [[X:%.*]], +; CHECK-NEXT: [[T3:%.*]] = fadd reassoc <2 x float> [[TMP1]], +; CHECK-NEXT: ret <2 x float> [[T3]] +; + %t1 = fadd <2 x float> , %x + %t3 = fmul reassoc <2 x float> %t1, + ret <2 x float> %t3 +} + +define @fmul_fadd_distribute_scalablevec( %x) { +; CHECK-LABEL: @fmul_fadd_distribute_scalablevec( +; CHECK-NEXT: [[TMP1:%.*]] = fmul reassoc [[X:%.*]], shufflevector ( insertelement ( undef, float 6.000000e+03, i32 0), undef, zeroinitializer) +; CHECK-NEXT: [[T3:%.*]] = fadd reassoc [[TMP1]], shufflevector ( insertelement ( undef, float 1.200000e+07, i32 0), undef, zeroinitializer) +; CHECK-NEXT: ret [[T3]] +; + %t1 = fadd shufflevector ( insertelement ( undef, float 2.0e+3, i32 0), undef, zeroinitializer), %x + %t3 = fmul reassoc %t1, shufflevector ( insertelement ( undef, float 6.0e+3, i32 0), undef, zeroinitializer) + + + ret %t3 +} + ; (X - C1) * C2 --> (X * C2) - C1*C2 define float @fmul_fsub_distribute1(float %x) { @@ -1169,6 +1193,7 @@ define double @fmul_sqrt_select(double %x, i1 %c) { define @mul_scalable_splat_zero( %z) { ; CHECK-LABEL: @mul_scalable_splat_zero( ; CHECK-NEXT: ret zeroinitializer +; %shuf = shufflevector insertelement ( undef, float 0.0, i32 0), undef, zeroinitializer %t3 = fmul fast %shuf, %z ret %t3 diff --git a/llvm/test/Transforms/InstCombine/icmp-vec.ll b/llvm/test/Transforms/InstCombine/icmp-vec.ll index 4bd6205..13690a8 100644 --- a/llvm/test/Transforms/InstCombine/icmp-vec.ll +++ b/llvm/test/Transforms/InstCombine/icmp-vec.ll @@ -373,3 +373,29 @@ define <4 x i1> @not_splat_icmp2(<4 x i8> %x) { %cmp = icmp sgt <4 x i8> %splatx, ret <4 x i1> %cmp } + +; Check that we don't absorb the compare into the select, which is in the +; canonical form of logical or. +define <2 x i1> @icmp_logical_or_vec(<2 x i64> %x, <2 x i64> %y, <2 x i1> %falseval) { +; CHECK-LABEL: @icmp_logical_or_vec( +; CHECK-NEXT: [[CMP_NE:%.*]] = icmp ne <2 x i64> [[X:%.*]], zeroinitializer +; CHECK-NEXT: [[SEL:%.*]] = select <2 x i1> [[CMP_NE]], <2 x i1> , <2 x i1> [[FALSEVAL:%.*]] +; CHECK-NEXT: ret <2 x i1> [[SEL]] +; + %cmp.ne = icmp ne <2 x i64> %x, zeroinitializer + %sel = select <2 x i1> %cmp.ne, <2 x i1> shufflevector (<2 x i1> insertelement (<2 x i1> undef, i1 true, i32 0), <2 x i1> undef, <2 x i32> zeroinitializer), <2 x i1> %falseval + ret <2 x i1> %sel +} + +; The above, but for scalable vectors. Absorbing the compare into the select +; and breaking the canonical form led to an infinite loop. +define @icmp_logical_or_scalablevec( %x, %y, %falseval) { +; CHECK-LABEL: @icmp_logical_or_scalablevec( +; CHECK-NEXT: [[CMP_NE:%.*]] = icmp ne [[X:%.*]], zeroinitializer +; CHECK-NEXT: [[SEL:%.*]] = select [[CMP_NE]], shufflevector ( insertelement ( undef, i1 true, i32 0), undef, zeroinitializer), [[FALSEVAL:%.*]] +; CHECK-NEXT: ret [[SEL]] +; + %cmp.ne = icmp ne %x, zeroinitializer + %sel = select %cmp.ne, shufflevector ( insertelement ( undef, i1 true, i32 0), undef, zeroinitializer), %falseval + ret %sel +} diff --git a/llvm/test/Transforms/InstCombine/sub.ll b/llvm/test/Transforms/InstCombine/sub.ll index 649d2e8..f0f3d0c 100644 --- a/llvm/test/Transforms/InstCombine/sub.ll +++ b/llvm/test/Transforms/InstCombine/sub.ll @@ -830,6 +830,46 @@ define i32 @test44(i32 %x) { ret i32 %sub } +define <2 x i32> @test44vec(<2 x i32> %x) { +; CHECK-LABEL: @test44vec( +; CHECK-NEXT: [[SUB:%.*]] = add nsw <2 x i32> [[X:%.*]], +; CHECK-NEXT: ret <2 x i32> [[SUB]] +; + %sub = sub nsw <2 x i32> %x, + ret <2 x i32> %sub +} + +; FIXME: We're not giving this new 'add' a nsw flag as in the fixed-length case +; above. We need to be able catch the splat with dyn_castNegVal. +define @test44scalablevec( %x) { +; CHECK-LABEL: @test44scalablevec( +; CHECK-NEXT: [[SUB:%.*]] = add [[X:%.*]], shufflevector ( insertelement ( undef, i32 -32768, i32 0), undef, zeroinitializer) +; CHECK-NEXT: ret [[SUB]] +; + %sub = sub nsw %x, shufflevector ( insertelement ( undef, i32 32768, i32 0), undef, zeroinitializer) + ret %sub +} + +define <2 x i16> @test44vecminval(<2 x i16> %x) { +; CHECK-LABEL: @test44vecminval( +; CHECK-NEXT: [[SUB:%.*]] = xor <2 x i16> [[X:%.*]], +; CHECK-NEXT: ret <2 x i16> [[SUB]] +; + %sub = sub nsw <2 x i16> %x, + ret <2 x i16> %sub +} + +; FIXME: This isn't combined to xor as above because the pattern in visitSub +; uses m_ImmConstant which matches Constant but (explicitly) not ConstantExpr. +define @test44scalablevecminval( %x) { +; CHECK-LABEL: @test44scalablevecminval( +; CHECK-NEXT: [[SUB:%.*]] = add [[X:%.*]], shufflevector ( insertelement ( undef, i16 -32768, i32 0), undef, zeroinitializer) +; CHECK-NEXT: ret [[SUB]] +; + %sub = sub nsw %x, shufflevector ( insertelement ( undef, i16 -32768, i32 0), undef, zeroinitializer) + ret %sub +} + define i32 @test45(i32 %x, i32 %y) { ; CHECK-LABEL: @test45( ; CHECK-NEXT: [[SUB:%.*]] = and i32 [[X:%.*]], [[Y:%.*]] -- 2.7.4