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:
Chris Fallin
2022-04-28 16:37:11 -07:00
committed by GitHub
parent 128c42fa09
commit 5b7d56f6f7
12 changed files with 496 additions and 55 deletions

View File

@@ -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