MLIR-Code Doc-Tutorials-Defining Dialect Attributes & Types

   this document is to get a quick start to the specific extension of dialect to the attribute and type system of MLIR. Although the main content of this teaching focuses on the definition of types, these instructions are almost the same for attributes.

Types

   types in MLIR (including attributes, locations, and many other things) are value types. This means that examples of Type are passed by value, not by pointer or reference. Typeclass 1 It acts as a wrapper for the internal storage object, which is unique in an MLIRContext instance.

Defining the Type class

   according to the above description, the Type object is of value Type in MLIR and depends on having an implicit internal storage object that holds the actual data of this Type. To define a new Type, there is no need to define a new storage class (this is the class of internal storage object mentioned above). Therefore, before defining the derived Type, it is very important to know which Type the Type we are going to define belongs to:
   some types are singletons in nature, which means they have no parameters and only one instance, such as index type.
   other types are parametric. They contain additional information to distinguish different instances of the same Type (instances here refer to types). For example, integer type contains bit width information, and i8 and i6 represent two different instances of integer type.
   parameterized types can also contain variable components. For example, this component can be used to construct self referencing recursive types. Variable components cannot be used to distinguish instances of Type classes, so parametric types that contain variable components usually contain other parameter components that identify them.

Singleton types

   for singleton types, we can directly skip to the step of defining the type class. Since there can only be one instance of a singleton type (the instance here refers to the instance of the type), we do not need to provide our own storage class.

/// This class defines a simple parameterless singleton type. All derived types
/// must inherit from the CRTP class 'Type::TypeBase'. It takes as template
/// parameters the concrete type (SimpleType), the base class to use (Type),
/// the internal storage class (the default TypeStorage here), and an optional
/// set of type traits and interfaces(detailed below).
///This class defines a simple parameterless singleton type.
///All types derived classes must inherit from CRTP class -'Type::TypeBase '.
///'Type::TypeBase 'will specify the specific type (SimpleType), the base class to be used (Type)
///The internal storage class (default typestorage here) and a set of optional type features and interfaces (described in detail below) are used as template parameters.
class SimpleType : public Type::TypeBase<SimpleType, Type, TypeStorage> {
public:
  /// Inherit some necessary constructors from 'TypeBase'.
  ///Inherit some necessary constructors from 'TypeBase'.
  using Base::Base;

  /// The `TypeBase` class provides the following utility methods for
  /// constructing instances of this type:
  ///The 'TypeBase' class provides the following utility methods to build an instance of the type
  /// static SimpleType get(MLIRContext *ctx);
};

Parametric types

   parameterized types are those with additional structures or unique constraints, which are allowed to be expressed as different instances of a class. Therefore, the parameterized type needs to define a type storage class to contain the parameter data of the parameterized type.

Defining a type storage

   the type storage object contains all the necessary parameters that a parameterized type instance can be built and unique. Storage classes must follow the following rules:

  • Inherit base type storage class - 'TypeStorage'
  • Define a Type alias, KeyTy, as the unique identifier of the Type derived class instance.
  • Provide a constructor that the storage class will use to allocate new instances.
    • static Storage *construct(TypeStorageAllocator &, const KeyTy &key)
  • A method is provided to store the comparison between the class (KeyTy) and the KeyTy
    • bool operator==(const KeyTy &) const
  • Provide a method to generate a KeyTy with a set of parameters and pass it to the unique identifier? (Note: This is not necessary unless KeyTy cannot be built by default with these parameters)
    • static KeyTy getKey(Args...&& args)
  • Provides a method for hashing instances of KeyTy. (Note: This is not necessary if there is a special llvm:: densemapinfo < KeyTy >)
    • static llvm::hash_code hasKey(const KeyTy &)
        let's take an example of a storage class:
/// Here we define a storage class for a ComplexType, that holds a non-zero
/// integer and an integer type.
///Here we define a storage class for ComplexType, which holds an unsigned and a Type
///Note that the Type here is a Type in MLIR, not the target Type mentioned in our article
struct ComplexTypeStorage : public TypeStorage {
  ComplexTypeStorage(unsigned nonZeroParam, Type integerType)
      : nonZeroParam(nonZeroParam), integerType(integerType) {}

