68  pdl Dialect

68.1 Beginner Summary

The pdl dialect is MLIR’s Pattern Descriptor Language.

It lets rewrite patterns be represented as MLIR IR. A normal rewrite pattern is usually C++ code: match some operation shape, check constraints, then rewrite the matched IR. PDL makes that structure visible in IR:

  • pdl.pattern defines one pattern.
  • pdl.operation, pdl.operand, pdl.attribute, and pdl.type describe what to match.
  • pdl.rewrite, pdl.replace, and pdl.erase describe what to do after a match.
  • Native hooks connect PDL to externally registered C++ constraints and rewrites.

Think of PDL as a declarative pattern language for describing MLIR rewrites, not as a language for representing the payload program being optimized.

68.2 Why This Dialect Exists

MLIR has many rewrite systems: canonicalization patterns, conversion patterns, folding, and custom optimization passes. Most of those patterns are written in C++, which is powerful but hard to inspect, transform, generate, or verify as IR.

The pdl dialect exists to make patterns themselves part of MLIR.

This matters because a pattern can then be:

  • Parsed and verified like other MLIR.
  • Generated by tools or frontends.
  • Lowered to an interpreter form.
  • Mixed with native C++ hooks where declarative matching is not enough.
  • Used by higher-level pattern languages such as PDLL.

The dialect description says the central idea directly: PDL represents patterns that transform MLIR as MLIR.

68.3 When It Matters

PDL matters when the compiler needs rewrite patterns as data.

It appears in:

  • Declarative rewrite authoring flows.
  • PDLL-generated patterns.
  • Testing and prototyping pattern matching behavior.
  • Pattern engines that lower PDL to pdl_interp.
  • Tools that want to inspect or transform rewrite patterns before running them.

It is less relevant when you are writing a simple one-off C++ pass and do not need patterns to be represented in IR.

68.4 When To Use It

Use pdl when you want to describe an MLIR rewrite pattern declaratively.

Use it for:

  • Matching operation names, operands, attributes, result types, and result values.
  • Replacing matched operations with existing values or newly created operations.
  • Erasing matched operations.
  • Calling native constraints that cannot be expressed in plain PDL.
  • Calling native rewrites for code that should remain in C++.

Do not use PDL as a replacement for dialect conversion infrastructure. PDL can describe patterns, but full conversions still need type converters, legality, materializations, and pass orchestration.

68.5 Core Concepts

68.5.1 Patterns As IR

A PDL pattern is an operation:

pdl.pattern @name : benefit(1) {
  ...
  pdl.rewrite ...
}

The benefit corresponds to rewrite pattern benefit. It helps the rewrite engine choose among competing matches.

Inside the pattern body, most operations describe matcher structure. The final operation is pdl.rewrite, which either names an external rewrite function or contains a rewrite region.

68.5.2 Handles, Not Payload Values

PDL values are handles to MLIR concepts:

PDL type C++ concept
!pdl.operation mlir::Operation *
!pdl.value mlir::Value
!pdl.attribute mlir::Attribute
!pdl.type mlir::Type
!pdl.range<T> A range of PDL handles.

A value of type !pdl.value is not an SSA value in the program being compiled. It is a handle to such a value. This distinction is the most important beginner idea in PDL.

68.5.3 Match Region And Rewrite Region

The main body of pdl.pattern describes the match. It can bind handles to payload operations, operands, attributes, types, and results.

The pdl.rewrite operation describes the action. It has two forms:

  • External rewrite: pdl.rewrite %root with "rewriter"(...args...)
  • Inline PDL rewrite: pdl.rewrite %root { ... }

Inline rewrites may create operations with pdl.operation, then use pdl.replace or pdl.erase.

68.5.4 Roots

The rewrite may name a root operation:

pdl.rewrite %root { ... }

The root is the starting point for the matcher. If the root is omitted, the PDL-to-PDLInterp lowering selects a root among candidate operations in the pattern.

68.5.5 Native Constraints And Rewrites

Some checks or rewrites are easier or only possible in C++. PDL supports this with named native hooks:

  • pdl.apply_native_constraint "name"(...args...)
  • pdl.apply_native_rewrite "name"(...args...)
  • pdl.rewrite %root with "name"(...args...)

The names are not defined by the PDL dialect itself. They must be registered by the consumer of the PDL pattern module.

68.5.6 PDL And PDLInterp

PDL is the authoring form. pdl_interp is the interpreter form.

The convert-pdl-to-pdl-interp pass lowers pdl.pattern operations to a matcher function and generated rewriter functions. That lowering builds a predicate tree from the PDL pattern, emits pdl_interp checks and switches, and erases the original pdl.pattern operations.

68.6 Operations

The dialect has 15 operations.

