ISLE: add support for extended left-hand sides with if-let clauses. (#4072)
This PR adds support for `if-let` clauses, as proposed in bytecodealliance/rfcs#21. These clauses augment the left-hand side (pattern-matching phase) of rules in the ISLE instruction-selection DSL with sub-patterns matching on sub-expressions. The ability to list additional match operations to perform, out-of-line from the original toplevel pattern, greatly simplifies some idioms. See the RFC for more details and examples of use.
This commit is contained in:
@@ -949,6 +949,118 @@ semantically as important as the core language: they are not
|
||||
implementation details, but rather, a well-defined interface by which
|
||||
ISLE can interface with the outside world (an "FFI" of sorts).
|
||||
|
||||
### If-Let Clauses
|
||||
|
||||
As an extension to the basic left-hand-side / right-hand-side rule
|
||||
idiom, ISLE allows *if-let clauses* to be used. These add additional
|
||||
pattern-matching steps, and can be used to perform additional tests
|
||||
and also to use constructors in place of extractors during the match
|
||||
phase when this is more convenient.
|
||||
|
||||
To introduce the concept, an example follows (this is taken from the
|
||||
[RFC](https://github.com/bytecodealliance/rfcs/tree/main/isle-extended-patterns.md)
|
||||
that proposed if-lets):
|
||||
|
||||
```lisp
|
||||
;; `u32_fallible_add` can now be used in patterns in `if-let` clauses
|
||||
(decl pure u32_fallible_add (u32 u32) u32)
|
||||
(extern constructor u32_fallible_add u32_fallible_add)
|
||||
|
||||
(rule (lower (load (iadd addr
|
||||
(iadd (uextend (iconst k1))
|
||||
(uextend (iconst k2))))))
|
||||
(if-let k (u32_fallible_add k1 k2))
|
||||
(isa_load (amode_reg_offset addr k)))
|
||||
```
|
||||
|
||||
The key idea is that we allow a `rule` form to contain the following
|
||||
sub-forms:
|
||||
|
||||
```lisp
|
||||
(rule LHS_PATTERN
|
||||
(if-let PAT2 EXPR2)
|
||||
(if-let PAT3 EXPR3)
|
||||
...
|
||||
RHS)
|
||||
```
|
||||
|
||||
The matching proceeds as follows: the main pattern (`LHS_PATTERN`)
|
||||
matches against the input value (the term to be rewritten), as
|
||||
described in detail above. Then, if this matches, execution proceeds
|
||||
to the if-let clauses in the order they are specified. For each, we
|
||||
evaluate the expression (`EXPR2` or `EXPR3` above) first. An
|
||||
expression in an if-let context is allowed to be "fallible": the
|
||||
constructors return `Option<T>` at the Rust level and can return
|
||||
`None`, in which case the whole rule application fails and we move on
|
||||
to the next rule as if the main pattern had failed to match. (MOre on
|
||||
the fallible constructors below.) If the expression evaluation
|
||||
succeeds, we match the associated pattern (`PAT2` or `PAT3` above)
|
||||
against the resulting value. This too can fail, causing the whole rule
|
||||
to fail. If it succeeds, any resulting variable bindings are
|
||||
available. Variables bound in the main pattern are available for all
|
||||
if-let expressions and patterns, and variables bound by a given if-let
|
||||
clause are available for all subsequent clauses. All bound variables
|
||||
(from the main pattern and if-let clauses) are available in the
|
||||
right-hand side expression.
|
||||
|
||||
#### Pure Expressions and Constructors
|
||||
|
||||
In order for an expression to be used in an if-let clause, it has to
|
||||
be *pure*: it cannot have side-effects. A pure expression is one that
|
||||
uses constants and pure constructors only. Enum variant constructors
|
||||
are always pure. In general constructors that invoke function calls,
|
||||
however (either as internal or external constructor calls), can lead
|
||||
to arbitrary Rust code and have side-effects. So, we add a new
|
||||
annotation to declarations as follows:
|
||||
|
||||
```lisp
|
||||
;; `u32_fallible_add` can now be used in patterns in `if-let` clauses
|
||||
(decl pure u32_fallible_add (u32 u32) u32)
|
||||
|
||||
;; This adds a method
|
||||
;; `fn u32_fallible_add(&mut self, _: u32, _: u32) -> Option<u32>`
|
||||
;; to the `Context` trait.
|
||||
(extern constructor u32_fallible_add u32_fallible_add)
|
||||
```
|
||||
|
||||
The `pure` keyword here is a declaration that the term, when used as a
|
||||
constructor, has no side-effects. Declaring an external constructor on
|
||||
a pure term is a promise by the ISLE programmer that the external Rust
|
||||
function we are naming (here `u32_fallible_add`) has no side-effects
|
||||
and is thus safe to invoke during the match phase of a rule, when we
|
||||
have not committed to a given rule yet.
|
||||
|
||||
When an internal constructor body is generated for a term that is pure
|
||||
(i.e., if we had `(rule (u32_fallible_add x y) ...)` in our program
|
||||
after the above declaration instead of the `extern`), the right-hand
|
||||
side expression of each rule that rewrites the term is also checked
|
||||
for purity.
|
||||
|
||||
#### `if` Shorthand
|
||||
|
||||
It is a fairly common idiom that if-let clauses are used as predicates
|
||||
on rules, such that their only purpose is to allow a rule to match,
|
||||
and not to perform any destructuring with a sub-pattern. For example,
|
||||
one might want to write:
|
||||
|
||||
```lisp
|
||||
(rule (lower (special_inst ...))
|
||||
(if-let _ (isa_extension_enabled))
|
||||
(isa_special_inst ...))
|
||||
```
|
||||
|
||||
where `isa_extension_enabled` is a pure constructor that is fallible,
|
||||
and succeeds only when a condition is true.
|
||||
|
||||
To enable more succinct expression of this idiom, we allow the
|
||||
following shorthand notation using `if` instead:
|
||||
|
||||
```lisp
|
||||
(rule (lower (special_inst ...))
|
||||
(if (isa_extension_enabled))
|
||||
(isa_special_inst ...))
|
||||
```
|
||||
|
||||
### Mapping to Rust: Constructors, Functions, and Control Flow
|
||||
|
||||
ISLE was designed to have a simple, easy-to-understand mapping from
|
||||
|
||||
Reference in New Issue
Block a user