Update Ch.2 of the Toy tutorial.
authorRiver Riddle <riddleriver@gmail.com>
Tue, 27 Aug 2019 19:43:55 +0000 (12:43 -0700)
committerA. Unique TensorFlower <gardener@tensorflow.org>
Tue, 27 Aug 2019 19:44:27 +0000 (12:44 -0700)
The code and documentation for this chapter of the tutorial have been updated to follow the new flow. The toy 'array' type has been replaced by usages of the MLIR tensor type. The code has also been cleaned up and modernized.

Closes tensorflow/mlir#101

PiperOrigin-RevId: 265744086

mlir/examples/toy/Ch1/include/toy/AST.h
mlir/examples/toy/Ch2/include/toy/AST.h
mlir/examples/toy/Ch2/include/toy/Lexer.h
mlir/examples/toy/Ch2/mlir/MLIRGen.cpp
mlir/examples/toy/Ch2/toyc.cpp
mlir/g3doc/Tutorials/Toy/Ch-2.md
mlir/test/Examples/Toy/Ch2/codegen.toy

index 814558e..2ad3392 100644 (file)
 
 namespace toy {
 
-/// A variable
+/// A variable type with shape information.
 struct VarType {
-  enum { TY_FLOAT, TY_INT } elt_ty;
-  std::vector<int> shape;
+  std::vector<int64_t> shape;
 };
 
 /// Base class for all expression nodes.
index 456a323..2ad3392 100644 (file)
 
 namespace toy {
 
-/// A variable
+/// A variable type with shape information.
 struct VarType {
-  enum { TY_FLOAT, TY_INT } elt_ty;
-  std::vector<int> shape;
+  std::vector<int64_t> shape;
 };
 
 /// Base class for all expression nodes.
@@ -50,9 +49,7 @@ public:
     Expr_Var,
     Expr_BinOp,
     Expr_Call,
-    Expr_Print, // builtin
-    Expr_If,
-    Expr_For,
+    Expr_Print,
   };
 
   ExprAST(ExprASTKind kind, Location location)
@@ -85,7 +82,7 @@ public:
   static bool classof(const ExprAST *C) { return C->getKind() == Expr_Num; }
 };
 
-///
+/// Expression class for a literal value.
 class LiteralExprAST : public ExprAST {
   std::vector<std::unique_ptr<ExprAST>> values;
   std::vector<int64_t> dims;
@@ -116,7 +113,7 @@ public:
   static bool classof(const ExprAST *C) { return C->getKind() == Expr_Var; }
 };
 
-///
+/// Expression class for defining a variable.
 class VarDeclExprAST : public ExprAST {
   std::string name;
   VarType type;
@@ -136,7 +133,7 @@ public:
   static bool classof(const ExprAST *C) { return C->getKind() == Expr_VarDecl; }
 };
 