  /// The hash key for this storage is a pair of the integer and type params.
  ///For this storage class, its key type is an unsigned key value pair with key.
  using KeyTy = std::pair<unsigned, Type>;

  /// Define the comparison function for the key type.
  ///Create a comparison function for the KeyTy type
  bool operator==(const KeyTy &key) const {
    return key == KeyTy(nonZeroParam, integerType);
  }

  /// Define a hash function for the key type.
  /// Note: This isn't necessary because std::pair, unsigned, and Type all have
  /// hash functions already available.
  ///Define a hash function for KeyTy.
  ///In this scenario, this function is unnecessary because STD:: pair, unsigned and type all have hash functions.
  static llvm::hash_code hashKey(const KeyTy &key) {
    return llvm::hash_combine(key.first, key.second);
  }

  /// Define a construction function for the key type.
  /// Note: This isn't necessary because KeyTy can be directly constructed with
  /// the given parameters.
  ///Define a constructor for KeyTy
  ///In this scenario, this function is not necessary, because KeyTy can be directly constructed through unsigned,Type,std::pair
  static KeyTy getKey(unsigned nonZeroParam, Type integerType) {
    return KeyTy(nonZeroParam, integerType);
  }

  /// Define a construction method for creating a new instance of this storage.
  ///Define a constructor to generate a new instance of the storage class
  static ComplexTypeStorage *construct(TypeStorageAllocator &allocator,
                                       const KeyTy &key) {
    return new (allocator.allocate<ComplexTypeStorage>())
        ComplexTypeStorage(key.first, key.second);
  }

  /// The parametric data held by the storage class.
  unsigned nonZeroParam;
  Type integerType;
};

Define Type class

  now that the storage class has been created, you can start defining the Type derived class. The structure is similar to the singleton Type, except that more functions provided by Type::TypeBase are used.

/// This class defines a parametric type. All derived types must inherit from
/// the CRTP class 'Type::TypeBase'. It takes as template parameters the
/// concrete type (ComplexType), the base class to use (Type), the storage
/// class (ComplexTypeStorage), and an optional set of traits and
/// interfaces(detailed below).
///This class defines a parameterized type.
///All derived types must inherit 'Type::TypeBase'.
///'Type::TypeBase 'will specify the specific type (SimpleType), the base class to be used (Type)
///The internal storage class (default typestorage here) and a set of optional type features and interfaces (described in detail below) are used as template parameters.
class ComplexType : public Type::TypeBase<ComplexType, Type,
                                          ComplexTypeStorage> {
public:
  /// Inherit some necessary constructors from 'TypeBase'.
  ///Inherit some necessary constructors from 'TypeBase'
  using Base::Base;

  /// This method is used to get an instance of the 'ComplexType'. This method
  /// asserts that all of the construction invariants were satisfied. To
  /// gracefully handle failed construction, getChecked should be used instead.
  ///This method is used to get an instance of 'ComplexType'.
  ///This method asserts that all construction invariants are satisfied (I don't know whether it will be asserted or guaranteed by the caller).
  ///To handle failed constructs gracefully, you should use getChecked
  static ComplexType get(unsigned param, Type type) {
    // Call into a helper 'get' method in 'TypeBase' to get a uniqued instance
    // of this type. All parameters to the storage class are passed after the
    // context.
    // Call the helper 'get' method in 'TypeBase' to get a unique instance of the type.
    // All parameters will be passed to the storage class through the context?
    // There are two questions: type Getcontext() should get an MLIRContext
    // 1. If my element Type does not contain Type, how can I get a global MLIRContext
    // 2. Why can I get the MLIRContext from the Type passed in
    return Base::get(type.getContext(), param, type);
  }

  /// This method is used to get an instance of the 'ComplexType'. If any of the
  /// construction invariants are invalid, errors are emitted with the provided
  /// `emitError` function and a null type is returned.
  /// Note: This method is completely optional.
  ///This method is used to get an instance of 'ComplexType'.
  ///If the construction invariant is invalid, the error will be emitted to the 'emitError' method, and getChecked returns Null Type
  static ComplexType getChecked(function_ref<InFlightDiagnostic()> emitError,
                                unsigned param, Type type) {
    // Call into a helper 'getChecked' method in 'TypeBase' to get a uniqued
    // instance of this type. All parameters to the storage class are passed
    // after the context.
    return Base::getChecked(emitError, type.getContext(), param, type);
  }

  /// This method is used to verify the construction invariants passed into the
  /// 'get' and 'getChecked' methods. Note: This method is completely optional.
  ///This method is used to verify the construction invariants passed to 'get' and 'getChecked'.
  static LogicalResult verify(function_ref<InFlightDiagnostic()> emitError,
                              unsigned param, Type type) {
    // Our type only allows non-zero parameters.
    if (param == 0)
      return emitError() << "non-zero parameter passed to 'ComplexType'";
    // Our type also expects an integer type.
    if (!type.isa<IntegerType>())
      return emitError() << "non integer-type passed to 'ComplexType'";
    return success();
  }

  /// Return the parameter value.
  ///These two functions are unnecessary. If you want to get the elements of the built storage object, you can do so.
  unsigned getParameter() {
    // 'getImpl' returns a pointer to our internal storage instance.
    return getImpl()->nonZeroParam;
  }

  /// Return the integer parameter type.
  IntegerType getParameterType() {
    // 'getImpl' returns a pointer to our internal storage instance.
    return getImpl()->integerType;
  }
};

