1 //===- Dialect.cpp - Toy IR Dialect registration in MLIR ------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 //
9 // This file implements the dialect for the Toy IR: custom type parsing and
10 // operation verification.
11 //
12 //===----------------------------------------------------------------------===//
13 
14 #include "toy/Dialect.h"
15 
16 #include "mlir/IR/Builders.h"
17 #include "mlir/IR/BuiltinTypes.h"
18 #include "mlir/IR/DialectImplementation.h"
19 #include "mlir/IR/OpImplementation.h"
20 #include "mlir/Transforms/InliningUtils.h"
21 
22 using namespace mlir;
23 using namespace mlir::toy;
24 
25 #include "toy/Dialect.cpp.inc"
26 
27 //===----------------------------------------------------------------------===//
28 // ToyInlinerInterface
29 //===----------------------------------------------------------------------===//
30 
31 /// This class defines the interface for handling inlining with Toy
32 /// operations.
33 struct ToyInlinerInterface : public DialectInlinerInterface {
34   using DialectInlinerInterface::DialectInlinerInterface;
35 
36   //===--------------------------------------------------------------------===//
37   // Analysis Hooks
38   //===--------------------------------------------------------------------===//
39 
40   /// All call operations within toy can be inlined.
41   bool isLegalToInline(Operation *call, Operation *callable,
42                        bool wouldBeCloned) const final {
43     return true;
44   }
45 
46   /// All operations within toy can be inlined.
47   bool isLegalToInline(Operation *, Region *, bool,
48                        BlockAndValueMapping &) const final {
49     return true;
50   }
51 
52   //===--------------------------------------------------------------------===//
53   // Transformation Hooks
54   //===--------------------------------------------------------------------===//
55 
56   /// Handle the given inlined terminator(toy.return) by replacing it with a new
57   /// operation as necessary.
58   void handleTerminator(Operation *op,
59                         ArrayRef<Value> valuesToRepl) const final {
60     // Only "toy.return" needs to be handled here.
61     auto returnOp = cast<ReturnOp>(op);
62 
63     // Replace the values directly with the return operands.
64     assert(returnOp.getNumOperands() == valuesToRepl.size());
65     for (const auto &it : llvm::enumerate(returnOp.getOperands()))
66       valuesToRepl[it.index()].replaceAllUsesWith(it.value());
67   }
68 
69   /// Attempts to materialize a conversion for a type mismatch between a call
70   /// from this dialect, and a callable region. This method should generate an
71   /// operation that takes 'input' as the only operand, and produces a single
72   /// result of 'resultType'. If a conversion can not be generated, nullptr
73   /// should be returned.
74   Operation *materializeCallConversion(OpBuilder &builder, Value input,
75                                        Type resultType,
76                                        Location conversionLoc) const final {
77     return builder.create<CastOp>(conversionLoc, resultType, input);
78   }
79 };
80 
81 //===----------------------------------------------------------------------===//
82 // Toy Operations
83 //===----------------------------------------------------------------------===//
84 
85 /// A generalized parser for binary operations. This parses the different forms
86 /// of 'printBinaryOp' below.
87 static mlir::ParseResult parseBinaryOp(mlir::OpAsmParser &parser,
88                                        mlir::OperationState &result) {
89   SmallVector<mlir::OpAsmParser::OperandType, 2> operands;
90   llvm::SMLoc operandsLoc = parser.getCurrentLocation();
91   Type type;
92   if (parser.parseOperandList(operands, /*requiredOperandCount=*/2) ||
93       parser.parseOptionalAttrDict(result.attributes) ||
94       parser.parseColonType(type))
95     return mlir::failure();
96 
97   // If the type is a function type, it contains the input and result types of
98   // this operation.
99   if (FunctionType funcType = type.dyn_cast<FunctionType>()) {
100     if (parser.resolveOperands(operands, funcType.getInputs(), operandsLoc,
101                                result.operands))
102       return mlir::failure();
103     result.addTypes(funcType.getResults());
104     return mlir::success();
105   }
106 
107   // Otherwise, the parsed type is the type of both operands and results.
108   if (parser.resolveOperands(operands, type, result.operands))
109     return mlir::failure();
110   result.addTypes(type);
111   return mlir::success();
112 }
113 
114 /// A generalized printer for binary operations. It prints in two different
115 /// forms depending on if all of the types match.
116 static void printBinaryOp(mlir::OpAsmPrinter &printer, mlir::Operation *op) {
117   printer << " " << op->getOperands();
118   printer.printOptionalAttrDict(op->getAttrs());
119   printer << " : ";
120 
121   // If all of the types are the same, print the type directly.
122   Type resultType = *op->result_type_begin();
123   if (llvm::all_of(op->getOperandTypes(),
124                    [=](Type type) { return type == resultType; })) {
125     printer << resultType;
126     return;
127   }
128 
129   // Otherwise, print a functional type.
130   printer.printFunctionalType(op->getOperandTypes(), op->getResultTypes());
131 }
132 
133 //===----------------------------------------------------------------------===//
134 // ConstantOp
135 
136 /// Build a constant operation.
137 /// The builder is passed as an argument, so is the state that this method is
138 /// expected to fill in order to build the operation.
139 void ConstantOp::build(mlir::OpBuilder &builder, mlir::OperationState &state,
140                        double value) {
141   auto dataType = RankedTensorType::get({}, builder.getF64Type());
142   auto dataAttribute = DenseElementsAttr::get(dataType, value);
143   ConstantOp::build(builder, state, dataType, dataAttribute);
144 }
145 
146 /// The 'OpAsmParser' class provides a collection of methods for parsing
147 /// various punctuation, as well as attributes, operands, types, etc. Each of
148 /// these methods returns a `ParseResult`. This class is a wrapper around
149 /// `LogicalResult` that can be converted to a boolean `true` value on failure,
150 /// or `false` on success. This allows for easily chaining together a set of
151 /// parser rules. These rules are used to populate an `mlir::OperationState`
152 /// similarly to the `build` methods described above.
153 static mlir::ParseResult parseConstantOp(mlir::OpAsmParser &parser,
154                                          mlir::OperationState &result) {
155   mlir::DenseElementsAttr value;
156   if (parser.parseOptionalAttrDict(result.attributes) ||
157       parser.parseAttribute(value, "value", result.attributes))
158     return failure();
159 
160   result.addTypes(value.getType());
161   return success();
162 }
163 
164 /// The 'OpAsmPrinter' class is a stream that allows for formatting
165 /// strings, attributes, operands, types, etc.
166 static void print(mlir::OpAsmPrinter &printer, ConstantOp op) {
167   printer << " ";
168   printer.printOptionalAttrDict(op->getAttrs(), /*elidedAttrs=*/{"value"});
169   printer << op.value();
170 }
171 
172 /// Verify that the given attribute value is valid for the given type.
173 static mlir::LogicalResult verifyConstantForType(mlir::Type type,
174                                                  mlir::Attribute opaqueValue,
175                                                  mlir::Operation *op) {
176   if (type.isa<mlir::TensorType>()) {
177     // Check that the value is an elements attribute.
178     auto attrValue = opaqueValue.dyn_cast<mlir::DenseFPElementsAttr>();
179     if (!attrValue)
180       return op->emitError("constant of TensorType must be initialized by "
181                            "a DenseFPElementsAttr, got ")
182              << opaqueValue;
183 
184     // If the return type of the constant is not an unranked tensor, the shape
185     // must match the shape of the attribute holding the data.
186     auto resultType = type.dyn_cast<mlir::RankedTensorType>();
187     if (!resultType)
188       return success();
189 
190     // Check that the rank of the attribute type matches the rank of the
191     // constant result type.
192     auto attrType = attrValue.getType().cast<mlir::TensorType>();
193     if (attrType.getRank() != resultType.getRank()) {
194       return op->emitOpError("return type must match the one of the attached "
195                              "value attribute: ")
196              << attrType.getRank() << " != " << resultType.getRank();
197     }
198 
199     // Check that each of the dimensions match between the two types.
200     for (int dim = 0, dimE = attrType.getRank(); dim < dimE; ++dim) {
201       if (attrType.getShape()[dim] != resultType.getShape()[dim]) {
202         return op->emitOpError(
203                    "return type shape mismatches its attribute at dimension ")
204                << dim << ": " << attrType.getShape()[dim]
205                << " != " << resultType.getShape()[dim];
206       }
207     }
208     return mlir::success();
209   }
210   auto resultType = type.cast<StructType>();
211   llvm::ArrayRef<mlir::Type> resultElementTypes = resultType.getElementTypes();
212 
213   // Verify that the initializer is an Array.
214   auto attrValue = opaqueValue.dyn_cast<ArrayAttr>();
215   if (!attrValue || attrValue.getValue().size() != resultElementTypes.size())
216     return op->emitError("constant of StructType must be initialized by an "
217                          "ArrayAttr with the same number of elements, got ")
218            << opaqueValue;
219 
220   // Check that each of the elements are valid.
221   llvm::ArrayRef<mlir::Attribute> attrElementValues = attrValue.getValue();
222   for (const auto it : llvm::zip(resultElementTypes, attrElementValues))
223     if (failed(verifyConstantForType(std::get<0>(it), std::get<1>(it), op)))
224       return mlir::failure();
225   return mlir::success();
226 }
227 
228 /// Verifier for the constant operation. This corresponds to the `::verify(...)`
229 /// in the op definition.
230 static mlir::LogicalResult verify(ConstantOp op) {
231   return verifyConstantForType(op.getResult().getType(), op.value(), op);
232 }
233 
234 static mlir::LogicalResult verify(StructConstantOp op) {
235   return verifyConstantForType(op.getResult().getType(), op.value(), op);
236 }
237 
238 /// Infer the output shape of the ConstantOp, this is required by the shape
239 /// inference interface.
240 void ConstantOp::inferShapes() { getResult().setType(value().getType()); }
241 
242 //===----------------------------------------------------------------------===//
243 // AddOp
244 
245 void AddOp::build(mlir::OpBuilder &builder, mlir::OperationState &state,
246                   mlir::Value lhs, mlir::Value rhs) {
247   state.addTypes(UnrankedTensorType::get(builder.getF64Type()));
248   state.addOperands({lhs, rhs});
249 }
250 
251 /// Infer the output shape of the AddOp, this is required by the shape inference
252 /// interface.
253 void AddOp::inferShapes() { getResult().setType(getOperand(0).getType()); }
254 
255 //===----------------------------------------------------------------------===//
256 // CastOp
257 
258 /// Infer the output shape of the CastOp, this is required by the shape
259 /// inference interface.
260 void CastOp::inferShapes() { getResult().setType(getOperand().getType()); }
261 
262 /// Returns true if the given set of input and result types are compatible with
263 /// this cast operation. This is required by the `CastOpInterface` to verify
264 /// this operation and provide other additional utilities.
265 bool CastOp::areCastCompatible(TypeRange inputs, TypeRange outputs) {
266   if (inputs.size() != 1 || outputs.size() != 1)
267     return false;
268   // The inputs must be Tensors with the same element type.
269   TensorType input = inputs.front().dyn_cast<TensorType>();
270   TensorType output = outputs.front().dyn_cast<TensorType>();
271   if (!input || !output || input.getElementType() != output.getElementType())
272     return false;
273   // The shape is required to match if both types are ranked.
274   return !input.hasRank() || !output.hasRank() || input == output;
275 }
276 
277 //===----------------------------------------------------------------------===//
278 // GenericCallOp
279 
280 void GenericCallOp::build(mlir::OpBuilder &builder, mlir::OperationState &state,
281                           StringRef callee, ArrayRef<mlir::Value> arguments) {
282   // Generic call always returns an unranked Tensor initially.
283   state.addTypes(UnrankedTensorType::get(builder.getF64Type()));
284   state.addOperands(arguments);
285   state.addAttribute("callee",
286                      mlir::SymbolRefAttr::get(builder.getContext(), callee));
287 }
288 
289 /// Return the callee of the generic call operation, this is required by the
290 /// call interface.
291 CallInterfaceCallable GenericCallOp::getCallableForCallee() {
292   return (*this)->getAttrOfType<SymbolRefAttr>("callee");
293 }
294 
295 /// Get the argument operands to the called function, this is required by the
296 /// call interface.
297 Operation::operand_range GenericCallOp::getArgOperands() { return inputs(); }
298 
299 //===----------------------------------------------------------------------===//
300 // MulOp
301 
302 void MulOp::build(mlir::OpBuilder &builder, mlir::OperationState &state,
303                   mlir::Value lhs, mlir::Value rhs) {
304   state.addTypes(UnrankedTensorType::get(builder.getF64Type()));
305   state.addOperands({lhs, rhs});
306 }
307 
308 /// Infer the output shape of the MulOp, this is required by the shape inference
309 /// interface.
310 void MulOp::inferShapes() { getResult().setType(getOperand(0).getType()); }
311 
312 //===----------------------------------------------------------------------===//
313 // ReturnOp
314 
315 static mlir::LogicalResult verify(ReturnOp op) {
316   // We know that the parent operation is a function, because of the 'HasParent'
317   // trait attached to the operation definition.
318   auto function = cast<FuncOp>(op->getParentOp());
319 
320   /// ReturnOps can only have a single optional operand.
321   if (op.getNumOperands() > 1)
322     return op.emitOpError() << "expects at most 1 return operand";
323 
324   // The operand number and types must match the function signature.
325   const auto &results = function.getType().getResults();
326   if (op.getNumOperands() != results.size())
327     return op.emitOpError()
328            << "does not return the same number of values ("
329            << op.getNumOperands() << ") as the enclosing function ("
330            << results.size() << ")";
331 
332   // If the operation does not have an input, we are done.
333   if (!op.hasOperand())
334     return mlir::success();
335 
336   auto inputType = *op.operand_type_begin();
337   auto resultType = results.front();
338 
339   // Check that the result type of the function matches the operand type.
340   if (inputType == resultType || inputType.isa<mlir::UnrankedTensorType>() ||
341       resultType.isa<mlir::UnrankedTensorType>())
342     return mlir::success();
343 
344   return op.emitError() << "type of return operand (" << inputType
345                         << ") doesn't match function result type ("
346                         << resultType << ")";
347 }
348 
349 //===----------------------------------------------------------------------===//
350 // StructAccessOp
351 
352 void StructAccessOp::build(mlir::OpBuilder &b, mlir::OperationState &state,
353                            mlir::Value input, size_t index) {
354   // Extract the result type from the input type.
355   StructType structTy = input.getType().cast<StructType>();
356   assert(index < structTy.getNumElementTypes());
357   mlir::Type resultType = structTy.getElementTypes()[index];
358 
359   // Call into the auto-generated build method.
360   build(b, state, resultType, input, b.getI64IntegerAttr(index));
361 }
362 
363 static mlir::LogicalResult verify(StructAccessOp op) {
364   StructType structTy = op.input().getType().cast<StructType>();
365   size_t index = op.index();
366   if (index >= structTy.getNumElementTypes())
367     return op.emitOpError()
368            << "index should be within the range of the input struct type";
369   mlir::Type resultType = op.getResult().getType();
370   if (resultType != structTy.getElementTypes()[index])
371     return op.emitOpError() << "must have the same result type as the struct "
372                                "element referred to by the index";
373   return mlir::success();
374 }
375 
376 //===----------------------------------------------------------------------===//
377 // TransposeOp
378 
379 void TransposeOp::build(mlir::OpBuilder &builder, mlir::OperationState &state,
380                         mlir::Value value) {
381   state.addTypes(UnrankedTensorType::get(builder.getF64Type()));
382   state.addOperands(value);
383 }
384 
385 void TransposeOp::inferShapes() {
386   auto arrayTy = getOperand().getType().cast<RankedTensorType>();
387   SmallVector<int64_t, 2> dims(llvm::reverse(arrayTy.getShape()));
388   getResult().setType(RankedTensorType::get(dims, arrayTy.getElementType()));
389 }
390 
391 static mlir::LogicalResult verify(TransposeOp op) {
392   auto inputType = op.getOperand().getType().dyn_cast<RankedTensorType>();
393   auto resultType = op.getType().dyn_cast<RankedTensorType>();
394   if (!inputType || !resultType)
395     return mlir::success();
396 
397   auto inputShape = inputType.getShape();
398   if (!std::equal(inputShape.begin(), inputShape.end(),
399                   resultType.getShape().rbegin())) {
400     return op.emitError()
401            << "expected result shape to be a transpose of the input";
402   }
403   return mlir::success();
404 }
405 
406 //===----------------------------------------------------------------------===//
407 // Toy Types
408 //===----------------------------------------------------------------------===//
409 
410 namespace mlir {
411 namespace toy {
412 namespace detail {
413 /// This class represents the internal storage of the Toy `StructType`.
414 struct StructTypeStorage : public mlir::TypeStorage {
415   /// The `KeyTy` is a required type that provides an interface for the storage
416   /// instance. This type will be used when uniquing an instance of the type
417   /// storage. For our struct type, we will unique each instance structurally on
418   /// the elements that it contains.
419   using KeyTy = llvm::ArrayRef<mlir::Type>;
420 
421   /// A constructor for the type storage instance.
422   StructTypeStorage(llvm::ArrayRef<mlir::Type> elementTypes)
423       : elementTypes(elementTypes) {}
424 
425   /// Define the comparison function for the key type with the current storage
426   /// instance. This is used when constructing a new instance to ensure that we
427   /// haven't already uniqued an instance of the given key.
428   bool operator==(const KeyTy &key) const { return key == elementTypes; }
429 
430   /// Define a hash function for the key type. This is used when uniquing
431   /// instances of the storage, see the `StructType::get` method.
432   /// Note: This method isn't necessary as both llvm::ArrayRef and mlir::Type
433   /// have hash functions available, so we could just omit this entirely.
434   static llvm::hash_code hashKey(const KeyTy &key) {
435     return llvm::hash_value(key);
436   }
437 
438   /// Define a construction function for the key type from a set of parameters.
439   /// These parameters will be provided when constructing the storage instance
440   /// itself.
441   /// Note: This method isn't necessary because KeyTy can be directly
442   /// constructed with the given parameters.
443   static KeyTy getKey(llvm::ArrayRef<mlir::Type> elementTypes) {
444     return KeyTy(elementTypes);
445   }
446 
447   /// Define a construction method for creating a new instance of this storage.
448   /// This method takes an instance of a storage allocator, and an instance of a
449   /// `KeyTy`. The given allocator must be used for *all* necessary dynamic
450   /// allocations used to create the type storage and its internal.
451   static StructTypeStorage *construct(mlir::TypeStorageAllocator &allocator,
452                                       const KeyTy &key) {
453     // Copy the elements from the provided `KeyTy` into the allocator.
454     llvm::ArrayRef<mlir::Type> elementTypes = allocator.copyInto(key);
455 
456     // Allocate the storage instance and construct it.
457     return new (allocator.allocate<StructTypeStorage>())
458         StructTypeStorage(elementTypes);
459   }
460 
461   /// The following field contains the element types of the struct.
462   llvm::ArrayRef<mlir::Type> elementTypes;
463 };
464 } // end namespace detail
465 } // end namespace toy
466 } // end namespace mlir
467 
468 /// Create an instance of a `StructType` with the given element types. There
469 /// *must* be at least one element type.
470 StructType StructType::get(llvm::ArrayRef<mlir::Type> elementTypes) {
471   assert(!elementTypes.empty() && "expected at least 1 element type");
472 
473   // Call into a helper 'get' method in 'TypeBase' to get a uniqued instance
474   // of this type. The first parameter is the context to unique in. The
475   // parameters after the context are forwarded to the storage instance.
476   mlir::MLIRContext *ctx = elementTypes.front().getContext();
477   return Base::get(ctx, elementTypes);
478 }
479 
480 /// Returns the element types of this struct type.
481 llvm::ArrayRef<mlir::Type> StructType::getElementTypes() {
482   // 'getImpl' returns a pointer to the internal storage instance.
483   return getImpl()->elementTypes;
484 }
485 
486 /// Parse an instance of a type registered to the toy dialect.
487 mlir::Type ToyDialect::parseType(mlir::DialectAsmParser &parser) const {
488   // Parse a struct type in the following form:
489   //   struct-type ::= `struct` `<` type (`,` type)* `>`
490 
491   // NOTE: All MLIR parser function return a ParseResult. This is a
492   // specialization of LogicalResult that auto-converts to a `true` boolean
493   // value on failure to allow for chaining, but may be used with explicit
494   // `mlir::failed/mlir::succeeded` as desired.
495 
496   // Parse: `struct` `<`
497   if (parser.parseKeyword("struct") || parser.parseLess())
498     return Type();
499 
500   // Parse the element types of the struct.
501   SmallVector<mlir::Type, 1> elementTypes;
502   do {
503     // Parse the current element type.
504     llvm::SMLoc typeLoc = parser.getCurrentLocation();
505     mlir::Type elementType;
506     if (parser.parseType(elementType))
507       return nullptr;
508 
509     // Check that the type is either a TensorType or another StructType.
510     if (!elementType.isa<mlir::TensorType, StructType>()) {
511       parser.emitError(typeLoc, "element type for a struct must either "
512                                 "be a TensorType or a StructType, got: ")
513           << elementType;
514       return Type();
515     }
516     elementTypes.push_back(elementType);
517 
518     // Parse the optional: `,`
519   } while (succeeded(parser.parseOptionalComma()));
520 
521   // Parse: `>`
522   if (parser.parseGreater())
523     return Type();
524   return StructType::get(elementTypes);
525 }
526 
527 /// Print an instance of a type registered to the toy dialect.
528 void ToyDialect::printType(mlir::Type type,
529                            mlir::DialectAsmPrinter &printer) const {
530   // Currently the only toy type is a struct type.
531   StructType structType = type.cast<StructType>();
532 
533   // Print the struct type according to the parser format.
534   printer << "struct<";
535   llvm::interleaveComma(structType.getElementTypes(), printer);
536   printer << '>';
537 }
538 
539 //===----------------------------------------------------------------------===//
540 // TableGen'd op method definitions
541 //===----------------------------------------------------------------------===//
542 
543 #define GET_OP_CLASSES
544 #include "toy/Ops.cpp.inc"
545 
546 //===----------------------------------------------------------------------===//
547 // ToyDialect
548 //===----------------------------------------------------------------------===//
549 
550 /// Dialect initialization, the instance will be owned by the context. This is
551 /// the point of registration of types and operations for the dialect.
552 void ToyDialect::initialize() {
553   addOperations<
554 #define GET_OP_LIST
555 #include "toy/Ops.cpp.inc"
556       >();
557   addInterfaces<ToyInlinerInterface>();
558   addTypes<StructType>();
559 }
560 
561 mlir::Operation *ToyDialect::materializeConstant(mlir::OpBuilder &builder,
562                                                  mlir::Attribute value,
563                                                  mlir::Type type,
564                                                  mlir::Location loc) {
565   if (type.isa<StructType>())
566     return builder.create<StructConstantOp>(loc, type,
567                                             value.cast<mlir::ArrayAttr>());
568   return builder.create<ConstantOp>(loc, type,
569                                     value.cast<mlir::DenseElementsAttr>());
570 }
571