8 Type
An MLIR type is a compile-time contract attached to every SSA value. It tells the verifier, analyses, transformations, and lowering passes what kind of entity a value represents and which operations may consume it. A type is more than a storage width: tensor<4x8xf32> and memref<4x8xf32> describe very different programming models despite sharing shape and element type.
%tensor = tensor.empty() : tensor<4x8xf32>
%buffer = memref.alloc() : memref<4x8xf32>
The tensor result represents an immutable value. The memref result represents mutable memory with layout and memory-space meaning. Treating them as interchangeable would hide reads, writes, aliasing, allocation, and ownership questions that later passes need to answer.
8.1 Types Are Interned, Immutable IR Objects
In the C++ API, Type is a lightweight handle to immutable storage managed by an MLIR context. Types are generally uniqued: asking the same context for the same structural type yields an equivalent, shared representation. This makes types cheap to copy and compare, but it also means they are not mutable annotations. A transformation cannot change the type of %x; it must create a new value with a new type and rewrite dependent operations.
%i = arith.constant 7 : i32
%f = arith.sitofp %i : i32 to f32
The conversion is an operation because the representation change has semantics. This remains true even when the conversion is eventually lowered away. Type changes in a compiler pipeline should always be visible in either an operation, a conversion materialization, or a replacement mapping.
8.2 Builtin And Dialect-Specific Types
MLIR’s builtin types cover common scalar and aggregate forms:
i1 // signless integer with one bit
i32 // signless integer with 32 bits
f16 // 16-bit floating-point
index // target-dependent index type
vector<8xf32>
tensor<2x?xf32>
memref<?x?xf32>
Dialects may define additional types when builtin types cannot express their domain. A GPU dialect may need address-space-sensitive types; an asynchronous dialect may use token or value-container types; a hardware dialect may model bit vectors or channel protocols. The type’s dialect owns its parsing, verification, storage, and C++ APIs, just as an operation’s dialect owns its semantics.
The printed syntax does not determine how much information a type carries. index deliberately does not state a bit width because its width is chosen for the target or lowering strategy. Conversely, a memref can carry rank, dynamic/static shape, element type, layout, and memory space. Read the type as a compact semantic contract, not simply a spelling to copy into an operation.
8.3 Shape, Element Type, Layout, And Memory Space
Shaped types deserve deliberate reading. In tensor<2x?xf32>, the rank is two, the first dimension is statically known to be two, the second is dynamic, and the element type is f32. A question mark does not mean “unknown rank”; it means that this dimension’s size is known only at runtime. An unranked tensor is written differently and gives up rank information entirely.
For memrefs, layout and memory space can affect legality and code generation:
memref<4x8xf32, strided<[8, 1], offset: 0>>
memref<1024xf32, 3>
The first describes a particular strided layout. The second uses memory space 3, whose exact meaning is defined by the surrounding lowering and target conventions. A transformation may be valid for a contiguous host memref and invalid for a non-identity layout or a device address space. This is why robust passes query the type rather than assume a default layout.
8.4 Signless Integers And Semantics
MLIR’s builtin integer types are signless: i32 does not itself say signed or unsigned. The operation supplies that interpretation. arith.divsi and arith.divui consume the same i32 type but define different division semantics. This design avoids duplicating every integer type as signed and unsigned while keeping the operation’s intended behavior explicit.
Do not conclude that signedness never matters. It matters precisely where an operation interprets bits, and it matters to transformations involving overflow, comparisons, extension, truncation, and lowering. A rewrite from a signed comparison to an unsigned comparison is not a type-preserving no-op.
8.5 Type Constraints And Inference
Operation definitions often express relationships among types: operands and results may have to match, elements may have to be integers, a result may be a shaped type with the same element type as an input, or a region terminator’s values may have to match the enclosing operation’s results. These constraints are verified at IR construction and parsing time.
Some types can be inferred. For example, many arithmetic operations infer a result type from their operands, and builders may omit a redundant result-type argument. Inference is a convenience, not a relaxation of correctness. When writing generic IR or debugging a parse failure, make types explicit until the invariants are understood.
8.6 Types Drive Conversion Boundaries
Lowering is often a series of representation changes:
tensor<?xf32> -> memref<?xf32> -> LLVM pointer/descriptors
index -> target-sized integer
vector<8xf32> -> target vector or scalarized instructions
A dialect conversion identifies old types that are no longer legal and maps them to legal replacements with a TypeConverter. Operations cannot simply retain old operand types while their results change: the conversion framework must either rewrite the operation and its users coherently or insert explicit materializations at a supported boundary. This is why type conversion is a first-class part of lowering rather than an afterthought.
8.7 Writing Type-Aware Passes
Use the most specific checks that the pass actually requires. If a pattern only works for ranked tensors with static shape, state that and fail to match other inputs. If it works for any shaped type, avoid hard-coding RankedTensorType. Use dialect type APIs and interfaces where available, and preserve encoding, layout, element type, and memory-space information unless your transformation has a documented reason to change them.
A common failure is to rebuild a shaped type from only its shape and element type, silently dropping an encoding or layout attribute. Another is to create a converted value but leave a user expecting the old type. The verifier may catch these immediately; when it does, the type mismatch is usually evidence that the transformation’s representation boundary was not fully modeled.