Mutable types

   a Type containing a variable component is a special instance of a parameterized Type that allows some parameters to be changed after construction.

Defining a type storage

   in addition to the requirements of type storage classes for typing parameters, type storage classes with variable components must also comply with the following provisions.

  • Variable components cannot be part of a KeyTy
  • Provides a mutable method for modifying an existing type store instance. This method modifies variable components through parameters. Any new dynamic allocation storage must use allocator. This method also needs to identify whether the modification is successful
    • LogicalResult muate(StorageAllocator &allocator, Args ...&& args)

   define a type storage class of simple recursive types, identify the type by name, and may include another type (including itself):

/// Here we define a storage class for a RecursiveType that is identified by its
/// name and contains another type.
///Define a type storage class for RecursiveType, identify the class by name, and package other types.
struct RecursiveTypeStorage : public TypeStorage {
  /// The type is uniquely identified by its name. Note that the contained type
  /// is _not_ a part of the key.
  ///The class is uniquely identified by name. Note that the include type is not part of the key
  using KeyTy = StringRef;

  /// Construct the storage from the type name. Explicitly initialize the
  /// containedType to nullptr, which is used as marker for the mutable
  /// component being not yet initialized.
  ///Type storage instances are constructed by type names. Initializes the containing type to a null pointer,
  ///The identity variable component has not been initialized.
  RecursiveTypeStorage(StringRef name) : name(name), containedType(nullptr) {}

  /// Define the comparison function.
  ///For comparison functions, MLIR does not prohibit body from participating in comparison!
  bool operator==(const KeyTy &key) const { return key == name; }

  /// Define a construction method for creating a new instance of the storage.
  static RecursiveTypeStorage *construct(StorageAllocator &allocator,
                                         const KeyTy &key) {
    // Note that the key string is copied into the allocator to ensure it
    // remains live as long as the storage itself.
    return new (allocator.allocate<RecursiveTypeStorage>())
        RecursiveTypeStorage(allocator.copyInto(key));
  }

  /// Define a mutation method for changing the type after it is created. In
  /// many cases, we only want to set the mutable component once and reject
  /// any further modification, which can be achieved by returning failure from
  /// this function.
  ///Create a variable function to change the type after the type storage instance is created.
  ///In many cases, we only need to set the variable component once and refuse to modify it again.
  ///This can be achieved by returning a failure from the function.
  LogicalResult mutate(StorageAllocator &, Type body) {
    // If the contained type has been initialized already, and the call tries
    // to change it, reject the change.
    if (containedType && containedType != body)
      return failure();

    // Change the body successfully.
    containedType = body;
    return success();
  }