Operation Purpose
pdl.pattern Defines one rewrite pattern and its benefit.
pdl.operation Describes an operation to match or an operation to create.
pdl.operand Binds one external operand value in the match.
pdl.operands Binds a range of external operand values.
pdl.attribute Binds or constructs an attribute handle.
pdl.type Binds or constructs one type handle.
pdl.types Binds or constructs a range of type handles.
pdl.result Extracts one result from an operation handle.
pdl.results Extracts a result group or all results from an operation handle.
pdl.rewrite Terminates a pattern and specifies the rewrite.
pdl.replace Replaces an operation with values or another operation’s results.
pdl.erase Erases an operation.
pdl.range Constructs a range of PDL values or types inside a rewrite.
pdl.apply_native_constraint Calls an externally registered native constraint.
pdl.apply_native_rewrite Calls an externally registered native rewrite inside a rewrite region.

68.6.1 Matcher Operations

pdl.operation is the central matcher operation. It can constrain:

  • Operation name.
  • Operand handles.
  • Attribute handles by name.
  • Result type handles.

Example:

%root = pdl.operation "arith.addi"(%lhs, %rhs : !pdl.value, !pdl.value)
    -> (%type : !pdl.type)

pdl.operand and pdl.operands describe incoming values that are not otherwise defined in the pattern. pdl.result and pdl.results walk from an operation handle to result value handles.

pdl.attribute, pdl.type, and pdl.types may be unconstrained or fixed to constant attributes/types. An unconstrained type can still be tied to operands, attributes, or operation results so equality constraints are implied.

68.6.2 Rewrite Operations

pdl.rewrite has either an inline body or a native rewrite name.

Inside an inline rewrite:

  • pdl.operation means create an operation.
  • pdl.replace replaces the matched op.
  • pdl.erase erases the matched op.
  • pdl.range can build replacement value/type ranges.
  • pdl.apply_native_rewrite can call out to C++.

The same operation name can therefore have different meaning depending on context. A pdl.operation in the matcher describes an operation that already exists. A pdl.operation inside pdl.rewrite describes an operation to create.

68.6.3 Native Hook Operations

pdl.apply_native_constraint checks a condition. It can also return PDL handles such as attributes, values, or types, but it cannot return an operation handle.

pdl.apply_native_rewrite performs or helps perform a rewrite. It may produce PDL handles that later operations use in the rewrite body.

The external hook names are string attributes. The dialect verifies the shape of the call, but the actual implementation is supplied by the pattern consumer.

68.6.4 Assembly Aliases

Inside pdl.pattern, the default dialect is pdl. Tests and examples often omit the prefix:

%root = operation "foo.op"
rewrite %root { erase %root }

That is the same dialect as:

%root = pdl.operation "foo.op"
pdl.rewrite %root { pdl.erase %root }

This book uses the pdl. prefix in most explanations to keep the dialect visible.

68.7 Types

Type Meaning
!pdl.attribute Handle to an MLIR attribute.
!pdl.operation Handle to an MLIR operation.
!pdl.value Handle to an MLIR SSA value.
!pdl.type Handle to an MLIR type.
!pdl.range<attribute> Range of attribute handles.
!pdl.range<operation> Range of operation handles.
!pdl.range<value> Range of value handles.
!pdl.range<type> Range of type handles.

!pdl.range cannot contain another range. The range element must be one of attribute, operation, value, or type.

The pdl.range operation itself currently constructs ranges of values or types in rewrite regions.

68.8 Transformations

The PDL dialect itself is mainly a representation. Its most important transformation is conversion to the interpreter dialect:

-convert-pdl-to-pdl-interp

This pass:

  • Creates a pdl_interp.func @matcher.
  • Creates a nested module @rewriters.
  • Lowers match structure to pdl_interp checks, switches, traversals, and record_match.
  • Lowers inline rewrites to pdl_interp rewrite operations.
  • Removes the original pdl.pattern operations.

PDL also has ordinary parsing, verification, and canonicalization support, but the major pipeline boundary is the PDL-to-PDLInterp lowering.

68.9 Conversions And Lowering Paths

68.9.1 Into PDL

PDL can be written directly, but it is often generated.

Common sources:

  • PDLL source compiled to MLIR PDL.
  • Tests or tools that construct pattern modules.
  • Frontends that want to emit rewrite patterns as data.
  • C++ code that builds pdl.pattern operations programmatically.

68.9.2 Out Of PDL

The main lowering path is:

pdl.pattern
  -> convert-pdl-to-pdl-interp
  -> pdl_interp matcher and rewriter functions
  -> bytecode/runtime pattern application infrastructure

PDL does not lower to LLVM, SPIR-V, or executable code directly. It lowers to a pattern interpreter representation used by MLIR rewrite infrastructure.