-///
+/// Expression class for a return operator.
 class ReturnExprAST : public ExprAST {
   llvm::Optional<std::unique_ptr<ExprAST>> expr;
 
index d73adb9..21f9261 100644 (file)
@@ -31,7 +31,7 @@ namespace toy {
 
 /// Structure definition a location in a file.
 struct Location {
-  std::shared_ptr<std::string> file; ///< filename
+  std::shared_ptr<std::string> file; ///< filename.
   int line;                          ///< line number.
   int col;                           ///< column number.
 };
index 82e2bfa..6f29490 100644 (file)
@@ -47,7 +47,6 @@ using llvm::ScopedHashTableScope;
 using llvm::SmallVector;
 using llvm::StringRef;
 using llvm::Twine;
-using std::make_unique;
 
 namespace {
 
@@ -60,14 +59,15 @@ namespace {
 /// At this point we take advantage of the "raw" MLIR APIs to create operations
 /// that haven't been registered in any way with MLIR. These operations are
 /// unknown to MLIR, custom passes could operate by string-matching the name of
-/// these operations, but no other type checking or semantic is associated with
-/// them natively by MLIR.
+/// these operations, but no other type checking or semantics are associated
+/// with them natively by MLIR.
 class MLIRGenImpl {
 public:
-  MLIRGenImpl(mlir::MLIRContext &context) : context(context) {}
+  MLIRGenImpl(mlir::MLIRContext &context)
+      : context(context), builder(&context) {}
 
   /// Public API: convert the AST for a Toy module (source file) to an MLIR
-  /// Module.
+  /// Module operation.
   mlir::ModuleOp mlirGen(ModuleAST &moduleAST) {
     // We create an empty MLIR module and codegen functions one at a time and
     // add them to the module.
@@ -82,9 +82,9 @@ public:
 
     // FIXME: (in the next chapter...) without registering a dialect in MLIR,
     // this won't do much, but it should at least check some structural
-    // properties.
+    // properties of the generated MLIR module.
     if (failed(mlir::verify(theModule))) {
-      emitError(mlir::UnknownLoc::get(&context), "Module verification error");
+      theModule.emitError("Module verification error");
       return nullptr;
     }
 
@@ -93,19 +93,17 @@ public:
 
 private:
   /// In MLIR (like in LLVM) a "context" object holds the memory allocation and
-  /// the ownership of many internal structure of the IR and provide a level
-  /// of "uniquing" across multiple modules (types for instance).
+  /// ownership of many internal structures of the IR and provides a level of
+  /// "uniquing" across multiple modules (types for instance).
   mlir::MLIRContext &context;
 
-  /// A "module" matches a source file: it contains a list of functions.
+  /// A "module" matches a Toy source file: containing a list of functions.
   mlir::ModuleOp theModule;
 
-  /// The builder is a helper class to create IR inside a function. It is
-  /// re-initialized every time we enter a function and kept around as a
-  /// convenience for emitting individual operations.
-  /// The builder is stateful, in particular it keeeps an "insertion point":
-  /// this is where the next operations will be introduced.
-  std::unique_ptr<mlir::OpBuilder> builder;
+  /// The builder is a helper class to create IR inside a function. The builder
+  /// is stateful, in particular it keeeps an "insertion point": this is where
+  /// the next operations will be introduced.
+  mlir::OpBuilder builder;
 
   /// The symbol table maps a variable name to a value in the current scope.
   /// Entering a function creates a new scope, and the function arguments are
@@ -115,18 +113,17 @@ private:
 
   /// Helper conversion for a Toy AST location to an MLIR location.
   mlir::Location loc(Location loc) {
-    return mlir::FileLineColLoc::get(mlir::Identifier::get(*loc.file, &context),
-                                     loc.line, loc.col, &context);
+    return builder.getFileLineColLoc(builder.getIdentifier(*loc.file), loc.line,
+                                     loc.col);
   }
 
-  /// Declare a variable in the current scope, return true if the variable
+  /// Declare a variable in the current scope, return success if the variable
   /// wasn't declared yet.
-  bool declare(llvm::StringRef var, mlir::Value *value) {
-    if (symbolTable.count(var)) {
-      return false;
-    }
+  mlir::LogicalResult declare(llvm::StringRef var, mlir::Value *value) {
+    if (symbolTable.count(var))
+      return mlir::failure();
     symbolTable.insert(var, value);
-    return true;
+    return mlir::success();
   }
 
   /// Create the prototype for an MLIR function with as many arguments as the
@@ -137,15 +134,14 @@ private:
     // Arguments type is uniformly a generic array.
     llvm::SmallVector<mlir::Type, 4> arg_types(proto.getArgs().size(),
                                                getType(VarType{}));
-    auto func_type = mlir::FunctionType::get(arg_types, ret_types, &context);
+    auto func_type = builder.getFunctionType(arg_types, ret_types);
     auto function = mlir::FuncOp::create(loc(proto.loc()), proto.getName(),
                                          func_type, /* attrs = */ {});
 
     // Mark the function as generic: it'll require type specialization for every
     // call site.
     if (function.getNumArguments())
-      function.setAttr("toy.generic", mlir::BoolAttr::get(true, &context));
-
+      function.setAttr("toy.generic", builder.getUnitAttr());
     return function;
   }
 
@@ -164,18 +160,22 @@ private:
     // argument list as the function itself.
     auto &entryBlock = *function.addEntryBlock();
     auto &protoArgs = funcAST.getProto()->getArgs();
+
     // Declare all the function arguments in the symbol table.
     for (const auto &name_value :
          llvm::zip(protoArgs, entryBlock.getArguments())) {
-      declare(std::get<0>(name_value)->getName(), std::get<1>(name_value));
+      if (failed(declare(std::get<0>(name_value)->getName(),
+                         std::get<1>(name_value))))
+        return nullptr;
     }
 
-    // Create a builder for the function, it will be used throughout the codegen
-    // to create operations in this function.
-    builder = std::make_unique<mlir::OpBuilder>(function.getBody());
+    // Set the insertion point in the builder to the beginning of the function
+    // body, it will be used throughout the codegen to create operations in this
+    // function.
+    builder.setInsertionPointToStart(&entryBlock);
 
     // Emit the body of the function.
-    if (!mlirGen(*funcAST.getBody())) {
+    if (mlir::failed(mlirGen(*funcAST.getBody()))) {
       function.erase();
       return nullptr;
     }
@@ -183,7 +183,7 @@ private:
     // Implicitly return void if no return statement was emitted.
     // FIXME: we may fix the parser instead to always return the last expression
     // (this would possibly help the REPL case later)
-    if (function.getBlocks().back().back().getName().getStringRef() !=
+    if (function.getBody().back().back().getName().getStringRef() !=
         "toy.return") {
       ReturnExprAST fakeRet(funcAST.getProto()->loc(), llvm::None);
       mlirGen(fakeRet);
@@ -224,7 +224,7 @@ private:
       op_name = "toy.mul";
       break;
     default:
-      emitError(loc(binop.loc()), "Error: invalid binary operator '")
+      emitError(location, "Error: invalid binary operator '")
           << binop.getOp() << "'";
       return nullptr;
     }
@@ -232,112 +232,108 @@ private:
     // Build the MLIR operation from the name and the two operands. The return
     // type is always a generic array for binary operators.
     mlir::OperationState result(location, op_name);
-    result.types.push_back(getType(VarType{}));
-    result.operands.push_back(L);
-    result.operands.push_back(R);
-    return builder->createOperation(result)->getResult(0);
+    result.addTypes(getType(VarType{}));
+    result.addOperands({L, R});
+    return builder.createOperation(result)->getResult(0);
   }
 
-  // This is a reference to a variable in an expression. The variable is
-  // expected to have been declared and so should have a value in the symbol
-  // table, otherwise emit an error and return nullptr.
+  /// This is a reference to a variable in an expression. The variable is
+  /// expected to have been declared and so should have a value in the symbol
+  /// table, otherwise emit an error and return nullptr.
   mlir::Value *mlirGen(VariableExprAST &expr) {
-    if (symbolTable.count(expr.getName()))
-      return symbolTable.lookup(expr.getName());
+    if (auto *variable = symbolTable.lookup(expr.getName()))
+      return variable;
+
     emitError(loc(expr.loc()), "Error: unknown variable '")
         << expr.getName() << "'";
     return nullptr;
   }
 
-  // Emit a return operation, return true on success.
-  bool mlirGen(ReturnExprAST &ret) {
-    auto location = loc(ret.loc());
+  /// Emit a return operation. This will return failure if any generation fails.
+  mlir::LogicalResult mlirGen(ReturnExprAST &ret) {
+    mlir::OperationState result(loc(ret.loc()), "toy.return");
+
     // `return` takes an optional expression, we need to account for it here.
-    mlir::OperationState result(location, "toy.return");
     if (ret.getExpr().hasValue()) {
       auto *expr = mlirGen(*ret.getExpr().getValue());
       if (!expr)
-        return false;
-      result.operands.push_back(expr);
+        return mlir::failure();
+      result.addOperands(expr);
     }
-    builder->createOperation(result);
-    return true;
+
+    builder.createOperation(result);
+    return mlir::success();
   }
 
-  // Emit a literal/constant array. It will be emitted as a flattened array of
-  // data in an Attribute attached to a `toy.constant` operation.
-  // See documentation on [Attributes](LangRef.md#attributes) for more details.
-  // Here is an excerpt:
-  //
-  //   Attributes are the mechanism for specifying constant data in MLIR in
-  //   places where a variable is never allowed [...]. They consist of a name
-  //   and a [concrete attribute value](#attribute-values). It is possible to
-  //   attach attributes to operations, functions, and function arguments. The
-  //   set of expected attributes, their structure, and their interpretation
-  //   are all contextually dependent on what they are attached to.
-  //
-  // Example, the source level statement:
-  //   var a<2, 3> = [[1, 2, 3], [4, 5, 6]];
-  // will be converted to:
-  //   %0 = "toy.constant"() {value: dense<tensor<2x3xf64>,
-  //     [[1.000000e+00, 2.000000e+00, 3.000000e+00],
-  //      [4.000000e+00, 5.000000e+00, 6.000000e+00]]>} : () -> memref<2x3xf64>
-  //
+  /// Emit a literal/constant array. It will be emitted as a flattened array of
+  /// data in an Attribute attached to a `toy.constant` operation.
+  /// See documentation on [Attributes](LangRef.md#attributes) for more details.
+  /// Here is an excerpt:
+  ///
+  ///   Attributes are the mechanism for specifying constant data in MLIR in
+  ///   places where a variable is never allowed [...]. They consist of a name
+  ///   and a concrete attribute value. The set of expected attributes, their
+  ///   structure, and their interpretation are all contextually dependent on
+  ///   what they are attached to.
+  ///
+  /// Example, the source level statement:
+  ///   var a<2, 3> = [[1, 2, 3], [4, 5, 6]];
+  /// will be converted to:
+  ///   %0 = "toy.constant"() {value: dense<tensor<2x3xf64>,
+  ///     [[1.000000e+00, 2.000000e+00, 3.000000e+00],
+  ///      [4.000000e+00, 5.000000e+00, 6.000000e+00]]>} : () -> tensor<2x3xf64>
+  ///
   mlir::Value *mlirGen(LiteralExprAST &lit) {
-    auto location = loc(lit.loc());
     auto type = getType(lit.getDims());
 
-    // The attribute is a vector with an attribute per element (number) in the
-    // array, see `collectData()` below for more details.
-    std::vector<mlir::Attribute> data;
+    // The attribute is a vector with a floating point value per element
+    // (number) in the array, see `collectData()` below for more details.
+    std::vector<double> data;
     data.reserve(std::accumulate(lit.getDims().begin(), lit.getDims().end(), 1,
                                  std::multiplies<int>()));
     collectData(lit, data);
 
-    // FIXME: using a tensor type is a HACK here.
-    // Can we do differently without registering a dialect? Using a string blob?
-    mlir::Type elementType = mlir::FloatType::getF64(&context);
-    auto dataType = builder->getTensorType(lit.getDims(), elementType);
+    // The type of this attribute is tensor of 64-bit floating-point with the
+    // shape of the literal.
+    mlir::Type elementType = builder.getF64Type();
+    auto dataType = builder.getTensorType(lit.getDims(), elementType);
 
-    // This is the actual attribute that actually hold the list of values for
-    // this array literal.
-    auto dataAttribute = builder->getNamedAttr(
-        "value", builder->getDenseElementsAttr(dataType, data)
-                     .cast<mlir::DenseElementsAttr>());
+    // This is the actual attribute that holds the list of values for this
+    // tensor literal.
+    auto dataAttribute =
+        mlir::DenseElementsAttr::get(dataType, llvm::makeArrayRef(data));
 
     // Build the MLIR op `toy.constant`, only boilerplate below.
-    mlir::OperationState result(location, "toy.constant");
-    result.types.push_back(type);
-    result.attributes.push_back(dataAttribute);
-    return builder->createOperation(result)->getResult(0);
+    mlir::OperationState result(loc(lit.loc()), "toy.constant");
+    result.addTypes(type);
+    result.addAttribute("value", dataAttribute);
+    return builder.createOperation(result)->getResult(0);
   }
 
-  // Recursive helper function to accumulate the data that compose an array
-  // literal. It flattens the nested structure in the supplied vector. For
-  // example with this array:
-  //  [[1, 2], [3, 4]]
-  // we will generate:
-  //  [ 1, 2, 3, 4 ]
-  // Individual numbers are wrapped in a light wrapper `mlir::FloatAttr`.
-  // Attributes are the way MLIR attaches constant to operations and functions.
-  void collectData(ExprAST &expr, std::vector<mlir::Attribute> &data) {
+  /// Recursive helper function to accumulate the data that compose an array
+  /// literal. It flattens the nested structure in the supplied vector. For
+  /// example with this array:
+  ///  [[1, 2], [3, 4]]
+  /// we will generate:
+  ///  [ 1, 2, 3, 4 ]
+  /// Individual numbers are represented as doubles.
+  /// Attributes are the way MLIR attaches constant to operations.
+  void collectData(ExprAST &expr, std::vector<double> &data) {
     if (auto *lit = dyn_cast<LiteralExprAST>(&expr)) {
       for (auto &value : lit->getValues())
         collectData(*value, data);
       return;
     }
+
     assert(isa<NumberExprAST>(expr) && "expected literal or number expr");
-    mlir::Type elementType = mlir::FloatType::getF64(&context);
-    auto attr = mlir::FloatAttr::getChecked(
-        elementType, cast<NumberExprAST>(expr).getValue(), loc(expr.loc()));
-    data.push_back(attr);
+    data.push_back(cast<NumberExprAST>(expr).getValue());
   }
 
-  // Emit a call expression. It emits specific operations for the `transpose`
-  // builtin. Other identifiers are assumed to be user-defined functions.
+  /// Emit a call expression. It emits specific operations for the `transpose`
+  /// builtin. Other identifiers are assumed to be user-defined functions.
   mlir::Value *mlirGen(CallExprAST &call) {
-    auto location = loc(call.loc());
-    std::string callee = call.getCallee();
+    llvm::StringRef callee = call.getCallee();
+
     // Codegen the operands first.
     SmallVector<mlir::Value *, 4> operands;
     for (auto &expr : call.getArgs()) {
@@ -346,51 +342,49 @@ private:
         return nullptr;
       operands.push_back(arg);
     }
-    // builtin have their custom operation, this is a straightforward emission.
+
+    // Builting calls have their custom operation, meaning this is a
+    // straightforward emission.
     if (callee == "transpose") {
-      mlir::OperationState result(location, "toy.transpose");
-      result.types.push_back(getType(VarType{}));
+      mlir::OperationState result(loc(call.loc()), "toy.transpose");
+      result.addTypes(getType(VarType{}));
       result.operands = std::move(operands);
-      return builder->createOperation(result)->getResult(0);
+      return builder.createOperation(result)->getResult(0);
     }
 
-    // Calls to user-defined functions are mapped to a custom call that takes
-    // the callee name as an attribute.
-    mlir::OperationState result(location, "toy.generic_call");
-    result.types.push_back(getType(VarType{}));
+    // Otherwise this is a call to a user-defined function. Calls to
+    // user-defined functions are mapped to a custom call that takes the callee
+    // name as an attribute.
+    mlir::OperationState result(loc(call.loc()), "toy.generic_call");
+    result.addTypes(getType(VarType{}));
     result.operands = std::move(operands);
-    auto calleeAttr = builder->getStringAttr(call.getCallee());
-    result.attributes.push_back(builder->getNamedAttr("callee", calleeAttr));
-    return builder->createOperation(result)->getResult(0);
+    result.addAttribute("callee", builder.getSymbolRefAttr(callee));
+    return builder.createOperation(result)->getResult(0);
   }
 
-  // Emit a call expression. It emits specific operations for two builtins:
-  // transpose(x) and print(x). Other identifiers are assumed to be user-defined
-  // functions. Return false on failure.
-  bool mlirGen(PrintExprAST &call) {
+  /// Emit a print expression. It emits specific operations for two builtins:
+  /// transpose(x) and print(x).
+  mlir::LogicalResult mlirGen(PrintExprAST &call) {
     auto *arg = mlirGen(*call.getArg());
     if (!arg)
-      return false;
-    auto location = loc(call.loc());
-    mlir::OperationState result(location, "toy.print");
-    result.operands.push_back(arg);
-    builder->createOperation(result);
-    return true;
+      return mlir::failure();
+
+    mlir::OperationState result(loc(call.loc()), "toy.print");
+    result.addOperands(arg);
+    builder.createOperation(result);
+    return mlir::success();
   }
 
-  // Emit a constant for a single number (FIXME: semantic? broadcast?)
+  /// Emit a constant for a single number (FIXME: semantic? broadcast?)
   mlir::Value *mlirGen(NumberExprAST &num) {
-    auto location = loc(num.loc());
-    mlir::OperationState result(location, "toy.constant");
-    mlir::Type elementType = mlir::FloatType::getF64(&context);
-    result.types.push_back(builder->getMemRefType({1}, elementType));
-    auto attr = mlir::FloatAttr::getChecked(elementType, num.getValue(),
-                                            loc(num.loc()));
-    result.attributes.push_back(builder->getNamedAttr("value", attr));
-    return builder->createOperation(result)->getResult(0);
+    mlir::OperationState result(loc(num.loc()), "toy.constant");
+    mlir::Type elementType = builder.getF64Type();
+    result.addTypes(builder.getTensorType({}, elementType));
+    result.addAttribute("value", builder.getF64FloatAttr(num.getValue()));
+    return builder.createOperation(result)->getResult(0);
   }
 
-  // Dispatch codegen for the right expression subclass using RTTI.
+  /// Dispatch codegen for the right expression subclass using RTTI.
   mlir::Value *mlirGen(ExprAST &expr) {
     switch (expr.getKind()) {
     case toy::ExprAST::Expr_BinOp:
@@ -411,38 +405,40 @@ private:
     }
   }
 
-  // Handle a variable declaration, we'll codegen the expression that forms the
-  // initializer and record the value in the symbol table before returning it.
-  // Future expressions will be able to reference this variable through symbol
-  // table lookup.
+  /// Handle a variable declaration, we'll codegen the expression that forms the
+  /// initializer and record the value in the symbol table before returning it.
+  /// Future expressions will be able to reference this variable through symbol
+  /// table lookup.
   mlir::Value *mlirGen(VarDeclExprAST &vardecl) {
-    mlir::Value *value = nullptr;
-    auto location = loc(vardecl.loc());
-    if (auto init = vardecl.getInitVal()) {
-      value = mlirGen(*init);
-      if (!value)
-        return nullptr;
-      // We have the initializer value, but in case the variable was declared
-      // with specific shape, we emit a "reshape" operation. It will get
-      // optimized out later as needed.
-      if (!vardecl.getType().shape.empty()) {
-        mlir::OperationState result(location, "toy.reshape");
-        result.types.push_back(getType(vardecl.getType()));
-        result.operands.push_back(value);
-        value = builder->createOperation(result)->getResult(0);
-      }
-    } else {
+    auto init = vardecl.getInitVal();
+    if (!init) {
       emitError(loc(vardecl.loc()),
                 "Missing initializer in variable declaration");
       return nullptr;
     }
+
+    mlir::Value *value = mlirGen(*init);
+    if (!value)
+      return nullptr;
+
+    // We have the initializer value, but in case the variable was declared
+    // with specific shape, we emit a "reshape" operation. It will get
+    // optimized out later as needed.
+    if (!vardecl.getType().shape.empty()) {
+      mlir::OperationState result(loc(vardecl.loc()), "toy.reshape");
+      result.addTypes(getType(vardecl.getType()));
+      result.addOperands(value);
+      value = builder.createOperation(result)->getResult(0);
+    }
+
     // Register the value in the symbol table
-    declare(vardecl.getName(), value);
+    if (failed(declare(vardecl.getName(), value)))
+      return nullptr;
     return value;
   }
 
-  /// Codegen a list of expression, return false if one of them hit an error.
-  bool mlirGen(ExprASTList &blockAST) {
+  /// Codegen a list of expression, return failure if one of them hit an error.
+  mlir::LogicalResult mlirGen(ExprASTList &blockAST) {
     ScopedHashTableScope<llvm::StringRef, mlir::Value *> var_scope(symbolTable);
     for (auto &expr : blockAST) {
       // Specific handling for variable declarations, return statement, and
@@ -450,44 +446,32 @@ private:
       // expressions.
       if (auto *vardecl = dyn_cast<VarDeclExprAST>(expr.get())) {
         if (!mlirGen(*vardecl))
-          return false;
+          return mlir::failure();
         continue;
       }
-      if (auto *ret = dyn_cast<ReturnExprAST>(expr.get())) {
-        if (!mlirGen(*ret))
-          return false;
-        return true;
-      }
+      if (auto *ret = dyn_cast<ReturnExprAST>(expr.get()))
+        return mlirGen(*ret);
       if (auto *print = dyn_cast<PrintExprAST>(expr.get())) {
-        if (!mlirGen(*print))
-          return false;
+        if (mlir::failed(mlirGen(*print)))
+          return mlir::success();
         continue;
       }
+
       // Generic expression dispatch codegen.
       if (!mlirGen(*expr))
-        return false;
+        return mlir::failure();
     }
-    return true;
+    return mlir::success();
   }
 
-  /// Build a type from a list of shape dimensions. Types are `array` followed
-  /// by an optional dimension list, example: array<2, 2>
-  /// They are wrapped in a `toy` dialect (see next chapter) and get printed:
-  ///   !toy.array<2, 2>
-  template <typename T> mlir::Type getType(T shape) {
-    std::string typeName = "array";
-    if (!shape.empty()) {
-      typeName += "<";
-      const char *sep = "";
-      for (auto dim : shape) {
-        typeName += sep;
-        typeName += llvm::Twine(dim).str();
-        sep = ", ";
-      }
-      typeName += ">";
-    }
-    return mlir::OpaqueType::get(mlir::Identifier::get("toy", &context),
-                                 typeName, &context);
+  /// Build a tensor type from a list of shape dimensions.
+  mlir::Type getType(llvm::ArrayRef<int64_t> shape) {
+    // If the shape is empty, then this type is unranked.
+    if (shape.empty())
+      return builder.getTensorType(builder.getF64Type());
+
+    // Otherwise, we use the given shape.
+    return builder.getTensorType(shape, builder.getF64Type());
   }
 
   /// Build an MLIR type from a Toy AST variable type
index ddf6f1a..7280ccb 100644 (file)
@@ -76,32 +76,36 @@ std::unique_ptr<toy::ModuleAST> parseInputFile(llvm::StringRef filename) {
 
 int dumpMLIR() {
   mlir::MLIRContext context;
-  mlir::OwningModuleRef module;
-  if (inputType == InputType::MLIR ||
-      llvm::StringRef(inputFilename).endswith(".mlir")) {
-    llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> fileOrErr =
-        llvm::MemoryBuffer::getFileOrSTDIN(inputFilename);
-    if (std::error_code EC = fileOrErr.getError()) {
-      llvm::errs() << "Could not open input file: " << EC.message() << "\n";
-      return -1;
-    }
-    llvm::SourceMgr sourceMgr;
-    sourceMgr.AddNewSourceBuffer(std::move(*fileOrErr), llvm::SMLoc());
-    module = mlir::parseSourceFile(sourceMgr, &context);
-    if (!module) {
-      llvm::errs() << "Error can't load file " << inputFilename << "\n";
-      return 3;
-    }
-    if (failed(mlir::verify(*module))) {
-      llvm::errs() << "Error verifying MLIR module\n";
-      return 4;
-    }
-  } else {
+
+  // Handle '.toy' input to the compiler.
+  if (inputType != InputType::MLIR &&
+      !llvm::StringRef(inputFilename).endswith(".mlir")) {
     auto moduleAST = parseInputFile(inputFilename);
-    module = mlirGen(context, *moduleAST);
+    mlir::OwningModuleRef module = mlirGen(context, *moduleAST);
+    if (!module)
+      return 1;
+
+    module->dump();
+    return 0;
   }
-  if (!module)
-    return 1;
+
+  // Otherwise, the input is '.mlir'.
+  llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> fileOrErr =
+      llvm::MemoryBuffer::getFileOrSTDIN(inputFilename);
+  if (std::error_code EC = fileOrErr.getError()) {
+    llvm::errs() << "Could not open input file: " << EC.message() << "\n";
+    return -1;
+  }
+
+  // Parse the input mlir.
+  llvm::SourceMgr sourceMgr;
+  sourceMgr.AddNewSourceBuffer(std::move(*fileOrErr), llvm::SMLoc());
+  mlir::OwningModuleRef module = mlir::parseSourceFile(sourceMgr, &context);
+  if (!module) {
+    llvm::errs() << "Error can't load file " << inputFilename << "\n";
+    return 3;
+  }
+
   module->dump();
   return 0;
 }
index cfde2a7..6e0da33 100755 (executable)
@@ -8,52 +8,58 @@ to compile Toy.
 ## Introduction: Multi-Level IR
 
 Other compilers like LLVM (see the
-[Kaleidoscope tutorial](https://llvm.org/docs/tutorial/LangImpl01.html)) offer
-a fixed set of predefined types and, usually *low-level* / RISC-like,
+[Kaleidoscope tutorial](https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html))
+offer a fixed set of predefined types and, usually *low-level* / RISC-like,
 instructions. It is up to the frontend for a given language to perform any
 language specific type-checking, analysis, or transformation before emitting
 LLVM IR. For example, clang will use its AST to perform static analysis but also
-transformation like C++ template instantiation through AST cloning and rewrite.
-Finally, languages with construction higher-level than C/C++ may require
+transformations like C++ template instantiation through AST cloning and rewrite.
+Finally, languages with construction at a higher-level than C/C++ may require
 non-trivial lowering from their AST to generate LLVM IR.
 
 As a consequence, multiple frontends end up reimplementing significant pieces of
 infrastructure to support the need for these analyses and transformation. MLIR
-addresses this issue by being designed for extensibility. As such, there is
-little to no pre-defined set of instructions (*operations* in MLIR
-terminology) or types.
+addresses this issue by being designed for extensibility. As such, there are
+little to no pre-defined instructions (*operations* in MLIR terminology) or
+types.
 
-## MLIR Module, Functions, Blocks, and Operations
+## MLIR Dialects and Operations
 
-[Language reference](../../LangRef.md#operations)
+[Language reference](../../LangRef.md#dialects)
 
-In MLIR (like in LLVM), the top level structure for the IR is a Module
-(equivalent to a translation unit in C/C++). A module contains a list of
-functions, and each function has a list of blocks forming a CFG. Each block is a
-list of operations that execute in sequence.
+In MLIR, the core unit of abstraction and computation is an `Operation`, similar
+in many ways to LLVM instructions. Operations can be used to represent all of
+the core IR structures in LLVM: instructions, globals(like functions), modules,
+etc; however MLIR does not have a closed set of operations. Instead, the MLIR
+operation set is fully extensible and operations can have application-specific
+semantics.
 
-Operations in MLIR are similar to instructions in LLVM, however MLIR does not
-have a closed set of operations. Instead, MLIR operations are fully extensible
-and can have application-specific semantics.
+MLIR supports this extensibility with the concept of
+[Dialects](../../LangRef.md#dialects). Among other things, Dialects provide a
+grouping mechanism for operations under a unique `namespace`. Dialects will be a
+discussed a bit more in the [next chapter](Ch-3.md).
 
 Here is the MLIR assembly for the Toy 'transpose' operations:
 
 ```MLIR(.mlir)
-%t_array = "toy.transpose"(%array) { inplace: true } : (!toy.array<2, 3>) -> !toy.array<3, 2>
+%t_tensor = "toy.transpose"(%tensor) { inplace = true } : (tensor<2x3xf64>) -> tensor<3x2xf64>
 ```
 
 Let's look at the anatomy of this MLIR operation:
 
 -   it is identified by its name, which is expected to be a unique string (e.g.
     `toy.transpose`).
+    *   the operation name is split in two parts: the dialect namespace prefix,
+        and the specific op name. This can be read as the `transpose` operation
+        in the `toy` dialect.
 -   it takes as input zero or more operands (or arguments), which are SSA values
-    defined by other operations or referring to function and block arguments
-    (e.g. `%array`).
--   it produces zero or more results (we will limit ourselves to single result
-    in the context of Toy), which are SSA values (e.g. `%t_array`).
+    defined by other operations or referring to block arguments (e.g.
+    `%tensor`).
+-   it produces zero or more results (we will limit ourselves to single result
+    operations in the context of Toy), which are SSA values (e.g. `%t_tensor`).
 -   it has zero or more attributes, which are special operands that are always
-    constant (e.g. `inplace: true`).
--   Lastly the type of the operation appears at the end in a functional form,
+    constant (e.g. `inplace = true`).
+-   lastly, the type of the operation appears at the end in a functional form,
     spelling the types of the arguments in parentheses and the type of the
     return values afterward.
 
@@ -63,58 +69,63 @@ in MLIR the location is a core requirement which translates in APIs manipulating
 operations requiring it. Dropping a location becomes an explicit choice and
 cannot happen by mistake.
 
+## Opaque API
 
-## Opaque Builder API
-
-Operations and types can be created with only their string names using the
-raw builder API. This allows MLIR to parse, represent, and round-trip any valid
-IR. For example, the following can round-trip through *mlir-opt*:
+MLIR is designed to be a completely extensible system, as such the
+infrastructure has the capability to opaquely represent operations (as well as
+attributes, types, etc.) that have not been registered. This allows MLIR to
+parse, represent, and round-trip any valid IR. For example, the following can
+round-trip through *mlir-opt*:
 
 ```MLIR(.mlir)
 func @some_func(%arg0: !random_dialect<"custom_type">) -> !another_dialect<"other_type"> {
-  %result = "custom.operation"(%arg0) : (!random_dialect<"custom_type">) -> !another_dialect<"other_type">
+  %result = "custom.operation"(%arg0) { attr = #random_dialect<"custom_attribute"> } : (!random_dialect<"custom_type">) -> !another_dialect<"other_type">
   return %result : !another_dialect<"other_type">
 }
 ```
 
 Here MLIR will enforce some structural constraints (SSA, block termination,
-return operand type coherent with function return type, etc.) but otherwise the
-types and the operation are completely opaque.
+etc.) but otherwise the types and the `custom.operation` are completely opaque.
 
-We will take advantage of this facility to emit MLIR for Toy by traversing the
-AST. Our types will be prefixed with "!toy" and our operation name with "toy.".
-MLIR refers to this prefix as a *dialect*, we will introduce this with more
-details in the [next chapter](Ch-3.md).
+We will take advantage of this facility for the initial emission of MLIR for Toy
+by traversing the AST. Our operation names will be prefixed `toy.` in
+preparation for a `toy` dialect, which we will introduce with more details in
+the [next chapter](Ch-3.md).
 
-Programmatically creating an opaque operation like the one above involves using
-the `mlir::OperationState` structure which group all the basic elements needs to
-build an operation with an `mlir::Builder`:
+Programmatically creating an opaque operation, like the one above, involves
+using the `mlir::OperationState` structure which group all the basic elements
+needed to build an operation with an `mlir::OpBuilder`:
 
 -   The name of the operation.
--   A location for debugging purpose. It is mandatory, but can be explicitly set
-    to "unknown".
--   The list of operand values.
--   The types for returned values.
--   The list of attributes.
--   A list of successors (for branches mostly).
+-   A location for debugging purposes. It is mandatory, but can be explicitly
+    set to `unknown`.
+-   A list of operand values.
+-   A list of types for result values.
+-   A list of attributes.
+-   A list of successors blocks (for branches mostly).
+-   A list of regions (for structural operations like functions).
 
 To build the `custom.operation` from the listing above, assuming you have a
 `Value *` handle to `%arg0`, is as simple as:
 
 ```c++
+// Creation of the state defining the operation:
+mlir::OperationState state(location, "custom.operation");
+state.addOperands(arg0);
+
 // The return type for the operation: `!another_dialect<"other_type">`
-auto another_dialect_prefix = mlir::Identifier::get("another_dialect", &context);
+auto anotherDialectPrefix = mlir::Identifier::get("another_dialect", &context);
 auto returnType = mlir::OpaqueType::get(another_dialect_prefix,
-                                         "custom_type", &context);
-// Creation of the state defining the operation:
-mlir::OperationState state(&context, location, "custom.operation");
-state.types.push_back(returnType);
-state.operands.push_back(arg0);
+                                        "custom_type", &context);
+state.addTypes(returnType);
+
+
 // Using a builder to create the operation and insert it where the builder
 // insertion point is currently set.
-auto customOperation = builder->createOperation(state);
+Operation *customOperation = builder.createOperation(state);
+
 // An operation is not an SSA value (unlike LLVM), because it can return
-// multiple SSA value, the resulting value can be obtained:
+// multiple SSA values, the resulting value can be obtained:
 Value *result = customOperation->getResult(0);
 ```
 
@@ -122,18 +133,18 @@ This approach is used in `Ch2/mlir/MLIRGen.cpp` to implement a naive MLIR
 generation through a simple depth-first search traversal of the Toy AST. Here is
 how we create a `toy.transpose` operation:
 
-```
+```c++
 mlir::Operation *createTransposeOp(OpBuilder &builder,
-                                   mlir::Value *input_array) {
-  // We bundle our custom type in a `toy` dialect.
-  auto toyDialect = mlir::Identifier::get("toy", builder->getContext());
-  // Create a custom type, in the MLIR assembly it is:  !toy.array<2, 2>
-  auto type = mlir::OpaqueType::get(toyDialect, "array<2, 2>", builder->getContext());
-
-  // Fill the `OperationState` with the required fields
-  mlir::OperationState result(builder->getContext(), location, "toy.transpose");
-  result.types.push_back(type);  // return type
-  result.operands.push_back(input_value); // argument
+                                   mlir::Value *input_tensor) {
+  // Fill the `OperationState` with the required fields.
+  mlir::OperationState result(location, "toy.transpose");
+  result.addOperands(input_tensor);
+
+  // We use the MLIR tensor type for 'toy' types.
+  auto type = builder.getTensorType({2, 2}, builder.getF64Type());
+  result.addTypes(type);
+
+  // Create the transpose operation.
   Operation *newTransposeOp = builder->createOperation(result);
   return newTransposeOp;
 }
@@ -141,10 +152,6 @@ mlir::Operation *createTransposeOp(OpBuilder &builder,
 
 ## Complete Toy Example
 
-FIXME: It would be nice to have an idea for the **need** of a custom **type** in
-Toy? Right now `toy<array>` could be replaced directly by unranked `tensor<*>`
-and `toy<array<YxZ>>` could be replaced by a `memref<YxZ>`.
-
 At this point we can already generate our "Toy IR" without having registered
 anything with MLIR. A simplified version of the previous example:
 
@@ -166,51 +173,50 @@ def main() {
 Results in the following IR:
 
 ```MLIR(.mlir)
-func @multiply_transpose(%arg0: !toy<"array">, %arg1: !toy<"array">)
-  attributes  {toy.generic: true} loc("test/codegen.toy":2:1) {
-  %0 = "toy.transpose"(%arg1) : (!toy<"array">) -> !toy<"array"> loc("test/codegen.toy":3:14)
-  %1 = "toy.mul"(%arg0, %0) : (!toy<"array">, !toy<"array">) -> !toy<"array"> loc("test/codegen.toy":3:14)
-  "toy.return"(%1) : (!toy<"array">) -> () loc("test/codegen.toy":3:3)
-}
-
-func @main() loc("test/codegen.toy":6:1) {
-  %0 = "toy.constant"() {value: dense<tensor<2x3xf64>, [[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]>} : () -> !toy.array<2, 3> loc("test/codegen.toy":7:17)
-  %1 = "toy.reshape"(%0) : (!toy.array<2, 3>) -> !toy.array<2, 3> loc("test/codegen.toy":7:3)
-  %2 = "toy.constant"() {value: dense<tensor<6xf64>, [1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]>} : () -> !toy.array<6> loc("test/codegen.toy":8:17)
-  %3 = "toy.reshape"(%2) : (!toy.array<6>) -> !toy.array<2, 3> loc("test/codegen.toy":8:3)
-  %4 = "toy.generic_call"(%1, %3, %1, %3) {callee: "multiply_transpose"} : (!toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>) -> !toy<"array"> loc("test/codegen.toy":9:11)
-  %5 = "toy.generic_call"(%3, %1, %3, %1) {callee: "multiply_transpose"} : (!toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>, !toy.array<2, 3>) -> !toy<"array"> loc("test/codegen.toy":10:11)
-  "toy.print"(%5) : (!toy<"array">) -> () loc("test/codegen.toy":11:3)
-  "toy.return"() : () -> () loc("test/codegen.toy":6:1)
-}
+module {
+  func @multiply_transpose(%arg0: tensor<*xf64>, %arg1: tensor<*xf64>)
+  attributes  {toy.generic} {
+    %0 = "toy.transpose"(%arg1) : (tensor<*xf64>) -> tensor<*xf64> loc("test/codegen.toy":3:14)
+    %1 = "toy.mul"(%arg0, %0) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64> loc("test/codegen.toy":3:14)
+    "toy.return"(%1) : (tensor<*xf64>) -> () loc("test/codegen.toy":3:3)
+  } loc("test/codegen.toy":2:1)
+  func @main() {
+    %0 = "toy.constant"() {value = dense<[[1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>} : () -> tensor<2x3xf64> loc("test/codegen.toy":7:17)
+    %1 = "toy.reshape"(%0) : (tensor<2x3xf64>) -> tensor<2x3xf64> loc("test/codegen.toy":7:3)
+    %2 = "toy.constant"() {value = dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64>} : () -> tensor<6xf64> loc("test/codegen.toy":8:17)
+    %3 = "toy.reshape"(%2) : (tensor<6xf64>) -> tensor<2x3xf64> loc("test/codegen.toy":8:3)
+    %4 = "toy.generic_call"(%1, %3) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/codegen.toy":9:11)
+    %5 = "toy.generic_call"(%3, %1) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64> loc("test/codegen.toy":10:11)
+    "toy.print"(%5) : (tensor<*xf64>) -> () loc("test/codegen.toy":11:3)
+    "toy.return"() : () -> () loc("test/codegen.toy":6:1)
+  } loc("test/codegen.toy":6:1)
+} loc("test/codegen.toy"0:0)
 ```
 
 You can build `toyc-ch2` and try yourself: `toyc-ch2 test/codegen.toy -emit=mlir
--mlir-print-debuginfo`. We can also check our RoundTrip: `toyc-ch2 test/codegen.toy
--emit=mlir -mlir-print-debuginfo > codegen.mlir` followed by `toyc-ch2 codegen.mlir
--emit=mlir`.
-
-Notice how these MLIR operations are prefixed with `toy.` ; by convention we use
-this similarly to a "namespace" in order to avoid conflicting with other
-operations with the same name. Similarly the syntax for types wraps an arbitrary
-string representing our custom types within our "namespace" `!toy<...>`. Of
-course at this point MLIR does not know anything about Toy, and so there is no
-semantic associated with the operations and types, everything is opaque and
-string-based. The only thing enforced by MLIR here is that the IR is in SSA
-form: values are defined once, and uses appears after their definition.
+-mlir-print-debuginfo`. We can also check our RoundTrip: `toyc-ch2
+test/codegen.toy -emit=mlir -mlir-print-debuginfo 2> codegen.mlir` followed by
+`toyc-ch2 codegen.mlir -emit=mlir`.
+
+At this point MLIR does not know anything about Toy, so there are no semantics
+associated with the operations, everything is opaque and string-based. The only
+thing enforced by MLIR here is that the IR is in SSA form: values are defined
+once, and uses appear after their definition.
 
 This can be observed by crafting what should be an invalid IR for Toy and see it
 round-trip without tripping the verifier:
 
 ```MLIR(.mlir)
 // RUN: toyc %s -emit=mlir
+
 func @main() {
-  %0 = "toy.print"() : () -> !toy.array<2, 3>
+  %0 = "toy.print"() : () -> tensor<2x3xf64>
 }
 ```
 
-There are multiple problems here: first the `toy.print` is not a terminator,
-then it should take an operand, and not return any value.
+There are multiple problems here: the `toy.print` operation is not a terminator,
+it should take an operand, and it shouldn't return any values.
 
 In the [next chapter](Ch-3.md) we will register our dialect and operations with
-MLIR, plug in the verifier, and add nicer APIs to manipulate our operations.
+MLIR, plug into the verifier, and add nicer APIs to manipulate our operations.
+
index 77ef2b4..1eb46fd 100644 (file)
@@ -13,20 +13,19 @@ def main() {
   print(d);
 }
 
-# CHECK-LABEL: func @multiply_transpose(%arg0: !toy.array, %arg1: !toy.array)
-# CHECK-NEXT:   attributes  {toy.generic = true} {
-# CHECK-NEXT:   %0 = "toy.transpose"(%arg1) : (!toy.array) -> !toy.array
-# CHECK-NEXT:   %1 = "toy.mul"(%arg0, %0) : (!toy.array, !toy.array) -> !toy.array
-# CHECK-NEXT:   "toy.return"(%1) : (!toy.array) -> ()
-# CHECK-NEXT: }
-
-# CHECK-LABEL: func @main() {
-# CHECK-NEXT:   %0 = "toy.constant"() {value = dense<{{\[\[}}1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>} : () -> !toy.array<2, 3>
-# CHECK-NEXT:   %1 = "toy.reshape"(%0) : (!toy.array<2, 3>) -> !toy.array<2, 3>
-# CHECK-NEXT:   %2 = "toy.constant"() {value = dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64>} : () -> !toy.array<6>
-# CHECK-NEXT:   %3 = "toy.reshape"(%2) : (!toy.array<6>) -> !toy.array<2, 3>
-# CHECK-NEXT:   %4 = "toy.generic_call"(%1, %3) {callee = "multiply_transpose"} : (!toy.array<2, 3>, !toy.array<2, 3>) -> !toy.array
-# CHECK-NEXT:   %5 = "toy.generic_call"(%3, %1) {callee = "multiply_transpose"} : (!toy.array<2, 3>, !toy.array<2, 3>) -> !toy.array
-# CHECK-NEXT:   "toy.print"(%5) : (!toy.array) -> ()
-# CHECK-NEXT:   "toy.return"() : () -> ()
+# CHECK-LABEL:   func @multiply_transpose(
+# CHECK-SAME:                             [[VAL_0:%.*]]: tensor<*xf64>, [[VAL_1:%.*]]: tensor<*xf64>)
+# CHECK-NEXT:    attributes  {toy.generic} {
+# CHECK-NEXT:     [[VAL_2:%.*]] = "toy.transpose"([[VAL_1]]) : (tensor<*xf64>) -> tensor<*xf64>
+# CHECK-NEXT:     [[VAL_3:%.*]] = "toy.mul"([[VAL_0]], [[VAL_2]]) : (tensor<*xf64>, tensor<*xf64>) -> tensor<*xf64>
+# CHECK-NEXT:     "toy.return"([[VAL_3]]) : (tensor<*xf64>) -> ()
 
+# CHECK-LABEL:   func @main() {
+# CHECK-NEXT:     [[VAL_4:%.*]] = "toy.constant"() {value = dense<{{\[\[}}1.000000e+00, 2.000000e+00, 3.000000e+00], [4.000000e+00, 5.000000e+00, 6.000000e+00]]> : tensor<2x3xf64>} : () -> tensor<2x3xf64>
+# CHECK-NEXT:     [[VAL_5:%.*]] = "toy.reshape"([[VAL_4]]) : (tensor<2x3xf64>) -> tensor<2x3xf64>
+# CHECK-NEXT:     [[VAL_6:%.*]] = "toy.constant"() {value = dense<[1.000000e+00, 2.000000e+00, 3.000000e+00, 4.000000e+00, 5.000000e+00, 6.000000e+00]> : tensor<6xf64>} : () -> tensor<6xf64>
+# CHECK-NEXT:     [[VAL_7:%.*]] = "toy.reshape"([[VAL_6]]) : (tensor<6xf64>) -> tensor<2x3xf64>
+# CHECK-NEXT:     [[VAL_8:%.*]] = "toy.generic_call"([[VAL_5]], [[VAL_7]]) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64>
+# CHECK-NEXT:     [[VAL_9:%.*]] = "toy.generic_call"([[VAL_7]], [[VAL_5]]) {callee = @multiply_transpose} : (tensor<2x3xf64>, tensor<2x3xf64>) -> tensor<*xf64>
+# CHECK-NEXT:     "toy.print"([[VAL_9]]) : (tensor<*xf64>) -> ()
+# CHECK-NEXT:     "toy.return"() : () -> ()