  StringRef name;
  Type containedType;
};

Define Type class

  now that we have a type storage class, we can define the type class itself. Type::TypeBase provides a mutate method, which forwards its parameters to the mutate method of the type storage class and ensures safe mutation (I don't know what burst is)

class RecursiveType : public Type::TypeBase<RecursiveType, Type,
                                            RecursiveTypeStorage> {
public:
  /// Inherit parent constructors.
  using Base::Base;

  /// Creates an instance of the Recursive type. This only takes the type name
  /// and returns the type with uninitialized body.
  static RecursiveType get(MLIRContext *ctx, StringRef name) {
    // Call into the base to get a uniqued instance of this type. The parameter
    // (name) is passed after the context.
    return Base::get(ctx, name);
  }

  /// Now we can change the mutable component of the type. This is an instance
  /// method callable on an already existing RecursiveType.
  ///Now we can change the variable component of this type.
  ///This is an instance method that is called in the existing RecursiveType instance.
  void setBody(Type body) {
    // Call into the base to mutate the type.
    // Call the change method provided by base to change the type.
    LogicalResult result = Base::mutate(body);

    // Most types expect the mutation to always succeed, but types can implement
    // custom logic for handling mutation failures.
    // Most types are expected to change successfully, but types still need to implement custom logic that can handle the failure of change.
    assert(succeeded(result) &&
           "attempting to change the body of an already-initialized type");

    // Avoid unused-variable warning when building without assertions.
    (void) result;
  }

  /// Returns the contained type, which may be null if it has not been
  /// initialized yet.
  Type getBody() {
    return getImpl()->containedType;
  }

  /// Returns the name.
  StringRef getName() {
    return getImpl()->name;
  }
};

Registering types with a dialect

  once the types in a Dialect are defined, they must be registered in a Dialect dialect. Register types through a mechanism similar to registering Op, with the help of addTypes method. The obvious difference from Op is that when registering a Type, its corresponding TypeStorage class definition must be visible.

struct MyDialect : public Dialect {
  MyDialect(MLIRContext *context) : Dialect(/*name=*/"mydialect", context) {
    /// Add these defined types to the dialect.
    ///The template parameter of this addTypes is different from the instance in the toy language. You have to look at the source code.
    addTypes<SimpleType, ComplexType, RecursiveType>();
  }
};

Parsing and printing

   as the last step of registration, the two hook functions printType and parseType of dialect must be rewritten. They support Type and can be used in Two way translation in the text at the end of mlir.

class MyDialect : public Dialect {
public:
  /// Parse an instance of a type registered to the dialect.
  Type parseType(DialectAsmParser &parser) const override;

  /// Print an instance of a type registered to the dialect.
  void printType(Type type, DialectAsmPrinter &printer) const override;
};

  these methods need a high-level parser or printer, so that you can easily get some necessary functions. According to MLIR language reference, the general format of dialect type should be as follows:! Dialect namespace < type data >, which can maintain a beautiful format in some cases. The responsibility of parser and printer is to provide type data in the expression.

Traits

  similar to Op, the Type class may also attach Traits to provide additional mixin methods and other types. The Traits class can be specified through the template parameter of the Type::TypeBase class. More about the definition and use of trail are described in the trail document.

Interfaces

   similar to Op, the Type class can attach Interfaces to provide an abstract interface for the Type. More about the definition and use of Interfaces are described in the Interfaces document.

Attributes

  as shown in the introduction, the process of defining dialect attributes is almost the same as that of defining dialect types. The key difference is that * Type appearing in the process of dialect Type definition is now replaced by * Attr.

  • Type::TypeBase -> Attribute::AttrBase
  • TypeStorageAllocator -> AttributeStorageAllocator
  • addTypes -> addAttributes

In addition, all interfaces used to uniquely instantiate and store constructs are the same.

  1. Class is used here instead of being translated into Chinese (class) to express a Type in MLIR. It looks like a class, but this class can not completely describe the Type, and it also needs the help of internal storage objects. ↩︎

Keywords: compiler

Added by darksystem on Tue, 08 Feb 2022 00:11:52 +0200