69 pdl_interp Dialect
69.1 Beginner Summary
The pdl_interp dialect is the executable form of PDL patterns.
PDL lets compiler developers describe rewrites in a declarative, pattern-shaped IR. pdl_interp lowers those patterns into smaller interpreter operations that can be compiled into MLIR’s pattern bytecode. A beginner should think of it as the “compiled pattern program” that MLIR runs when applying PDL rewrite patterns.
Most users do not write pdl_interp by hand. They write PDL, C++ rewrite patterns, or Transform dialect scripts. pdl_interp matters when you want to understand how PDL is executed, why pattern matching can be optimized, and how declarative rewrites become real matcher and rewriter code.
69.2 Why This Dialect Exists
PDL is intentionally high level. It describes ideas such as “match an operation with this name”, “check this operand type”, and “rewrite the root operation”. That form is pleasant for humans and tools, but it is not the most direct form for an interpreter.
pdl_interp exists to make PDL execution explicit:
- It separates matching from rewriting.
- It turns high-level PDL constraints into explicit predicate checks.
- It uses branches, switches, and loops to encode matcher control flow.
- It records successful matches with enough metadata to invoke the right rewriter later.
- It gives MLIR’s rewrite engine a compact form that can be compiled into pattern bytecode.
The source dialect summary calls it an “Interpreted pattern execution dialect”. That phrase is literal: the dialect is built for running patterns.
69.3 When It Matters
pdl_interp matters when you are studying or debugging:
- PDL pattern lowering.
- Pattern bytecode generation.
- Why a PDL pattern matches or does not match.
- How native constraints and native rewrites connect to PDL.
- Multi-root or multi-operation patterns.
- Pattern benefit, root operation selection, and generated operation metadata.
It is especially important if you are implementing infrastructure around RewritePatternSet, FrozenRewritePatternSet, or tools that generate PDL patterns.
69.4 When To Use It
Use pdl_interp directly when:
- You are testing PDL interpreter internals.
- You are reading the output of
-convert-pdl-to-pdl-interp. - You are debugging the lowering of a PDL pattern.
- You are working on MLIR’s rewrite infrastructure.
Do not usually use it as the authoring dialect for compiler rewrites. For normal work, write patterns in PDL or C++, then let MLIR lower PDL into pdl_interp.
69.5 Core Concepts
69.5.1 Matcher Function
The lowering creates a matcher function named matcher. It accepts the current root operation as !pdl.operation and branches through a decision tree of checks. If a pattern matches, the matcher emits pdl_interp.record_match.
69.5.2 Rewriter Module
The lowering creates a nested module named rewriters. That module contains pdl_interp.func functions that perform the rewrite after a match is selected.
69.5.3 PDL Values
The dialect depends on the PDL dialect and uses PDL handle types:
!pdl.operationis a handle to an operation.!pdl.valueis a handle to an SSA value.!pdl.typeis a handle to a type.!pdl.attributeis a handle to an attribute.!pdl.range<...>is a range of PDL handles.
These are not normal runtime program values. They are interpreter handles used while matching and rewriting MLIR IR.
69.5.4 Predicate Control Flow
Many pdl_interp operations combine a predicate with branches. For example, pdl_interp.check_operation_name both checks an operation name and branches to the success or failure block. This is different from a normal IR style where a compare operation produces an i1 and a separate branch consumes it.
That fusion is deliberate. It reduces interpreter work.
69.5.5 Null Handles
Several query operations can return a null PDL handle. For example, pdl_interp.get_operand 3 of %op returns null if %op has no operand at index 3. Matchers usually follow those queries with pdl_interp.is_not_null.
69.5.6 Match Recording
pdl_interp.record_match does not rewrite immediately. It records that a pattern matched, which rewriter function should run, what benefit the pattern has, which operations contributed to the match location, and what values should be passed into the rewriter.
69.6 Operations
The dialect has 39 operations.
69.6.1 Function And Control Operations
| Operation | Meaning |
|---|---|
pdl_interp.func |
Defines an interpreter function for matcher or rewriter code. |
pdl_interp.finalize |
Terminates a matcher or rewriter sequence. |
pdl_interp.branch |
Unconditionally branches to another block. |
pdl_interp.foreach |
Iterates over a range of PDL entities. |
pdl_interp.continue |
Continues the nearest pdl_interp.foreach loop. |
pdl_interp.record_match |
Records a successful pattern match and its rewriter metadata. |
69.6.2 Predicate And Check Operations
| Operation | Meaning |
|---|---|
pdl_interp.apply_constraint |
Calls a registered native constraint and branches on success or failure. |
pdl_interp.are_equal |
Checks equality between two PDL entities or ranges. |
pdl_interp.check_attribute |
Checks an attribute against a constant attribute. |
pdl_interp.check_operand_count |
Checks an operation operand count, either exact or at least a minimum. |
pdl_interp.check_operation_name |
Checks an operation name. |
pdl_interp.check_result_count |
Checks an operation result count, either exact or at least a minimum. |
pdl_interp.check_type |
Checks a type handle against a known type. |
pdl_interp.check_types |
Checks a range of type handles against a known type list. |
pdl_interp.is_not_null |
Checks that a PDL entity or range handle exists. |
69.6.3 Switch Operations
| Operation | Meaning |
|---|---|
pdl_interp.switch_attribute |
Dispatches based on an attribute value. |
pdl_interp.switch_operand_count |
Dispatches based on an operation’s operand count. |
pdl_interp.switch_operation_name |
Dispatches based on an operation name. |
pdl_interp.switch_result_count |
Dispatches based on an operation’s result count. |
pdl_interp.switch_type |
Dispatches based on one type value. |
pdl_interp.switch_types |
Dispatches based on a range of type values. |
Switch operations are useful when multiple PDL patterns share the same query but have different constant answers. Instead of emitting repeated checks, lowering can build a compact dispatch.
69.6.4 IR Query Operations
| Operation | Meaning |
|---|---|
pdl_interp.extract |
Extracts one item from a PDL range. |
pdl_interp.get_attribute |
Gets a named attribute from an operation. |
pdl_interp.get_attribute_type |
Gets the type associated with an attribute. |
pdl_interp.get_defining_op |
Gets the defining operation for a value or value range. |
pdl_interp.get_operand |
Gets one indexed operand from an operation. |
pdl_interp.get_operands |
Gets one operand group, or all operands, from an operation. |
pdl_interp.get_result |
Gets one indexed result from an operation. |
pdl_interp.get_results |
Gets one result group, or all results, from an operation. |
pdl_interp.get_users |
Gets users of a value or value range. |
pdl_interp.get_value_type |
Gets the type of a value, or the type range for a value range. |
These operations navigate the IR being matched. They are the interpreter-level equivalent of calling APIs such as Operation::getOperand, Operation::getResult, Value::getDefiningOp, and Value::getUsers.
69.6.5 Rewrite And Construction Operations
| Operation | Meaning |
|---|---|
pdl_interp.apply_rewrite |
Calls a registered native rewrite function. |
pdl_interp.create_attribute |
Creates a PDL handle for a constant attribute. |
pdl_interp.create_operation |
Creates an MLIR operation from PDL handles. |
pdl_interp.create_range |
Creates a PDL range from PDL entities or ranges. |
pdl_interp.create_type |
Creates a PDL handle for a constant type. |
pdl_interp.create_types |
Creates a PDL range handle for constant types. |
pdl_interp.erase |
Erases an operation through the pattern rewriter. |
pdl_interp.replace |
Replaces an operation through the pattern rewriter. |
These operations are normally found inside the generated rewriter functions.
69.7 Transformations
pdl_interp is not a general-purpose program optimization dialect. It does not have a large suite of standalone optimization passes like linalg, scf, or vector.
The important transformation is:
PDL patterns -> PDLInterp matcher/rewriter IR -> PDL bytecode
The lowering process performs important pattern-compiler work:
- It creates the
matcherfunction. - It creates the
rewritersmodule. - It orders predicates into a matcher decision tree.
- It reuses shared constraint checks when possible.
- It lowers PDL rewrites into explicit create, erase, replace, and native rewrite calls.
- It emits loops such as
pdl_interp.foreachfor multi-root or user-walking patterns. - It emits switch operations when several patterns can share one dispatch.
Inside the rewrite engine, FrozenRewritePatternSet can lower PDL patterns with the convert-pdl-to-pdl-interp pass and then build a PDLByteCode object from the resulting module.
69.8 Conversions And Lowering Paths
69.8.1 Main Pass
The main conversion pass is:
-convert-pdl-to-pdl-interp
It is a module pass. It converts pdl.pattern operations into pdl_interp matcher and rewriter operations.
69.8.2 Normal Pipeline Position
The usual path is:
pdl.pattern
-> -convert-pdl-to-pdl-interp
-> pdl_interp.func @matcher plus module @rewriters
-> PDL bytecode inside the rewrite engine
-> PatternApplicator runs matches and rewrites
pdl_interp is usually not lowered to LLVM IR, SPIR-V, or another target dialect. It is consumed by MLIR’s rewrite infrastructure.
69.8.3 What Gets Lowered From PDL
Common PDL constructs lower as follows:
| PDL idea | Typical pdl_interp form |
|---|---|
| Match operation name | pdl_interp.check_operation_name or pdl_interp.switch_operation_name |
| Match operand count | pdl_interp.check_operand_count or pdl_interp.switch_operand_count |
| Match result count | pdl_interp.check_result_count or pdl_interp.switch_result_count |
| Match type | pdl_interp.check_type, pdl_interp.check_types, pdl_interp.switch_type, or pdl_interp.switch_types |
| Native constraint | pdl_interp.apply_constraint |
| Native rewrite | pdl_interp.apply_rewrite |
| PDL rewrite region | pdl_interp.create_operation, pdl_interp.erase, pdl_interp.replace, and helpers |
| Successful pattern | pdl_interp.record_match |
69.9 Example IR
69.9.1 A Small Matcher And Rewriter
This example matches a root operation named foo.op with zero operands. On success, it records a match that invokes @rewriters::@rewrite.
module {
pdl_interp.func @matcher(%root: !pdl.operation) {
pdl_interp.check_operation_name of %root is "foo.op" -> ^matched, ^failed
^failed:
pdl_interp.finalize
^matched:
pdl_interp.check_operand_count of %root is 0 -> ^record, ^failed
^record:
pdl_interp.record_match @rewriters::@rewrite(%root : !pdl.operation) :
benefit(1), loc([%root]), root("foo.op") -> ^failed
}
module @rewriters {
pdl_interp.func @rewrite(%root: !pdl.operation) {
pdl_interp.erase %root
pdl_interp.finalize
}
}
}
The key point is that the matcher does not erase %root directly. It records a match. The rewriter function performs the erase after the rewrite engine chooses that match.
69.9.2 Creating And Replacing Operations
module {
pdl_interp.func @rewriter(%input: !pdl.value,
%attribute: !pdl.attribute,
%type: !pdl.type) {
%created = pdl_interp.create_operation "foo.op"(%input : !pdl.value)
{"attr" = %attribute} -> (%type : !pdl.type)
%result = pdl_interp.get_result 0 of %created
pdl_interp.replace %created with (%result : !pdl.value)
pdl_interp.finalize
}
}
This shows how construction and replacement are expressed with PDL handles. In real generated code, the replaced operation is usually the matched root, not the newly created operation in the same snippet.
69.9.3 Iterating Over Users
module {
pdl_interp.func @walk_users(%value: !pdl.value) {
%users = pdl_interp.get_users of %value : !pdl.value
pdl_interp.foreach %user : !pdl.operation in %users {
%operand = pdl_interp.get_operand 0 of %user
pdl_interp.continue
} -> ^done
^done:
pdl_interp.finalize
}
}
foreach is useful for patterns that need to look away from a single root and inspect related operations.
69.9.4 Dispatching On Operation Name
module {
pdl_interp.func @switch_on_root(%root: !pdl.operation) {
pdl_interp.switch_operation_name of %root to ["foo.op", "bar.op"](^foo, ^bar) -> ^unknown
^foo:
pdl_interp.finalize
^bar:
pdl_interp.finalize
^unknown:
pdl_interp.finalize
}
}
This is the compact form of several operation-name checks sharing the same queried root.
69.10 Mental Model
Think of PDLInterp as a small instruction set for pattern matching:
- Start with a candidate operation.
- Query facts from that operation and nearby IR.
- Branch based on names, counts, types, attributes, equality, and native constraints.
- Record every successful pattern match.
- Later, invoke the recorded rewriter to create, erase, replace, or call native rewrite code.
That mental model explains why the dialect has many operations that look like basic C++ rewrite APIs. The dialect is not modeling the user’s program. It is modeling the pattern engine’s work.
69.11 Gotchas
pdl_interpis internal-facing. If you are just writing a compiler rewrite, start with PDL or C++ patterns.- Query operations can return null handles. A missing
is_not_nullcan make a matcher behave differently than expected. record_matchrecords a possible rewrite; it does not perform the rewrite.benefitbelongs to the match metadata and is used when choosing among matches.root("...")metadata helps connect a match to a root operation kind.- Native constraints and rewrites must be registered with the interpreter environment. The IR only stores their names and arguments.
pdl_interpfunctions are not normal application functions. They are interpreter functions over PDL handles.- The dialect depends on PDL types. Understanding
!pdl.operation,!pdl.value,!pdl.type,!pdl.attribute, and!pdl.range<...>is necessary before reading complex examples.
69.12 Source Map
Important source files in the LLVM tree:
mlir/include/mlir/Dialect/PDLInterp/IR/PDLInterpOps.tddefines the dialect and all operations.mlir/lib/Dialect/PDLInterp/IR/PDLInterp.cppimplements parsing, printing, verification, and helpers.mlir/include/mlir/Conversion/Passes.tddeclaresconvert-pdl-to-pdl-interp.mlir/lib/Conversion/PDLToPDLInterp/PDLToPDLInterp.cpplowers PDL patterns intopdl_interp.mlir/lib/Conversion/PDLToPDLInterp/Predicate*.{h,cpp}andRootOrdering.*support matcher planning and predicate ordering.mlir/lib/Rewrite/FrozenRewritePatternSet.cppshows how PDL patterns are lowered before bytecode construction.mlir/lib/Rewrite/ByteCode.cppconsumespdl_interpoperations and builds the bytecode interpreter program.mlir/test/Dialect/PDLInterp/ops.mlircontains parser and printer examples.mlir/test/Conversion/PDLToPDLInterp/contains examples of PDL lowering intopdl_interp.