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