68.10 Example IR

68.10.1 Replace An Operation With One Operand

This pattern matches an arith.addi with two operands of the same type and replaces it with the left-hand operand.

pdl.pattern @replace_addi_with_lhs : benefit(1) {
  %type = pdl.type
  %lhs = pdl.operand : %type
  %rhs = pdl.operand : %type
  %root = pdl.operation "arith.addi"(%lhs, %rhs : !pdl.value, !pdl.value)
      -> (%type : !pdl.type)

  pdl.rewrite %root {
    pdl.replace %root with (%lhs : !pdl.value)
  }
}

Important details:

  • %type is a handle to a type, not a concrete SSA type by itself.
  • Using %type for both operands and the result ties those matched types together.
  • %root is a handle to the matched operation.
  • %lhs is a handle to a payload SSA value.

68.10.2 Native Constraint And Native Rewrite

pdl.pattern @external_rewrite : benefit(2) {
  %input = pdl.operand
  %root = pdl.operation "test.erase_me"(%input : !pdl.value)
  pdl.apply_native_constraint "hasOneUse"(%root : !pdl.operation)
  pdl.rewrite %root with "eraseOrReplace"(%input : !pdl.value)
}

This pattern delegates both the constraint and the rewrite to externally registered functions. PDL records the shape and data flow of the pattern; the consumer supplies the actual native implementation.

68.10.3 Lowering To PDLInterp

Running:

mlir-opt -convert-pdl-to-pdl-interp

on the first example produces a matcher with checks such as operation-name, operand-count, result-count, type equality, and then a generated rewriter function that performs the replacement.

The exact block structure is implementation detail, but the conceptual lowering is:

pdl.operation "arith.addi"
  -> pdl_interp.check_operation_name
  -> pdl_interp.check_operand_count
  -> pdl_interp.check_result_count
  -> pdl_interp.are_equal type checks
  -> pdl_interp.record_match
  -> generated pdl_interp.replace rewriter

68.11 Mental Model

PDL is a graph pattern written as IR.

Read a pdl.pattern as:

  1. Bind handles to pieces of payload IR.
  2. Add constraints by naming operations, types, attributes, operands, and results.
  3. Choose a root for matching.
  4. Describe how the matched IR is replaced, erased, or handed to native code.

The pattern does not execute like normal program IR. It is compiled into matcher and rewriter machinery.

68.12 Gotchas

PDL values are handles. !pdl.value is not the same as the payload value’s actual type. Use pdl.type and pdl.types handles to constrain payload types.

pdl.operation changes meaning by context. In the matcher body, it describes an existing operation. In a rewrite body, it creates a new operation.

Unbound matcher values are rejected. Matcher-side pdl.operand, pdl.operands, pdl.attribute, and similar declarations must be connected to a bindable use.

Inline rewrite operations need inferable result types. If a created operation’s result types cannot be inferred from constants, matched entities, replacement uses, native rewrite results, or InferTypeOpInterface, verification can fail.

Native hook names are just names until registered. A PDL module can parse with "myConstraint" or "myRewrite", but applying the pattern requires a consumer that knows those names.

pdl.apply_native_constraint must have at least one argument, and it cannot return an operation handle.

pdl.apply_native_rewrite must have at least one argument or one result.

pdl.attribute inside a pdl.rewrite region must have a constant value.

pdl.range construction is limited compared with the type system. The !pdl.range<T> type can describe ranges of attributes, operations, values, or types, but pdl.range constructs value/type ranges in rewrite regions.

68.13 Source Map

Primary definitions:

  • mlir/include/mlir/Dialect/PDL/IR/PDLDialect.td
  • mlir/include/mlir/Dialect/PDL/IR/PDLOps.td
  • mlir/include/mlir/Dialect/PDL/IR/PDLTypes.td

Implementation:

  • mlir/lib/Dialect/PDL/IR/PDL.cpp
  • mlir/lib/Dialect/PDL/IR/PDLTypes.cpp

Conversion:

  • mlir/include/mlir/Conversion/Passes.td
  • mlir/include/mlir/Conversion/PDLToPDLInterp/PDLToPDLInterp.h
  • mlir/lib/Conversion/PDLToPDLInterp/PDLToPDLInterp.cpp
  • mlir/lib/Conversion/PDLToPDLInterp/PredicateTree.cpp
  • mlir/lib/Conversion/PDLToPDLInterp/Predicate.cpp
  • mlir/lib/Conversion/PDLToPDLInterp/RootOrdering.cpp

Tests:

  • mlir/test/Dialect/PDL/
  • mlir/test/Conversion/PDLToPDLInterp/
  • mlir/test/mlir-pdll/