1# ISLE: Instruction Selection Lowering Expressions
2
3This document will describe ISLE (Instruction Selection Lowering
4Expressions), a DSL (domain-specific language) that we have developed
5in order to help us express certain parts of the Cranelift compiler
6backend more naturally. ISLE was first [described in RFC
7#15](https://github.com/bytecodealliance/rfcs/pull/15) and now is used
8by and lives in the Cranelift tree in
9[cranelift/isle](https://github.com/bytecodealliance/wasmtime/tree/main/cranelift/isle).
10
11Documentation on how ISLE is used in Cranelift can be found
12[here](../../docs/isle-integration.md).
13
14## Intro and Whirlwind Tour: DSL for Instruction Lowering
15
16The goal of ISLE is to represent *instruction lowering patterns*. An
17instruction lowering pattern is a specification that a certain
18combination of operators in the IR (CLIF), when combined under certain
19conditions, can be compiled down into a certain sequence of machine
20instructions. For example:
21
22- An `iadd` (integer add) operator can always be lowered to an x86
23  `ADD` instruction with two register sources.
24
25- An `iadd` operator with one `iconst` (integer-constant) argument can
26  be lowered to an x86 `ADD` instruction with a register and an
27  immediate.
28
29One could write something like the following in ISLE (simplified from
30the real code [here](../codegen/src/isa/x64/lower.isle)):
31
32```lisp
33;; Add two registers.
34(rule (lower (iadd x y))
35      (value_reg (add
36                   (put_in_reg x)
37                   (RegMemImm.Reg (put_in_reg y)))))
38
39;; Add a register and an immediate.
40(rule (lower (iadd x (simm32_from_value y))
41      (value_reg (add
42                   (put_in_reg x)
43                   ;; `y` is a `RegMemImm.Imm`.
44                   y)))
45```
46
47ISLE lets the compiler backend developer express this information in a
48declarative way -- i.e., just write down a list of patterns, without
49worrying how the compilation process tries them out -- and the ISLE
50DSL compiler will convert this list of patterns into efficient Rust
51code that becomes part of Cranelift.
52
53The rest of this document will describe the semantics of the DSL
54itself. ISLE has been designed to be a general-purpose DSL that can
55apply to any sort of backtracking pattern-matching problem, and will
56generate a decision tree in Rust that can call into arbitrary
57interface code.
58
59Separate documentation will describe how we have written *bindings*
60and *helpers* to allow ISLE to be specifically used to write Cranelift
61lowering patterns like the above. (TODO: link this documentation)
62
63## Outline of This Document
64
65This document is organized into the following sections:
66
67* Term-Rewriting Systems: a general overview of how term-rewriting
68  systems work, how to think about nested terms, patterns and rewrite
69  rules, how they provide a general mechanism for computation, and how
70  term-rewriting is often used in a compiler-implementation context.
71
72* Core ISLE: the foundational concepts of the ISLE DSL, building upon
73  a general-purpose term-rewriting base. Covers the type system (typed
74  terms) and how rules are written.
75
76* ISLE with Rust: covers how ISLE provides an "FFI" (foreign function
77  interface) of sorts to allow interaction with Rust code, and
78  describes the scheme by which ISLE execution is mapped onto Rust
79  (data structures and control flow).[^1]
80
81* ISLE Internals: describes how the ISLE compiler works. Provides
82  insight into how an unordered collection of rewrite rules are
83  combined into executable Rust code that efficiently traverses the
84  input and matches on it.
85
86[^1]: One might call this the BRIDGE (Basic Rust Interface Designed
87    for Good Efficiency) to the ISLE, but unfortunately we missed the
88    chance to introduce that backronym when we wrote the initial
89    implementation.
90
91## Background: Term-Rewriting Systems
92
93*Note: this section provides general background on term-rewriting
94systems that is useful to better understand the context for ISLE and
95how to develop systems using it. Readers already familiar with
96term-rewriting systems, or wishing to skip to details on ISLE's
97version of term rewriting, can skip to the [next
98section](#core-isle-a-term-rewriting-system).*
99
100A [term-rewriting
101system](https://en.wikipedia.org/wiki/Rewriting#Term_rewriting_systems),
102or TRS, is a system that works by representing data as *terms* and
103then applying *rules* to "rewrite" the terms. This rewrite process
104continues until some application-specific end-condition is met, for
105example until no more rules are applicable or until the term reaches a
106"lowered" state by some definition, at which point the resulting term
107is the system's output.
108
109Term-rewriting systems are a general kind of computing system, at the
110same level as (e.g.) Turing machines or other abstract computing
111machines. Term-rewriting is actually Turing-complete, or in other
112words, can express any program, if no limits are placed on term length
113or recursion.[^2]
114
115[^2]: In fact, the [lambda
116      calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
117      introduced by Alonzo Church is actually a term-rewriting system
118      and was developed at the same time as Turing's concepts of
119      universal computation!
120
121Why might one want to use a TRS rather than some other, more
122conventional, way of computing an answer? One reason is that they are
123highly applicable to *pattern-matching* problems: for example,
124translating data in one domain to data in another domain, where the
125translation consists of a bunch of specific equivalences. This is part
126of why term-rewriting is so interesting in the compiler domain:
127compiler backends work to lower certain patterns in the program (e.g.:
128a multiply-add combination) into instructions that the target machine
129provides (e.g.: a dedicated multiply-add instruction).
130
131Term rewriting as a process also naturally handles issues of
132*priority*, i.e. applying a more specific rule before a less specific
133one. This is because the abstraction allows for multiple rules to be
134"applicable", and so there is a natural place to reason about priority
135when we choose which rule to apply. This permits a nice separation of
136concerns: we can specify which rewrites are *possible* to apply
137separately from which are *desirable* to apply, and adjust or tune the
138latter (the "strategy") at will without breaking the system's
139correctness.
140
141Additionally, term rewriting allows for a sort of *modularity* that is
142not present in hand-written pattern-matching code: the specific rules
143can be specified in any order, and the term-rewriting engine "weaves"
144them together so that in any state, when we have partially matched the
145input and are narrowing down which rule will apply, we consider all
146the related rules at once. Said another way: hand-written code tends
147to accumulate a lot of nested conditionals and switch/match
148statements, i.e., resembles a very large decision tree, while
149term-rewriting code tends to resemble a flat list of simple patterns
150that, when composed and combined, become that more complex tree. This
151allows the programmer to more easily maintain and update the set of
152lowering rules, considering each in isolation.
153
154### Data: Nested Trees of Constructors
155
156A term-rewriting system typically operates on data that is in a *tree*
157form, or at least can be interpreted that way.[^3]
158
159[^3]: In the most fundamental and mathematical sense, a TRS just
160      operates on a sequence of symbols, but we can talk about
161      structure that is present in those symbols in any well-formed
162      sequence. For example, we can define a TRS that only operates on
163      terms with balanced parentheses; then we have our tree.
164
165In ISLE and hence in this document, we operate on terms that are
166written in an
167[S-expression](https://en.wikipedia.org/wiki/S-expression) syntax,
168borrowed from the Lisp world. So we might have a term:
169
170```lisp
171    (a (b c 1 2) (d) (e 3 4))
172```
173
174which we can write more clearly as the tree:
175
176```lisp
177    (a
178      (b
179        c 1 2)
180      (d)
181      (e
182        3 4))
183```
184
185Each term consists of either a *constructor* (which looks like a
186function call to Lisp-trained eyes) or a *primitive*. In the above,
187the `(a ...)`, `(b ...)`, `(d)`, and `(e ...)` terms/subterms are
188constructor invocations. A constructor takes some number of arguments
189(its *arity*), each of which is itself a term. Primitives can be
190things like integer, string, or boolean constants, or variable names.
191
192Some term-rewriting systems have other syntax conventions: for
193example, systems based on
194[Prolog](https://en.wikipedia.org/wiki/Prolog) tend to write terms
195like `a(b(c, 1, 2), d, e(3, 4))`, i.e., with the name of the term on
196the outside of the parentheses. This is just a cosmetic difference to
197the above, but we note it to make clear that the term structure is
198important, not the syntax.
199
200It may not be immediately clear how to use this data representation,
201but we can give a small flavor here: if one defines *constructors* for
202each instruction or operator in a compiler's intermediate
203representation (IR), one can start to write expressions from that IR
204as terms; for example:
205
206```lisp
207    v1 = imul y, z
208    v2 = iadd x, v1
209```
210
211could become:
212
213```lisp
214    (iadd x (imul y z))
215```
216
217This will become much more useful once we have rewrite rules to
218perform transformations on the terms!
219
220Representing an IR is, of course, just one possible use of term data
221(albeit the original "MVP" that guided ISLE's design); there are many
222others, too. Interested readers are encouraged to read more on, e.g.,
223[Prolog](https://en.wikipedia.org/wiki/Prolog), which has been used to
224represent logical predicates, "facts" in expert systems, symbolic
225mathematical terms, and more.
226
227### Rules: Left-hand-side Patterns, Right-hand-side Expressions
228
229The heart of a term-rewriting system is in the set of *rules* that
230actually perform the rewrites. The "program" itself, in a
231term-rewriting DSL, consists simply of an unordered list of
232rules. Each rule may or may not apply; if it applies, then it can be
233used to edit the term. Execution consists of repeated application of
234rules until some criteria are met.
235
236A rule consists of two parts: the left-hand side (LHS), or *pattern*,
237and right-hand side (RHS), or *expression*. The left-hand and
238right-hand nomenclature comes from a common way of writing rules as:
239
240```plain
241    A -> B              ;; any term "A" is rewritten to "B"
242
243    (A x) -> (B (C x))  ;; any term (A x), for some x, is rewritten to (B (C x)).
244
245    (A _) -> (D)        ;; any term (A _), where `_` is a wildcard (any subterm),
246                        ;; is rewritten to (D).
247```
248
249#### Left-hand Sides: Patterns
250
251Each left-hand side is written in a pattern language that commonly has
252a few different kinds of "matchers", or operators that can match
253subterms:
254
255* `(A pat1 pat2 ...)` matches a constructor `A` with patterms for each
256  of its arguments.
257
258* `x` matches any subterm and captures its value in a variable
259  binding, which can be used later when we specify the right-hand side
260  (so that the rewrite contains parts of the original term).
261
262* `_` is a wildcard and matches anything, without capturing it.
263
264* Primitive constant values, such as `42` or `$Symbol`, match only if
265  the term is exactly equal to this constant.
266
267These pattern-matching operators can be combined, so we could write,
268for example, `(A (B x _) z)`. This pattern would match the term `(A (B
2691 2) 3)` but not `(A (C 4 5) 6)`.
270
271A pattern can properly be seen as a partial function from input term
272to captured (bound) variables: it either matches or it doesn't, and if
273it does, it provides specific term values for each variable binding
274that can be used by the right-hand side.
275
276A fully-featured term rewriting system usually has other operators as
277well, for convenience: for example, "match already-captured value", or
278"bind variable to subterm and also match it with subpattern", or
279"match subterm with all of these subpatterns". But even the above is
280powerful enough for Turing-complete term reduction, surprisingly; the
281essence of term-rewriting's power is just its ability to trigger
282different rules on different "shapes" of the tree of constructors in
283the input, and on special cases for certain argument values.
284
285Pattern-based term rewriting has a notable and important feature: it
286typically allows *overlapping* rules. This means that more than one
287pattern might match on the input. For example, the two rules:
288
289```plain
290    (A (B x)) -> (C x)
291    (A _) -> (D)
292```
293
294could *both* apply to an input term `(A (B 1))`. The first rule would
295rewrite this input to `(C 1)`, and the second rule would rewrite it to
296`(D)`. Either rewrite would be an acceptable execution step under the
297base semantics of most term-rewriting systems; ordinarily, the
298*correctness* of the rewrite should not depend on which rule is
299chosen, only possibly the "optimality" of the output (whatever that
300means for the application domain in question) or the number of rewrite
301steps to get there.
302
303However, in order to provide a deterministic answer, the system must
304somehow specify which rule will be applied in such a situation based
305on precedence, or specificity, or some other tie-breaker. A common
306heuristic is "more specific rule wins". We will see how ISLE resolves
307this question below by using both this heuristic and an explicit
308priority mechanism.[^4]
309
310[^4]: Some term-rewriting systems actually elaborate the entire space
311      of possibilities, following *all* possible rule application
312      sequences / rewrite paths. For example, the *equality
313      saturation* technique
314      ([paper](https://cseweb.ucsd.edu/~lerner/papers/popl09.pdf),
315      [example implementation
316      Egg](https://blog.sigplan.org/2021/04/06/equality-saturation-with-egg/))
317      builds a data structure that represents all equivalent terms
318      under a set of rewrite rules, from which a heuristic
319      (cost/goodness function) can be used to extract one answer when
320      needed.
321
322#### Right-hand Sides: Rewrite Expressions
323
324Given a rule whose pattern has matched, we now need to compute the
325rewritten term that replaces the original input term. This rewrite is
326specified by the right-hand side (RHS), which consists of an
327*expression* that generates a new term. This expression can use parts
328of the input term that have been captured by variables in the
329pattern.
330
331We have already seen a few examples of this above: simple term
332expressions, with variables used in place of concrete subterms where
333desired. A typical term-rewrite system allows just a few options in
334the output expression:
335
336* Terms, with sub-expressions as arguments;
337* Constant primitives (`42` or `$Symbol`); and
338* Captured variable values (`x`).
339
340The options are more limited in expressions than in patterns (e.g.,
341there are no wildcards) because a pattern is matching on a range of
342possible terms while an expression must specify a specific rewrite
343result.
344
345### Rewrite Steps and Intermediate Terms
346
347Now that we can specify rewrites via a list of rules, we can study how
348the top-level execution of a term-rewriting system proceeds. Much of
349the power of term-rewriting comes from the fact that rewrites can
350*chain together* into a multi-step traversal through several
351intermediate terms before the final answer is computed.
352
353For a simple example, consider the following rules:
354
355```plain
356    (A (B x)) -> (C x)
357    (C (D x)) -> (E x)
358    (C (F x)) -> (G x)
359```
360
361This set of rules will rewrite `(A (B (D 42)))` to `(C (D 42))`, then
362to `(E 42)` (via the first and third rules respectively).
363
364How is this useful? First, rewriting one term to another (here, `C` at
365the top level) that in turn appears in the left-hand side of other
366rules allows us to *factor* a "program" of term-rewriting rules in the
367same way that imperative programs are factored into separate
368functions.[^5] The usual advantages of a well-factored program, where
369each problem is solved with a small step that "reduces to a previously
370solved problem", apply here.
371
372Second, repeating the rewrite step is actually what grants
373term-rewriting its Turing-completeness: it allows for arbitrary
374control flow.[^6] This might be useful in cases where, for example, a
375term-rewriting program needs "loop" over a list of elements in the
376input: it can recurse and use intermediate terms to store state.
377
378While this full generality may not be used often in the
379domain-specific applications of term-rewriting that emphasize its
380pattern-matching (such as instruction selectors), the user should not
381be afraid to define and use intermediate terms -- rewriting into them,
382then defining additional rules to rewrite further -- when it helps to
383factor common behavior out of multiple rules, or aids in conceptual
384clarity.
385
386[^5]: In fact, ISLE actually compiles rules for different top-level
387      pattern terms (`(A ...)` and `(C ...)` in the example) into
388      separate Rust functions, so factoring rules to use intermediate
389      terms can provide code-size and compile-time benefits for the
390      ISLE-generated Rust code as well.
391
392[^6]: The [lambda calculus' reduction
393      rules](https://en.wikipedia.org/wiki/Lambda_calculus#Reduction)
394      are a good example of this.
395
396### Application to Compilers: A Term is a Value; Rewrites are Lowerings
397
398So far this has been a fairly abstract introduction to term-rewriting
399systems as a general computing paradigm. How does this relate (or, how
400is it commonly mapped) to the instruction-selection problem?
401
402In a domain such as instruction selection, we manipulate terms that
403represent computations described by an IR, and the terms are
404eventually rewritten into terms that name specific machine
405instructions. We can think of each term as having a denotational value
406that that *is* that program value. Then, any rewrite is correct if it
407preserves the denotational value of the term.
408
409In other words, terms are just values, and rules specify alternative
410ways to compute the same values. We might have rewrite rules that
411correspond to common algebraic identities (`a + b` == `b + a`, and
412`a + 0` == `a`), for example. The main sort of rewrite rule, however,
413will be one that takes a machine-*independent* operator term and
414rewrites it into a machine-*dependent* instruction term. For example:
415
416```plain
417    (iadd a b) -> (isa.add_reg_reg a b)
418
419    (iadd a (iconst 0)) -> a
420
421    (iadd a (iconst n)) (isa.add_reg_imm a n)
422```
423
424These rules specify three ways to convert an IR `iadd` operator into
425an ISA-specific instruction. Recall from above that in general, an
426application of a term-rewriting system should not depend for
427correctness on the order or choice of rule application: when multiple
428rules are applicable, then any sequence of rewrites that ends in a
429terminating state (a state with no further-applicable rules) should be
430considered a "correct" answer.[^7] Here, this is true: if, for
431example, we choose the register-plus-register form (the first rule)
432for an `iadd` operation, but the second argument is actually an
433`iconst`, then that is still valid, and the `iconst` will separately
434be rewritten by some other rule that generates a constant into a
435register. It simply may not be as efficient as the more specific third
436rule (or second rule, if the constant is zero). Hence, rule ordering
437and prioritization is nevertheless important for the quality of the
438instruction selector.
439
440[^7]: Note that this suggests an interesting testing strategy: we
441      could choose arbitrary (random) orders of lowering rules to
442      apply, or even deliberately worst-case orders according to some
443      heuristic. If we can differentially test programs compiled with
444      such randomized lowerings against "normally" compiled programs
445      and show that the results are always the same, then we have
446      shown that are lowering rules are "internally consistent",
447      without any other external oracle. This will have a similar
448      effect to
449      [wasm-mutate](https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasm-mutate),
450      but takes mutations implicitly from the pool of rules rather
451      than a separate externally-defined pool of mutations. This idea
452      remains future work.
453
454## Core ISLE: a Term-Rewriting System
455
456This section describes the core ISLE language. ISLE's core is a
457term-rewriting system, with a design that very closely follows the
458generic concepts that we have described above.
459
460In the core language, ISLE's key departure from many other
461term-rewriting systems is that it is *strongly typed*. A classical
462term-rewriting system, especially one designed for instruction
463rewriting, will typically have just one type of term, corresponding to
464a "value" in the program. In contrast, ISLE is designed so that terms
465can represent various concepts in a compiler backend: values, but also
466machine instructions or parts of those instructions ("integer
467immediate value encodable in machine's particular format"), or
468abstract bundles of information with invariants or guarantees encoded
469in the type system ("load that I can sink", "instruciton that produces
470flags").
471
472ISLE's other key departure from many other systems is its first-class
473integration with Rust, including a well-defined "FFI" mapping that
474allows ISLE rules to call into Rust in both their patterns and
475expressions, and to operate directly on types that are defined in the
476surrounding Rust code. This allows for easy and direct embedding into
477an existing compiler backend. We will cover this aspect more in the
478next section, [ISLE to Rust](#isle-to-rust).
479
480### Rules
481
482ISLE allows the user to specify rewrite rules, with a syntax similar
483in spirit to that shown above:
484
485```lisp
486    (rule
487      ;; left-hand side (pattern): if the input matches this ...
488      (A (B _ x) (C y))
489      ;; ... then rewrite to this:
490      (D x y))
491```
492
493The pattern (left-hand side) is made up of the following match
494operators:
495
496* Wildcards (`_`).
497* Integer constants (decimal/hex, positive/negative: `1`, `-1`,
498  `0x80`, `-0x80`) and boolean constants (`#t`, `#f`).
499* constants imported from the embedding, of arbitrary type
500  (`$MyConst`).
501* Variable captures and matches (bare identifiers like `x`; an
502  identifier consists of alphanumeric characters and underscores, and
503  does not start with a digit). The first occurrence of a variable `x`
504  captures the value; each subsequent occurrence matches on the
505  already-captured value, rejecting the match if not equal.
506* Variable captures with sub-patterns: `x @ PAT`, which captures the
507  subterm in `x` as above but also matches `PAT` against the
508  subterm. For example, `x @ (A y z)` matches an `A` term and captures
509  its arguments as `y` and `z`, but also captures the whole term as
510  `x`.
511* conjunctions of subpatterns: `(and PAT1 PAT2 ...)` matches all of
512  the subpatterns against the term. If any subpattern does not match,
513  then this matcher fails.
514* Term deconstruction: `(term PAT1 PAT2 ...)`, where `term` is a
515  defined term (type variant or constructor) and the subpatterns are
516  applied to each argument value in turn. Note that `term` cannot be a
517  wildcard; it must be a specific, concrete term.
518
519The expression (right-hand side) is made up of the following
520expression operators:
521
522* Integer and symbolic constants, as above.
523* Boolean constants `#t` and `#f` (following Scheme syntax).
524* Variable uses (bare `x` identifier).
525* Term constructors (`(term EXPR1 EXPR2 ...)`, where each
526  subexpression is evaluated first and then the term is constructed).
527* `let`-expressions that bind new variables, possibly using the values
528  multiple times within the body: `(let ((var1 type1 EXPR1) (var2 ...)
529  ...) BODY ...)`. Each variable's initialization expression can refer
530  to the immediately previous variable bindings (i.e., this is like a
531  `let*` in Scheme). `let`s are lexically-scoped, meaning that bound
532  variables are available only within the body of the `let`.
533
534When multiple rules are applicable to rewrite a particular term, ISLE
535will choose the "more specific" rule according to a particular
536heuristic: in the lowered sequence of matching steps, when one
537left-hand side completes a match while another with the same prefix
538continues with further steps, the latter (more specific) is chosen.
539
540The more-specific-first heuristic is usually good enough, but when an
541undesirable choice occurs, explicit priorities can be specified.
542Rules with explicit priorities are written as `(rule PRIO lhs rhs)`
543where `PRIO` is a signed (positive or negative) integer. An applicable
544rule with a higher priority will always match before a rule with a
545lower priority. The default priority for all rules if not otherwise
546specified is `0`.
547
548Note that the system allows multiple applicable rules to exist with
549the same priority: that is, while the priority system allows for
550manual tie-breaking, this tie-breaking is not required.
551
552Finally, one important note: the priority system is considered part of
553the core language semantics and execution of rules with different
554priorities is well-defined, so can be relied upon when specifying
555correct rules. However, the tie-breaking heuristic is *not* part of
556the specified language semantics, and so the user should never write
557rules whose correctness depends on one rule overriding another
558according to the heuristic.
559
560### Typed Terms
561
562ISLE allows the programmer to define types, and requires every term to
563have *parameter types* and a *return type* (analogous to first-order
564functions).
565
566The universe of types is very simple: there are *primitives*, which
567can be integers or symbolic constants (imported from the Rust
568embedding), and *enums*, which correspond direclty to Rust enums with
569variants that have named fields. There is no subtyping. Some examples
570of type definitions are:
571
572```lisp
573
574    (type u32 (primitive u32))  ;; u32 is a primitive, and is
575                                ;; spelled `u32` in the generated Rust code.
576
577    (type MyType (enum
578                   (A (x u32) (y u32))
579                   (B (z u32))
580                   (C)))        ;; MyType is an enum, with variants
581                                ;; `MyType::A { x, y }`, `MyType::B { z }`,
582                                ;; and `MyType::C`.
583
584    (type MyType2 extern (enum (A)))
585                                ;; MyType2 is an enum with variant `MyType2::A`.
586                                ;; Its type definition is not included in the
587                                ;; generated Rust, but rather, assumed to exist
588                                ;; in surrounding code. Useful for binding to
589                                ;; existing definitions.
590```
591
592We then declare constructors with their parameter and return types as
593follows:
594
595```lisp
596
597    (decl Term1 (u32 u32) MyType)  ;; Term1 has two `u32`-typed parameters,
598                                   ;; and itself has type `MyType`.
599    (decl Term2 () u32)            ;; Term2 has no parameters and type `u32`.
600```
601
602Note that when an enum type is defined, its variants are implicitly
603defined as constructors as well. These constructors are namespaced
604under the name of the type, to avoid ambiguity (or the need to do
605type-dependent lookups in the compiler, which can complicate type
606inference). For example, given the above `MyType` definitions, we
607automatically have the following constructors:
608
609```lisp
610
611    ;; These definitions are implicit and do not need to be written (doing
612    ;; so is a compile-time error, actually). We write them here just to
613    ;; show what they would look like.
614
615    (decl MyType.A (u32 u32) MyType)
616    (decl MyType.B (u32) MyType)
617    (decl MyType.C () MyType)
618
619    (decl MyType2.A () MyType2)
620```
621
622### Why Types?
623
624For terms that are not enum variants, the notion that a term "has a
625type" is somewhat foreign to a classical term-rewriting system. In
626most formal symbolic systems, the terms are manipulated as opaque
627sequences or trees of symbols; they have no inherent meaning other
628than what the user implicitly defines with the given rules. What does
629it mean for a term to "have a type" when it is just data? Or, said
630another way: why isn't the type of `Term2` just `Term2`?
631
632The types come into play when we define *rules*: one term of type `T`
633can only be rewritten into another term of type `T`, and when a
634parameter has a certain type, only subterms with that type can
635appear. Without explicit types on terms and their parameters, any term
636could be rewritten to any other, or substituted in as a parameter, and
637there is thus a kind of dynamic typing about which the programmer must
638have some caution. In most applications of a term-rewriting system,
639there is already some de-facto "schema": some parameter of a term
640representing a machine instruction can only take on one of a few
641subterms (representing, say, different addressing modes). ISLE's types
642just make this explicit.
643
644Thus, the first answer to "why types" is that they enforce a schema on
645the terms, allowing the programmer to have stronger well-formed-data
646invariants.
647
648The second reason is that the types are an integral part of the
649compilation-to-Rust strategy: every constructor actually does evaluate
650to a Rust value of the given "return value" type, given actual Rust
651values for its parameters of the appropriate parameter types. We will
652see more on this below.
653
654### Well-Typed Rules and Type Inference
655
656Now that we understand how to define types, let's examine in more
657detail how they are used to verify that the pattern and rewrite
658expression of a rule have the same type.
659
660ISLE uses a simple unidirectional type-inference algorithm that
661propagates type information through the pattern, resulting in a "type
662environment" that specifies the type for each captured variable, and
663then uses this to typecheck the rewrite expression. The result of this
664is that types are almost completely inferred, and are only annotated
665in a few places (`let` bindings specifically).
666
667The typing rules for patterns in ISLE are:
668
669* At the root of the pattern, we require that a *constructor* pattern
670  is used, rather than some other match operation (a wildcard, integer
671  constant, etc.). This is because compilation and dispatch into rules
672  is organized by the top-level constructor of the term being
673  rewritten.
674
675* At each part of the pattern except the root, there is an "expected
676  type" that is inferred from the surrounding context. We check that
677  this matches the actual type of the pattern.
678
679* A constructor pattern `(C x y z)`, given a constructor `(decl C (T1
680  T2 T2) R)`, has type `R` and provides expected types `T1`, `T2`, and
681  `T3` to its subpatterns.
682
683* A variable capture pattern `x` is compatible with any expected type
684  the first time it appears, and captures this expected type under the
685  variable identifier `x` in the type environment. Subsequent
686  appearances of `x` check that the expected type matches the
687  already-captured type.
688
689* A conjunction `(and PAT1 PAT2 ...)` checks that each subpattern is
690  compatible with the expected type.
691
692* Integer constants are compatible with any primitive expected
693  type. (This may change in the future if we add non-numeric
694  primitives, such as strings.)
695
696If we are able to typecheck the pattern, we have a type environment
697that is a map from variable bindings to types: e.g., `{ x: MyType, y:
698MyType2, z: u32 }`. We then typecheck the rewrite expression.
699
700* Every expression also has an expected type, from the surrounding
701  context. We check that the provided expression matches this type.
702
703* The top-level rewrite expression must have the same type as the
704  top-level constructor in the pattern. (In other words, a term can
705  only be rewritten to another term of the same type.)
706
707* Constructors check their return values against the expected type,
708  and typecheck their argument expressions against their parameter
709  types.
710
711* A `let` expression provides types for additional variable bindings;
712  these are added to the type environment while typechecking the
713  body. The expected type for the body is the same as the expected
714  type for the `let` itself.
715
716### A Note on Heterogeneous Types
717
718We should illuminate one particular aspect of the ISLE type system
719that we described above. We have said that a term must be rewritten to
720another term of the same type. Note that this does *not* mean that,
721for example, a set of ISLE rules cannot be used to translate a term of
722type `T1` to a term of type `T2`. The trick is to define a top-level
723"driver" that wraps the `T1`, such that reducing this term results in
724a `T2`. Concretely:
725
726```lisp
727    (type T1 ...)
728    (type T2 ...)
729
730    (decl Translate (T1) T2)
731
732    (rule (Translate (T1.A ...)
733                     (T2.X ...)))
734    (rule (Translate (T1.B ...)
735                     (T2.Y ...)))
736```
737
738This gets to the heart of rewrite-system-based computation, and has
739relevance for applications of ISLE to compiler backends. A common
740technique in rewrite systems is to "kick off" a computation by
741wrapping a term in some intermediate term that then drives a series of
742reductions. Here we are using `Translate` as this top-level term. A
743difference between ISLE and some other rewrite-based instruction
744selectors is that rewrites are always driven by term reduction from
745such a toplevel term, rather than a series of equivalences directly
746from IR instruction to machine instruction forms.
747
748In other words, a conventional instruction selection pattern engine
749might let one specify `(Inst.A ...) -> (Inst.X ...)`. In this
750conventional design, the instruction/opcode type on the LHS and RHS
751must be the same single instruction type (otherwise rewrites could not
752be chained), and rewrite relation (which we wrote as `->`) is in
753essence a single privileged relation. One can see ISLE as a
754generalization: we can define many different types, and many different
755toplevel terms from which we can start the reduction. In principle,
756one could have:
757
758```lisp
759
760    (type IR ...)
761    (type Machine1 ...)
762    (type Machine2 ...)
763
764    (decl TranslateToMachine1 (IR) Machine1)
765    (decl TranslateToMachine2 (IR) Machine2)
766
767    (rule (TranslateToMachine1 (IR.add a b) (Machine1.add a b)))
768    (rule (TranslateToMachine2 (IR.add a b) (Machine2.weird_inst a b)))
769```
770
771and then both translations are available. We are "rewriting" from `IR`
772to `Machine1` and from `IR` to `Machine2`, even if rewrites always
773preserve the same type; we get around the rule by using a constructor.
774
775### Constructors and Extractors
776
777So far, we have spoken of terms and constructors: a term is a schema
778for data, like `(A arg1 arg2)`, while we have used the term
779"constructor" to refer to the `A`, like a function. We now refine this
780notion somewhat and define what it means for a term to appear in the
781left-hand (pattern) or right-hand (expression) side of a rule.
782
783More precisely, a term, like `A`, can have three kinds of behavior
784associated with it: it can be an enum type variant, it can be a
785constructor, or it can be an *extractor*, which we will define in a
786moment. A term can be both an extractor and constructor
787simultaneously, but the enum type variant case is mutually exclusive
788with the others.
789
790The distinction between a "constructor" and an "extractor" is whether
791a term is being deconstructed (matched on) -- by an extractor -- or
792constructed -- by a constructor.
793
794#### Constructors
795
796Constructor behavior on a term allows it to be invoked in the
797right-hand side of a rule. A term can have either an "external
798constructor" (see below) or an "internal constructor", defined in
799ISLE. Any term `A` that has one or more `(rule (A ...) RHS)` rules in
800the ISLE source implicitly has an internal constructor, and this
801constructor can be invoked from the right-hand side of other rules.
802
803#### Extractors
804
805Extractor behavior on a term allows it to be used in a pattern in the
806left-hand side of a rule. If one considers a constructor to be a
807function that goes from argument values to the complete term, then an
808extractor is a function that takes a complete term and possibly
809matches on it (it is fallible). If it does match, it provides the
810arguments *as results*.
811
812One can see extractors as "programmable match operators". They are a
813generalization of enum-variant deconstruction. Where a traditional
814term-rewriting system operates on a term data-structure that exists in
815memory, and can discover that a pattern `(A x y)` matches a term `A`
816at a particular point in the input, an extractor-based system instead
817sees `A` as an *arbitrary programmable operator* that is invoked
818wherever a pattern-match is attempted, and can return "success" with
819the resulting "fields" as if it were actually an enum variant. For
820more on this topic, see the motivation and description in [RFC 15
821under "programmable matching on virtual
822nodes"](https://github.com/bytecodealliance/rfcs/blob/main/accepted/cranelift-isel-isle-peepmatic.md#extractors-programmable-matching-on-virtual-nodes).
823
824To provide a concrete example, if we have the term declarations
825
826```lisp
827    (decl A (u32 u32) T)
828    (decl B (T) U)
829```
830
831then if we write a rule like
832
833```lisp
834    (rule (B (A x y))
835          (U.Variant1 x y))
836```
837
838then we have used `A` as an *extractor*. When `B` is invoked as a
839constructor with some `T`, this rule uses `A` as an extractor and
840attempts (via whatever programmable matching behavior) to use `A` to
841turn the `T` into two `u32`s, binding `x` and `y`. `A` can succeed or
842fail, just as any other part of a pattern-match can.
843
844Just as for constructors, there are *internal* and *external*
845extractors. Most of the interesting programmable behavior occurs in
846external extractors, which are defined in Rust; we will discuss this
847further in a section below. Internal extractors, in contrast, behave
848like macros, and can be defined for convenience: for example, we can
849write
850
851```lisp
852    (decl A (u32 u32) T)
853    (extractor (A pat1 pat2)
854               (and
855                 (extractArg1 pat1)
856                 (extractArg2 pat2)))
857```
858
859which will, for example, expand a pattern `(A (subterm ...) _)` into
860`(and (extractArg1 (subterm ...)) (extractArg2 _))`: in other words,
861the arguments to `A` are substituted into the extractor body and then
862this body is inlined.
863
864#### Implicit Type Conversions
865
866For convenience, ISLE allows the program to associate terms with pairs
867of types, so that type mismatches are *automatically resolved* by
868inserting that term.
869
870For example, if one is writing a rule such as
871
872```lisp
873    (decl u_to_v (U) V)
874    (rule ...)
875
876    (decl MyTerm (T) V)
877    (rule (MyTerm t)
878          (u_to_v t))
879```
880
881the `(u_to_v t)` term would not typecheck given the ISLE language
882functionality that we have seen so far, because it expects a `U` for
883its argument but `t` has type `T`. However, if we define
884
885
886```lisp
887    (convert T U t_to_u)
888
889    ;; For the above to be valid, `t_to_u` should be declared with the
890    ;; signature:
891    (decl t_to_u (T) U)
892    (rule ...)
893```
894
895then the DSL compiler will implicitly understand the above `MyTerm` rule as:
896
897```lisp
898    (rule (MyTerm t)
899          (u_to_v (t_to_u t)))
900```
901
902This also works in the extractor position: for example, if one writes
903
904```lisp
905    (decl defining_instruction (Inst) Value)
906    (extern extractor definining_instruction ...)
907
908    (decl iadd (Value Value) Inst)
909
910    (rule (lower (iadd (iadd a b) c))
911          ...))
912
913    (convert Inst Value defining_instruction)
914```
915
916then the `(iadd (iadd a b) c)` form will be implicitly handled like
917`(iadd (defining_instruction (iadd a b)) c)`. Note that the conversion
918insertion needs to have local type context in order to find the right
919converter: so, for example, it cannot infer a target type from a
920pattern where just a variable binding occurs, even if the variable is
921used in some typed context on the right-hand side. Instead, the
922"inner" and "outer" types have to come from explicitly typed terms.
923
924#### Summary: Terms, Constructors, and Extractors
925
926We start with a `term`, which is just a schema for data:
927
928```lisp
929    (decl Term (A B C u32 u32) T)
930```
931
932A term can have:
933
9341. A single internal extractor body, via a toplevel `(extractor ...)`
935   form, OR
936
9372. A single external extractor binding (see next section); AND
938
9393. One or more `(rule (Term ...) ...)` toplevel forms, which together
940   make up an internal constructor definition, OR
941
9424. A single external constructor binding (see next section).
943
944## ISLE to Rust
945
946Now that we have described the core ISLE language, we will document
947how it interacts with Rust code. We consider these interactions to be
948semantically as important as the core language: they are not
949implementation details, but rather, a well-defined interface by which
950ISLE can interface with the outside world (an "FFI" of sorts).
951
952### Mapping to Rust: Constructors, Functions, and Control Flow
953
954ISLE was designed to have a simple, easy-to-understand mapping from
955its language semantics to Rust semantics. This means that the
956execution of ISLE rewriting has a well-defined implementation in
957Rust. The basic principles are:
958
9591. Every term with rules in ISLE becomes a single Rust function. The
960   arguments are the Rust function arguments. The term's "return
961   value" is the Rust function's return value (wrapped in an `Option`
962   because pattern coverage can be incomplete).
963
9642. One rewrite step is one Rust function call.
965
9663. Rewriting is thus eager, and reified through ordinary Rust control
967   flow. When we construct a term that appears on the left-hand side
968   of rules, we do so by calling a function (the "constructor body");
969   and this function *is* the rewrite logic, so the term is rewritten
970   as soon as it exists. The code that embeds the ISLE generated code
971   will kick off execution by calling a top-level "driver"
972   constructor. The body of the constructor will eventually choose one
973   of several rules to apply, and execute code to build the right-hand
974   side expression; this can invoke further constructors for its
975   subparts, kicking off more rewrites, until eventually a value is
976   returned.
977
9784. This design means that "intermediate terms" -- constructed terms
979   that are then further rewritten -- are never actually built as
980   in-memory data-structures. Rather, they exist only as ephemeral
981   stack-frames while the corresponding Rust function executes. This
982   means that there is very little or no performance penalty to
983   factoring code into many sub-rules (subject only to function-call
984   overhead and/or the effectiveness of the Rust inliner).
985
9865. Backtracking -- attempting to match rules, and backing up to follow
987   a different path when a match fails -- exists, but is entirely
988   internal to the generated Rust function for rewriting one
989   term. Once we are rewriting a term, we have committed to that term
990   existing as a rewrite step; we cannot backtrack further. However,
991   backtracking can occur within the delimited scope of this one
992   term's rewrite; we have a phase during which we evaluate left-hand
993   sides, trying to find a matching rule, and once we find one, we
994   commit and start to invoke constructors to build the right-hand
995   side.
996
997   Said another way, the principle is that left-hand sides are
998   fallible, and have no side-effects as they execute; right-hand
999   sides, in contrast, are infallible. This simplifies the control
1000   flow and makes reasoning about side-effects (especially with
1001   respect to external Rust actions) easier.
1002
1003This will become more clear as we look at how Rust interfaces are
1004defined, and how the generated code appears, below.
1005
1006### Extern Constructors and Extractors
1007
1008ISLE programs interact with the surrounding Rust code in which they
1009are embedded by allowing the programmer to define a term to have
1010*external constructors* and an *external extractor*.
1011
1012The design philosophy of ISLE is that while internally it operates as
1013a fairly standard term-rewriting system, on the boundaries the "terms"
1014should be virtual, and defined procedurally rather than reified into
1015data structures, in order to allow for very flexible binding to the
1016embedding application. Thus, when term-rewriting bottoms out on the
1017input side, it just calls "extractors" to match on whatever ultimate
1018input the user provides, and these are fully programmable; and when it
1019bottoms out on the output side, the "term tree" is reified as a tree
1020of Rust function calls rather than plain data.
1021
1022#### Constructors
1023
1024As we defined above, a "constructor" is a term form that appears in an
1025expression and builds its return value from its argument
1026values. During the rewriting process, a constructor that can trigger
1027further rewriting rules results in a Rust function call to the body of
1028the "internal constructor" built from these rules; the term thus never
1029exists except as argument values on the stack. However, ultimately the
1030ISLE code needs to return some result to the outside world, and this
1031result may be built up of many parts; this is where *external
1032constructors* come into play.
1033
1034For any term declared like
1035
1036```lisp
1037    (decl T (A B C) U)
1038```
1039
1040the programmer can declare
1041
1042```lisp
1043    (extern constructor T ctor_func)
1044```
1045
1046which means that there is a Rust function `ctor_func` on the context
1047trait (see below) that can be *invoked* with arguments of type `A`,
1048`B`, `C` (actually borrows `&A`, `&B`, `&C`, for non-primitive types)
1049and returns a `U`.
1050
1051External constructors are infallible: that is, they must succeed, and
1052always return their return type. In contrast, internal constructors
1053are fallible because they are implemented by a list of rules whose
1054patterns may not cover the entire domain. If fallible behavior is
1055needed when invoking external Rust code, that behavior should occur in
1056an extractor (see below) instead: only pattern left-hand sides are
1057meant to be fallible. Note, incidentally, that fallibility in a
1058constructor does not backtrack to try other rules; instead it just
1059trickles the `None` failure all the way up.
1060
1061#### Extractors
1062
1063An *external extractor* is an implementation of matching behavior in
1064left-hand sides (patterns) that is fully programmable to interface
1065with the embedding application. When the generated pattern-matching
1066code is attempting to match a rule, and has a value to match against
1067an extractor pattern defined as an external extractor, it simply calls
1068a Rust function with the value of the term to be deconstructed, and
1069receives an `Option<(arg1, arg2, ...)>` in return. In other words, the
1070external extractor can choose to match or not, and if it does, it
1071provides the values that are in turn matched by sub-patterns.
1072
1073For any term declared like
1074
1075```lisp
1076    (decl T (A B C) U)`
1077```
1078
1079the programmer can declare
1080
1081```lisp
1082    (extern extractor T etor_func)
1083```
1084
1085which means that there is a Rust function `etor_func` on the context
1086trait (see below) that can be *invoked* with an argument of type `&U`,
1087and returns an `Option<(A, B, C)>`.
1088
1089If an extractor returns `None`, then the generated matching code
1090proceeds just as if an enum variant match had failed: it moves on to
1091try the next rule in turn.
1092
1093#### Advanced Extractors: Arg-Polarity
1094
1095There is one shortcoming in the extractor mechanism as defined so far:
1096there is no mechanism to provide additional context or "parameterize"
1097an external extractor; it only receives the term to deconstruct, with
1098no other parameters.  For example, one might wish to write a
1099`(GetNthArg n arg_pattern)` extractor that matches on an instruction,
1100fetches the `n`th "arg" from it, and then returns that as the
1101extracted match result, allowing `arg_pattern` to match against it.
1102
1103Inspired by Prolog, where argument data can flow in both directions
1104during "unification", we implement a limited form of bidirectionality
1105in ISLE for extractor arguments. Specifically, we can declare the
1106"polarity" of the arguments to an extractor so that some of the
1107arguments flow "in" rather than "out". This lets us provide data to
1108the extractor via expressions embedded in the pattern.
1109
1110An example might make this more clear: we can define a term
1111
1112```lisp
1113    (decl GetNthArg (u32 Arg) Inst)
1114```
1115
1116that we wish to use as in the example given above, and then we can define
1117an external extractor
1118
1119```lisp
1120    (extern extractor GetNthArg get_nth_arg (in out))
1121```
1122
1123which indicates that `get_nth_arg()` will take parameters of `&Inst`
1124(the value being extracted) *and* `u32` (the in-arg), and return
1125`Option<(Arg,)>`, i.e., *just* the out-args.
1126
1127In order to use this extractor, to avoid parse ambiguity (i.e., to
1128avoid the need for the parser to know argument polarity while it is
1129still parsing), we require special syntax for the in-argument so that
1130it is parsed as an expression: it must be prepended by `<`. So one
1131might use `GetNthArg` as follows:
1132
1133```lisp
1134    (rule (Lower (and
1135                   (Inst ...)
1136                   (GetNthArg <2 second_arg)))
1137          ...)
1138```
1139
1140This functionality is meant to improve the expressive power of the
1141DSL, but is not intended to be commonly used outside of "binding" or
1142"library" ISLE code. In other words, because it is somewhat
1143non-intuitive, it is best to wrap it within friendlier internal
1144extractors.
1145
1146### Mapping Type Declarations to Rust
1147
1148When we declare a type like
1149
1150```lisp
1151    (decl MyEnum (enum
1152                   (A (x u32) (y u32))
1153                   (B)))
1154```
1155
1156ISLE will generate the Rust type definition
1157
1158```rust
1159#[derive(Clone, Debug)]
1160pub enum MyEnum {
1161    A { x: u32, y: u32, },
1162    B,
1163}
1164```
1165
1166Note that enum variants with no fields take on the brace-less form,
1167while those with fields use the named-struct-field `A { x: ... }`
1168form. If all variants are field-less, then the type will additionally
1169derive `Copy`, `PartialEq`, and `Eq`.
1170
1171If the type is declared as extern (`(decl MyEnum extern (enum ...))`)
1172then the same definition is assumed to exist. Primitives (`(decl u32
1173(primitive u32))`) are assumed to be defined already, and are required
1174to be `Copy`.
1175
1176All imported/extern types are pulled in via `use super::*` at the top
1177of the generated code; thus, these types should exist in (or be
1178re-exported from) the parent module.
1179
1180### Symbolic Constants
1181
1182ISLE allows the user to refer to external constants as follows:
1183
1184```lisp
1185    (extern const $I32 Type)
1186```
1187
1188This allows code to refer to `$I32` whenever a value of type `Type` is
1189needed, in either a pattern (LHS) or an expression (RHS). These
1190constants are pulled in via the same `use super::*` that imports all
1191external types.
1192
1193### Exported Interface: Functions and Context Trait
1194
1195The generated ISLE code provides an interface that is designed to be
1196agnostic to the embedding application. This means that ISLE knows
1197nothing about, e.g., Cranelift or compiler concepts in
1198general. Rather, the generated code provides function entry points
1199with well-defined signatures based on the terms, and imports the
1200extern constructors and extractors via a context trait that the
1201embedder must implement.
1202
1203When a term `T` is defined like
1204
1205```lisp
1206    (decl T (A B C) U)
1207```
1208
1209and has an internal constructor (provided by `rule` bodies), then a
1210function with the following signature will be exported from the
1211generated code:
1212
1213```rust
1214    pub fn constructor_T<C: Context>(ctx: &mut C, arg0: &A, arg1: &B, arg2: &C) -> Option<U>;
1215```
1216
1217In other words, `constructor_` is prepended, and the function takes
1218the expected arguments, along with a "context" (more on this
1219below). It returns an `Option<U>` because internal constructors can be
1220partial: if no rule's pattern matches, then the constructor
1221fails. Note that if a sub-constructor fails, no backtracking occurs;
1222rather, the failure propagates all the way up to the entry point.
1223
1224What is this "context" for? The context argument is used to allow
1225external extractors and constructors to access the necessary state of
1226the embedding application. (For example, in Cranelift, it might be the
1227`LowerCtx` that controls the code-lowering process.)
1228
1229The context is a trait because we want to decouple the generated code
1230from the application as much as possible. The trait will have a method
1231for each defined external extractor and constructor. For example, if
1232we have the following terms and declarations:
1233
1234```lisp
1235    (decl A (u32 u32) T)
1236    (extern constructor A build_a)
1237
1238    (decl B (T) U)
1239    (external extractor B disassemble_b)
1240```
1241
1242then the `Context` trait will include these methods:
1243
1244```rust
1245    trait Context {
1246        fn build_a(&mut self, arg0: u32, arg1: u32) -> T;
1247        fn disassemble_b(&mut self, arg0: &U) -> Option<T>;
1248    }
1249```
1250
1251These functions should be implemented as described above for external
1252constructors and extractors.
1253
1254Note that some external extractors are known to always succeed, for
1255example if they are just fetching some information that is always
1256present; in this case, the generated code can be made slightly more
1257efficient if we tell the ISLE compiler that this is so. By declaring
1258
1259```lisp
1260    (external extractor infallible B disassemble_b)
1261```
1262
1263we eliminate the `Option` on the return type, so the method is instead
1264
1265```rust
1266    trait Context {
1267        // ...
1268        fn disassemble_b(&mut self, arg0: &U) -> T;
1269    }
1270```
1271
1272## ISLE Internals
1273
1274### Compiler Stages
1275
1276Some detail and pointers to the compilation stages can be found in the
1277[README](../isle/README.md). The sequence starts as any ordinary
1278compiler: lexing, parsing, semantic analysis, and generation of an
1279IR. The most unique part is the "decision trie generation", which is
1280what converts the unordered-list-of-rule representation into something
1281that corresponds to the final Rust code's control flow and order of
1282matching operations.
1283
1284We describe this data structure below with the intent to provide an
1285understanding of how the DSL compiler weaves rules together into Rust
1286control flow. While this understanding shouldn't be strictly necessary
1287to use the DSL, it may be helpful. (The ultimate answer to "how does
1288the generated code work" is, of course, found by reading the generated
1289code; some care has been taken to ensure it is reasonably legible for
1290human consumption!)
1291
1292### Decision Trie
1293
1294The heart of the ISLE transformation lies in how the compiler converts
1295a list of rules into a scheme to attempt to match rules in some order,
1296possibly sharing match operations between similar rules to reduce
1297work.
1298
1299The core data structure we produce is a "decision trie" per internal
1300constructor body. This is an intermediate representation of sorts that
1301is built from individual-rule IR (LHS + RHS) sequences, and is then
1302used to generate Rust source.
1303
1304The decision trie is, as the name implies, a kind of decision tree, in
1305the sense that we start at the root and move down the tree based on
1306the result of match operations (each feeding one "decision").
1307
1308It is a "trie" (which is a kind of tree) because at each level, its
1309edges are labeled with match operations; a trie is a tree where one
1310input character from an alphabet is used to index children at each
1311level.
1312
1313Each node in the tree is either an internal decision node, or a leaf
1314"expression" node (which we reach once we have a successful rule
1315match). The "execution semantics" of the trie are
1316backtracking-based. We attempt to find some path down the tree through
1317edges whose match ops run successfully; when we do this to reach a
1318leaf, we have the values generated by all of the match ops, and we can
1319execute the sequence of "expression instructions" in the leaf. Each
1320rule's left-hand side becomes a series of edges (merged into the
1321existing tree as we process rules) and each rule's right-hand side
1322becomes one leaf node with expression instructions.
1323
1324At any point, if a match op does not succeed, we try the next out-edge
1325in sequence. If we have tried all out-edges from a decision node and
1326none were successful, then we backtrack one level further. Thus, we
1327simply perform an in-order tree traversal and find the first
1328successful match.
1329
1330Though this sounds possibly very inefficient if some decision node has
1331a high fan-out, in practice it is not because the edges are often
1332known to be *mutually exclusive*. The canonical example of this is
1333when an enum-typed value is destructured into different variants by
1334various edges; we can use a Rust `match` statement in the generated
1335source and have `O(1)` (or close to it) cost for the dispatch at this
1336level.[^8]
1337
1338Building the trie is a somewhat subtle procedure; see [this block
1339comment](https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/isle/isle/src/trie.rs#L15-L166)
1340for more information regarding the trie construction algorithm.
1341
1342[^8]: The worst-case complexity for a single term rewriting operation
1343      is still the cost of evaluating each rule's left-hand side
1344      sequentially, because in general there is no guarantee of
1345      overlap between the patterns. Ordering of the edges out of a
1346      decision node also affects complexity: if mutually-exclusive
1347      match operations are not adjacent, then they cannot be merged
1348      into a single `match` with `O(1)` dispatch. In general this
1349      ordering problem is quite difficult. We could do better with
1350      stronger heuristics; this is an open area for improvement in the
1351      DSL compiler!
1352
1353## Reference: ISLE Language Grammar
1354
1355Baseline: allow arbitrary whitespace, and Lisp-style comments (`;` to
1356newline). The grammar accepted by the parser is as follows:
1357
1358```bnf
1359
1360<ISLE> ::= <def>*
1361
1362<def> ::= "(" "type" <typedecl> ")"
1363        | "(" "decl" <decl> ")"
1364        | "(" "rule" <rule> ")"
1365        | "(" "extractor" <etor> ")"
1366        | "(" "extern" <extern> ")"
1367
1368<typedecl> ::= <ident> [ "extern" ] <typevalue>
1369
1370<ident> ::= ( "A".."Z" | "a".."z" | "_" | "$" )
1371            ( "A".."Z" | "a".."z" | "_" | "$" | "0".."9" | "." )*
1372<const-ident> ::= "$" ( "A".."Z" | "a".."z" | "_" | "$" | "0".."9" | "." )*
1373
1374<int> ::= [ "-" ] ( "0".."9" )+
1375        | [ "-" ] "0x" ( "0".."9" "A".."F" "a".."f" )+
1376
1377<typevalue> ::= "(" "primitive" <ident> ")"
1378              | "(" "enum" <enumvariant>* ")"
1379
1380<enumvariant> ::= <ident>
1381                | "(" <ident> <enumfield>* ")"
1382
1383<enumfield> ::= "(" <ident> <ty> ")"
1384
1385<ty> ::= <ident>
1386
1387<decl> ::= <ident> "(" <ty>* ")" <ty>
1388
1389<rule> ::= <pattern> <expr>
1390         | <prio> <pattern> <expr>
1391
1392<prio> ::= <int>
1393
1394<etor> ::= "(" <ident> <ident>* ")" <pattern>
1395
1396<pattern> ::= <int>
1397            | <const-ident>
1398            | "_"
1399            | <ident>
1400            | <ident> "@" <pattern>
1401            | "(" "and" <pattern>* ")"
1402            | "(" <ident> <pattern-arg>* ")"
1403
1404<pattern-arg> ::= <pattern>
1405                | "<" <expr>  ;; in-argument to an extractor
1406
1407<expr> ::= <int>
1408         | <const-ident>
1409         | "#t"               ;; Scheme-like "true": shorthand for 1
1410         | "#f"               ;; Scheme-like "false": shorthand for 0
1411         | <ident>
1412         | "(" "let" "(" <let-binding>* ")" <expr> ")"
1413         | "(" <ident> <expr>* ")"
1414
1415<let-binding> ::= "(" <ident> <ty> <expr> ")"
1416
1417<extern> ::= "constructor" <ident> <ident>
1418           | "extractor" [ "infallible" ] <ident> <ident>
1419             [ "(" <polarity>* ")" ]
1420           | "const" <const-ident> <ident> <ty>
1421
1422<polarity> ::= "in" | "out"
1423
1424```
1425