| # PDLL - PDL Language | |
| This document details the PDL Language (PDLL), a custom frontend language for | |
| writing pattern rewrites targeting MLIR. | |
| Note: This document assumes a familiarity with MLIR concepts; more specifically | |
| the concepts detailed within the | |
| [MLIR Pattern Rewriting](PatternRewriter.md) and | |
| [Operation Definition Specification (ODS)](DefiningDialects/Operations.md) | |
| documentation. | |
| [TOC] | |
| ## Introduction | |
| Pattern matching is an extremely important component within MLIR, as it | |
| encompasses many different facets of the compiler. From canonicalization, to | |
| optimization, to conversion; every MLIR based compiler will heavily rely on the | |
| pattern matching infrastructure in some capacity. | |
| The PDL Language (PDLL) provides a declarative pattern language designed from | |
| the ground up for representing MLIR pattern rewrites. PDLL is designed to | |
| natively support writing matchers on all of MLIRs constructs via an intuitive | |
| interface that may be used for both ahead-of-time (AOT) and just-in-time (JIT) | |
| pattern compilation. | |
| ## Rationale | |
| This section provides details on various design decisions, their rationale, and | |
| alternatives considered when designing PDLL. Given the nature of software | |
| development, this section may include references to areas of the MLIR compiler | |
| that no longer exist. | |
| ### Why build a new language instead of improving TableGen DRR? | |
| Note: This section assumes familiarity with | |
| [TDRR](DeclarativeRewrites.md), please refer the | |
| relevant documentation before continuing. | |
| Tablegen DRR (TDRR), i.e. | |
| [Table-driven Declarative Rewrite Rules](DeclarativeRewrites.md), | |
| is a declarative DSL for defining MLIR pattern rewrites within the | |
| [TableGen](https://llvm.org/docs/TableGen/index.html) language. This | |
| infrastructure is currently the main way in which patterns may be defined | |
| declaratively within MLIR. TDRR utilizes TableGen's `dag` support to enable | |
| defining MLIR patterns that fit nicely within a DAG structure; in a similar way | |
| in which tablegen has been used to defined patterns for LLVM's backend | |
| infrastructure (SelectionDAG/Global Isel/etc.). Unfortunately however, the | |
| TableGen language is not as amenable to the structure of MLIR patterns as it has | |
| been for LLVM. | |
| The issues with TDRR largely stem from the use of TableGen as the host language | |
| for the DSL. These issues have risen from a mismatch in the structure of | |
| TableGen compared to the structure of MLIR, and from TableGen having different | |
| motivational goals than MLIR. A majority (or all depending on how stubborn you | |
| are) of the issues that we've come across with TDRR have been addressable in | |
| some form; the sticking point here is that the solutions to these problems have | |
| often been more "creative" than we'd like. This is a problem, and why we decided | |
| not to invest a larger effort into improving TDRR; users generally don't want | |
| "creative" APIs, they want something that is intuitive to read/write. | |
| To highlight some of these issues, below we will take a tour through some of the | |
| problems that have arisen, and how we "fixed" them. | |
| #### Multi-result operations | |
| MLIR natively supports a variable number of operation results. For the DAG based | |
| structure of TDRR, any form of multiple results (operations in this instance) | |
| creates a problem. This is because the DAG wants a single root node, and does | |
| not have nice facilities for indexing or naming the multiple results. Let's take | |
| a look at a quick example to see how this manifests: | |
| ```tablegen | |
| // Suppose we have a three result operation, defined as seen below. | |
| def ThreeResultOp : Op<"three_result_op"> { | |
| let arguments = (ins ...); | |
| let results = (outs | |
| AnyTensor:$output1, | |
| AnyTensor:$output2, | |
| AnyTensor:$output3 | |
| ); | |
| } | |
| // To bind the results of `ThreeResultOp` in a TDRR pattern, we bind all results | |
| // to a single name and use a special naming convention: `__N`, where `N` is the | |
| // N-th result. | |
| def : Pattern<(ThreeResultOp:$results ...), | |
| [(... $results__0), ..., (... $results__2), ...]>; | |
| ``` | |
| In TDRR, we "solved" the problem of accessing multiple results, but this isn't a | |
| very intuitive interface for users. Magical naming conventions obfuscate the | |
| code and can easily introduce bugs and other errors. There are various things | |
| that we could try to improve this situation, but there is a fundamental limit to | |
| what we can do given the limits of the TableGen dag structure. In PDLL, however, | |
| we have the freedom and flexibility to provide a proper interface into | |
| operations, regardless of their structure: | |
| ```pdll | |
| // Import our definition of `ThreeResultOp`. | |
| #include "ops.td" | |
| Pattern { | |
| ... | |
| // In PDLL, we can directly reference the results of an operation variable. | |
| // This provides a closer mental model to what the user expects. | |
| let threeResultOp = op<my_dialect.three_result_op>; | |
| let userOp = op<my_dialect.user_op>(threeResultOp.output1, ..., threeResultOp.output3); | |
| ... | |
| } | |
| ``` | |
| #### Constraints | |
| In TDRR, the match dag defines the general structure of the input IR to match. | |
| Any non-structural/non-type constraints on the input are generally relegated to | |
| a list of constraints specified after the rewrite dag. For very simple patterns | |
| this may suffice, but with larger patterns it becomes quite problematic as it | |
| separates the constraint from the entity it constrains and negatively impacts | |
| the readability of the pattern. As an example, let's look at a simple pattern | |
| that adds additional constraints to its inputs: | |
| ```tablegen | |
| // Suppose we have a two result operation, defined as seen below. | |
| def TwoResultOp : Op<"two_result_op"> { | |
| let arguments = (ins ...); | |
| let results = (outs | |
| AnyTensor:$output1, | |
| AnyTensor:$output2 | |
| ); | |
| } | |
| // A simple constraint to check if a value is use_empty. | |
| def HasNoUseOf: Constraint<CPred<"$_self.use_empty()">, "has no use">; | |
| // Check if two values have a ShapedType with the same element type. | |
| def HasSameElementType : Constraint< | |
| CPred<"cast<ShapedType>($0.getType()).getElementType() == " | |
| "cast<ShapedType>($1.getType()).getElementType()">, | |
| "values have same element type">; | |
| def : Pattern<(TwoResultOp:$results $input), | |
| [(...), (...)], | |
| [(HasNoUseOf:$results__1), | |
| (HasSameElementType $results__0, $input)]>; | |
| ``` | |
| Above, when observing the constraints we need to search through the input dag | |
| for the inputs (also keeping in mind the magic naming convention for multiple | |
| results). For this simple pattern it may be just a few lines above, but complex | |
| patterns often grow to 10s of lines long. In PDLL, these constraints can be | |
| applied directly on or next to the entities they apply to: | |
| ```pdll | |
| // The same constraints that we defined above: | |
| Constraint HasNoUseOf(value: Value) [{ | |
| return success(value.use_empty()); | |
| }]; | |
| Constraint HasSameElementType(value1: Value, value2: Value) [{ | |
| return success(cast<ShapedType>(value1.getType()).getElementType() == | |
| cast<ShapedType>(value2.getType()).getElementType()); | |
| }]; | |
| Pattern { | |
| // In PDLL, we can apply the constraint as early (or as late) as we want. This | |
| // enables better structuring of the matcher code, and improves the | |
| // readability/maintainability of the pattern. | |
| let op = op<my_dialect.two_result_op>(input: Value); | |
| HasNoUseOf(op.output2); | |
| HasSameElementType(input, op.output2); | |
| // ... | |
| } | |
| ``` | |
| #### Replacing Multiple Operations | |
| Often times a pattern will transform N number of input operations into N number | |
| of result operations. In PDLL, replacing multiple operations is as simple as | |
| adding two [`replace` statements](#replace-statement). In TDRR, the situation is | |
| a bit more nuanced. Given the single root structure of the TableGen dag, | |
| replacing a non-root operation is not nicely supported. It currently isn't | |
| natively possible, and instead requires using multiple patterns. We could | |
| potentially add another special rewrite directive, or extend `replaceWithValue`, | |
| but this simply highlights how even a basic IR transformation is muddled by the | |
| complexity of the host language. | |
| ### Why not build a DSL in "X"? | |
| Yes! Well yes and no. To understand why, we have to consider what types of users | |
| we are trying to serve and what constraints we enforce upon them. The goal of | |
| PDLL is to provide a default and effective pattern language for MLIR that all | |
| users of MLIR can interact with immediately, regardless of their host | |
| environment. This language is available with no extra dependencies and comes | |
| "free" along with MLIR. If we were to use an existing host language to build our | |
| new DSL, we would need to make compromises along with it depending on the | |
| language. For some, there are questions of how to enforce matching environments | |
| (python2 or python3?, which version?), performance considerations, integration, | |
| etc. As an LLVM project, this could also mean enforcing a new language | |
| dependency on the users of MLIR (many of which may not want/need such a | |
| dependency otherwise). Another issue that comes along with any DSL that is | |
| embeded in another language: mitigating the user impedance mismatch between what | |
| the user expects from the host language and what our "backend" supports. For | |
| example, the PDL IR abstraction only contains limited support for control flow. | |
| If we were to build a DSL in python, we would need to ensure that complex | |
| control flow is either handled completely or effectively errors out. Even with | |
| ideal error handling, not having the expected features available creates user | |
| frustration. In addition to the environment constraints, there is also the issue | |
| of language tooling. With PDLL we intend to build a very robust and modern | |
| toolset that is designed to cater the needs of pattern developers, including | |
| code completion, signature help, and many more features that are specific to the | |
| problem we are solving. Integrating custom language tooling into existing | |
| languages can be difficult, and in some cases impossible (as our DSL would | |
| merely be a small subset of the existing language). | |
| These various points have led us to the initial conclusion that the most | |
| effective tool we can provide for our users is a custom tool designed for the | |
| problem at hand. With all of that being said, we understand that not all users | |
| have the same constraints that we have placed upon ourselves. We absolutely | |
| encourage and support the existence of various PDL frontends defined in | |
| different languages. This is one of the original motivating factors around | |
| building the PDL IR abstraction in the first place; to enable innovation and | |
| flexibility for our users (and in turn their users). For some, such as those in | |
| research and the Machine Learning space, they may already have a certain | |
| language (such as Python) heavily integrated into their workflow. For these | |
| users, a PDL DSL in their language may be ideal and we will remain committed to | |
| supporting and endorsing that from an infrastructure point-of-view. | |
| ## Language Specification | |
| Note: PDLL is still under active development, and the designs discussed below | |
| are not necessarily final and may be subject to change. | |
| The design of PDLL is heavily influenced and centered around the | |
| [PDL IR abstraction](https://mlir.llvm.org/docs/Dialects/PDLOps/), which in turn | |
| is designed as an abstract model of the core MLIR structures. This leads to a | |
| design and structure that feels very similar to if you were directly writing the | |
| IR you want to match. | |
| ### Includes | |
| PDLL supports an `include` directive to import content defined within other | |
| source files. There are two types of files that may be included: `.pdll` and | |
| `.td` files. | |
| #### `.pdll` includes | |
| When including a `.pdll` file, the contents of that file are copied directly into | |
| the current file being processed. This means that any patterns, constraints, | |
| rewrites, etc., defined within that file are processed along with those within | |
| the current file. | |
| #### `.td` includes | |
| When including a `.td` file, PDLL will automatically import any pertinent | |
| [ODS](DefiningDialects/Operations.md) information within that file. | |
| This includes any defined operations, constraints, interfaces, and more, making | |
| them implicitly accessible within PDLL. This is important, as ODS information | |
| allows for certain PDLL constructs, such as the | |
| [`operation` expression](#operation), to become much more powerful. | |
| ### Patterns | |
| In any pattern descriptor language, pattern definition is at the core. In PDLL, | |
| patterns start with `Pattern` optionally followed by a name and a set of pattern | |
| metadata, and finally terminated by a pattern body. A few simple examples are | |
| shown below: | |
| ```pdll | |
| // Here we have defined an anonymous pattern: | |
| Pattern { | |
| // Pattern bodies are separated into two components: | |
| // * Match Section | |
| // - Describes the input IR. | |
| let root = op<toy.reshape>(op<toy.reshape>(arg: Value)); | |
| // * Rewrite Section | |
| // - Describes how to transform the IR. | |
| // - Last statement starts the rewrite. | |
| replace root with op<toy.reshape>(arg); | |
| } | |
| // Here we have defined a pattern named `ReshapeReshapeOptPattern` with a | |
| // benefit of 10: | |
| Pattern ReshapeReshapeOptPattern with benefit(10) { | |
| replace op<toy.reshape>(op<toy.reshape>(arg: Value)) | |
| with op<toy.reshape>(arg); | |
| } | |
| ``` | |
| After the definition of the pattern metadata, we specify the pattern body. The | |
| structure of a pattern body is comprised of two main sections, the `match` | |
| section and the `rewrite` section. The `match` section of a pattern describes | |
| the expected input IR, whereas the `rewrite` section describes how to transform | |
| that IR. This distinction is an important one to make, as PDLL handles certain | |
| variables and expressions differently within the different sections. When | |
| relevant in each of the sections below, we shall explicitly call out any | |
| behavioral differences. | |
| The general layout of the `match` and `rewrite` section is as follows: the | |
| *last* statement of the pattern body is required to be a | |
| [`operation rewrite statement`](#operation-rewrite-statements), and denotes the | |
| `rewrite` section; every statement before denotes the `match` section. | |
| #### Pattern metadata | |
| Rewrite patterns in MLIR have a set of metadata that allow for controlling | |
| certain behaviors, and providing information to the rewrite driver applying the | |
| pattern. In PDLL, a pattern can provide a non-default value for this metadata | |
| after the pattern name. Below, examples are shown for the different types of | |
| metadata supported: | |
| ##### Benefit | |
| The benefit of a Pattern is an integer value that represents the "benefit" of | |
| matching that pattern. It is used by pattern drivers to determine the relative | |
| priorities of patterns during application; a pattern with a higher benefit is | |
| generally applied before one with a lower benefit. | |
| In PDLL, a pattern has a default benefit set to the number of input operations, | |
| i.e. the number of distinct `Op` expressions/variables, in the match section. This | |
| rule is driven by an observation that larger matches are more beneficial than smaller | |
| ones, and if a smaller one is applied first the larger one may not apply anymore. | |
| Patterns can override this behavior by specifying the benefit in the metadata section | |
| of the pattern: | |
| ```pdll | |
| // Here we specify that this pattern has a benefit of `10`, overriding the | |
| // default behavior. | |
| Pattern with benefit(10) { | |
| ... | |
| } | |
| ``` | |
| ##### Bounded Rewrite Recursion | |
| During pattern application, there are situations in which a pattern may be | |
| applicable to the result of a previous application of that same pattern. If the | |
| pattern does not properly handle this recusive application, the pattern driver | |
| could become stuck in an infinite loop of application. To prevent this, patterns | |
| by-default are assumed to not have proper recursive bounding and will not be | |
| recursively applied. A pattern can signal that it does have proper handling for | |
| recursion by specifying the `recusion` flag in the pattern metadata section: | |
| ```pdll | |
| // Here we signal that this pattern properly bounds recursive application. | |
| Pattern with recusion { | |
| ... | |
| } | |
| ``` | |
| #### Single Line "Lambda" Body | |
| Patterns generally define their body using a compound block of statements, as | |
| shown below: | |
| ```pdll | |
| Pattern { | |
| replace op<my_dialect.foo>(operands: ValueRange) with operands; | |
| } | |
| ``` | |
| Patterns also support a lambda-like syntax for specifying simple single line | |
| bodies. The lambda body of a Pattern expects a single | |
| [operation rewrite statement](#operation-rewrite-statements): | |
| ```pdll | |
| Pattern => replace op<my_dialect.foo>(operands: ValueRange) with operands; | |
| ``` | |
| ### Variables | |
| Variables in PDLL represent specific instances of IR entities, such as `Value`s, | |
| `Operation`s, `Type`s, etc. Consider the simple pattern below: | |
| ```pdll | |
| Pattern { | |
| let value: Value; | |
| let root = op<mydialect.foo>(value); | |
| replace root with value; | |
| } | |
| ``` | |
| In this pattern we define two variables, `value` and `root`, using the `let` | |
| statement. The `let` statement allows for defining variables and constraining | |
| them. Every variable in PDLL is of a certain type, which defines the type of IR | |
| entity the variable represents. The type of a variable may be determined via | |
| either a constraint, or an initializer expression. | |
| #### Variable "Binding" | |
| In addition to having a type, variables must also be "bound", either via an initializer | |
| expression or to a non-native constraint or rewrite use within the `match` section of the | |
| pattern. "Binding" a variable contextually identifies that variable within either the | |
| input (i.e. `match` section) or output (i.e. `rewrite` section) IR. In the `match` section, | |
| this allows for building the match tree from the pattern's root operation, which must be | |
| "bound" to the [operation rewrite statement](#operation-rewrite-statements) that denotes the | |
| `rewrite` section of the pattern. All non-root variables within the `match` | |
| section must be bound in some way to the "root" operation. To help illustrate | |
| the concept, let's take a look at a quick example. Consider the `.mlir` snippet | |
| below: | |
| ```mlir | |
| func.func @baz(%arg: i32) { | |
| %result = my_dialect.foo %arg, %arg -> i32 | |
| } | |
| ``` | |
| Say that we want to write a pattern that matches `my_dialect.foo` and replaces | |
| it with its unique input argument. A naive way to write this pattern in PDLL is | |
| shown below: | |
| ```pdll | |
| Pattern { | |
| // ** match section ** // | |
| let arg: Value; | |
| let root = op<my_dialect.foo>(arg, arg); | |
| // ** rewrite section ** // | |
| replace root with arg; | |
| } | |
| ``` | |
| In the above pattern, the `arg` variable is "bound" to the first and second operands | |
| of the `root` operation. Every use of `arg` is constrained to be the same `Value`, i.e. | |
| the first and second operands of `root` will be constrained to refer to the same input | |
| Value. The same is true for the `root` operation, it is bound to the "root" operation of the | |
| pattern as it is used in input of the top-level [`replace` statement](#replace-statement) | |
| of the `rewrite` section of the pattern. Writing this pattern using the C++ API, the concept | |
| of "binding" becomes more clear: | |
| ```c++ | |
| struct Pattern : public OpRewritePattern<my_dialect::FooOp> { | |
| LogicalResult matchAndRewrite(my_dialect::FooOp root, PatternRewriter &rewriter) { | |
| Value arg = root->getOperand(0); | |
| if (arg != root->getOperand(1)) | |
| return failure(); | |
| rewriter.replaceOp(root, arg); | |
| return success(); | |
| } | |
| }; | |
| ``` | |
| If a variable is not "bound" properly, PDLL won't be able to identify what value | |
| it would correspond to in the IR. As a final example, let's consider a variable | |
| that hasn't been bound: | |
| ```pdll | |
| Pattern { | |
| // ** match section ** // | |
| let arg: Value; | |
| let root = op<my_dialect.foo> | |
| // ** rewrite section ** // | |
| replace root with arg; | |
| } | |
| ``` | |
| If we were to write this exact pattern in C++, we would end up with: | |
| ```c++ | |
| struct Pattern : public OpRewritePattern<my_dialect::FooOp> { | |
| LogicalResult matchAndRewrite(my_dialect::FooOp root, PatternRewriter &rewriter) { | |
| // `arg` was never bound, so we don't know what input Value it was meant to | |
| // correspond to. | |
| Value arg; | |
| rewriter.replaceOp(root, arg); | |
| return success(); | |
| } | |
| }; | |
| ``` | |
| #### Variable Constraints | |
| ```pdll | |
| // This statement defines a variable `value` that is constrained to be a `Value`. | |
| let value: Value; | |
| // This statement defines a variable `value` that is constrained to be a `Value` | |
| // *and* constrained to have a single use. | |
| let value: [Value, HasOneUse]; | |
| ``` | |
| Any number of single entity constraints may be attached directly to a variable | |
| upon declaration. Within the `matcher` section, these constraints may add | |
| additional checks on the input IR. Within the `rewriter` section, constraints | |
| are *only* used to define the type of the variable. There are a number of | |
| builtin constraints that correlate to the core MLIR constructs: `Attr`, `Op`, | |
| `Type`, `TypeRange`, `Value`, `ValueRange`. Along with these, users may define | |
| custom constraints that are implemented within PDLL, or natively (i.e. outside | |
| of PDLL). See the general [Constraints](#constraints) section for more detailed | |
| information. | |
| #### Inline Variable Definition | |
| Along with the `let` statement, variables may also be defined inline by | |
| specifying the constraint list along with the desired variable name in the first | |
| place that the variable would be used. After definition, the variable is visible | |
| from all points forward. See below for an example: | |
| ```pdll | |
| // `value` is used as an operand to the operation `root`: | |
| let value: Value; | |
| let root = op<my_dialect.foo>(value); | |
| replace root with value; | |
| // `value` could also be defined "inline": | |
| let root = op<my_dialect.foo>(value: Value); | |
| replace root with value; | |
| ``` | |
| Note that the point of definition of an inline variable is the point of reference, | |
| meaning that an inline variable can be used immediately in the same parent | |
| expression within which it was defined: | |
| ```pdll | |
| let root = op<my_dialect.foo>(value: Value, _: Value, value); | |
| replace root with value; | |
| ``` | |
| ##### Wildcard Variable Definition | |
| Often times when defining a variable inline, the variable isn't intended to be | |
| used anywhere else in the pattern. For example, this may happen if you want to | |
| attach constraints to a variable but have no other use for it. In these | |
| situations, the "wildcard" variable can be used to remove the need to provide a | |
| name, as "wildcard" variables are not visible outside of the point of | |
| definition. An example is shown below: | |
| ```pdll | |
| Pattern { | |
| let root = op<my_dialect.foo>(arg: Value, _: Value, _: [Value, I64Value], arg); | |
| replace root with arg; | |
| } | |
| ``` | |
| In the above example, the second operand isn't needed for the pattern but we | |
| need to provide it to signal that a second operand does exist (we just don't | |
| care what it is in this pattern). | |
| ### Operation Expression | |
| An operation expression in PDLL represents an MLIR operation. In the `match` | |
| section of the pattern, this expression models one of the input operations to | |
| the pattern. In the `rewrite` section of the pattern, this expression models one | |
| of the operations to create. The general structure of the operation expression | |
| is very similar to that of the "generic form" of textual MLIR assembly: | |
| ```pdll | |
| let root = op<my_dialect.foo>(operands: ValueRange) {attr = attr: Attr} -> (resultTypes: TypeRange); | |
| ``` | |
| Let's walk through each of the different components of the expression: | |
| #### Operation name | |
| The operation name signifies which type of MLIR Op this operation corresponds | |
| to. In the `match` section of the pattern, the name may be elided. This would | |
| cause this pattern to match *any* operation type that satifies the rest of the | |
| constraints of the operation. In the `rewrite` section, the name is required. | |
| ```pdll | |
| // `root` corresponds to an instance of a `my_dialect.foo` operation. | |
| let root = op<my_dialect.foo>; | |
| // `root` could be an instance of any operation type. | |
| let root = op<>; | |
| ``` | |
| #### Operands | |
| The operands section corresponds to the operands of the operation. This section | |
| of an operation expression may be elided, which within a `match` section means | |
| that the operands are not constrained in any way. If elided within a `rewrite` | |
| section, the operation is treated as having no operands. When present, the | |
| operands of an operation expression are interpreted in the following ways: | |
| 1) A single instance of type `ValueRange`: | |
| In this case, the single range is treated as all of the operands of the | |
| operation: | |
| ```pdll | |
| // Define an instance with single range of operands. | |
| let root = op<my_dialect.foo>(allOperands: ValueRange); | |
| ``` | |
| 2) A variadic number of either `Value` or `ValueRange`: | |
| In this case, the inputs are expected to correspond with the operand groups as | |
| defined on the operation in ODS. | |
| Given the following operation definition in ODS: | |
| ```tablegen | |
| def MyIndirectCallOp { | |
| let arguments = (ins FunctionType:$call, Variadic<AnyType>:$args); | |
| } | |
| ``` | |
| We can match the operands as so: | |
| ```pdll | |
| let root = op<my_dialect.indirect_call>(call: Value, args: ValueRange); | |
| ``` | |
| #### Results | |
| The results section corresponds to the result types of the operation. This section | |
| of an operation expression may be elided, which within a `match` section means | |
| that the result types are not constrained in any way. If elided within a `rewrite` | |
| section, the results of the operation are [inferred](#inferred-results). When present, | |
| the result types of an operation expression are interpreted in the following ways: | |
| 1) A single instance of type `TypeRange`: | |
| In this case, the single range is treated as all of the result types of the | |
| operation: | |
| ```pdll | |
| // Define an instance with single range of types. | |
| let root = op<my_dialect.foo> -> (allResultTypes: TypeRange); | |
| ``` | |
| 2) A variadic number of either `Type` or `TypeRange`: | |
| In this case, the inputs are expected to correspond with the result groups as | |
| defined on the operation in ODS. | |
| Given the following operation definition in ODS: | |
| ```tablegen | |
| def MyOp { | |
| let results = (outs SomeType:$result, Variadic<SomeType>:$otherResults); | |
| } | |
| ``` | |
| We can match the result types as so: | |
| ```pdll | |
| let root = op<my_dialect.op> -> (result: Type, otherResults: TypeRange); | |
| ``` | |
| #### Inferred Results | |
| Within the `rewrite` section of a pattern, the result types of an | |
| operation are inferred if they are elided or otherwise not | |
| previously bound. The ["variable binding"](#variable-binding) section above | |
| discusses the concept of "binding" in more detail. Below are various examples | |
| that build upon this to help showcase how a result type may be "bound": | |
| * Binding to a [constant](#type-expression): | |
| ```pdll | |
| op<my_dialect.op> -> (type<"i32">); | |
| ``` | |
| * Binding to types within the `match` section: | |
| ```pdll | |
| Pattern { | |
| replace op<dialect.inputOp> -> (resultTypes: TypeRange) | |
| with op<dialect.outputOp> -> (resultTypes); | |
| } | |
| ``` | |
| * Binding to previously inferred types: | |
| ```pdll | |
| Pattern { | |
| rewrite root: Op with { | |
| // `resultTypes` here is *not* yet bound, and will be inferred when | |
| // creating `dialect.op`. Any uses of `resultTypes` after this expression, | |
| // will use the types inferred when creating this operation. | |
| op<dialect.op> -> (resultTypes: TypeRange); | |
| // `resultTypes` here is bound to the types inferred when creating `dialect.op`. | |
| op<dialect.bar> -> (resultTypes); | |
| }; | |
| } | |
| ``` | |
| * Binding to a [`Native Rewrite`](#native-rewriters) method result: | |
| ```pdll | |
| Rewrite BuildTypes() -> TypeRange; | |
| Pattern { | |
| rewrite root: Op with { | |
| op<dialect.op> -> (BuildTypes()); | |
| }; | |
| } | |
| ``` | |
| Below are the set of contexts in which result type inferrence is supported: | |
| ##### Inferred Results of Replacement Operation | |
| Replacements have the invariant that the types of the replacement values must | |
| match the result types of the input operation. This means that when replacing | |
| one operation with another, the result types of the replacement operation may | |
| be inferred from the result types of the operation being replaced. For example, | |
| consider the following pattern: | |
| ```pdll | |
| Pattern => replace op<dialect.inputOp> with op<dialect.outputOp>; | |
| ``` | |
| This pattern could be written in a more explicit way as: | |
| ```pdll | |
| Pattern { | |
| replace op<dialect.inputOp> -> (resultTypes: TypeRange) | |
| with op<dialect.outputOp> -> (resultTypes); | |
| } | |
| ``` | |
| ##### Inferred Results with InferTypeOpInterface | |
| `InferTypeOpInterface` is an interface that enables operations to infer its result | |
| types from its input attributes, operands, regions, etc. When the result types of | |
| an operation cannot be inferred from any other context, this interface is invoked | |
| to infer the result types of the operation. | |
| #### Attributes | |
| The attributes section of the operation expression corresponds to the attribute | |
| dictionary of the operation. This section of an operation expression may be | |
| elided, in which case the attributes are not constrained in any way. The | |
| composition of this component maps exactly to how attribute dictionaries are | |
| structured in the MLIR textual assembly format: | |
| ```pdll | |
| let root = op<my_dialect.foo> {attr1 = attrValue: Attr, attr2 = attrValue2: Attr}; | |
| ``` | |
| Within the `{}` attribute entries are specified by an identifier or string name, | |
| corresponding to the attribute name, followed by an assignment to the attribute | |
| value. If the attribute value is elided, the value of the attribute is | |
| implicitly defined as a | |
| [`UnitAttr`](https://mlir.llvm.org/docs/Dialects/Builtin/#unitattr). | |
| ```pdll | |
| let unitConstant = op<my_dialect.constant> {value}; | |
| ``` | |
| ##### Accessing Operation Results | |
| In multi-operation patterns, the result of one operation often feeds as an input | |
| into another. The result groups of an operation may be accessed by name or by | |
| index via the `.` operator: | |
| Note: Remember to import the definition of your operation via | |
| [include](#`.td`_includes) to ensure it is visible to PDLL. | |
| Given the following operation definition in ODS: | |
| ```tablegen | |
| def MyResultOp { | |
| let results = (outs SomeType:$result); | |
| } | |
| def MyInputOp { | |
| let arguments = (ins SomeType:$input, SomeType:$input); | |
| } | |
| ``` | |
| We can write a pattern where `MyResultOp` feeds into `MyInputOp` as so: | |
| ```pdll | |
| // In this example, we use both `result`(the name) and `0`(the index) to refer to | |
| // the first result group of `resultOp`. | |
| // Note: If we elide the result types section within the match section, it means | |
| // they aren't constrained, not that the operation has no results. | |
| let resultOp = op<my_dialect.result_op>; | |
| let inputOp = op<my_dialect.input_op>(resultOp.result, resultOp.0); | |
| ``` | |
| Along with result name access, variables of `Op` type may implicitly convert to | |
| `Value` or `ValueRange`. If these variables are registered (has ODS entry), they | |
| are converted to `Value` when they are known to only have one result, otherwise | |
| they will be converted to `ValueRange`: | |
| ```pdll | |
| // `resultOp` may also convert implicitly to a Value for use in `inputOp`: | |
| let resultOp = op<my_dialect.result_op>; | |
| let inputOp = op<my_dialect.input_op>(resultOp); | |
| // We could also inline `resultOp` directly: | |
| let inputOp = op<my_dialect.input_op>(op<my_dialect.result_op>); | |
| ``` | |
| #### Unregistered Operations | |
| A variable of unregistered op is still available for numeric result indexing. | |
| Given that we don't have knowledge of its result groups, numeric indexing | |
| returns a Value corresponding to the individual result at the given index. | |
| ```pdll | |
| // Use the index `0` to refer to the first result value of the unregistered op. | |
| let inputOp = op<my_dialect.input_op>(op<my_dialect.unregistered_op>.0); | |
| ``` | |
| ### Attribute Expression | |
| An attribute expression represents a literal MLIR attribute. It allows for | |
| statically specifying an MLIR attribute to use, by specifying the textual form | |
| of that attribute. | |
| ```pdll | |
| let trueConstant = op<arith.constant> {value = attr<"true">}; | |
| let applyResult = op<affine.apply>(args: ValueRange) {map = attr<"affine_map<(d0, d1) -> (d1 - 3)>">} | |
| ``` | |
| ### Type Expression | |
| A type expression represents a literal MLIR type. It allows for statically | |
| specifying an MLIR type to use, by specifying the textual form of that type. | |
| ```pdll | |
| let i32Constant = op<arith.constant> -> (type<"i32">); | |
| ``` | |
| ### Tuples | |
| PDLL provides native support for tuples, which are used to group multiple | |
| elements into a single compound value. The values in a tuple can be of any type, | |
| and do not need to be of the same type. There is also no limit to the number of | |
| elements held by a tuple. The elements of a tuple can be accessed by index: | |
| ```pdll | |
| let tupleValue = (op<my_dialect.foo>, attr<"10 : i32">, type<"i32">); | |
| let opValue = tupleValue.0; | |
| let attrValue = tupleValue.1; | |
| let typeValue = tupleValue.2; | |
| ``` | |
| You can also name the elements of a tuple and use those names to refer to the | |
| values of the individual elements. An element name consists of an identifier | |
| followed immediately by an equal (=). | |
| ```pdll | |
| let tupleValue = ( | |
| opValue = op<my_dialect.foo>, | |
| attr<"10 : i32">, | |
| typeValue = type<"i32"> | |
| ); | |
| let opValue = tupleValue.opValue; | |
| let attrValue = tupleValue.1; | |
| let typeValue = tupleValue.typeValue; | |
| ``` | |
| Tuples are used to represent multiple results from a | |
| [constraint](#constraints-with-multiple-results) or | |
| [rewrite](#rewrites-with-multiple-results). | |
| ### Constraints | |
| Constraints provide the ability to inject additional checks on the input IR | |
| within the `match` section of a pattern. Constraints can be applied anywhere | |
| within the `match` section, and depending on the type can either be applied via | |
| the constraint list of a [variable](#variables) or via the call operator (e.g. | |
| `MyConstraint(...)`). There are three main categories of constraints: | |
| #### Core Constraints | |
| PDLL defines a number of core constraints that constrain the type of the IR | |
| entity. These constraints can only be applied via the | |
| [constraint list](#variable-constraints) of a variable. | |
| * `Attr` (`<` type `>`)? | |
| A single entity constraint that corresponds to an `mlir::Attribute`. This | |
| constraint optionally takes a type component that constrains the result type of | |
| the attribute. | |
| ```pdll | |
| // Define a simple variable using the `Attr` constraint. | |
| let attr: Attr; | |
| let constant = op<arith.constant> {value = attr}; | |
| // Define a simple variable using the `Attr` constraint, that has its type | |
| // constrained as well. | |
| let attrType: Type; | |
| let attr: Attr<attrType>; | |
| let constant = op<arith.constant> {value = attr}; | |
| ``` | |
| * `Op` (`<` op-name `>`)? | |
| A single entity constraint that corresponds to an `mlir::Operation *`. | |
| ```pdll | |
| // Match only when the input is from another operation. | |
| let inputOp: Op; | |
| let root = op<my_dialect.foo>(inputOp); | |
| // Match only when the input is from another `my_dialect.foo` operation. | |
| let inputOp: Op<my_dialect.foo>; | |
| let root = op<my_dialect.foo>(inputOp); | |
| ``` | |
| * `Type` | |
| A single entity constraint that corresponds to an `mlir::Type`. | |
| ```pdll | |
| // Define a simple variable using the `Type` constraint. | |
| let resultType: Type; | |
| let root = op<my_dialect.foo> -> (resultType); | |
| ``` | |
| * `TypeRange` | |
| A single entity constraint that corresponds to a `mlir::TypeRange`. | |
| ```pdll | |
| // Define a simple variable using the `TypeRange` constraint. | |
| let resultTypes: TypeRange; | |
| let root = op<my_dialect.foo> -> (resultTypes); | |
| ``` | |
| * `Value` (`<` type-expr `>`)? | |
| A single entity constraint that corresponds to an `mlir::Value`. This constraint | |
| optionally takes a type component that constrains the result type of the value. | |
| ```pdll | |
| // Define a simple variable using the `Value` constraint. | |
| let value: Value; | |
| let root = op<my_dialect.foo>(value); | |
| // Define a variable using the `Value` constraint, that has its type constrained | |
| // to be same as the result type of the `root` op. | |
| let valueType: Type; | |
| let input: Value<valueType>; | |
| let root = op<my_dialect.foo>(input) -> (valueType); | |
| ``` | |
| * `ValueRange` (`<` type-expr `>`)? | |
| A single entity constraint that corresponds to a `mlir::ValueRange`. This | |
| constraint optionally takes a type component that constrains the result types of | |
| the value range. | |
| ```pdll | |
| // Define a simple variable using the `ValueRange` constraint. | |
| let inputs: ValueRange; | |
| let root = op<my_dialect.foo>(inputs); | |
| // Define a variable using the `ValueRange` constraint, that has its types | |
| // constrained to be same as the result types of the `root` op. | |
| let valueTypes: TypeRange; | |
| let inputs: ValueRange<valueTypes>; | |
| let root = op<my_dialect.foo>(inputs) -> (valueTypes); | |
| ``` | |
| #### Defining Constraints in PDLL | |
| Aside from the core constraints, additional constraints can also be defined | |
| within PDLL. This allows for building matcher fragments that can be composed | |
| across many different patterns. A constraint in PDLL is defined similarly to a | |
| function in traditional programming languages; it contains a name, a set of | |
| input arguments, a set of result types, and a body. Results of a constraint are | |
| returned via a `return` statement. A few examples are shown below: | |
| ```pdll | |
| /// A constraint that takes an input and constrains the use to an operation of | |
| /// a given type. | |
| Constraint UsedByFooOp(value: Value) { | |
| op<my_dialect.foo>(value); | |
| } | |
| /// A constraint that returns a result of an existing operation. | |
| Constraint ExtractResult(op: Op<my_dialect.foo>) -> Value { | |
| return op.result; | |
| } | |
| Pattern { | |
| let value = ExtractResult(op<my_dialect.foo>); | |
| UsedByFooOp(value); | |
| } | |
| ``` | |
| ##### Constraints with multiple results | |
| Constraints can return multiple results by returning a tuple of values. When | |
| returning multiple results, each result can also be assigned a name to use when | |
| indexing that tuple element. Tuple elements can be referenced by their index | |
| number, or by name if they were assigned one. | |
| ```pdll | |
| // A constraint that returns multiple results, with some of the results assigned | |
| // a more readable name. | |
| Constraint ExtractMultipleResults(op: Op<my_dialect.foo>) -> (Value, result1: Value) { | |
| return (op.result1, op.result2); | |
| } | |
| Pattern { | |
| // Return a tuple of values. | |
| let result = ExtractMultipleResults(op: op<my_dialect.foo>); | |
| // Index the tuple elements by index, or by name. | |
| replace op<my_dialect.foo> with (result.0, result.1, result.result1); | |
| } | |
| ``` | |
| ##### Constraint result type inference | |
| In addition to explicitly specifying the results of the constraint via the | |
| constraint signature, PDLL defined constraints also support inferring the result | |
| type from the return statement. Result type inference is active whenever the | |
| constraint is defined with no result constraints: | |
| ```pdll | |
| // This constraint returns a derived operation. | |
| Constraint ReturnSelf(op: Op<my_dialect.foo>) { | |
| return op; | |
| } | |
| // This constraint returns a tuple of two Values. | |
| Constraint ExtractMultipleResults(op: Op<my_dialect.foo>) { | |
| return (result1 = op.result1, result2 = op.result2); | |
| } | |
| Pattern { | |
| let values = ExtractMultipleResults(op<my_dialect.foo>); | |
| replace op<my_dialect.foo> with (values.result1, values.result2); | |
| } | |
| ``` | |
| ##### Single Line "Lambda" Body | |
| Constraints generally define their body using a compound block of statements, as | |
| shown below: | |
| ```pdll | |
| Constraint ReturnSelf(op: Op<my_dialect.foo>) { | |
| return op; | |
| } | |
| Constraint ExtractMultipleResults(op: Op<my_dialect.foo>) { | |
| return (result1 = op.result1, result2 = op.result2); | |
| } | |
| ``` | |
| Constraints also support a lambda-like syntax for specifying simple single line | |
| bodies. The lambda body of a Constraint expects a single expression, which is | |
| implicitly returned: | |
| ```pdll | |
| Constraint ReturnSelf(op: Op<my_dialect.foo>) => op; | |
| Constraint ExtractMultipleResults(op: Op<my_dialect.foo>) | |
| => (result1 = op.result1, result2 = op.result2); | |
| ``` | |
| #### Native Constraints | |
| Constraints may also be defined outside of PDLL, and registered natively within | |
| the C++ API. | |
| ##### Importing existing Native Constraints | |
| Constraints defined externally can be imported into PDLL by specifying a | |
| constraint "declaration". This is similar to the PDLL form of defining a | |
| constraint but omits the body. Importing the declaration in this form allows for | |
| PDLL to statically know the expected input and output types. | |
| ```pdll | |
| // Import a single entity value native constraint that checks if the value has a | |
| // single use. This constraint must be registered by the consumer of the | |
| // compiled PDL. | |
| Constraint HasOneUse(value: Value); | |
| // Import a multi-entity type constraint that checks if two values have the same | |
| // element type. | |
| Constraint HasSameElementType(value1: Value, value2: Value); | |
| Pattern { | |
| // A single entity constraint can be applied via the variable argument list. | |
| let value: HasOneUse; | |
| // Otherwise, constraints can be applied via the call operator: | |
| let value: Value = ...; | |
| let value2: Value = ...; | |
| HasOneUse(value); | |
| HasSameElementType(value, value2); | |
| } | |
| ``` | |
| External constraints are those registered explicitly with the `RewritePatternSet` via | |
| the C++ PDL API. For example, the constraints above may be registered as: | |
| ```c++ | |
| static LogicalResult hasOneUseImpl(PatternRewriter &rewriter, Value value) { | |
| return success(value.hasOneUse()); | |
| } | |
| static LogicalResult hasSameElementTypeImpl(PatternRewriter &rewriter, | |
| Value value1, Value Value2) { | |
| return success(cast<ShapedType>(value1.getType()).getElementType() == | |
| cast<ShapedType>(value2.getType()).getElementType()); | |
| } | |
| void registerNativeConstraints(RewritePatternSet &patterns) { | |
| patternList.getPDLPatterns().registerConstraintFunction( | |
| "HasOneUse", hasOneUseImpl); | |
| patternList.getPDLPatterns().registerConstraintFunction( | |
| "HasSameElementType", hasSameElementTypeImpl); | |
| } | |
| ``` | |
| ##### Defining Native Constraints in PDLL | |
| In addition to importing native constraints, PDLL also supports defining native | |
| constraints directly when compiling ahead-of-time (AOT) for C++. These | |
| constraints can be defined by specifying a string code block after the | |
| constraint declaration: | |
| ```pdll | |
| Constraint HasOneUse(value: Value) [{ | |
| return success(value.hasOneUse()); | |
| }]; | |
| Constraint HasSameElementType(value1: Value, value2: Value) [{ | |
| return success(cast<ShapedType>(value1.getType()).getElementType() == | |
| cast<ShapedType>(value2.getType()).getElementType()); | |
| }]; | |
| Pattern { | |
| // A single entity constraint can be applied via the variable argument list. | |
| let value: HasOneUse; | |
| // Otherwise, constraints can be applied via the call operator: | |
| let value: Value = ...; | |
| let value2: Value = ...; | |
| HasOneUse(value); | |
| HasSameElementType(value, value2); | |
| } | |
| ``` | |
| The arguments of the constraint are accessible within the code block via the | |
| same name. See the ["type translation"](#native-constraint-type-translations) below for | |
| detailed information on how PDLL types are converted to native types. In addition to the | |
| PDLL arguments, the code block may also access the current `PatternRewriter` using | |
| `rewriter`. The result type of the native constraint function is implicitly defined | |
| as a `::llvm::LogicalResult`. | |
| Taking the constraints defined above as an example, these function would roughly be | |
| translated into: | |
| ```c++ | |
| LogicalResult HasOneUse(PatternRewriter &rewriter, Value value) { | |
| return success(value.hasOneUse()); | |
| } | |
| LogicalResult HasSameElementType(Value value1, Value value2) { | |
| return success(cast<ShapedType>(value1.getType()).getElementType() == | |
| cast<ShapedType>(value2.getType()).getElementType()); | |
| } | |
| ``` | |
| TODO: Native constraints should also be allowed to return values in certain cases. | |
| ###### Native Constraint Type Translations | |
| The types of argument and result variables are generally mapped to the corresponding | |
| MLIR type of the [constraint](#constraints) used. Below is a detailed description | |
| of how the mapped type of a variable is determined for the various different types of | |
| constraints. | |
| * Attr, Op, Type, TypeRange, Value, ValueRange: | |
| These are all core constraints, and are mapped directly to the MLIR equivalent | |
| (that their names suggest), namely: | |
| * `Attr` -> "::mlir::Attribute" | |
| * `Op` -> "::mlir::Operation *" | |
| * `Type` -> "::mlir::Type" | |
| * `TypeRange` -> "::mlir::TypeRange" | |
| * `Value` -> "::mlir::Value" | |
| * `ValueRange` -> "::mlir::ValueRange" | |
| * Op<dialect.name> | |
| A named operation constraint has a unique translation. If the ODS registration of the | |
| referenced operation has been included, the qualified C++ is used. If the ODS information | |
| is not available, this constraint maps to "::mlir::Operation *", similarly to the unnamed | |
| variant. For example, given the following: | |
| ```pdll | |
| // `my_ops.td` provides the ODS definition of the `my_dialect` operations, such as | |
| // `my_dialect.bar` used below. | |
| #include "my_ops.td" | |
| Constraint Cst(op: Op<my_dialect.bar>) [{ | |
| return success(op ... ); | |
| }]; | |
| ``` | |
| The native type used for `op` may be of the form `my_dialect::BarOp`, as opposed to the | |
| default `::mlir::Operation *`. Below is a sample translation of the above constraint: | |
| ```c++ | |
| LogicalResult Cst(my_dialect::BarOp op) { | |
| return success(op ... ); | |
| } | |
| ``` | |
| * Imported ODS Constraints | |
| Aside from the core constraints, certain constraints imported from ODS may use a unique | |
| native type. How to enable this unique type depends on the ODS constraint construct that | |
| was imported: | |
| * `Attr` constraints | |
| - Imported `Attr` constraints utilize the `storageType` field for native type translation. | |
| * `Type` constraints | |
| - Imported `Type` constraints utilize the `cppClassName` field for native type translation. | |
| * `AttrInterface`/`OpInterface`/`TypeInterface` constraints | |
| - Imported interfaces utilize the `cppInterfaceName` field for native type translation. | |
| #### Defining Constraints Inline | |
| In addition to global scope, PDLL Constraints and Native Constraints defined in | |
| PDLL may be specified *inline* at any level of nesting. This means that they may | |
| be defined in Patterns, other Constraints, Rewrites, etc: | |
| ```pdll | |
| Constraint GlobalConstraint() { | |
| Constraint LocalConstraint(value: Value) { | |
| ... | |
| }; | |
| Constraint LocalNativeConstraint(value: Value) [{ | |
| ... | |
| }]; | |
| let someValue: [LocalConstraint, LocalNativeConstraint] = ...; | |
| } | |
| ``` | |
| Constraints that are defined inline may also elide the name when used directly: | |
| ```pdll | |
| Constraint GlobalConstraint(inputValue: Value) { | |
| Constraint(value: Value) { ... }(inputValue); | |
| Constraint(value: Value) [{ ... }](inputValue); | |
| } | |
| ``` | |
| When defined inline, PDLL constraints may reference any previously defined | |
| variable: | |
| ```pdll | |
| Constraint GlobalConstraint(op: Op<my_dialect.foo>) { | |
| Constraint LocalConstraint() { | |
| let results = op.results; | |
| }; | |
| } | |
| ``` | |
| ### Rewriters | |
| Rewriters define the set of transformations to be performed within the `rewrite` | |
| section of a pattern, and, more specifically, how to transform the input IR | |
| after a successful pattern match. All PDLL rewrites must be defined within the | |
| `rewrite` section of the pattern. The `rewrite` section is denoted by the last | |
| statement within the body of the `Pattern`, which is required to be an | |
| [operation rewrite statement](#operation-rewrite-statements). There are two main | |
| categories of rewrites in PDLL: operation rewrite statements, and user defined | |
| rewrites. | |
| #### Operation Rewrite statements | |
| Operation rewrite statements are builtin PDLL statements that perform an IR | |
| transformation given a root operation. These statements are the only ones able | |
| to start the `rewrite` section of a pattern, as they allow for properly | |
| ["binding"](#variable-binding) the root operation of the pattern. | |
| ##### `erase` statement | |
| ```pdll | |
| // A pattern that erases all `my_dialect.foo` operations. | |
| Pattern => erase op<my_dialect.foo>; | |
| ``` | |
| The `erase` statement erases a given operation. | |
| ##### `replace` statement | |
| ```pdll | |
| // A pattern that replaces the root operation with its input value. | |
| Pattern { | |
| let root = op<my_dialect.foo>(input: Value); | |
| replace root with input; | |
| } | |
| // A pattern that replaces the root operation with multiple input values. | |
| Pattern { | |
| let root = op<my_dialect.foo>(input: Value, _: Value, input2: Value); | |
| replace root with (input, input2); | |
| } | |
| // A pattern that replaces the root operation with another operation. | |
| // Note that when an operation is used as the replacement, we can infer its | |
| // result types from the input operation. In these cases, the result | |
| // types of replacement operation may be elided. | |
| Pattern { | |
| // Note: In this pattern we also inlined the `root` expression. | |
| replace op<my_dialect.foo> with op<my_dialect.bar>; | |
| } | |
| ``` | |
| The `replace` statement allows for replacing a given root operation with either | |
| another operation, or a set of input `Value` and `ValueRange` values. When an operation | |
| is used as the replacement, we allow infering the result types from the input operation. | |
| In these cases, the result types of replacement operation may be elided. Note that no | |
| other components aside from the result types will be inferred from the input operation | |
| during the replacement. | |
| ##### `rewrite` statement | |
| ```pdll | |
| // A simple pattern that replaces the root operation with its input value. | |
| Pattern { | |
| let root = op<my_dialect.foo>(input: Value); | |
| rewrite root with { | |
| ... | |
| replace root with input; | |
| }; | |
| } | |
| ``` | |
| The `rewrite` statement allows for rewriting a given root operation with a block | |
| of nested rewriters. The root operation is not implicitly erased or replaced, | |
| and any transformations to it must be expressed within the nested rewrite block. | |
| The inner body may contain any number of other rewrite statements, variables, or | |
| expressions. | |
| #### Defining Rewriters in PDLL | |
| Additional rewrites can also be defined within PDLL, which allows for building | |
| rewrite fragments that can be composed across many different patterns. A | |
| rewriter in PDLL is defined similarly to a function in traditional programming | |
| languages; it contains a name, a set of input arguments, a set of result types, | |
| and a body. Results of a rewrite are returned via a `return` statement. A few | |
| examples are shown below: | |
| ```pdll | |
| // A rewrite that constructs and returns a new operation, given an input value. | |
| Rewrite BuildFooOp(value: Value) -> Op { | |
| return op<my_dialect.foo>(value); | |
| } | |
| Pattern { | |
| // We invoke the rewrite in the same way as functions in traditional | |
| // languages. | |
| replace op<my_dialect.old_op>(input: Value) with BuildFooOp(input); | |
| } | |
| ``` | |
| ##### Rewrites with multiple results | |
| Rewrites can return multiple results by returning a tuple of values. When | |
| returning multiple results, each result can also be assigned a name to use when | |
| indexing that tuple element. Tuple elements can be referenced by their index | |
| number, or by name if they were assigned one. | |
| ```pdll | |
| // A rewrite that returns multiple results, with some of the results assigned | |
| // a more readable name. | |
| Rewrite CreateRewriteOps() -> (Op, result1: ValueRange) { | |
| return (op<my_dialect.bar>, op<my_dialect.foo>); | |
| } | |
| Pattern { | |
| rewrite root: Op<my_dialect.foo> with { | |
| // Invoke the rewrite, which returns a tuple of values. | |
| let result = CreateRewriteOps(); | |
| // Index the tuple elements by index, or by name. | |
| replace root with (result.0, result.1, result.result1); | |
| } | |
| } | |
| ``` | |
| ##### Rewrite result type inference | |
| In addition to explicitly specifying the results of the rewrite via the rewrite | |
| signature, PDLL defined rewrites also support inferring the result type from the | |
| return statement. Result type inference is active whenever the rewrite is | |
| defined with no result constraints: | |
| ```pdll | |
| // This rewrite returns a derived operation. | |
| Rewrite ReturnSelf(op: Op<my_dialect.foo>) => op; | |
| // This rewrite returns a tuple of two Values. | |
| Rewrite ExtractMultipleResults(op: Op<my_dialect.foo>) { | |
| return (result1 = op.result1, result2 = op.result2); | |
| } | |
| Pattern { | |
| rewrite root: Op<my_dialect.foo> with { | |
| let values = ExtractMultipleResults(op<my_dialect.foo>); | |
| replace root with (values.result1, values.result2); | |
| } | |
| } | |
| ``` | |
| ##### Single Line "Lambda" Body | |
| Rewrites generally define their body using a compound block of statements, as | |
| shown below: | |
| ```pdll | |
| Rewrite ReturnSelf(op: Op<my_dialect.foo>) { | |
| return op; | |
| } | |
| Rewrite EraseOp(op: Op) { | |
| erase op; | |
| } | |
| ``` | |
| Rewrites also support a lambda-like syntax for specifying simple single line | |
| bodies. The lambda body of a Rewrite expects a single expression, which is | |
| implicitly returned, or a single | |
| [operation rewrite statement](#operation-rewrite-statements): | |
| ```pdll | |
| Rewrite ReturnSelf(op: Op<my_dialect.foo>) => op; | |
| Rewrite EraseOp(op: Op) => erase op; | |
| ``` | |
| #### Native Rewriters | |
| Rewriters may also be defined outside of PDLL, and registered natively within | |
| the C++ API. | |
| ##### Importing existing Native Rewrites | |
| Rewrites defined externally can be imported into PDLL by specifying a | |
| rewrite "declaration". This is similar to the PDLL form of defining a | |
| rewrite but omits the body. Importing the declaration in this form allows for | |
| PDLL to statically know the expected input and output types. | |
| ```pdll | |
| // Import a single input native rewrite that returns a new operation. This | |
| // rewrite must be registered by the consumer of the compiled PDL. | |
| Rewrite BuildOp(value: Value) -> Op; | |
| Pattern { | |
| replace op<my_dialect.old_op>(input: Value) with BuildOp(input); | |
| } | |
| ``` | |
| External rewrites are those registered explicitly with the `RewritePatternSet` via | |
| the C++ PDL API. For example, the rewrite above may be registered as: | |
| ```c++ | |
| static Operation *buildOpImpl(PDLResultList &results, Value value) { | |
| // insert special rewrite logic here. | |
| Operation *resultOp = ...; | |
| return resultOp; | |
| } | |
| void registerNativeRewrite(RewritePatternSet &patterns) { | |
| patterns.getPDLPatterns().registerRewriteFunction("BuildOp", buildOpImpl); | |
| } | |
| ``` | |
| ##### Defining Native Rewrites in PDLL | |
| In addition to importing native rewrites, PDLL also supports defining native | |
| rewrites directly when compiling ahead-of-time (AOT) for C++. These rewrites can | |
| be defined by specifying a string code block after the rewrite declaration: | |
| ```pdll | |
| Rewrite BuildOp(value: Value) -> (foo: Op<my_dialect.foo>, bar: Op<my_dialect.bar>) [{ | |
| return {my_dialect::FooOp::create(rewriter, value), my_dialect::BarOp::create(rewriter)}; | |
| }]; | |
| Pattern { | |
| let root = op<my_dialect.foo>(input: Value); | |
| rewrite root with { | |
| // Invoke the native rewrite and use the results when replacing the root. | |
| let results = BuildOp(input); | |
| replace root with (results.foo, results.bar); | |
| } | |
| } | |
| ``` | |
| The arguments of the rewrite are accessible within the code block via the | |
| same name. See the ["type translation"](#native-rewrite-type-translations) below for | |
| detailed information on how PDLL types are converted to native types. In addition to the | |
| PDLL arguments, the code block may also access the current `PatternRewriter` using | |
| `rewriter`. See the ["result translation"](#native-rewrite-result-translation) section | |
| for detailed information on how the result type of the native function is determined. | |
| Taking the rewrite defined above as an example, this function would roughly be | |
| translated into: | |
| ```c++ | |
| std::tuple<my_dialect::FooOp, my_dialect::BarOp> BuildOp(Value value) { | |
| return {my_dialect::FooOp::create(rewriter, value), my_dialect::BarOp::create(rewriter)}; | |
| } | |
| ``` | |
| ###### Native Rewrite Type Translations | |
| The types of argument and result variables are generally mapped to the corresponding | |
| MLIR type of the [constraint](#constraints) used. The rules of native `Rewrite` type translation | |
| are identical to those of native `Constraint`s, please view the corresponding | |
| [native `Constraint` type translation](#native-constraint-type-translations) section for a | |
| detailed description of how the mapped type of a variable is determined. | |
| ###### Native Rewrite Result Translation | |
| The results of a native rewrite are directly translated to the results of the native function, | |
| using the type translation rules [described above](#native-rewrite-type-translations). The section | |
| below describes the various result translation scenarios: | |
| * Zero Result | |
| ```pdll | |
| Rewrite createOp() [{ | |
| my_dialect::FooOp::create(rewriter); | |
| }]; | |
| ``` | |
| In the case where a native `Rewrite` has no results, the native function returns `void`: | |
| ```c++ | |
| void createOp(PatternRewriter &rewriter) { | |
| my_dialect::FooOp::create(rewriter); | |
| } | |
| ``` | |
| * Single Result | |
| ```pdll | |
| Rewrite createOp() -> Op<my_dialect.foo> [{ | |
| return my_dialect::FooOp::create(rewriter); | |
| }]; | |
| ``` | |
| In the case where a native `Rewrite` has a single result, the native function returns the corresponding | |
| native type for that single result: | |
| ```c++ | |
| my_dialect::FooOp createOp(PatternRewriter &rewriter) { | |
| return my_dialect::FooOp::create(rewriter); | |
| } | |
| ``` | |
| * Multi Result | |
| ```pdll | |
| Rewrite complexRewrite(value: Value) -> (Op<my_dialect.foo>, FunctionOpInterface) [{ | |
| ... | |
| }]; | |
| ``` | |
| In the case where a native `Rewrite` has multiple results, the native function returns a `std::tuple<...>` | |
| containing the corresponding native types for each of the results: | |
| ```c++ | |
| std::tuple<my_dialect::FooOp, FunctionOpInterface> | |
| complexRewrite(PatternRewriter &rewriter, Value value) { | |
| ... | |
| } | |
| ``` | |
| #### Defining Rewrites Inline | |
| In addition to global scope, PDLL Rewrites and Native Rewrites defined in PDLL | |
| may be specified *inline* at any level of nesting. This means that they may be | |
| defined in Patterns, other Rewrites, etc: | |
| ```pdll | |
| Rewrite GlobalRewrite(inputValue: Value) { | |
| Rewrite localRewrite(value: Value) { | |
| ... | |
| }; | |
| Rewrite localNativeRewrite(value: Value) [{ | |
| ... | |
| }]; | |
| localRewrite(inputValue); | |
| localNativeRewrite(inputValue); | |
| } | |
| ``` | |
| Rewrites that are defined inline may also elide the name when used directly: | |
| ```pdll | |
| Rewrite GlobalRewrite(inputValue: Value) { | |
| Rewrite(value: Value) { ... }(inputValue); | |
| Rewrite(value: Value) [{ ... }](inputValue); | |
| } | |
| ``` | |
| When defined inline, PDLL rewrites may reference any previously defined | |
| variable: | |
| ```pdll | |
| Rewrite GlobalRewrite(op: Op<my_dialect.foo>) { | |
| Rewrite localRewrite() { | |
| let results = op.results; | |
| }; | |
| } | |
| ``` |