51 shape Dialect
51.1 Beginner Summary
The shape dialect is MLIR’s dialect for reasoning about tensor and memref shapes as first-class IR values.
It gives the compiler explicit operations for:
- Asking for the shape, rank, or extent of a value.
- Building shapes from scalar extents.
- Computing shape algebra such as broadcast, concat, split, min, max, and number of elements.
- Representing unknown, invalid, or partially known shape information.
- Expressing shape constraints as compiler-visible witness values.
- Keeping shape computation separate from payload computation.
Think of shape as the dialect for the question “what will the shape of this value be?” It is not a tensor compute dialect. It is a dialect for describing, checking, simplifying, and lowering shape calculations.
51.2 Why This Dialect Exists
Dynamic shapes are one of the hardest parts of a compiler IR.
Consider a tensor operation whose result shape depends on input dimensions:
result shape = broadcast(shape_of(lhs), shape_of(rhs))
The compiler may need to know that result shape before it has lowered the actual tensor computation. It may also need to prove that the broadcast is valid, preserve a shape formula across dialect conversion, or lower the formula to runtime code later.
The shape dialect provides a common language for that work. Without it, every high-level dialect would need its own private representation for ranks, dimensions, shape functions, dynamic shape checks, and invalid shape propagation.
The dialect is also useful because it distinguishes several related ideas that beginners often collapse into one:
- A
!shape.shapecan be unknown, partially known, or invalid. - A
!shape.sizeis a dimension-like scalar that can also be unknown or invalid. - A
tensor<?xindex>extent tensor is a concrete runtime list of extents and is assumed to be error-free. - A
!shape.witnessis not data. It is a proof-like token that carries the fact that a constraint is satisfied. - A
!shape.value_shapecouples a payload value with shape knowledge about that value.
Those distinctions let MLIR model shape reasoning before deciding how much of it becomes runtime code.
51.3 When It Matters
The shape dialect matters whenever a pipeline has dynamic dimensions and needs shape information to stay explicit.
It commonly appears around:
- Shape inference for tensor-producing operations.
- Runtime checks for shape compatibility.
- Broadcasting and elementwise operation result-shape calculation.
- Reified result shapes from higher-level dialects.
- Dynamic rank or dynamic extent manipulation.
- Lowering shape calculations to
arith,tensor,scf,cf, and asserts. - Temporarily outlining shape computation so other passes can operate on the main payload IR.
A typical high-level flow looks like this:
higher-level tensor dialect
-> reify result shapes with shape.shape_of / shape.with_shape
-> simplify and canonicalize shape computations
-> optionally outline-shape-computation
-> shape-to-shape-lowering
-> convert-shape-to-std / convert-shape-constraints
-> arith + tensor + scf + cf
The important point is that shape often lives between semantic tensor dialects and executable control-flow or scalar dialects.
51.4 When To Use It
Use the shape dialect when the IR needs to compute, compare, refine, or check shapes explicitly.
Use it for:
- Querying the shape of a tensor or other shaped value.
- Computing extents, ranks, and element counts.
- Representing shape formulas before lowering them to loops or scalar code.
- Expressing broadcasting rules.
- Capturing runtime shape assertions.
- Associating computed shape information with a value.
- Writing shape functions that describe the result shape of another operation.
Do not use it as the primary representation for tensor element computation. shape tells you about sizes and compatibility. Tensor contents still belong in dialects such as tensor, linalg, tosa, arith, or target-specific dialects.
Also do not keep shape too late in a pipeline if you are producing executable code. Witnesses, constraints, and shape functions are compiler abstractions; they must eventually fold away, become assertions, or lower to ordinary runtime computations.
51.5 Core Concepts
51.5.1 Shape Values
!shape.shape represents a shape as a compiler value.
It can represent:
- An unranked shape.
- A ranked shape with known and unknown extents.
- A fully static shape.
- A scalar shape with rank zero.
- An invalid shape.
The Shape dialect documentation describes printed examples like:
[*]
[?, 2]
[3, 4]
[]
[1]
[invalid]
The key beginner idea is that !shape.shape is richer than a runtime tensor of extents. It can carry unknown and invalid states directly.
51.5.2 Size Values
!shape.size is a non-negative dimension-like scalar.
It is similar to index, but it can also represent unknown and invalid shape states. Shape arithmetic operations such as shape.add, shape.mul, shape.div, shape.min, and shape.max understand that behavior.
If both operands are plain index, these operations can lower to ordinary index arithmetic. If a !shape.size participates, error and unknown behavior must be preserved.
51.5.3 Extent Tensors
An extent tensor is a one-dimensional tensor of index values:
tensor<?xindex>
It represents a runtime list of extents. Unlike !shape.shape, it is guaranteed to be error-free.
Many Shape operations accept both !shape.shape and extent tensors. This lets a pipeline move between abstract shape reasoning and concrete runtime extent lists.
51.5.4 Witness Values
!shape.witness is a proof-like token used to order shape assumptions and constraints.
Constraint operations such as shape.cstr_eq, shape.cstr_broadcastable, and shape.cstr_require produce witnesses. The shape.assuming family consumes those witnesses and marks code that may rely on them.
A witness is not physical runtime data. It is a compiler representation of “this constraint has been established.” Later passes either prove it, erase it, or lower it to side-effecting checks such as cf.assert.
51.5.5 Value Shapes
!shape.value_shape conceptually pairs a value with shape information about that value.
shape.with_shape creates or refines that pairing. shape.value_of extracts the payload value. This is useful when a compiler has computed shape information for an operation result and wants to keep that information attached without immediately changing the payload computation.
51.5.6 Unknown Versus Invalid
Unknown and invalid are different.
An unknown dimension means the compiler does not know the value yet. It may be runtime-dependent and still valid.
An invalid shape means a shape computation found an impossible condition, such as incompatible static dimensions. Most Shape operations that return shapes propagate invalid operands to avoid producing many redundant diagnostics for one root shape error.
51.5.7 Shape Functions
shape.func describes a shape transfer function: a function whose job is to compute a shape, not to compute tensor payload data.
shape.function_library groups shape functions and maps them to operations. This gives a pipeline a way to preserve reusable shape semantics separately from the main computation.
The outline-shape-computation pass uses this idea to pull shape calculations out of the main IR and record how they relate to payload values.
51.6 Operations
51.6.1 Constants And Construction
shape.const_shape creates a constant shape or constant extent tensor.
%s0 = shape.const_shape [] : !shape.shape
%s1 = shape.const_shape [1, 2, 3] : !shape.shape
%e0 = shape.const_shape [4, 5, 6] : tensor<3xindex>
shape.const_size creates a constant !shape.size.
shape.const_witness creates a statically known witness value, usually true or false.
shape.from_extents builds a !shape.shape from SSA extent values.
%shape = shape.from_extents %m, %n : index, index
shape.from_extent_tensor converts a one-dimensional tensor<...xindex> into a !shape.shape.
shape.to_extent_tensor converts a shape-like value into a one-dimensional index tensor. If the input is an invalid !shape.shape, the behavior is undefined because extent tensors cannot carry the invalid state.
shape.index_to_size converts a builtin index to !shape.size.
shape.size_to_index converts !shape.size back to index. Its behavior is undefined for unknown or invalid !shape.size values.
shape.value_as_shape interprets a value, usually a one-dimensional integer or index tensor, as a shape.
51.6.2 Query Operations
shape.shape_of returns the shape of a shaped value or a !shape.value_shape. It may return either !shape.shape or an extent tensor, depending on the result type requested.
shape.dim returns one extent from the shape of a shaped value. It is a convenience form of shape.get_extent(shape.shape_of(value), index).
shape.get_extent extracts one extent from a shape or extent tensor.
shape.rank returns the number of extents in a shape or extent tensor.
shape.num_elements returns the product of all extents.
51.6.3 Shape Algebra
shape.add, shape.mul, and shape.div perform arithmetic on index and !shape.size values while respecting shape error propagation rules.
shape.min and shape.max compute elementwise minimum or maximum on sizes or same-rank shapes.
shape.broadcast computes the result shape of broadcasting two or more input shapes. Smaller ranks are padded with leading ones, and each aligned dimension must either be equal or one side must be one.
shape.is_broadcastable returns an i1 predicate describing whether shape.broadcast would succeed.
shape.concat concatenates two shapes.
shape.split_at splits one shape into a head and tail at an index. Negative indices count from the back.
shape.any returns an arbitrary compatible shape from its inputs. It is useful when multiple shapes are known to be compatible and the compiler only needs one representative.
shape.meet computes the least general shape or size of its operands. It refines unknown information when possible and produces invalid shape information when operands contradict each other.
shape.shape_eq returns an i1 indicating whether one or more shapes or extent tensors are equal.
51.6.4 Constraints And Assumptions
shape.cstr_eq creates a witness that all input shapes are equal.
shape.cstr_broadcastable creates a witness that input shapes can be broadcasted.
shape.cstr_require creates a witness from an arbitrary i1 predicate and an error message.
shape.assuming_all combines multiple witnesses with logical AND.
shape.assuming executes a region under a witness. The region may use facts guaranteed by that witness.
shape.assuming_yield terminates a shape.assuming region and yields its results.
These operations are not intended to survive to final executable IR. They are a structured way to say “this code depends on these shape facts.”
51.6.5 Shape Functions And Regions
shape.func defines a shape function.
shape.return returns values from a shape.func.
shape.function_library groups shape functions and maps them to the operations whose shape behavior they describe.
shape.reduce iterates over the extents of a shape or extent tensor and reduces them with a region.
shape.yield yields values from shape.reduce or shape.function_library.
51.6.6 Value And Shape Pairing
shape.with_shape returns a !shape.value_shape by pairing an operand value with explicit shape information.
shape.value_of extracts the payload value from a !shape.value_shape.
This pair is often used after a shape calculation has been reified. The payload operation can stay in the original dialect while the shape formula is kept available to the compiler.
51.6.7 Debugging
shape.debug_print prints a shape or size and returns it. It is intended for testing and debugging, not as a stable part of production lowering.
51.7 Transformations
51.7.1 Canonicalization And Folding
Many Shape operations have folders or canonicalizers.
Common simplifications include:
- Folding constant shapes and sizes.
- Simplifying known-true or known-false witnesses.
- Combining
shape.assuming_allvalues. - Removing redundant casts between shape and extent-tensor forms.
- Simplifying query operations on constant or statically shaped values.
Canonicalization is especially important for the Shape dialect because many shape checks become trivial once constants, static ranks, or static dimensions are visible.
51.7.2 outline-shape-computation
outline-shape-computation is a module pass.
It finds shape computations introduced by shape reification, especially around shape.with_shape, and outlines them into shape.func functions. The pass also populates shape mapping analysis so the compiler knows which shape function describes which payload value.
Use this when later passes should operate on the main payload IR without being confused by inline shape computation, or when reifying shapes again after a conversion would be difficult.
51.7.3 remove-shape-constraints
remove-shape-constraints is a function pass.
It replaces cstr_ operations with a true witness. This is useful in pipelines that want to erase shape constraints rather than lower them to runtime checks.
This is a semantic choice. Removing constraints means the compiler is no longer preserving those runtime assertions.
51.7.4 shape-to-shape-lowering
shape-to-shape-lowering is a function pass.
It rewrites higher-level Shape dialect constructs into forms that are easier to convert to ordinary scalar and control-flow dialects, especially Arith-related lowering. It is a preparation pass for later conversion, not a final lowering by itself.
51.8 Conversions And Lowering Paths
51.8.1 convert-shape-to-std
convert-shape-to-std lowers many Shape operations to standard MLIR dialects, mainly arith, tensor, and scf.
Examples include:
shape.addandshape.mulon plainindexvalues lower toarith.addiandarith.muli.shape.rankon an extent tensor lowers totensor.dim.shape.get_extenton an extent tensor lowers totensor.extract.shape.const_shapelowers totensor.from_elements.shape.reducelowers toscf.for.shape.shape_ofon tensors lowers to tensor-rank, tensor-dim, or constant extent construction when possible.shape.broadcast,shape.is_broadcastable,shape.shape_eq, andshape.split_atlower to combinations ofarith,tensor, andscfwhen the operands are extent tensors.
The conversion cannot blindly lower every Shape operation in every type form. For example, operations involving !shape.shape or !shape.size may remain when their invalid or unknown behavior still matters.
51.8.2 convert-shape-constraints
convert-shape-constraints lowers shape constraints to eager runtime checking.
For example:
shape.cstr_broadcastablecan become a broadcastability predicate pluscf.assert.shape.cstr_eqcan become a shape equality predicate pluscf.assert.shape.cstr_requirecan becomecf.assertdirectly.
This pass is separate from convert-shape-to-std because a pipeline may want to lower computations and constraints at different times.
51.8.3 Typical Lowering Strategy
A practical lowering sequence often looks like this:
canonicalize shape computations
-> optionally outline-shape-computation
-> shape-to-shape-lowering
-> convert-shape-constraints or remove-shape-constraints
-> convert-shape-to-std
-> lower arith/tensor/scf/cf further
The exact order depends on whether constraints should become runtime asserts or be erased.
51.9 Example IR
51.9.1 Querying Rank And Extent
func.func @rank_and_extent(%arg0: tensor<?x4xf32>, %i: index)
-> (index, index) {
%shape = shape.shape_of %arg0 : tensor<?x4xf32> -> tensor<2xindex>
%rank = shape.rank %shape : tensor<2xindex> -> index
%extent = shape.get_extent %shape, %i
: tensor<2xindex>, index -> index
return %rank, %extent : index, index
}
This example asks MLIR to materialize the tensor shape as an extent tensor, then query the rank and one selected extent.
51.9.2 Broadcasting Under A Witness
func.func @broadcast(%a: tensor<2xindex>, %b: tensor<2xindex>)
-> tensor<?xindex> {
%ok = shape.cstr_broadcastable %a, %b
: tensor<2xindex>, tensor<2xindex>
%result = shape.assuming %ok -> tensor<?xindex> {
%broadcast = shape.broadcast %a, %b
: tensor<2xindex>, tensor<2xindex> -> tensor<?xindex>
shape.assuming_yield %broadcast : tensor<?xindex>
}
return %result : tensor<?xindex>
}
The witness says the broadcast is valid. The region computes the broadcasted shape under that assumption.
51.9.3 Attaching A Computed Shape To A Value
func.func @attach(%value: tensor<?xf32>, %n: index) -> tensor<?xf32> {
%shape = shape.from_extents %n : index
%vs = shape.with_shape %value, %shape : tensor<?xf32>, !shape.shape
%out = shape.value_of %vs : tensor<?xf32>
return %out : tensor<?xf32>
}
This does not compute tensor elements. It attaches the computed one-dimensional shape %shape to %value, then extracts the original payload value again.
51.9.4 Reducing Over Extents
func.func @num_elements(%shape: tensor<?xindex>) -> index {
%one = arith.constant 1 : index
%count = shape.reduce(%shape, %one) : tensor<?xindex> -> index {
^bb0(%i: index, %extent: index, %acc: index):
%next = arith.muli %acc, %extent : index
shape.yield %next : index
}
return %count : index
}
This is the explicit form of multiplying all extents in a shape. The convert-shape-to-std pass lowers this structure to an scf.for loop.
51.10 Mental Model
The Shape dialect is a small symbolic system embedded in MLIR.
It lets the compiler say:
Here is the shape formula.
Here are the constraints that make the formula valid.
Here is where payload IR depends on those shape facts.
Later, decide whether to fold, assert, erase, outline, or lower it.
For beginners, the most useful split is:
shape.shape_of,shape.rank,shape.get_extent, andshape.dimask questions about shapes.shape.from_extents,shape.const_shape, andshape.to_extent_tensorconvert between shape representations.shape.broadcast,shape.concat,shape.split_at,shape.meet, andshape.anycompute new shapes.shape.cstr_*,shape.assuming*, and!shape.witnessexpress facts that must hold.shape.with_shape,shape.value_of,shape.func, andshape.function_librarypreserve shape knowledge across larger compiler transformations.
51.11 Gotchas
!shape.shape and tensor<?xindex> are not the same thing. An extent tensor is an ordinary runtime tensor of extents and cannot represent invalid shape states. A !shape.shape can.
!shape.size and index are also not the same thing. index is a builtin machine-sized integer-like type. !shape.size carries shape-specific unknown and invalid behavior.
Witnesses are not booleans. A witness is a compiler token for an established constraint. Use shape.is_broadcastable or shape.shape_eq when you need an ordinary i1 predicate.
shape.assuming is not a runtime if. It represents code that may assume a witness is true. Later passes decide whether the associated constraint becomes an assertion, folds away, or is removed.
shape.any is intentionally under-specified when inputs disagree. It should be used only when prior reasoning makes the candidate shapes compatible enough for the pipeline’s purposes.
shape.value_of has undefined behavior for unknown or invalid !shape.value_shape operands. It is not a dynamic verifier by itself.
remove-shape-constraints erases checks by replacing constraints with true witnesses. Use convert-shape-constraints instead when runtime assertions must be preserved.
51.12 Source Map
Important local source files:
mlir/include/mlir/Dialect/Shape/IR/ShapeBase.tddefines the dialect and core types:!shape.shape,!shape.size,!shape.value_shape, extent tensors, and!shape.witness.mlir/include/mlir/Dialect/Shape/IR/ShapeOps.tddefines the Shape dialect operations.mlir/lib/Dialect/Shape/IR/Shape.cppimplements parsing, verification, folding, canonicalization hooks, and operation behavior.mlir/lib/Dialect/Shape/IR/ShapeCanonicalization.tdcontains declarative canonicalization patterns.mlir/include/mlir/Dialect/Shape/Transforms/Passes.tddeclaresoutline-shape-computation,remove-shape-constraints, andshape-to-shape-lowering.mlir/lib/Dialect/Shape/Transforms/OutlineShapeComputation.cppimplements shape-computation outlining.mlir/lib/Dialect/Shape/Transforms/RemoveShapeConstraints.cppimplements constraint removal.mlir/lib/Dialect/Shape/Transforms/ShapeToShapeLowering.cppimplements Shape-to-Shape preparation for later lowering.mlir/include/mlir/Conversion/Passes.tddeclaresconvert-shape-to-stdandconvert-shape-constraints.mlir/lib/Conversion/ShapeToStandard/ShapeToStandard.cppimplements general Shape-to-standard lowering.mlir/lib/Conversion/ShapeToStandard/ConvertShapeConstraints.cppimplements shape constraint lowering to assertions.mlir/test/Dialect/Shape/contains parser, verifier, canonicalization, and transform tests for the dialect.mlir/test/Conversion/ShapeToStandard/contains conversion tests.
Generated operation documentation for this checkout lists these 40 operations:
shape.add
shape.any
shape.assuming
shape.assuming_all
shape.assuming_yield
shape.broadcast
shape.concat
shape.const_shape
shape.const_size
shape.const_witness
shape.cstr_broadcastable
shape.cstr_eq
shape.cstr_require
shape.debug_print
shape.dim
shape.div
shape.from_extent_tensor
shape.from_extents
shape.func
shape.function_library
shape.get_extent
shape.index_to_size
shape.is_broadcastable
shape.max
shape.meet
shape.min
shape.mul
shape.num_elements
shape.rank
shape.reduce
shape.return
shape.shape_eq
shape.shape_of
shape.size_to_index
shape.split_at
shape.to_extent_tensor
shape.value_as_shape
shape.value_of
shape.with_shape
shape.yield