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