29 #include "llvm/ADT/ArrayRef.h"
30 #include "llvm/ADT/STLExtras.h"
31 #include "llvm/ADT/SmallVector.h"
32 #include "llvm/ADT/SmallVectorExtras.h"
33 #include "llvm/Support/FormatVariadic.h"
44 return values[0].getDefiningOp<arith::ConstantIndexOp>().value();
47 return cast<IntegerAttr>(attr[0]).getInt();
50 return (*attr.getAsValueRange<IntegerAttr>().begin()).getZExtValue();
53 auto attr = foldResults[0].dyn_cast<
Attribute>();
65 if (
auto vectorType = dyn_cast<VectorType>(type))
66 return vectorType.getNumElements() * vectorType.getElementTypeBitWidth();
76 matchAndRewrite(vector::ShapeCastOp shapeCastOp, OpAdaptor adaptor,
78 Type dstType = getTypeConverter()->convertType(shapeCastOp.getType());
84 if (dstType == adaptor.getSource().getType() ||
85 shapeCastOp.getResultVectorType().getNumElements() == 1) {
86 rewriter.
replaceOp(shapeCastOp, adaptor.getSource());
95 struct VectorBitcastConvert final
100 matchAndRewrite(vector::BitCastOp bitcastOp, OpAdaptor adaptor,
102 Type dstType = getTypeConverter()->convertType(bitcastOp.getType());
106 if (dstType == adaptor.getSource().getType()) {
107 rewriter.
replaceOp(bitcastOp, adaptor.getSource());
114 Type srcType = adaptor.getSource().getType();
118 llvm::formatv(
"different source ({0}) and target ({1}) bitwidth",
123 adaptor.getSource());
128 struct VectorBroadcastConvert final
133 matchAndRewrite(vector::BroadcastOp castOp, OpAdaptor adaptor,
136 getTypeConverter()->convertType(castOp.getResultVectorType());
140 if (isa<spirv::ScalarType>(resultType)) {
141 rewriter.
replaceOp(castOp, adaptor.getSource());
146 adaptor.getSource());
153 struct VectorExtractOpConvert final
158 matchAndRewrite(vector::ExtractOp extractOp, OpAdaptor adaptor,
160 if (extractOp.hasDynamicPosition())
163 Type dstType = getTypeConverter()->convertType(extractOp.getType());
167 if (isa<spirv::ScalarType>(adaptor.getVector().getType())) {
168 rewriter.
replaceOp(extractOp, adaptor.getVector());
174 extractOp, adaptor.getVector(), id);
179 struct VectorExtractStridedSliceOpConvert final
184 matchAndRewrite(vector::ExtractStridedSliceOp extractOp, OpAdaptor adaptor,
186 Type dstType = getTypeConverter()->convertType(extractOp.getType());
196 Value srcVector = adaptor.getOperands().front();
199 if (isa<spirv::ScalarType>(dstType)) {
206 std::iota(indices.begin(), indices.end(), offset);
209 extractOp, dstType, srcVector, srcVector,
216 template <
class SPIRVFMAOp>
221 matchAndRewrite(vector::FMAOp fmaOp, OpAdaptor adaptor,
223 Type dstType = getTypeConverter()->convertType(fmaOp.getType());
227 adaptor.getRhs(), adaptor.getAcc());
232 struct VectorInsertOpConvert final
237 matchAndRewrite(vector::InsertOp insertOp, OpAdaptor adaptor,
239 if (isa<VectorType>(insertOp.getSourceType()))
241 if (!getTypeConverter()->convertType(insertOp.getDestVectorType()))
243 "unsupported dest vector type");
246 if (insertOp.getSourceType().isIntOrFloat() &&
247 insertOp.getDestVectorType().getNumElements() == 1) {
248 rewriter.
replaceOp(insertOp, adaptor.getSource());
254 insertOp, adaptor.getSource(), adaptor.getDest(), id);
259 struct VectorExtractElementOpConvert final
264 matchAndRewrite(vector::ExtractElementOp extractOp, OpAdaptor adaptor,
266 Type resultType = getTypeConverter()->convertType(extractOp.getType());
270 if (isa<spirv::ScalarType>(adaptor.getVector().getType())) {
271 rewriter.
replaceOp(extractOp, adaptor.getVector());
278 extractOp, resultType, adaptor.getVector(),
282 extractOp, resultType, adaptor.getVector(), adaptor.getPosition());
287 struct VectorInsertElementOpConvert final
292 matchAndRewrite(vector::InsertElementOp insertOp, OpAdaptor adaptor,
294 Type vectorType = getTypeConverter()->convertType(insertOp.getType());
298 if (isa<spirv::ScalarType>(vectorType)) {
299 rewriter.
replaceOp(insertOp, adaptor.getSource());
306 insertOp, adaptor.getSource(), adaptor.getDest(),
307 cstPos.getSExtValue());
310 insertOp, vectorType, insertOp.getDest(), adaptor.getSource(),
311 adaptor.getPosition());
316 struct VectorInsertStridedSliceOpConvert final
321 matchAndRewrite(vector::InsertStridedSliceOp insertOp, OpAdaptor adaptor,
323 Value srcVector = adaptor.getOperands().front();
324 Value dstVector = adaptor.getOperands().back();
331 if (isa<spirv::ScalarType>(srcVector.
getType())) {
332 assert(!isa<spirv::ScalarType>(dstVector.
getType()));
334 insertOp, dstVector.
getType(), srcVector, dstVector,
339 uint64_t totalSize = cast<VectorType>(dstVector.getType()).getNumElements();
340 uint64_t insertSize =
341 cast<VectorType>(srcVector.
getType()).getNumElements();
344 std::iota(indices.begin(), indices.end(), 0);
345 std::iota(indices.begin() + offset, indices.begin() + offset + insertSize,
349 insertOp, dstVector.getType(), dstVector, srcVector,
357 vector::ReductionOp reduceOp, vector::ReductionOp::Adaptor adaptor,
359 int numElements =
static_cast<int>(srcVectorType.getDimSize(0));
361 values.reserve(numElements + (adaptor.getAcc() ? 1 : 0));
364 for (
int i = 0; i < numElements; ++i) {
365 values.push_back(rewriter.
create<spirv::CompositeExtractOp>(
366 loc, srcVectorType.getElementType(), adaptor.getVector(),
369 if (
Value acc = adaptor.getAcc())
370 values.push_back(acc);
375 struct ReductionRewriteInfo {
380 FailureOr<ReductionRewriteInfo>
static getReductionInfo(
381 vector::ReductionOp op, vector::ReductionOp::Adaptor adaptor,
387 auto srcVectorType = dyn_cast<VectorType>(adaptor.getVector().getType());
388 if (!srcVectorType || srcVectorType.getRank() != 1)
392 extractAllElements(op, adaptor, srcVectorType, rewriter);
394 return ReductionRewriteInfo{resultType, std::move(extractedElements)};
397 template <
typename SPIRVUMaxOp,
typename SPIRVUMinOp,
typename SPIRVSMaxOp,
398 typename SPIRVSMinOp>
403 matchAndRewrite(vector::ReductionOp reduceOp, OpAdaptor adaptor,
406 getReductionInfo(reduceOp, adaptor, rewriter, *getTypeConverter());
407 if (failed(reductionInfo))
410 auto [resultType, extractedElements] = *reductionInfo;
412 Value result = extractedElements.front();
413 for (
Value next : llvm::drop_begin(extractedElements)) {
414 switch (reduceOp.getKind()) {
416 #define INT_AND_FLOAT_CASE(kind, iop, fop) \
417 case vector::CombiningKind::kind: \
418 if (llvm::isa<IntegerType>(resultType)) { \
419 result = rewriter.create<spirv::iop>(loc, resultType, result, next); \
421 assert(llvm::isa<FloatType>(resultType)); \
422 result = rewriter.create<spirv::fop>(loc, resultType, result, next); \
426 #define INT_OR_FLOAT_CASE(kind, fop) \
427 case vector::CombiningKind::kind: \
428 result = rewriter.create<fop>(loc, resultType, result, next); \
438 case vector::CombiningKind::AND:
439 case vector::CombiningKind::OR:
440 case vector::CombiningKind::XOR:
445 #undef INT_AND_FLOAT_CASE
446 #undef INT_OR_FLOAT_CASE
454 template <
typename SPIRVFMaxOp,
typename SPIRVFMinOp>
455 struct VectorReductionFloatMinMax final
460 matchAndRewrite(vector::ReductionOp reduceOp, OpAdaptor adaptor,
463 getReductionInfo(reduceOp, adaptor, rewriter, *getTypeConverter());
464 if (failed(reductionInfo))
467 auto [resultType, extractedElements] = *reductionInfo;
469 Value result = extractedElements.front();
470 for (
Value next : llvm::drop_begin(extractedElements)) {
471 switch (reduceOp.getKind()) {
473 #define INT_OR_FLOAT_CASE(kind, fop) \
474 case vector::CombiningKind::kind: \
475 result = rewriter.create<fop>(loc, resultType, result, next); \
486 #undef INT_OR_FLOAT_CASE
499 matchAndRewrite(vector::SplatOp op, OpAdaptor adaptor,
501 Type dstType = getTypeConverter()->convertType(op.getType());
504 if (isa<spirv::ScalarType>(dstType)) {
505 rewriter.
replaceOp(op, adaptor.getInput());
507 auto dstVecType = cast<VectorType>(dstType);
517 struct VectorShuffleOpConvert final
522 matchAndRewrite(vector::ShuffleOp shuffleOp, OpAdaptor adaptor,
524 VectorType oldResultType = shuffleOp.getResultVectorType();
525 Type newResultType = getTypeConverter()->convertType(oldResultType);
528 "unsupported result vector type");
531 shuffleOp.getMask(), [](
Attribute attr) -> int32_t {
532 return cast<IntegerAttr>(attr).getValue().getZExtValue();
535 VectorType oldV1Type = shuffleOp.getV1VectorType();
536 VectorType oldV2Type = shuffleOp.getV2VectorType();
540 if (oldV1Type.getNumElements() > 1 && oldV2Type.getNumElements() > 1 &&
541 oldResultType.getNumElements() > 1) {
543 shuffleOp, newResultType, adaptor.getV1(), adaptor.getV2(),
551 auto getElementAtIdx = [&rewriter, loc = shuffleOp.getLoc()](
553 if (
auto vecTy = dyn_cast<VectorType>(scalarOrVec.getType()))
554 return rewriter.
create<spirv::CompositeExtractOp>(loc, scalarOrVec,
557 assert(idx == 0 &&
"Invalid scalar element index");
561 int32_t numV1Elems = oldV1Type.getNumElements();
563 for (
auto [shuffleIdx, newOperand] : llvm::zip_equal(mask, newOperands)) {
564 Value vec = adaptor.getV1();
565 int32_t elementIdx = shuffleIdx;
566 if (elementIdx >= numV1Elems) {
567 vec = adaptor.getV2();
568 elementIdx -= numV1Elems;
571 newOperand = getElementAtIdx(vec, elementIdx);
575 if (newOperands.size() == 1) {
576 rewriter.
replaceOp(shuffleOp, newOperands.front());
581 shuffleOp, newResultType, newOperands);
586 struct VectorInterleaveOpConvert final
591 matchAndRewrite(vector::InterleaveOp interleaveOp, OpAdaptor adaptor,
594 VectorType oldResultType = interleaveOp.getResultVectorType();
595 Type newResultType = getTypeConverter()->convertType(oldResultType);
598 "unsupported result vector type");
601 VectorType sourceType = interleaveOp.getSourceVectorType();
602 int n = sourceType.getNumElements();
608 Value newOperands[] = {adaptor.getLhs(), adaptor.getRhs()};
610 interleaveOp, newResultType, newOperands);
614 auto seq = llvm::seq<int64_t>(2 * n);
615 auto indices = llvm::map_to_vector(
616 seq, [n](
int i) {
return (i % 2 ? n : 0) + i / 2; });
620 interleaveOp, newResultType, adaptor.getLhs(), adaptor.getRhs(),
627 struct VectorDeinterleaveOpConvert final
632 matchAndRewrite(vector::DeinterleaveOp deinterleaveOp, OpAdaptor adaptor,
636 VectorType oldResultType = deinterleaveOp.getResultVectorType();
637 Type newResultType = getTypeConverter()->convertType(oldResultType);
640 "unsupported result vector type");
642 Location loc = deinterleaveOp->getLoc();
645 Value sourceVector = adaptor.getSource();
646 VectorType sourceType = deinterleaveOp.getSourceVectorType();
647 int n = sourceType.getNumElements();
653 auto elem0 = rewriter.
create<spirv::CompositeExtractOp>(
656 auto elem1 = rewriter.
create<spirv::CompositeExtractOp>(
659 rewriter.
replaceOp(deinterleaveOp, {elem0, elem1});
664 auto seqEven = llvm::seq<int64_t>(n / 2);
666 llvm::map_to_vector(seqEven, [](
int i) {
return i * 2; });
669 auto seqOdd = llvm::seq<int64_t>(n / 2);
671 llvm::map_to_vector(seqOdd, [](
int i) {
return i * 2 + 1; });
674 auto shuffleEven = rewriter.
create<spirv::VectorShuffleOp>(
675 loc, newResultType, sourceVector, sourceVector,
678 auto shuffleOdd = rewriter.
create<spirv::VectorShuffleOp>(
679 loc, newResultType, sourceVector, sourceVector,
682 rewriter.
replaceOp(deinterleaveOp, {shuffleEven, shuffleOdd});
687 struct VectorLoadOpConverter final
692 matchAndRewrite(vector::LoadOp loadOp, OpAdaptor adaptor,
694 auto memrefType = loadOp.getMemRefType();
696 dyn_cast_or_null<spirv::StorageClassAttr>(memrefType.getMemorySpace());
699 loadOp,
"expected spirv.storage_class memory space");
701 const auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>();
702 auto loc = loadOp.getLoc();
705 adaptor.getIndices(), loc, rewriter);
708 loadOp,
"failed to get memref element pointer");
710 spirv::StorageClass storageClass = attr.getValue();
711 auto vectorType = loadOp.getVectorType();
713 Value castedAccessChain =
714 rewriter.
create<spirv::BitcastOp>(loc, vectorPtrType, accessChain);
722 struct VectorStoreOpConverter final
727 matchAndRewrite(vector::StoreOp storeOp, OpAdaptor adaptor,
729 auto memrefType = storeOp.getMemRefType();
731 dyn_cast_or_null<spirv::StorageClassAttr>(memrefType.getMemorySpace());
734 storeOp,
"expected spirv.storage_class memory space");
736 const auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>();
737 auto loc = storeOp.getLoc();
740 adaptor.getIndices(), loc, rewriter);
743 storeOp,
"failed to get memref element pointer");
745 spirv::StorageClass storageClass = attr.getValue();
746 auto vectorType = storeOp.getVectorType();
748 Value castedAccessChain =
749 rewriter.
create<spirv::BitcastOp>(loc, vectorPtrType, accessChain);
751 adaptor.getValueToStore());
757 struct VectorReductionToIntDotProd final
761 LogicalResult matchAndRewrite(vector::ReductionOp op,
763 if (op.getKind() != vector::CombiningKind::ADD)
766 auto resultType = dyn_cast<IntegerType>(op.getType());
771 if (!llvm::is_contained({32, 64}, resultBitwidth))
774 VectorType inVecTy = op.getSourceVectorType();
775 if (!llvm::is_contained({4, 3}, inVecTy.getNumElements()) ||
776 inVecTy.getShape().size() != 1 || inVecTy.isScalable())
779 auto mul = op.getVector().getDefiningOp<arith::MulIOp>();
782 op,
"reduction operand is not 'arith.muli'");
784 if (succeeded(handleCase<arith::ExtSIOp, arith::ExtSIOp, spirv::SDotOp,
785 spirv::SDotAccSatOp,
false>(op, mul, rewriter)))
788 if (succeeded(handleCase<arith::ExtUIOp, arith::ExtUIOp, spirv::UDotOp,
789 spirv::UDotAccSatOp,
false>(op, mul, rewriter)))
792 if (succeeded(handleCase<arith::ExtSIOp, arith::ExtUIOp, spirv::SUDotOp,
793 spirv::SUDotAccSatOp,
false>(op, mul, rewriter)))
796 if (succeeded(handleCase<arith::ExtUIOp, arith::ExtSIOp, spirv::SUDotOp,
797 spirv::SUDotAccSatOp,
true>(op, mul, rewriter)))
804 template <
typename LhsExtensionOp,
typename RhsExtensionOp,
typename DotOp,
805 typename DotAccOp,
bool SwapOperands>
806 static LogicalResult handleCase(vector::ReductionOp op, arith::MulIOp mul,
808 auto lhs = mul.getLhs().getDefiningOp<LhsExtensionOp>();
811 Value lhsIn = lhs.getIn();
812 auto lhsInType = cast<VectorType>(lhsIn.
getType());
813 if (!lhsInType.getElementType().isInteger(8))
816 auto rhs = mul.getRhs().getDefiningOp<RhsExtensionOp>();
819 Value rhsIn = rhs.getIn();
820 auto rhsInType = cast<VectorType>(rhsIn.
getType());
821 if (!rhsInType.getElementType().isInteger(8))
824 if (op.getSourceVectorType().getNumElements() == 3) {
825 IntegerType i8Type = rewriter.
getI8Type();
829 lhsIn = rewriter.
create<spirv::CompositeConstructOp>(
831 rhsIn = rewriter.
create<spirv::CompositeConstructOp>(
838 std::swap(lhsIn, rhsIn);
840 if (
Value acc = op.getAcc()) {
852 struct VectorReductionToFPDotProd final
857 matchAndRewrite(vector::ReductionOp op, OpAdaptor adaptor,
859 if (op.getKind() != vector::CombiningKind::ADD)
862 auto resultType = getTypeConverter()->convertType<
FloatType>(op.getType());
866 Value vec = adaptor.getVector();
867 Value acc = adaptor.getAcc();
869 auto vectorType = dyn_cast<VectorType>(vec.
getType());
871 assert(isa<FloatType>(vec.
getType()) &&
872 "Expected the vector to be scalarized");
893 rewriter.
getFloatAttr(vectorType.getElementType(), 1.0);
895 rhs = rewriter.
create<spirv::ConstantOp>(loc, vectorType, oneAttr);
900 Value res = rewriter.
create<spirv::DotOp>(loc, resultType, lhs, rhs);
902 res = rewriter.
create<spirv::FAddOp>(loc, acc, res);
913 matchAndRewrite(vector::StepOp stepOp, OpAdaptor adaptor,
915 const auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>();
921 int64_t numElements = stepOp.getType().getNumElements();
927 if (numElements == 1) {
934 source.reserve(numElements);
935 for (int64_t i = 0; i < numElements; ++i) {
937 Value constOp = rewriter.
create<spirv::ConstantOp>(loc, intType, intAttr);
938 source.push_back(constOp);
947 #define CL_INT_MAX_MIN_OPS \
948 spirv::CLUMaxOp, spirv::CLUMinOp, spirv::CLSMaxOp, spirv::CLSMinOp
950 #define GL_INT_MAX_MIN_OPS \
951 spirv::GLUMaxOp, spirv::GLUMinOp, spirv::GLSMaxOp, spirv::GLSMinOp
953 #define CL_FLOAT_MAX_MIN_OPS spirv::CLFMaxOp, spirv::CLFMinOp
954 #define GL_FLOAT_MAX_MIN_OPS spirv::GLFMaxOp, spirv::GLFMinOp
959 VectorBitcastConvert, VectorBroadcastConvert,
960 VectorExtractElementOpConvert, VectorExtractOpConvert,
961 VectorExtractStridedSliceOpConvert, VectorFmaOpConvert<spirv::GLFmaOp>,
962 VectorFmaOpConvert<spirv::CLFmaOp>, VectorInsertElementOpConvert,
963 VectorInsertOpConvert, VectorReductionPattern<GL_INT_MAX_MIN_OPS>,
964 VectorReductionPattern<CL_INT_MAX_MIN_OPS>,
965 VectorReductionFloatMinMax<CL_FLOAT_MAX_MIN_OPS>,
966 VectorReductionFloatMinMax<GL_FLOAT_MAX_MIN_OPS>, VectorShapeCast,
967 VectorInsertStridedSliceOpConvert, VectorShuffleOpConvert,
968 VectorInterleaveOpConvert, VectorDeinterleaveOpConvert,
969 VectorSplatPattern, VectorLoadOpConverter, VectorStoreOpConverter,
970 VectorStepOpConvert>(typeConverter, patterns.
getContext(),
975 patterns.
add<VectorReductionToFPDotProd>(typeConverter, patterns.
getContext(),
981 patterns.
add<VectorReductionToIntDotProd>(patterns.
getContext());
static Value getZero(OpBuilder &b, Location loc, Type elementType)
Get zero value for an element type.
static uint64_t getFirstIntValue(ValueRange values)
Returns the integer value from the first valid input element, assuming Value inputs are defined by a ...
static int getNumBits(Type type)
Returns the number of bits for the given scalar/vector type.
#define INT_AND_FLOAT_CASE(kind, iop, fop)
#define INT_OR_FLOAT_CASE(kind, fop)
Attributes are known-constant values of operations.
IntegerAttr getIntegerAttr(Type type, int64_t value)
ArrayAttr getI32ArrayAttr(ArrayRef< int32_t > values)
FloatAttr getFloatAttr(Type type, double value)
IntegerType getIntegerType(unsigned width)
This class implements a pattern rewriter for use with ConversionPatterns.
void replaceOp(Operation *op, ValueRange newValues) override
PatternRewriter hook for replacing an operation.
static DenseElementsAttr get(ShapedType type, ArrayRef< Attribute > values)
Constructs a dense elements attribute from an array of element values.
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Operation * create(const OperationState &state)
Creates an operation given the fields represented as an OperationState.
OpConversionPattern is a wrapper around ConversionPattern that allows for matching and rewriting agai...
OpConversionPattern(MLIRContext *context, PatternBenefit benefit=1)
Location getLoc()
The source location the operation was defined or derived from.
This class represents the benefit of a pattern match in a unitless scheme that ranges from 0 (very li...
A special type of RewriterBase that coordinates the application of a rewrite pattern on the current I...
MLIRContext * getContext() const
RewritePatternSet & add(ConstructorArg &&arg, ConstructorArgs &&...args)
Add an instance of each of the pattern types 'Ts' to the pattern list with the given arguments.
std::enable_if_t<!std::is_convertible< CallbackT, Twine >::value, LogicalResult > notifyMatchFailure(Location loc, CallbackT &&reasonCallback)
Used to notify the listener that the IR failed to be rewritten because of a match failure,...
OpTy replaceOpWithNewOp(Operation *op, Args &&...args)
Replace the results of the given (original) op with a new op that is created without verification (re...
Type conversion from builtin types to SPIR-V types for shader interface.
LogicalResult convertType(Type t, SmallVectorImpl< Type > &results) const
Convert the given type.
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
unsigned getIntOrFloatBitWidth() const
Return the bit width of an integer or a float type, assert failure on other types.
This class provides an abstraction over the different types of ranges over Values.
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Type getType() const
Return the type of this value.
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
static PointerType get(Type pointeeType, StorageClass storageClass)
Value getElementPtr(const SPIRVTypeConverter &typeConverter, MemRefType baseType, Value basePtr, ValueRange indices, Location loc, OpBuilder &builder)
Performs the index computation to get to the element at indices of the memory pointed to by basePtr,...
Include the generated interface declarations.
bool matchPattern(Value value, const Pattern &pattern)
Entry point for matching a pattern over a Value.
detail::constant_int_value_binder m_ConstantInt(IntegerAttr::ValueType *bind_value)
Matches a constant holding a scalar/vector/tensor integer (splat) and writes the integer value to bin...
void populateVectorToSPIRVPatterns(SPIRVTypeConverter &typeConverter, RewritePatternSet &patterns)
Appends to a pattern list additional patterns for translating Vector Ops to SPIR-V ops.
void populateVectorReductionToSPIRVDotProductPatterns(RewritePatternSet &patterns)
Appends patterns to convert vector reduction of the form:
auto get(MLIRContext *context, Ts &&...params)
Helper method that injects context only if needed, this helps unify some of the attribute constructio...
OpRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting against an...
OpRewritePattern(MLIRContext *context, PatternBenefit benefit=1, ArrayRef< StringRef > generatedNames={})
Patterns must specify the root operation name they match against, and can also specify the benefit of...