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](https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/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 second 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", "instruction 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/binary/octal, positive/negative: `1`, `-1`, 498 `0x80`, `-0x80`). Hex constants can start with either `0x` or `0X`. 499 Binary constants start with `0b`. Octal constants start with `0o`. 500 Integers can also be interspersed with `_` as a separator, for example 501 `1_000` or `0x1234_5678`, for readability. 502* constants imported from the embedding, of arbitrary type 503 (`$MyConst`). 504* Variable captures and matches (bare identifiers like `x`; an 505 identifier consists of alphanumeric characters and underscores, and 506 does not start with a digit). The first occurrence of a variable `x` 507 captures the value; each subsequent occurrence matches on the 508 already-captured value, rejecting the match if not equal. 509* Variable captures with sub-patterns: `x @ PAT`, which captures the 510 subterm in `x` as above but also matches `PAT` against the 511 subterm. For example, `x @ (A y z)` matches an `A` term and captures 512 its arguments as `y` and `z`, but also captures the whole term as 513 `x`. 514* conjunctions of subpatterns: `(and PAT1 PAT2 ...)` matches all of 515 the subpatterns against the term. If any subpattern does not match, 516 then this matcher fails. 517* Term deconstruction: `(term PAT1 PAT2 ...)`, where `term` is a 518 defined term (type variant or constructor) and the subpatterns are 519 applied to each argument value in turn. Note that `term` cannot be a 520 wildcard; it must be a specific, concrete term. 521 522The expression (right-hand side) is made up of the following 523expression operators: 524 525* Integer and symbolic constants, as above. 526* Variable uses (bare `x` identifier). 527* Term constructors (`(term EXPR1 EXPR2 ...)`, where each 528 subexpression is evaluated first and then the term is constructed). 529* `let`-expressions that bind new variables, possibly using the values 530 multiple times within the body: `(let ((var1 type1 EXPR1) (var2 ...) 531 ...) BODY ...)`. Each variable's initialization expression can refer 532 to the immediately previous variable bindings (i.e., this is like a 533 `let*` in Scheme). `let`s are lexically-scoped, meaning that bound 534 variables are available only within the body of the `let`. 535 536When multiple rules are applicable to rewrite a particular term, ISLE 537will choose the "more specific" rule according to a particular 538heuristic: in the lowered sequence of matching steps, when one 539left-hand side completes a match while another with the same prefix 540continues with further steps, the latter (more specific) is chosen. 541 542The more-specific-first heuristic is usually good enough, but when an 543undesirable choice occurs, explicit priorities can be specified. 544Rules with explicit priorities are written as `(rule PRIO lhs rhs)` 545where `PRIO` is a signed (positive or negative) integer. An applicable 546rule with a higher priority will always match before a rule with a 547lower priority. The default priority for all rules if not otherwise 548specified is `0`. 549 550Note that the system allows multiple applicable rules to exist with 551the same priority: that is, while the priority system allows for 552manual tie-breaking, this tie-breaking is not required. 553 554Finally, one important note: the priority system is considered part of 555the core language semantics and execution of rules with different 556priorities is well-defined, so can be relied upon when specifying 557correct rules. However, the tie-breaking heuristic is *not* part of 558the specified language semantics, and so the user should never write 559rules whose correctness depends on one rule overriding another 560according to the heuristic. 561 562### Typed Terms 563 564ISLE allows the programmer to define types, and requires every term to 565have *parameter types* and a *return type* (analogous to first-order 566functions). 567 568The universe of types is very simple: there are *primitives*, which 569can be integers or symbolic constants (imported from the Rust 570embedding), and *enums*, which correspond directly to Rust enums with 571variants that have named fields. There is no subtyping. Some examples 572of type definitions are: 573 574```lisp 575 576 (type u32 (primitive u32)) ;; u32 is a primitive, and is 577 ;; spelled `u32` in the generated Rust code. 578 579 (type MyType (enum 580 (A (x u32) (y u32)) 581 (B (z u32)) 582 (C))) ;; MyType is an enum, with variants 583 ;; `MyType::A { x, y }`, `MyType::B { z }`, 584 ;; and `MyType::C`. 585 586 (type MyType2 extern (enum (A))) 587 ;; MyType2 is an enum with variant `MyType2::A`. 588 ;; Its type definition is not included in the 589 ;; generated Rust, but rather, assumed to exist 590 ;; in surrounding code. Useful for binding to 591 ;; existing definitions. 592``` 593 594We then declare constructors with their parameter and return types as 595follows: 596 597```lisp 598 599 (decl Term1 (u32 u32) MyType) ;; Term1 has two `u32`-typed parameters, 600 ;; and itself has type `MyType`. 601 (decl Term2 () u32) ;; Term2 has no parameters and type `u32`. 602``` 603 604Note that when an enum type is defined, its variants are implicitly 605defined as constructors as well. These constructors are namespaced 606under the name of the type, to avoid ambiguity (or the need to do 607type-dependent lookups in the compiler, which can complicate type 608inference). For example, given the above `MyType` definitions, we 609automatically have the following constructors: 610 611```lisp 612 613 ;; These definitions are implicit and do not need to be written (doing 614 ;; so is a compile-time error, actually). We write them here just to 615 ;; show what they would look like. 616 617 (decl MyType.A (u32 u32) MyType) 618 (decl MyType.B (u32) MyType) 619 (decl MyType.C () MyType) 620 621 (decl MyType2.A () MyType2) 622``` 623 624### Why Types? 625 626For terms that are not enum variants, the notion that a term "has a 627type" is somewhat foreign to a classical term-rewriting system. In 628most formal symbolic systems, the terms are manipulated as opaque 629sequences or trees of symbols; they have no inherent meaning other 630than what the user implicitly defines with the given rules. What does 631it mean for a term to "have a type" when it is just data? Or, said 632another way: why isn't the type of `Term2` just `Term2`? 633 634The types come into play when we define *rules*: one term of type `T` 635can only be rewritten into another term of type `T`, and when a 636parameter has a certain type, only subterms with that type can 637appear. Without explicit types on terms and their parameters, any term 638could be rewritten to any other, or substituted in as a parameter, and 639there is thus a kind of dynamic typing about which the programmer must 640have some caution. In most applications of a term-rewriting system, 641there is already some de-facto "schema": some parameter of a term 642representing a machine instruction can only take on one of a few 643subterms (representing, say, different addressing modes). ISLE's types 644just make this explicit. 645 646Thus, the first answer to "why types" is that they enforce a schema on 647the terms, allowing the programmer to have stronger well-formed-data 648invariants. 649 650The second reason is that the types are an integral part of the 651compilation-to-Rust strategy: every constructor actually does evaluate 652to a Rust value of the given "return value" type, given actual Rust 653values for its parameters of the appropriate parameter types. We will 654see more on this below. 655 656### Well-Typed Rules and Type Inference 657 658Now that we understand how to define types, let's examine in more 659detail how they are used to verify that the pattern and rewrite 660expression of a rule have the same type. 661 662ISLE uses a simple unidirectional type-inference algorithm that 663propagates type information through the pattern, resulting in a "type 664environment" that specifies the type for each captured variable, and 665then uses this to typecheck the rewrite expression. The result of this 666is that types are almost completely inferred, and are only annotated 667in a few places (`let` bindings specifically). 668 669The typing rules for patterns in ISLE are: 670 671* At the root of the pattern, we require that a *constructor* pattern 672 is used, rather than some other match operation (a wildcard, integer 673 constant, etc.). This is because compilation and dispatch into rules 674 is organized by the top-level constructor of the term being 675 rewritten. 676 677* At each part of the pattern except the root, there is an "expected 678 type" that is inferred from the surrounding context. We check that 679 this matches the actual type of the pattern. 680 681* A constructor pattern `(C x y z)`, given a constructor `(decl C (T1 682 T2 T2) R)`, has type `R` and provides expected types `T1`, `T2`, and 683 `T3` to its subpatterns. 684 685* A variable capture pattern `x` is compatible with any expected type 686 the first time it appears, and captures this expected type under the 687 variable identifier `x` in the type environment. Subsequent 688 appearances of `x` check that the expected type matches the 689 already-captured type. 690 691* A conjunction `(and PAT1 PAT2 ...)` checks that each subpattern is 692 compatible with the expected type. 693 694* Integer constants are compatible with any primitive expected 695 type. (This may change in the future if we add non-numeric 696 primitives, such as strings.) 697 698If we are able to typecheck the pattern, we have a type environment 699that is a map from variable bindings to types: e.g., `{ x: MyType, y: 700MyType2, z: u32 }`. We then typecheck the rewrite expression. 701 702* Every expression also has an expected type, from the surrounding 703 context. We check that the provided expression matches this type. 704 705* The top-level rewrite expression must have the same type as the 706 top-level constructor in the pattern. (In other words, a term can 707 only be rewritten to another term of the same type.) 708 709* Constructors check their return values against the expected type, 710 and typecheck their argument expressions against their parameter 711 types. 712 713* A `let` expression provides types for additional variable bindings; 714 these are added to the type environment while typechecking the 715 body. The expected type for the body is the same as the expected 716 type for the `let` itself. 717 718### A Note on Heterogeneous Types 719 720We should illuminate one particular aspect of the ISLE type system 721that we described above. We have said that a term must be rewritten to 722another term of the same type. Note that this does *not* mean that, 723for example, a set of ISLE rules cannot be used to translate a term of 724type `T1` to a term of type `T2`. The trick is to define a top-level 725"driver" that wraps the `T1`, such that reducing this term results in 726a `T2`. Concretely: 727 728```lisp 729 (type T1 ...) 730 (type T2 ...) 731 732 (decl Translate (T1) T2) 733 734 (rule (Translate (T1.A ...)) 735 (T2.X ...)) 736 (rule (Translate (T1.B ...)) 737 (T2.Y ...)) 738``` 739 740This gets to the heart of rewrite-system-based computation, and has 741relevance for applications of ISLE to compiler backends. A common 742technique in rewrite systems is to "kick off" a computation by 743wrapping a term in some intermediate term that then drives a series of 744reductions. Here we are using `Translate` as this top-level term. A 745difference between ISLE and some other rewrite-based instruction 746selectors is that rewrites are always driven by term reduction from 747such a toplevel term, rather than a series of equivalences directly 748from IR instruction to machine instruction forms. 749 750In other words, a conventional instruction selection pattern engine 751might let one specify `(Inst.A ...) -> (Inst.X ...)`. In this 752conventional design, the instruction/opcode type on the LHS and RHS 753must be the same single instruction type (otherwise rewrites could not 754be chained), and rewrite relation (which we wrote as `->`) is in 755essence a single privileged relation. One can see ISLE as a 756generalization: we can define many different types, and many different 757toplevel terms from which we can start the reduction. In principle, 758one could have: 759 760```lisp 761 762 (type IR ...) 763 (type Machine1 ...) 764 (type Machine2 ...) 765 766 (decl TranslateToMachine1 (IR) Machine1) 767 (decl TranslateToMachine2 (IR) Machine2) 768 769 (rule (TranslateToMachine1 (IR.add a b)) (Machine1.add a b)) 770 (rule (TranslateToMachine2 (IR.add a b)) (Machine2.weird_inst a b)) 771``` 772 773and then both translations are available. We are "rewriting" from `IR` 774to `Machine1` and from `IR` to `Machine2`, even if rewrites always 775preserve the same type; we get around the rule by using a constructor. 776 777### Constructors and Extractors 778 779So far, we have spoken of terms and constructors: a term is a schema 780for data, like `(A arg1 arg2)`, while we have used the term 781"constructor" to refer to the `A`, like a function. We now refine this 782notion somewhat and define what it means for a term to appear in the 783left-hand (pattern) or right-hand (expression) side of a rule. 784 785More precisely, a term, like `A`, can have three kinds of behavior 786associated with it: it can be an enum type variant, it can be a 787constructor, or it can be an *extractor*, which we will define in a 788moment. A term can be both an extractor and constructor 789simultaneously, but the enum type variant case is mutually exclusive 790with the others. 791 792The distinction between a "constructor" and an "extractor" is whether 793a term is being deconstructed (matched on) -- by an extractor -- or 794constructed -- by a constructor. 795 796#### Constructors 797 798Constructor behavior on a term allows it to be invoked in the 799right-hand side of a rule. A term can have either an "external 800constructor" (see below) or an "internal constructor", defined in 801ISLE. Any term `A` that has one or more `(rule (A ...) RHS)` rules in 802the ISLE source implicitly has an internal constructor, and this 803constructor can be invoked from the right-hand side of other rules. 804 805#### Extractors 806 807Extractor behavior on a term allows it to be used in a pattern in the 808left-hand side of a rule. If one considers a constructor to be a 809function that goes from argument values to the complete term, then an 810extractor is a function that takes a complete term and possibly 811matches on it (it is fallible). If it does match, it provides the 812arguments *as results*. 813 814One can see extractors as "programmable match operators". They are a 815generalization of enum-variant deconstruction. Where a traditional 816term-rewriting system operates on a term data-structure that exists in 817memory, and can discover that a pattern `(A x y)` matches a term `A` 818at a particular point in the input, an extractor-based system instead 819sees `A` as an *arbitrary programmable operator* that is invoked 820wherever a pattern-match is attempted, and can return "success" with 821the resulting "fields" as if it were actually an enum variant. For 822more on this topic, see the motivation and description in [RFC 15 823under "programmable matching on virtual 824nodes"](https://github.com/bytecodealliance/rfcs/blob/main/accepted/cranelift-isel-isle-peepmatic.md#extractors-programmable-matching-on-virtual-nodes). 825 826To provide a concrete example, if we have the term declarations 827 828```lisp 829 (decl A (u32 u32) T) 830 (decl B (T) U) 831``` 832 833then if we write a rule like 834 835```lisp 836 (rule (B (A x y)) 837 (U.Variant1 x y)) 838``` 839 840then we have used `A` as an *extractor*. When `B` is invoked as a 841constructor with some `T`, this rule uses `A` as an extractor and 842attempts (via whatever programmable matching behavior) to use `A` to 843turn the `T` into two `u32`s, binding `x` and `y`. `A` can succeed or 844fail, just as any other part of a pattern-match can. 845 846Just as for constructors, there are *internal* and *external* 847extractors. Most of the interesting programmable behavior occurs in 848external extractors, which are defined in Rust; we will discuss this 849further in a section below. Internal extractors, in contrast, behave 850like macros, and can be defined for convenience: for example, we can 851write 852 853```lisp 854 (decl A (u32 u32) T) 855 (extractor (A pat1 pat2) 856 (and 857 (extractArg1 pat1) 858 (extractArg2 pat2))) 859``` 860 861which will, for example, expand a pattern `(A (subterm ...) _)` into 862`(and (extractArg1 (subterm ...)) (extractArg2 _))`: in other words, 863the arguments to `A` are substituted into the extractor body and then 864this body is inlined. 865 866#### Implicit Type Conversions 867 868For convenience, ISLE allows the program to associate terms with pairs 869of types, so that type mismatches are *automatically resolved* by 870inserting that term. 871 872For example, if one is writing a rule such as 873 874```lisp 875 (decl u_to_v (U) V) 876 (rule ...) 877 878 (decl MyTerm (T) V) 879 (rule (MyTerm t) 880 (u_to_v t)) 881``` 882 883the `(u_to_v t)` term would not typecheck given the ISLE language 884functionality that we have seen so far, because it expects a `U` for 885its argument but `t` has type `T`. However, if we define 886 887 888```lisp 889 (convert T U t_to_u) 890 891 ;; For the above to be valid, `t_to_u` should be declared with the 892 ;; signature: 893 (decl t_to_u (T) U) 894 (rule ...) 895``` 896 897then the DSL compiler will implicitly understand the above `MyTerm` rule as: 898 899```lisp 900 (rule (MyTerm t) 901 (u_to_v (t_to_u t))) 902``` 903 904This also works in the extractor position: for example, if one writes 905 906```lisp 907 (decl defining_instruction (Inst) Value) 908 (extern extractor defining_instruction ...) 909 910 (decl iadd (Value Value) Inst) 911 912 (rule (lower (iadd (iadd a b) c)) 913 ...) 914 915 (convert Inst Value defining_instruction) 916``` 917 918then the `(iadd (iadd a b) c)` form will be implicitly handled like 919`(iadd (defining_instruction (iadd a b)) c)`. Note that the conversion 920insertion needs to have local type context in order to find the right 921converter: so, for example, it cannot infer a target type from a 922pattern where just a variable binding occurs, even if the variable is 923used in some typed context on the right-hand side. Instead, the 924"inner" and "outer" types have to come from explicitly typed terms. 925 926#### Summary: Terms, Constructors, and Extractors 927 928We start with a `term`, which is just a schema for data: 929 930```lisp 931 (decl Term (A B C u32 u32) T) 932``` 933 934A term can have: 935 9361. A single internal extractor body, via a toplevel `(extractor ...)` 937 form, OR 938 9392. A single external extractor binding (see next section); AND 940 9413. One or more `(rule (Term ...) ...)` toplevel forms, which together 942 make up an internal constructor definition, OR 943 9444. A single external constructor binding (see next section). 945 946### If-Let Clauses 947 948As an extension to the basic left-hand-side / right-hand-side rule 949idiom, ISLE allows *if-let clauses* to be used. These add additional 950pattern-matching steps, and can be used to perform additional tests 951and also to use constructors in place of extractors during the match 952phase when this is more convenient. 953 954To introduce the concept, an example follows (this is taken from the 955[RFC](https://github.com/bytecodealliance/rfcs/tree/main/accepted/isle-extended-patterns.md) 956that proposed if-lets): 957 958```lisp 959;; `u32_fallible_add` can now be used in patterns in `if-let` clauses 960(decl pure u32_fallible_add (u32 u32) u32) 961(extern constructor u32_fallible_add u32_fallible_add) 962 963(rule (lower (load (iadd addr 964 (iadd (uextend (iconst k1)) 965 (uextend (iconst k2)))))) 966 (if-let k (u32_fallible_add k1 k2)) 967 (isa_load (amode_reg_offset addr k))) 968``` 969 970The key idea is that we allow a `rule` form to contain the following 971sub-forms: 972 973```lisp 974(rule LHS_PATTERN 975 (if-let PAT2 EXPR2) 976 (if-let PAT3 EXPR3) 977 ... 978 RHS) 979``` 980 981The matching proceeds as follows: the main pattern (`LHS_PATTERN`) 982matches against the input value (the term to be rewritten), as 983described in detail above. Then, if this matches, execution proceeds 984to the if-let clauses in the order they are specified. For each, we 985evaluate the expression (`EXPR2` or `EXPR3` above) first. An 986expression in an if-let context is allowed to be "fallible": the 987constructors return `Option<T>` at the Rust level and can return 988`None`, in which case the whole rule application fails and we move on 989to the next rule as if the main pattern had failed to match. (More on 990the fallible constructors below.) If the expression evaluation 991succeeds, we match the associated pattern (`PAT2` or `PAT3` above) 992against the resulting value. This too can fail, causing the whole rule 993to fail. If it succeeds, any resulting variable bindings are 994available. Variables bound in the main pattern are available for all 995if-let expressions and patterns, and variables bound by a given if-let 996clause are available for all subsequent clauses. All bound variables 997(from the main pattern and if-let clauses) are available in the 998right-hand side expression. 999 1000#### Pure Expressions and Constructors 1001 1002In order for an expression to be used in an if-let clause, it has to 1003be *pure*: it cannot have side-effects. A pure expression is one that 1004uses constants and pure constructors only. Enum variant constructors 1005are always pure. In general constructors that invoke function calls, 1006however (either as internal or external constructor calls), can lead 1007to arbitrary Rust code and have side-effects. So, we add a new 1008annotation to declarations as follows: 1009 1010```lisp 1011;; `u32_fallible_add` can now be used in patterns in `if-let` clauses 1012(decl pure u32_fallible_add (u32 u32) u32) 1013 1014;; This adds a method 1015;; `fn u32_fallible_add(&mut self, _: u32, _: u32) -> Option<u32>` 1016;; to the `Context` trait. 1017(extern constructor u32_fallible_add u32_fallible_add) 1018``` 1019 1020The `pure` keyword here is a declaration that the term, when used as a 1021constructor, has no side-effects. Declaring an external constructor on 1022a pure term is a promise by the ISLE programmer that the external Rust 1023function we are naming (here `u32_fallible_add`) has no side-effects 1024and is thus safe to invoke during the match phase of a rule, when we 1025have not committed to a given rule yet. 1026 1027When an internal constructor body is generated for a term that is pure 1028(i.e., if we had `(rule (u32_fallible_add x y) ...)` in our program 1029after the above declaration instead of the `extern`), the right-hand 1030side expression of each rule that rewrites the term is also checked 1031for purity. 1032 1033#### `partial` Expressions 1034 1035ISLE's `partial` keyword on a term indicates that the term's 1036constructors may fail to match, otherwise, the ISLE compiler assumes 1037the term's constructors are infallible. 1038 1039For example, the following term's constructor only matches if the value 1040is zero: 1041 1042``` 1043;; Match any zero value. 1044(decl pure partial is_zero_value (Value) Value) 1045(extern constructor is_zero_value is_zero_value) 1046``` 1047 1048Internal constructors without the `partial` keyword can 1049only use other constructors that also do not have the `partial` keyword. 1050 1051#### `if` Shorthand 1052 1053It is a fairly common idiom that if-let clauses are used as predicates 1054on rules, such that their only purpose is to allow a rule to match, 1055and not to perform any destructuring with a sub-pattern. For example, 1056one might want to write: 1057 1058```lisp 1059(rule (lower (special_inst ...)) 1060 (if-let _ (isa_extension_enabled)) 1061 (isa_special_inst ...)) 1062``` 1063 1064where `isa_extension_enabled` is a pure constructor that is fallible, 1065and succeeds only when a condition is true. 1066 1067To enable more succinct expression of this idiom, we allow the 1068following shorthand notation using `if` instead: 1069 1070```lisp 1071(rule (lower (special_inst ...)) 1072 (if (isa_extension_enabled)) 1073 (isa_special_inst ...)) 1074``` 1075 1076#### Recursion 1077 1078ISLE terms may be recursive: a rewrite rule's RHS can reference the term it 1079matches on, either directly or via a reference cycle. However, recursive terms 1080present a risk of potentially unbounded term rewriting. In the compilation 1081context, it is possible that certain recursive rules could be exploited to 1082induce a stack overflow with a malicious input program. Therefore, ISLE 1083disallows recursion by default. 1084 1085Recursion can still be justified when it can be shown to be bounded, therefore 1086ISLE allows certain terms to opt-in to recursive definitions. To permit 1087recursive references in a term's rules, declare the term with the `rec` 1088attribute: `(decl rec A ...)`. In the case of a reference cycle, all terms in 1089the cycle must have the `rec` attribute. When using the `rec` attribute, 1090developers should provide a `; Recursion: ...` comment explaining why this use 1091is bounded. 1092 1093 1094## ISLE to Rust 1095 1096Now that we have described the core ISLE language, we will document 1097how it interacts with Rust code. We consider these interactions to be 1098semantically as important as the core language: they are not 1099implementation details, but rather, a well-defined interface by which 1100ISLE can interface with the outside world (an "FFI" of sorts). 1101 1102### Mapping to Rust: Constructors, Functions, and Control Flow 1103 1104ISLE was designed to have a simple, easy-to-understand mapping from 1105its language semantics to Rust semantics. This means that the 1106execution of ISLE rewriting has a well-defined implementation in 1107Rust. The basic principles are: 1108 11091. Every term with rules in ISLE becomes a single Rust function. The 1110 arguments are the Rust function arguments. The term's "return 1111 value" is the Rust function's return value (wrapped in an `Option` 1112 because pattern coverage can be incomplete). 1113 11142. One rewrite step is one Rust function call. 1115 11163. Rewriting is thus eager, and reified through ordinary Rust control 1117 flow. When we construct a term that appears on the left-hand side 1118 of rules, we do so by calling a function (the "constructor body"); 1119 and this function *is* the rewrite logic, so the term is rewritten 1120 as soon as it exists. The code that embeds the ISLE generated code 1121 will kick off execution by calling a top-level "driver" 1122 constructor. The body of the constructor will eventually choose one 1123 of several rules to apply, and execute code to build the right-hand 1124 side expression; this can invoke further constructors for its 1125 subparts, kicking off more rewrites, until eventually a value is 1126 returned. 1127 11284. This design means that "intermediate terms" -- constructed terms 1129 that are then further rewritten -- are never actually built as 1130 in-memory data-structures. Rather, they exist only as ephemeral 1131 stack-frames while the corresponding Rust function executes. This 1132 means that there is very little or no performance penalty to 1133 factoring code into many sub-rules (subject only to function-call 1134 overhead and/or the effectiveness of the Rust inliner). 1135 11365. Backtracking -- attempting to match rules, and backing up to follow 1137 a different path when a match fails -- exists, but is entirely 1138 internal to the generated Rust function for rewriting one 1139 term. Once we are rewriting a term, we have committed to that term 1140 existing as a rewrite step; we cannot backtrack further. However, 1141 backtracking can occur within the delimited scope of this one 1142 term's rewrite; we have a phase during which we evaluate left-hand 1143 sides, trying to find a matching rule, and once we find one, we 1144 commit and start to invoke constructors to build the right-hand 1145 side. 1146 1147 Said another way, the principle is that left-hand sides can be 1148 fallible, and have no side-effects as they execute; right-hand 1149 sides, in contrast, are infallible. This simplifies the control 1150 flow and makes reasoning about side-effects (especially with 1151 respect to external Rust actions) easier. 1152 1153This will become more clear as we look at how Rust interfaces are 1154defined, and how the generated code appears, below. 1155 1156### Extern Constructors and Extractors 1157 1158ISLE programs interact with the surrounding Rust code in which they 1159are embedded by allowing the programmer to define a term to have 1160*external constructors* and an *external extractor*. 1161 1162The design philosophy of ISLE is that while internally it operates as 1163a fairly standard term-rewriting system, on the boundaries the "terms" 1164should be virtual, and defined procedurally rather than reified into 1165data structures, in order to allow for very flexible binding to the 1166embedding application. Thus, when term-rewriting bottoms out on the 1167input side, it just calls "extractors" to match on whatever ultimate 1168input the user provides, and these are fully programmable; and when it 1169bottoms out on the output side, the "term tree" is reified as a tree 1170of Rust function calls rather than plain data. 1171 1172#### Constructors 1173 1174As we defined above, a "constructor" is a term form that appears in an 1175expression and builds its return value from its argument 1176values. During the rewriting process, a constructor that can trigger 1177further rewriting rules results in a Rust function call to the body of 1178the "internal constructor" built from these rules; the term thus never 1179exists except as argument values on the stack. However, ultimately the 1180ISLE code needs to return some result to the outside world, and this 1181result may be built up of many parts; this is where *external 1182constructors* come into play. 1183 1184For any term declared like 1185 1186```lisp 1187 (decl T (A B C) U) 1188``` 1189 1190the programmer can declare 1191 1192```lisp 1193 (extern constructor T ctor_func) 1194``` 1195 1196which means that there is a Rust function `ctor_func` on the context 1197trait (see below) that can be *invoked* with arguments of type `A`, 1198`B`, `C` (actually borrows `&A`, `&B`, `&C`, for non-primitive types) 1199and returns a `U`. 1200 1201External constructors are infallible: that is, they must succeed, and 1202always return their return type. In contrast, internal constructors 1203can be fallible because they are implemented by a list of rules whose 1204patterns may not cover the entire domain (in which case, the term 1205should be marked `partial`). If fallible behavior is needed when 1206invoking external Rust code, that behavior should occur in an extractor 1207(see below) instead: only pattern left-hand sides are meant to be 1208fallible. 1209 1210#### Extractors 1211 1212An *external extractor* is an implementation of matching behavior in 1213left-hand sides (patterns) that is fully programmable to interface 1214with the embedding application. When the generated pattern-matching 1215code is attempting to match a rule, and has a value to match against 1216an extractor pattern defined as an external extractor, it simply calls 1217a Rust function with the value of the term to be deconstructed, and 1218receives an `Option<(arg1, arg2, ...)>` in return. In other words, the 1219external extractor can choose to match or not, and if it does, it 1220provides the values that are in turn matched by sub-patterns. 1221 1222For any term declared like 1223 1224```lisp 1225 (decl T (A B C) U)` 1226``` 1227 1228the programmer can declare 1229 1230```lisp 1231 (extern extractor T etor_func) 1232``` 1233 1234which means that there is a Rust function `etor_func` on the context 1235trait (see below) that can be *invoked* with an argument of type `&U`, 1236and returns an `Option<(A, B, C)>`. 1237 1238If an extractor returns `None`, then the generated matching code 1239proceeds just as if an enum variant match had failed: it moves on to 1240try the next rule in turn. 1241 1242### Mapping Type Declarations to Rust 1243 1244When we declare a type like 1245 1246```lisp 1247 (decl MyEnum (enum 1248 (A (x u32) (y u32)) 1249 (B))) 1250``` 1251 1252ISLE will generate the Rust type definition 1253 1254```rust 1255#[derive(Clone, Debug)] 1256pub enum MyEnum { 1257 A { x: u32, y: u32, }, 1258 B, 1259} 1260``` 1261 1262Note that enum variants with no fields take on the brace-less form, 1263while those with fields use the named-struct-field `A { x: ... }` 1264form. If all variants are field-less, then the type will additionally 1265derive `Copy`, `PartialEq`, and `Eq`. 1266 1267If the type is declared as extern (`(decl MyEnum extern (enum ...))`) 1268then the same definition is assumed to exist. Primitives (`(decl u32 1269(primitive u32))`) are assumed to be defined already, and are required 1270to be `Copy`. 1271 1272All imported/extern types are pulled in via `use super::*` at the top 1273of the generated code; thus, these types should exist in (or be 1274re-exported from) the parent module. 1275 1276### Symbolic Constants 1277 1278ISLE allows the user to refer to external constants as follows: 1279 1280```lisp 1281 (extern const $I32 Type) 1282``` 1283 1284This allows code to refer to `$I32` whenever a value of type `Type` is 1285needed, in either a pattern (LHS) or an expression (RHS). These 1286constants are pulled in via the same `use super::*` that imports all 1287external types. 1288 1289### Exported Interface: Functions and Context Trait 1290 1291The generated ISLE code provides an interface that is designed to be 1292agnostic to the embedding application. This means that ISLE knows 1293nothing about, e.g., Cranelift or compiler concepts in 1294general. Rather, the generated code provides function entry points 1295with well-defined signatures based on the terms, and imports the 1296extern constructors and extractors via a context trait that the 1297embedder must implement. 1298 1299When a term `T` is defined like 1300 1301```lisp 1302 (decl T (A B C) U) 1303``` 1304 1305and has an internal constructor (provided by `rule` bodies), then a 1306function with the following signature will be exported from the 1307generated code: 1308 1309```rust 1310 pub fn constructor_T<C: Context>(ctx: &mut C, arg0: &A, arg1: &B, arg2: &C) -> Option<U>; 1311``` 1312 1313In other words, `constructor_` is prepended, and the function takes 1314the expected arguments, along with a "context" (more on this 1315below). It returns an `Option<U>` because internal constructors can be 1316partial: if no rule's pattern matches, then the constructor 1317fails. Note that if a sub-constructor fails, no backtracking occurs; 1318rather, the failure propagates all the way up to the entry point. 1319 1320What is this "context" for? The context argument is used to allow 1321external extractors and constructors to access the necessary state of 1322the embedding application. (For example, in Cranelift, it might be the 1323`LowerCtx` that controls the code-lowering process.) 1324 1325The context is a trait because we want to decouple the generated code 1326from the application as much as possible. The trait will have a method 1327for each defined external extractor and constructor. For example, if 1328we have the following terms and declarations: 1329 1330```lisp 1331 (decl A (u32 u32) T) 1332 (extern constructor A build_a) 1333 1334 (decl B (T) U) 1335 (external extractor B disassemble_b) 1336``` 1337 1338then the `Context` trait will include these methods: 1339 1340```rust 1341 trait Context { 1342 fn build_a(&mut self, arg0: u32, arg1: u32) -> T; 1343 fn disassemble_b(&mut self, arg0: &U) -> Option<T>; 1344 } 1345``` 1346 1347These functions should be implemented as described above for external 1348constructors and extractors. 1349 1350Note that some external extractors are known to always succeed, for 1351example if they are just fetching some information that is always 1352present; in this case, the generated code can be made slightly more 1353efficient if we tell the ISLE compiler that this is so. By declaring 1354 1355```lisp 1356 (external extractor infallible B disassemble_b) 1357``` 1358 1359we eliminate the `Option` on the return type, so the method is instead 1360 1361```rust 1362 trait Context { 1363 // ... 1364 fn disassemble_b(&mut self, arg0: &U) -> T; 1365 } 1366``` 1367 1368## ISLE Internals 1369 1370### Compiler Stages 1371 1372Some detail and pointers to the compilation stages can be found in the 1373[README](../isle/README.md). The sequence starts as any ordinary 1374compiler: lexing, parsing, semantic analysis, and generation of an 1375IR. The most unique part is the "decision trie generation", which is 1376what converts the unordered-list-of-rule representation into something 1377that corresponds to the final Rust code's control flow and order of 1378matching operations. 1379 1380We describe this data structure below with the intent to provide an 1381understanding of how the DSL compiler weaves rules together into Rust 1382control flow. While this understanding shouldn't be strictly necessary 1383to use the DSL, it may be helpful. (The ultimate answer to "how does 1384the generated code work" is, of course, found by reading the generated 1385code; some care has been taken to ensure it is reasonably legible for 1386human consumption!) 1387 1388### Decision Trie 1389 1390The heart of the ISLE transformation lies in how the compiler converts 1391a list of rules into a scheme to attempt to match rules in some order, 1392possibly sharing match operations between similar rules to reduce 1393work. 1394 1395The core data structure we produce is a "decision trie" per internal 1396constructor body. This is an intermediate representation of sorts that 1397is built from individual-rule IR (LHS + RHS) sequences, and is then 1398used to generate Rust source. 1399 1400The decision trie is, as the name implies, a kind of decision tree, in 1401the sense that we start at the root and move down the tree based on 1402the result of match operations (each feeding one "decision"). 1403 1404It is a "trie" (which is a kind of tree) because at each level, its 1405edges are labeled with match operations; a trie is a tree where one 1406input character from an alphabet is used to index children at each 1407level. 1408 1409Each node in the tree is either an internal decision node, or a leaf 1410"expression" node (which we reach once we have a successful rule 1411match). The "execution semantics" of the trie are 1412backtracking-based. We attempt to find some path down the tree through 1413edges whose match ops run successfully; when we do this to reach a 1414leaf, we have the values generated by all of the match ops, and we can 1415execute the sequence of "expression instructions" in the leaf. Each 1416rule's left-hand side becomes a series of edges (merged into the 1417existing tree as we process rules) and each rule's right-hand side 1418becomes one leaf node with expression instructions. 1419 1420At any point, if a match op does not succeed, we try the next out-edge 1421in sequence. If we have tried all out-edges from a decision node and 1422none were successful, then we backtrack one level further. Thus, we 1423simply perform an in-order tree traversal and find the first 1424successful match. 1425 1426Though this sounds possibly very inefficient if some decision node has 1427a high fan-out, in practice it is not because the edges are often 1428known to be *mutually exclusive*. The canonical example of this is 1429when an enum-typed value is destructured into different variants by 1430various edges; we can use a Rust `match` statement in the generated 1431source and have `O(1)` (or close to it) cost for the dispatch at this 1432level.[^8] 1433 1434[^8]: The worst-case complexity for a single term rewriting operation 1435 is still the cost of evaluating each rule's left-hand side 1436 sequentially, because in general there is no guarantee of 1437 overlap between the patterns. Ordering of the edges out of a 1438 decision node also affects complexity: if mutually-exclusive 1439 match operations are not adjacent, then they cannot be merged 1440 into a single `match` with `O(1)` dispatch. In general this 1441 ordering problem is quite difficult. We could do better with 1442 stronger heuristics; this is an open area for improvement in the 1443 DSL compiler! 1444 1445## Reference: ISLE Language Grammar 1446 1447Baseline: allow arbitrary whitespace, and wasm-style comments (`;` to 1448newline, or nested block-comments with `(;` and `;)`). 1449 1450The grammar accepted by the parser is as follows: 1451 1452```bnf 1453<skip> ::= <whitespace> | <comment> 1454 1455<whitespace> ::= " " 1456 | "\t" 1457 | "\n" 1458 | "\r" 1459 1460<comment> ::= <line-comment> | <block-comment> 1461 1462<line-comment> ::= ";" <line-char>* (<newline> | eof) 1463<line-char> ::= <any character other than "\n" or "\r"> 1464<newline> ::= "\n" | "\r" 1465 1466<block-comment> ::= "(;" <block-char>* ";)" 1467<block-char> ::= <any character other than ";" or "("> 1468 | ";" if the next character is not ")" 1469 | "(" if the next character is not ";" 1470 | <block-comment> 1471 1472<ISLE> ::= <def>* 1473 1474<def> ::= "(" "pragma" <pragma> ")" 1475 | "(" "type" <typedecl> ")" 1476 | "(" "decl" <decl> ")" 1477 | "(" "rule" <rule> ")" 1478 | "(" "extractor" <extractor> ")" 1479 | "(" "extern" <extern> ")" 1480 | "(" "convert" <converter> ")" 1481 1482;; No pragmas are defined yet 1483<pragma> ::= <ident> 1484 1485<typedecl> ::= <ident> [ "extern" | "nodebug" ] <type-body> 1486 1487<ident> ::= <ident-start> <ident-cont>* 1488<const-ident> ::= "$" <ident-cont>* 1489<ident-start> ::= <any non-whitespace character other than "-", "0".."9", "(", ")", ";", "#" or "$"> 1490<ident-cont> ::= <any non-whitespace character other than "(", ")", ";" or "@"> 1491 1492<type-body> ::= "(" "primitive" <ident> ")" 1493 | "(" "enum" <enum-variant>* ")" 1494 1495<enum-variant> ::= <ident> 1496 | "(" <ident> <variant-field>* ")" 1497 1498<variant-field> ::= "(" <ident> <ty> ")" 1499 1500<ty> ::= <ident> 1501 1502<decl> ::= [ "pure" ] [ "multi" ] [ "partial" ] [ "rec" ] <ident> "(" <ty>* ")" <ty> 1503 1504<rule> ::= [ <ident> ] [ <prio> ] <pattern> <stmt>* <expr> 1505 1506<prio> ::= <int> 1507 1508<int> ::= [ "-" ] ( "0".."9" ) ( "0".."9" | "_" )* 1509 | [ "-" ] "0" ("x" | "X") ( "0".."9" | "A".."F" | "a".."f" | "_" )+ 1510 | [ "-" ] "0" ("o" | "O") ( "0".."7" | "_" )+ 1511 | [ "-" ] "0" ("b" | "B") ( "0".."1" | "_" )+ 1512 1513<pattern> ::= <int> 1514 | "true" | "false" 1515 | <const-ident> 1516 | "_" 1517 | <ident> 1518 | <ident> "@" <pattern> 1519 | "(" "and" <pattern>* ")" 1520 | "(" <ident> <pattern>* ")" 1521 1522<stmt> ::= "(" "if-let" <pattern> <expr> ")" 1523 | "(" "if" <expr> ")" 1524 1525<expr> ::= <int> 1526 | "true" | "false" 1527 | <const-ident> 1528 | <ident> 1529 | "(" "let" "(" <let-binding>* ")" <expr> ")" 1530 | "(" <ident> <expr>* ")" 1531 1532<let-binding> ::= "(" <ident> <ty> <expr> ")" 1533 1534<extractor> ::= "(" <ident> <ident>* ")" <pattern> 1535 1536<extern> ::= "constructor" <ident> <ident> 1537 | "extractor" [ "infallible" ] <ident> <ident> 1538 | "const" <const-ident> <ty> 1539 1540<converter> ::= <ty> <ty> <ident> 1541``` 1542 1543## Reference: ISLE Language Grammar verification extensions 1544 1545```bnf 1546<def> += "(" "spec" <spec> ")" 1547 | "(" "model" <model> ")" 1548 | "(" "form" <form> ")" 1549 | "(" "instantiate" <instantiation> ")" 1550 1551<spec> ::= "(" <ident> <ident>* ")" <provide> [ <require> ] 1552<provide> ::= "(" "provide" <spec-expr>* ")" 1553<require> ::= "(" "require" <spec-expr>* ")" 1554 1555<model> ::= <ty> "(" "type" <model-ty> ")" 1556 | <ty> "(" "enum" <model-variant>* ")" 1557 1558<model-ty> ::= "Bool" 1559 | "Int" 1560 | "Unit" 1561 | "(" "bv" [ <int> ] ")" 1562 1563<model-variant> ::= "(" <ident> [ <spec-expr> ] ")" 1564 1565<form> ::= <ident> <signature>* 1566 1567<instantiation> ::= <ident> <signature>* 1568 | <ident> <ident> 1569 1570<spec-expr> ::= <int> 1571 | <spec-bv> 1572 | "true" | "false" 1573 | <ident> 1574 | "(" "switch" <spec-expr> <spec-pair>* ")" 1575 | "(" <spec-op> <spec-expr>* ")" 1576 | "(" <ident> ")" 1577 | "(" ")" 1578 1579<spec-bv> ::= "#b" [ "+" | "-" ] ("0".."1")+ 1580 | "#x" [ "+" | "-" ] ("0".."9" | "A".."F" | "a".."f")+ 1581 1582<spec-pair> ::= "(" <spec-expr> <spec-expr> ")" 1583 1584<spec-op> ::= "and" | "not" | "or" | "=>" 1585 | "=" | "<=" | "<" | ">=" | ">" 1586 | "bvnot" | "bvand" | "bvor" | "bvxor" 1587 | "bvneg" | "bvadd" | "bvsub" | "bvmul" 1588 | "bvudiv" | "bvurem" | "bvsdiv" | "bvsrem" 1589 | "bvshl" | "bvlshr" | "bvashr" 1590 | "bvsaddo" | "subs" 1591 | "bvule" | "bvult" | "bvugt" | "bvuge" 1592 | "bvsle" | "bvslt" | "bvsgt" | "bvsge" 1593 | "rotr" | "rotl" 1594 | "extract" | "concat" | "conv_to" 1595 | "zero_ext" | "sign_ext" 1596 | "int2bv" | "bv2int" 1597 | "widthof" 1598 | "if" | "switch" 1599 | "popcnt" | "rev" | "cls" | "clz" 1600 | "load_effect" | "store_effect" 1601 1602<signature> ::= "(" <sig-args> <sig-ret> <sig-canon> ")" 1603<sig-args> ::= "(" "args" <model-ty>* ")" 1604<sig-ret> ::= "(" "ret" <model-ty>* ")" 1605<sig-canon> ::= "(" "canon" <model-ty>* ")" 1606``` 1607