{"title":"cfallin.org","link":[{"@attributes":{"rel":"self","type":"application\/atom+xml","href":"https:\/\/cfallin.org\/feed.xml"}},{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org"}}],"generator":"Zola","updated":"2026-05-06T11:47:46.790056103-07:00","id":"https:\/\/cfallin.org\/feed.xml","entry":[{"title":"The acyclic e-graph: Cranelift's mid-end optimizer","published":"2026-04-09T00:00:00+00:00","updated":"2026-04-09T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2026\/04\/09\/aegraph\/"}},"id":"https:\/\/cfallin.org\/blog\/2026\/04\/09\/aegraph\/","content":"<p>Today, I'll be writing about the <em>aegraph<\/em>, or <em>acyclic egraph<\/em>, the\ndata structure at the heart of Cranelift's mid-end optimizer. I\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/blob\/main\/accepted\/cranelift-egraph.md\">introduced this approach in\n2022<\/a>\nand, after a somewhat circuitous path involving one full rewrite, a\nnumber of interesting realizations and \"patches\" to the initial idea,\nvarious discussions with the wider e-graph community (including a\n<a rel=\"external\" href=\"https:\/\/vimeo.com\/843540328\">talk<\/a>\n(<a rel=\"external\" href=\"https:\/\/cfallin.org\/pubs\/egraphs2023_aegraphs_slides.pdf\">slides<\/a>)\nat the <a rel=\"external\" href=\"https:\/\/pldi23.sigplan.org\/home\/egraphs-2023\">EGRAPHS workshop at PLDI\n2023<\/a> and a recent talk\nand discussions at the <a rel=\"external\" href=\"https:\/\/www.dagstuhl.de\/en\/seminars\/seminar-calendar\/seminar-details\/26022\">e-graphs Dagstuhl\nseminar<\/a>),\nand a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/commits\/main\/cranelift\/codegen\/src\/opts\">whole bunch of contributed rewrite rules over the past three\nyears<\/a>,\nit is time that I describe the <em>why<\/em> (why an e-graph? what benefits\ndoes it bring?), the <em>how<\/em> (how did we escape the pitfalls of full\nequality saturation? how did we make this efficient enough to\nproductionize in Cranelift?), and the <em>how much<\/em> (does it help? how\ncan we evaluate it against alternatives?).<\/p>\n<p>For those who are already familiar with Cranelift's mid-end and its\naegraph, note that I'm taking a slightly different approach in this\npost. I've come to the viewpoint that the \"sea-of-nodes\" aspect of our\naegraph, and the translation passes we've designed to translate into\nand out of it (with optimizations fused in along the way), are\nactually more fundamental than the \"multi-representation\" part of the\naegraph, or in other words, the \"equivalence class\" part itself. I'm\nchoosing to introduce the ideas from sea-of-nodes-first in this post,\nso we will see a \"trivial eclass of one enode\" version of the aegraph\nfirst (no union nodes), then motivate unions later. In actuality, when\nI was experimenting then building this functionality in Cranelift in\n2022, the desire to integrate e-graphs came first, and aegraphs were\ncreated to make them practical; the pedagogy and design taxonomy have\nonly become clear to me over time. With that, let's jump in!<\/p>\n<h2 id=\"initial-context-fixpoint-loops-and-the-pass-ordering-problem\">Initial context: Fixpoint Loops and the Pass-Ordering Problem<\/h2>\n<p>Around May of 2022, I had introduced a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/4163\">simple alias analysis and\nrelated\noptimizations<\/a>\n(removing redundant loads, and doing store-to-load forwarding). It\nworked fine on all of the expected test cases, and we saw real speedup\non a few benchmarks (e.g. 5% on <code>meshoptimizer<\/code>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/4163#issuecomment-1130829170\">here<\/a>)\nbut led to a new question as well: how should we integrate this pass\nwith our other optimization passes, which at the time included GVN\n(global value numbering), LICM (loop-invariant code motion), constant\npropagation and some algebraic rewrites?<\/p>\n<p>To see why this is an interesting question, consider how GVN, which\ncanonicalizes values, and redundant load elimination interact, on the\nfollowing IR snippet:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>v2 = load.i64 v0+8<\/span><\/span>\n<span class=\"giallo-l\"><span>v3 = iadd v2, v1   ;; e.g., array indexing<\/span><\/span>\n<span class=\"giallo-l\"><span>v4 = load.i8 v3<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>;; ... (no stores or other side effects here) ...<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>v10 = load.i64 v0+8<\/span><\/span>\n<span class=\"giallo-l\"><span>v11 = iadd v10, v1<\/span><\/span>\n<span class=\"giallo-l\"><span>v12 = load.i8 v11<\/span><\/span><\/code><\/pre>\n<p>Redundant load elimination (RLE) will be able to see that the load\ndefining <code>v10<\/code> can be removed, and <code>v10<\/code> can be made an alias of\n<code>v2<\/code>, in a single pass. In a perfect world, we should then be able to\nsee that <code>v11<\/code> becomes the same as <code>v3<\/code> by means of GVN's\ncanonicalization, and subsequently, <code>v12<\/code> becomes an alias of\n<code>v4<\/code>. But those last two steps imply a tight cooperation between two\ndifferent optimization passes: we need to run one full pass of RLE\n(result: <code>v10<\/code> rewritten), then one full pass of GVN (result: <code>v11<\/code>\nrewritten), then one additional full pass of RLE (result: <code>v12<\/code>\nrewritten). One can see that an arbitrarily long chain of such\nreasoning steps, bouncing through different passes, might require an\narbitrarily long sequence of pass invocations to fully simplify. Not\ngood!<\/p>\n<p>This is known as the <em>pass-ordering problem<\/em> in the study of compilers\nand is a classical heuristic question with no easy answers as long as\nthe passes remain separate, coarse-grained algorithms (i.e., not\ninterwoven). To permit some interesting cases to work in the initial\nCranelift integration of alias analysis-based rewrites, I made a\nsomewhat <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/4163\/changes#diff-c7a66b91ac03843c5aafe984938022ccba235c80c3fad786772964dc7b9da152R166-R170\">ad-hoc\nchoice<\/a>\nto invoke GVN once after the alias-analysis rewrite pass.<\/p>\n<p>But this is clearly arbitrary, wastes compilation effort in the common\ncase, and we should be able to do better. In general, the solution\nshould reason about all passes' possible rewrites in a unified\nframework, and interleave them in a fine-grained way: so, for example,\nif we can apply RLE then GVN five times in a row just for one\nlocalized expression, we should be able to do that, without running\neach of these passes on the whole function body. In other words, we\nwant a \"single fixpoint loop\" that iterates until optimization is done\nat a fine granularity.<\/p>\n<h2 id=\"three-building-blocks-rewrites-code-motion-and-canonicalization\">Three Building Blocks: Rewrites, Code Motion, and Canonicalization<\/h2>\n<p>Let's review the optimizations we had at this point:<\/p>\n<ul>\n<li>\n<p>GVN (global value numbering), which is a <em>canonicalization<\/em>\noperation: within a given scope where a value is defined (for SSA\nIRs, the subtree of the <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Dominator_(graph_theory)\">dominance\ntree<\/a> below\na given definition), any identical computations of that value should\nbe canonicalized to the original one.<\/p>\n<\/li>\n<li>\n<p>LICM (loop-invariant code motion), which is a <em>code-motion<\/em>\noperation: computations that occur within a loop, but whose value is\nguaranteed to be the same on each iteration, should be moved\nout. Loop invariance can be defined recursively: values already\noutside the loop, or pure operators inside the loop whose arguments\nare all loop-invariant. The transform doesn't change any operators,\nit only moves where they occur.<\/p>\n<\/li>\n<li>\n<p>Constant propagation (cprop) and algebraic rewrites: these are\ntransforms like rewriting <code>1 + 2<\/code> to <code>3<\/code> (cprop) or <code>x + 0<\/code> to <code>x<\/code>\n(algebraic). They can all be expressed as substitutions for\nexpressions that match a given pattern.<\/p>\n<\/li>\n<li>\n<p>Redundant load elimination and store-to-load forwarding: these both\nreplace <code>load<\/code> operators with the SSA value that operator is known\nto load.<\/p>\n<\/li>\n<li>\n<p>And one that we wanted to implement: <em>rematerialization<\/em>, which\nreduces register pressure for values that are easier to recompute on\ndemand (e.g., integer constants) by re-defining them with a new\ncomputation. This can be seen as a kind of code motion as well.<\/p>\n<\/li>\n<\/ul>\n<p>As a start to thinking about frameworks, we can categorize the above\ninto <em>code motion<\/em>, <em>canonicalization<\/em>, and <em>rewrites<\/em>. Code motion is\nwhat it sounds like: it involves moving where a computation occurs,\nbut not changing it otherwise. Canonicalization is the unifying of\nmore than one instance of a computation into one (\"canonical\")\ninstance. And rewrites are any optimization that replaces one\nexpression with another that should compute the same value. Said more\nintuitively (and colloquially), these three categories attempt to\ncover the whole space of possibilities for \"simple\" optimizations: one\ncan move code, merge identical code, or replace code with equivalent\ncode. (The notable missing possibility here is the ability to change\ncontrol flow and\/or make use of control-flow-related reasoning; more\non that in a later section.) Thus, if we can build a framework that\nhandles these kinds of transforms, we should have a good\ninfrastructure for the next steps in Cranelift's evolution.<\/p>\n<h2 id=\"ir-design-sea-of-nodes-and-intermediate-points\">IR Design, Sea-of-Nodes, and Intermediate Points<\/h2>\n<p>From first principles, one might ask: how <em>should<\/em> a unifying\nframework for these concerns look? Code motion and canonicalization\ntogether imply that perhaps computations (operator nodes) should <em>not<\/em>\nhave a \"location\" in the program, whenever that can be avoided. In\nother words, perhaps we should find a way to represent <code>add v1, v2<\/code> in\nour IR without putting it somewhere concrete in the control flow. Then\nall instances of that same computation would be merged (because\nduplicates would differ only by their location, which we removed), and\ncode motion is... inapplicable, because code does not have a location?<\/p>\n<p>Well, not quite: the idea is that one <em>starts<\/em> with a conventional IR\n(with control flow), and <em>ends<\/em> with it too, but <em>in the middle<\/em> one\ncan eliminate locations where possible. So in the transition <em>to<\/em> this\nrepresentation, we erase locations, and canonicalize; and in the\ntransition <em>from<\/em> this representation, we re-assign locations, and\ncode-motion can be a side-effect of how we do that.<\/p>\n<p>What we just described above is called a\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Sea_of_nodes\">sea-of-nodes<\/a> IR.  A\nsea-of-nodes IR is one that dispenses with a classical \"sequential\norder\" for all instructions or operators in the program, and instead\nbuilds a graph (the \"sea\") of operators (the \"nodes\") with edges to\ndenote the actual dependencies, either for dataflow or control flow.<\/p>\n<p>In the purest form of this design, one can represent <em>every IR\ntransform<\/em> as a graph rewrite, because a graph is all there is. For\nexample, LICM, a kind of code motion that hoists a computation out of\na loop, is a purely algebraic rewrite on the subgraph representing the\nloop body. This is because the loop itself is a kind of node in the\nsea of nodes, with control-flow edges like any other edge; code motion\nis not a \"special\" action outside the scope of the expression\nlanguage (nodes and their operands).<\/p>\n<p>While that kind of flexibility is tempting, it comes with a\nsignificant complexity tax as well: it means that reasoning through\nand implementing classical compiler analyses and transforms is more\ndifficult, at least for existing compiler engineers with their\nexperience, because the IR is so different from the classical data\nstructure (CFG of basic blocks). The V8 team <a rel=\"external\" href=\"https:\/\/v8.dev\/blog\/leaving-the-sea-of-nodes\">wrote about this\ndifficulty<\/a> recently as\nsupport for their decision to migrate away from a pure Sea-of-Nodes\nrepresentation.<\/p>\n<p>However, we might achieve some progress toward our goal -- providing a\ngeneral framework for rewrites, code motion and canonicalization -- if\nwe take inspiration from sea-of-nodes' handling of <em>pure<\/em>\n(side-effect-free) operators, and the way that they can \"float\" in the\nsea, unmoored by any anchor other than actual inputs and outputs\n(dataflow edges). Stated succinctly: what if we kept the CFG for the\nside-effectful instructions (call it the \"side-effect skeleton\") and\nused a sea-of-nodes for the rest?<\/p>\n<p><img src=\"\/assets\/2026-03-29-cfg-skeleton-web.svg\" alt=\"Figure: sea-of-nodes-with-CFG\" \/><\/p>\n<p>This would allow for us to unify code motion, canonicalization and\nrewrites, as described above: canonicalization works on pure\noperators, because we remove distinctions based on location;\ncode-motion can occur when we put pure operators back in the CFG; and\nrewrites can occur on pure operators. In fact rewrites are now both\n(i) simpler to reason about, because we don't have to place expression\nnodes at locations in an IR, only create them \"floating in the air\",\nand (ii) more efficient, because they occur once on a canonicalized\ninstance of an expression, rather than all instances separately.<\/p>\n<p>We'll call this representation a \"sea-of-nodes with CFG\".<\/p>\n<h3 id=\"implementing-sea-of-nodes-with-cfg\">Implementing Sea-of-Nodes-with-CFG<\/h3>\n<p>Now, to practical implementation: architecting the entire compiler\naround sea-of-nodes for pure operators might make sense from first\nprinciples, but as a modification of the existing Cranelift compiler\npipeline, we would not want to (or be able to) make such a radical\nchange in one step. Rather, I wanted to build this as a replacement\nfor the mid-end, taking CLIF (our conventional CFG-based SSA IR) as\ninput and producing CLIF as output. So we need a three-stage\noptimizer:<\/p>\n<ol>\n<li>\n<p>Lift all pure operators out of the CFG, leaving behind the\nskeleton. Put these operators into the \"sea\" of pure computation\nnodes, deduplicating\n(<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Hash_consing\">hash-consing<\/a>) as we\ngo.<\/p>\n<\/li>\n<li>\n<p>Perform rewrites on these operators, replacing some values with\nothers according to whatever rules we have that preserve value\nequivalence.<\/p>\n<\/li>\n<li>\n<p>Convert this sea-of-pure-nodes back to sequential IR by scheduling\nnodes into the CFG. We'll call this process \"elaboration\" of the\ncomputations.<\/p>\n<\/li>\n<\/ol>\n<p>This is in fact how the heart of Cranelift's mid-end now works; we'll\ngo through each part above in turn.<\/p>\n<h3 id=\"into-sea-of-nodes-with-cfg-canonicalization\">Into Sea-of-Nodes-with-CFG: Canonicalization<\/h3>\n<p>Let's talk about how we get <em>into<\/em> the sea-of-nodes representation\nfirst. The most straightforward answer, of course, would be to simply\n\"remove the nodes from the CFG\" and let them free-float, referenced by\ntheir uses that remain in the skeleton -- and that's it. But that\ngives up on the obvious opportunity offered by the fact that these\noperators are <em>pure<\/em> (have no side-effects, or implicit dependencies\non the rest of the world): an operator <code>op v1, v2<\/code> <em>always<\/em> produces\nthe same value given the same inputs, and two separate instances of\nthis node have no distinguishing features or other properties that\nshould lead to different results. Hence, we should canonicalize, or\n<em>hash-cons<\/em>, nodes.<\/p>\n<p><a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Hash_consing\">Hash-consing<\/a> is a\nstandard technique in systems that have value- or operator-nodes: the\nidea is to keep a lookup table indexed by the contents of each value\nor operator, perform lookups in this table when creating a new node,\nand reuse existing nodes when a match occurs.<\/p>\n<p>What is the <em>equivalence class<\/em> by which we deduplicate? (In other\nwords, more concretely, how do we define <code>Eq<\/code> and <code>Hash<\/code> on\nsea-of-nodes values?) We adopt a very simple answer (and deal with\nsubtleties later, as is often the case!): the (shallow) content of a\ngiven node is its identity. In other words, if we have <code>iadd v1, v2<\/code>,\nthen that is \"equal to\" (deduplicates with) any other such operator.<\/p>\n<p>Now, this shallow notion of equality may not seem like enough to\ncanonicalize all instances of the same expression tree. Consider if we\nhad<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>v0 = ...<\/span><\/span>\n<span class=\"giallo-l\"><span>v1 = ...<\/span><\/span>\n<span class=\"giallo-l\"><span>v2 = iadd v0, v1<\/span><\/span>\n<span class=\"giallo-l\"><span>v3 = iconst 42<\/span><\/span>\n<span class=\"giallo-l\"><span>v4 = imul v2, v3<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>v5 = iadd v0, v1<\/span><\/span>\n<span class=\"giallo-l\"><span>v6 = iconst 42<\/span><\/span>\n<span class=\"giallo-l\"><span>v7 = imul v5, v6<\/span><\/span><\/code><\/pre>\n<p>Clearly any reasonable canonicalization algorithm should consider <code>v4<\/code>\nand <code>v7<\/code> to be the same, and condense uses of them into uses of one\ncanonical node. But the nodes are not <em>shallowly<\/em> equal. How do we\nget from here to there?<\/p>\n<p>One possible answer is induction: we could canonicalize a node only\nafter all of its operands have been canonicalized (and rewritten), so\nwe know that if subtrees are identical, we will have identical value\nnumbers. Thus, inductively, all values would be canonicalized deeply.<\/p>\n<p>This requires processing definitions of a node before its uses,\nhowever. Fortunately, the SSA CFG from which we are constructing the\nsea-of-nodes-with-CFG provides us this property already if we traverse\nit in a particular order: we need to visit blocks in the control-flow\ngraph in some <em>preorder<\/em> of the dominance tree (domtree), which we\nusually have available already.<\/p>\n<p>So we have an algorithm something like the following pseudo-code to\ncanonicalize the SSA CFG into a sea-of-nodes-with-CFG:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>def canonicalize(basic_block):<\/span><\/span>\n<span class=\"giallo-l\"><span>  rename_map = {}                     # map from SSA value to SSA value<\/span><\/span>\n<span class=\"giallo-l\"><span>  for inst in basic_block:<\/span><\/span>\n<span class=\"giallo-l\"><span>    inst.rename_values(rename_map)    # rewrite uses according to a value-&gt;value map<\/span><\/span>\n<span class=\"giallo-l\"><span>    if is_pure(inst):                 # only dedup and move to sea-of-nodes for &quot;pure&quot; insts;<\/span><\/span>\n<span class=\"giallo-l\"><span>                                      # leave the &quot;skeleton&quot; in place<\/span><\/span>\n<span class=\"giallo-l\"><span>      basic_block.remove(inst)<\/span><\/span>\n<span class=\"giallo-l\"><span>      if inst in hashcons_map:        # equality defined by shallow content<\/span><\/span>\n<span class=\"giallo-l\"><span>        rename_map[inst.value] = hashcons_map[inst]<\/span><\/span>\n<span class=\"giallo-l\"><span>      else:<\/span><\/span>\n<span class=\"giallo-l\"><span>        nodes.push(inst)              # add to the sea-of-nodes<\/span><\/span>\n<span class=\"giallo-l\"><span>        hashcons_map[inst] = inst.value<\/span><\/span>\n<span class=\"giallo-l\"><span>    else:<\/span><\/span>\n<span class=\"giallo-l\"><span>      pass<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>  # recursive domtree-preorder traversal.<\/span><\/span>\n<span class=\"giallo-l\"><span>  for child in domtree.children(basic_block):<\/span><\/span>\n<span class=\"giallo-l\"><span>    canonicalize(child)<\/span><\/span><\/code><\/pre>\n<p>This will handle not only the above example, where we have \"deep\nequality\" (because we will canonicalize and rename e.g. <code>v5<\/code> into <code>v2<\/code>\nbefore visiting <code>v5<\/code>'s use), but also more complex examples with the\nredundancies spread across basic blocks.<\/p>\n<p>Finally: how does the \"-with-CFG\" aspect of all of this work? So far, we\nhave very much glossed over any values that are defined in the CFG\nskeleton, other than to imply above that they are never renamed (because\nwe never take the <code>is_pure<\/code> branch). But is this OK?<\/p>\n<p>Yes, in a sense, by construction: we have defined all impure values to\nhave their own \"identity\", distinct from any other such value, even if\nshallowly equal at a syntactic level. This aligns with the notion that\nimpure computations have implicit inputs: for example, <code>load v0<\/code>\nappearing twice in the program may produce different values at those\ntwo different times, so we cannot deduplicate it. This can be relaxed\nif we have a dedicated analysis that can reason about such implicit\ndependencies, and in fact for loads we do have one (alias analysis,\nfeeding into redundant-load elimination and store-to-load\nforwarding). But in general, we cannot do anything with these\n\"roots\". Rather, they stay in the skeleton, feed values into the sea\nof nodes, and consume values back out of that sea of nodes.<\/p>\n<h3 id=\"out-of-sea-of-nodes-with-cfg-scoped-elaboration\">Out of Sea-of-Nodes-with-CFG: Scoped Elaboration<\/h3>\n<p>Given a sea-of-nodes + skeleton representation of a program, how do we\ngo back to a conventional CFG, with fully linearized operators (i.e.,\neach of which has a concrete program-point where it is computed), to\nfeed to the compiler backend and lower to machine code?<\/p>\n<p>The basic task is to decide a location at which to put each\noperator. Since nodes in the sea-of-nodes are \"rooted\" (referenced and\nultimately computed\/used) by side-effectful operators in the CFG\nskeleton, the first idea one might have is to copy pure nodes back\ninto the CFG where they are referenced. One could do this recursively:\nif e.g. we have a side-effecting instruction <code>store v1, v2<\/code>, we can\nplace the (pure operator) definitions of <code>v1<\/code> and <code>v2<\/code> just before\nthis instruction; if those definitions require other values, likewise\ncompute them first. We could call this \"elaboration\".<\/p>\n<p>Let's consider the single-basic-block case first and then define\nsomething like the following pseudocode:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>def demand_based_elaboration(bb):<\/span><\/span>\n<span class=\"giallo-l\"><span>  for inst in bb:<\/span><\/span>\n<span class=\"giallo-l\"><span>    elaborate_inst(inst, bb, before=inst)<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>def elaborate_inst(inst, bb, before):<\/span><\/span>\n<span class=\"giallo-l\"><span>  for value in inst.args:<\/span><\/span>\n<span class=\"giallo-l\"><span>    inst.rewrite_arg(value, elaborate_value(value, bb, before=inst))<\/span><\/span>\n<span class=\"giallo-l\"><span>  if is_pure(inst):<\/span><\/span>\n<span class=\"giallo-l\"><span>    bb.insert_before(before, inst)<\/span><\/span>\n<span class=\"giallo-l\"><span>  return inst.def<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>def elaborate_value(value, bb, before):<\/span><\/span>\n<span class=\"giallo-l\"><span>  if defined_by_inst(value):   # some values are blockparam roots, not inst defs<\/span><\/span>\n<span class=\"giallo-l\"><span>    return elaborate_inst(value.inst, bb, before)<\/span><\/span>\n<span class=\"giallo-l\"><span>  else:<\/span><\/span>\n<span class=\"giallo-l\"><span>    return value<\/span><\/span><\/code><\/pre>\n<p>This would certainly work, but is far too simple: it <em>duplicates<\/em>\ncomputation every time a value is used, and no value (other than\nblockparam roots) is ever used more than once. This will almost\ncertainly result in extreme blowup in program size!<\/p>\n<p>So if we use a value multiple times, it seems that we should compute\nit <em>once<\/em>, some place in the program before any of the uses. For\nexample, perhaps we could augment the above algorithm with a map that\nrecords the resulting value number the first time we elaborate a node,\nand reuses it (i.e., memoizes the elaboration):<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span># ...<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>def elaborate_value(value, bb, before):<\/span><\/span>\n<span class=\"giallo-l\"><span>  if value in elaborated:<\/span><\/span>\n<span class=\"giallo-l\"><span>    return already_elaborated[value]<\/span><\/span>\n<span class=\"giallo-l\"><span>  else if defined_by_inst(inst):<\/span><\/span>\n<span class=\"giallo-l\"><span>    result = elaborate_inst(value.inst, bb, before)<\/span><\/span>\n<span class=\"giallo-l\"><span>    elaborated[value] = result<\/span><\/span>\n<span class=\"giallo-l\"><span>    return result<\/span><\/span>\n<span class=\"giallo-l\"><span>  else:<\/span><\/span>\n<span class=\"giallo-l\"><span>    return value<\/span><\/span><\/code><\/pre>\n<p>This modified algorithm will handle the case of a single block with\nreuse efficiently, computing a value the first time it is used (\"on\ndemand\") as expected.<\/p>\n<p>Now let's consider <em>multiple<\/em> basic blocks. One might be tempted to\nwrap the above with a traversal, as we did for the translation into\nsea-of-nodes:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>def elaborate_domtree(bb):<\/span><\/span>\n<span class=\"giallo-l\"><span>  demand_based_elaboration(bb)<\/span><\/span>\n<span class=\"giallo-l\"><span>  for child in domtree.children(bb):<\/span><\/span>\n<span class=\"giallo-l\"><span>    elaborate_domtree(child)<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>def elaborate(func):<\/span><\/span>\n<span class=\"giallo-l\"><span>  elaborate_domtree(func.entry)<\/span><\/span><\/code><\/pre>\n<p>But this, too, has an issue. Consider a program that began as a CFG\nwith many paths, two of which compute the same value:<\/p>\n<p><img src=\"\/assets\/2026-03-29-cfg-web.svg\" alt=\"Figure: CFG with some redundancy between code paths\" \/><\/p>\n<p>If we define some traversal over all basic blocks to perform an\nelaboration as above, with a single map <code>elaborated<\/code>, we will<\/p>\n<ul>\n<li>Elaborate a computation of <code>v2<\/code> in <code>bb2<\/code> and use it there;<\/li>\n<li>Use it in <code>bb3<\/code> as well in place of <code>v3<\/code>, since it has already been\ncomputed and is thus memoized;<\/li>\n<li>And thus generate <em>invalid SSA<\/em>, where a value is used on a path\nwhere it is never computed!<\/li>\n<\/ul>\n<p>Perhaps we could hoist the computation to a \"common ancestor\" of all\nof its uses instead. Here that would be <code>bb1<\/code>. But that creates yet\nanother problem: if control flows from <code>bb1<\/code> to <code>bb4<\/code>, then we will\nhave computed the value and never used it -- in supposedly optimized\ncode! This is sometimes called a \"partial redundancy\": a computation\nthat is sometimes unused, depending on control flow. We would like to\navoid this if possible.<\/p>\n<p>It turns out that this problem exactly corresponds to <em>common\nsubexpression elimination<\/em> (CSE), which aims to find one place to\ncompute a value possibly used multiple times. The usual approach in\nSSA code, <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Value_numbering\">global value\nnumbering<\/a> (GVN),\nsolves the problem by reasoning about <em>scopes<\/em>, where a \"scope\" is the\nregion in which a value has already been computed. The intuition is\nthat at any given use, we can cast a \"shadow\" downward and remove\nredundant uses but only in that shadow. So in our example program, if\n<code>bb1<\/code> computed <code>v2<\/code> then we could reuse it in <code>bb2<\/code> and <code>bb3<\/code>; but\nbecause it occurs independently in two subtrees with no common\nancestor, we do nothing; we <em>duplicate it<\/em> (re-elaborate it).<\/p>\n<p>SSA \"scopes\" -- regions in which a value can be used -- are defined by\nthe dominance relation, and so we can work with a domtree traversal to\nimplement the needed behavior. Concretely, we can do a domtree\npreorder traversal; we can keep the <code>elaborated<\/code> map but separate it\ninto scope \"overlays\", and push a new overlay for each subtree. This\nformalizes the \"shadow\" intuition above. We call this <em>scoped\nelaboration<\/em>. Pseudo-code follows:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>def find_in_scope(value, scope):<\/span><\/span>\n<span class=\"giallo-l\"><span>  if value in scope.map:<\/span><\/span>\n<span class=\"giallo-l\"><span>    return scope.map[value]<\/span><\/span>\n<span class=\"giallo-l\"><span>  elif scope.parent:<\/span><\/span>\n<span class=\"giallo-l\"><span>    return find_in_scope(value, scope.parent)<\/span><\/span>\n<span class=\"giallo-l\"><span>  else:<\/span><\/span>\n<span class=\"giallo-l\"><span>    return None<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>def elaborate_value(value, bb, before, scope):<\/span><\/span>\n<span class=\"giallo-l\"><span>  if find_in_scope(value, scope):<\/span><\/span>\n<span class=\"giallo-l\"><span>    # ...<\/span><\/span>\n<span class=\"giallo-l\"><span>  # ...<\/span><\/span>\n<span class=\"giallo-l\"><span>  <\/span><\/span>\n<span class=\"giallo-l\"><span>def elaborate_domtree(bb, scope):<\/span><\/span>\n<span class=\"giallo-l\"><span>  demand_based_elaboration(bb, scope)<\/span><\/span>\n<span class=\"giallo-l\"><span>  for child in domtree.children(bb):<\/span><\/span>\n<span class=\"giallo-l\"><span>    subscope = { map = {}, parent = scope }<\/span><\/span>\n<span class=\"giallo-l\"><span>    elaborate_domtree(child, subscope)<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>def elaborate(func):<\/span><\/span>\n<span class=\"giallo-l\"><span>  root_scope = { map = {}, parent = None }<\/span><\/span>\n<span class=\"giallo-l\"><span>  elaborate_domtree(func.entry, root_scope)<\/span><\/span><\/code><\/pre>\n<p>The <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/scoped_hash_map.rs\">real\nimplementation<\/a>\nof our scoped hashmap takes advantage of the fact that keys will not\noverlap between overlay layers (because once defined, a value will not\nbe re-defined in a lower layer), and this enables us to have true O(1)\nrather than O(depth) lookup using some tricks with a <code>layer<\/code> number\nand <code>generation<\/code>-per-layer (see implementation for\ndetails!). Nevertheless, the semantics are the same as above.<\/p>\n<p>As we foreshadowed above, just as the problem is closely related to\nCSE and GVN, scoped elaboration is as well. In fact, the approach of\ntracking a definition-within-scope for scopes that correspond to\nsubtrees in the domtree, given a preorder traversal on the domtree, is\nexactly how <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/465913eb2c91998c99ae9222e47f8e9f9a88a546\/cranelift\/codegen\/src\/simple_gvn.rs\">Cranelift's old\nimplementation<\/a>\nworks as well. We even borrowed the scoped hashmap implementation!<\/p>\n<p>A few more observations are in order. First, it's fairly interesting\nthat we sometimes <em>re-elaborate<\/em> a node into multiple dom subtrees;\nwhy is this? Does this introduce inefficiency (e.g. in code size) or\nis it the best we can do?<\/p>\n<p>The duplication is, in my opinion, best seen as a <em>dual<\/em> of the\ncanonicalization. The original code may have multiple copies of a pure\ncomputation in multiple paths, with no common ancestor that computes\nthat value. When translating to sea-of-nodes, we will canonicalize\nthat computation, so we can optimize it once. But then when returning\nto the original linearized IR, we may need to restore the <em>original<\/em>\nduplication if there truly was no (non-redundancy-producing)\noptimization opportunity.<\/p>\n<p>Note that this means we will sometimes elaborate a value in more than\none place even if it appeared only once in the original program. For\nexample, pure computation above an if\/else split that is only used\nwithin the if\/else branches may be \"sunk\" into those branched paths. I\nsuppose one could see this as an optimization in the case that the\ncomputation is not used in <em>every<\/em> successor, but in any case, we\ndon't have an analysis that can show a value is definitely used in\nevery successor or not. So the the program size may grow beyond the\noriginal in these cases. (Thanks very much to Jamey Sharp for\n<a rel=\"external\" href=\"https:\/\/mastodon.world\/@jamey@toot.cat\/116386234595356078\">pointing this\nout<\/a> on an\nearlier, incorrect, version of this post!)<\/p>\n<p>Another interesting observation is that by driving elaboration by\ndemand (from the roots in the side-effecting CFG skeleton), we do\ndead-code elimination (DCE) of the pure operations <em>for free<\/em>. Their\nexistence in the sea of nodes may cost us some compile time if we\nspend effort to optimize them (only to throw them away later); but\nanything that becomes dead <em>because of<\/em> rewrites in sea-of-nodes will\nthen naturally disappear from the final result.<\/p>\n<p>A third observation is that elaboration gives us a central location to\ncontrol when and where code is placed in the final program. In other\nwords, there is room for us to add <em>heuristics<\/em> beyond the simplest\nversion of the algorithm described above. For example: we stated that\nwe did not want to introduce any partial redundancies. But for\ncorrectness, we don't <em>need<\/em> to adhere to this: our only real\nrestriction is that a pure computation cannot happen before its\narguments are computed (i.e., we have to obey dataflow dependencies).\nSo, for example, if we have the <em>loop nest<\/em> (structure of loops in the\nprogram) available, if a pure computation within a loop does not use\nany values that are computed within that loop, we know it is\n<em>loop-invariant<\/em> and we may choose to elaborate it before the loop\nbegins (into the \"preheader\"), in a transform known as <em>loop-invariant\ncode motion<\/em> (LICM). This is redundant if the loop executes zero\niterations, but most loops execute at least once; and performing a\nloop-invariant computation only once can be a huge efficiency\nimprovement.<\/p>\n<p>In the other direction -- pushing computation downward rather than\nupward -- we could choose to implement <em>rematerialization<\/em> by\nstrategically <em>forgetting<\/em> a value in the already-elaborated scope and\nrecomputing it at a new use. Why would we do this? Perhaps it is\ncheaper to recompute than to <em>thread the original value through the\nprogram<\/em>. For example, constant values are very cheap to \"compute\"\n(typically 1 or 2 instructions) but burning a machine register to keep\na constant across a long function can be expensive.<\/p>\n<p>There is a lot of room for heuristic <em>code scheduling<\/em> within\nelaboration as well (LICM and rematerialization can be seen as\nscheduling too, but here I mean the order that operations are\nlinearized within the block they are otherwise elaborated into). For a\nmodern\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Out-of-order_execution\">out-of-order<\/a>\nCPU, this may not matter too much to the hardware -- but it <em>may<\/em>\nmatter to the register allocator, because reordering instructions\nchanges the \"interference graph\", or the way that different live\nregister values compete for finite resources (hardware\nregisters). E.g., pushing an instruction that uses many values for the\nlast time \"earlier\" (to eliminate the need to store those values) is\ngreat; but this minimization is not always straightforward.  In fact,\nordering instructions that define and use values to minimize the\ncoloring count for the resulting live-range interference graph is an\nNP-complete problem. So it goes, too often, in compiler engineering!<\/p>\n<p>Despite the complexities that may arise in combining many heuristics,\nthese three dimensions -- LICM, rematerialization, and code scheduling\nfor register pressure -- are an interesting high-dimensional cost\noptimization problem and one that we still haven't fully solved (see\ne.g. <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/6159\">#6159<\/a>,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/6260\">#6260<\/a> and\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/8959\">#8959<\/a>).<\/p>\n<h3 id=\"optimizing-pure-expression-nodes-rewrite-framework\">Optimizing Pure Expression Nodes: Rewrite Framework<\/h3>\n<p>We've covered the transitions into and out of the\nsea-of-nodes-with-CFG program representation. We've seen how merely\nthis translation gives us GVN (deduplication), DCE, LICM, and\nrematerialization \"for free\" (not really free, but falling out as a\nnatural consequence of the algorithms). But we still haven't covered\none of the most classical sets of optimizations: algebraic (and other)\nrewrites from one expression to another equivalent one (e.g, <code>x+0<\/code> to\n<code>x<\/code>). How can we do this on the sea-of-nodes?<\/p>\n<p>In principle, the answer is as \"simple\" as: build the logic that\npattern-matches the \"left-hand side\" of a rewrite (the part that we\nhave a \"better\" equivalent expression for), and then replaces it with\nthe \"right-hand side\". That is, in <code>x + 0 -&gt; x<\/code>, the left-hand side is\n<code>x + 0<\/code> and the right-hand side is <code>x<\/code>. Such a framework is highly\namenable to a <em>domain-specific language<\/em> to express these rewrites:\nideally one doesn't want to write code that manually iterates through\nnodes to find these patterns. Fortunately for us, in the Cranelift\nproject we have the <em>ISLE<\/em> (instruction-selection and\nlowering-expressions) DSL\n(<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/27\">RFC<\/a>, <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/isle\/docs\/language-reference.md\">language\nreference<\/a>,\n<a href=\"\/blog\/2023\/01\/20\/cranelift-isle\/\">blog post<\/a>). I originally designed\nISLE in the context of instruction lowering, as the name implies, but\nI was careful to keep a separation between the core language and its\n\"prelude\" binding it to a particular environment. Hence we could adapt\nit fairly easily to rewrite a graph of Cranelift IR operators as\nwell. The idea is that, as in instruction lowering, for mid-end\noptimizations we invoke an ISLE constructor (entry point) on a\n<em>particular<\/em> node and the ruleset produces a possibly better node.<\/p>\n<p>That gives us the logic for one expression, but there is still an open\nquestion how to apply these rewrites: to which nodes, in what order,\nand how to manage or update any uses of a node when that node is\nrewritten.<\/p>\n<p>The two general design axes one might consider are:<\/p>\n<ul>\n<li>\n<p>Eager or deferred: do we apply rewrites to a node as soon as it\nexists, or apply them later (perhaps as some sort of batch-rewrite)?<\/p>\n<\/li>\n<li>\n<p>Single-rewrite or fixpoint loop: do we rewrite a node only once, or\napply rewrite rules again to the result of a rewrite? Also, if the\noperand of a node is rewritten, do we (and how do we) rewrite users\nof that node as well, since more tree-matching patterns may now\napply to the new subtree?<\/p>\n<\/li>\n<\/ul>\n<p>It is clear that different choices to these questions could lead to\ndifferent efficiency-quality tradeoffs: most obviously, applying\nrewrites in a fixpoint should produce better code at the cost of\nlonger compile time. But also, it seems possible that either eager or\ndeferred rewrite processing could win, depending on the workload and\nparticular rules: batching (hence, deferred until one bulk pass) often\nleads to efficiency advantages (see the <a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/10.1145\/3434304\">egg\npaper<\/a> and discussion below!),\nbut also, deferral may require additional bookkeeping vs. eagerly\nrewriting before making use of the (soon to be stale) original value.<\/p>\n<p>For the overall design that we have described so far, there turns out\nto be a fairly clear optimal answer, surprisingly: because we build an\nacyclic sea-of-nodes, as long as we keep it acyclic during rewrites,\nwe should be able to do a single rewrite pass rather than a\nfixpoint. And, to make that single pass work, we rewrite eagerly, as\nsoon as we create a node; then use the final rewritten version of that\nnode for any uses of the original value. Because we visit defs before\nuses and do rewrites immediately at the def, we never need to update\n(and re-canonicalize!) nodes after creation.<\/p>\n<p>An aside is in order: while it is fairly clear why the\nsea-of-nodes-with-CFG is initially acyclic -- because SSA permits\ndataflow cycles only through block-parameters \/ phi-nodes, and those\nremain in the CFG, which we don't \"look through\" when applying\nrewrites -- it is less clear why rewrites should <em>maintain<\/em>\nacyclicity, especially in the face of hashconsing, which may \"tie the\nknot\" of a cycle if we're not careful. The answer lies in the previous\nparagraph: once we create a node, we never update it. That's it! We've\nnow maintained acyclicity, by construction.<\/p>\n<p>Perhaps surprisingly as well, this rewrite process can be <em>fused<\/em> with\nthe translation pass into the sea-of-nodes itself. So we can amend the\nabove <code>canonicalize<\/code> to<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>def canonicalize(basic_block):<\/span><\/span>\n<span class=\"giallo-l\"><span>  for inst in basic_block:<\/span><\/span>\n<span class=\"giallo-l\"><span>    if is_pure(inst):<\/span><\/span>\n<span class=\"giallo-l\"><span>      # ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      if inst in hashcons_map:<\/span><\/span>\n<span class=\"giallo-l\"><span>        # ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      else:<\/span><\/span>\n<span class=\"giallo-l\"><span>        inst = rewrite(inst)          # NEW<\/span><\/span>\n<span class=\"giallo-l\"><span>        nodes.push(inst)              # add to the sea-of-nodes<\/span><\/span>\n<span class=\"giallo-l\"><span>        hashcons_map[inst] = inst.value<\/span><\/span>\n<span class=\"giallo-l\"><span>    else:<\/span><\/span>\n<span class=\"giallo-l\"><span>       # ...<\/span><\/span>\n<span class=\"giallo-l\"><\/span><\/code><\/pre>\n<p>i.e., simply add the rewrite rule application at the place we create\nnodes, and hashcons based on the final version of the instruction.<\/p>\n<p>Now, note that this is not quite complete yet: <code>inst = rewrite(inst)<\/code>\nis doing some heavy lifting, and is actually a bit too simplistic, in\nthe sense that this implies that a rewrite rule can only ever rewrite\nto <em>one<\/em> instruction on the right hand side. This isn't quite right:\nfor example, one may want a DeMorgan rewrite rule <code>~(x &amp; y) -&gt; ~x | ~y<\/code>. The right-hand side includes three operator nodes (instructions):\ntwo bitwise-NOTs and the OR that uses them. What if <code>x<\/code> or <code>y<\/code> in this\npattern also match a subexpression that can be simplified with some\nlogic rule?<\/p>\n<p>There seem to be two general answers: create the original right-hand\nside nodes un-rewritten and later apply rewrites, or immediately and\n<em>recursively<\/em> rewrite. As we observed above, deferral requires\nadditional bookkeeping and re-canonicalization as a node's inputs\nchange, so we choose the recursive approach. So, concretely, given\n<code>~((a &amp; b) &amp; (c &amp; d))<\/code> and the one rewrite rule above, we would:<\/p>\n<ul>\n<li>Encounter the top-level <code>~<\/code>, and try to match the rewrite rule's\nleft-hand side. It would match with bindings <code>x = (a &amp; b)<\/code> and <code>y = (c &amp; d)<\/code>.<\/li>\n<li>Apply the right-hand side <code>~x | ~y<\/code> bottom-up, building nodes and rewriting\nthem as we go:\n<ul>\n<li>First, <code>~x<\/code>. This creates <code>~(a &amp; b)<\/code>, which recursively fires the\nrule, which results in <code>(~a | ~b)<\/code>.<\/li>\n<li>Then, <code>~y<\/code>. This creates <code>~(c &amp; d)<\/code>, again recursively firing the\nrule, which results in <code>(~c | ~d)<\/code>.<\/li>\n<li>We then create the top-level node on the right-hand side,\nresulting in <code>(~a | ~b) | (~c | ~d)<\/code>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>One needs to limit the recursion if there is any concern that rule\nchain depths may not be statically bounded or easily analyzable, but\notherwise this yields the correct answer in a single pass without the\nneed to track users of a node to later rewrite and recanonicalize it.<\/p>\n<p>And that's the whole pipeline: we now have a way to optimize code by\ntranslating to sea-of-nodes-with-CFG, applying rewrites as we go, then\ntranslating back to classical SSA CFG. In the process we've achieved\nall the goals we set out with: GVN, LICM, DCE, rematerialization, and\nalgebraic rewrites.<\/p>\n<h2 id=\"e-graphs-representing-many-possible-rewrites\">E-graphs: Representing Many Possible Rewrites<\/h2>\n<p>So far, we've described a system that has <em>zero or one<\/em> deterministic\nrewrite for any given node; this is analogous to a classical compiler\npipeline that destructively updates instructions\/operators. This is\ngreat for rewrite rules like <code>x+0 -&gt; x<\/code>: the right-hand side is\nunambiguously better if it is \"smaller\" (rewrites a whole expression\ninto only one of its parts). This is also fine when instructions have\nclear and very distinct costs, such as integer divide (typically tens\nof cycles or more even on modern CPUs) by a constant <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/opts\/div_const.rs\">converted into\nmagic wrapping\nmultiplies<\/a>.<\/p>\n<p>But what about cases where the benefit of a rewrite is less clear, or\ndepends on context, or depends on how it may or may not be able to\ncompose with or enable other rewrites in a given program?<\/p>\n<p>For example, consider the classical example from the <a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/pdf\/10.1145\/3434304\">2021 paper on\negg, an e-graph\nframework<\/a>: if we have the\nexpression <code>(x * 2) \/ 2<\/code> in our program, we would expect that to\nsimplify to <code>x<\/code><sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>. To implement this simplification, we might have a\ngeneral rewrite rule <code>(x * k) \/ k -&gt; x<\/code>. But we might also,\nseparately, have a rewrite rule that <code>(x * 2^k) -&gt; (x &lt;&lt; k)<\/code>, i.e.,\nconvert a multiplication into a left-shift operation. If we performed\nthis latter rewrite eagerly, the former rewrite rule might never match.<\/p>\n<p>(Now, you might complain that we could <em>also<\/em> convert the divide into\na right-shift, then we have another rewrite rule that simplifies <code>(x &lt;&lt; k) &gt;&gt; k -&gt; x<\/code>. In this particular example, that might be\nreasonable. But (i) that required careful thinking about canonical\nforms, where multiplies\/divides by powers-of-2 are always\ncanonicalized down to shifts, and (ii) this same fortunate behavior\nmight not exist for all rulesets.)<\/p>\n<p>In general, we also have a question at the rule-application level: if\nmultiple rules apply, which do we take? In the above example, we would\nhave had to have some prioritization scheme to (say) apply\nstrength-reduction rules to convert to shifts before we examine\ndivide-of-multiply. That's an extra layer of heuristic engineering\nthat must be considered when designing the optimizer.<\/p>\n<p>Onto the scene, then, comes a new data structure: the <em>e-graph<\/em>, or\n<em>equivalence graph<\/em>, which is a kind of sea-of-nodes\nprogram\/expression representation that can represent <em>many different\nequivalent forms of a program at once<\/em>. The key idea is that, rather\nthan have a single expression node as a referent for any value, we\nhave an e-class (equivalence class) that contains many e-nodes, and we\ncan pick any of these e-nodes to compute the value.<\/p>\n<p>The idea is a sort of <em>principled<\/em> approach to the optimization\nproblem: let's model the state space explicitly, and then pick the\nbest result objectively. Typically one uses the result of an e-graph\nby \"extracting\" one possible representation of the program according\nto a cost metric. (More on this below, but a simple cost metric could\nbe a static number per operator kind, plus cost of inputs.)<\/p>\n<p>The magic of e-graphs is how they can <em>compress<\/em> a very large\ncombinatorial space of equivalent programs into a small data\nstructure. A detailed exploration of how this works is beyond the\nscope of this blog post (please read the egg paper: it's very good!)\nbut a very short intuitive summary might be something like:<\/p>\n<ul>\n<li>\n<p>Ensuring that all value uses point to an <em>e-class<\/em> rather than a\nparticular node will propagate knowledge of equivalences to\nmaximally many places. That is, if we know that <code>op1 v1, v2<\/code> is\nequivalent to <code>op2 v3, v4<\/code>, all users of the <code>op1 v1, v2<\/code> expression\nshould automatically get the knowledge propagated that they can use\nany form. This knowledge propagation is the essence of \"equality\nsaturation\" that e-graphs enable.<\/p>\n<\/li>\n<li>\n<p>A strong regime of canonicalization and \"re-interning\"\n(re-hashconsing), which the egg paper calls \"rebuilding\", ensures\nthat such information is maximally propagated. Basically, when we\ndiscover that the <code>op1<\/code> and <code>op2<\/code> expressions above are equivalent,\nwe re-process all users of both op1 and op2, looking for more\nfollow-on consequences. Merging those two might in turn cause other\nexpressions to be equivalent or other rewrite rules to fire.<\/p>\n<\/li>\n<\/ul>\n<h3 id=\"practical-efficiency-of-classical-e-graphs\">Practical Efficiency of Classical E-graphs<\/h3>\n<p>The two problems that arise with a \"classical e-graph\" (by which I\ninclude the 2021 egg paper's batched-rebuilding formulation) are\n<em>blowup<\/em> -- that is, too many rewrite rules apply and the e-graph\nbecomes too large -- and <em>data-structure inefficiency<\/em>.<\/p>\n<p>The blowup problem is easier to understand: if we allow for\nrepresenting many different forms of the program, maybe we will\nrepresent too many, and run out of memory and processing time. It is\noften hard to control how rules will compose and lead to blowup, as\nwell: each rewrite rule may seem reasonable in isolation, but the\ntransitive closure of all possible programs under a well-developed set\nof equivalences can be massive. So practical applications of e-graphs\nusually need some kind of meta\/strategy driver layer that uses \"fuel\"\nto bound effort, and\/or selectively applies rewrites where they are\nlikely to lead to better outcomes. Even then, this operating regime\noften has compile-times measured in seconds or worse. This may be\nappropriate for certain kinds of optimization problems where\ncompilation happens once or rarely and the quality of the outcome is\nextremely important (e.g., hardware design), but not for a fast\ncompiler like Cranelift.<\/p>\n<p>We can protect against such outcomes with careful heuristics, though,\nand the possibility of allowing for objective choice of the best\npossible expression is still very tempting. So in my initial\nexperiments, I applied the <a rel=\"external\" href=\"https:\/\/crates.io\/crates\/egg\">egg crate<\/a>\nto the problem and eventually, with custom tweaks, managed to get\ne-graph roundtripping to <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/27#issuecomment-1176689988\">23%\noverhead<\/a>\n-- with no rewrites applied. That's not bad at first glance but it\nproposes to replace an optimization pipeline that itself takes only\n10% of compile-time, and we haven't yet added the rewrites to the\n23%. (And the 23% came after a good amount of data-structure\nengineering to reduce storage; the initial overhead was over 2x.)<\/p>\n<p>In profiling the optimizer's execution, the overheads were occurring\nmore or less in <em>building the e-graph itself<\/em> (that is, cache misses\nthroughout the code transcribing IR to the e-graph). And what does the\ne-graph contain? Per e-class, it contains a \"parent pointer\" list: we\nneed to track users of every e-class so that we can re-canonicalize\nthem during the \"rebuild\" step when e-classes are merged (a new\nequivalence is discovered). And, even more fundamentally, it stores\ne-nodes separately from e-classes, which is an essential element of\nthe idea but means that we have (at least) two different entities for\neach value, even when most e-classes have only one e-node.<\/p>\n<p>Is there any way to simplify the data structures so that we don't have\nto store so many different bits for one value?<\/p>\n<h3 id=\"insight-1-implicit-e-graph-in-the-ssa-ir\">Insight #1: Implicit E-graph in the SSA IR<\/h3>\n<p>The first major insight that enabled efficient implementation of an\ne-graph in Cranelift was that we could <em>redefine the existing IR into\nan implicit e-graph<\/em>, without copying over the whole function body\ninto an e-graph and back, thus avoiding the compile-time penalty of\nthis data movement. (Data movement can be very expensive when the main\nloops of a program are otherwise fairly optimized! It is best to keep\nand operate on data in-place whenever possible.)<\/p>\n<p>We start with a sea-of-nodes-with-CFG, where we have an IR with SSA\nvalues not placed in basic blocks. We can already build this\n\"in-place\" in Cranelift's IR, CLIF, by removing existing SSA\ndefinitions from the CFG but keeping their data in the data-flow graph\n(DFG) data structures.<\/p>\n<p>Then, to allow for multi-representation in an e-graph, the idea is to\ndiscard the separation between e-classes and e-nodes, and instead\ndefine a new kind of IR node that is a <em>union<\/em> node. Rather than two\nindex spaces, for e-nodes and e-classes, we have only one index space,\nthe SSA value space. An SSA value is either an ordinary operator\nresult or a block parameter (as before), or a <em>union<\/em> of two other SSA\nvalues. Any arbitrary e-class can then be represented via a binary\ntree of union nodes. We don't need to change anything about operator\narguments to make use of this representation: operators already refer\nto value numbers, and an e-class of multiple e-nodes (defined by the\n\"top\" union node in its union tree) already has a value number.<\/p>\n<p>The coolest thing about this representation is: once we have a\nsea-of-nodes, it is <em>already implicitly an e-graph<\/em>, with \"trivial\"\n(one-member) e-classes for each e-node. Thus, the lift from\nsea-of-nodes to e-graph is a no-op -- the best (and cheapest) kind of\ncompile-time pass. We only pay for multi-representation when we use\nthat capability, creating union nodes as needed.<\/p>\n<h3 id=\"insight-2-acyclicity-with-eager-rewrites\">Insight #2: Acyclicity with Eager Rewrites<\/h3>\n<p>The other aspect of the classical e-graph data structure's cost has to\ndo with its need to <em>rebuild<\/em>, and in order to do so, to track all\nuses of an e-class (its \"parents\" in egg's terminology). Cranelift\ndoes not keep bidirectional use-def links, and the binary tree of\nunion nodes would make this even more complex still to track.<\/p>\n<p>In trying to address this cost, I started with a somewhat radical\nquestion: what would happen if we <em>never<\/em> rebuilt (to propagate\nequalities)? How much \"optimization goodness\" would that give up?<\/p>\n<p>If one (i) builds an e-graph then (ii) applies rewrite rules to find\nbetter versions of nodes, adding to e-classes, then the answer is that\nthis would hardly work at all: this would mean that all users of a\nvalue would see only its initial form and never its rewrites. The\nrewritten forms would float in the sea-of-nodes, and union-nodes\njoining them to the original forms would exist, but no users would\nactually refer to those union nodes.<\/p>\n<p>Instead, what is needed is to apply rewrites <em>eagerly<\/em>. When we create\na new node in the sea-of-nodes, we apply all rewrites immediately,\nthen join those rewrites with the original form with union nodes. The\n\"top\" of that union tree is then the value number used as the\n\"optimized form\" of that original value, referenced by all subsequent\nuses.<\/p>\n<p>The union-node representation plays a key part of this story: it acts\nas an <em>immutable data structure<\/em> in a sense, where we always append\nnew knowledge and union it with existing values, and refer to that\n\"newer version\" of an e-class; but we never go back and update\nexisting references.<\/p>\n<p>This has a very nice implication for the graph structure of the sea of\nnodes as well: it preserves acyclicity! Classical e-graphs, in their\nrebuild step, can create cycles even when the input is acyclic because\nthey can condense nodes arbitrarily. But when we eagerly rewrite, then\nfreeze, we can never \"tie the knot\" and create a cycle.<\/p>\n<p>This acyclicity is important because it permits a <em>single pass<\/em> for\nthe rewrites. In fact, taking our sea-of-nodes build algorithm above\nas a baseline, we can add eager rewriting as a very small change: when\nwe apply rewrites, we build a \"union-node spine\" to join all rewritten\nforms, rather than destructively take only the rewritten form.<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>def canonicalize_and_rewrite(basic_block):<\/span><\/span>\n<span class=\"giallo-l\"><span>  for inst in basic_block:<\/span><\/span>\n<span class=\"giallo-l\"><span>    if is_pure(inst):<\/span><\/span>\n<span class=\"giallo-l\"><span>      # ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      if inst in hashcons_map:<\/span><\/span>\n<span class=\"giallo-l\"><span>        # ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      else:<\/span><\/span>\n<span class=\"giallo-l\"><span>        optimized = rewrites(inst)                     # NEW<\/span><\/span>\n<span class=\"giallo-l\"><span>        union = join_with_union_nodes(inst, optimized) # NEW<\/span><\/span>\n<span class=\"giallo-l\"><span>        optimized_form[inst.def] = union<\/span><\/span>\n<span class=\"giallo-l\"><span>    else:<\/span><\/span>\n<span class=\"giallo-l\"><span>       # ...<\/span><\/span>\n<span class=\"giallo-l\"><\/span><\/code><\/pre>\n<p>All of these aspects work together and cannot really be separated:<\/p>\n<ul>\n<li>Union nodes allow for a cheap, pay-as-you-go representation for\ne-classes, without a two-level data structure (nodes and classes)\nand without parent pointers.<\/li>\n<li>Eager rewriting, applied as we build the e-graph (sea of nodes),\nallows for a single-pass algorithm and ensures all members of the\ne-class are present before it is \"sealed\" by union nodes and\nreferenced by uses.<\/li>\n<li>Acyclicity, present in the input (because of SSA), is preserved by\nthe append-only, immutable nature of union nodes, and permits eager\nrewriting to work in a single pass.<\/li>\n<\/ul>\n<p>Note that here we are glossing over <em>recursive<\/em> rewrites. Due to space\nconstraints I will only outline the problem and solution briefly: the\nright-hand side of a rewrite rule application (<code>rewrites<\/code> above) will\nproduce nodes that themselves may be able to trigger further\nrewrites. Rather than leave this to another iteration of a rewrite\nloop, as a classical e-graph driver might do, we want to eagerly\nrewrite this right-hand side as well before establishing any uses of\nit. So we recursively re-invoke <code>rewrites<\/code>; and this occurs within the\nright-hand side of rules as <em>pieces<\/em> of the final expression are\ncreated, as well. This recursion is tightly bounded (in levels and in\ntotal rewrite invocations per top-level loop iteration) to prevent\nblowup.<\/p>\n<p>Finally, we are also glossing over details of how we apply our\npattern-matching\/rewrite DSL, ISLE, to the rewrite problem when\n<em>multiple<\/em> rewrites are now permitted. In brief, we extended the\nlanguage to permit \"multi-extractors\" and \"multi-constructors\": rather\nthan matching only one rule, and disambiguating by priority, we take\nall applicable rules. The\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/27\">RFC<\/a> has more\ndetails.<\/p>\n<h3 id=\"the-extraction-problem\">The Extraction Problem<\/h3>\n<p>So we now have a way to represent <em>multiple<\/em> expressions as\nalternatives to compute the same value. How do we compile this\nprogram? It surely wouldn't make sense to compile <em>all<\/em> of these\nexpressions: they produce the same bits, so we only need one. Which\none do we pick?<\/p>\n<p>This is the <em>extraction problem<\/em>, and it is both easy to state and\ndeceptively hard (in fact, NP-hard): choose the <em>easiest<\/em> (cheapest)\nexpression to compute any given value.<\/p>\n<p>Why is this <em>hard<\/em>? First, let's construct the case where it's easy.\nLet's say that we have one root expression (say, returned from a\nfunction) with all pure operators. This forms a tree of choices: each\neclass lets us choose one enode to compute it, and that enode has\narguments that themselves refer to eclasses with choices.<\/p>\n<p>Given this <em>tree<\/em> of choices, with every choice independent, we can\npick the best choice for each subtree, and compute the cost of any\ngiven expression node as best-cost-of-args plus that own node's cost\nto compute. In more formal algorithmic terms, that is <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Optimal_substructure\"><em>optimal\nsubstructure<\/em><\/a>.<\/p>\n<p>Unfortunately, as soon as we permit <em>references to shared nodes<\/em> (a\nDAG rather than a tree), this nice structure evaporates. To see why,\nconsider: we could have two eclasses we wish to compute<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>v0 = union v10, v11<\/span><\/span>\n<span class=\"giallo-l\"><span>v1 = union v10, v12<\/span><\/span><\/code><\/pre>\n<p>with computations (not shown) <code>v10<\/code> that costs 10 units to compute,\nand <code>v11<\/code> and <code>v12<\/code> that each cost 7 units to compute. The optimal\nchoice at each subproblem is to choose the cheaper computation (<code>v11<\/code>\nor <code>v12<\/code>), but the program would actually be more globally optimal if\nwe computed only <code>v10<\/code> (cost of 10 total). A solver that tries to\nrecognize this would either process each root (<code>v0<\/code> and <code>v1<\/code>) one at a\ntime and \"backtrack\" at some point once it sees the additional use, or\nsomehow build a shared representation of the problem, which is no\nlonger deconstructed in a way that permits sub-problem solutions to\ncompose.<\/p>\n<p>In fact, the extraction problem is <em>NP-hard<\/em>. To see why, I will show\na simple linear-time reduction (mapping) from a known NP-hard problem,\nweighted set-cover, to eclass extraction.<\/p>\n<p>Take each weighted set <code>S<\/code> with weight <code>w<\/code> and elements <code>S = { x_1, x_2, ... }<\/code>. Add an enode (operator with args) <code>N<\/code>, with self-cost\n(not including args) <code>w<\/code>, and no arguments. Then for each element\n<code>x_n<\/code> in the universe (the union of all sets' elements), define an\neclass: that is, if we have an <code>x_i<\/code>, define an eclass <code>C_i<\/code>. Then for\neach set-element edge (for each <code>i<\/code>, <code>j<\/code> such that <code>x_i \u2208 S_j<\/code>), add\nan enode to <code>C_i<\/code> with opaque zero-cost operator <code>SetElt_ij(y)<\/code> where\n<code>y<\/code> is the eclass for <code>x_i<\/code>.<\/p>\n<p>Performing an optimal (lowest-cost) extraction, with all eclasses\n<code>C_i<\/code> taken as roots, will compute the lowest-weight set cover: the\nchoice of enode in each eclass <code>C_i<\/code> encodes which set we choose to\ncover element <code>x_i<\/code>. Thus, because egraph extraction with shared\nstructure can compute the solution to an NP-hard problem (weighted set\ncover), egraph extraction with shared structure is NP-hard.<\/p>\n<p>OK, but we want a fast compiler. What do we do?<\/p>\n<p>The classical compiler-literature answer to this problem -- seen over\nand over in a 50-year history -- is \"solve a simpler approximation\nproblem\". Register allocation, for example, is filled with simplified\nproblem models (linear scan, no live-range splitting, ...) that\nreduce the decision space and allow for a simpler algorithm.<\/p>\n<p>In our case, we solve the extraction problem with a simplifying\nchoice: we will not try to account for shared substructure and the way\nthat it complicates accounting of cost. In other words, we'll ignore\nshared substructure, pretending that each use of a subtree counts that\nsubtree's cost anew. For each enode, having computed the cost of each\nof its arguments, we can compute its own cost easily as the sum of its\narguments plus its own computation cost; and for each eclass, we can\npick the minimum-cost enode. That's it!<\/p>\n<p>We implement this with a <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Dynamic_programming\">dynamic\nprogramming<\/a>\nalgorithm: we do a toposort of the aegraph (which can always be done,\nbecause it's acyclic), then process nodes from leaves upward,\naccumulating cost and picking minima at each subproblem. <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/dd2dd8d9f0a0a06c34e364716d58acf67236ba6a\/cranelift\/codegen\/src\/egraph\/elaborate.rs#L307-L391\">This is a\nsingle\npass<\/a>\nand is a relatively fast and straightforward algorithm.<\/p>\n<p>After the Dagstuhl seminar in January, I had an ongoing discussion\nwith collaborators Alexa VanHattum and Nick Fitzgerald about whether\nwe could do better here. Alexa and Nick both prototyped a bunch of\ninteresting alternatives: dynamically updating (shortcutting to zero)\ncosts when subtrees become used (\"sunk-cost\" accounting), computing\ncosts by doing full top-down traversals rather than bottom-up dynamic\nprogramming (and then mixing in memoization somehow), trying to\naccount for sharing by doing DP but <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/12230\">tracking the full set of covered\nleaves<\/a>, and\nsome other things. This was an interesting exploration but in the end\nwe didn't find anything that looked better in the compile-time \/\nexecution-time tradeoff space. We have an <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/12156\">issue tracking\nthis<\/a> and\nmore ideas are always welcome, of course.<\/p>\n<h3 id=\"other-aspects\">Other Aspects<\/h3>\n<p>There are two other aspects of our aegraph implementation that I don't\nhave space to go into in this post:<\/p>\n<ul>\n<li>\n<p>There is an interesting problem that arises with respect to the\ndomtree and SSA invariants when different values are merged together\nwith a union node and some of them have wider \"scope\" than\nothers. For example, via store-to-load forwarding we may know that a\nload instruction produces a constant <code>0<\/code>; so we might have a union\nnode with <code>iconst 0<\/code>. The load can only happen at its current\nlocation, but <code>iconst 0<\/code> can be computed anywhere. A user of this\neclass should be able to pick either value (said another way:\nextraction should not be load-bearing for correctness). If the user\nis within the dominance subtree under the load, then all is fine,\nbut if not, e.g. if some other user of <code>iconst 0<\/code> elsewhere in the\nfunction errantly happened upon the eclass-neighbor load\ninstruction, we might get an invalid program.<\/p>\n<p>There are many ways one might be tempted to solve this, but in the\nend we landed on an \"available block\" analysis that runs as we build\nnodes. For every node, we record which block is the \"highest\" in the\ndomtree that it can be computed: function entry for pure\nzero-argument nodes, current block for any impure nodes, otherwise\nthe lowest node in the domtree among available blocks for all\narguments. (Claim: the available-nodes for all args of a node will\nform an ancestor path in the domtree; one will always exist that is\ndominated by all others. This follows from the properties of SSA.)\nThen when we insert into the hashcons map, we insert at the level\nthat the final union is available.<\/p>\n<\/li>\n<li>\n<p>We also have an important optimization that we call <code>subsume<\/code>. This\nis an identity operator that wraps a value returned by a rewrite\nrule. It is not required for correctness, but its semantics are: if\nany value is marked \"subsume\", all \"subsuming\" values <em>erase<\/em>\nexisting members of the eclass. Usually, only one subsuming rule\nwill match (but this, also, is not necessary for correctness).<\/p>\n<p>The usual use-case is for rules that have clear \"directionality\": it\nis always better to say <code>2<\/code> than <code>(iadd 1 1)<\/code>, so let's go ahead and\nshrink the eclass so that all further matching, and eventual\nextraction, is more efficient.<\/p>\n<\/li>\n<\/ul>\n<h2 id=\"evaluation\">Evaluation<\/h2>\n<p>So how does all of this actually work? Do aegraphs benefit Cranelift's\nstrength as a compiler -- its ability to optimize code, its efficiency\nin doing so quickly, or both?<\/p>\n<p>This is the part where I offer a somewhat surprising conclusion: the\ntl;dr of this post is that I believe the <em>sea-of-nodes-with-CFG<\/em>\naspect of this mid-end works great, but the <em>aegraph itself<\/em> -- the\nability to represent multiple options for one value -- may not (yet?)\nbe pulling its weight. It doesn't really hurt much either, so maybe\nit's a reasonable capability to keep around. But in any case, it's an\ninteresting conclusion and we'll dig more into it below.<\/p>\n<p>The main interesting evaluation is a two-dimension comparison of\n<em>compile time<\/em> -- that is, how long Cranelift takes to compile code --\non the X-axis, versus <em>execution time<\/em> -- that is, how long the\nresulting code takes to execute -- on the Y-axis. This forms a\ntradeoff space: it may be good to spend a little more time to compile\nif the resulting code runs faster (or vice-versa), for example. Of\ncourse, reducing both is best. One point may be \"strictly better\" than\nanother if it reduces both -- then there is no tradeoff, because one\nwould always choose the configuration that both compiles faster and\nproduces better code. (One can then find the <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Pareto_front\">Pareto\nfrontier<\/a> of points that\nform a set in which none is strictly better than another -- these are\nall \"valid configuration points\" that one may rationally choose\ndepending on one's goals.)<\/p>\n<p>Below we have a compile-time vs. execution-time plot for a number of\nconfigurations of Cranelift, compiling and running the <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/sightglass\/\">Sightglass\nbenchmark suite<\/a>:<\/p>\n<ul>\n<li>No optimizations enabled;<\/li>\n<li>The (on by default) aegraph-based optimization pipeline, as\ndescribed in this post, with several variants (below);<\/li>\n<li>A \"classical optimization pipeline\" that does <em>not<\/em> form a\nsea-of-nodes-with-CFG at all; instead, it applies exactly the same\nrewrite rules, but in-place, and interleaves with classical GVN and\nLICM passes;<\/li>\n<li>Variants of the aegraphs pipeline and classical pipeline with the\nwhole mid-end repeated 2 or 3 times (to test whether code continues\nto get better).<\/li>\n<\/ul>\n<p>Here's the main result:<\/p>\n<p><img src=\"\/assets\/2026-04-09-scatterplot-web.svg\" alt=\"Figure: compile time vs. execution time\" \/><\/p>\n<p>A few conclusions are in order. First, the aegraph pipeline does\ngenerate better code than the classical pipeline. This objective\nresult is \"mission accomplished\" with respect to the aegraph effort's\noriginal motivation: we wanted to allow optimization passes to\ninteract more finely and optimize more completely. Note in particular\nthat repeating the classical pipeline multiple times does <em>not<\/em> get\nthe same result; we could not have obtained the ~2% speedup without\nbuilding a new optimization framework.<\/p>\n<p>Second, though, there is clearly a Pareto frontier that includes \"no\noptimizations\" and \"classical pipeline\" as well as the aegraph\nvariants: each takes more compilation time than the previous. In other\nwords, moving from a classical compiler pipeline to the design\ndescribed here, we spend about 7-8% more compile time. Notably, this\nis <em>not<\/em> the result that we had when we first built the aegraphs\nimplementation in 2023 and switched over -- at that time, we were more\nor less at parity. This is likely a result of the growth of the body\nof rewrite rules over the intervening three years.<\/p>\n<p>To get a better picture of how aegraph's various design choices\nmatter, let's zoom into the area in the red ellipse above, which\ncontains multiple <em>variants<\/em> of the aegraphs pipeline:<\/p>\n<ul>\n<li>\"aegraph\": Exactly as described in this post, and default Cranelift\nconfiguration;<\/li>\n<li>\"no multivalue (eager pick)\": sea-of-nodes-with-CFG, without union\nnodes; i.e., not actually representing more than one equivalent\nvalue in an eclass. Instead, after evaluating rewrite rules, we pick\nthe best option and use that one option (destructively replacing the\noriginal);<\/li>\n<li>\"no rematerialization\": testing the effect of this aspect of the\nelaboration algorithm;<\/li>\n<li>\"no subsume\": testing this efficiency tweak of the rewrite-rule\napplication.<\/li>\n<\/ul>\n<p>Here's the plot:<\/p>\n<p><img src=\"\/assets\/2026-04-09-scatterplot-aegraph-web.svg\" alt=\"Figure: aegraphs variants\" \/><\/p>\n<p>One can see that there are some definite tradeoffs, <em>but<\/em> looking\nclosely at the axis scales, these effects are very very small. In\nparticular, moving from sea-of-nodes-with-CFG to true aegraph (taking\nall rewritten values, and picking the best in a principled way with\ncost-based extraction) nets us ~0.1% execution-time improvement, at\n~0.005% compile-time cost. That's more-or-less in the noise.<\/p>\n<p>Supporting that conclusion is the statistic that the average eclass\nsize after rewriting is 1.13 enodes: in other words, very few cases\nwith our ruleset and benchmark corpus actually result in more than one\noption.<\/p>\n<p>Finally, the most interesting question in my view: does the <em>eager<\/em>\naspect of aegraphs -- applying rewrite rules right away, and never\ngoing back to \"fill in\" other equivalences -- matter? In other words,\ndoes skipping equality saturation take the egraph goodness out of an\negraph(-alike)?<\/p>\n<p>We can measure this, too: I instrumented our implementation to track\nwhen a subtree of an eclass is <em>not<\/em> chosen by extraction, and then\nany node in that subtree is later actually elaborated (in other words,\nwhen we use a suboptimal choice because we could not see an equality\nin the \"wrong\" direction). This should only happen if, in theory, our\nrules rewrite <code>f<\/code> to <code>g<\/code> where <code>cost(g) &gt; cost(f)<\/code>, and we don't have\na rewrite <code>g<\/code> to <code>f<\/code>: then a user of <code>g<\/code> might never directly get a\nrewrite of <code>f<\/code> eagerly, but a later coincidentally-occurring <code>f<\/code> might\nrewrite onto <code>g<\/code> (but we'll never propagate that equality into the\noriginal users of <code>g<\/code>).<\/p>\n<p>It turns out that, in all of our benchmarks, with ~4 million value\nnodes created overall, this happens two (2) times. Both instances\noccur in <code>spidermonkey.wasm<\/code> (a large benchmark that consists of the\nSpiderMonkey JS engine, compiled to WebAssembly, then run through\nWasmtime+Cranelift), and occur due to an ireduce-of-iadd rewrite rule\nthat violates this move-toward-lower-cost principle (explicitly, in\nthe name of simplicity). Overall, we conclude that the eager rewrites\nare effective <em>as long as<\/em> the ruleset is designed with <em>optimization<\/em>\n(rather than mere exploration of all equivalent expressions) in mind.<\/p>\n<h2 id=\"discussion\">Discussion<\/h2>\n<p>The most surprising conclusion in all of the data was, for me, that\naegraphs (per se) -- multi-value representations -- don't seem to\nmatter. What?! That was the entire point of the project, and (proper)\ne-graphs have seen great promise in other application areas.<\/p>\n<p>I think the main reason for this is that our workload is somewhat\n\"small\" in a combinatorial possibility-space sense: we are (i)\ncompiling workloads that are often optimized already (as Wasm modules)\nbefore hitting the Cranelift compilation pipeline, and (ii) applying a\nset of rewrite rules that, while large and growing (hundreds of\nrules), explicitly do <em>not<\/em> include identities like associativity and\ncommutativity, or arbitrary algebraic identities, that do not\n\"simplify\" somehow. In other words, if we're generally applying\nrewrites that look more like simple, obvious \"cleanups\", we would\nexpect that we don't hold a \"superposition\" of multiple good\nexpression options very often.<\/p>\n<p>Given that it doesn't cost us <em>that<\/em> much compile time to keep\naegraphs around, though, maybe this is... fine? Having the\n<em>capability<\/em> to do principled cost-based extraction is great, versus\nhaving to think about whether a rewrite rule should exist. We still do\ntry to be careful not to introduce rules that are <em>never<\/em> productive,\nof course.<\/p>\n<p>And, further into the future, one could imagine that workloads with\nmore optimization opportunity could cause more interesting situations\nto occur within the aegraph, leading to more emergent composition in\nthe rewrites.<\/p>\n<h2 id=\"future-directions\">Future Directions<\/h2>\n<p>There are a bunch of directions we could (and should) take this in the\nfuture. In terms of evaluation: finding the \"corner of the use-case\ndomain\" where aegraphs truly shine is still an open question. More\nconcretely: if we evaluate Cranelift with new and different workloads,\nand\/or pile on more rewrite rules, do we get to a point where the\nclassical benefit of \"multi-representation with cost-based extraction\"\npays off in a conventional compiler? I don't know!<\/p>\n<p>There is also still a lot of room to improve the core algorithms:<\/p>\n<ul>\n<li>\n<p>Better extraction, as mentioned above: something that accounts for\nshared substructure would be great, as long as we don't have to pay\nthe NP-hard cost for it. Maybe there's a nice approximation\nalgorithm that's better than our current dynamic-programming\napproach.<\/p>\n<\/li>\n<li>\n<p>We'd like to be able to handle more rewrites that alter the <em>CFG\nskeleton<\/em> as well. Right now, we have a separate ISLE entry-point\nthat allows for destructive rewriting of skeleton instructions\n(thanks to my colleague Nick Fitzgerald for building this!). However,\nmaybe we could remove redundant block parameters (phi nodes), for\nexample; and\/or maybe we could fold branches; and\/or maybe we could\napply path-sensitive knowledge to values when used in certain\ncontrol-flow contexts (<code>x=1<\/code> in the dominance subtree under the\n\"true\" branch for <code>if x==1, goto ...<\/code>). My former colleague Jamey\nSharp wrote up a few excellent, in-depth issues on these topics in\nour tracker\n(<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/5623\">#5623<\/a>,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/6109\">#6109<\/a>,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/6129\">#6129<\/a>)\nand I think there is a lot of potential here.<\/p>\n<p>(The full version of this is, again, something like\n<a rel=\"external\" href=\"https:\/\/arxiv.org\/abs\/1912.05036\">RVSDG<\/a> in the node language seems\nlike the most principled option to express all useful forms of\ncontrol-flow rewrites; Jamey also has a <a rel=\"external\" href=\"https:\/\/github.com\/jameysharp\/optir\">prototype called\n<code>optir<\/code><\/a> for this.)<\/p>\n<\/li>\n<li>\n<p>It would be interesting to experiment with incorporating our\n<em>lowering backend rules<\/em> into the aegraph somehow: they are a rich,\nfruitful target-specific database of natural \"costs\" for various\noperations. For example, on AArch64 we can fold shifts and extends\ninto (some) arithmetic operations for \"free\"; maybe this alters the\nextraction choices we make. Or likewise for the various odd corners\nof addressing modes on each architecture.<\/p>\n<p>The simple version of this idea is to incorporate lowering rules as\nrewrites, and make the egraph's node language a union of CLIF and\nthe machine's instruction set. But maybe there's something better we\ncould do instead, allowing multi-extractors to see the aegraph\neclasses directly and keeping various VCode sequences. I need to\nwrite up more of my ideas on this topic someday. Jamey also has more\nthoughts on this in\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/8529\">#8529<\/a>.<\/p>\n<\/li>\n<\/ul>\n<p>I'm sure there are other things that could be done here too!<\/p>\n<h2 id=\"further-reading\">Further Reading<\/h2>\n<ul>\n<li>\n<p>I gave a talk about aegraphs at <a rel=\"external\" href=\"https:\/\/pldi23.sigplan.org\/home\/egraphs-2023\">EGRAPHS\n2023<\/a>:\n<a rel=\"external\" href=\"https:\/\/cfallin.org\/pubs\/egraphs2023_aegraphs_slides.pdf\">slides<\/a>,\n<a rel=\"external\" href=\"https:\/\/vimeo.com\/843540328\">re-recorded video<\/a> (the original was\nnot recorded).<\/p>\n<\/li>\n<li>\n<p>I gave a talk about aegraphs at the January 2026 <a rel=\"external\" href=\"https:\/\/www.dagstuhl.de\/seminars\/seminar-calendar\/seminar-details\/26022\">Dagstuhl e-graphs\nseminar<\/a>;\nthe <a href=\"\/assets\/cfallin-aegraphs-dagstuhl-20260108.pdf\">slides<\/a> are a\nheavily updated and amended version of the 2023 talk, with the\nexperiments\/data I presented here.<\/p>\n<\/li>\n<li>\n<p>There is a Cranelift RFC on aegraphs\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/27\">here<\/a>, and one on\nISLE (the rewrite DSL that we use to drive rewrites in the aegraph)\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/15\">here<\/a>.<\/p>\n<\/li>\n<li>\n<p>The main PR that implemented the current form of aegraphs is\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/5382\">here<\/a>,\nco-authored by my former colleague Jamey Sharp (this production\nimplementation was a fantastically fun and productive\npair-programming project!).<\/p>\n<\/li>\n<\/ul>\n<h2 id=\"acknowledgments\">Acknowledgments<\/h2>\n<p>Thanks to many folks for discussion of the ideas around aegraphs\nthrough the years: Nick Fitzgerald, Jamey Sharp, Trevor Elliott, Max\nWillsey, Alexa VanHattum, Max Bernstein, and many others at the\nDagstuhl e-graphs seminar. None of them reviewed this post (it had\nbeen languishing for too long already and I wanted to get it out) so all fault\nfor any errors herein is solely my own!<\/p>\n<hr \/>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>Possibly with masking of the top bit if our IR semantics have\ndefined wrapping\/truncation behavior: <code>x &amp; 0x7fff..ffff<\/code>.<\/p>\n<\/div>\n"},{"title":"Exceptions in Cranelift and Wasmtime","published":"2025-11-06T00:00:00+00:00","updated":"2025-11-06T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2025\/11\/06\/exceptions\/"}},"id":"https:\/\/cfallin.org\/blog\/2025\/11\/06\/exceptions\/","content":"<p><em>Note: this post is also cross-posted to the <a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/articles\/\">Bytecode Alliance\nblog<\/a>\n<a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/articles\/wasmtime-exceptions\">here<\/a>.<\/em><\/p>\n<p>This is a blog post outlining the odyssey I recently took to implement\nthe <a rel=\"external\" href=\"https:\/\/github.com\/webassembly\/exception-handling\">Wasm exception-handling\nproposal<\/a> in\n<a rel=\"external\" href=\"https:\/\/wasmtime.dev\/\">Wasmtime<\/a>, the open-source\n<a rel=\"external\" href=\"https:\/\/webassembly.org\/\">WebAssembly<\/a> engine for which I'm a core\nteam member\/maintainer, and its <a rel=\"external\" href=\"https:\/\/cranelift.dev\/\">Cranelift<\/a>\ncompiler backend.<\/p>\n<p>When first discussing this work, I made an off-the-cuff estimate in\nthe Wasmtime biweekly project meeting that it would be \"maybe two\nweeks on the compiler side and a week in Wasmtime\". Reader, I need to\nmake a confession now: I was wrong and it was <em>not<\/em> a three-week\ntask. This work spanned from late March to August of this year\n(roughly half-time, to be fair; I wear many hats). Let that be a\nlesson!<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup><\/p>\n<p>In this post we'll first cover what exceptions are and why some\nlanguages want them (and what other languages do instead) -- in\nparticular what the big deal is about (so-called) \"zero-cost\"\nexception handling. Then we'll see how Wasm has specified a\nbytecode-level foundation that serves as a least-common denominator\nbut also has some unique properties. We'll then take a roundtrip\nthrough what it means for a <em>compiler<\/em> to support exceptions -- the\ncontrol-flow implications, how one reifies the communication with the\nunwinder, how all this intersects with the ABI, etc. -- before finally\nlooking at how Wasmtime puts it all together (and is careful to avoid\nperformance pitfalls and stay true to the intended performance of the\nspec).<\/p>\n<h2 id=\"why-exceptions\">Why Exceptions?<\/h2>\n<p>Many readers will already be familiar with exceptions as they are\npresent in languages as widely varied as\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Python_(programming_language)\">Python<\/a>,\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Java_(programming_language)\">Java<\/a>,\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/JavaScript\">JavaScript<\/a>,\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/C%2B%2B\">C++<\/a>,\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Lisp_(programming_language)\">Lisp<\/a>,\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/OCaml\">OCaml<\/a>, and many more. But let's\nbriefly review so we can (i) be precise what we mean by an exception,\nand (ii) discuss <em>why<\/em> exceptions are so popular.<\/p>\n<p><a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Exception_handling_(programming)\">Exception\nhandling<\/a>\nis a mechanism for <em>nonlocal flow control<\/em>. In particular, most\nflow-control constructs are <em>intraprocedural<\/em> (send control to other\ncode in the current function) and <em>lexical<\/em> (target a location that\ncan be known statically). For example, <code>if<\/code> statements and <code>loop<\/code>s\nboth work this way: they stay within the local function, and we know\nexactly where they will transfer control. In contrast, exceptions are\n(or can be) <em>interprocedural<\/em> (can transfer control to some point in\nsome other function) and <em>dynamic<\/em> (target a location that depends on\nruntime state).<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup><\/p>\n<p>To unpack that a bit: an exception is <em>thrown<\/em> when we want to signal\nan error or some other condition that requires \"unwinding\" the current\ncomputation, i.e., backing out of the current context; and it is\n<em>caught<\/em> by a \"handler\" that is interested in the particular kind of\nexception and is currently \"active\" (waiting to catch that\nexception). That handler can be in the current function, or in any\nfunction that has called it. Thus, an exception throw and catch can\nresult in an abnormal, early return from a function.<\/p>\n<p>One can understand the need for this mechanism by considering how\nprograms can handle errors. In some languages, such as Rust, it is\ncommon to see function signatures of the form <code>fn foo(...) -&gt; Result&lt;T, E&gt;<\/code>. The\n<a rel=\"external\" href=\"https:\/\/doc.rust-lang.org\/std\/result\/enum.Result.html\"><code>Result<\/code><\/a> type\nindicates that <code>foo<\/code> normally returns a value of type <code>T<\/code>, but may\nproduce an error of type <code>E<\/code> instead. The key to making this ergonomic\nis providing some way to \"short-circuit\" execution if an error is\nreturned, propagating that error upward: that is, Rust's <code>?<\/code> operator,\nfor example, which turns into essentially \"if there was an error,\nreturn that error from this function\".<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup> This is quite conceptually\nnice in many ways: why should error handling be different than any\nother data flow in the program? Let's describe the type of results to\ninclude the possibility of errors; and let's use normal control flow\nto handle them. So we can write code like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> f<\/span><span class=\"z-punctuation\">()<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> Result<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">u32<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-entity z-name z-type\"> Error<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  if<\/span><span class=\"z-variable z-other\"> bad<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    return<\/span><span class=\"z-entity z-name z-type\"> Err<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-entity z-name z-type\">Error<\/span><span class=\"z-keyword z-operator\">::<\/span><span class=\"z-entity z-name z-function\">new<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator\">...<\/span><span class=\"z-punctuation\">));<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-type\">  Ok<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-constant z-numeric\">0<\/span><span class=\"z-punctuation\">)<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> g<\/span><span class=\"z-punctuation\">()<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> Result<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">u32<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-entity z-name z-type\"> Error<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ The `?` propagates any error to our caller, returning early.<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  let<\/span><span class=\"z-variable z-other\"> result<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-function\"> f<\/span><span class=\"z-punctuation\">()<\/span><span class=\"z-keyword z-operator\">?<\/span><span class=\"z-punctuation\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-type\">  Ok<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">result<\/span><span class=\"z-keyword z-operator\"> +<\/span><span class=\"z-constant z-numeric\"> 1<\/span><span class=\"z-punctuation\">)<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>and we don't have to do anything special in <code>g<\/code> to propagate errors\nfrom <code>f<\/code> further, other than use the <code>?<\/code> operator.<\/p>\n<p>But there is a <em>cost<\/em> to this: it means that every error-producing\nfunction has a larger return type, which might have ABI implications\n(another return register at least, if not a stack-allocated\nrepresentation of the <code>Result<\/code> and the corresponding loads\/stores to\nmemory), and also, there is at least one conditional branch after\nevery call to such a function that checks if we need to handle the\nerror. The dynamic efficiency of the \"happy path\" (with no thrown\nexceptions) is thus impacted. Ideally, we skip any cost unless an\nerror actually occurs (and then perhaps we accept slightly more cost\nin that case, as tradeoffs often go).<\/p>\n<p>It turns out that this is possible with the <em>help of the language\nruntime<\/em>. Consider what happens if we omit the <code>Result<\/code> return types\nand error checks at each return. We will need to reach the code that\nhandles the error in some other way. Perhaps we can jump directly to\nthis code somehow?<\/p>\n<p>The key idea of \"zero-cost exception handling\" is to get the compiler\nto build side-tables to <em>tell us<\/em> where this code -- known as a\n\"handler\" -- is. We can walk the callstack, visiting our caller and\nits caller and onward, until we find a function that would be\ninterested in the error condition we are raising. This logic is\nimplemented with the help of these side-tables and some code in the\nlanguage runtime called the \"unwinder\" (because it \"unwinds\" the\nstack). If no errors are raised, then none of this logic is executed\nat runtime. And we no longer have our explicit checks for error\nreturns in the \"happy path\" where no errors occur. This is why the\ncommon term for this style of error-handling is called \"zero-cost\":\nmore precisely, it is zero-cost when <em>no<\/em> errors occur, but the\nunwinding in case of error can still be expensive.<\/p>\n<p>This is the status quo for exception-handling implementations in most\nproduction languages: for example, in the C++ world, exception\nhandling is commonly implemented via the <a rel=\"external\" href=\"https:\/\/itanium-cxx-abi.github.io\/cxx-abi\/abi-eh.html\">Itanium C++\nABI<\/a><sup class=\"footnote-reference\"><a href=\"#4\">4<\/a><\/sup>, which\ndefines a comprehensive set of tables emitted by the compiler and a\ncomplex dance between the system unwinding library and\ncompiler-generated code to find and transfer control to\nhandlers. Handler tables and stack unwinders are common in interpreted\nand just-in-time (JIT)-compiled language implementations, too: for\nexample, SpiderMonkey has <a rel=\"external\" href=\"https:\/\/searchfox.org\/firefox-main\/rev\/50a34d25155fd70628ee69c7d68a2509c0e3445d\/js\/src\/vm\/StencilEnums.h#18\">try\nnotes<\/a>\non its bytecode (so named for \"try blocks\") and a\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/firefox-main\/rev\/a5316cedc669bcec09efae23521e0af6b9d3d257\/js\/src\/jit\/JitFrames.cpp#691\">HandleException<\/a>\nfunction that <a rel=\"external\" href=\"https:\/\/searchfox.org\/firefox-main\/rev\/a5316cedc669bcec09efae23521e0af6b9d3d257\/js\/src\/jit\/JitFrames.cpp#751-845\">walks stack\nframes<\/a>\nto find a handler.<\/p>\n<h2 id=\"the-wasm-exception-handling-spec\">The Wasm Exception-Handling Spec<\/h2>\n<p>The WebAssembly specification now (since version 3.0) has <a rel=\"external\" href=\"https:\/\/github.com\/webassembly\/exception-handling\">exception\nhandling<\/a>. This\nproposal was a long time in the making by various folks in the\nstandards, toolchain and browser worlds, and the CG (standards group)\nhas now merged it into the spec and included it in the\nrecently-released \"Wasm 3.0\" milestone. If you're already familiar\nwith the proposal, you can skip over this section to the Cranelift-\nand Wasmtime-specific bits below.<\/p>\n<p>First: let's discuss <em>why<\/em> Wasm needs an extension to the bytecode\ndefinition to support exceptions. As we described above, the key idea\nof zero-cost exception handling is that an unwinder visits stack\nframes and looks for handlers, transferring control directly to the\nfirst handler it finds, outside the normal function return\npath. Because the call stack is <em>protected<\/em>, or not directly readable\nor writable from Wasm code (part of Wasm's <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Control-flow_integrity\">control-flow\nintegrity<\/a>\naspect), an unwinder that works this way necessarily must be a\nprivileged part of the Wasm runtime itself. We can't implement it in\n\"userspace\" because there is no way for Wasm bytecode to transfer\ncontrol directly back to a distant caller, aside from a chain of\nreturns. This missing functionality is what the extension to the\nspecification adds.<\/p>\n<p>The implementation comes down to only three opcodes (!), and some new\ntypes in the bytecode-level type system. (In other words -- given the\nlength of this post -- it's deceptively simple.) These opcodes are:<\/p>\n<ul>\n<li>\n<p><code>try_table<\/code>, which wraps an inner body, and specifies <em>handlers<\/em> to\nbe active during that body. For example:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>(block $b1    ;; defines a label for a forward edge to the end of this block<\/span><\/span>\n<span class=\"giallo-l\"><span>  (block $b2  ;; likewise, another label<\/span><\/span>\n<span class=\"giallo-l\"><span>    (try_table<\/span><\/span>\n<span class=\"giallo-l\"><span>      (catch $tag1 $b1) ;; exceptions with tag `$tag1` will be caught by code at $b1<\/span><\/span>\n<span class=\"giallo-l\"><span>      (catch_all $b2)   ;; all other exceptions will be caught by code at $b2<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>      body...)))<\/span><\/span><\/code><\/pre>\n<p>In this example, if an exception is thrown from within the code in\n<code>body<\/code>, and it matches one of the specified tags (more below!),\ncontrol will transfer to the location defined by the end of the\ngiven block. (This is the same as other control-flow transfers in\nWasm: for example, a branch <code>br $b1<\/code> also jumps to the end of\n<code>$b1<\/code>.)<\/p>\n<p>This construct is the single all-purpose \"catch\" mechanism, and is\npowerful enough to directly translate typical <code>try<\/code>\/<code>catch<\/code> blocks\nin most programming languages with exceptions.<\/p>\n<\/li>\n<li>\n<p><code>throw<\/code>: an instruction to directly throw a new exception. It\ncarries the tag for the exception, like: <code>throw $tag1<\/code>.<\/p>\n<\/li>\n<li>\n<p><code>throw_ref<\/code>, used to rethrow an exception that has already been\ncaught and is held by reference (more below!).<\/p>\n<\/li>\n<\/ul>\n<p>And that's it! We implement those three opcodes and we are \"done\".<\/p>\n<h3 id=\"payloads\">Payloads<\/h3>\n<p>That's not the whole story, of course. Ordinarily a source language\nwill offer the ability to carry some <em>data<\/em> as part of an exception:\nthat is, the error condition is not just one of a static set of kinds\nof errors, but contains some fields as well. (E.g.: not just \"file not\nfound\", but \"file not found: $PATH\".)<\/p>\n<p>One could build this on top of an bytecode-level exception-throw\nmechanism that only had throw\/catch with static tags, with the help of\nsome global state, but that would be cumbersome; instead, the Wasm\nspecification offers <em>payloads<\/em> on each exception. For full\ngenerality, this payload can actually take the form of a <em>list<\/em> of\nvalues; i.e., it is a full product type (struct type).<\/p>\n<p>We alluded to \"tags\" above but didn't describe them in detail. These\ntags are key to the payload definition: each tag is effectively a type\ndefinition that specifies its list of payload value types as\nwell. (Technically, in the Wasm AST, a tag definition names a\n<em>function type<\/em> with only parameters, no returns, which is a nice way\nof reusing an existing entity\/concept.) Now we show how they are\ndefined with a sample module:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>(module<\/span><\/span>\n<span class=\"giallo-l\"><span> ;; Define a &quot;tag&quot;, which serves to define the specific kind of exception<\/span><\/span>\n<span class=\"giallo-l\"><span> ;; and specify its payload values.<\/span><\/span>\n<span class=\"giallo-l\"><span> (tag $t (param i32 i64))<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span> (func $f (param i32 i64)<\/span><\/span>\n<span class=\"giallo-l\"><span>       ;; Throw an exception, to be caught by whatever handler is &quot;closest&quot;<\/span><\/span>\n<span class=\"giallo-l\"><span>       ;; dynamically.<\/span><\/span>\n<span class=\"giallo-l\"><span>       (throw $t (local.get 0) (local.get 1)))<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span> (func $g (result i32 i64)<\/span><\/span>\n<span class=\"giallo-l\"><span>       (block $b (result i32 i64)<\/span><\/span>\n<span class=\"giallo-l\"><span>              ;; Run a body below, with the given handlers (catch-clauses)<\/span><\/span>\n<span class=\"giallo-l\"><span>              ;; in-scope to catch any matching exceptions.<\/span><\/span>\n<span class=\"giallo-l\"><span>              ;;<\/span><\/span>\n<span class=\"giallo-l\"><span>              ;; Here, if an exception with tag `$t` is thrown within the body,<\/span><\/span>\n<span class=\"giallo-l\"><span>              ;; control is transferred to the end of block `$b` (as if we had<\/span><\/span>\n<span class=\"giallo-l\"><span>              ;; branched to it), with the payload values for that exception<\/span><\/span>\n<span class=\"giallo-l\"><span>              ;; pushed to the operand stack.<\/span><\/span>\n<span class=\"giallo-l\"><span>              (try_table (catch $t $b)<\/span><\/span>\n<span class=\"giallo-l\"><span>                         (call $f (i32.const 1) (i64.const 2)))<\/span><\/span>\n<span class=\"giallo-l\"><span>              (i32.const 3)<\/span><\/span>\n<span class=\"giallo-l\"><span>              (i64.const 4))))<\/span><\/span><\/code><\/pre>\n<p>Here we've defined one tag (the Wasm text format lets us attach a name\n<code>$t<\/code>, but in the binary format it is only identified by its index, 0),\nwith two payload values. We can throw an exception with this tag given\nvalues of these types (as in function <code>$f<\/code>) and we can catch it if we\nspecify a catch destination as the end of a block meant to return\nexactly those types as well.  Here, if function <code>$g<\/code> is invoked, the\nexception payload values <code>1<\/code> and <code>2<\/code> will be thrown with the\nexception, which will be caught by the <code>try_table<\/code>; the results of\n<code>$g<\/code> will be <code>1<\/code> and <code>2<\/code>. (The values <code>3<\/code> and <code>4<\/code> are present to allow\nthe Wasm module to validate, i.e. have correct types, but they are\ndynamically unreachable because of the throw in <code>$f<\/code> and will not be\nreturned.)<\/p>\n<p>This is an instance where Wasm, being a bytecode, can afford to\ngeneralize a bit relative to real-metal ISAs and offer conveniences to\nthe Wasm producer (i.e., toolchain generating Wasm modules). In this\nsense, it is a little more like a compiler IR. In contrast, most other\nexception-throw ABIs have a fixed definition of payload, e.g., one or\ntwo machine register-sized values. In practice some producers might\nchoose a small fixed signature for all exception tags anyway, but\nthere is no reason to impose such an artificial limit if there is a\ncompiler and runtime behind the Wasm in any case.<\/p>\n<h3 id=\"unwind-cleanup-and-destructors\">Unwind, Cleanup, and Destructors<\/h3>\n<p>So far, we've seen how Wasm's primitives can allow for basic exception\nthrows and catches, but what about languages with scoped resources,\ne.g. C++ with its destructors? If one writes something like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"cpp\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span> Scoped<\/span><span class=\"z-punctuation z-section\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-function\">    Scoped<\/span><span class=\"z-punctuation z-section\">() {}<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-function\">    ~Scoped<\/span><span class=\"z-punctuation z-section\">() {<\/span><span class=\"z-entity z-name z-function\"> cleanup<\/span><span class=\"z-punctuation z-section\">()<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">void<\/span><span class=\"z-entity z-name z-function\"> f<\/span><span class=\"z-punctuation z-section\">() {<\/span><\/span>\n<span class=\"giallo-l\"><span>    Scoped<\/span><span class=\"z-entity z-name z-function\"> s<\/span><span class=\"z-punctuation z-section\">()<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    throw<\/span><span class=\"z-entity z-name z-function\"> my_exception<\/span><span class=\"z-punctuation z-section\">()<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><\/span><\/code><\/pre>\n<p>then the <code>throw<\/code> should transfer control out of <code>f<\/code> and upward to\nwhatever handler matches, but the destructor of <code>s<\/code> still needs to run\nand call <code>cleanup<\/code>. This is not quite a \"catch\" because we don't want\nto terminate the search: we aren't actually handling the error\ncondition.<\/p>\n<p>The usual approach to compile such a program is to \"catch and\nrethrow\". That is, the program is lowered to something like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"cpp\"><span class=\"giallo-l\"><span class=\"z-keyword\">try<\/span><span class=\"z-punctuation z-section\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    throw<\/span><span> ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><span class=\"z-entity z-name z-function\"> catch_any<\/span><span class=\"z-punctuation z-section\">(<\/span><span>e<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-function\">    cleanup<\/span><span class=\"z-punctuation z-section\">()<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span>    rethrow e<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><\/span><\/code><\/pre>\n<p>where <code>catch_any<\/code> catches <em>any<\/em> exception propagating past this point\non the stack, and <code>rethrow<\/code> re-throws the same exception.<\/p>\n<p>Wasm's exception primitives provide exactly the pieces we need for\nthis: a <code>catch_all_ref<\/code> clause, which <em>catches all exceptions<\/em> and\n<em>boxes the caught exception as a reference<\/em>; and a <code>throw_ref<\/code>\ninstruction, which <em>re-throws a previously-caught exception<\/em>.<sup class=\"footnote-reference\"><a href=\"#5\">5<\/a><\/sup><\/p>\n<p>In actuality there is a two-by-two matrix of \"catch\" options: we can\n<code>catch<\/code> a specific tag or <code>catch_all<\/code>; and we can catch and\nimmediately unpack the exception into its payload values (as we saw\nabove), or we can catch it as a reference. So we have <code>catch<\/code>,\n<code>catch_ref<\/code>, <code>catch_all<\/code>, and <code>catch_all_ref<\/code>.<sup class=\"footnote-reference\"><a href=\"#6\">6<\/a><\/sup><\/p>\n<h3 id=\"dynamic-identity-and-compositionality\">Dynamic Identity and Compositionality<\/h3>\n<p>There is one final detail to the Wasm proposal, and in fact it's the\npart that I find the most interesting and unique. Given the above\nintroduction, and any familiarity with exception systems in other\nlanguage semantics and\/or runtime systems, one might expect that the\n\"tags\" identifying kinds of exceptions and matching throws with\nparticular catch handlers would be static labels. In other words, if I\nthrow an exception with tag <code>$tA<\/code>, then the first handler for <code>$tA<\/code>\nanywhere up the stack, from any module, should catch it.<\/p>\n<p>However, one of Wasm's most significant properties as a bytecode is\nits emphasis on isolation. It has a distinction between static\n<em>modules<\/em> and dynamic <em>instances<\/em> of those modules, and modules have\nno \"static members\": every entity (e.g., memory, table, or global\nvariable) defined by a module is replicated per instance of that\nmodule. This creates a clean separation between instances and means\nthat, for example, one can freely reuse a common module (say, some\nkind of low-level glue or helper module) with separate instances in\nmany places without them somehow communicating or interfering with\neach other.<\/p>\n<p>Consider what happens if we have an instance A that invokes some other\n(dynamically provided) function reference which ultimately invokes a\ncallback in A. Say that the instance throws an exception from within\nits callback in order to unwind all the way to its outer stack frames,\nacross the intermediate functions in some other Wasm instance(s):<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>                A.f   ---------call---------&gt;   B.g   --------call---------&gt;    A.callback<\/span><\/span>\n<span class=\"giallo-l\"><span>                 ^                                                                  v<\/span><\/span>\n<span class=\"giallo-l\"><span>               catch $t                                                           throw $t<\/span><\/span>\n<span class=\"giallo-l\"><span>                 |                                                                  |<\/span><\/span>\n<span class=\"giallo-l\"><span>                 `----------------------------&lt;-------------------------------------&#39;<\/span><\/span><\/code><\/pre>\n<p>The instance A expects that the exception that it throws from its\ncallback function to <code>f<\/code> is a <em>local<\/em> concern to that instance only,\nand that B cannot interfere. After all, if the exception tag is\ndefined inside A, and Wasm preserves modularity, then B should not be\nable to name that tag to catch exceptions by that tag, even if it also\nuses exception handling internally. The two modules should not\ninteract: that is the meaning of modularity, and it permits us to\nreason about each instance's behavior locally, with the effects of\n\"the rest of the world\" confined to imports and exports.<\/p>\n<p>Unfortunately, if one designed a straightforward \"static\" tag-matching\nscheme, this might not be the case if B were an instance of the same\nmodule as A: in that case, if B also used a tag <code>$t<\/code> internally and\nregistered handlers for that tag, it could interfere with the desired\nthrow\/catch behavior, and violate modularity.<\/p>\n<p>So the Wasm exception handling standard specifies that tags have\n<em>dynamic instances<\/em> as well, just as memories, tables and globals\ndo. (Put in programming-language theory terms, tags are <em>generative<\/em>.)\nEach instance of a module creates its own dynamic identities for the\nstatically-defined tags in those modules, and uses those dynamic\nidentities to tag exceptions and find handlers. This means that no\nmatter what instance B is, above, if instance A does not export its\ntag <code>$t<\/code> for B to import, there is no way for B to catch the thrown\nexception explicitly (it can still catch <em>all<\/em> exceptions, and it may\ndo so and rethrow to perform some cleanup). Local modular reasoning is\nrestored.<\/p>\n<p>Once we have tags as dynamic entities, just like Wasm memories, we can\ntake the same approach that we do for the other entities to allow them\nto be imported and exported. Thus, visibility of exception payloads\nand ability for modules to catch certain exceptions is completely\ncontrolled by the instantiation graph and the import\/export linking,\njust as for all other Wasm storage.<\/p>\n<p>This is surprising (or at least was to me)! It creates some pretty\nunique implementation challenges in the unwinder -- in essence, it\nmeans that we need to know about instance identity for each stack\nframe, not just static code location and handler list.<\/p>\n<h2 id=\"compiling-exceptions-in-cranelift\">Compiling Exceptions in Cranelift<\/h2>\n<p>Before we implement the primitives for exception handling in Wasmtime,\nwe need to support exceptions in our underlying compiler backend,\nCranelift.<\/p>\n<p>Why should this be a compiler concern? What is special about\nexceptions that makes them different from, say, new Wasm instructions\nthat implement additional mathematical operators (when we already have\nmany arithmetic operators in the IR), or Wasm memories (when we\nalready have loads\/stores in the IR)?<\/p>\n<p>In brief, the complexities come in three flavors: new kinds of control\nflow, fundamentally different than ordinary branches or calls in that\nthey are \"externally actuated\" (by the unwinder); a new facet of the\nABI (that we get to define!) that governs how the unwinder interacts\nwith compiled code; and interactions between the \"scoped\" nature of\nhandlers and inlining in particular. We'll talk about each below.<\/p>\n<p>Note that much of this discussion started with an\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/36\">RFC<\/a> for\nWasmtime\/Cranelift, which had been posted way back in August of 2024\nby Daniel Hillerstrom with help from my colleague Nick Fitzgerald, and\nwas discussed then; many of the choices within were subsequently\nrefined as I discovered interesting nuances during implementation and\nwe talked them through.<\/p>\n<h3 id=\"control-flow\">Control Flow<\/h3>\n<p>There are a few ways to think about exception handlers from the point\nof view of compiler <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Intermediate_representation\">IR (intermediate\nrepresentation)<\/a>.\nFirst, let's recognize that exception handling (i) is a form of\ncontrol flow, and (ii) has all the same implications various compiler\nstages that other kinds of control flow do. For example, the register\nallocator has to consider how to get registers into the right state\nwhenever control moves from one basic block to the next (\"edge\nmoves\"); exception catches are a new kind of edge, and so the regalloc\nneeds to be aware of that, too.<\/p>\n<p>One could see every call or other opcode that could throw as having\nregular control-flow edges to every possible handler that could\nmatch. I'll call this the \"regular edges\" approach. The upside is that\nit's pretty simple to retrofit: one \"only\" needs to add new kinds of\ncontrol-flow opcodes that have out-edges, but that's already a kind of\nthing that IRs have. The disadvantage is that, in functions with a lot\nof possible throwing opcodes and\/or handlers, the overhead can get\nquite high. And control-flow graph overhead is a bad kind of overhead:\nmany analyses' runtimes are heavily dependent the edge and node (basic\nblock) counts, sometimes superlinearly.<\/p>\n<p>The other major option is to build a kind of <em>implicit<\/em> new control\nflow into the IR's semantics. For example, one could lower the\nsource-language semantics of a \"try block\" down to regions in the IR,\nwith one set of handlers attached.  This is clearly more efficient\nthan adding out-edges from (say) every callsite within the try-block\nto every handler in scope. On the other hand, it's hard to understate\nhow invasive this change would be. This means that <em>every<\/em> traversal\nover IR, analyzing dataflow or reachability or any other property, has\nto consider these new implicit edges anyway. In a large established\ncompiler like Cranelift, we can lean on Rust's type system for a lot\nof different kinds of refactors, but changing a fundamental invariant\ngoes beyond that: we would likely have a long tail of issues stemming\nfrom such a change, and it would permanently increase the cognitive\noverhead of making new changes to the compiler. In general we want to\ntrend toward a smaller, simpler core and compositional rather than\nentangled complexity.<\/p>\n<p>Thus, the choice is clear: in Cranelift we opted to introduce one new\ninstruction, <code>try_call<\/code>, that calls a function and catches (some)\nexceptions.  In other words, there are now two possible kinds of\nreturn paths: a normal return or (possibly one of many) exceptional\nreturn(s). The handled exceptions and block targets are enumerated in\nan <em>exception table<\/em>. Because there are control-flow edges stemming\nfrom this opcode, it is a block terminator, like a conditional\nbranch. It looks something like (in Cranelift's IR, CLIF):<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>function %f0(i32) -&gt; i32, f32, f64 {<\/span><\/span>\n<span class=\"giallo-l\"><span>    sig0 = (i32) -&gt; f32 tail<\/span><\/span>\n<span class=\"giallo-l\"><span>    fn0 = %g(i32) -&gt; f32 tail<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>    block0(v1: i32):<\/span><\/span>\n<span class=\"giallo-l\"><span>        v2 = f64const 0x1.0<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; exception-catching callsite<\/span><\/span>\n<span class=\"giallo-l\"><span>        try_call fn0(v1), sig0, block1(ret0, v2), [ tag0: block2(exn0), default: block3(exn0) ]<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>    ;; normal return path<\/span><\/span>\n<span class=\"giallo-l\"><span>    block1(v3: f32, v4: f64):<\/span><\/span>\n<span class=\"giallo-l\"><span>        v5 = iconst.i32 1<\/span><\/span>\n<span class=\"giallo-l\"><span>        return v5, v3, v4<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>    ;; exception handler for tag0<\/span><\/span>\n<span class=\"giallo-l\"><span>    block2(v6: i64):<\/span><\/span>\n<span class=\"giallo-l\"><span>        v7 = ireduce.i32 v6<\/span><\/span>\n<span class=\"giallo-l\"><span>        v8 = iadd_imm.i32 v7, 1<\/span><\/span>\n<span class=\"giallo-l\"><span>        v9 = f32const 0x0.0        <\/span><\/span>\n<span class=\"giallo-l\"><span>        return v8, v9, v2<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>    ;; exception handler for all other exceptions<\/span><\/span>\n<span class=\"giallo-l\"><span>    block2(v10: i64):<\/span><\/span>\n<span class=\"giallo-l\"><span>        v11 = ireduce.i32 v10<\/span><\/span>\n<span class=\"giallo-l\"><span>        v12 = f32.const 0x0.0<\/span><\/span>\n<span class=\"giallo-l\"><span>        v13 = f64.const 0x0.0<\/span><\/span>\n<span class=\"giallo-l\"><span>        return v11, v12, v13<\/span><\/span>\n<span class=\"giallo-l\"><span>}<\/span><\/span><\/code><\/pre>\n<p>There are a few aspects to note here. First, why are we only concerned\nwith calls? What about other sources of exceptions? This is an\nimportant invariant in the IR: exception <em>throws<\/em> are <em>only externally\nsourced<\/em>. In other words, if an exception has been thrown, if we go\ndeep enough into the callstack, we will find that that throw was\nimplemented by calling out into the runtime.  The IR itself has no\nother opcodes that throw! This turns out to be sufficient: (i) we only\nneed to build what Wasmtime needs, here, and (ii) we can implement\nWasm's throw opcodes as \"libcalls\", or calls into the Wasmtime\nruntime. So, within Cranelift-compiled code, exception throws always\nhappen at callsites. We can thus get away with adding only one opcode,\n<code>try_call<\/code>, and attach handler information directly to that opcode.<\/p>\n<p>The next characteristic of note is that handlers are ordinary basic\nblocks.  Thus may not seem remarkable unless one has seen other\ncompiler IRs, such as LLVM's, where exception handlers are definitely\nspecial: they start with \"landing pad\" instructions, and cannot be\nbranched to as ordinary basic blocks. That might look something like:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>function %f() {<\/span><\/span>\n<span class=\"giallo-l\"><span>    block0:<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; Callsite defining a return value `v0`, with normal<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; return path to `block1` and exception handler `block2`.<\/span><\/span>\n<span class=\"giallo-l\"><span>        v0 = try_call ..., block1, [ tag0: block2 ]<\/span><\/span>\n<span class=\"giallo-l\"><span>        <\/span><\/span>\n<span class=\"giallo-l\"><span>    block1:<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; Normal return; use returned value.<\/span><\/span>\n<span class=\"giallo-l\"><span>        return v0<\/span><\/span>\n<span class=\"giallo-l\"><span>        <\/span><\/span>\n<span class=\"giallo-l\"><span>    block2 exn_handler: ;; Specially-marked block!<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; Exception handler payload value.<\/span><\/span>\n<span class=\"giallo-l\"><span>        v1 = exception_landing_pad<\/span><\/span>\n<span class=\"giallo-l\"><span>        ...<\/span><\/span>\n<span class=\"giallo-l\"><span>}<\/span><\/span><\/code><\/pre>\n<p>This bifurcation of kinds of blocks (normal and exception handler) is\nundesirable from our point of view: just as exceptional edges add a\nnew cross-cutting concern that every analysis and transform needs to\nconsider, so would new kinds of blocks with restrictions. It was an\nexplicit design goal (and we have tests that show!) that the same\nblock can be both an ordinary block and a handler block -- not because\nthat would be common, necessarily (handlers usually do very different\nthings than normal code paths), but because it's one less weird quirk\nof the IR.<\/p>\n<p>But then if handlers are normal blocks, the data flow question becomes\nvery interesting. An exception-catching call, unlike every other\nopcode in our IR, has <em>conditionally-defined values<\/em>: that is, its\nnormal function return value(s) are available only if the callee\nreturns normally, and the <em>exception payload value(s)<\/em>, which are\npassed in from the unwinder and carry information about the caught\nexception, are available only if the callee throws an exception that\nwe catch. How can we ensure that these values are represented such\nthat they can only be used in valid ways? We can't make them all\nregular SSA definitions of the opcode: that would mean that all\nsuccessors (regular return and exceptional) get to use them, as in:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>function %f() {<\/span><\/span>\n<span class=\"giallo-l\"><span>    block0:<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; Callsite defining a return value `v0`, with normal return path<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; to `block1` and exception handler `block2`.<\/span><\/span>\n<span class=\"giallo-l\"><span>        v0 = try_call ..., block1, [ tag0: block2 ]<\/span><\/span>\n<span class=\"giallo-l\"><span>      <\/span><\/span>\n<span class=\"giallo-l\"><span>    block1:<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; Use `v0` legally: it is defined on normal return.<\/span><\/span>\n<span class=\"giallo-l\"><span>        return v0<\/span><\/span>\n<span class=\"giallo-l\"><span>      <\/span><\/span>\n<span class=\"giallo-l\"><span>    block2:<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; Oops! We use `v0` here, but the normal return value is undefined<\/span><\/span>\n<span class=\"giallo-l\"><span>        ;; when an exception is caught and control reaches this handler block.<\/span><\/span>\n<span class=\"giallo-l\"><span>        return v0<\/span><\/span>\n<span class=\"giallo-l\"><span>}<\/span><\/span><\/code><\/pre>\n<p>This is the reason that a compiler may choose to make handler blocks\nspecial: by bifurcating the universe of blocks, one ensures that\nnormal-return and exceptional-return values are used only where\nappropriate. Some compiler IRs reify exceptional return payloads via\n\"landing pad\" instructions that must start handler blocks, just as\nphis start regular blocks (in phi- rather than blockparam-based\nSSA). But, again, this bifurcation is undesirable.<\/p>\n<p>Our insight here, after <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/36\">a lot of\ndiscussion<\/a>, was to\nput the definitions where they belong: <em>on the edges<\/em>. That is,\nregular returns are only defined once we know we're following the\nregular-return edge, and likewise for exception payloads. But we don't\nwant to have special instructions that must be in the successor\nblocks: that's a weird distributed invariant and, again, likely to\nlead to bugs when transforming IR. Instead, we leverage the fact that\nwe use <em>blockparam-based SSA<\/em> and we widen the domain of allowable\nblock-call arguments.<\/p>\n<p>Whereas previously one might end a block like <code>brif v1, block2(v2, v3), block3(v4, v5)<\/code>, i.e. with blockparams assigned values in the\nchosen successor via a list of value-uses in the branch, we now allow\n(i) SSA values, (ii) a special \"normal return value\" sentinel, or\n(iii) a special \"exceptional return value\" sentinel. The latter two\nare indexed because there can be more than one of each. So one can\nwrite a block-call in a <code>try_call<\/code> as <code>block2(ret0, v1, ret1)<\/code>, which\npasses the two return values of the call and a normal SSA value; or\n<code>block3(exn0, exn1)<\/code>, which passes just the two exception payload\nvalues.  We do have a new well-formedness check on the IR that ensures\nthat (i) normal returns are used only in the normal-return blockcall,\nand exception payloads are used only in the handler-table blockcalls;\n(ii) normal returns' indices are bounded by the signature; and (iii)\nexception payloads' indices are bounded by the ABI's number of\nexception payload values; but all of these checks are local to the\ninstruction, not distributed across blocks. That's nice, and conforms\nwith the way that all of our other instructions work, too. (Block-call\nargument types are then checked against block-parameter types in the\nsuccessor block, but that happens the same as for any branch.) So we\nhave, repeating from above, a callsite like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    block1:<\/span><\/span>\n<span class=\"giallo-l\"><span>        try_call fn0(v1), block2(ret0), [ tag0: block3(exn0, exn1) ]<\/span><\/span><\/code><\/pre>\n<p>with all of the desired properties: only one kind of block, explicit\ncontrol flow, and SSA values defined only where they are legal to use.<\/p>\n<p>All of this may seem somewhat obvious in hindsight, but as attested by\nthe above GitHub discussions and Cranelift weekly meeting minutes, it\nwas far from clear when we started how to design all of this to\nmaximize simplicity and generality and minimize quirks and\nfootguns. I'm pretty happy with our final design: it feels like a\nnatural extension of our core blockparam-SSA control flow graph, and I\nmanaged to <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10510\">put it into the\ncompiler<\/a>\nwithout too much trouble at all (well, <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10502\">a\nfew<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10485\">PRs<\/a> and\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10555\">associated<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10554\">fixes<\/a> to\nCranelift\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/214\">and<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/220\">regalloc2<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/216\">functionality<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/224\">and testing<\/a>;\nand I'm sure I've missed a few).<\/p>\n<h3 id=\"data-flow-and-abi\">Data Flow and ABI<\/h3>\n<p>So we have defined an IR that can express exception handlers -- what\nabout the interaction between this function body and the unwinder? We\nwill need to define a different kind of semantics to nail down that\ninterface: in essence, it is a property of the <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Application_Binary_Interface\">ABI (Application\nBinary\nInterface)<\/a>.<\/p>\n<p>As mentioned above, existing exception-handling ABIs exist for native\ncode, such as compiled C++. While we are certainly willing to draw\ninspiration from native ABIs and align with them as much as makes\nsense, in Wasmtime we already define our own ABI<sup class=\"footnote-reference\"><a href=\"#7\">7<\/a><\/sup>, and so we are\nnot necessarily constrained by existing standards.<\/p>\n<p>In particular, there is a very good reason we would prefer not to: to\nunwind to a particular exception handler, register state must be\nrestored as specified in the ABI, and the standard Itanium ABI\nrequires the usual callee-saved (\"non-volatile\") registers on the\ntarget ISA to be restored. But this requires (i) having the register\nstate at time of throw, and (ii) processing unwind metadata at each\nstack frame as we walk up the stack, reading out values of saved\nregisters from stack frames. The latter is <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/2710\">already\nsupported<\/a>\nwith a generic \"unwind pseudoinstruction\" framework I built four years\nago, but would still add complexity to our unwinder, and this\ncomplexity would be load-bearing for correctness; and the former is\nextremely difficult with Wasmtime's normal runtime-entry\ntrampolines. So we instead choose to have a simpler exception ABI: all\n<code>try_call<\/code>s, that is, callsites with handlers, clobber <em>all<\/em>\nregisters. This means that the compiler's ordinary register-allocation\nbehavior will save all live values to the stack and restore them on\neither a normal or exceptional return. We only have to restore the\nstack (stack pointer and frame pointer registers) and redirect the\nprogram counter (PC) to a handler.<\/p>\n<p>The other aspect of the ABI that matters to the exception-throw\nunwinder is exceptional payload. The native Itanium ABI specifies two\nregisters on most platforms (e.g.: <code>rax<\/code> and <code>rdx<\/code> on x86-64, or <code>x0<\/code>\nand <code>x1<\/code> on aarch64) to carry runtime-defined playload; so for\nsimplicity, we adopt the same convention.<\/p>\n<p>That's all well and good; now how do we implement <code>try_call<\/code> with the\nappropriate register-allocator behavior to conform to this? We already\nhave fairly complex ABI handling\n(<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/machinst\/abi.rs\">machine-independent<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/isa\/x64\/abi.rs\">and<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/isa\/aarch64\/abi.rs\">five<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/isa\/s390x\/abi.rs\">different<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/isa\/riscv64\/abi.rs\">architecture<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/isa\/pulley_shared\/abi.rs\">implementations<\/a>)\nin Cranelift, but it follows a general pattern: we generate a single\ninstruction at the register-allocator level, and emit uses and defs\nwith fixed-register constraints. That is, we tell regalloc that\nparameters must be in certain registers (e.g., <code>rdi<\/code>, <code>rsi<\/code>, <code>rcx<\/code>,\n<code>rdx<\/code>, <code>r8<\/code>, <code>r9<\/code> on x86-64 System-V calling-convention platforms, or\n<code>x0<\/code> up to <code>x7<\/code> on aarch64 platforms) and let it handle any necessary\nmoves. So in the simplest case, a call might look like (on aarch64),\nwith register-allocator uses\/defs and constraints annotated:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>bl (call) v0 [def, fixed(x0)], v1 [use, fixed(x0)], v2 [use, fixed(x1)]<\/span><\/span><\/code><\/pre>\n<p>It is not always this simple, however: calls are not actually always a\nsingle instruction, and this turned out to be quite problematic for\nexception-handling support. In particular, when values are returned in\nmemory, as the ABI specifies they must be when there are more return\nvalues than registers, we add (or added, prior to this work!) load\ninstructions <em>after<\/em> the call to load the extra results from their\nlocations on the stack. So a callsite might generate instructions like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>bl v0 [def, fixed(x0)], ..., v7 [def, fixed(x7)] # first eight return values<\/span><\/span>\n<span class=\"giallo-l\"><span>ldr v8, [sp]     # ninth return value<\/span><\/span>\n<span class=\"giallo-l\"><span>ldr v9, [sp, #8] # tenth return value<\/span><\/span><\/code><\/pre>\n<p>and so on. This is problematic simply because we said that the\n<code>try_call<\/code> was a terminator; and it is at the IR level, but no longer\nat the regalloc level, and regalloc expects correctly-formed\ncontrol-flow graphs as well. So I had to <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10502\">do a\nrefactor<\/a> to\nmerge these return-value loads into a single regalloc-level\npseudoinstruction, and in turn this cascaded into a few regalloc fixes\n(<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/226\">allowing more than 256\noperands<\/a> and\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/214\">more aggressively splitting live-ranges to allow worst-case\nallocation<\/a>,\nplus a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/220\">fix to the live range-splitting\nfix<\/a> and a\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/216\">fuzzing\nimprovement<\/a>).<\/p>\n<p>There is one final question that might arise when considering the\ninteraction of exception handling and register allocation in\nCranelift-compiled code. In Cranelift, we have an invariant that the\nregister allocator is allowed to insert <em>moves<\/em> between any two\ninstructions -- register-to-register, or loads or stores to\/from\nspill-slots in the stack frame, or moves between different spill-slots\n-- and indeed it does this whenever there is more state than fits in\nregisters. It also needs to insert <em>edge moves<\/em> \"between\" blocks,\nbecause when jumping to another spot in the code, we might need the\nregister values in a differently-assigned configuration. When we have\nan unwinder that jumps to a different spot in the code to invoke a\nhandler, we need to ensure that all the proper moves have executed so\nthe state is as expected.<\/p>\n<p>The answer here turns out to be a careful argument that we don't need\nto do anything at all. (That's the best kind of solution to a problem,\nbut only if one is correct!) The crux of the argument has to do with\ncritical edges. A critical edge is one from a block with multiple\nsuccessors to one with multiple predecessors: for example, in the graph<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>   A    D<\/span><\/span>\n<span class=\"giallo-l\"><span>  \/ \\  \/<\/span><\/span>\n<span class=\"giallo-l\"><span> B   C<\/span><\/span><\/code><\/pre>\n<p>where A can jump to B or C, and D can also jump to C, then A-to-C is a\ncritical edge. The problem with critical edges is that there is\nnowhere to put code that has to run on the transition from A to C (it\ncan't go in A, because we may go to B or C; and it can't go in C,\nbecause we may have come from A or D). So the register allocator\nprohibits them, and we \"split\" them when generating code by inserting\nempty blocks (<code>e<\/code> below) on them:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>   A    D<\/span><\/span>\n<span class=\"giallo-l\"><span>  \/ \\   |<\/span><\/span>\n<span class=\"giallo-l\"><span> |   e  |<\/span><\/span>\n<span class=\"giallo-l\"><span> |   \\ \/<\/span><\/span>\n<span class=\"giallo-l\"><span> B    C<\/span><\/span><\/code><\/pre>\n<p>The key insight is that a <code>try_call<\/code> always has more than one\nsuccessor as long as it has a handler (because it must always have a\nnormal return-path successor too)<sup class=\"footnote-reference\"><a href=\"#8\">8<\/a><\/sup>; and in this case, because we\nsplit critical edges, the immediate successor block on the\nexception-catch path has only one predecessor. So the register\nallocator can always put its moves that have to run on catching an\nexception in the successor (handler) block rather than the predecessor\nblock. Our rule for where to put edge moves prefers the successor\n(block \"after\" the edge) unless it has multiple in-edges, so this was\nalready the case. The only thing we have to be careful about is to\nrecord the address of the <em>inserted edge block<\/em>, if any (<code>e<\/code> above),\nrather than the IR-level handler block (<code>C<\/code> above), in the handler\ntable.<\/p>\n<p>And that's pretty much it, as far as register allocation is concerned!<\/p>\n<p>We've now covered the basics of Cranelift's exception support. At this\npoint, having landed the compiler half but not the Wasmtime half, I\ncontext-switched away for a bit, and in the meantime, bjorn3 picked\nthis support up right away as a means to add panic-unwinding support\nto\n<a rel=\"external\" href=\"https:\/\/github.com\/rust-lang\/rustc_codegen_cranelift\"><code>rustc_codegen_cranelift<\/code><\/a>,\nthe Cranelift-based Rust compiler backend. With <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10593\">a few small\nchanges<\/a> they\ncontributed, and a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10709\">followup edge-case\nfix<\/a> and a\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10609\">refactor<\/a>,\npanic-unwinding support in <code>rustc_codegen_cranelift<\/code> was working. That\nwas very good intermediate validation that what I had built was usable\nand relatively solid.<\/p>\n<h2 id=\"exceptions-in-wasmtime\">Exceptions in Wasmtime<\/h2>\n<p>We have a compiler that supports exceptions; we understand Wasm\nexception semantics; let's build support into Wasmtime! How hard could\nit be?<\/p>\n<h3 id=\"challenge-1-garbage-collection-interactions\">Challenge 1: Garbage Collection Interactions<\/h3>\n<p>I started by sketching out the codegen for each of the three opcodes\n(<code>try_table<\/code>, <code>throw<\/code>, and <code>throw_ref<\/code>). My mental model at the very\nbeginning of this work, having read but not fully internalized the\nWasm exception-handling proposal, was that I would be able to\nimplement a \"basic\" throw\/catch first, and then somehow build the\n<code>exnref<\/code> objects later. And I had figured I could build <code>exnref<\/code>s in a\n(in hindsight) somewhat hacky way, by aggregating values together in a\nkind of tuple and creating a table of such tuples indexed by exnrefs,\njust as Wasmtime does for externrefs.<\/p>\n<p>This understanding quickly gave way to a deeper one when I realized a\nfew things:<\/p>\n<ul>\n<li>\n<p>Exception objects (exnrefs) can carry references to other GC objects\n(that is, GC types can be part of the payload signature of an\nexception), and GC objects can store exnrefs in fields. Hence,\nexnrefs need to be traced, and can participate in GC cycles; this\neither implies an additional collector on top of our GC collector\n(ugh) or means that exception objects needs to be on the GC heap\nwhen GC is enabled.<\/p>\n<\/li>\n<li>\n<p>We'll need a host API to introspect and build exception objects, and\nwe already have nice host APIs for GC objects.<\/p>\n<\/li>\n<\/ul>\n<p>There was a question <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11230\">in an extensively-discussed\nPR<\/a> whether\nwe could build a cheap \"subset\" implementation that doesn't mandate\nthe existence of a GC heap for storing exception objects. This would\nbe great in theory for guests that use exceptions for C-level\nsetjmp\/longjmp but no other GC features.  However, it's a little\ntricky for a few reasons. First, this would require the subset to\nexclude <code>throw_ref<\/code> (so we don't have to invent another kind of\nexception object storage). But it's not great to subset the spec --\nand <code>throw_ref<\/code> is not just for GC guest languages, but also for\nrethrows. Second, more generally, this is additional maintenance and\ntesting surface that we'd rather not have for now. Instead we expect\nthat we can make GC cheap enough, and its growth heuristic smart\nenough that a \"frequent setjmp\/longjmp\" stress-test of exceptions (for\nexample) should live within a very small (e.g., few-kilobyte) GC heap,\nessentially approximating the purpose-built storage. My colleague Nick\nFitzgerald (who built and is driving improvements to Wasmtime's GC\nsupport) wrote up <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/11256\">a nice\nissue<\/a>\ndescribing the tradeoffs and ideas we have.<\/p>\n<p>All of that said, we'll only build one exception object implementation\n-- great! -- but it will have to be a new kind of GC object. This\nspawned a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11230\">large\nPR<\/a> to build\nout exception objects first, prior to actual support for throwing and\ncatching them, with host APIs to allocate them and inspect their\nfields. In essence, they are structs with immutable fields and with a\nless-exposed type lattice and no subtyping.<\/p>\n<h3 id=\"challenge-2-generative-tags-and-dynamic-identity\">Challenge 2: Generative Tags and Dynamic Identity<\/h3>\n<p>So there I was, implementing the <code>throw<\/code> instruction's libcall\n(runtime implementation), and finally getting to the heart of the\nmatter: the unwinder itself, which walks stack frames to find a\nmatching exception handler.  This is the final bit of functionality\nthat ties it all together. We're almost there!<\/p>\n<p>But wait: check out that <a rel=\"external\" href=\"https:\/\/webassembly.github.io\/spec\/core\/exec\/instructions.html#xref-syntax-instructions-syntax-instr-control-mathsf-throw-x\">spec\nlanguage<\/a>.\nWe load the \"tag address\" from the store in step 9: we allocate the\nexception instance <code>{tag z.tags[x], fields val^n}<\/code>. What is this\n<code>tags<\/code> array on the store (<code>z<\/code>) in the runtime semantics? Tags have\ndynamic identity, not static identity! (This is the part where I\nlearned about the thing I described\n<a href=\"https:\/\/cfallin.org\/blog\/2025\/11\/06\/exceptions\/#dynamic-identity-and-compositionality\">above<\/a>.)<\/p>\n<p>This was a problem, because I had defined exception tables to\nassociate handlers with tags that were identified by integer (<code>u32<\/code>)\n-- like most other entities in Cranelift IR, I had figured this would\nbe sufficient to let Wasmtime define indices (say: index of the tag in\nthe module), and then we could compare static tag IDs.<\/p>\n<p>Perhaps this is no problem: the static index defines the entity ID in\nthe module (defined or imported tag), and we can compare that and the\ninstance ID to see if a handler is a match. But how do we get the\ninstance ID from the stack frame?<\/p>\n<p>It turns out that Wasmtime didn't have a way, because nothing had\nneeded that yet. (This deficiency had been noticed before when\nimplementing Wasm coredumps, but there hadn't been enough reason or\nmotivation to fix it then.) So I <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/11285\">filed an\nissue<\/a> with\na few ideas. We could add a new field in every frame storing the\ninstance pointer -- and in fact this is a simple version of what at\nleast one other production Wasm implementation, in the SpiderMonkey\nweb engine,\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/firefox-main\/rev\/643d732886fe0de4e2a3eee3c5ed9bd0d47c77cf\/js\/src\/wasm\/WasmFrame.h#112-115\">does<\/a>\n(though as described in that <code>[SMDOC]<\/code> comment, it only stores\ninstance pointers on transitions between frames of different\ninstances; this is enough for the unwinder when walking linearly up\nthe stack). But that would add overhead to <em>every<\/em> Wasm function (or\nwith SpiderMonkey's approach, require adding trampolines between\ninstances, which would be a large change for Wasmtime), and exception\nhandling is still used somewhat rarely in practice.  Ideally we'd have\na \"pay-as-you-go\" scheme with as little extra complexity as posible.<\/p>\n<p>Instead, I came up with an idea to <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11321\">add \"dynamic context\" items to\nexception handler\nlists<\/a>. The\nidea is that we inject an SSA value into the list and it is stored in\na stack location that is given in the handler table metadata, so the\nstack-walker can find it. To Cranelift, this is some arbitrary opaque\nvalue; Wasmtime will use it to store the raw instance pointer\n(<code>vmctx<\/code>) for use by the unwinder.<\/p>\n<p>This filled out the design to a more general state nicely: it is\nsymmetric with exception payload, in the sense that the compiled code\ncan communicate context or state <em>to<\/em> the unwinder as it reads the\nframes, and the unwinder in turn can communicate data <em>to<\/em> the\ncompiled code when it unwinds.<\/p>\n<p>It turns out -- though I didn't intend this at all at the time -- that\nthis also nicely solves the <em>inlining problem<\/em>. In brief, we want all\nof our IR to be \"local\", not treating the function boundary specially;\nthis way, IR can be composed by the inliner without anything\nbreaking. Storing some \"current instance\" state for the whole function\nwill, of course, break when we inline a function from one module\n(hence instance) into another!<\/p>\n<p>Instead, we can give a nice operational semantics to handler tables\nwith dynamic-context items: the unwinder should read left-to-right,\nupdating its \"current dynamic context\" at each dynamic-context item,\nand checking for a tag match at tag-handler items. Then the inliner\ncan <em>compose<\/em> exception tables: when a <code>try_call<\/code> callsite inlines a\nfunction body as its callee, and that body itself has any other\ncallsites, we attach a handler table that simply concatenates the\nexception table items.<\/p>\n<p>It's important, here, to point out another surprising fact about Wasm\nsemantics: we <em>cannot do certain optimizations<\/em> to resolve handlers\nstatically or optimize the handler list, or at least not naively,\nwithout global program analysis to understand where tags come\nfrom. For example, if we see a handler for tag 0 then one for tag 1,\nand we see a throw for tag 1 directly inside the <code>try_table<\/code>s body, we\ncannot necessarily resolve it: tag 0 and tag 1 could be the same tag!<\/p>\n<p>Wait, how can that be? Well, consider <em>tag imports<\/em>:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>(module<\/span><\/span>\n<span class=\"giallo-l\"><span>  (import &quot;test&quot; &quot;e0&quot; (tag $e0))<\/span><\/span>\n<span class=\"giallo-l\"><span>  (import &quot;test&quot; &quot;e1&quot; (tag $e1))<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>  (func ...<\/span><\/span>\n<span class=\"giallo-l\"><span>        (try_table<\/span><\/span>\n<span class=\"giallo-l\"><span>                   (catch $e0 $b0)<\/span><\/span>\n<span class=\"giallo-l\"><span>                   (catch $e1 $b1)<\/span><\/span>\n<span class=\"giallo-l\"><span>                   (throw $e1)<\/span><\/span>\n<span class=\"giallo-l\"><span>                   (unreachable))))<\/span><\/span><\/code><\/pre>\n<p>We could instantiate this module giving the same dynamic tag instance\ntwice, for both imports; then the first handler (to block <code>$b0<\/code>)\nmatches; or separate tags; then the block <code>$b1<\/code> matches. The only way\nto win the optimization game is not to play -- we have to preserve the\noriginal handler list.  Fortunately, that makes the compiler's job\neasier. We transcribe the <code>try_table<\/code>'s handlers directly to Cranelift\nexception-handler tables, and those directly to metadata in the\ncompiled module, read in exactly that order by the unwinder's\nhandler-matching logic.<\/p>\n<h3 id=\"challenge-3-rooting\">Challenge 3: Rooting<\/h3>\n<p>Since exception objects are GC-managed objects, we have to ensure that\nthey are properly <em>rooted<\/em>: that is, any handles to these objects\noutside of references inside other GC objects need to be known to the\nGC so the objects remain alive (and so the references are updated in\nthe case of a moving GC).<\/p>\n<p>Within a Wasm-to-Wasm exception throw scenario, this is fairly easy:\nthe references are rooted in the compiled code on either side of the\ncontrol-flow transfer, and the reference only briefly passes through\nthe unwinder. As long as we are careful to handle it with the\nappropriate types, all will work fine.<\/p>\n<p>Passing exceptions across the host\/Wasm boundary is another matter,\nthough. We support the full matrix of {host, Wasm} x {host, Wasm}\nexception catch\/throw pairs: that is, exceptions can be thrown from\nnative host code called by Wasm (via a Wasm import), and exceptions\ncan be thrown out of Wasm code and returned as a kind of error to the\nhost code that invoked the Wasm. This works by boxing the exception\ninside an <code>anyhow::Error<\/code> so we use Rust-style value-based error\npropagation (via <code>Result<\/code> and the <code>?<\/code> operator) in host code.<\/p>\n<p>What happens when we have a value inside the <code>Error<\/code> that holds an\nexception object in the Wasmtime <code>Store<\/code>? How does Wasmtime know this\nis rooted?<\/p>\n<p>The answer in Wasmtime prior to recent work was to use one of two\nkinds of external rooting wrappers: <code>Rooted<\/code> and\n<code>ManuallyRooted<\/code>. Both wrappers hold an index into a table contained\ninside the <code>Store<\/code>, and that table contains the actual GC\nreference. This allows the GC to easily see the roots and update them.<\/p>\n<p>The difference lies in the lifetime disciplines: <code>ManuallyRooted<\/code>\nrequires, as the name implies, manual unrooting; it has no <code>Drop<\/code>\nimplementation, and so easily creates leaks. <code>Rooted<\/code>, on the other\nhand, had a LIFO (last-in first-out) discipline based on a <code>Scope<\/code>, an\nRAII type created by the embedder (user) of Wasmtime. <code>Rooted<\/code> GC\nreferences that escape that dynamic scope are unrooted, and will cause\nan error (panic) at runtime if used. Neither of those behaviors is\nideal for a value type -- an exception -- that is <em>meant<\/em> to escape\nscopes via <code>?<\/code>-propagation.<\/p>\n<p>The design that we landed on, instead, takes a different and much\nsimpler approach: the <code>Store<\/code> has a single, explicit root slot for the\n\"pending exception\", and host code can set this and then return a\n<em>sentinel value<\/em> (<code>wasmtime::ThrownException<\/code>) in the <code>Result<\/code>'s error\ntype (boxed up into an <code>anyhow::Error<\/code>). This easily allows\npropagation to work as expected, with no unbounded leaks (there is\nonly one pending exception that is rooted) and no unrooted propagating\nexceptions (because no actual GC reference propagates, only the\nsentinel).<\/p>\n<p>As a side-quest, while thinking through this rooting dilemma, I also\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/11445\">realized<\/a>\nthat it <em>should<\/em> be possible to create an \"owned\" rooted reference\nthat behaves more like a conventional owned Rust value (e.g. <code>Box<\/code>);\nhence <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11514\"><code>OwnedRooted<\/code> was born to replace\n<code>ManuallyRooted<\/code><\/a>.\nThis type works without requiring access to the <code>Store<\/code> to unroot when\ndropped; the key idea is to hold a refcount to a separate tiny\nallocation that is used as a \"drop flag\", and then have the store\nperiodically scan these drop-flags and lazily remove roots, with a\nthresholding algorithm to give that scanning amortized linear-time\nbehavior.<sup class=\"footnote-reference\"><a href=\"#9\">9<\/a><\/sup><\/p>\n<p>Now that we have this, in theory, we could pass an\n<code>OwnedRooted&lt;ExnRef&gt;<\/code> directly in the <code>Error<\/code> type to propagate\nexceptions through host code; but the store-rooted approach is simple\nenough, has a marginal performance advantage (no separate allocation),\nand so I don't see a strong need to change the API at the moment.<\/p>\n<h3 id=\"life-of-an-exception-quick-walkthrough\">Life of an Exception: Quick Walkthrough<\/h3>\n<p>Now that we've discussed all the design choices, let's walk through\nthe life an exception throw\/catch, from start to finish. Let's assume\na Wasm-to-Wasm throw\/catch for simplicity here.<\/p>\n<ul>\n<li>First, the Wasm program is executing within a <code>try_table<\/code>, which\nresults in an exception handler <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/translate\/code_translator.rs#L609-L613\">catch blocks being\ncreated<\/a>\nfor each handler case listed in the <code>try_table<\/code> instruction. The\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/translate\/code_translator.rs#L4325\"><code>create_catch_block<\/code><\/a>\nfunction generates code that invokes\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/func_environ\/gc\/enabled.rs#L415\"><code>translate_exn_unbox<\/code><\/a>,\nwhich reads out all of the fields from the exception object and\npushes them onto the Wasm operand stack in the handler path. This\nhandler block is registered in the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/translate\/stack.rs#L570\"><code>HandlerState<\/code><\/a>,\nwhich tracks the current lexical stack of handlers (and hands out\ncheckpoints so that when we pop out of a Wasm block-type operator,\nwe can pop the handlers off the state as well). These handlers are\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/translate\/stack.rs#L611\">provided as an\niterator<\/a>\nwhich is <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/translate\/code_translator.rs#L661\">passed to the <code>translate_call<\/code>\nmethod<\/a>\nand eventually ends up <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/func_environ.rs#L2366-L2379\">creating an exception\ntable<\/a>\non a <code>try_call<\/code> instruction. This <code>try_call<\/code> will invoke whatever\nWasm code is about to throw the exception.<\/li>\n<li>Then, the Wasm program reaches a <code>throw<\/code> opcode, which is\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/translate\/code_translator.rs#L621\">translated<\/a>\nvia\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/func_environ.rs#L2825\"><code>FuncEnvironment::translate_exn_throw<\/code><\/a>\nto a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/func_environ\/gc\/enabled.rs#L473-L482\">three-operation\nsequence<\/a>\nthat fetches the current instance ID (via a libcall into the\nruntime), allocates a new exception object with that instance ID and\na fixed tag number and fills in its slots with the given values\npopped from the Wasm operand stack, and delegates to <code>throw_ref<\/code>.<\/li>\n<li>The <code>throw_ref<\/code> opcode implementation then invokes the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/cranelift\/src\/func_environ\/gc\/enabled.rs#L519\"><code>throw_ref<\/code><\/a>\nlibcall.<\/li>\n<li>This libcall is deceptively simple: its\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/libcalls.rs#L1695-L1707\">implementation<\/a>\nsets the pending exception on the store, and returns a sentinel that\nsignals a pending exception. That's it!<\/li>\n<li>This works because the glue code for <em>all<\/em> libcalls processes errors\n(via the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/traphandlers.rs#L152\"><code>HostResult<\/code><\/a>\ntrait implementations) and eventually reaches <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/traphandlers.rs#L773-L786\">this\ncase<\/a>\nwhich sees a pending exception sentinel and invokes\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/throw.rs#L15\"><code>compute_handler<\/code><\/a>. Now\nwe're getting to the heart of the exception-throw implementation.<\/li>\n<li><code>compute_handler<\/code> walks the stack with\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/throw.rs#L45\"><code>Handler::find<\/code><\/a>,\nwhich itself is based on\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/stackwalk.rs#L248\"><code>visit_frames<\/code><\/a>,\nwhich does about what one would expect for code with a frame-pointer\nchain: it walks the singly-linked list of frames. At each frame, the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/throw.rs#L51\">closure<\/a>\nthat <code>compute_handler<\/code> gave to <code>Handler::find<\/code> looks up the program\ncounter in that frame (which will be a return address, i.e., the\ninstruction after the call that created the next lower frame) using\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/module\/registry.rs#L74\"><code>lookup_module_by_pc<\/code><\/a>\nto find a <code>Module<\/code>, which itself has an\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/exception_table.rs#L225\"><code>ExceptionTable<\/code><\/a>\n(a parser for serialized metadata produced during compilation from\nCranelift metadata) that knows how to look up a PC within a\nmodule. This will produce an <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/exception_table.rs#L310\"><code>Iterator<\/code> over\nhandlers<\/a>\nwhich we <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/throw.rs#L63\">test in\norder<\/a>\nto see if any match. (The groups of exception-handler table items\nthat come out of Cranelift are post-processed\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/exception_table.rs#L144\">here<\/a>\nto generate the tables that the above routines search.)<\/li>\n<li>If we find a handler, that is, if <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/throw.rs#L108-L109\">the dynamic tag instance is the\nsame<\/a>\nor <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/throw.rs#L67\">we reach a catch-all\nhandler<\/a>,\nthen we have an exception handler! We return the PC and SP to\nrestore\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/throw.rs#L112-L120\">here<\/a>,\ncomputing SP via an FP-to-SP offset (i.e., the size of the frame),\nwhich is fixed and included in the exception tables when we\nconstruct them.<\/li>\n<li>That action then becomes an <code>UnwindState::UnwindToWasm<\/code>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/traphandlers.rs#L779\">here<\/a>.<\/li>\n<li>This <code>UnwindToWasm<\/code> state then triggers <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/wasmtime\/src\/runtime\/vm\/traphandlers.rs#L913-L933\">this\ncase<\/a>\nin the <code>unwind<\/code> libcall, which is invoked whenever any libcall\nreturns an error code; that eventually calls the no-return function\n<code>resume_to_exception_handler<\/code>, which is a little function written in\ninline assembly that does exactly what it says on the tin. <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/arch\/x86.rs#L32-L34\">These\nthree\ninstructions<\/a>\nset <code>rsp<\/code> and <code>rbp<\/code> to their new values, and jump to the new <code>rip<\/code>\n(PC). The same stub exists for each of our four native-compilation\narchitectures (x86-64 above,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/arch\/aarch64.rs#L60-L62\">aarch64<\/a>,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/arch\/riscv64.rs#L29-L31\">riscv64<\/a>,\nand\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/8e22ff89f6affe4f79fcebdb416d0ab401d43c97\/crates\/unwinder\/src\/arch\/s390x.rs#L32-L33\">s390x<\/a><sup class=\"footnote-reference\"><a href=\"#10\">10<\/a><\/sup>).\nThat transfers control to the catch-block created above, and the\nWasm continues running, unboxing the exception payload and running\nthe handler!<\/li>\n<\/ul>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>So we have Wasm exception handling now! For all of the interesting\ndesign questions we had to work through, the end was pretty\nanticlimactic. I landed <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11326\">the final\nPR<\/a>, and\nafter a follow-up cleanup PR\n(<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11467\">1<\/a>) and\nsome fuzzbug fixes\n(<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11500\">1<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11507\">2<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11530\">3<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11531\">4<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11535\">5<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11564\">6<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11554\">7<\/a>) having\nmostly to do with null-pointer handling and other edge cases in the\ntype system, plus one interaction with tail-calls (and a\nseparate\/pre-existing <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11689\">s390x ABI\nbug<\/a> that it\nuncovered), it has been basically stable. We pretty quickly got a few\nuser reports:\n<a rel=\"external\" href=\"https:\/\/bytecodealliance.zulipchat.com\/#narrow\/channel\/217117-cranelift\/topic\/Running.20lua.205.2E1.20wasi\/near\/535368031\">here<\/a>\nit was reported as working for a Lua interpreter using setjmp\/longjmp\ninside Wasm based on exceptions, and\n<a rel=\"external\" href=\"https:\/\/bytecodealliance.zulipchat.com\/#narrow\/channel\/217126-wasmtime\/topic\/WebAssembly.20exceptions.20proposal.20is.20now.20implemented\/near\/536299383\">here<\/a>\nit enabled Kotlin-on-Wasm to run and <a rel=\"external\" href=\"https:\/\/bytecodealliance.zulipchat.com\/#narrow\/channel\/217126-wasmtime\/topic\/WebAssembly.20exceptions.20proposal.20is.20now.20implemented\/near\/543748960\">pass a large\ntestsuite<\/a>.\nNot bad!<\/p>\n<!--\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/220: +82 -84\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/223: +5 -6\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/224: +16 -19\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10510: +4199 -423\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10609: +109 -100\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10709: +49 -5\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/226: +26 -10\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/221: +1 -1\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10571: +36 -24\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/225: +1 -1\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10590: +19 -5\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/227: +1 -1\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10747: +84 -5\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10748: +84 -5\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10919: +1472 -347\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11230: +2490 -191\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/231: +93 -12\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11321: +1771 -322\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11326: +2593 -523\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11467: +269 -227\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11500: +246 -116\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11507: +15 -1\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11511: +6 -2\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11514: +758 -531\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11530: +12 -2\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11531: +1 -0\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11533: +31 -20\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11535: +31 -1\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11554: +2 -0\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11564: +29 -5\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10485: +303 -203\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/212: +1 -2\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10502: +1338 -767\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/214: +7 -3\n  https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/216: +56 -8\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10554: +15 -26\n  https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10555: +13 -6\n-->\n<p>All told, this took 37 PRs with a diff-stat of <code>+16264 -4004<\/code> (16KLoC\ntotal) -- certainly not the \"small-to-medium-sized\" project I had\ninitially optimistically expected, but I'm happy we were able to build\nit out and get it to a stable state relatively easily. It was a\nrewarding journey in a different way than a lot of my past work\n(mostly on the Cranelift side) -- where many of my past projects have\nbeen really very open-ended design or even research questions, here we\nhad the high-level shape already and all of the work was in designing\nhigh-quality details and working out all the interesting interactions\nwith the rest of the system. I'm happy with how clean the IR design\nturned out in particular, and I don't think it would have done so\nwithout the really excellent continual discussion with the rest of the\nCranelift and Wasmtime contributors (thanks to Nick Fitzgerald and\nAlex Crichton in particular here).<\/p>\n<p>As an aside: I am happy to see how, aside from use-cases for Wasm\nexception handling, the exception support in Cranelift itself has been\nuseful too.  As mentioned above, <code>cg_clif<\/code> picked it up almost as soon\nas it was ready; but then, as an unexpected and pleasant surprise,\nAlex subsequently <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11592\">rewrote Wasmtime's trap\nunwinding<\/a> to\nuse Cranelift exception handlers in our entry trampolines rather than\na setjmp\/longjmp, as the latter have longstanding semantic\nquestions\/issues in Rust. This took <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/11629\">one more\nintrinsic<\/a>,\nwhich I implemented after discussing with Alex how best to expose\nexception handler addresses to custom unwind logic without the full\nexception unwinder, but was otherwise a pretty direct application of\n<code>try_call<\/code> and our exception ABI.  General building blocks prove\ngenerally useful, it seems!<\/p>\n<hr \/>\n<p><em>Thanks to Alex Crichton and Nick Fitzgerald for providing feedback on\na draft of this post!<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>To explain myself a bit, I underestimated the interactions of\nexception handling with garbage collection (GC); I hadn't\nrealized yet that <code>exnref<\/code>s were a full first-class value and\nwould need to be supported including in the host API. Also, it\nturns out that exceptions can cross the host\/guest boundary, and\ngoodness knows that gets really fun really fast. I was <em>only<\/em>\noff by a factor of two on the compiler side at least!<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>From an implementation perspective, the dynamic, interprocedural\nnature of exceptions is what makes them far more interesting,\nand involved, than classical control flow such as conditionals,\nloops, or calls! This is why we need a mechanism that involves\nruntime data structrues, \"stack walks\", and lookup tables,\nrather than simply generating a jump to the right place: the\ntarget of an exception-throw can only be computed at runtime,\nand we need a convention to transfer control with \"payload\" to\nthat location.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>For those so inclined, this is a\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Monad_(functional_programming)\"><em>monad<\/em><\/a>,\nand e.g. <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Haskell\">Haskell<\/a>\nimplements the ability to have \"result or error\" types that\nreturn from a sequence early via\n<a rel=\"external\" href=\"https:\/\/hackage.haskell.org\/package\/base-4.21.0.0\/docs\/Data-Either.html#t:Either\"><code>Either<\/code><\/a>,\nexplicitly describing the concept as such. The <code>?<\/code> operator\nserves as the \"bind\" of the monad: it connects an\nerror-producing computation with a use of the non-error value,\nreturning the error directly if one is given instead.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"4\"><sup class=\"footnote-definition-label\">4<\/sup>\n<p>So named for the <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/IA-64\">Intel Itanium\n(IA-64)<\/a>, an instruction-set\narchitecture that happened to be the first ISA where this scheme was\nimplemented for C++, and is now essentially dead (before its time! woefully\nmisunderstood!) but for that legacy...<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"5\"><sup class=\"footnote-definition-label\">5<\/sup>\n<p>It's worth briefly noting here that the Wasm exception handling\nproposal went through a somewhat twisty journey, with an earlier\nvariant (now called \"legacy exception handling\") that shipped in\nsome browsers but was never standardized handling rethrows in a\ndifferent way. In particular, that proposal did not offer\nfirst-class exception object references that could be rethrown;\ninstead, it had an explicit <code>rethrow<\/code> instruction. I wasn't\naround for the early debates about this design, but in my\nopinion, providing first-class exception object references that\ncan be plumbed around via ordinary dataflow is far nicer. It\nalso permits a simpler implementation, as long as one literally\nimplements the semantics by always allocating an exception\nobject.<sup class=\"footnote-reference\"><a href=\"#11\">11<\/a><\/sup><\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"6\"><sup class=\"footnote-definition-label\">6<\/sup>\n<p>To be precise, because it may be a little surprising:\n<code>catch_ref<\/code> pushes both the payload values <em>and<\/em> the exception\nreference onto the operand stack at the handler destination. In\nessence, the rule is: tag-specific variants always unpack the\npayloads; and <em>also<\/em>, <code>_ref<\/code> variants always push the exception\nreference.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"7\"><sup class=\"footnote-definition-label\">7<\/sup>\n<p>In particular, we have defined our own ABI in Wasmtime to allow\nuniversal tail calls between any two signatures to work, as\nrequired by the Wasm tail-calling opcodes. This ABI, called\n\"<code>tail<\/code>\", is based on the standard System V calling convention\nbut differs in that the callee cleans up any stack arguments.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"8\"><sup class=\"footnote-definition-label\">8<\/sup>\n<p>It's not compiler hacking without excessive trouble from\nedge-cases, of course, so we had one <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10709\">interesting\nbug<\/a>\nfrom the <em>empty handler-list<\/em> case which means we have to force\nedge-splitting anyway for all <code>try_call<\/code>s for this subtle\nreason.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"9\"><sup class=\"footnote-definition-label\">9<\/sup>\n<p>Of course, while doing this, I managed to create\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/security\/advisories\/GHSA-vvp9-h8p2-xwfc\">CVE-2025-61670<\/a>\nin the C\/C++ API by a combination of (i) a simple typo in the C\nFFI bindings (<code>as<\/code> vs. <code>from<\/code>, which is important when\ntransferring ownership!) and (ii) not realizing that the C++\nwrapper does not properly maintain single ownership. We didn't\nhave ASAN tests, so I didn't see this upfront; Alex discovered\nthe issue while updating the Python bindings (which quickly\nfound the leak) and managed the CVE. Sorry and thanks!<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"10\"><sup class=\"footnote-definition-label\">10<\/sup>\n<p>It turns out that even three lines of assembly are hard to get\nright: the s390x variant <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/10973\">had a\nbug<\/a>\nwhere we got the register constraints wrong (GPR 0 is special on\ns390x, and a branch-to-register can only take GPR 1--15; we\nneeded a different constraint to represent that)and had a\nmiscompilation as a result. Thanks to our resident s390x\ncompiler hacker Ulrich Weigand for tracking this down.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"11\"><sup class=\"footnote-definition-label\">11<\/sup>\n<p>Of course, always boxing exceptions is not the only way to\nimplement the proposal. It should be possible to \"unbox\"\nexceptions and skip the allocation, carrying payloads directly\nthrough some other engine state, if they are not caught as\nreferences. We haven't implemented this optimization in Wasmtime\nand we expect the allocation performance for small exception\nobjects to be adequate for most use-cases.<\/p>\n<\/div>\n"},{"title":"Compilation of JavaScript to Wasm, Part 3: Partial Evaluation","published":"2024-08-28T00:00:00+00:00","updated":"2024-08-28T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2024\/08\/28\/weval\/"}},"id":"https:\/\/cfallin.org\/blog\/2024\/08\/28\/weval\/","content":"<p><em>This is the final post of a three-part series covering my work on\n\"fast JS on Wasm\"; the <a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/\">first\npost<\/a> covered PBL, a portable\ninterpreter that supports inline caches, the <a href=\"\/blog\/2024\/08\/27\/aot-js\/\">second\npost<\/a> covered ahead-of-time compilation in\ngeneral terms, and this post discusses how we actually build the\nahead-of-time compiler backends. Please read the first two posts for\nuseful context!<\/em><\/p>\n<hr \/>\n<p>In the last post, we covered how one might perform ahead-of-time\ncompilation of JavaScript to low-level code -- WebAssembly bytecode --\nin high-level terms. We discussed the two kinds of bytecode present in\n<a rel=\"external\" href=\"https:\/\/spidermonkey.dev\/\">SpiderMonkey<\/a>: bytecode for JavaScript\nfunction bodies (as a 1-to-1 translation from the source JS) and\nbytecode for the inline cache (IC) bodies that implement each operator\nin those function bodies. We outlined the low-level code that the AOT\ncompilation of each bytecode would produce. But how do we actually\nperform that compilation?<\/p>\n<h2 id=\"direct-single-pass-compiler-style\">Direct (Single-Pass) Compiler Style<\/h2>\n<p>It would be straightforward enough to implement a direct compiler from\neach bytecode (JS and CacheIR) to Wasm. The most direct kind of\ncompiler is sometimes called a \"template compiler\" or (in the context\nof JIT engines) a \"template JIT\": for each source bytecode, emit a\nfixed sequence of target code. This is a fairly common technique, and\nif one examines the compiler tiers of the well-known JIT compilers\ntoday, one finds implementations such as\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/43d431ff148b331b463fcf61e99c176e3d3c0fb4\/js\/src\/jit\/BaselineCodeGen.cpp#2764-2782\">this<\/a>\n(implementation of <code>StrictEq<\/code> opcode in SpiderMonkey's JS opcode\nbaseline compiler, emitting two pops, an IC invocation, and a push) or\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/2389dcceaad666ad1fb6acfd1520bbb4ea5a85e4\/winch\/codegen\/src\/visitor.rs#L1889-L1902\">this<\/a>\n(implementation of a ternary <code>select<\/code> Wasm opcode in Wasmtime's Winch\nbaseline compiler, emitting three pops, a compare, a conditional move,\nand a push). SpiderMonkey already has baseline compiler\nimplementations for <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/source\/js\/src\/jit\/BaselineCodeGen.cpp\">JS\nopcodes<\/a>\nand <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/source\/js\/src\/jit\/BaselineCacheIRCompiler.cpp\">CacheIR\nopcodes<\/a>,\nabstracted over the\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/43d431ff148b331b463fcf61e99c176e3d3c0fb4\/js\/src\/jit\/MacroAssembler.h#53\">MacroAssembler<\/a>\nallowing for reasonably easy retargeting to different ISAs. Why not\nport these backends to produce Wasm bytecode?<\/p>\n<h2 id=\"porting-spidermonkey-jit-to-wasm\">Porting SpiderMonkey JIT to Wasm?<\/h2>\n<p>This approach <a rel=\"external\" href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1863986\">has actually been\ntaken<\/a>, more or\nless performing a \"direct port\" to Wasm by emitting Wasm bytecode\n\"in-process\" (inside the Wasm module) and then using a special\nhostcall interface to add this as a new callable function. This works\nwell enough where the hostcall approach is acceptable, but as I\ndiscussed <a href=\"\/blog\/2024\/08\/27\/aot-js\/#unique-characteristics-of-a-wasm-platform\">last\ntime<\/a>,\na few factors conspire against a direct port (i.e., the ISA target is\nonly part of the problem):<\/p>\n<ul>\n<li>\n<p>For operational reasons (ahead-of-time deployment and instant start)\nand security reasons (less attack surface, statically-knowable\ncode), Wasm-based platforms often disallow runtime code\ngeneration. The \"add a new function\" hostcall\/hook might simply not\nbe available.<\/p>\n<\/li>\n<li>\n<p>We might be running very short-lived request handlers or similar,\neach starting from an identical snapshot: thus, there is no time to\n\"warm up\", generate appropriate Wasm bytecode at runtime, and load\nand run it, even though execution may still be bottlenecked on (many\ninstances of) these short-lived request handlers.<\/p>\n<\/li>\n<\/ul>\n<p>There is another practical reason as well. JITs are famously hard to\ndevelop and debug because (i) the code that is actually running on the\nsystem does not exist anywhere in the source -- it is ephemeral -- and\n(ii) it is often tightly integrated with various low-level ABI details\nand system invariants which can be easy to get wrong. On native\nplatforms, the difficulty of debugging subtle bugs in Firefox (and its\nSpiderMonkey JIT engine) led Mozilla engineers to develop\n<a rel=\"external\" href=\"https:\/\/rr-project.org\/\">rr<\/a>, the well-known open-source reversible\n(time-travel) debugger. Now imagine developing a JIT that runs within\na Wasm module with runtime codegen hooks, <em>without<\/em> a state-of-the-art\ndebugging capability; in fact, in <a rel=\"external\" href=\"https:\/\/wasmtime.dev\">Wasmtime<\/a>,\nwhich I help to develop, source-level debugging is improving slowly\nbut no debugger for Wasm bytecode-level execution exists at all\nyet. (We hope to get there someday.) If I am to have any hope of\nsuccess, it seems like I will need to find another way to nail down\nthe correct semantics of the compiler's output -- either by testing\nand debugging in another (native) build somehow, or building better\ntooling.<\/p>\n<h2 id=\"a-new-hope-single-source-of-semantics\">A New Hope: Single Source of Semantics<\/h2>\n<p>Here I come back to <a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/\">PBL<\/a>: I\nalready have an\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/source\/js\/src\/vm\/PortableBaselineInterpret.cpp\">interpreter<\/a>\nthat I painstakingly developed for both CacheIR bodies of IC stubs,\nand the JS bytecode that invokes them, faithfully upholding all the\ninvariants of baseline-level execution in SpiderMonkey. In addition,\nwork on PBL can proceed in a native build as well -- in fact much of\nmy debugging was with the venerable <code>rr<\/code>, just as with any\nnative-platform SpiderMonkey development. PBL is \"just\" a portable C++\nprogram, and so all of the work developing it on any platform then\ntransfers right to the Wasm build as well. PBL embodies a lot of\nhard-won work encoding the correct semantics for each opcode,\nsometimes not well-documented in the native backends -- for example,\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/43d431ff148b331b463fcf61e99c176e3d3c0fb4\/js\/src\/vm\/PortableBaselineInterpret.cpp#4300-4302\">this\ntidbit<\/a>\n(far too much time to discover that wrinkle!), or <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/43d431ff148b331b463fcf61e99c176e3d3c0fb4\/js\/src\/vm\/PortableBaselineInterpret.cpp#1303-1304\">this\none<\/a>. What's\nmore, these semantics sometimes change, or new opcodes are added.<\/p>\n<p>Ideally we do not add more locations that encode these semantics than\nabsolutely necessary -- once per tier is already quite a lot. Can we\nsomehow develop (or -- major foreshadowing here -- <em>automatically\nderive<\/em>) our compiler from all of the semantics written in direct\nstyle in interpreter code?<\/p>\n<p>Let's take a look once more at (slightly simplified) implementations\nof an \"integer add\" opcode on a hypothetical interpreted stack\nmachine, and then a baseline compiler implementation of that opcode\nwhere the operand stack uses the native machine stack<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>\/\/ Interpreter                    | \/\/ Compiler<\/span><\/span>\n<span class=\"giallo-l\"><span>...                               | ...<\/span><\/span>\n<span class=\"giallo-l\"><span>case Opcode::Add: {               | void visit_Add(Assembler&amp; asm) {<\/span><\/span>\n<span class=\"giallo-l\"><span>  uint32_t lhs = stack.pop();     |   asm.pop(r1);<\/span><\/span>\n<span class=\"giallo-l\"><span>  uint32_t rhs = stack.pop();     |   asm.pop(r2);<\/span><\/span>\n<span class=\"giallo-l\"><span>  uint32_t result = lhs + rhs;    |   asm.add(r1, r2);<\/span><\/span>\n<span class=\"giallo-l\"><span>  stack.push(result);             |   asm.push(r1);<\/span><\/span>\n<span class=\"giallo-l\"><span>  break;                          | }<\/span><\/span>\n<span class=\"giallo-l\"><span>}<\/span><\/span>\n<span class=\"giallo-l\"><span>...<\/span><\/span><\/code><\/pre>\n<p>If you squint enough, you might almost forget whether you're looking\nat the interpreter (<em>directly<\/em> executing the semantics) or the\ncompiler (<em>indirectly<\/em> executing the semantics, by emitting\ncode). This correspondence is not accidental, and the observation that\ndoing-the-thing and emitting-code-to-do-the-thing are so close is at\nthe heart of what is sometimes called staged programming, with (again\ngiven sufficient squinting) practical examples in Lisp macros,\n<a rel=\"external\" href=\"https:\/\/www.cambridge.org\/core\/books\/abs\/twolevel-functional-languages\/contents\/4DA77E9B08CF24C00221555FC241D1C9\">\"two-level functional\nlanguages\"<\/a>,\n<a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/10.1145\/258993.259019\">MetaML<\/a>, and <a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/10.1145\/2184319.2184345\">more\nmodern takes<\/a> that\nstrive to provide as transparent a syntax as possible. There are even\nproposals to leverage this correspondence directly when writing JIT\ncompilers (see\n<a rel=\"external\" href=\"https:\/\/blog.mozilla.org\/javascript\/2017\/10\/20\/holyjit-a-new-hope\/\">HolyJit<\/a>),\nwriting down the semantics once to provide both the interpreter and\nthe compiler tier.<\/p>\n<p>Most prominently in this space, of course, is\n<a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/10.1145\/3062341.3062381\">GraalVM<\/a> (<a rel=\"external\" href=\"https:\/\/labs.oracle.com\/pls\/apex\/f?p=LABS:0::APPLICATION_PROCESS%3DGETDOC_INLINE:::DOC_ID:1014\">alternate\nPDF<\/a>),\na production-grade JIT compiler framework that takes <em>interpreters<\/em>\nfor arbitrary languages and <em>partially evaluating<\/em> the interpreter\ncode itself, with the user's program, to produce compiled code. This\nmind-bending technique is known as the <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Partial_evaluation#Futamura_projections\">first Futamura\nprojection<\/a>. (This\nis the approach we will take too, with some significant\n<a href=\"https:\/\/cfallin.org\/blog\/2024\/08\/28\/weval\/#graalvm\">differences<\/a>.)<\/p>\n<p>How does this work?<\/p>\n<h2 id=\"the-first-futamura-projection\">The First Futamura Projection<\/h2>\n<p>Let's say that we have a function <code>f(x, y)<\/code>. If we take <code>x<\/code> as some\nconstant <code>C<\/code>, we should be able to derive a function <code>f_C(y) = f(C, y)<\/code> that eliminates all occurrences of <code>x<\/code> (i.e., <code>x<\/code> is no longer a\nfree variable). This is \"just\" constant propagation, or <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Partial_application\">partial\napplication<\/a>, of\nthe function.<\/p>\n<p>A little more concretely, let's take <code>f<\/code> to be an interpreter, with\ninputs <code>x<\/code>, the program to be interpreted, and <code>y<\/code>, the input that the\ninterpreted program computes on. <code>f(x, y)<\/code> is then the result of\nrunning the program. If we could find <code>f_C(y)<\/code>, then <code>f_C<\/code> would be,\nsomehow, a <em>combination<\/em> of the interpreter and the program it\ninterprets, merged together: a compiled program.<\/p>\n<p>Futamura, in his <a rel=\"external\" href=\"https:\/\/repository.kulib.kyoto-u.ac.jp\/dspace\/handle\/2433\/103401\">seminal\npaper<\/a>,\ncalls this combination a \"projection\" and describes three levels of\nprojection. The first is as above: combining an interpreter with its\nprogram. The second and third projections are far more exotic:\ncombining the partial evaluator itself with an interpreter, producing\na compiler (that can then be applied to a program); or combining the\npartial evaluator with itself as input, producing a compiler-compiler\n(that can then be applied to an interpreter, producing a compiler,\nthat can then be applied to an input program, producing compiled\noutput). Mind-bending stuff.<\/p>\n<p>I can now tell you what <a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/weval\">weval<\/a>, the\nWebAssembly partial evaluator, is: it is a tool that derives the first\nFutamura projection of an interpreter inside a snapshot of a\nWebAssembly module together with its input (bytecode).<\/p>\n<p>Alright, but how?! How does one \"combine\" an interpreter with its\ninput?<\/p>\n<p>The thinking that gives rise to the Futamura projections is very\n<em>algebraic<\/em> in nature. In conventional algebra (with addition and\nmultiplication over the reals, for example, as taught to students\neverywhere), we have a notion of \"plugging in\" certain constants and\nsimplifying the expression. Given <code>z = 2x + 3y + 4<\/code>, we can hold <code>x<\/code>\nconstant, say <code>x = 10<\/code>, then \"partially evaluate\" <code>z = 2*10 + 3y + 4 = 3y + 24<\/code>. We have produced an output (expression) that fully\nincorporates the new knowledge and has no further references to the\ninput (variable) <code>x<\/code>.<\/p>\n<p>An interpreter and its interpreted program are far, far more complex\nthan an algebraic expression and a numeric constant, of course. Let's\nexplore in a bit more detail how one might \"plug in\" a program to an\ninterpreter and simplify the result.<\/p>\n<h2 id=\"practical-first-futamura-projection-via-context-specialization\">Practical First Futamura Projection via Context Specialization<\/h2>\n<h3 id=\"a-naive-approach-constant-propagation\">A Naive Approach: Constant Propagation<\/h3>\n<p>We might start with the observation that, in the compiler-optimization\nsphere, <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Constant_folding\">constant\nfolding<\/a> looks a lot\nlike the \"plug in a value and simplify\" mechanism that we know from\nalgebra. In essence, what we want to say is: take an interpreter,\nspecify that the program it interprets is a <em>constant<\/em>, and (somehow)\npropagate the consequences of that through the code.<\/p>\n<p>Let's take a look at an interpreter body that interprets a <em>single\nopcode<\/em> first:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"cpp\"><span class=\"giallo-l\"><span class=\"z-keyword\">switch<\/span><span class=\"z-punctuation z-section\">(<\/span><span class=\"z-keyword z-operator\">*<\/span><span>pc<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Add<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">    uint32_t<\/span><span> lhs <\/span><span class=\"z-keyword z-operator\">=<\/span><span class=\"z-variable z-other\"> stack<\/span><span class=\"z-punctuation\">[<\/span><span>sp<\/span><span class=\"z-keyword z-operator\">++<\/span><span class=\"z-punctuation\">]<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">        \/\/ stack pop<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">    uint32_t<\/span><span> rhs <\/span><span class=\"z-keyword z-operator\">=<\/span><span class=\"z-variable z-other\"> stack<\/span><span class=\"z-punctuation\">[<\/span><span>sp<\/span><span class=\"z-keyword z-operator\">++<\/span><span class=\"z-punctuation\">]<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">        \/\/ stack pop<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">    uint32_t<\/span><span> result <\/span><span class=\"z-keyword z-operator\">=<\/span><span> lhs <\/span><span class=\"z-keyword z-operator\">+<\/span><span> rhs<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">       \/\/ operation logic<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">    stack<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-keyword z-operator\">--<\/span><span>sp<\/span><span class=\"z-punctuation\">]<\/span><span class=\"z-keyword z-operator\"> =<\/span><span> result<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">              \/\/ stack push<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword z-punctuation z-terminator\">    break;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Sub<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/* ... *\/<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Cmp<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/* ... *\/<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Jmp<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/* ... *\/<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><\/span><\/code><\/pre>\n<p>Let's take <code>pc<\/code> to be a constant, and furthermore assume that the\n<em>memory that <code>pc<\/code> points to<\/em> (the bytecode) is constant; and for this\nexample, assume that <code>pc[0] == Opcode::Add<\/code>. Let's also say that <code>sp<\/code>\nstarts at <code>1024<\/code>. Then we can \"constant fold\" the whole interpreter by\n(i) simplifying the switch-statement, which is now operating on a\n<em>constant<\/em> input (the opcode), and (ii) propagating through any other\nconstants we know based on initial state, such as <code>sp<\/code>:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"cpp\"><span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ at pc[0]: Opcode::Add, with initial sp == 1024.<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  uint32_t<\/span><span> lhs <\/span><span class=\"z-keyword z-operator\">=<\/span><span class=\"z-variable z-other\"> stack<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">1024<\/span><span class=\"z-punctuation\">]<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  uint32_t<\/span><span> rhs <\/span><span class=\"z-keyword z-operator\">=<\/span><span class=\"z-variable z-other\"> stack<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">1025<\/span><span class=\"z-punctuation\">]<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  uint32_t<\/span><span> result <\/span><span class=\"z-keyword z-operator\">=<\/span><span> lhs <\/span><span class=\"z-keyword z-operator\">+<\/span><span> rhs<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  stack<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">1025<\/span><span class=\"z-punctuation\">]<\/span><span class=\"z-keyword z-operator\"> =<\/span><span> result<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span>  sp <\/span><span class=\"z-keyword z-operator\">=<\/span><span class=\"z-constant z-numeric\"> 1025<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span><\/code><\/pre>\n<p>If one squints appropriately, one could claim that we \"compiled\" the\n<code>Add<\/code> opcode here: we converted the generic interpreter, with\nswitch-cases for every opcode and operand-stack accesses parameterized\non current stack depth, to code that is specific to the actual program\nwe're interpreting.<\/p>\n<p>So is that it? Can we do a first Futamura projection by holding the\n\"bytecode\" memory constant, and doing constant folding (including\nbranch folding)? It can't be that easy, right?<\/p>\n<h3 id=\"the-problem-lattices-and-loops\">The Problem: Lattices and Loops<\/h3>\n<p>Indeed, it becomes much more difficult quickly. Consider what happens\nas we widen our scope from programs of <em>one<\/em> opcode to programs of\n<em>two<\/em> opcodes (!). We'll have to update our interpreter to include a\nloop, and an update to <code>pc<\/code> after it \"fetches\" each opcode:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"cpp\"><span class=\"giallo-l\"><span class=\"z-keyword\">while<\/span><span class=\"z-punctuation z-section\"> (<\/span><span class=\"z-constant z-language\">true<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  switch<\/span><span class=\"z-punctuation z-section\"> (<\/span><span class=\"z-keyword z-operator\">*<\/span><span>pc<\/span><span class=\"z-keyword z-operator\">++<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Add<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/* ... *\/<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Sub<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/* ... *\/<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Cmp<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/* ... *\/<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Jmp<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/* ... *\/<\/span><span class=\"z-punctuation z-section\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><\/span><\/code><\/pre>\n<p>A compiler's constant-propagation\/folding pass, and other passes that\ncompute <em>properties of program values<\/em> and then mutate the program\naccordingly, usually are built around an iterated dataflow fixpoint\nsolver<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup>. To modify a program at compile time, we must work with\nproperties that are always true -- even when a particular expression\nor variable in the program might have many different values during\nexecution. For example, a loop body might increment an <code>index<\/code>\nvariable, giving a different value for each iteration of the loop, so\nwe cannot conclude that the value is constant. We need a way of\nmerging different possibilities together to find properties that are\ntrue in all cases.<\/p>\n<p>The constant-propagation analysis works by computing properties that\nlie in a <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Lattice\">lattice<\/a>, a\nmathematical structure with values that allow for \"merging\" (the\n\"meet\" function) and with certain properties around that merging that\nmake it well-behaved, so that we always arrive at the same single\nsolution. Typically, on a lattice used by a\nconstant-propagation\/folding analysis, if we see in one instance that\n<code>x<\/code> is constant <code>K1<\/code>, and in another instance that <code>x<\/code> is constant\n<code>K2<\/code>, then we have to move \"down the lattice\" and \"merge\" to a fact\nthat claims nothing at all: \"<code>x<\/code> has some arbitrary non-constant value\nat runtime\". This is sometimes called the \"meet-over-all-paths\"\nsolution, because we merge possible behavior over all control-flow\npaths that could lead to some program point.<\/p>\n<p>Consider the <code>pc<\/code> variable in this loop: on the first iteration, it\nbegins at the start of the bytecode buffer (offset <code>0<\/code>). After one\nopcode, it lies at offset <code>1<\/code>. And so on. Can a constant-propagation\npass \"bake in\" a single constant value for <code>pc<\/code> that will be valid for\nevery iteration of the loop? No: in fact, it is not constant.<\/p>\n<p>Likewise for the bytecode pointed-to by <code>pc<\/code>: in the first iteration\nof the loop, it may be <code>Add<\/code>, and in the second iteration, something\nelse; there is no <em>single<\/em> constant opcode that we can propagate into\nthe <code>switch<\/code> statement to simplify the whole interpreter body down to\nthe specific code for each opcode.<\/p>\n<p>The essence of the problem here is that the first Futamura projection\nof an interpreter needs to <em>somehow be aware of the interpreter\nloop<\/em>. In essence, it needs to \"unroll\" the loop: it needs to do a\n<em>separate<\/em> constant propagation for each opcode.<\/p>\n<p>One might be tempted to build a transform that \"traces\" across edges,\nincluding the interpreter loop\nbackedge(s). <a rel=\"external\" href=\"https:\/\/pypy.org\/\">PyPy<\/a>'s metatracing works something\nlike this. One runs into two issues though: (i) the compilation is\n\"loop-centric\" (rather than complete compilation of each\nfunction\/method), and (ii) merge-points are tricky. Consider an\nif-else sequence with two sides that eventually jump back to the same\n<code>pc<\/code>. Do we keep \"tracing\" the path on each side or do we detect\nreconvergence and stitch the resulting code back together? That is, if\nwe have the bytecode<\/p>\n<p><img src=\"\/assets\/2024-08-22-bytecode-cfg-web.svg\" alt=\"Figure: control-flow if-else diamond with bytecode\" \/><\/p>\n<p>we might naively \"follow the control flow\" in the interpreter,\ngenerating one path that is <code>pc=0,1,2,3,5,6,7<\/code> and another that is\n<code>pc=0,1,4,5,6,7<\/code>:<\/p>\n<p><img src=\"\/assets\/2024-08-22-bytecode-cfg-traces-web.svg\" alt=\"Figure: control-flow if-else diamond with bytecode, with traces along control-flow paths\" \/><\/p>\n<p>Note that we have duplicated the \"tail\" (<code>pc=5<\/code> onward) and this code\ngrowth can become exponential if we have, say, a tree of conditionals.<\/p>\n<p>The property that we want is that the CFG of the original bytecode is\nsomehow reflected into the compiled code: take the original\n<em>structure<\/em> of the bytecode, for each opcode simulate the execution of\none iteration of the interpreter loop (on that opcode), and stitch it\ntogether. So given the initial <em>interpreter loop<\/em> (left) and <em>bytecode\nbeing interpreter<\/em> (right) here:<\/p>\n<p><img src=\"\/assets\/2024-08-22-interpreter-and-bytecode-cfg-web.svg\" alt=\"Figure: interpreter CFG and bytecode CFG\" \/><\/p>\n<p>we want something like the following, where each interpreted opcode is\ninitially \"unrolled\" into a whole copy of the interpreter loop:<\/p>\n<p><img src=\"\/assets\/2024-08-22-bytecode-cfg-full-of-interpreters-web.svg\" alt=\"Figure: bytecode CFG filled with copies of interpreter CFG\" \/><\/p>\n<p>This might seem a bit wild at first -- a whole bunch of copies of the\nentire interpreter? -- until one sees that this <em>reduces the problem\nto a previously-solved one above<\/em>, namely, how to specialize an\ninterpreter for a single opcode. We can take each <em>copy<\/em> of the\ninterpreter loop and constant-propagate and branch-fold it. The effect\nis as if we surgically plucked the implementations of each opcode out\nof the middle of the interpreter CFG and built a compiled version of\nthe bytecode with them:<\/p>\n<p><img src=\"\/assets\/2024-08-22-bytecode-cfg-full-of-specialized-interpreters-web.svg\" alt=\"Figure: bytecode CFG with specialized copies of interpreter CFG\" \/><\/p>\n<p>To make this work, it still seems like we would need some sort of\n\"visibility\" into the workings of the interpreter: we would need to\ntell the transform tool, the first Futamura projector, about our\nbytecode and its structure, and the interpreter loop that is meant to\noperate on it, so it could perform this careful surgery.<\/p>\n<p>GraalVM solves this problem in a very simple and direct way: the\ninterpreter needs to be written in terms of GraalVM AST classes, and\ngiven uses of those classes, the transform can \"pick out\" the right\nparts of the logic, while otherwise being aware of the overall CFG of\nthe interpreted program <em>directly<\/em>.<\/p>\n<p>I considered but rejected such an approach: I did not want to build a\ntool that would require rewriting an existing interpreter, or deep\nsurgery to rebuild it in terms of the framework's new\nabstractions. Such a transform would be very error-prone and hinder\nadoption. Is there another way to get the transform to \"see\" the\niterations of the interpreter loop separately?<\/p>\n<h3 id=\"contexts\">Contexts<\/h3>\n<p>The breakthrough for weval came when I realized that <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Pointer_analysis#Context-Insensitive,_Flow-Insensitive_Algorithms\"><em>context\nsensitivity<\/em><\/a>,\na standard tool in static analysis (e.g. pointer\/alias analysis),\ncould allow us to escape the tyranny of meet-over-all-paths collapsing\nour carefully-curated constant values into a mush of \"runtime\".<\/p>\n<p>The key idea is to make <code>pc<\/code> itself the context. The constant-folding\nanalysis otherwise <em>doesn't know anything about interpreters<\/em>; it just\nhas an intrinsic that means \"please process the code that flows from\nthis point in a different context\", and it keeps the \"constant value\"\nstate <em>separately<\/em> for each context.<\/p>\n<p>Given this intrinsic, we can \"simply\" update the context whenever we\nupdate <code>pc<\/code>; so the loop looks something like this:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"cpp\"><span class=\"giallo-l\"><span class=\"z-entity z-name z-function\">update_context<\/span><span class=\"z-punctuation z-section\">(<\/span><span>pc<\/span><span class=\"z-punctuation z-section\">)<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ intrinsic visible to the analysis, otherwise a no-op<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">while<\/span><span class=\"z-punctuation z-section\"> (<\/span><span class=\"z-constant z-language\">true<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  switch<\/span><span class=\"z-punctuation z-section\"> (<\/span><span class=\"z-keyword z-operator\">*<\/span><span>pc<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    case<\/span><span> Opcode<\/span><span class=\"z-punctuation\">::<\/span><span>Add<\/span><span class=\"z-punctuation z-punctuation z-section\">: {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">      \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      pc<\/span><span class=\"z-keyword z-operator z-punctuation z-terminator\">++;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-function\">      update_context<\/span><span class=\"z-punctuation z-section\">(<\/span><span>pc<\/span><span class=\"z-punctuation z-section\">)<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ intrinsic visible to the analysis, otherwise a no-op<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword z-punctuation z-terminator\">      break;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">               \/\/ loop backedge<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">    }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><\/span><\/code><\/pre>\n<p>Let's walk through a constant-folding example. We start with <code>pc<\/code> at\nthe beginning of the bytecode (for simplicity let's say <code>pc=0<\/code>, though\nit's actually somewhere in the heap snapshot), and we know that <code>*pc<\/code>\nis <code>Opcode::Add<\/code>. So in the <em>context<\/em> of <code>pc=0<\/code>, we can know that <code>pc<\/code>\nat the top of the loop starts as a constant. We see the <code>switch<\/code>, we\nbranch-fold because the opcode at <code>*pc<\/code> is a constant (because <code>pc<\/code> is\na constant and it points to constant memory). We trace through the\nimplementation of <code>Opcode::Add<\/code> (and disregard all the other\nswitch-cases). We increment <code>pc<\/code> and see a loop backedge -- isn't this\nwhere the constant-value analysis sees that the <code>pc=0<\/code> case (this\niteration) and the <code>pc=1<\/code> case (the next iteration) \"meet\" and we\ncan't conclude it's a constant at all?<\/p>\n<p>No! Because just before the loop backedge, we <em>updated the\ncontext<\/em>. As we trace through the code and track constant values, and\nfollow control-flow edges and propagate that state to their\ndestinations, we are now in the context of <code>pc=1<\/code>. We reach the loop\nheader again, but in a <em>new context<\/em>, so nothing collapses to \"not\nconstant\" \/ \"runtime\"; <code>pc<\/code> is actually a known constant\neverywhere. In other words, we go from this analysis situation:<\/p>\n<p><img src=\"\/assets\/2024-08-22-constant-prop-loop-web.svg\" alt=\"Figure: constant-propagation attempting to find a constant PC in an interpreter loop\" \/><\/p>\n<p>where the analysis <em>correctly<\/em> determines that PC is not constant (the\nbackedge carries a value <code>pc=1<\/code> into the same block that receives\n<code>pc=0<\/code> from the entry point, so we can only conclude <code>Unknown<\/code>), to\nthis one:<\/p>\n<p><img src=\"\/assets\/2024-08-22-constant-prop-loop-ctx-web.svg\" alt=\"Figure: constant-propagation finds constant PCs given context-sensitivity\" \/><\/p>\n<p>The <em>contexts<\/em> are thus the way in which we do the code duplication\nimplied earlier (a separate copy of the interpreter body for each PC\nlocation, then simplify). However, note that they are completely\ndriven by the intrinsics in the code that the interpreter developer\nwrites: the analysis <em>does not know about the interpreter loop<\/em>\notherwise.<\/p>\n<p>The overall analysis loop processes <code>(context, block)<\/code> <em>tuples<\/em>, and\ncarries abstract state (known constant, or <code>Unknown<\/code>) per <code>(context, SSA value)<\/code> <em>tuple<\/em>. When we perform the transform, we duplicate each\noriginal basic block into a basic block per context, but only on\ndemand, when the block is reached in some context. Branches in each\nblock resolve to the appropriate target block in the appropriate\n(possibly updated) context. This is key: the interpreter <em>backedge<\/em> in\nthe original CFG becomes an edge from \"tail block in context i\" to\n\"loop header block in context i+1\"; it becomes an edge to the next\nopcode's code in the compiled function body. This may be a forward\nedge or a backedge, depending on the original bytecode CFG's shape.<\/p>\n<p>One can see this as a sort of tracing through the control flow of the\ninterpreter, but with one crucial difference: contexts serve as a way\nto <em>reconnect<\/em> merge points. We thus don't get straight-line traces;\nrather, when we encounter a point in the interpreter that we could\nbranch to <code>pc=K1<\/code> or <code>pc=K2<\/code>, we see a context update to <code>K1<\/code> or <code>K2<\/code>\nand a branch to <code>entry<\/code>; we emit in the specialized function body an\nedge to the <code>(K1, entry)<\/code> or <code>(K2, entry)<\/code> block, which is the point\nin the compiled code that corresponds to the start of the \"copy of the\ninterpreter for that opcode\".<\/p>\n<p>We thus get <em>exactly the behavior we outlined as our desired endpoint\nabove<\/em>, where the bytecode's CFG gets populated with interpreter cases\nfor each opcode, and it \"just falls out\" of context sensitivity. This\nis mind-blowing (at least, to me!).<\/p>\n<p>An animation of a worked example for a simple two-opcode loop is\navailable in my talk\n<a rel=\"external\" href=\"https:\/\/www.youtube.com\/watch?v=_T3s6-C38JI&amp;t=27m06s\">here<\/a>.<\/p>\n<h3 id=\"generic-mechanism-vs-interpreter-specific-transform\">Generic Mechanism vs. Interpreter-Specific Transform<\/h3>\n<p>Seen a certain way, the use of constant-folding plus contexts seems a\nlittle circuitous at first: <code>pc<\/code> at the top of the loop is a constant\n<code>k<\/code> when we're in context <code>pc=k<\/code>; isn't that more complex than a\nmechanism that \"just\" understands interpreter PCs directly? Well,\nperhaps on the surface, but actually the implications of the\ncontext-sensitivity are fairly deep and make much of the rest of the\nanalysis \"just work\" as well. In particular, <em>all<\/em> values in the loop\nbody get their own PC-specific context to specialize in, and\ncontext-sensitivity is a fairly mechanical change to make to a\nstandard dataflow analysis, whereas something <em>specific<\/em> to\ninterpreters would likely be a lot more fragile and complex to\nmaintain.<\/p>\n<p>One way that the robustness of this \"compositional\" approach becomes\nmore clear is when considering (and trying to convince oneself of) the\n<em>correctness<\/em> of the approach. An extremely important property of the\n\"context\" is that it is, with respect to correctness (i.e., semantic\nequivalence to the original interpreter), <em>purely heuristic<\/em>. We could\ndecide to compute some arbitrary value and enter that context at any\npoint; we could get the PC \"wrong\", miss an update somewhere, etc. The\nworst that will happen is that we have two different constant values\nof <code>pc<\/code> merge at the loop header, and we can no longer branch-fold and\nspecialize the interpreter loop body for an opcode. The degenerate\nbehavior is that we get a copy of the interpreter loop body (with\nruntime control flow remaining) for every opcode. That's not great as\nan optimization, and of course we don't want it, but it is <em>still\ncorrect<\/em>. There is nothing that the (minor) modifications to the\ninterpreter, to add context sensitivity, can get wrong that will break\nthe semantics.<\/p>\n<h2 id=\"the-weval-transform\">The weval Transform<\/h2>\n<p>We're finally ready to see \"the weval transform\", which is the heart\nof the first Futamura projection that weval performs. It actually does\nthe constant-propagation analysis at the same time as the code\ntransform.<\/p>\n<p>The analysis and transform operate on SSA-based IR, which weval\nobtains from the original Wasm function via my\n<a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/waffle\">waffle<\/a> library, and then compiles\nthe SSA IR of the resulting specialized function back to Wasm function\nbytecode.<\/p>\n<p>The transform can be summarized with the following pseudocode:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>Input: - original function in SSA,   Output: - specialized function in SSA,<\/span><\/span>\n<span class=\"giallo-l\"><span>       - with some parameters                  whose semantics are<\/span><\/span>\n<span class=\"giallo-l\"><span>         specified as                          identical to the<\/span><\/span>\n<span class=\"giallo-l\"><span>         - &quot;constant&quot; or                       original function,<\/span><\/span>\n<span class=\"giallo-l\"><span>         - &quot;points to constant mem&quot;,           - given constants for<\/span><\/span>\n<span class=\"giallo-l\"><span>       - and with &quot;update_context&quot;               parameters, and<\/span><\/span>\n<span class=\"giallo-l\"><span>         intrinsics added.                     - assuming &quot;constant&quot; memory<\/span><\/span>\n<span class=\"giallo-l\"><span>                                                 remains the same.<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>State:<\/span><\/span>\n<span class=\"giallo-l\"><span>- Map from (context, block_orig) to block_specialized<\/span><\/span>\n<span class=\"giallo-l\"><span>- Map from (context, value_orig) to value_specialized<\/span><\/span>\n<span class=\"giallo-l\"><span>- Workqueue of (context, block_orig)<\/span><\/span>\n<span class=\"giallo-l\"><span>- Abstract state (Constant, ConstantMem or Unknown)<\/span><\/span>\n<span class=\"giallo-l\"><span>                 for each (context, value)<\/span><\/span>\n<span class=\"giallo-l\"><span>- Dependency map from value_specialized to<\/span><\/span>\n<span class=\"giallo-l\"><span>                      set((context, block_orig))<\/span><\/span>\n<span class=\"giallo-l\"><span>- Flow-sensitive state<\/span><\/span>\n<span class=\"giallo-l\"><span>  (context, abstract values for any global state)<\/span><\/span>\n<span class=\"giallo-l\"><span>  per basic-block input<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>Init:<\/span><\/span>\n<span class=\"giallo-l\"><span>- Push (empty context, entry block) onto workqueue.<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>Main loop:<\/span><\/span>\n<span class=\"giallo-l\"><span>- Pull a (context, block) from the workqueue.<\/span><\/span>\n<span class=\"giallo-l\"><span>- If a specialized block doesn&#39;t exist in the map,<\/span><\/span>\n<span class=\"giallo-l\"><span>  create one.<\/span><\/span>\n<span class=\"giallo-l\"><span>- For each value in the original block:<\/span><\/span>\n<span class=\"giallo-l\"><span>  - Fetch the abstract values for all operands<\/span><\/span>\n<span class=\"giallo-l\"><span>    (Constant or Unknown).<\/span><\/span>\n<span class=\"giallo-l\"><span>  - Evaluate the operator: if constant input(s) imply<\/span><\/span>\n<span class=\"giallo-l\"><span>    a constant output, compute that.<\/span><\/span>\n<span class=\"giallo-l\"><span>  - Copy either the original operator with translated<\/span><\/span>\n<span class=\"giallo-l\"><span>    input values, or a &quot;constant value&quot; operator,<\/span><\/span>\n<span class=\"giallo-l\"><span>    to the output block.<\/span><\/span>\n<span class=\"giallo-l\"><span>  - Update value map and abstract state.<\/span><\/span>\n<span class=\"giallo-l\"><span>    - If dependency map shows dependency to<\/span><\/span>\n<span class=\"giallo-l\"><span>      already-processed block, enqueue on workqueue<\/span><\/span>\n<span class=\"giallo-l\"><span>      for reprocessing.<\/span><\/span>\n<span class=\"giallo-l\"><span>  - On visiting an update_context intrinsic, update<\/span><\/span>\n<span class=\"giallo-l\"><span>    flow-sensitive state.<\/span><\/span>\n<span class=\"giallo-l\"><span>- For the block terminator:<\/span><\/span>\n<span class=\"giallo-l\"><span>  - Branch-fold if able based on constant inputs<\/span><\/span>\n<span class=\"giallo-l\"><span>    to conditionals or switches.<\/span><\/span>\n<span class=\"giallo-l\"><span>  - For each possible target,<\/span><\/span>\n<span class=\"giallo-l\"><span>    - update blockparams, meeting into existing<\/span><\/span>\n<span class=\"giallo-l\"><span>      abstract state;<\/span><\/span>\n<span class=\"giallo-l\"><span>    - enqueue on workqueue if blockparam abstract state<\/span><\/span>\n<span class=\"giallo-l\"><span>      changed or if new block;<\/span><\/span>\n<span class=\"giallo-l\"><span>    - rewrite target in branch to specialized block<\/span><\/span>\n<span class=\"giallo-l\"><span>      for this context, block pair.<\/span><\/span><\/code><\/pre>\n<p>And that's it!<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup> Note a few things in particular:<\/p>\n<ul>\n<li>\n<p>The algorithm is never aware that it is operating on \"an\ninterpreter\", much less any specific interpreter. This is good: we\ndon't need to tightly couple the compiler tooling here with the\nuse-case (a SpiderMonkey interpreter loop), and it means that we can\ndebug and test each separately.<\/p>\n<\/li>\n<li>\n<p>The algorithm, at this high level at least, has no special cases or\nother oddities: it is more or less what one would expect out of a\nconstant-propagation and branch folding pass that operates in a\nfixpoint, with the only really novel part that it keeps a \"current\ncontext\" as state and parameterizes all lookups in maps on that\ncontext. Once one accepts that \"duplication of code\" (processing the\nsame blocks multiple times, in different contexts) is correct, it's\nreasonable to convince oneself that the whole algorithm is correct.<\/p>\n<\/li>\n<li>\n<p>The algorithm leaves room for other kinds of flow-sensitive state\nand intrinsics, if we <em>do<\/em> want to provide more optimizations to the\nuser. (More on this below!)<\/p>\n<\/li>\n<\/ul>\n<p>I am omitting some details around SSA properties -- specifically, the\nuse of values in basic blocks in the original function that are\ndefined in other blocks higher in the domtree (which is perfectly\nlegal in SSA). Because the new specialized function body may have\ndifferent dominance relationships between blocks, some of these\nuse-def links are no longer legal. The naive approach to this problem\n(and the one I took at first) is to transform into \"maximal SSA\"\nfirst, in which all live values are carried as blockparams at every\nblock; that turns out to be really expensive, so weval computes a\n\"cut-set\" of blocks at which max-SSA is actually necessary based on\nthe locations of context-update intrinsics (intuitively, approximately\njust the interpreter backedge, but we want the definition of this to\nbe independent of the notion of an interpreter and where it does\ncontext updates).<\/p>\n<h2 id=\"using-weval-function-specialization-in-a-snapshot-abstraction\">Using weval: \"Function Specialization in a Snapshot\" Abstraction<\/h2>\n<p>So how does one interact with this tool, as an interpreter author?<\/p>\n<p>As noted above, for a Futamura projection, the key abstraction is\n\"function specialization\" (or partial evaluation). In the context of\nweval, this means that the interpreter requests a specialization of\nsome \"generic\" interpreter function, with some constant inputs (at\nleast the bytecode, presumably!), and gets back a function pointer to\nanother function in return. That specialized function is semantically\nequivalent to the original, except that it has all of the specified\nconstants (function parameters and memory contents) \"baked in\" and has\nbeen optimized accordingly.<\/p>\n<p>Said another way: the specialized function body will be the compiled\nform of the given bytecode.<\/p>\n<p>Because the specialization works in the context of data in memory (the\nbytecode), it has to happen in the context of running program\nstate. For example, in SpiderMonkey, there is a <code>class JSFunction<\/code>\nthat ultimately points to some bytecode, and we want to produce a new\nWasm function that bakes in everything about that\n<code>JSFunction<\/code>. Because this object only exists in the Wasm heap after\nthe Wasm code has run, parsed some JS source, allocated some space on\nthe heap, and emitted its internal bytecode, we cannot do the\nspecialization on the \"original Wasm\" outside the scope of some\nparticular execution.<\/p>\n<p>Recall from the <a href=\"\/blog\/2024\/08\/27\/aot-js\/\">earlier post<\/a> that we want\na \"phase separation\": we want a fully ahead-of-time process. We\ndefinitely cannot embed a weval transform in a Wasm engine and expose\nit at runtime. How do we reconcile this need for phasing with weval's\nrequirement to see the heap snapshot and specialize over it?<\/p>\n<p>The answer comes in the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wizer\">Wizer<\/a> tool, or more\ngenerally the notion of <em>snapshotting<\/em>: we modify the top-level logic\nof the language runtime so that we can parse source and produce\ninternal bytecode when invoked with some entry point, and set up\nglobal state so that an invocation from another entry point actually\nruns the code. This work has already been done for various runtimes,\nincluding SpiderMonkey, because it is a useful transform to \"bundle\"\nan interpreter with pre-parsed bytecode and in-memory data structures,\neven if we don't do any compilation.<\/p>\n<p>weval thus builds on top of Wizer: it takes a Wasm snapshot, with all\nthe runtime's data structures, finds the bytecode and interpreter\nfunction, does the specialization, and appends new functions to the\nWasm snapshot.<\/p>\n<p>How does weval find what it needs to specialize? The interpreter,\ninside the Wasm module, makes \"weval requests\" via an API that\n(nominally, in simplified terms) looks like:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"c\"><span class=\"giallo-l\"><span class=\"z-storage\">void<\/span><span class=\"z-entity z-name z-function\"> weval_request<\/span><span class=\"z-punctuation z-section\">(<\/span><span class=\"z-support z-type\">func_t<\/span><span class=\"z-keyword z-operator\">*<\/span><span class=\"z-variable z-parameter\"> specialized<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-support z-type\"> func_t<\/span><span class=\"z-variable z-parameter\"> generic<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-support z-type\"> param_t<\/span><span class=\"z-keyword z-operator\">*<\/span><span class=\"z-variable z-parameter\"> params<\/span><span class=\"z-punctuation z-section\">)<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span><\/code><\/pre>\n<p>where <code>generic<\/code> is a function pointer to the generic function,\n<code>specialized<\/code> is a location on the heap to place a function pointer to\nthe resulting specialized function, and <code>params<\/code> encodes all of the\ninformation to specialize on (including the pointer to bytecode).<\/p>\n<p>Internally, this \"request\" is recorded as a data structure with a\nwell-known format in the Wasm heap, and is linked into a linked list\nthat is reachable via a well-known global. weval knows where to look\nin the Wasm snapshot to find these requests.<\/p>\n<p>From the point of view of the guest, these requests are fulfilled\nasynchronously: the <code>specialized<\/code> function pointer will not be filled\nin right away with a new function, but will be at some point in the\nfuture. The interpreter thus must have a conditional when invoking\neach function: if the specialized version exists, call that, otherwise\ncall the generic interpreter body.<\/p>\n<p>Why asynchronously? Because this is how snapshotting appears from the\nguest perspective: code runs during Wizer's pre-initialization phase,\nthe language runtime's frontend parses source and creates bytecode,\nand weval requests are created for every function that is \"born\". It\nis only when pre-initialization execution completes, a snapshot of the\nheap is taken, and that snapshot is processed by weval, that weval can\nappend new functions, fill in the function pointers in the heap\nsnapshot, and write out a new <code>.wasm<\/code> file. When <em>that<\/em> Wasm module is\nlater instantiated, it \"wakes up\" again from the snapshot and the\nspecialized functions suddenly exist. Voil\u00e0, compiled code.<\/p>\n<h2 id=\"correctness-via-semantics-preserving-specialization\">Correctness via Semantics-Preserving Specialization<\/h2>\n<p>It's worth emphasizing one aspect of this whole design again. In order\nto use weval, an interpreter (i) adds some <code>update_context<\/code> intrinsics\nwhenever <code>pc<\/code> is updated, and (ii) makes weval requests for function\npointers to specialized code. That's it.<\/p>\n<p>In particular, nothing in the interpreter has to encode the execution\nsemantics of the guest-language bytecode in a way that is only seen\nduring compiled-code execution. Rather, weval'd compiled code and\ninterpreted code execute using <em>exactly the same opcode\nimplementations<\/em>, by construction. This is a <em>radical simplification<\/em>\nin the testing complexity of the whole source-language compilation\napproach: we can test and debug the interpreter (and running wherever\nwe like, including in a native rather than Wasm build), and we can\nseparately test and debug weval.<\/p>\n<p>How do we test weval? During its development, I added a lockstep\nexecution mode where the \"generic\" function and \"specialized\" function\nboth run in a snapshot and we compare the results. If the transform is\ncorrect, they should be identical, independent of whatever the\ninterpreter code is doing. Certainly we are running end-to-end tests\nof weval'd JS code as well for added assurance, but this test\nfactoring was a huge help in the \"bring-up\" of this approach.<\/p>\n<h2 id=\"optimized-codegen-abstracted-interpreter-state\">Optimized Codegen: Abstracted Interpreter State<\/h2>\n<p>One final suite of optimization ideas extends from the question: how\ncan we make the interpreter's management of guest-language state more\nefficient?<\/p>\n<p>To unpack this a bit: when compiling any imperative language (at least\nlanguages with reasonable semantics), we can usually identify elements\nof the guest-language state, such as local variables, and map that\nstate directly to \"registers\" or other fast storage with fixed, static\nnames. (In Wasm bytecode, we use locals for this.) However, when\ninterpreting such a language, usually we have implementations for\nopcodes that are generic over <em>which<\/em> variables we are reading and\nwriting, so we have runtime indirection. To make this concrete, if we\nhave a statement<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>z = x + y;<\/span><\/span><\/code><\/pre>\n<p>we can compile this to something like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>add z, x, y<\/span><\/span><\/code><\/pre>\n<p>in machine code or Wasm bytecode (with <code>add<\/code> suitably replaced by\nwhatever the static or dynamic types require). But if we have a case\nfor this operator in an interpreter loop, we have to write<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>regs[z] = regs[x] + regs[y];<\/span><\/span><\/code><\/pre>\n<p>and in the compiled interpreter body, this implies many reads and\nwrites to the <code>regs<\/code> array in memory.<\/p>\n<p>The weval approach to compilation-by-specializing-interpreters\ndirectly copies the interpreter cases for each opcode. We can\nspecialize the register\/variable indices <code>x<\/code>, <code>y<\/code>, and <code>z<\/code> in the\nabove code, but we cannot (or should not) turn memory loads and stores\nto an in-memory <code>regs<\/code> array into direct values in registers (Wasm\nlocals).<\/p>\n<p>Why? Not only would this require a fair amount of complexity, it would\noften be <em>incorrect<\/em>, at least in the case where <code>regs<\/code> is observable\nfrom other parts of the interpreter. For example, if the interpreter\nhas a moving garbage collector, every part of the interpreter state\nmight be subject to tracing and pointer rewriting at any \"GC\nsafepoint\", which could be at many different points in the interpreter\nbody.<\/p>\n<p>Better, and more in the spirit of weval, would be to provide\nintrinsics that the interpreter can <em>opt into<\/em> to indicate that some\nvalue storage <em>can<\/em> be rewritten into direct dataflow and memory loads\nand stores can be optimized out. weval provides such intrinsics for\n\"locals\", which are indexed by integers that must be constant during\nconst-prop (i.e., must come directly from the bytecode), and for an\n\"operand stack\", so that interpreters of stack VM-based bytecode (as\nSpiderMonkey's JS opcode VM is) can perform <em>virtual<\/em> pushes and\npops. weval tracks the abstract state of these locals and stack slots,\nand \"flushes\" the state to memory only on a \"synchronize\" intrinsic,\nwhich the interpreter uses when its state may be externally observable\n(or never, if its design allows that). These intrinsics provided a\nsubstantial speedup in SpiderMonkey's case.<\/p>\n<h2 id=\"other-optimizations-and-details\">Other Optimizations and Details<\/h2>\n<p>I've elided a fair number of other details, of course! Worthy of note\nare a few other things:<\/p>\n<ul>\n<li>\n<p>The weval tool has top-level caching per \"specialization\nrequest\". It takes a hash of the input Wasm module and the \"argument\nstring\", which includes the bytecode and other constant values, of\neach specialization request. If it has seen the exact input module\nand a particular argument string before, it copies the resulting\nWasm function body directly out of the cache, as fast as SQLite and\nthe user's disk can go. This turns out to be really useful for the\n\"AOT ICs\" corpus mentioned in the previous post.<\/p>\n<\/li>\n<li>\n<p>I added special handling to elide the remnants of LLVM's shadow\nstack in function bodies where constant-propagation and\nspecialization removed all uses of data on the shadow stack. This\ntechnically removes \"side-effects\" (updates to the shadow stack\npointer global) but only if they are unobserved, as determined by an\nescape analysis.<\/p>\n<\/li>\n<li>\n<p>Some kinds of patterns result from the partial evaluation that can\nbe cleaned up further. In particular, when an initial value (such as\na <code>pc<\/code> or <code>sp<\/code>) is <code>Unknown<\/code> (not constant or known at compile\ntime), but <em>offsets<\/em> from that value <em>are<\/em> constant properties of\nprogram points in the bytecode, then we see chains of <code>pc' = pc + 3<\/code>, <code>pc'' = pc' + 4<\/code>, <code>pc''' = pc'' + 1<\/code>, etc. We can rewrite all of\nthese in terms of the original non-constant value, in essence a\nre-association of <code>(x + k1) + k2<\/code> to <code>x + (k1 + k2)<\/code>, which removes\na lot of extraneous dataflow from the fastpath of compiled code.<\/p>\n<\/li>\n<li>\n<p><code>switch<\/code> statements in the source language result in\nuser-data-dependent \"next contexts\" and <code>pc<\/code> values; what is needed\nis to instead \"value-specialize\", i.e. split into sub-contexts each\nof which assumes <code>i = 0<\/code>, <code>i = 1<\/code>, ..., <code>i &gt;= N<\/code>, generate a\nWasm-level switch opcode in the output, and constant-propagate\naccordingly. This is how we can turn arbitrary-fanout control flow\nin the source bytecode directly into the same control flow in Wasm.<\/p>\n<\/li>\n<\/ul>\n<h2 id=\"results-annotation-overhead-and-runtime-speedups\">Results: Annotation Overhead and Runtime Speedups<\/h2>\n<p>weval is a general tool, usable by any interpreter; to evaluate it,\nwe'll need to consider it in the consequence of particular\ninterpreters. Because it was developed with the SpiderMonkey JS engine\nin mind, and and for my work to enable fully ahead-of-time JS\ncompilation, I'll mainly present results in that context<\/p>\n<p><em>Annotation Overhead<\/em>: the <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/gecko-dev\/pull\/48\">PR that adds weval\nsupport<\/a> to\nSpiderMonkey shows a total lines-of-code delta of <code>+1045 -2<\/code> -- a\nlittle over a thousand lines of code -- though much of this is the\nvendored <code>weval.h<\/code> (part of weval proper, imported into the tree), and\na C++ RAII wrapper around weval requests (reusable in other\ninterpreters). The actual changes to the interpreter loop come in at\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/gecko-dev\/pull\/48\/files#diff-4958b3e538c41183f77fdc060e3947e5bbbcd7e8da1905dada8b4c9abe3ebabe\">133 lines of\ncode<\/a>\nin an alternative set of macro definitions -- and, if we're being\nfair, the earlier mechanical changes to use these macros to access and\nupdate interpreter state rather than direct code. Then there is a\nlittle bit of plumbing to create weval requests when a function is\ncreated (<code>EnqueueScriptSpecialization<\/code> and\n<code>EnqueueICStubSpecialization<\/code>), about a hundred lines total including\nthe definitions of those functions.<\/p>\n<p>As an additional demonstration of ease-of-use, one might consider <a rel=\"external\" href=\"https:\/\/bernsteinbear.com\/blog\/weval\/\">Max\nBernstein's post<\/a>, in which Max\nand I wrote a toy interpreter (10 opcodes) and weval'd it in a few\nhours.<\/p>\n<p><em>Runtime Speedup<\/em>: In my <a href=\"\/blog\/2024\/08\/27\/aot-js\/#results\">earlier\npost<\/a> I presented results showing\noverall 2.77x geomean speedup, with up to 4.39x on the highest\nbenchmark (over 4x on 3 of 13). This is the work of significant\noptimization in SpiderMonkey too -- this speedup does not come for\nfree on day one of a weval-adapted interpreter -- but the results are\nabsolutely enabled by AOT compilation, with the weval'd PBL\ninterpreter body running as an interpreter providing only 1.27x\nperformance over the generic interpreter. This means that weval's\ncompilation via the first Futamura projection, as well as lifting of\ninterpreter dataflow out of memory to SSA, and other miscellaneous\npost-processing optimization passes, are responsible for a 2.19x\nspeedup (and that part truly is \"for free\").<\/p>\n<p>Overall, I'm pretty happy with these results; and we are working on\nshipping the use of weval to AOT-compile JS in the Bytecode Alliance\n(see\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/StarlingMonkey\/pull\/91\">here<\/a>).<\/p>\n<h2 id=\"related-work\">Related Work<\/h2>\n<h3 id=\"graalvm\">GraalVM<\/h3>\n<p>Finally, to place weval in a broader context: in addition to the\nfoundational work on Futamura projections, weval is absolutely\ninspired by GraalVM, the only other significant (in fact far more\nsignificant) compiler based on partial-evaluation-of-interpreters to\nmy knowledge. weval is in broad strokes doing \"the same thing\" as\nGraalVM, but makes a few (massive) simplifications and a few\ngeneralizations as well:<\/p>\n<ul>\n<li>\n<p>weval targets WebAssembly, a general-purpose low-level compiler\ntarget that can support interpreters written in many languages\n(e.g., C and C++, covering a majority of the most frequently-used\nscripting language interpreters and JavaScript interpreters today),\nwhile GraalVM targets the Java Virtual Machine.<\/p>\n<\/li>\n<li>\n<p>weval is fully ahead-of-time, while classically at least, GraalVM is\nclosely tied with the JVM's JIT and requires runtime codegen and\n(infamously) long warmup times. More recent versions of GraalVM also\nsupport <a rel=\"external\" href=\"https:\/\/www.graalvm.org\/latest\/reference-manual\/native-image\/\">GraalVM Native\nImage<\/a>,\nthough GraalVM still carries complexity due to its support for,\ne.g., runtime de-optimization.<\/p>\n<\/li>\n<li>\n<p>weval requires the interpreter to add a few intrinsics (details\nbelow) but otherwise \"meets interpreters where they are\", allowing\nadaptation of existing industrial-grade language implementations\n(such as SpiderMonkey!), while GraalVM requires the interpreter to\nbe written in terms of its own AST classes.<\/p>\n<\/li>\n<li>\n<p>On the other hand, weval, as an AOT-only transform, does not support\nany speculative-optimization or deoptimization mechanisms, or\ngradual warmup, vastly simplifying the design as compared to GraalVM\nbut also limiting performance and flexibility (for now).<\/p>\n<\/li>\n<\/ul>\n<h3 id=\"porffor-static-hermes-static-typescript-assemblyscript\">porffor, Static Hermes, Static TypeScript, AssemblyScript<\/h3>\n<p>There have been a variety of ahead-of-time compilers that accept\neither JavaScript\n(<a rel=\"external\" href=\"https:\/\/www-sop.inria.fr\/members\/Manuel.Serrano\/publi\/serrano-dls18.pdf\">Hopc<\/a>),\n<a rel=\"external\" href=\"https:\/\/porffor.dev\/\">porffor<\/a>) or annotated JS (<a rel=\"external\" href=\"https:\/\/github.com\/facebook\/hermes\">Static\nHermes<\/a>, <a rel=\"external\" href=\"https:\/\/www.microsoft.com\/en-us\/research\/uploads\/prod\/2019\/09\/static-typescript-draft2.pdf\">Static\nTypeScript<\/a>),\nor a JavaScript\/TypeScript-like language\n(<a rel=\"external\" href=\"https:\/\/www.assemblyscript.org\/\">AssemblyScript<\/a>).<\/p>\n<p>Manuel Serrano in his <a rel=\"external\" href=\"https:\/\/www-sop.inria.fr\/members\/Manuel.Serrano\/publi\/serrano-dls18.pdf\">DLS'18\npaper<\/a>\ndescribes building an ahead-of-time compiler, Hopc, for JS that takes\na simple approach to specialization without runtime type observations:\nit builds <em>one<\/em> specialized version of each function alongside the\ngeneric version, and uses a set of type-inference rules to pick the\nmost <em>likely<\/em> types. This is shown to work quite well in the\npaper. The main downside is the limit to how far this inference can\ngo: for example, SpiderMonkey's ICs can specialize property accesses\non object shape, while Hopc does not infer object shapes and in\ngeneral such an analysis is hard (similar to global pointer analysis).<\/p>\n<p>The porffor JS compiler is (as best I can tell!)  emitting \"direct\"\ncode for JS operators, with some type inference to reduce the number\nof cases needed. The speedups on small programs are compelling, and I\nthink that taking this approach from scratch actually makes a lot of\nsense. That said, JS is a large language with a lot of awkward\ncorner-cases, and with (as of writing) 39% of the ECMA-262 test suite\npassing, porffor still has a hill to climb to reach parity with mature\nengines such as SpiderMonkey or V8. I sincerely wish them all the best\nof luck and would love to see the project succeed!<\/p>\n<p>Static Hermes and Static TypeScript both adopt the idea: what if we\nrequire type annotations for compilation? In that case, we can\ndispense with the whole idea of dynamic IC dispatch -- we know which\ncase(s) will be needed ahead of time and we can inline them\ndirectly. This is definitely the right answer if one has those\nannotations. More modern codebases tend to have more discipline in\nthis regard; unfortunately, the world is filled with \"legacy JS\" and\nso the idea is not universally applicable.<\/p>\n<p>AssemblyScript is another design point in that same spirit, though\nfurther: though it superficially resembles TypeScript, its semantics\nare often a little different, in a way that makes the language simpler\n(to the implementer) overall but creates major compatibility hurdles\nin any effort to port existing TypeScript. For example, it does not\nsupport function closures (lambdas), iterators or exceptions, all\nregularly used features in large JS\/TS codebases today.<\/p>\n<p>Compared to all of the cases above, the approach we have taken here --\nadapting SpiderMonkey and turning a 100%-coverage interpreter tier\ninto a compiler with weval -- provides <em>full compatibility<\/em> with\neverything that the SpiderMonkey engine supports, i.e., all recent\nJavaScript features and APIs, without requiring anything special.<\/p>\n<h2 id=\"conclusion-principled-derivation-of-a-jit\">Conclusion: Principled Derivation of a JIT<\/h2>\n<p>weval has been an incredibly fun project, and honestly, that it is\nworking this well has surprised me. GraalVM is certainly an existence\nproof for the idea, but it also has engineer-decades (perhaps\ncenturies?)  of work poured into it; it was a calculated risk I took\nto follow my intuitive sense that one could do it simpler if focused\non the AOT-only case, and if leveraging the simplicity and full\nencapsulation of Wasm as a basis for program transforms. I do believe\nthat weval was the shortest path I could have taken to produce an\nahead-of-time compiler (that is, no warmup or runtime codegen) for\nJavaScript. This is largely for the reason that it let me factor out\nthe \"semantics of the bytecode execution\" problem, i.e., writing the\nPBL interpreter, from the code generation problem; get the former\nright with a native debugger and native test workflow; and then do a\nprincipled transform to get a compiler out of it.<\/p>\n<p>This is the big-picture story I want to tell as well: we <em>can<\/em> \"factor\ncomplexity\" by replacing manual implementations of large, complex\nsystems with automatic derivations in some cases. Likely it won't ever\nbe completely as good as the hand-written system; but it might get\nclose, if one can find the right abstractions to express all the right\noptimizations. Profile-guided inlining of ICs to take AOT-compiled\n(weval'd) JS code to the next performance level is another good\nexample, and in fact it was inspired by WarpMonkey which took the same\n\"principled derivation from one single source of truth\" approach to\nbuilding a higher-performance tier. There is a lot more here to do.<\/p>\n<hr \/>\n<p><em>Thanks to Luke Wagner, Nick Fitzgerald, and Max Bernstein for reading\nand providing feedback on a draft of this post!<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>In reality, native baseline compilers often keep a \"virtual\nstack\" that allows avoiding some push and pop operations --\nbasically by keeping track of which registers represent the\nvalues on the top of the stack whose pushes have been deferred,\nand using them directly rather than popping when possible,\nforcing a deferred \"synchronization\" only when the full stack\ncontents have to be reified for, say, a call or a GC safepoint.\nBoth SpiderMonkey's baseline compiler and Wasmtime's Winch\nimplement this optimization, which can be seen as a simple form\nof <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Abstract_interpretation\">abstract\ninterpretation<\/a>.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>For more on this topic, <a rel=\"external\" href=\"https:\/\/www.cs.cmu.edu\/afs\/cs\/academic\/class\/15745-s13\/public\/lectures\/L5-Foundations-of-Dataflow-1up.pdf\">these\nslides<\/a>\nare a good introduction. The online book <a rel=\"external\" href=\"https:\/\/cs.au.dk\/~amoeller\/spa\/spa.pdf\">Static Program\nAnalysis<\/a> by M\u00f8ller and\nSchwartzbach is also an invaluable resource, as well as the\nclassic <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Compilers:_Principles,_Techniques,_and_Tools\">Dragon\nBook<\/a>\n(Aho, Lam, Sethi, Ullman).<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>\"Simple\", right?! The real implementation is <a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/weval\/blob\/bb253a398f0fd622f64af69abaa9e39e54f56769\/src\/eval.rs\">about 2400 lines\nof\ncode<\/a>,\nwhich is actually not terrible for something this powerful,\nIMHO. I'm personally shocked how \"small\" weval turned out to be.<\/p>\n<\/div>\n"},{"title":"Compilation of JavaScript to Wasm, Part 2: Ahead-of-Time vs. JIT","published":"2024-08-27T00:00:00+00:00","updated":"2024-08-27T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2024\/08\/27\/aot-js\/"}},"id":"https:\/\/cfallin.org\/blog\/2024\/08\/27\/aot-js\/","content":"<p><em>This is a continuation of my \"fast JS on Wasm\" series; the <a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/\">first\npost<\/a> covered PBL, a portable\ninterpreter that supports inline caches, this post adds ahead-of-time\ncompilation, and the final post will discuss the details of that\nahead-of-time compilation. Please read the first post first for useful\ncontext!<\/em><\/p>\n<hr \/>\n<p>The most popular programming language in the world, by a wide margin\n-- thanks to the ubiquity of the web -- is\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/JavaScript\">JavaScript<\/a> (or, if you\nprefer to follow international standards, ECMAScript per\n<a rel=\"external\" href=\"https:\/\/ecma-international.org\/publications-and-standards\/standards\/ecma-262\/\">ECMA-262<\/a>). For\na computing platform to be relevant to many modern kinds of\napplications, it should run JavaScript.<\/p>\n<p>For the past four years or so, I have been working on\n<a rel=\"external\" href=\"https:\/\/webassembly.org\/\">WebAssembly<\/a> (Wasm) tooling and platforms,\nand in particular, running Wasm \"outside the browser\" (where it was\nborn), using it for strong\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Sandbox_(computer_security)\">sandboxing<\/a>\nof untrusted server-side code instead.<\/p>\n<p>This blog post will describe my work, over the past 1.5 years, to\nbuild an <em>ahead-of-time compiler<\/em> from JavaScript to WebAssembly\nbytecode, achieving a 3-5x speedup. The work is now technically\nlargely complete: it has been integrated into the <a rel=\"external\" href=\"https:\/\/www.bytecodealliance.org\/\">Bytecode\nAlliance<\/a>'s version of SpiderMonkey\n(to be eventually upstreamed, ideally), then our shared\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/starlingmonkey\">StarlingMonkey<\/a>\nJS-on-Wasm runtime on top of that (available with the <code>--aot<\/code> flag to\nthe <code>componentize.sh<\/code> toplevel build command), then my employer's JS\nSDK built on top of StarlingMonkey. It passes all \"JIT tests\" and \"JS\ntests\" in the SpiderMonkey tree, and all Web Platform Tests at the runtime\nlevel. It's still in \"beta\" and considered experimental, but now is a good time\nto do a deep-dive into how it works!<\/p>\n<p>This JavaScript AOT compilation approach is built on top of my\n<a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/\">Portable Baseline Interpreter<\/a>\nwork in SpiderMonkey, combined with my\n<a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/weval\">weval<\/a> partial program evaluator to\nprovide compilation from an interpreter body \"for free\" (using a\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Partial_evaluation#Futamura_projections\">Futamura\nprojection<\/a>). I\nhave recently <a rel=\"external\" href=\"https:\/\/www.youtube.com\/watch?v=_T3s6-C38JI\">given a\ntalk<\/a> about this work. I\nhope to go into a bit more depth in this post and a followup one;\nfirst, how one can compile JavaScript ahead-of-time at all, and to\ncome later, how weval enables easier construction of the necessary\ncompiler backends.<\/p>\n<h2 id=\"background\">Background<\/h2>\n<p>How do we run JavaScript (JS) on a platform that supports WebAssembly?\nAt first, this question was needless, because Wasm originated within\nexisting JS engines as a way to provide lower-level, strongly-typed\ncode directly to the engine's compiler backend: so one could run JS\ncode alongside any Wasm modules and they could interact at a\nfunction-call level. But on Wasm-first platforms, \"outside the\nbrowser\" as we say, we have no system-level JS engine; we can only run\nWasm modules that have been uploaded by the user, which interact with\nthe platform via direct \"hostcalls\" available as Wasm imports.<\/p>\n<p>To quote my <a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/\">earlier blog post<\/a> on\nthis topic, by adopting this restriction, we obtain a number of\nadvantages:<\/p>\n<blockquote>\n<p>Running an entire JavaScript engine <em>inside<\/em> of a Wasm module may\nseem like a strange approach at first, but it serves real\nuse-cases. There are platforms that accept WebAssembly-sandboxed\ncode for security reasons, as it ensures complete memory isolation\nbetween requests while remaining very fine-grained (hence with lower\noverheads). In such an environment, JavaScript code needs to bring\nits own engine, because no platform-native JS engine is\nprovided. This approach ensures a sandbox <em>without trusting the\nJavaScript engine's security<\/em> -- because the JS engine is just\nanother application on the hardened Wasm platform -- and carries\nother benefits too: for example, the JS code can interact with other\nlanguages that compile to Wasm easily, and we can leverage Wasm's\ndeterminism and modularity to snapshot execution and then perform\nextremely fast cold startup.<\/p>\n<\/blockquote>\n<p>So we have very fine-grained isolation and security, we eliminate JIT\nbugs altogether (the most productive source of CVEs in production\nbrowsers today!), and the modularity enables interesting new system\ndesign points.<\/p>\n<p>Unfortunately, the state of the art for bundling JS \"with its own\nengine\" today is, still, to combine an <em>interpreter<\/em> with a bytecode\nrepresentation of the JS, without any compilation of the JS to\nspecialized \"native\" (or in this case Wasm) code. There are two\nreasons. The first and most straightforward one is that Wasm is simply\na new platform; JS engines' interpreters can be ported relatively\nstraightforwardly, because they have little dependence on the\nunderlying instruction set architecture\/platform, but JIT compilers\nvery much do.<\/p>\n<h2 id=\"compiling-js-why-is-it-hard\">Compiling JS: Why is it Hard?<\/h2>\n<p>It's worth reviewing first why this is a hard problem. It's possible\nto compile quite a few languages to a Wasm target today: C or C++\n(with <a rel=\"external\" href=\"https:\/\/github.com\/webassembly\/wasi-sdk\">wasi-sdk<\/a> or\n<a rel=\"external\" href=\"https:\/\/emscripten.org\/\">Emscripten<\/a>, for example), Rust (with its\n<a rel=\"external\" href=\"https:\/\/www.rust-lang.org\/what\/wasm\">first-class Wasm support<\/a>),\n<a rel=\"external\" href=\"https:\/\/swiftwasm.org\/\">Swift<\/a>,\n<a rel=\"external\" href=\"https:\/\/kotlinlang.org\/docs\/wasm-overview.html\">Kotlin<\/a>,\n<a rel=\"external\" href=\"https:\/\/dev.virtuslab.com\/p\/scala-to-webassembly-how-and-why\">Scala<\/a>,\n<a rel=\"external\" href=\"https:\/\/ghc.gitlab.haskell.org\/ghc\/doc\/users_guide\/wasm.html\">Haskell<\/a>,\n<a rel=\"external\" href=\"https:\/\/github.com\/ocaml-wasm\/wasm_of_ocaml\">OCaml<\/a>, and probably\nmany more. What makes JavaScript different?<\/p>\n<p>The main difficulty comes from JS's <em>dynamic typing<\/em>: because\nvariables are not annotated or constrained to a single primitive type\nor object class, a simple expression like <code>x + y<\/code> could mean many\ndifferent things. This could be an integer or floating-point numeric\naddition, a string concatenation, or an invocation of arbitrary JS\ncode (via <code>.toString()<\/code>, <code>.valueOf()<\/code>, proxy objects, or probably\nother tricks too). A naive compiler would generate a large amount of\ncode for this simple expression that performs type checks and\ndispatches to one of these different cases. For both runtime\nperformance reasons (checking for all cases is quite slow) and\ncode-size reasons (can we afford hundreds or thousands of Wasm\ninstructions for each JS operator?), this is impractical. We will need\nto somehow adapt the techniques that modern JS engines have invented\nto the Wasm platform.<\/p>\n<p>This leads us directly to the engineering marvel\nthat is the modern JIT (just-in-time) compiler for JavaScript. These\nengines, such as <a rel=\"external\" href=\"https:\/\/spidermonkey.dev\/\">SpiderMonkey<\/a> (part of\n<a rel=\"external\" href=\"https:\/\/firefox.org\/\">Firefox<\/a>), <a rel=\"external\" href=\"https:\/\/v8.dev\/\">V8<\/a> (part of\n<a rel=\"external\" href=\"https:\/\/www.chromium.org\/Home\/\">Chromium<\/a>), and\n<a rel=\"external\" href=\"https:\/\/github.com\/WebKit\/WebKit\/tree\/main\/Source\/JavaScriptCore\">JavaScriptCore<\/a>\n(part of <a rel=\"external\" href=\"https:\/\/webkit.org\/\">WebKit<\/a>), generate <em>native machine\ncode<\/em> for JavaScript code. They work around the above difficulty by\ngenerating only code for the cases that <em>actually occur<\/em>, specializing\nbased on runtime observations.<\/p>\n<p>These JITs work fantastically well today on native platforms, but\nthere are technical reasons why a Wasm-based platform is a poor fit\nfor current JS engines' designs.<\/p>\n<h2 id=\"unique-characteristics-of-a-wasm-platform\">Unique Characteristics of a Wasm Platform<\/h2>\n<p>A typical \"Wasm-first\" platform -- whether that be\n<a rel=\"external\" href=\"https:\/\/wasmtime.dev\/\">Wasmtime<\/a> running server-side, serving\nrequests with untrusted handler code, or a sandboxed plugin interface\ninside a desktop application, or an embedded system -- has a few key\ncharacteristics that distinguish it from a typical native OS\nenvironment (such as that seen by a Linux x86-64 process).<\/p>\n<h3 id=\"no-runtime-codegen\">No Runtime Codegen<\/h3>\n<p>First, these platforms typically <em>lack any dynamic\/runtime\ncode-generation mechanism<\/em>: that is, unlike a native process' ability\nto JIT-compile new machine code and run it, a Wasm module has no\ninterface by which it can add new code at runtime. In other words,\ncode-loading functionality is <em>outside the sandbox<\/em>, typically by a\n\"deployment\" or \"control plane\" functionality on the platform.<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>\nThis allows greater management flexibility for the platform: for\nexample, it allows deployment of pre-compiled machine code for all\nWasm modules from a central control plane, allowing \"instant start\"\nwhen a module is instantiated to serve a request. It also brings\nsignificant security benefits: all code that is executing is known\na-priori, which makes security exploits harder to hide.<\/p>\n<p>The major downside of this choice, of course, is that one cannot\nimplement a JIT in the traditional way: one cannot generate new code\nbased on observed behavior at runtime, because there is simply no\nmechanism to invoke it.<\/p>\n<h3 id=\"protected-call-stack-and-no-primitive-control-flow\">Protected Call Stack and No Primitive Control Flow<\/h3>\n<p>Second, Wasm itself has some interesting divergences from conventional\ninstruction-set architecture (ISA) design. While a typical ISA on a\nconventional CPU provides \"raw\" branch and call instructions that\ntransfer control to addresses in the main memory address space,\nWebAssembly has first-class abstractions for modules and functions\nwithin those modules, and all of its inter-function transfer\ninstructions (calls and returns) target known function entry points by\nfunction index and maintain a protected (unmodifiable) call-stack. The\nmain advantage of this design choice is that a \"trusted stack\" allows\nfor function-level interop between different, mutually untrusting,\nmodules in a Wasm VM, potentially written in different languages; and\nwhen Wasm-level features such as exceptions are implemented and used\nwidely, seamless interop of those features as well. In essence, it is\nan enforced ABI. (My colleague Dan Gohman has some interesting\nthoughts about this in <a rel=\"external\" href=\"https:\/\/www.youtube.com\/watch?v=97jw9v2dRaw&amp;t=13m0s\">this talk, at 13 minutes\nin<\/a>.)<\/p>\n<p>This is great for language interop and for security, but it rules out\na number of interesting control-flow patterns that language runtimes\nuse to implement features such as exceptions, generators\/coroutines,\npluggable \"stubs\" of optimized code, patchable jump-points, and\ndynamic transfer between different optimization levels of the same\ncode (\"on-stack replacement\"). In other words, it conflicts with how\nlanguage runtimes want to implement <em>their<\/em> ABIs.<\/p>\n<h3 id=\"per-request-isolation-and-lack-of-warmup\">Per-Request Isolation and Lack of Warmup<\/h3>\n<p>Third, and specific to some of the Wasm-first platforms we care about,\nWasm's <a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/articles\/wasmtime-10-performance\"><em>fast\ninstantiation<\/em><\/a>\nallows for some very fine-grained sandboxing approaches. In\nparticular, when a new Wasm instance takes only a few microseconds to\nstart from a snapshot, it becomes feasible to treat instances as\ndisposable, and use them for small units of work. In the platform I\nwork on, we have an <em>instance-per-request isolation model<\/em>: each Wasm\ninstance serves only one HTTP request, and then goes away. This is\nfantastic for security and bug mitigation: the blast radius of an\nexploit or guest-runtime bug is only a single request, and can never\nsee the data from other users of the platform or even other requests\nby the same user.<\/p>\n<p>This fine-grained isolation is, again, great for security and\nrobustness, but throws a wrench into any language runtime's plan that\nbegins with \"observe the program for a while and...\". In other words,\nwe cannot implement a scheme for a dynamically-typed language that\nrequires us to specialize based on observation (of types, or dynamic\ndispatch targets, or object schemas, or the like) because by the time\nwe make those observations for one request, our instance is disposed\nand the next request starts \"fresh\" from the original snapshot.<\/p>\n<p>What are we to do?<\/p>\n<h2 id=\"making-js-fast-specialized-runtime-codegen\">Making JS Fast: Specialized Runtime Codegen<\/h2>\n<p>Let's first recap how JavaScript engines's JITs typically work -- at a\nhigh level, <em>how<\/em> they observe program behavior, and compile the JS\ninto machine code based on that behavior.<\/p>\n<h3 id=\"inline-caches\">Inline Caches<\/h3>\n<p>As I described in more detail in my <a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/\">earlier post about\nPBL<\/a>, the key technique that all\nmodern JS engines use to accelerate a dynamically-typed language is to\nobserve execution -- with particular care toward actual types (<code>var x<\/code>\nis always a string or an integer, for example), object shapes (<code>x<\/code>\nalways has properties <code>x.a<\/code>, <code>x.b<\/code> and <code>x.c<\/code> and we can store them in\nmemory in that order), or other specialized cases (<code>x.length<\/code> always\naccesses the length slot of an array object) -- and then generate\nspecialized code for the actually-observed cases. For example, if we\nobserve <code>x<\/code> is always an array, we can compile <code>x.length<\/code> to a single\nnative load instruction of the array's length in its object header; we\ndon't need to handle cases where <code>x<\/code> is some other type. This\nspecialized codegen is necessarily at runtime, hence the name for the\ntechnique, \"just-in-time (JIT) compilation\": we generate the\nspecialized code while the program is running, just before it is\nexecuted.<\/p>\n<p>One could imagine building some ad-hoc framework to collect a lot of\nobservations (\"type feedback\") and then direct the compiler\nappropriately, and in fact you can get pretty far building a JIT this\nway. However, SpiderMonkey uses a slightly more principled approach:\nit uses <em>inline caches<\/em> for all type-feedback and other runtime\nobservations, and encodes the observed cases in a specialized compiler\nIR, called <em>CacheIR<\/em>.<\/p>\n<p>The basic idea is: at every point in the program where there could be\nsome specialized behavior based on what we observe at runtime, we have\nan \"IC site\". This is a dynamic dispatch point: it invokes some\n<em>stub<\/em>, or sequence of code, that has been \"attached\" to the IC site,\nand we can attach new stubs as we execute the code. We always start\nwith a \"fallback stub\" that handles every case generically, but we can\nemit new stubs as we learn. There is a good example of IC-based\nspecialization in my <a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/#systematic-fast-paths-inline-caches\">earlier\npost<\/a>,\nsummarized with this figure:<\/p>\n<p><img src=\"\/assets\/2023-10-03-ics-web.svg\" alt=\"Figure: Inline-cache stubs in a JavaScript function\" \/><\/p>\n<h3 id=\"compilation-phasing-for-ics\">Compilation Phasing for ICs<\/h3>\n<p>So the question is now: how do we adapt a system that fundamentally\n<em>generates new code at runtime<\/em> -- in this case, in the form of IC\nstub sequences, which encode observed special cases (\"if conditions X\nand Y, do Z\") -- to a Wasm-based platform that restricts the program\nto its existing, static code?<\/p>\n<p>Let's ask a basic question: are IC bodies really always completely\nnovel, or do we see the same sequences over and over? Intuitively, one\nwould expect a few common sequences to dominate most programs. For\nexample, the inline cache sequence for a common object property access\nis a few CacheIR opcodes (simplifying a bit, but not too much, from\nthe <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/e51b2630b44a8836a7ff35a876a2d8b555041d4a\/js\/src\/jit\/CacheIR.cpp#4537\">actual\ncode<\/a>):<\/p>\n<ul>\n<li>Check that the receiver (<code>x<\/code> in<code>x.a<\/code>) is an object;<\/li>\n<li>Check that its \"shape\" (mapping from property names to slots) is the\nsame;<\/li>\n<li>Load or store the appropriate slot.<\/li>\n<\/ul>\n<p>One would expect nearly any real JavaScript program to use that IC\nbody at some point to set an object property. Can't we include it in\nthe engine build itself, avoiding the need to compile it at runtime?<\/p>\n<p>We could special-case \"built-in ICs\", but there's a more elegant\napproach: retain the uniform CacheIR representation (we <em>always<\/em>\ngenerate IR for cases we observe), but then look up the generated\nCacheIR bytecode sequence in a table of <em>included precompiled ICs<\/em>.<\/p>\n<p>How do we decide which ICs to include, and do we need to write them\nout manually? Well, not necessarily: it turns out that computers are\n<em>great<\/em> at automating boring tasks, and we could simply <em>collect all\nICs ever generated<\/em> during a bunch of JavaScript execution (say, the\nentire testsuite for SpiderMonkey) and build them in. Lookup tables\nare cheap, and ICs tend to be small in terms of compiled code size, so\nthere isn't much downside to including a few thousand ICs\n(<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/gecko-dev\/tree\/ff-127-0-2\/js\/src\/ics\/\">2367<\/a>\nby latest count).<\/p>\n<p>This is the approach I took with <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/gecko-dev\/pull\/45\">AOT\nICs<\/a>: I built\ninfrastructure to compile ICs into the engine, loading them into the\nlookup table (pre-existing to deduplicate compiled IC bodies) at\nstartup, and built an <em>enforcing mode<\/em> to allow for gathering the\ncorpus.<\/p>\n<p>The latter part -- collecting the corpus, and testing that the corpus\nis complete when running the whole testsuite -- is key, because it\nanswers the social\/software-engineering question of how to keep the\ncorpus up-to-date. The idea is to make updating it as easy as\npossible. When the testsuite runs <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/gecko-dev\/blob\/dc07fd26d11b4ccfbc82b35c4721f30696fe098d\/.github\/workflows\/main.yml#L15\">in\nCI<\/a>,\nwe test in \"AOT ICs\" mode that errors out if an IC is generated that\nis not already in the compiled-in corpus. However, this failure also\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/gecko-dev\/blob\/dc07fd26d11b4ccfbc82b35c4721f30696fe098d\/js\/src\/jit\/BaselineCacheIRCompiler.cpp#L2537-L2568\">dumps a new IC\nfile<\/a>\nand provides a message with instructions: move this file into\n<code>js\/src\/ics\/<\/code> and rebuild, and the observed-but-not-collected IC\nbecomes part of the corpus. The goal is to catch the case where a\ndeveloper adds a new IC path and make the corresponding corpus\naddition as painless as possible.<\/p>\n<p>Another point worth noting is that while there is no guarantee that\nthe engine won't generate novel IC bodies during execution of some\nprogram -- for example, during accesses to an object with a very long\nprototype chain that results in a long sequence of object-prototype\nchecks -- by integrating the corpus check with the testsuite\nexecution, there are <em>aligned incentives<\/em>. Anyone adding new\nIC-accelerated functionality should add a test case that covers it;\nand when we execute this testcase, we will check whether the corpus\nincludes the relevant IC(s) or not, too.<\/p>\n<h2 id=\"what-about-js-bytecode\">What About JS Bytecode?<\/h2>\n<p>We now have a corpus of IC bodies that cover all the relevant cases\nfor, say, the <code>+<\/code> operator in JavaScript, but we still haven't\naddressed the actual <em>compilation of JavaScript source<\/em>. Fortunately,\nthis is now the easiest part: we do a mostly 1-to-1 translation of JS\nsource to code that consists of a series of IC sites, invoking the\ncurrent IC-stub pointer for each operator instance in turn. The\ndataflow (connectivity between the operators) and control flow\n(conditionals and loops) become the compiled code \"skeleton\" around\nthese IC sites. So, for example, the JS function<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"javascript\"><span class=\"giallo-l\"><span class=\"z-storage\">function<\/span><span class=\"z-entity z-name z-function\"> f<\/span><span class=\"z-punctuation z-definition z-parameters\">(<\/span><span class=\"z-variable z-parameter\">x<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-parameter\"> y<\/span><span class=\"z-punctuation z-definition z-parameters z-punctuation\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  if<\/span><span class=\"z-variable z-other\"> (y<\/span><span class=\"z-keyword z-operator\"> &gt;=<\/span><span class=\"z-constant z-numeric\"> 0<\/span><span>)<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    return<\/span><span class=\"z-variable z-other\"> x<\/span><span class=\"z-keyword z-operator\"> +<\/span><span class=\"z-variable z-other\"> y<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">  }<\/span><span class=\"z-keyword\"> else<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    return<\/span><span class=\"z-variable z-other\"> x<\/span><span class=\"z-keyword z-operator\"> -<\/span><span class=\"z-variable z-other\"> y<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>might become something like the following pseudo-C code sketch:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"c\"><span class=\"giallo-l\"><span>Value <\/span><span class=\"z-entity z-name z-function\">f<\/span><span class=\"z-punctuation z-section\">(<\/span><span class=\"z-variable z-parameter\">Value x<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-parameter\"> Value y<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span>  Value t1 <\/span><span class=\"z-keyword z-operator\">=<\/span><span> ics<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">0<\/span><span class=\"z-punctuation z-punctuation z-section\">](<\/span><span>y<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-constant z-numeric\"> 0<\/span><span class=\"z-punctuation z-section\">)<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ `&gt;=` operator<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  bool<\/span><span> t2 <\/span><span class=\"z-keyword z-operator\">=<\/span><span> ics<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">1<\/span><span class=\"z-punctuation z-punctuation z-section\">](<\/span><span>t1<\/span><span class=\"z-punctuation z-section\">)<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">     \/\/ Value-to-bool coercion<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  if<\/span><span class=\"z-punctuation z-section\"> (<\/span><span>t2<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span>    Value t3 <\/span><span class=\"z-keyword z-operator\">=<\/span><span> ics<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">2<\/span><span class=\"z-punctuation z-punctuation z-section\">](<\/span><span>x<\/span><span class=\"z-punctuation\">,<\/span><span> y<\/span><span class=\"z-punctuation z-section\">)<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/\/ `+` operator<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    return<\/span><span> t3<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">  }<\/span><span class=\"z-keyword\"> else<\/span><span class=\"z-punctuation z-section\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span>    Value t4 <\/span><span class=\"z-keyword z-operator\">=<\/span><span> ics<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">3<\/span><span class=\"z-punctuation z-punctuation z-section\">](<\/span><span>x<\/span><span class=\"z-punctuation\">,<\/span><span> y<\/span><span class=\"z-punctuation z-section\">)<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/\/ `-` operator<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">    return<\/span><span> t4<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">}<\/span><\/span><\/code><\/pre>\n<p>Given this 1-to-1 compilation of all JS functions in the JS source,\nand a corpus of precompiled ICs, we have a full <em>ahead-of-time\ncompiler<\/em> for JavaScript.<\/p>\n<h2 id=\"ahead-of-time-aot-compilation-of-ic-based-js\">Ahead-of-Time (AOT) Compilation of IC-Based JS<\/h2>\n<p>It's worth highlighting this fact again: we have built an\n<em>ahead-of-time compiler<\/em> for JavaScript. How did this happen? Isn't\nruntime code generation necessary for efficient compilation of a\ndynamically-typed language?<\/p>\n<p>The key insight here is that the dynamism has been <em>pushed to runtime\nlate-binding<\/em> in form of the indirect calls to IC bodies. In other\nwords: behavior may vary widely depending on types; but rather than\nbuild all the options in statically, we have \"attachment points\" for\nseveral points, and we can dynamically insert certain behaviors by\n<em>setting a function pointer<\/em> rather than generating new code. We're\npatching together fragments of static code by updating dynamic data\ninstead.<\/p>\n<p>This execution model is known in SpiderMonkey as <em>baseline\ncompilation<\/em>. The main goal of the baseline compiler in SpiderMonkey\nis to generate code as quickly as possible, which is a somewhat\nseparate goal to generating code with no runtime observations about\nthat code; but these goals overlap somewhat, as both are relevant at\nlower tiers of the compiler hierarchy. In any case, a key fact about\nbaseline compilation is: it does not require any type feedback, and\ncan be done with <em>only<\/em> the JS source and nothing else. It admits\nfully AOT compilation.<\/p>\n<h3 id=\"going-further-bringing-in-runtime-feedback\">Going Further: Bringing In Runtime Feedback?<\/h3>\n<p>Baseline compilation works well enough, but this runtime binding has one\nkey limitation: it means that the compiler cannot optimize <em>combinations\nof ICs together<\/em>. For example, if we have a series of IC stubs for each\noperator in a function, we cannot \"blend\" these IC bodies into one large\nfunction body and allow optimizations to take hold; we cannot propagate\nknowledge across ICs. One <code>+<\/code> operator may produce an integer, and\nwithin the IC stub for that case, we can make use of the known result\ntype; but the next <code>+<\/code> operator to consume that value has to do its\ntype-checks over again. This is a fundamental fact of the \"late\nbinding\": we've pushed composition of behaviors to runtime via the\ndynamic function-pointer \"attachment\", and we don't have a compiler at\nruntime, so there is nothing to be done.<\/p>\n<p>If we want to go further, though, we can learn one more lesson from\nSpiderMonkey's design: baseline compilation with ICs is a solid\n<em>framework<\/em> for re-admitting this kind of whole-function optimization\nwith types. Specifically, SpiderMonkey's\n<a rel=\"external\" href=\"https:\/\/hacks.mozilla.org\/2020\/11\/warp-improved-js-performance-in-firefox-83\/\">WarpMonkey<\/a>\noptimizing compiler tier works by allowing baseline code to \"warm up\"\nits ICs, collecting the most relevant \/ most common special cases;\nthen it <em>inlines<\/em> those ICs at the call sites. And that's it!<\/p>\n<p>Said a different way: putting all type feedback into a \"call\nspecialized code stubs\" framework reduces type-feedback compilation to\nan inlining problem. Compiler engineers know how to build inliners;\nthat's far easier than an ad-hoc optimizing JIT tier.<\/p>\n<p>This leads to a natural way we could build a further-optimizing tier\non top of our initial AOT JS compiler: build a \"profile-guided\ninliner\". In fact, this has been done: my colleague <a rel=\"external\" href=\"https:\/\/github.com\/fitzgen\">Nick\nFitzgerald<\/a> built a prototype,\n<a rel=\"external\" href=\"https:\/\/github.com\/fitzgen\/winliner\">Winliner<\/a>, that profiles Wasm\nindirect calls and inlines the most frequent targets. The idea in the\ncontext of JS is to record the most frequent call target <code>T<\/code> at each\nIC dispatch site, then replace the indirect-call with <code>if target == T { \/* inlined code *\/ } else { call target }<\/code>.<\/p>\n<p>The beauty of this approach is that it is <em>semantics-preserving<\/em>: the\ntransform itself does not know anything about JavaScript engines, and\nthe resulting code will behave exactly the same, so if we have gotten\nour baseline-level AOT compiler to generate correct code, this\noptimizing tier will, as well, without new bugs. Winliner and\nprofile-guided inlining let us \"level-up\" from baseline compilation to\noptimized compilation in a way that is as close to \"for free\" as one\ncould hope for. The inlining tool and the baseline compiler can be\nseparately tested and verified; we can carefully reason about each\none, and convince ourselves they are correct; and we don't need to\nworry about bugs in the combination of the two pieces (where bugs most\noften lurk) because by taking the Wasm ISA as the interface, they\ncompose correctly.<\/p>\n<h2 id=\"results\">Results<\/h2>\n<p>That's all well and good; what are the results?<\/p>\n<p>The <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/gecko-dev\/pull\/48\">final PR<\/a>\nin which I introduced AOT compilation to our SpiderMonkey branch\nquotes these numbers on the\n<a rel=\"external\" href=\"https:\/\/chromium.github.io\/octane\/\">Octane<\/a> benchmark suite (numbers\nare rates, higher is better):<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>SpiderMonkey running inside a Wasm module:<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>- generic interpreter (in production today) vs.<\/span><\/span>\n<span class=\"giallo-l\"><span>- ahead-of-time compilation (this post)<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>               interpreter        AOT compilation     Speedup<\/span><\/span>\n<span class=\"giallo-l\"><span>Richards        166                 729               4.39x<\/span><\/span>\n<span class=\"giallo-l\"><span>DeltaBlue       169                 686               4.06x<\/span><\/span>\n<span class=\"giallo-l\"><span>Crypto          412                1255               3.05x<\/span><\/span>\n<span class=\"giallo-l\"><span>RayTrace        525                1315               2.50x<\/span><\/span>\n<span class=\"giallo-l\"><span>EarleyBoyer     728                2561               3.52x<\/span><\/span>\n<span class=\"giallo-l\"><span>RegExp          271                 461               1.70x<\/span><\/span>\n<span class=\"giallo-l\"><span>Splay          1262                3258               2.58x<\/span><\/span>\n<span class=\"giallo-l\"><span>NavierStokes    656                2255               3.44x<\/span><\/span>\n<span class=\"giallo-l\"><span>PdfJS          2182                5991               2.75x<\/span><\/span>\n<span class=\"giallo-l\"><span>Mandreel        166                 503               3.03x<\/span><\/span>\n<span class=\"giallo-l\"><span>Gameboy        1357                4659               3.43x<\/span><\/span>\n<span class=\"giallo-l\"><span>CodeLoad      19417               17488               0.90x<\/span><\/span>\n<span class=\"giallo-l\"><span>Box2D           927                3745               4.04x<\/span><\/span>\n<span class=\"giallo-l\"><span>----<\/span><\/span>\n<span class=\"giallo-l\"><span>Geomean         821                2273               2.77x<\/span><\/span><\/code><\/pre>\n<p>or about a 2.77x geomean improvement overall, with a maximum of 4.39x\non one benchmark and most benchmarks around 2.5x-3.5x. Not bad! For\ncomparison, the native baseline compiler in SpiderMonkey obtains\naround a 5x geomean -- so we have some room to grow still, but this is\na solid initial release. And of course, as noted above, by inlining\nICs then optimizing further, we should be able to incorporate\nprofile-guided feedback (as native JITs do) to obtain higher\nperformance in the future.<\/p>\n<h2 id=\"other-approaches\">Other Approaches?<\/h2>\n<p>It's worth noting at this point that a few other JS compilers exist\nthat attempt to do <em>specialized<\/em> codegen without runtime type\nobservations or other profiling\/warmup. Manuel Serrano's Hopc\ncompiler, described in his <a rel=\"external\" href=\"https:\/\/www-sop.inria.fr\/members\/Manuel.Serrano\/publi\/serrano-dls18.pdf\">DLS'18\npaper<\/a>,\nworks by building <em>one<\/em> specialized version of each function alongside\nthe generic version, and uses a set of type-inference rules to pick\nthe most <em>likely<\/em> types. This is shown to work quite well in the\npaper.<\/p>\n<p>The <a rel=\"external\" href=\"https:\/\/porffor.dev\/\">porffor<\/a> compiler similarly is a fully AOT\nJS compiler that appears to use some inference to emit the right cases\nonly when possible. That project is a work-in-progress, with support\nfor 39% of ECMA-262 currently, but seems promising.<\/p>\n<p>The main downside to an inference-based approach (as opposed to the\ndynamic indirection through ICs in our work) is the limit to how far\nthis inference can go: for example, SpiderMonkey's ICs can specialize\nproperty accesses on object shape, while Hopc does not infer object\nshapes and in general such an analysis is hard (similar to global\npointer analysis).<\/p>\n<p>I'd be remiss not to note a practical aspect too: there is a large\nbody of code embedding SpiderMonkey and written against its APIs; and\nthe engine supports all of the latest JS proposals and is actively\nmaintained; the cost of reaching parity with this level of support\nwith a from-scratch AOT compiler was one of the main reasons I opted\nto build on SpiderMonkey (adapting ICs to be AOT and compiling its\nbytecode) instead.<\/p>\n<h2 id=\"up-next-compiler-backends-for-free\">Up Next: Compiler Backends (For Free)<\/h2>\n<p>Astute readers will note that while I stated that we <em>do<\/em> a\ncompilation from the JS source to Wasm bytecode, and from IC bodies in\nthe corpus to Wasm bytecode, I haven't said <em>how<\/em> we do this\ncompilation. In my <a href=\"\/blog\/2023\/10\/11\/spidermonkey-pbl\/\">previous post<\/a>\non Portable Baseline, I described implementing an <em>interpreter<\/em> for IC\nbodies, and an interpreter for JS bytecode that can invoke these IC\nbodies. Developing two compiler backends for these two kinds of\nbytecode (CacheIR and JS) is quite the mountain to climb, and one\nnaturally wonders whether all the effort to design, build, and debug\nthe interpreters could be reused somehow.  If you do indeed wonder\nthis, then you'll love the upcoming <em>part 3<\/em> of this series, where I\ndescribe how we can derive these compiler backends <em>automatically<\/em>\nfrom the interpreters, reducing maintenance burden and complexity\nsignificantly. Stay tuned!<\/p>\n<hr \/>\n<p><em>Thanks to Luke Wagner, Nick Fitzgerald, and Max Bernstein for reading\nand providing feedback on a draft of this post!<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>On the Web, a Wasm module can \"trampoline\" out to JavaScript to\nload and instantiate another module with new code, sharing the\nsame heap, then invoke that new module via a shared\nfunction-reference table. This is technically workable but\nsomewhat slow and cumbersome, and this mechanism does not exist\non Wasm-only platforms. Note that the primary reason is the\ndesign <em>choice<\/em> to allow code-loading only via a control plane;\nnothing <em>technically<\/em> stops the platform from providing a direct\nWasm hostcall for the same purpose.<\/p>\n<\/div>\n"},{"title":"Path Generics in Rust: A Sketch Proposal for Simplicity and Generality","published":"2024-06-12T00:00:00+00:00","updated":"2024-06-12T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2024\/06\/12\/rust-path-generics\/"}},"id":"https:\/\/cfallin.org\/blog\/2024\/06\/12\/rust-path-generics\/","content":"<p>The <a rel=\"external\" href=\"https:\/\/www.rust-lang.org\/\">Rust<\/a> programming language is\nbest-known for its memory-related type system features that encode\n<a rel=\"external\" href=\"https:\/\/doc.rust-lang.org\/book\/ch04-00-understanding-ownership.html\">ownership and\nborrowing<\/a>:\nthese ensure memory safety (no dangling pointers), and also enforce a\nkind of \"mutual exclusion\" discipline that allows for provably safe\nparallelism. It's fantastic stuff; but it can also be utterly\nmaddening when one attempts to twist the borrow checker in a direction\nit doesn't want to go.<\/p>\n<p>In a recent <a rel=\"external\" href=\"https:\/\/smallcultfollowing.com\/babysteps\/blog\/2024\/06\/02\/the-borrow-checker-within\/\">blog\npost<\/a>,\nNiko Matsakis described an ambitious (but in my opinion very\nachievable) vision for perfecting the \"borrow checker within\": a\nseries of well-considered generalizations and features that make\nRust's lifetime and borrowing system more ergonomic and a better fit\nfor a wider variety of usage patterns.<\/p>\n<p>In this post, my goal is to tie an analogy from the \"place-based\nlifetimes\" in that post and an idea I proposed in 2020 called\n\"deferred borrows\" (<a href=\"\/pubs\/ecoop2020_defborrow.pdf\">paper<\/a><sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>). I'll\ndescribe how I see <em>path generics<\/em> (one might call them \"place\ngenerics\" in analogy with place-based lifetimes) as a new idea in Rust\nthat could allow place-based lifetimes, but also when generalized\nsufficiently, allow significant new expressivity in terms of <em>branded\ntypes<\/em> -- that is, one value in one place (say, a handle) irrevocably\ntied to another (say, a container). Hopefully all of this will become\nclear soon.<\/p>\n<p>Note also that this is very much <em>not<\/em> my day job: I work <em>in<\/em> Rust,\nnot <em>on<\/em> Rust -- so take these as the semi-amateur scribblings that\nthey are, for inspiration or discussion material at best; but I have\nno plans at the moment to try to push this further, beyond writing up\nthe ideas as they exist in my head, with the hope they might be\ninteresting.<\/p>\n<h2 id=\"recap-places-as-lifetimes\">Recap: Places as Lifetimes<\/h2>\n<p>In Rust today, memory is managed primarily by an \"ownership tree\"\nidiom built around RAII and linear types; some types -- structs and\nstandard-library containers -- can own other types in turn. Unique\nownership that is tied to program scopes with RAII is extremely useful\nbecause, by itself, it guarantees memory safety: we always know when\nto free memory, and there is no way to access it later because it is\nno longer in scope. This scheme also allows for race-free parallelism\nbecause subpieces of the program heap have completely distinct names\nand no aliasing.<\/p>\n<p>However, unique tree ownership is quite cumbersome on its own, hence\nborrows: temporary loaning of a subtree (by reference) as its own\nfirst-class value. To ensure this is safe, the compiler checks that we\ndon't hold the pointer for too long -- for example, longer than the\nlifetime of the original owning path. Rust reifies this concept at the\nsyntax level with a <em>lifetime<\/em> and allows naming explicit lifetimes at\nthe type level when describing borrows. Because ownership ultimately\ntraces back to RAII and local bindings (perhaps in <code>main()<\/code>, but\nsomewhere up the stack<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup>), these lifetimes correspond to static\nspans of code in some active stack frame.<\/p>\n<p>This is all very abstract; the language has precise definitions, of\ncourse, but the intuition can be difficult to internalize, especially\nwhen lifetimes arise as lifetime parameters. For example, the function\nsignature<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> f<\/span><span class=\"z-punctuation\">&lt;&#39;<\/span><span class=\"z-entity z-name z-type\">a<\/span><span class=\"z-punctuation\">, &#39;<\/span><span class=\"z-entity z-name z-type\">b<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-punctuation\"> &#39;<\/span><span class=\"z-entity z-name z-type\">a<\/span><span class=\"z-punctuation\">&gt;(<\/span><span class=\"z-keyword z-operator\">...<\/span><span class=\"z-punctuation\">) {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>defines a function that works in an abstract context where two\nlifetimes -- two sets of code spans -- exist either directly in the\ncaller or further up the stack, and for all times when <code>'a<\/code> is valid,\n<code>'b<\/code> is too (due to the \"outlives\" constraint introduced with\n<code>:<\/code>). This is a useful context to establish -- for example, it may let\none store a reference to something in <code>'b<\/code> in a field something in\n<code>'a<\/code> -- but it's <em>very hard<\/em> for one to grasp if one hasn't seen this\nconcept before.<\/p>\n<p>At the core of the intuitive difficulty is the fact that this is\nanother <em>kind<\/em> of program entity for the programmer to mentally\ntrack. In scope-based resource-management paradigms (such as C++ or\nRust RAII) one is already somewhat accustomed to thinking of an\nobject's lifetime, in some scope, conveying ownership. Lifetimes can\nfeel like some other semantic layer that is either redundantly\ndescribing that structure, or perhaps coarsening\/summarizing it into\n\"categories\" that are enough to prove safety to the borrow checker\nsomehow.<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup><\/p>\n<p>The idea conveyed in <a rel=\"external\" href=\"https:\/\/smallcultfollowing.com\/babysteps\/blog\/2024\/06\/02\/the-borrow-checker-within\/\">Niko Matsakis'\npost<\/a>\n-- noted as already existing under-the-hood in a <a rel=\"external\" href=\"https:\/\/github.com\/rust-lang\/polonius\">new borrow\nchecker<\/a>, but surfaced as\nsyntax and type-system semantics in a new proposal -- is a wonderful\nsimplification: name <em>storage places<\/em>, which are already roots of the\nownership tree, as lifetimes as well. In essence, a borrow temporarily\ntransfers ownership; so why not name the location the ownership came\nfrom, to allow checking compatibility of the lifetimes directly?<\/p>\n<p>The blog post gives a simple example where a lifetime parameter is\nneeded today, but use of a place-name instead is more intuitive:\nfunctions that return borrows to a piece of an argument. Rather than\nwriting<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> find_element<\/span><span class=\"z-punctuation\">&lt;&#39;<\/span><span class=\"z-entity z-name z-type\">a<\/span><span class=\"z-punctuation\">&gt;(<\/span><span class=\"z-keyword z-operator\">&amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">a<\/span><span class=\"z-variable z-language\"> self<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> key<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> K<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator\"> -&gt; &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">a V<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>where the <code>'a<\/code> exists only to tie <code>self<\/code> to the return value, we can\ninstead write<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> find_element<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator z-variable z-language\">&amp;self<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> key<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> K<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator\"> -&gt; &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">self V<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>with the same result. The post then expands further into\nself-referential types, where one element of a struct may borrow from\nanother:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  text<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> String<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  pieces<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Vec<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-keyword z-operator\">&amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">self<\/span><span class=\"z-keyword z-operator\">.<\/span><span>text <\/span><span class=\"z-entity z-name z-type\">str<\/span><span class=\"z-punctuation\">&gt;,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>which is an excellent improvement not just in ergonomics, as above,\nbut actual expressivity as well. A variant of this pattern can occur\nwhen <code>text<\/code> is a local binding and we build a local index into it:\nthen given a local <code>let text: String = ...;<\/code> we may later have a type\nsuch as <code>Vec&lt;&amp;'text str&gt;<\/code>.<\/p>\n<p>There is one remaining expressivity gap with the new kind of lifetime:\nit must have a binding in-scope to use it as a lifetime. In other\nwords, one can say<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> process_whole_and_part<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">whole<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-entity z-name z-type\">T<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> part<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">whole T<\/span><span class=\"z-punctuation\">) {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>but if <code>whole<\/code> is not a parameter in that signature, or equivalently\nnot a sibling field in a struct with borrow fields, one still needs a\ntraditional lifetime parameter. As far as I can tell, the proposal\ndoes not propose removing lifetime parameters entirely (nor would one\nwant to in a backwards-compatible evolution of the language); but\ncould we close the gap by introducing a little more syntax (thus in\nturn creating a simpler and more uniform semantic landscape)?<\/p>\n<h2 id=\"replacing-lifetimes-completely-path-parameters\">Replacing Lifetimes Completely: Path Parameters?<\/h2>\n<p>Here's the core of my proposal: allow <em>path parameters<\/em> alongside type\nparameters and lifetime parameters for any generic type. This\ngeneralizes the places-as-lifetimes proposal by allowing introductions\nof abstract place names, and would look something like:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-entity z-name z-type\"> P<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  borrow<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">P u32<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>... which looks almost exactly like a lifetime parameter. So what has\nchanged exactly?<\/p>\n<p>The key bit is that when we <em>use<\/em> this type elsewhere, it is tied to a\nspecific <em>path<\/em> (that is, a variable binding) rather than an abstract\nlifetime. This is an important difference: it binds two values, the\nborrow and the borrow-ee, together more tightly than a lifetime does.<sup class=\"footnote-reference\"><a href=\"#4\">4<\/a><\/sup><\/p>\n<p>For example, where we use <code>S<\/code>, we might write (with full type\nascriptions here for clarity):<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> foo<\/span><span class=\"z-punctuation\">() {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  let<\/span><span class=\"z-variable z-other\"> i<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> u32<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-constant z-numeric\"> 0<\/span><span class=\"z-punctuation\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  let<\/span><span class=\"z-variable z-other\"> s<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">i<\/span><span class=\"z-punctuation\">&gt;<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-variable z-other\"> borrow<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-variable z-other\">i<\/span><span class=\"z-punctuation\"> };<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>when the generic struct type is instantiated with the path parameter\n<code>i<\/code>, we get a borrow of type <code>&amp;'i u32<\/code>, just as we saw above. So far\nso good.<\/p>\n<p>We gain some nice clarity-of-intent when we use these path parameters\nin context structs and the like:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> Ctx<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-entity z-name z-type\"> Data<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  part1<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">Data T<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  part2<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">Data U<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> find_parts<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">data<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-entity z-name z-type\">u8<\/span><span class=\"z-punctuation\">])<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> Ctx<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">data<\/span><span class=\"z-punctuation\">&gt; {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>This is fairly nice for self-documentation purposes, and is perhaps\nmore intuitive than a separate concept of lifetimes, but in idiomatic\nRust today we sometimes already see descriptive lifetime names --\n<code>'ctx<\/code>, <code>'data<\/code>, <code>'input<\/code>, and the like. What actual new expressive\npowers -- powers to describe invariants to the compiler and get better\nsafety checks -- does this grant us?<\/p>\n<p>The main \"new power\" we obtain is a means of <em>branding<\/em>, as we alluded\nto above: <code>Ctx&lt;data&gt;<\/code> is truly tied to <code>data<\/code>, not anything with a\ncompatible lifetime. For memory safety this may not matter as such,\nbut the ability to tie a handle to a \"parent\" object is something that\nhas been discussed\n(<a rel=\"external\" href=\"https:\/\/internals.rust-lang.org\/t\/static-path-dependent-types-and-deferred-borrows\/14270\/20\">1<\/a>,\n<a rel=\"external\" href=\"https:\/\/www.reddit.com\/r\/rust\/comments\/kq6sz3\/design_pattern_for_compiletime_tying_of_handles\/\">2<\/a>)\nand seems generally useful as a matter of expressing API\ninvariants. Certainly whenever implementing an index-based (ECS-like)\nsystem in Rust -- such as for general graphs in a compiler IR -- it\nwould be nice to express <code>Id&lt;graph&gt;<\/code> (where <code>graph<\/code> is a specific\n<code>Graph<\/code> object) rather than just <code>Id<\/code>. Especially so when multiple\nindex spaces exist (instruction indices in two different function\nbodies or the like).<\/p>\n<p>So to recap: we've seen how taking paths rather than lifetimes in\ngeneral parameter lists can give us the same \"borrow to an external\nthing\" capability inside an aggregate type, and also lets us bind\n<em>which<\/em> external thing a little more tightly. It (kind of) reduces the\ninventory of cognitive concepts by one, as well -- we no longer have\nto think about lifetimes, just about local bindings. The genericity\nover a path makes it clear and explicit what Rust paths have always\nbeen -- the lifetime of some object held by some stack frame somewhere\nelse (up the call stack).<\/p>\n<p>Good so far! But some readers might now wonder: how does this\n<em>actually<\/em> work with respect to the borrow checker's tracking? The way\nthat traditional lifetime parameters \"hold a borrow open\" on an\nexternal source of data is a little subtle, and depends on the code\nthat constructs a type: for example,<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> make_struct<\/span><span class=\"z-punctuation\">&lt;&#39;<\/span><span class=\"z-entity z-name z-type\">a<\/span><span class=\"z-punctuation\">&gt;(<\/span><span class=\"z-variable z-other\">data<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">a<\/span><span class=\"z-punctuation\"> [<\/span><span class=\"z-entity z-name z-type\">u8<\/span><span class=\"z-punctuation\">])<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\">&lt;&#39;<\/span><span class=\"z-entity z-name z-type\">a<\/span><span class=\"z-punctuation\">&gt; {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>causes <code>data<\/code> to be borrowed for as long as <code>S<\/code> exists because of the\ncombination of two constraints: well-formedness of <code>S<\/code> means that any\nlifetime mentioned in <code>S<\/code> (here <code>'a<\/code>) outlives <code>S<\/code>, and then that\nlifetime <code>'a<\/code> is tied to a borrow of <code>data<\/code> that is initially created\nby the caller. So, in the caller's context, whatever local path we\nneeded to borrow to get <code>data<\/code> will be blocked (cannot be borrowed\nmutably, dropped, written to, or moved out of) while this <code>S&lt;'a&gt;<\/code>\nexists.<\/p>\n<p>How do we get equivalent behavior with path parameters? With the\nequivalent signature<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> make_struct<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">data<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-entity z-name z-type\">u8<\/span><span class=\"z-punctuation\">])<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">data<\/span><span class=\"z-punctuation\">&gt; {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>in order to enforce the same \"<code>data<\/code> borrowed as long as <code>S<\/code> is alive\"\nproperty that we had from the signature and well-formedness above, we\nneed some analogous well-formedness rules for path parameters.<\/p>\n<p>The simple way out would be to say that a path parameter always\nborrows the path it mentions. Then we can use it for lifetimes, and we\nhave the same semantics as above. But wait: what kind of borrow,\nimmutable or mutable? In the lifetime-based signature above, we knew\nthis because the caller created the borrow then explicitly saw that\nthe borrow's lifetime was captured by <code>S<\/code>; here <code>S<\/code> captures the\n<em>path<\/em> but isn't necessarily tied to the borrow on <code>data<\/code>. So we need\nsomething more explicit, somewhere, to denote the long-term borrow.<\/p>\n<p>Here's an even more interesting question: could it ever be meaningful\nand useful to mention, and bind to, a path, <em>without<\/em> borrowing it?<\/p>\n<h2 id=\"path-parameter-modes\">Path Parameter Modes<\/h2>\n<p>To resolve these questions, we add <em>borrow modes<\/em> to path parameters:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-keyword z-operator\"> &amp;<\/span><span class=\"z-entity z-name z-type\">Data<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  part1<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">Data<\/span><span class=\"z-punctuation\"> [<\/span><span class=\"z-entity z-name z-type\">u8<\/span><span class=\"z-punctuation\">],<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  part2<\/span><span class=\"z-keyword z-operator\">: &amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">Data<\/span><span class=\"z-punctuation\"> [<\/span><span class=\"z-entity z-name z-type\">u8<\/span><span class=\"z-punctuation\">],<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>and then add a rule about well-formed types: the borrow-mode of a path\nin the parameter list must subsume its mode in any use. For example,\nif used to denote the lifetime of an immutable borrow as above, we\nmust have <code>path &amp;Data<\/code>; it would be a compile-time error to have only\n<code>path Data<\/code>.<sup class=\"footnote-reference\"><a href=\"#5\">5<\/a><\/sup><\/p>\n<p>The meaning of this path parameter mode is exactly as if the <code>S<\/code> held\nthe corresponding borrow for its lifetime. Thus we recover the full\nsemantics of the struct with the traditional lifetime parameter above,\nand likewise we can encode the semantics of a mutable borrow.<\/p>\n<p>This also neatly handles the question of whether paths can overlap or\nmust be disjoint: e.g., for <code>struct S&lt;path P, path Q&gt;<\/code>, are <code>P<\/code> and\n<code>Q<\/code> separate bindings at the instantiation site, so we can have two\nfields <code>&amp;'P mut u32<\/code> and <code>&amp;'Q mut u32<\/code>? If we require path parameter\nmodes to subsume their uses, then we would need <code>struct S&lt;path &amp;mut P, path &amp;mut Q&gt;<\/code>, and then at the instantiation site the disjointness\nwould be enforced. Immutably-borrowed paths need not be disjoint (and\nconsequently we cannot assume that when typechecking within their\nscope).<\/p>\n<p>Now a new idea arises: if we can have immutably-borrowed and\nmutably-borrowed modes on path parameters, and these are explicit,\nwhat would it mean to have <em>no<\/em> borrow on a path?<\/p>\n<p>For example, one might declare<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-entity z-name z-type\"> Parent<\/span><span class=\"z-punctuation\">&gt; {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span><\/code><\/pre>\n<p>and then create<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> T<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">impl<\/span><span class=\"z-entity z-name z-type\"> T<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  fn<\/span><span class=\"z-entity z-name z-function\"> make_handle<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator z-variable z-language\">&amp;self<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-language\">self<\/span><span class=\"z-punctuation\">&gt; {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> foo<\/span><span class=\"z-punctuation\">() {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">   let<\/span><span class=\"z-variable z-other\"> a<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> T<\/span><span class=\"z-keyword z-operator\"> = ...<\/span><span class=\"z-punctuation\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">   let<\/span><span class=\"z-variable z-other\"> b<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> T<\/span><span class=\"z-keyword z-operator\"> = ...<\/span><span class=\"z-punctuation\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">   let<\/span><span class=\"z-variable z-other\"> handle_a<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">a<\/span><span class=\"z-punctuation\">&gt;<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-variable z-other\"> a<\/span><span class=\"z-keyword z-operator\">.<\/span><span class=\"z-entity z-name z-function\">make_handle<\/span><span class=\"z-punctuation\">();<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">   let<\/span><span class=\"z-variable z-other\"> handle_b<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">b<\/span><span class=\"z-punctuation\">&gt;<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-variable z-other\"> b<\/span><span class=\"z-keyword z-operator\">.<\/span><span class=\"z-entity z-name z-function\">make_handle<\/span><span class=\"z-punctuation\">();<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>and <code>handle_a<\/code> and <code>handle_b<\/code> are tied irrevocably to <code>a<\/code> and <code>b<\/code>. If\n<code>a<\/code> goes out of scope, then <code>handle_a<\/code>'s lifetime must have ended by\nthat point: that works the same as any lifetime constraint. However,\nextremely importantly, we <em>do not borrow <code>'a<\/code><\/em> while this handle\nexists. <code>a<\/code> could be passed as a <code>&amp;mut self<\/code> to various method calls,\nwe could do arbitrary things to it, and the handle type allows that;\nit is <em>only<\/em> tied to the continued <em>existence<\/em> of <code>a<\/code>, the binding.<sup class=\"footnote-reference\"><a href=\"#6\">6<\/a><\/sup><\/p>\n<p>What good is this, then? For one, we can define methods on the handle\nthat <em>must<\/em> take the original container or parent object. So we don't\nhold a borrow open for the duration of the handle, but we do take the\nborrow just for a particular access or mutation. This is analogous to\nthe ubiquitous \"index into <code>Vec<\/code> as reference\" pattern in Rust (in\nfact, keep reading!) but at the type level:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">impl<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-entity z-name z-type\"> Parent<\/span><span class=\"z-punctuation\">&gt;<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">Parent<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  fn<\/span><span class=\"z-entity z-name z-function\"> do_mutation<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator z-variable z-language\">&amp;self<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> parent<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-type\"> Parent<\/span><span class=\"z-keyword z-operator z-storage\">: &amp;mut<\/span><span class=\"z-entity z-name z-type\"> T<\/span><span class=\"z-punctuation\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>(Some might call this a \"singleton type\". Please feel free to bikeshed\nthat syntax!). The idea is that we take a mutable borrow to the path\nthat we irrevocably tied ourselves to -- but we only take that during\nthe method call.<\/p>\n<p>But if the typechecker will <em>only<\/em> let us pass that path to the\nmethod, why require it to be written at all? Hence the next idea,\n<em>implicit path-constrained parameters<\/em>:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">impl<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-entity z-name z-type\"> Parent<\/span><span class=\"z-punctuation\">&gt;<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">Parent<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  fn<\/span><span class=\"z-variable z-other\"> do_mutation<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-variable z-other\">parent<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-type\"> Parent<\/span><span class=\"z-keyword z-operator z-storage\">: &amp;mut<\/span><span class=\"z-entity z-name z-type\"> T<\/span><span class=\"z-punctuation\">](<\/span><span class=\"z-keyword z-operator z-variable z-language\">&amp;self<\/span><span class=\"z-punctuation\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">  }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>(Please <em>really<\/em> bikeshed that syntax: is a separate argument list\nreally the best?) This means that we call the method with a signature\nthat takes only <code>self<\/code> -- in some sense, its type really is (a subtype\nof) <code>Fn(Self)<\/code> -- but it has captured the <em>path<\/em> and will borrow it\nwhen called.<\/p>\n<p>Then we can do something like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> use_it<\/span><span class=\"z-punctuation\">() {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  let<\/span><span class=\"z-variable z-other\"> a<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> T<\/span><span class=\"z-keyword z-operator\"> = ...<\/span><span class=\"z-punctuation\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  let<\/span><span class=\"z-variable z-other\"> handle_1<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-variable z-other\"> a<\/span><span class=\"z-keyword z-operator\">.<\/span><span class=\"z-entity z-name z-function\">lookup_elt<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-constant z-numeric\">1<\/span><span class=\"z-punctuation\">);<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">  let<\/span><span class=\"z-variable z-other\"> handle_2<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-variable z-other\"> a<\/span><span class=\"z-keyword z-operator\">.<\/span><span class=\"z-entity z-name z-function\">lookup_elt<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-constant z-numeric\">2<\/span><span class=\"z-punctuation\">);<\/span><\/span>\n<span class=\"giallo-l\"><span>  <\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  handle_1<\/span><span class=\"z-keyword z-operator\">.<\/span><span class=\"z-entity z-name z-function\">do_mutation<\/span><span class=\"z-punctuation\">();<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ implicitly borrows `&amp;mut a`, just for the call<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  handle_2<\/span><span class=\"z-keyword z-operator\">.<\/span><span class=\"z-entity z-name z-function\">do_mutation<\/span><span class=\"z-punctuation\">();<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ implicitly borrows `&amp;mut a`, just for the call<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>Pretty convenient, no? (If you're worried about the dangling-iterator\nproblem -- what if a mutation invalidates a handle? -- read on.)<\/p>\n<p>There are a few bikeshedding points noted above, and possible\nextensions such as type constraints on paths (say that I want <code>path Parent<\/code> to be a <code>T<\/code> specifically) but overall I like this design and\nbelieve it has some potential beyond just ergonomics. Before I get to\nthat though -- under \"deferred borrows\" below -- one more detour, to\ntalk about mutable borrows and invalidation.<\/p>\n<h2 id=\"precise-invalidation-and-encoding-data-structure-properties\">Precise Invalidation and Encoding Data Structure Properties<\/h2>\n<p>Above, we created \"handles\" that are tied to a parent object; they\nhold no borrow or other form of reservation until use. We irrevocably\ntied the handles to the <em>binding<\/em> <code>a<\/code>, but what stops an intervening\ncall<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">let<\/span><span class=\"z-variable z-other\"> old<\/span><span class=\"z-keyword z-operator\"> =<\/span><span> std<\/span><span class=\"z-keyword z-operator\">::<\/span><span>mem<\/span><span class=\"z-keyword z-operator\">::<\/span><span class=\"z-entity z-name z-function\">replace<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator z-storage\">&amp;mut<\/span><span class=\"z-variable z-other\"> a<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-entity z-name z-function\"> new_container<\/span><span class=\"z-punctuation\">());<\/span><\/span><\/code><\/pre>\n<p>from replacing whatever internal state the handles referenced?<\/p>\n<p>Conventional, idiomatic Rust with borrow-based APIs avoids this\nscenario by holding the borrow on the container\/parent object for the\nentire lifetime of the handle. This essentially freezes the container\nin place so that whatever piece we're logically referencing with the\nhandle continues to exist. But by doing so, we give up all the\nconvenience (and expressive power!) of handles-without-borrows: it is\noften useful to collect \"fingers\" into a data structure, then perform\nmutations through them later. For example, we may have several\n<code>handle_a<\/code>s above (an array of them, or a hashmap, perhaps), and wish\nto write to a referenced element through some handle, and may not know\nwhich until we've collected several handles.<\/p>\n<p>But why is that desired usage pattern safe? The only reason is that we\nknow that our later mutations will mutate <em>elements<\/em>, but not the\nshape of the container itself. In essence, idiomatic Rust with\nhandles-that-hold-borrows blurs this distinction because that is the\nbest the type system can do. This is where the indices-as-references\npattern can save us again: if we use indices into a <code>Vec<\/code>, we don't\nactually borrow until we directly access an element. This works too,\nbut leads to other sorts of awkwardness. For example, in\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/\">regalloc2<\/a>, which\nmakes pervasive use of the index-as-reference pattern, I often have to\n\"re-derive\" a true borrow from an index because I perform some other\nmutation (to some other disjoint state that I know shouldn't matter)\nin the meantime.<\/p>\n<p>In principle we should be able to encode in the type system that some\nhandle relies on the \"shape\" of the container or parent object\nremaining the same, but nothing else. For this, we propose an idiom\nthat we call \"virtual fields\" -- encoded as zero-sized unit fields,\nperhaps with better syntax later -- that we can hold borrows on,\ncombined with the use of <a rel=\"external\" href=\"https:\/\/smallcultfollowing.com\/babysteps\/\/blog\/2021\/11\/05\/view-types\/\">view\ntypes<\/a>\n(also recapped in Niko's <a rel=\"external\" href=\"https:\/\/smallcultfollowing.com\/babysteps\/blog\/2024\/06\/02\/the-borrow-checker-within\/\">latest\npost<\/a>)\nin path parameter specs and\/or method bodies.<\/p>\n<p>(To be absolutely clear, the proposal in this subsection is an\n<em>idiom<\/em>, and depends only on the \"view types\" extension proposed in\nthose posts.)<\/p>\n<p>For example, to sketch what this might look like:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  pub<\/span><span class=\"z-variable z-other\"> shape<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-punctuation\"> (),<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ ^^ &quot;virtual field&quot;: a unit-typed field on which we use borrows via<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ view types to encode which methods modify which properties.<\/span><\/span>\n<span class=\"giallo-l\"><span>  <\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  contents<\/span><span class=\"z-keyword z-operator\">: ...<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\">&amp;<\/span><span class=\"z-variable z-other\">shape<\/span><span class=\"z-punctuation\">}<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ *really* needs some bikeshedding ^^<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment\">  \/\/<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ meaning is: irrevocably bind to a given container; hold an immutable<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ borrow on its `shape` field always; but don&#39;t otherwise hold a borrow<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ on it<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  elt<\/span><span class=\"z-keyword z-operator z-storage\">: *const<\/span><span> Element<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ internally unsafe pointer can be held because shape is constant<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">impl<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  fn<\/span><span class=\"z-entity z-name z-function\"> handle_mut<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator z-variable z-language\">&amp;self<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> idx<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> usize<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-language\">self<\/span><span class=\"z-punctuation\">&gt; {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ ^^ on return, `self.shape` is borrowed immutably, but nothing else is<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">impl<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-variable z-other\">path<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\">&amp;<\/span><span class=\"z-variable z-other\">shape<\/span><span class=\"z-punctuation\">}&gt;<\/span><span class=\"z-entity z-name z-type\"> Handle<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">Container<\/span><span class=\"z-punctuation\">&gt; {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  fn<\/span><span class=\"z-variable z-other\"> deref<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-variable z-other\">c<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\">&amp;^<\/span><span class=\"z-variable z-other\">shape<\/span><span class=\"z-punctuation\">}<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-punctuation\">](<\/span><span class=\"z-keyword z-operator z-variable z-language\">&amp;self<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator\"> -&gt; &amp;<\/span><span class=\"z-entity z-name z-type\">Element<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ ^^ when *this* returns, we actually borrow Container, but that borrow only<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ lasts as long as we hold the true borrow; the handle is &quot;just as good&quot;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ (except that it doesn&#39;t freeze the actual element contents)<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment\">  \/\/<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ The `&amp;^shape` syntax means &quot;immutably borrow everything in `Container`<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ except the `shape` field&quot;; this signature is promising that we don&#39;t<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ mutate the container&#39;s shape.<\/span><\/span>\n<span class=\"giallo-l\"><span>  <\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">  fn<\/span><span class=\"z-variable z-other\"> deref_mut<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-variable z-other\">c<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\">&amp;^<\/span><span class=\"z-variable z-other\">shape<\/span><span class=\"z-punctuation\">}<\/span><span class=\"z-entity z-name z-type\"> Container<\/span><span class=\"z-punctuation\">](<\/span><span class=\"z-keyword z-operator z-variable z-language\">&amp;self<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator z-storage\"> -&gt; &amp;mut<\/span><span class=\"z-entity z-name z-type\"> Element<\/span><span class=\"z-punctuation\"> {<\/span><span class=\"z-keyword z-operator\"> ...<\/span><span class=\"z-punctuation\"> }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>Note that this is quickly encroaching on territory well-trodden by\n<code>Pin<\/code> and pin-projection and self-referential types; this post is\ncertainly not the first to ever meditate on the difference between\n\"immutable shape\" and deep immutability in Rust. Interesting\nhypothesis: perhaps someday <code>Pin<\/code> could be encoded with careful use of\nview-types and virtual fields denoting properties. Even more\ninteresting hypothesis: containers with invariants not currently\ndescribed in the type system today -- such as the fact that moving a\n<code>String<\/code> does not move the pointed-to content, likewise for a <code>Vec<\/code> --\ncould loosen their signatures and encode this as well (with careful\nthought toward backward-compatibility). This might be a way to encode\nthe \"self-referential struct\" example<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-storage\">struct<\/span><span class=\"z-entity z-name z-type\"> S<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  data<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> String<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">  parts<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Vec<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-keyword z-operator\">&amp;<\/span><span class=\"z-punctuation\">&#39;<\/span><span class=\"z-entity z-name z-type\">self<\/span><span class=\"z-keyword z-operator\">.<\/span><span>data <\/span><span class=\"z-entity z-name z-type\">str<\/span><span class=\"z-punctuation\">&gt;,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>where the <code>parts<\/code> hold <code>data<\/code>'s contents borrowed, but not <code>data<\/code>\nitself, so the <code>S<\/code> is still movable.<\/p>\n<p>I'll readily admit I've gotten increasingly handwavy as this section\ncontinues; but I believe there is <em>something<\/em> here and with enough\ncareful specification it could form a cohesive, backward-compatible\nlanguage overlay that adds expressive power to Rust APIs.<\/p>\n<h2 id=\"new-pattern-deferred-borrows\">New Pattern: Deferred Borrows<\/h2>\n<p>Finally we've reached (one) end application of all this language\nmachinery: implementing container types with <em>deferred borrows<\/em>, as I\nhad proposed in my <a href=\"\/pubs\/ecoop2020_defborrow.pdf\">2020 paper<\/a>.<\/p>\n<p>The key idea is to define a kind of auto-deref trait, like\n<a rel=\"external\" href=\"https:\/\/doc.rust-lang.org\/std\/ops\/trait.Deref.html\"><code>Deref<\/code><\/a>, but\nwith a captured path and implicit parameter such that it only\n(necessarily) borrows that implicit \"context\" when converting to a\ntrue borrow. Then we could implement this trait for handles and have\nthem work as any other smart-pointer type today (such as <code>Rc<\/code> or\n<code>Box<\/code>) except that they would not unnecessarily hold open a borrow on\nthe parent container; only its \"shape\" or whatever invariants are\nneeded to keep elements alive.<\/p>\n<p>In the paper I proposed several variants of each container and a kind\nof API-level typestate to encode what handles do hold borrows on,\nhence how efficient they can be. For example, a <code>Vec<\/code> might become a\n<code>FrozenVec<\/code>, where the length is fixed; then handles can be true\npointers, and the deferred borrows are as efficient as real\nborrows. Or it could be an <code>AppendOnlyVec<\/code>, with indices, incurring an\nindirection through the storage base pointer (which may change as\ngrowth causes reallocations) on each conversion to a true borrow but\n<em>not<\/em> a dynamic bounds-check. With the above \"virtual fields\" idea and\nview types, I think we could get away without the typestate-like API,\ninstead handing out different kind of handles (<code>PtrHandle<\/code> vs\n<code>IndexHandle<\/code> vs ...) directly. Perhaps other variants could exist as\nwell; read the paper for more. (My intent here is not to summarize the\nentire paper, but rather to show that its proposals become possible\nonce one has handles tied to paths and able to implicitly borrow\nthem.)<\/p>\n<h2 id=\"related-approaches\">Related Approaches<\/h2>\n<p>One might reasonably ask: is there a way to \"trick\" the lifetime\nsystem into giving us branded types today, where one value is\nirrevocably tied to another?<\/p>\n<p>Inherently what one needs is <em>generativity<\/em>, a kind of type-system\nfeature where each execution or instance or an operation yields a\ndifferent type.<sup class=\"footnote-reference\"><a href=\"#7\">7<\/a><\/sup><\/p>\n<p>I am aware of at least one instance of a \"generativity trick\" with\nRust lifetimes, described by <a rel=\"external\" href=\"https:\/\/faultlore.com\/blah\/\">Gankra<\/a>'s\n<a rel=\"external\" href=\"https:\/\/raw.githubusercontent.com\/Gankra\/thesis\/master\/thesis.pdf\">thesis<\/a>\nin section 6.3, where closures and forced-invariance of lifetimes are\nused to create separate lifetimes for separate arrays (in the example)\nsuch that the handle type (indices in the example) can be uniquely\nassociated with only one array. This is undoubtedly an extremely\nimpressive hack; nevertheless, better would be for the language to\nprovide a first-class, readable way for the user to write their intent\n(<code>Handle&lt;myvec&gt;<\/code>) without workaround.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>This blog post has outlined an idea for \"path generics\", where path\nparameters live alongside (and perhaps someday mostly replace?)\nlifetime parameters as a more intuitive means of describing the\nlifetime origin of some borrowed data. Along with that, we've seen how\nthey add expressive power in \"branding\" one type to be tied\nirrevocably to some parent value (path), allowing for typesafe \"handle\npatterns\", and how a bit of configurability in their interaction with\nborrow checking can lead to interesting and useful new APIs that were\nstrictly inexpressible before.<\/p>\n<p>What do I hope to come of all this? As I noted in a footnote<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>\nabove, in hindsight I've tried to push ideas in the \"deferred borrows\nfamily\" in sort of halfhearted ways before; mainly, I have a day-job\nthat is \"in Rust\"<sup class=\"footnote-reference\"><a href=\"#8\">8<\/a><\/sup> but is not \"the Rust language\" and I don't feel\nI have the energy to push forward a full language proposal on the side\nor really go much beyond this post. (See above also re: me not being a\nproperly trained PL person.) But I <em>do<\/em> think there may be something\nhere, to the extent I want to braindump a bit and see what folks\nthink, especially now that there is some explicit thinking and\ndiscussion about \"places as lifetimes\", view types, and other\nborrow-system extensions.<\/p>\n<p>So: maybe someone else will also see some value here (and see past the\nundoubtedly rough bikesheddable surface design details) and explore\nfurther. Maybe not. Either way the ideas are written down and out\nthere. Feedback welcome!<\/p>\n<hr \/>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>In hindsight, this paper was very much not the right way to get\nthe idea out. I published this paper just after entering\nindustry from academia, still not quite sure what I wanted to\ndo; I didn't feel I had the energy to engage with Rust and\nactually drive an idea forward, but I did want to write it up\n<em>somewhere<\/em>, so I attempted that rare beast, a single-author\npaper written in one's free time (with all the corresponding\nlimits on completeness). This blog post is, in one way of seeing\nit, another attempt at \"putting it out there\": I almost\ncertainly don't have the bandwidth to spec or implement this but\nI'm curious what folks think and it feels more relevant now that\nit is an \"incremental generalization\" on top of another idea\njust proposed.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>For pedagogical reasons let's ignore global variables and\n<code>'static<\/code> as well for now. The former is unsafe for a reason,\nand the latter is the \"boring pathological case\" where data is\nvalid because it literally lives forever. In principle any\ninteresting lifetime in Rust is tied to a span of code executing\nin some stack frame at some level.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>In day-to-day use of Rust, explicit lifetimes are fairly rare\noutside of writing custom containers and perhaps \"context\nstructs\" that bundle a set of borrows; but they <em>do<\/em> occur, and\nsometimes efficiency (non-copying) concerns and layering force\nnested lifetimes as well. The worst I've personally had to write\nwas a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/af59c4d568d487b7efbb49d7d31a861e7c3933a6\/cranelift\/codegen\/src\/opts.rs#L76-L79\">three-level\nnesting<\/a>\nnecessary to encode a long-lived data structure, an analysis\ncontext over it, and an iteration context within that analysis\ncontext. It would have been great to have some better\nabstraction here! Even I was a bit frustrated, and I am solidly\non Team \"Borrow Checker Saves Me Time and Improves My Code In\nThe Long Run\"!<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"4\"><sup class=\"footnote-definition-label\">4<\/sup>\n<p>Path variables used as lifetimes are semantically <em>different<\/em>,\nand in the way described more powerful, but they also do not\nfully subsume separate lifetime variables, as far as I can tell:\none cannot have a path that is the \"meet\" of two different\npaths, e.g. a borrow in a struct that comes from either <code>P<\/code> <em>or<\/em>\n<code>Q<\/code> (both of which live long enough). For this reason one may\nstill want to use conventional lifetimes, or perhaps path\nparameters could be extended with a notion of \"union paths\"\n(TBD!).<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"5\"><sup class=\"footnote-definition-label\">5<\/sup>\n<p>There are likely some more details in exactly how path\nparameters interact with both well-formedness and subtyping that\nI haven't worked out, mostly because I am not a type theorist (I\nonly play one in blog posts, and then only for the fun\nparts). For example, I suspect one would want a kind of \"plus\nconstraint\" analogous to <code>T + 'a<\/code> for paths too, to say\nsomething like \"<code>T<\/code> and it's allowed to borrow <code>P<\/code>\".<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"6\"><sup class=\"footnote-definition-label\">6<\/sup>\n<p>One might worry that this is insufficient for a really robust\nhandle type: what if I take a mutable borrow of <code>a<\/code> and do a\n<code>std::mem::replace<\/code> on it? This proposal's answer is that it is\nthe responsibility of the API author to define signatures such\nthat this is not possible; but with \"virtual fields\" (keep\nreading) there is a way to enfoce this.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"7\"><sup class=\"footnote-definition-label\">7<\/sup>\n<p>Note this is a bit subtle: we could mean each operation at a\ndifferent static program point -- this is the kind of\ngenerativity that, say, <a rel=\"external\" href=\"https:\/\/ocaml.org\/manual\/5.2\/generativefunctors.html\">OCaml generative functor\napplication<\/a>\nprovides -- or we could mean that each <em>dynamic<\/em> instance of,\nsay, an object allocation produces a conceptually new type. We\nwant something like the latter for branding: it shouldn't be\npossible to allocate a list of objects in a loop, put them all\nin a vector of same-typed values and mix up their handles.)<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"8\"><sup class=\"footnote-definition-label\">8<\/sup>\n<p>Cranelift, Wasmtime, and other compilers and runtimes stuff, so\nvery intensively \"in Rust\", but definitely the language itself\nis out of scope!<\/p>\n<\/div>\n"},{"title":"Fast(er) JavaScript on WebAssembly: Portable Baseline Interpreter and Future Plans","published":"2023-10-11T00:00:00+00:00","updated":"2023-10-11T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2023\/10\/11\/spidermonkey-pbl\/"}},"id":"https:\/\/cfallin.org\/blog\/2023\/10\/11\/spidermonkey-pbl\/","content":"<p>For the past year, I have been hard at work trying to improve the\nperformance of the <a rel=\"external\" href=\"https:\/\/spidermonkey.dev\/\">SpiderMonkey<\/a>\nJavaScript engine when compiled as a\n<a rel=\"external\" href=\"https:\/\/webassembly.org\/\">WebAssembly<\/a> module. For server-side\napplications that use WebAssembly (and <a rel=\"external\" href=\"https:\/\/wasi.dev\/\">WASI<\/a>, its\n\"system\" layer) as a software distribution and sandboxing technology\nwith <a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/articles\/webassembly-the-updated-roadmap-for-developers\">significant exciting\npotential<\/a>,\nthis is an important enabling technology: it allows existing software\nwritten in JavaScript to be run within the sandboxed environment and\nto interact with other Wasm modules.<\/p>\n<p>Running an entire JavaScript engine <em>inside<\/em> of a Wasm module may seem\nlike a strange approach at first, but it serves real use-cases. There\nare platforms that accept WebAssembly-sandboxed code for security\nreasons, as it ensures complete memory isolation between requests\nwhile remaining very fine-grained (hence with lower overheads). In\nsuch an environment, JavaScript code needs to bring its own engine,\nbecause no platform-native JS engine is provided. This approach ensures\na sandbox <em>without trusting the JavaScript engine's security<\/em> --\nbecause the JS engine is just another application on the hardened Wasm\nplatform -- and carries other benefits too: for example, the JS code\ncan interact with other languages that compile to Wasm easily, and we\ncan leverage Wasm's determinism and modularity to snapshot execution\nand then perform extremely fast cold startup. We have been using this\nstrategy to great success for a while now: we did the initial port of\nSpiderMonkey to WASI in 2020, and we wrote <a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/articles\/making-javascript-run-fast-on-webassembly\">two years\nago<\/a>\nabout how we can leverage Wasm's clean modularity and determinism to\nuse snapshots for fast startup.<\/p>\n<p>This post is an update to that effort. At the end of that prior post,\nwe hinted at efforts to bring more of the JavaScript performance\nengineering magic that browsers have done to the JS-in-Wasm\nenvironment. Today we'll see how we've successfully adapted <em>inline\ncaches<\/em>, achieving significant speedups (~2x in some cases) without\ncompromising the security of the interpreter-based strategy. At the\nend of this post, I'll hint at how we plan to use this as a foundation\nfor ahead-of-time compilation as well. Exciting times ahead!<\/p>\n<p><em>Note: the <a rel=\"external\" href=\"https:\/\/docs.google.com\/document\/d\/1XZZnc5xxfOVnemxRrTbZqvCg0PH4B9GyGgXnBhULROA\">design\ndocument<\/a>\nis also available, and SpiderMonkey patches can be found on the\n<a rel=\"external\" href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1855321\">upstreaming\nbug<\/a>.<\/em><\/p>\n<h2 id=\"current-state-interpreters-only-beyond-this-point\">Current State: Interpreters Only Beyond This Point<\/h2>\n<p>A distinguishing feature of some platforms is that they <em>do not allow\nruntime code generation<\/em>. For example, a WebAssembly module may\ncontain functions; these functions are compiled at some point before\nthe functions are run; but the functions cannot do anything to create\n<em>new<\/em> functions in the same code space, at least without going through\nsome lower-level and nonstandard system interface.<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup><\/p>\n<p>On the other hand, JavaScript engines over the past several decades\nhave <em>embraced<\/em> runtime code generation. The basic reason for this is\nthat there are a lot of facts about a JS program that one cannot know\n(or not easily, without human intelligence and reasoning and a view of\nthe whole program) until the program executes. For example: in the\nsimple one-line function <code>function(x) { return x + x; }<\/code>, what is\n<code>x<\/code>'s type? It could be an integer, a floating-point number, a string,\nor an object that can be converted to a string, or probably many other\nthings.<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup> If one were to try to generate machine code for that\nfunction ahead-of-time, it would look very different than that of,\nsay, a C function with the same <code>return x + x;<\/code> body but an\ninteger-typed <code>x<\/code>. It would have to contain type-checks, branches, and\nimplementations for all the different possible cases. With that\ndynamic dispatch overhead, and the bloated and difficult-to-optimize\nfunction body that supports all combinations of types, we are unlikely\nto see much speedup <em>unless<\/em> we can know something about the types. A\nsimilar problem arises in other \"dynamic\" aspects of the language:\nwhen we say <code>obj.x<\/code>, where in the memory for the object <code>obj<\/code> is the\nfield <code>x<\/code>? When we call a function <code>f(1, 2, 3)<\/code>, is <code>f<\/code> another\nJavaScript function, a native function in the runtime, or something\neven more special that we handle in a different way?<\/p>\n<p>The modern JavaScript engine's answer to performance, then, is to\ncollect a lot of information as a program executes and then\ndynamically generate machine-code that <em>is<\/em> specialized to what the\nprogram is actually doing, but can fall back to the generic\nimplementation if conditions change. Because we can't generate this\ncode until the program is already running, we need the platform to\nsupport the ability to add more executable code as we are running:\nthis is \"runtime codegen\", or as it is often known, \"JIT\n(just-in-time) compilation\".<\/p>\n<p>So we have a conundrum: we run JavaScript inside a Wasm module, we\nwant performance, but the usual way to get that performance is to\nJIT-compile specialized machine-code versions of the JS code after\nobserving it, and we can't do that from within a Wasm module. What are\nwe to do?<\/p>\n<h2 id=\"systematic-fast-paths-inline-caches\">Systematic Fast-Paths: Inline Caches<\/h2>\n<p>It's worth understanding <em>how<\/em> JITs specialize the execution of the\nprogram based on runtime observations. A simple approach might be to\nbuild in \"ad-hoc\" kinds of observations to the interpreter, and use\nthose as needed in a type-specific \"specializing\" compiler. For\nexample, we could record types seen at a <code>+<\/code> operator at a given point\nin the program, and then generate only the cases we've observed when\nwe compile that operator (perhaps with \"fallback code\" to call into a\ngeneric implementation if our assumption becomes wrong). However, this\nad-hoc approach does not scale well: every semantic piece (operators,\nfunction calls, object accesses) of the language implementation would\nhave to become a profiler, an analysis pass, and a profile-guided\ncompiler.<\/p>\n<p>Instead, JITs specialize with a general <em>dispatch mechanism<\/em> known as\n\"inline caches\" (ICs), and ICs build straight-line sequences of\n\"fast-paths\" in a uniform <em>program representation<\/em>.<\/p>\n<p>The usual approach is to define certain points in the original program\nat which we have an operator with widely-varying behavior, and place\nan inline cache site in the compiled code. The idea of an inline cache\nsite is that it performs an indirect dispatch to some other \"stub\":\nthese are \"pluggable implementations\" that replace a generic operator,\nlike <code>+<\/code>, with some specific case, like \"if both inputs are\n<code>int32<\/code>-typed, do an integer add\". For example, we might compile the\nfollowing function body into the polymorphic (type-generic) code on\nthe left, then generate the specialized fast-paths and attach them as\nstubs on the right:<\/p>\n<p><img src=\"\/assets\/2023-10-03-ics-web.svg\" alt=\"Figure: Inline-cache stubs in a JavaScript function\" \/><\/p>\n<p>The IC site starts with a link to a generic implementation -- just\nlike the naive interpreter -- that works for any case. However, after\nit executes, it also <em>generates a fast-path for that case<\/em> and\n\"attaches\" the new stub to the IC site. The stubs form a \"chain\", or\nsingly-linked list, with the generic case at the end. Some examples of\nfast-paths that we see in practice are:<\/p>\n<ul>\n<li>\n<p>For an access to a property on an object, we can generate a\nfast-path that checks whether the object is a known \"shape\" --\ndefined as the set of existing properties and their layout in memory\n-- and directly accessing the appropriate memory offset on a\nmatch. This avoids an expensive lookup by property name.<\/p>\n<\/li>\n<li>\n<p>For any of the polymorphic built-in operators, like <code>+<\/code>, we can\ngenerate a fast-path that checks types and does the appropriate\nprimitive action (integer addition, floating-point addition, or\nstring concatenation, say).<\/p>\n<\/li>\n<li>\n<p>For a call to a built-in or \"well-known\" function, we can generate a\nfast-path that avoids a function call altogether. For example, if\nthe user calls <code>String.length<\/code>, and this has not been overridden\nglobally (we need to check!) and the input is a string, then the IC\ncan load the string length directly from the known length-field\nlocation in the object. This replaces a call into the JS runtime's\nnative string-implementation code with just a few IC instructions.<\/p>\n<\/li>\n<\/ul>\n<p>Each stub has a simple format: it checks some conditions, then either\ndoes its action (if it is a matching fast-path) or jumps to the next\nstub in the chain.<\/p>\n<p>This collection of stubs, once \"warmed up\" by program execution, is\nuseful in at least two ways. First, it represents a knowledge-base of\nthe program's actual behavior. The execution has been \"tuned\" to have\nfast-paths inserted for cases that are actually observed, and will\nbecome faster as a result. That is quite powerful indeed!<\/p>\n<p>Second, an even more interesting opportunity arises (first introduced\nin the\n<a rel=\"external\" href=\"https:\/\/hacks.mozilla.org\/2020\/11\/warp-improved-js-performance-in-firefox-83\/\">WarpMonkey<\/a>\neffort from the SpiderMonkey team, to their knowledge a <a rel=\"external\" href=\"https:\/\/2023.splashcon.org\/details\/mplr-2023-papers\/4\/CacheIR-The-Benefits-of-a-Structured-Representation-for-Inline-Caches\">novel\ncontribution<\/a>):\nonce we have the IC chains, we can use the combination of two parts --\nthe original program bytecode, and the pluggable stub fast-paths -- to\ncompile fully specialized code by <em>translating both to one IR and\ninlining<\/em>. This is how we achieve specialized-variant compilation in a\nsystematic way: we just write out the necessary fast-paths as we need\nthem, and then we later incorporate them.<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup> The following figure\nillustrates this process:<\/p>\n<p><img src=\"\/assets\/2023-10-03-ic-inlining-web.svg\" alt=\"Figure: optimized JS compilation by inlining ICs\" \/><\/p>\n<p>In the SpiderMonkey engine, there are three JIT tiers that make use of ICs:<\/p>\n<ul>\n<li>\n<p>The \"<a rel=\"external\" href=\"https:\/\/hacks.mozilla.org\/2019\/08\/the-baseline-interpreter-a-faster-js-interpreter-in-firefox-70\/\">baseline\ninterpreter<\/a>\"\ninterprets the JS function body's opcodes, but accelerates\nindividual operations with ICs. The interpreter-based approach means\nwe have fast startup (because we don't need to compile the function\nbody), while ICs give significant speedups on many operations.<\/p>\n<\/li>\n<li>\n<p>The \"baseline compiler\" translates the JS function body into machine\ncode on a 1-for-1 basis (each JS opcode becomes a small snippet of\nmachine code), and dispatches to the same ICs that the baseline\ninterpreter does. The main speedup over the baseline interpreter is\nthat we no longer have the \"JS opcode dispatch overhead\" (the cost\nof fetching JS opcodes and jumping to the right interpreter case),\nthough we do still have the \"IC dispatch overhead\" (the cost of\njumping to the right fast-path).<\/p>\n<\/li>\n<li>\n<p>The optimizing compiler, known as\n<a rel=\"external\" href=\"https:\/\/hacks.mozilla.org\/2020\/11\/warp-improved-js-performance-in-firefox-83\/\">WarpMonkey<\/a>,\ninlines ICs and JS bytecode to perform specialized compilation.<\/p>\n<\/li>\n<\/ul>\n<p>We can summarize the advantages and tradeoffs of these tiers as\nfollows:<\/p>\n<table><thead><tr><th>Name<\/th><th>Data Required<\/th><th>JS opcode dispatch<\/th><th>ICs<\/th><th>Optimization scope<\/th><th>CacheIR dispatch<\/th><th>Codegen at runtime<\/th><\/tr><\/thead><tbody>\n<tr><td>Generic (C++) interpreter<\/td><td>JS bytecode<\/td><td>Interpreter (C++)<\/td><td>None<\/td><td>None<\/td><td>N\/A<\/td><td>No<\/td><\/tr>\n<tr><td>----------------------------------<\/td><td>-----------------------------------<\/td><td>--------------------------------------------<\/td><td>------------------<\/td><td>----------------------------------------<\/td><td>------------------<\/td><td>----------------------------------<\/td><\/tr>\n<tr><td>Baseline interpreter<\/td><td>JS bytecode + IC data structures<\/td><td>Interpreter (generated at startup)<\/td><td>Dynamic dispatch<\/td><td>Special cases within one opcode via IC<\/td><td>Compiled<\/td><td>Yes (IC bodies, interp body)<\/td><\/tr>\n<tr><td>----------------------------------<\/td><td>-----------------------------------<\/td><td>--------------------------------------------<\/td><td>------------------<\/td><td>----------------------------------------<\/td><td>------------------<\/td><td>----------------------------------<\/td><\/tr>\n<tr><td>Baseline compiler<\/td><td>JS bytecode + IC data structures<\/td><td>Compiled function body (1:1 with bytecode)<\/td><td>Dynamic dispatch<\/td><td>Special cases within one opcode via IC<\/td><td>Compiled<\/td><td>Yes (IC bodies, function bodies)<\/td><\/tr>\n<tr><td>----------------------------------<\/td><td>-----------------------------------<\/td><td>--------------------------------------------<\/td><td>------------------<\/td><td>----------------------------------------<\/td><td>------------------<\/td><td>----------------------------------<\/td><\/tr>\n<tr><td>Optimizing compiler (WarpMonkey)<\/td><td>JS bytecode + warmed-up IC chains<\/td><td>Compiled function body (optimized)<\/td><td>Inlined<\/td><td>Across opcodes \/ whole function<\/td><td>Compiled<\/td><td>Yes (optimized function body)<\/td><\/tr>\n<\/tbody><\/table>\n<h2 id=\"can-we-use-ics-in-a-wasi-program\">Can we Use ICs in a WASI Program?<\/h2>\n<p>Given that we have a means to speed up execution beyond that of a\ngeneric interpreter, namely, inline caches (ICs), and given that\nSpiderMonkey supports ICs, surely we can simply make use of this\nfeature in a build of SpiderMonkey for WASI (i.e., when running inside\nof a Wasm module)?<\/p>\n<p>Not so fast! There are two basic problems:<\/p>\n<ul>\n<li>\n<p>As designed, the IC stubs can only be run as <em>compiled code<\/em>. Even\nthe \"baseline interpreter\" above will invoke a pointer to an IC stub\nof machine code compiled with a dedicated IC-stub compiler.<\/p>\n<p>This works well for SpiderMonkey on a native platform -- the fastest\nway to implement a fast-path is to produce a purpose-built sequence\nof a handful of machine instructions -- but is not compatible with\nWebAssembly's inability to add new code at runtime that we noted\nabove. This is because SpiderMonkey only knows what the fast-paths\nshould be after it starts executing, which is too late to add code\nto the Wasm module.<\/p>\n<\/li>\n<li>\n<p>Less fundamental, but still a roadblock: the \"baseline interpreter\"\nin SpiderMonkey is <em>also<\/em> JIT-compiled, albeit once at JS engine\nstartup rather than as code is executing. This is more of an\nimplementation\/engineering tradeoff, wherein the SpiderMonkey\nauthors <a rel=\"external\" href=\"https:\/\/hacks.mozilla.org\/2019\/08\/the-baseline-interpreter-a-faster-js-interpreter-in-firefox-70\/\">realized they could reuse the baseline compiler\nbackend<\/a>\nto cheaply produce a new tier (a brilliant idea!), but again is not\ncompatible with the WASI environment.<\/p>\n<\/li>\n<\/ul>\n<p>You might already be thinking: the above two points are not laws of\nnature -- nothing says that we can't <em>interpret<\/em> whatever code we\nwould have JIT-compiled and executed in native SpiderMonkey. And you\nwould be right: in fact, that's the starting point for the Portable\nBaseline Interpreter (PBL)!<sup class=\"footnote-reference\"><a href=\"#4\">4<\/a><\/sup><\/p>\n<h2 id=\"a-baseline-without-jit-portable-baseline\">A Baseline without JIT: Portable Baseline<\/h2>\n<p>Here we can now introduce the <em>Portable Baseline Interpreter<\/em>, or PBL\nfor short. PBL is a new <em><a rel=\"external\" href=\"https:\/\/firefox-source-docs.mozilla.org\/js\/index.html#javascript-jits\">execution\ntier<\/a><\/em>\nthat replaces the native \"baseline interpreter\" described above. Its\nkey distinguishing feature is that it does not require any runtime\ncode generation (JIT-compilation). Thus, it is suitable for use in a\nWasm\/WASI program, or in any other environment where runtime codegen\nis prohibited.<\/p>\n<p>The key design principle of PBL is to stick as <em>closely as possible<\/em>\nto the other baseline tiers. In SpiderMonkey, significant shared\nmachinery exists for the (existing) baseline interpreter and baseline\ncompiler: there is a defined stack layout and execution state, there\nis code that understands how to garbage-collect, introspect, and\nunwind this state, and there are mechanisms to track the inline caches\nassociated with baseline execution. PBL's goal at a technical level is\nto perform exactly the work that the (native) baseline interpreter\nwould do, except in portable C++ code rather than runtime-generated\ncode.<\/p>\n<p>To achieve this goal, the two major tasks were:<\/p>\n<ul>\n<li>\n<p>Implementing a new interpreter loop over JS opcodes. We cannot use\nthe generic interpreter tier's <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/630d2c45fc127a44756e3cca8cef51c35654a4b4\/js\/src\/vm\/Interpreter.cpp#2179\">main\nloop<\/a>\n(what SpiderMonkey calls the \"C++ interpreter\"), because the actions\nfor each opcode in that implementation are \"generic\" -- they do not\nuse ICs to specialize on types or other kinds of fast-paths -- and\nso are not suitable for our purposes. Likewise, we cannot use the\nbaseline interpreter's main loop because it is <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/630d2c45fc127a44756e3cca8cef51c35654a4b4\/js\/src\/jit\/Ion.cpp#143\">generated at\nstartup<\/a>\nusing the JIT backend, and so is not suitable for use in a context\nwhere we can only run portable C++.<\/p>\n<p>Instead, we need to implement a <a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/gecko-dev\/blob\/981fbf34ea6ee1400136cf94ed04e1105adf8799\/js\/src\/vm\/PortableBaselineInterpret.cpp#L2533\">new interpreter\nloop<\/a>\nwhose actions for each opcode invoke ICs where appropriate --\nexactly the actions that the baseline interpreter does, but written\nin portable code. This is superficially \"simple\", but turns out to\nrequire careful attention to many subtle details, because\nhandwritten JIT-compiled code can control some aspects of execution\nmuch more precisely than C++ ordinarily can. (More on this below!)<\/p>\n<\/li>\n<li>\n<p>Implementing an interpreter for\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/630d2c45fc127a44756e3cca8cef51c35654a4b4\/js\/src\/jit\/CacheIR.h#27\">CacheIR<\/a>,\nthe intermediate representation in which the \"fast-path code\" for IC\nstubs is represented. CacheIR opcodes encode the \"guards\", or\npreconditions necessary for a fast-path to apply, and the actions to\nperform. There are many specialized CacheIR opcodes to particular\ndata structures or runtime state -- it is a heavily custom IR -- but\nthis tight fit to SpiderMonkey's design is exactly what gives it its\nability to concisely encode many fast-paths.<\/p>\n<\/li>\n<\/ul>\n<p>In principle, developing an interpreter for an IR that already has two\ncompilers (to <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/630d2c45fc127a44756e3cca8cef51c35654a4b4\/js\/src\/jit\/BaselineCacheIRCompiler.cpp\">machine\ncode<\/a>\nand <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/630d2c45fc127a44756e3cca8cef51c35654a4b4\/js\/src\/jit\/WarpCacheIRTranspiler.cpp\">optimizing compiler IR\n(MIR)<\/a>)\nshould be relatively straightforward: we transliterate the actions\nthat the compiled code is performing into a direct C++\nimplementation. In a system as complex as a JavaScript engine, though,\nnothing is ever quite \"simple\". Challenges encountered in implementing\nthe CacheIR interpreter fall into two general categories: aspects of\nexecution that cannot be directly replicated in C++ code, so need to\nbe \"emulated\" in some way; and achieving practical performance by\nkeeping the \"virtual machine\" model lightweight and playing some other\ntricks too. We'll give a few examples of each kind of challenge below.<\/p>\n<h3 id=\"challenge-baseline-stack-layout\">Challenge: Baseline Stack Layout<\/h3>\n<p>The first challenge that arose consists of <em>emulating the stack<\/em> as\nthe JIT-compiled code would have managed it. SpiderMonkey's baseline\ntiers build a series of stack frames with a <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/37d9d6f0a77147292a87ab2d7f5906a62644f455\/js\/src\/jit\/JitFrames.h#37\">carefully-defined\nformat<\/a>\nthat can be traversed for various purposes: finding (and updating)\ngarbage-collection roots, handling exceptions and unwinding execution,\nproducing stack backtraces, providing information to the debugger API\nand allowing the debugger to update state, and so on.<\/p>\n<p>The format of a single stack frame consists of a <code>JitFrame<\/code> that looks\na lot like a \"normal\" function prologue's frame -- return address,\nprevious frame pointer -- but also includes a \"callee token\" that the\ncaller pushes, describing the called function at the JS level, and a\n\"receiver\" (the <code>this<\/code> value in JavaScript). The <code>BaselineFrame<\/code> below\nthat records the JS bytecode virtual machine state in a known format,\nso it can be introspected: current bytecode PC, current IC slot, and\nso on. Below that, the JS bytecode VM's operand\/value stack is\nmaintained on the real machine stack. And, just before calling any\nother function, a \"footer\" descriptor is pushed: this denotes the kind\nof frame that just finished, so it can be handled appropriately.<\/p>\n<p>This format has a very important property: it has <em>no gaps<\/em>. It is not\nsimply a linked list of fixed-size descriptor or header structures. If\nit were, we could potentially place <code>BaselineFrame<\/code> \/ <code>JitFrame<\/code>\ninstances on the C++ stack in PBL, and link them together with the\nprevious-FP fields as normal. But this won't work: rather, every\nmachine word of the baseline-format stack is accounted for.<\/p>\n<p><img src=\"\/assets\/2023-10-03-baseline-stack-web.svg\" alt=\"Figure: baseline stack\" \/><\/p>\n<p>This works fine for JIT-compiled code, because we control the code\nthat is emitted and can maintain whatever stack-format we define.  But\nbecause the C++ compiler owns and manages the machine stack layout\nwhen we are running in C++ code, PBL is not able to maintain the\nactual machine stack in this format.<\/p>\n<p>Thus, we instead define an <em>auxiliary stack<\/em>, build a series of real\nbaseline frames on it, and maintain this in parallel to the executing\nC++ code's actual machine stack. When we enter a new frame at the C++\nlevel, we push a new frame on the auxiliary stack; when we return, we\npop a frame.<sup class=\"footnote-reference\"><a href=\"#5\">5<\/a><\/sup> This auxiliary stack is what the garbage collector,\nexception unwinder, debugger, and other components introspect: we\nstore pointers to its frames in the JIT state, and so on. As far as\nthe rest of the engine is concerned, it is the real stack. The only\nmajor difference is that all return addresses are <code>nullptr<\/code>s: we don't\nneed them, because we still manage control flow at the C++ level.<\/p>\n<h3 id=\"challenge-unwinding\">Challenge: Unwinding<\/h3>\n<p>A second issue that arises from the differences between a native\nmachine model and that of PBL is <em>unwinding<\/em>. In JIT code, where we\nhave complete control over emitted instructions, the call stack is\njust a convention and we are free to skip over frames and jump to any\ncode location we please. The exception unwinder uses this to great\neffect: when an exception is thrown, the runtime walks the stack and\nlooks for any appropriate handler. This might be several call-frames\nup the stack. When one is found, it sets the stack pointer and frame\npointer to within that frame -- effectively popping all deeper frames\nin one action -- and jumps directly to the appropriate program counter\nin that handler's frame. Unfortunately, this is not possible to do\ndirectly in portable C++ code.<sup class=\"footnote-reference\"><a href=\"#6\">6<\/a><\/sup><\/p>\n<p>Instead, starting from the invariant that one C++ frame in the PBL\ninterpreter function \"owns\" one (or more as an optimization -- see\nbelow) baseline frames, we implement a <em>linear-time<\/em> unwinding scheme:\neach C++ interpreter invocation remembers its \"entry frame\"; when\nunwinding, after an exception or otherwise, we compare the new\nframe-pointer value to this entry frame; if \"above\" (higher in\naddress, for a downward-growing stack), we return from the interpreter\nfunction with a special code indicating an unwind is happening. The\ncaller instance of the PBL interpreter function then performs the same\nlogic, propagating upward until we reach the correct C++ frame. The\nfollowing figure contrasts the native and PBL strategies:<\/p>\n<p><img src=\"\/assets\/2023-10-03-unwind-web.svg\" alt=\"Figure: native baseline and PBL unwinding\" \/><\/p>\n<p>Thus, we do not have the same asymptotic <code>O(1)<\/code> unwind-efficiency\nguarantee that native baseline execution does, but we remain\ncompletely portable, able to execute anywhere that standard C++ runs.<\/p>\n<h3 id=\"challenge-vm-exits\">Challenge: VM exits<\/h3>\n<p>A third issue that often arose was that of <em>emulating VM exits<\/em>. On\nthe native baseline platform, when JIT code is executing, the stack is\n\"under construction\", in a sense: the innermost frame is not complete\n(there is no footer descriptor word) and is not reachable from the VM\ndata structures. JIT code can call back into the runtime only via a\ncarefully-controlled \"VM exit\" mechanism, which pushes a special kind\nof \"exit frame\", records the end of the series of contiguous JIT\nframes (the \"JIT activation\"), and then invokes C++ code:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span># JIT code:<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>  ...<\/span><\/span>\n<span class=\"giallo-l\"><span>  push arg2<\/span><\/span>\n<span class=\"giallo-l\"><span>  push arg1                # trampoline<\/span><\/span>\n<span class=\"giallo-l\"><span>  call VMHelper  -----&gt;    push exit frame<\/span><\/span>\n<span class=\"giallo-l\"><span>                           cx-&gt;exitFP = fp<\/span><\/span>\n<span class=\"giallo-l\"><span>                           call VMHelperImpl    -----&gt;  walkStack(cx-&gt;exitFP)<\/span><\/span>\n<span class=\"giallo-l\"><span>                                                        doStuff(cx)<\/span><\/span>\n<span class=\"giallo-l\"><span>                           pop exit frame       &lt;-----  ret<\/span><\/span>\n<span class=\"giallo-l\"><span>  ...            &lt;-----    ret<\/span><\/span><\/code><\/pre>\n<p>which results in a stack layout that looks like:<\/p>\n<p><img src=\"\/assets\/2023-10-03-vmexit-stack-web.svg\" alt=\"Figure: the baseline stack after a proper VM exit\" \/><\/p>\n<p>While executing within the C++ PBL interpreter function, it is very\ntempting to simply call into the rest of the runtime as required. This\nresults in a stack that looks like the below, and unfortunately breaks\nin all sorts of exciting and subtle ways: it may appear to work, but\nframes are missing and GC roots are not updated after a moving GC; or\nif the dangling exit FP is not null, an entirely bogus set of stack\nframes may be traced. Either way, various impossible-to-find bugs\narise.<\/p>\n<p><img src=\"\/assets\/2023-10-03-vmexit-stack-incomplete-web.svg\" alt=\"Figure: the baseline stack after calling into the runtime without a proper VM exit\" \/><\/p>\n<p>PBL thus requires extreme discipline in separating \"JIT-code mode\" (or\nits emulation, in a portable C++ interpreter) and \"runtime mode\". To\nmake this distinction clearer, I designed a type-enforced mechanism\nthat leverages an important idiom in SpiderMonkey: every function that\nmight perform a GC or otherwise introspect overall VM state will take\na <code>JSContext<\/code> parameter. In the PBL interpreter function, we hide the\n<code>JSContext<\/code> (rename the local and set it to <code>nullptr<\/code> normally). We\nthen have a helper RAII class that pushes an exit frame and does\neverything that a \"VM exit\" trampoline would do, then behaves as a\n<em>restricted-scope local<\/em> that implicitly converts to the true\n<code>JSContext<\/code>. This looks like the below:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"cpp\"><span class=\"giallo-l\"><span class=\"z-entity z-name z-function\">  CASE<\/span><span class=\"z-punctuation z-section\">(<\/span><span>Opcode<\/span><span class=\"z-punctuation z-section\">) {<\/span><\/span>\n<span class=\"giallo-l\"><span>    Value arg0 <\/span><span class=\"z-keyword z-operator\">=<\/span><span class=\"z-entity z-name z-function\"> POP<\/span><span class=\"z-punctuation z-section z-punctuation\">().<\/span><span class=\"z-entity z-name z-function\">asValue<\/span><span class=\"z-punctuation z-section\">()<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span class=\"z-punctuation z-definition z-comment z-comment\">  \/\/ POP() is a macro that uses the `sp` local.<\/span><\/span>\n<span class=\"giallo-l\"><span>    Value arg1 <\/span><span class=\"z-keyword z-operator\">=<\/span><span class=\"z-entity z-name z-function\"> POP<\/span><span class=\"z-punctuation z-section z-punctuation\">().<\/span><span class=\"z-entity z-name z-function\">asValue<\/span><span class=\"z-punctuation z-section\">()<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ here, `sp` is the top-of-stack for our in-progress frame in our<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ in-progress activation. We are &quot;in JIT code&quot; from the engine&#39;s<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ perspective, even though this is still C++.<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">    {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">      \/\/ This macro completes the activation and creates a `cx` local<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">      \/\/ that gives us the JSContext* for use.<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-function\">      PUSH_EXIT_FRAME<\/span><span class=\"z-punctuation z-section\">()<\/span><span class=\"z-punctuation z-terminator\">;<\/span><span> <\/span><\/span>\n<span class=\"giallo-l\"><span>      <\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">      if<\/span><span class=\"z-punctuation z-section\"> (<\/span><span class=\"z-keyword z-operator\">!<\/span><span class=\"z-entity z-name z-function\">DoEngineThings<\/span><span class=\"z-punctuation z-section\">(<\/span><span>cx<\/span><span class=\"z-punctuation\">,<\/span><span> arg0<\/span><span class=\"z-punctuation\">,<\/span><span> arg1<\/span><span class=\"z-punctuation z-section\">)) {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-keyword\">        goto<\/span><span> error<\/span><span class=\"z-punctuation z-terminator\">;<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">      }<\/span><\/span>\n<span class=\"giallo-l\"><span>      <\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">    }<\/span><span class=\"z-punctuation z-definition z-comment z-comment\"> \/\/ pops the exit frame.<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-section\">  }<\/span><\/span><\/code><\/pre>\n<p>This idiom works fairly well in practice and statically prevents us\nfrom making most kinds of stack-frame-related mistakes.<\/p>\n<h3 id=\"optimization-avoiding-function-call-overhead\">Optimization: Avoiding Function-Call Overhead<\/h3>\n<p>At this point, we have introduced techniques to enable PBL to run\ncorrectly; we now have a functioning JavaScript interpreter that can\ninvoke ICs. (Take a breath and celebrate!) Unfortunately, I arrived at\nthis point and found that performance still lagged behind that of the\ngeneric interpreter. How could this be, when ICs directly encode\nfast-paths and allow us to short-circuit expensive runtime calls?<\/p>\n<p>The first realization came after profiling both a native build of PBL,\nand especially a Wasm build: C++ function calls can be\n<em>expensive<\/em>. The basic PBL design consisted of a JS interpreter that\ninvoked the IC interpreter for every opcode with an IC -- a majority\nof them, in most programs (all numeric operators, property accesses,\nfunction calls, and so on!). Thus function calls are extremely\nfrequent. Their high cost is for a few basic reasons:<\/p>\n<ul>\n<li>\n<p>When the interpreter function is large and has a lot of context\n(live variables), register pressure is high; when the called\nfunction is similar, we effectively have a full \"context switch\"\n(save all register values and use for new variables) on every\ncall\/return.<\/p>\n<\/li>\n<li>\n<p>Splitting logic across multiple functions precludes optimizations\nthat span the logic of both functions. For example, the IC\ninterpreter \"reified control flow as data\" by returning an enum\nvalue that the JS interpreter then switched on. Combining the two\nfunctions would allow us to embed the switch-bodies directly where\nthe return code is set.<\/p>\n<\/li>\n<li>\n<p>On many Wasm implementations, including\n<a rel=\"external\" href=\"https:\/\/wasmtime.dev\/\">Wasmtime<\/a> (my VM of choice and the main\noptimization target for our WASI port), function prologues have some\nextra cost: the generated code needs to check for stack overflow,\nand may need to check for interruption or preemption. This is a part\nof the cost of sandboxing that can only be avoided by staying within\na single function frame.<\/p>\n<\/li>\n<\/ul>\n<p>Thus, it is very important to avoid function-call overhead whenever\npossible. I optimized this in two ways. First, the IC interpreter is\naggressively inlined into the JS interpreter. This produces one\nsuper-interpreter that can run both kinds of bytecode -- JS bytecodes\nand CacheIR -- without extra frame setup at every IC site.<\/p>\n<p>Second, and more important in practice, <em>multiple JS frames<\/em> are\nhandled by one C++ frame (interpreter invocation). In a technique\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/37d9d6f0a77147292a87ab2d7f5906a62644f455\/js\/src\/vm\/Interpreter.cpp#3464-3488\">borrowed from SpiderMonkey's generic\ninterpreter<\/a>,\nwhen certain conditions are met, we\n<a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/gecko-dev\/blob\/c60c6d313c9fbbd50fd64e9b46d87bbf01e3dcc9\/js\/src\/vm\/PortableBaselineInterpret.cpp#L4104-L4196\">handle<\/a>\na JS call opcode by pushing a JS frame and dispatching directly to the\ncallee's first opcode without any C++-level call. (This may be an\nobvious implementation to anyone who has written an interpreter\nvirtual machine before, but disentangling C++ frames and JS frames is\nactually not trivial at all, given the prologue\/epilogue logic --\nhence the required conditions!) This interacts in subtle ways with\nunwinding described above: it means that the mapping from JS to C++\nframes is 1-to-many, and thus requires some care. (As a silver lining,\nhowever, the logic for a \"return\" is substantially similar to that for\n\"unwind\": we can use the same conditions to know when to return at the\nC++ level.)<\/p>\n<h3 id=\"optimization-hybrid-ics\">Optimization: Hybrid ICs<\/h3>\n<p>Having implemented all of the above techniques, I was still finding\nPBL to have somewhat disappointing performance numbers. Fortunately,\none final insight came: perhaps the tradeoffs related to which\noperations are profitable to fast-path <em>change<\/em>, when the cost of the\nfast-path mechanism (an IC) itself changes?<\/p>\n<p>For example: in native baseline execution, every arithmetic operator\nuses ICs to dispatch to type-specific behavior. The <code>+<\/code> operator, our\nfavorite example, has possible fast-paths for integers, floating-point\nnumbers, strings, and more. This is profitable in \"native baseline\"\nbecause the cost of an IC is extremely low: the JIT controls register\nallocation so it can effectively do global allocation across the\nfunction body and IC stubs by using special-purpose registers and a\ncustom calling convention, and it can avoid generating any\nprologue\/epilogue in the IC stubs themselves. As a result, ICs can\nliterally be a handful of instructions: call, check type tag in\nregisters 0 and 1, integer add, return. PBL, in contrast, is both\nemulating virtual-machine state (rather than using an optimized IC\ncalling convention), and paying the interpreter-dispatch cost for\nevery IC opcode.<\/p>\n<p>So I ran a simple experiment: in a native PBL build, I added <code>rdtsc<\/code>\n(CPU time counter)-based timing measurements around execution of each\nJS opcode both in the generic interpreter and in PBL's interpreter\nloop, and binned the results by opcode type. The results were\nfascinating: property accesses (e.g., <code>GetProp<\/code>) were significantly\nfaster with ICs, for example, but many simpler operators, like <code>Add<\/code>,\nwere twice as slow.<\/p>\n<p>Then given this data, I developed the \"hybrid ICs\" approach, namely:\nuse ICs only where they help! For the <code>Add<\/code> operator, the PBL\ninterpreter now has <a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/gecko-dev\/blob\/c60c6d313c9fbbd50fd64e9b46d87bbf01e3dcc9\/js\/src\/vm\/PortableBaselineInterpret.cpp#L2806-L2841\">specific cases for integer and floating-point\naddition<\/a>,\nand then invokes the generic interpreter's behavior (<code>AddOperation<\/code>);\nit never invokes the IC chain, but rather skips over it entirely. This\nbehavior is configurable -- with faster IC mechanisms in the future,\nwe may be able to use ICs for these opcodes again, so the code for\nboth strategies remains.<\/p>\n<p>The results were striking: PBL was finally showing significant\nspeedups on almost all benchmarks. The final \"hybrid IC\" set includes:<\/p>\n<ul>\n<li>\n<p><em>Property accesses.<\/em> These are extremely common in most JavaScript\ncode, and can benefit from fast-path behavior whenever objects\nusually have the same \"shape\", or set of properties, at a given\npoint. This is because the engine can encode a fast-path that\ndirectly accesses a particular memory offset in the object in\nmemory, without looking up a property by name.<\/p>\n<\/li>\n<li>\n<p><em>Calls.<\/em> This is somewhat less intuitive: for an ordinary call to\nanother JavaScript function, there is not much an IC can do -- we\njust need to update interpreter state to the callee and start\ndispatching. But for calls to built-in functions, as described\nabove, the benefits can be huge: string and array operations, for\nexample, transform from an expensive call into the runtime (through\nseveral layers of generic JS function-call logic) into just a few\ndirect field accesses or other operations on a known object type.<\/p>\n<\/li>\n<\/ul>\n<p>Every other JS opcode is executed with generic logic.<\/p>\n<h2 id=\"results\">Results<\/h2>\n<p>Enough description -- how well does it perform?<\/p>\n<p>The best test of any language runtime or platform is a \"real-world\"\nuse-case, and PBL has been fortunate to see some early adoption, where\ntwo real applications saw wall-clock CPU time reductions of 42% and\n17%, respectively, when executing on a Wasm\/WASI platform. That is\nquite significant and exciting, and is motivating adoption and further\nwork of PBL.<\/p>\n<p>While developing PBL, I did most of my benchmarking with\n<a rel=\"external\" href=\"https:\/\/chromium.github.io\/octane\/\">Octane<\/a>, which is\n<a rel=\"external\" href=\"https:\/\/v8.dev\/blog\/retiring-octane\">deprecated<\/a> but still useful\nwhen hacking on the core of a JS engine (one just needs to give the\nappropriate caveats that benchmark speedups will have an uncertain\ncorrelation to real-world speedups). On Octane, PBL currently sees a\n1.26x speedup (that is, throughput is 26% higher; or, equivalently,\nthere is a runtime reduction of <code>1 - 1\/1.26<\/code>, or 21%). That is quite\nsomething as well, for a new engine tier that remains completely\nportable as a pure interpreter!<\/p>\n<p>Because of these exciting results, and our future plans below, we have\nworked with the SpiderMonkey team themselves to plan <em>upstreaming<\/em> --\nincorporating PBL into the main SpiderMonkey tree. This will ease\nmaintenance because it will allow PBL to be updated and evolved (i.e.,\nkept compiling and running) as SpiderMonkey itself does, will allow us\nto use SpiderMonkey without a heavy patch-stack on top, and will make\nPBL available for others to use as well. We believe it could be useful\nbeyond the Wasm\/WASI world: for example, high-security contexts that\ndisallow JIT could benefit as well. The <a rel=\"external\" href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1855321\">upstreaming\ncode-review<\/a> is\nin-progress and we look forward to completing it!<\/p>\n<h2 id=\"future-compiled-code\">Future: Compiled Code<\/h2>\n<p><em>Note: this section describes my own thoughts and plans, but goes\nbeyond what is currently being upstreamed into SpiderMonkey, and is\nnot necessarily endorsed yet by upstream. My plan and hope is to\ndevelop the ideas to maturity and, if results hold up, propose\nadditional upstreaming -- but that is further out.<\/em><\/p>\n<p>PBL has an attractive simplicity as a pure interpreter, and has\nsurprised us with speedups even under that restriction. However, the\nlarger question, for me at least, has always been: how can we\n<em>compile<\/em> JS ahead-of-time in a performant way?<\/p>\n<p>Recall that the main restriction of the WebAssembly platform is not\nthat we can't generate code at all; it's just that all code, no matter\nthe producer (the traditional Wasm compiler toolchain or our own JS\ntools), needs to be generated before any execution occurs.<\/p>\n<p>SpiderMonkey's native baseline tiers hint at a way forward here. PBL\nas described above is roughly equivalent to the baseline interpreter\n(modulo the <em>way<\/em> that ICs are executed). Can we (i) produce compiled\ncode for ICs, and (ii) do the equivalent of the baseline <em>compiler<\/em>,\ngenerating a specialized Wasm function for every JS function body?<\/p>\n<p>In principle, this should be possible without information from\nexecution, because it handles the type-specific specialization with\nthe <em>runtime dispatch<\/em> inherent in the IC chains. In other words,\ntypes are late-binding, so we retain late-binding control flow to\nmatch.<\/p>\n<p>This still requires us to know what <em>possible<\/em> ICs we might need, but\nhere we can play a trick: we can collect many IC bodies ahead of time,\nand generate straight-line compiled Wasm functions for these IC\nbodies. This is more-or-less the trick we described in our post <a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/articles\/making-javascript-run-fast-on-webassembly\">two\nyears\nago<\/a>.<\/p>\n<p>But all of this is still implying the development of a Wasm <em>compiler<\/em>\nbackend. How does PBL help us at all? Isn't it a dead-end, if we are\neventually able to compile JS source (which we typically have\navailable ahead-of-time -- performance-critical <code>eval()<\/code> usage is\nrare) straight to specialized Wasm, with late-bound ICs?<\/p>\n<p>The answer to that lies in <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Partial_evaluation\">partial\nevaluation<\/a>. Over\nthe past year I have developed a tool called\n<a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/weval\">weval<\/a> that takes an interpreter in\na Wasm module, with a few minor intrinsic-call annotations (to specify\nwhat to specialize, and to denote that memory storing bytecode is\n\"constant\" and can be assumed not to self-modify dynamically), and\ngenerates a Wasm module with specialized functions appended. This\ngives us a compiler \"for free\" once we have an interpreter, and PBL\nhas been designed to be that interpreter.<\/p>\n<p>In particular, the JS opcode and IC opcode interpreters in PBL were\ndesigned carefully to work efficiently with weval, and in a next step\nto the project (<a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/gecko-dev\/tree\/pbl-weval\">development\nbranch<\/a>), I have\nthe whole thing working. Whereas pure-interpreter PBL got a 1.26x\nspeedup on Octane, PBL with weval gets a 1.58x speedup, up to 2.4x or\nso, with a bunch of low-hanging fruit remaining that will hopefully\npush that number further.<\/p>\n<p>This combination isn't quite ready for production use yet, but I\ncontinue to polish it, and we hope sometime early next year it will be\nready, taking us to \"conceptual parity\" (if not engineering\nfine-tuning parity!) with SpiderMonkey's native baseline compiler. We\nhave some more thoughts on going beyond that -- inlining ICs like\nWarpMonkey does, hoisting guards, and all the rest -- but more on that\nin due time.<\/p>\n<p>Given all of that, one could compare PBL and PBL+weval to\nSpiderMonkey's existing tiers. Recall our table above:<\/p>\n<table><thead><tr><th>Name<\/th><th>Data Required<\/th><th>JS opcode dispatch<\/th><th>ICs<\/th><th>Optimization scope<\/th><th>CacheIR dispatch<\/th><th>Codegen at runtime<\/th><\/tr><\/thead><tbody>\n<tr><td>Generic (C++) interpreter<\/td><td>JS bytecode<\/td><td>Interpreter (C++)<\/td><td>None<\/td><td>None<\/td><td>N\/A<\/td><td>No<\/td><\/tr>\n<tr><td>----------------------------------<\/td><td>-----------------------------------<\/td><td>--------------------------------------------<\/td><td>------------------<\/td><td>----------------------------------------<\/td><td>-------------------<\/td><td>----------------------------------<\/td><\/tr>\n<tr><td>Baseline interpreter<\/td><td>JS bytecode + IC data structures<\/td><td>Interpreter (generated at startup)<\/td><td>Dynamic dispatch<\/td><td>Special cases within one opcode via IC<\/td><td>Compiled<\/td><td>Yes (IC bodies, interp body)<\/td><\/tr>\n<tr><td>----------------------------------<\/td><td>-----------------------------------<\/td><td>--------------------------------------------<\/td><td>------------------<\/td><td>----------------------------------------<\/td><td>-------------------<\/td><td>----------------------------------<\/td><\/tr>\n<tr><td>Baseline compiler<\/td><td>JS bytecode + IC data structures<\/td><td>Compiled function body (1:1 with bytecode)<\/td><td>Dynamic dispatch<\/td><td>Special cases within one opcode via IC<\/td><td>Compiled<\/td><td>Yes (IC bodies, function bodies)<\/td><\/tr>\n<tr><td>----------------------------------<\/td><td>-----------------------------------<\/td><td>--------------------------------------------<\/td><td>------------------<\/td><td>----------------------------------------<\/td><td>-------------------<\/td><td>----------------------------------<\/td><\/tr>\n<tr><td>Optimizing compiler (WarpMonkey)<\/td><td>JS bytecode + warmed-up IC chains<\/td><td>Compiled function body (optimized)<\/td><td>Inlined<\/td><td>Across opcodes \/ whole function<\/td><td>Compiled<\/td><td>Yes (optimized function body)<\/td><\/tr>\n<\/tbody><\/table>\n<p>To which we could add the row:<\/p>\n<table><thead><tr><th>Name<\/th><th>Data Required<\/th><th>JS opcode dispatch<\/th><th>ICs<\/th><th>Optimization scope<\/th><th>CacheIR dispatch<\/th><th>Codegen at runtime<\/th><\/tr><\/thead><tbody>\n<tr><td>PBL (interpreter)<\/td><td>JS bytecode + IC data structures<\/td><td>Interpreter (C++)<\/td><td>Dynamic dispatch<\/td><td>Special cases within one opcode via IC<\/td><td>Interpreter (C++)<\/td><td>No<\/td><\/tr>\n<\/tbody><\/table>\n<p>And then, with weval and pre-collected ICs (but no profiling of the JS code!), we could have:<\/p>\n<table><thead><tr><th>Name<\/th><th>Data Required<\/th><th>JS opcode dispatch<\/th><th>ICs<\/th><th>Optimization scope<\/th><th>CacheIR dispatch<\/th><th>Codegen at runtime<\/th><\/tr><\/thead><tbody>\n<tr><td>PBL (wevaled)<\/td><td>JS bytecode + IC data structures<\/td><td>Compiled function body (1:1 with bytecode)<\/td><td>Dynamic dispatch<\/td><td>Special cases within one opcode via IC<\/td><td>Compiled<\/td><td>No (!!)<\/td><\/tr>\n<\/tbody><\/table>\n<p>which one will note is identical to the baseline-compiler row above,\nexcept that no runtime codegen is required.<\/p>\n<p>Finally, if we have reliable profiling information, such as from a\nprofiling run at build time, we could use this profile (just as one\ndoes in a standard C\/C++ \"PGO\" or \"<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Profile-guided_optimization\">profile-guided\noptimization<\/a>\"\nbuild) to <em>inline<\/em> the ICs. Note that this could be done in a way that\nis <em>completely agnostic to the underlying interpreter<\/em>, because IC\ninvocations are just indirect calls: that is, it is also a\nsemantics-preserving, independently-verifiable transform. Having done\nthat, we would then have:<\/p>\n<table><thead><tr><th>Name<\/th><th>Data Required<\/th><th>JS opcode dispatch<\/th><th>ICs<\/th><th>Optimization scope<\/th><th>CacheIR dispatch<\/th><th>Codegen at runtime<\/th><\/tr><\/thead><tbody>\n<tr><td>PBL (wevaled + inlined ICs)<\/td><td>JS bytecode + IC data structures + warmed-up IC chains<\/td><td>Compiled function body (optimized)<\/td><td>Inlined<\/td><td>Across opcodes \/ whole function<\/td><td>Compiled<\/td><td>No (!!)<\/td><\/tr>\n<\/tbody><\/table>\n<p>which approximates WarpMonkey. Note that this will require significant\nadditional engineering -- SpiderMonkey's native JITs, after all,\nembody engineer-centuries of effort (much of which we leverage by\nreusing its well-tuned CacheIR sequences, but much which we can't) --\nbut is a clear path to allow for optimized JS without runtime code\ngeneration.<\/p>\n<p>The thing that excites me most about this direction is that it is, in\nsome sense, \"deriving a JIT from scratch\": we are writing down the\nsemantics of the opcodes, and we're explicitly extracting fast-paths,\nbut we're using semantics-preserving tools to go beyond that. (Weval's\nsemantics are that it provides a function pointer to a specialized\nfunction that behaves identically to the original.) That allows us to\ndecouple the correctness aspects of our work from performance, mostly,\nand makes life far simpler -- no more insidious JIT bugs, or\ndivergence between the interpreter and compiler tiers.  More to come!<\/p>\n<hr \/>\n<p><em>Many, many thanks to <a rel=\"external\" href=\"https:\/\/github.com\/lukewagner\">Luke Wagner<\/a>, <a rel=\"external\" href=\"https:\/\/fitzgeraldnick.com\/\">Nick\nFitzgerald<\/a>, <a rel=\"external\" href=\"https:\/\/github.com\/elliottt\">Trevor\nElliott<\/a>, <a rel=\"external\" href=\"https:\/\/jamey.thesharps.us\/\">Jamey\nSharp<\/a>, <a rel=\"external\" href=\"https:\/\/tia.mat.br\/\">L Pereira<\/a>,\n<a rel=\"external\" href=\"https:\/\/michelledaviest.github.io\/\">Michelle Thalakottur<\/a>, and others with\nwhom I've discussed these ideas over the past several years. Thanks to Luke,\nNick, <a rel=\"external\" href=\"https:\/\/github.com\/linclark\">Lin Clark<\/a>, and <a rel=\"external\" href=\"https:\/\/www.mgaudet.ca\/\">Matt\nGaudet<\/a> for feedback on this post. Thanks also to\nTrevor Elliott and <a rel=\"external\" href=\"https:\/\/github.com\/JakeChampion\">Jake Champion<\/a> for help in\ngetting PBL integrated with other infrastructure, Jamey Sharp for ramping up\nefforts to fill out PBL's CacheIR opcode support, and the <a rel=\"external\" href=\"https:\/\/spidermonkey.dev\/\">Mozilla SpiderMonkey\nteam<\/a> for graciously hearing our ideas and agreeing\nto upstream this work.<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>WebAssembly running in a browser could implement runtime code\ngeneration and loading by calling out to external JavaScript. In\nessence, it would generate a new Wasm module in memory, then\ncall JS to instantiate that module into an instance that shares\nthe current linear memory, and call into it. However, this is\nfundamentally a feature of the Web platform and not built-in to\nWasm; and many Wasm platforms, especially those designed with\nsecurity among untrusted co-tenants in mind, do not allow this\nand strictly enforce ahead-of-time compilation of fixed code\ninstead.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>Honestly, even after <em>writing a new interpreter tier<\/em> for\nSpiderMonkey, I couldn't tell you the answer to this. (I run the\nbytecode, I don't lower to it!) The language's semantics are\nsomething to marvel at, in the edge cases, and this is all the\nmore reason to centralize on a few well-tested, well-engineered\nshared implementations.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>Note that there are some details here omitted for\nsimplicity. Most importantly, once inlining ICs, we need to\n<em>hoist the guards<\/em>, or the conditions that exist at the\nbeginning of every IC, so that they are shared in common (or\nremoved entirely, if we can prove they will always\nsucceed). Consider a function that operates on floating-point\nnumbers: every IC will be some form of \"check if inputs are\nfloats, do operation, tag result as float\" but instead we could\ncheck that the function arguments are floats <em>once<\/em>, then\npropagate from \"produce float\" in one IC to \"check if float\" in\nthe next. ICs <em>enable<\/em> this by expressing the necessary\npreconditions (checks) and postconditions (produced values and\ntheir types) for each operator, and inlining is necessary as\nwell because it places everything in one code-path so it is in\nscope to be cross-optimized; but guard-hoisting and -elimination\nare JIT-compiler-specific optimizations.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"4\"><sup class=\"footnote-definition-label\">4<\/sup>\n<p>One can see this idea as a variant of the <a rel=\"external\" href=\"https:\/\/publications.sba-research.org\/publications\/dls10.pdf\">interpreter\nquickening<\/a>\nidea, in a way: the CacheIR sequences are a shorter or more\nefficient implementation of particular behavior that we rewrite\nthe interpreted program to use (via the pluggable IC sites) as\nwe learn more about its execution.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"5\"><sup class=\"footnote-definition-label\">5<\/sup>\n<p>The correspondence isn't actually 1-to-1, unfortunately (that\nwould have been much simpler!): instead, we sometimes push an\nadditional frame for VM exits, and we also handle some calls\n\"inline\", pushing the frame and going right to the top of the\ndispatch loop again. The actual invariant is that every\nauxiliary stack frame is \"owned\" by one C++ function invocation,\nbut there may be several such frames. It is thus a 1-to-many\nrelationship.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"6\"><sup class=\"footnote-definition-label\">6<\/sup>\n<p>Strictly speaking, we could have used <code>setjmp()<\/code>\/<code>longjmp()<\/code> to\nimplement similar constant-time unwinding. However, this\ninteracts poorly with C++ destructors, and is also problematic\n-- that is, does not exist -- on WebAssembly with\nWASI. Eventually the <a rel=\"external\" href=\"https:\/\/github.com\/WebAssembly\/exception-handling\/blob\/main\/proposals\/exception-handling\/Exceptions.md\">exception handling\nproposal<\/a>\nfor Wasm may be directly usable for this purpose, but it is not\nfinalized yet.<\/p>\n<\/div>\n"},{"title":"Cranelift's Instruction Selector DSL, ISLE: Term-Rewriting Made Practical","published":"2023-01-20T00:00:00+00:00","updated":"2023-01-20T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2023\/01-20\/cranelift-isle\/"}},"id":"https:\/\/cfallin.org\/blog\/2023\/01-20\/cranelift-isle\/","content":"<p>Today I'm going to be writing about <strong>ISLE<\/strong>, or the \"instruction\nselection\/lowering expressions\" domain-specific language\n(<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Domain_specific_language\">DSL<\/a>), which\nover the past year we have designed, improved, and fully adopted in\nthe\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\/\">Cranelift<\/a>\ncompiler\nproject. <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\/isle\">ISLE<\/a>\nis now used to express both our instruction-lowering patterns for each\nof four target architectures, and also machine-independent optimizing\nrewrites. It allows us to develop these parts of the compiler in an\nextremely productive way: we can write the key idea -- that one opcode\nor instruction should map to another -- in a concise way, while\nmaintaining type-safety with an expressive type system, and allowing\nus to use the declarative patterns for many different purposes.<\/p>\n<p>The goal of this blog post is to illustrate the requirements and the\ndesign-space that led to ISLE's key ideas, and especially its\ndepartures from other \"<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Rewriting#Term_rewriting_systems\">term-rewriting\nsystems<\/a>\"\nand blending of ideas from backtracking languages like\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Prolog\">Prolog<\/a>. In particular, we'll\ntalk about how ISLE has a strong type system, with terms of distinct\ntypes (as opposed to one \"value\" type); how it matches on abstract\n\"extractors\" provided by an embedding environment, which effectively\ndefine a virtual \"input term\" without ever reifying it; and how it was\nexplicitly designed to have a simple\n\"<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Foreign_function_interface\">FFI<\/a>\" with\nRust for easy-to-understand, no-magic interactions with the rest of\nthe compiler. All of these properties allowed us to chart an\nincremental path from a fully handwritten compiler backend to one with\nall lowering logic in the DSL (in 27k lines of DSL code), with a\nrelatively low defect rate over the year-long migration and with\nsignificant correctness improvements along the way.<\/p>\n<p>The ISLE project was also a really interesting moment in my career\npersonally: it was <em>very much<\/em> a \"research project\", in that it\nrequired synthesis of existing approaches and careful thought about\nthe domain and requirements, and invention of slightly new takes on\nold ideas in order to make the whole thing practical. At the same\ntime, there was quite a lot of work put into the <em>incrementalist<\/em> and\n<em>pragmatic<\/em> aspect of the design (something I also <a rel=\"external\" href=\"https:\/\/cfallin.org\/blog\/2022\/06\/09\/cranelift-regalloc2\/#compatibility-and-migration-path\">talk about in an\nearlier post on\nregalloc2<\/a>),\nand a lot of care and feeding to a 12-month migration effort, curation\nof our understanding of the language and how to use it, and nurturing\nof ongoing ideas for improvement. I feel pretty fortunate to have (i)\nbeen given the space to wrestle the problem space down to its\nessential kernel and find a working design, and (ii) a cohort of\nreally great coworkers who ran with it and made it real. (Especially\nthanks to my brilliant colleague <a rel=\"external\" href=\"https:\/\/fitzgeraldnick.com\/\">Nick Fitzgerald\n(@fitzgen)<\/a> who completed the Cranelift\nintegration of ISLE and who pioneered the idea of rewrite DSLs in\nCranelift with his\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/dba74024aa412f284871375db292c1bf9079d769\/cranelift\/peepmatic\">Peepmatic<\/a>\nproject, which inspired many parts of ISLE and primed the project for\nthis effort.) I wrote more about the benefits we've seen so far from\nISLE, and anticipate to see,\n<a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/articles\/cranelift-progress-2022#isle-dsl-for-backends-and-mid-end\">here<\/a>;\nsuffice it to say that we're happy we've gone this way and we look\nforward to additional results that this work has enabled.<\/p>\n<p>This post covers material and repeats some arguments I made early in\nISLE's\n<a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/rfcs\/blob\/cranelift-isel-pre-rfc\/accepted\/cranelift-isel-pre-rfc.md\">pre-RFC<\/a>\nand\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/blob\/main\/accepted\/cranelift-isel-isle-peepmatic.md\">RFC<\/a>;\nI recommend reading those documents as well for further background, if\ndesired. The <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/isle\/docs\/language-reference.md\">ISLE language\nreference<\/a>\nis the canonical definition of the DSL.<\/p>\n<p>Let's first go through some background on Cranelift, on compiler DSLs\nin general, and motivate the case for a DSL in Cranelift; then we'll\nget into the details of ISLE proper.<\/p>\n<h2 id=\"context-new-cranelift-backends-and-handwritten-code\">Context: New Cranelift Backends and Handwritten Code<\/h2>\n<p>In the Cranelift project, starting in 2020, we developed a new\nframework for the machine backends -- the part of the compiler that\ntakes the final optimized machine-independent <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Intermediate_representation\">IR (intermediate\nrepresentation)<\/a>\nand converts it to instructions for the target instruction set,\nallocates registers, lowers control flow, and emits machine code.  (I\ndescribed more details in an earlier post series:\n<a href=\"\/blog\/2020\/09\/18\/cranelift-isel-1\/\">first<\/a>,\n<a href=\"\/blog\/2021\/01\/22\/cranelift-isel-2\/\">second<\/a>,\n<a href=\"\/blog\/2021\/03\/15\/cranelift-isel-3\/\">third<\/a>.)<\/p>\n<p>Prior to introducing ISLE, we build three backends in this framework:\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\/codegen\/src\/isa\/aarch64\">AArch64<\/a>,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\/codegen\/src\/isa\/x64\">x86-64<\/a>,\nand <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\/codegen\/src\/isa\/s390x\">s390x (IBM\nZ)<\/a>. The\ngeneral experience was quite positive -- the simplicity that we aimed\nto achieve by focusing on a \"straightforward handwritten lowering\npass\" design allowed us to quickly implement quite complete support\nfor WebAssembly (core and SIMD) and support other users of Cranelift\n(like\n<a rel=\"external\" href=\"https:\/\/github.com\/bjorn3\/rustc_codegen_cranelift\">cg_clif<\/a>). In\nMarch 2021, we <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/2718\">made the new x86-64 backend the\ndefault<\/a>, and\nat the end of September 2021, we were able to <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/3009\">remove the old x86\nbackend<\/a> with\nits legacy, complex, and generally slower framework.<\/p>\n<h2 id=\"the-need-for-a-dsl\">The Need for a DSL<\/h2>\n<p>However, as with many engineering problems, there is a tradeoff point\nin the design space. The simplicity of the \"just write the <code>match<\/code> on\nthe opcode and emit the instructions\"\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/blob\/main\/accepted\/cranelift-isel-isle-peepmatic.md\">approach<\/a>\neventually becames a downside: we had an\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/blob\/main\/accepted\/cranelift-isel-isle-peepmatic.md\">increasingly-deeply-nested<\/a>\nlowering function with many conditions matching on types,\nsub-expressions, and special cases, and keeping track of it all became\nmore and more difficult. In total, we found we were running into\n<a rel=\"external\" href=\"https:\/\/github.com\/cfallin\/rfcs\/blob\/cranelift-isel-pre-rfc\/accepted\/cranelift-isel-pre-rfc.md#downsides-as-complexity-increases\">three major\nproblems<\/a>:<\/p>\n<ol>\n<li>\n<p>It became very <em>tedious<\/em> to write what was essentially a longhand\nform of a list of lowering patterns. If adding a special lowering\nfor a combination of IR operators requires understanding control in\nhandwritten code, and writing out the checks for special conditions\nor searches for other combining operators by hand, then we are much\nless likely to improve the compiler backends: the incentives\ninstead point toward keeping the code as simple and as minimalistic\nas possible and discouraging change.<\/p>\n<\/li>\n<li>\n<p>It became difficult to refactor the code at all: the compiler\n\"lowering API\" was ossifying as more and more handwritten backend\ncode came to depend on its subtle details, and refactors became\nvery hard or impossible. This became especially apparent with the\nregalloc2 work.<\/p>\n<\/li>\n<li>\n<p>It became more and more difficult to maintain correctness, and\nensure that the backends were generating the code we\nexpected. While writing code against the lowering API, one had to\nkeep in mind the subtle correctness invariants for when one can\n\"combine\" instructions, when one can \"sink\" an operation, and so\non; and the rules for how to use registers and temporaries\nproperly. Even when generating some kind of correct code, it was\neasy to miss a corner of the state space and, say, omit a lowering\nfor a particular combination of input types, or skip an intended\noptimized lowering and use a general one instead, in an accidental\nand hard-to-reason-about way due to complex control flow. As we'll\nsee below, a DSL allows us to solve both of these problems with (i)\nprincipled strongly-typed abstractions in the DSL, and (ii)\n\"overlap checks\", respectively.<\/p>\n<\/li>\n<\/ol>\n<p>It became clear that generating lowering patterns from some\nmeta-description would lead to overall clearer and more maintainable\ncompiler source, and would give us more flexibility if we wanted to\nchange any details of or optimize the translation, as well. Hence, our\nrealization that we probably needed a <em>domain-specific language<\/em> (DSL)\nto generate this part of Cranelift.<\/p>\n<h2 id=\"dsls-in-compilers-and-term-rewriting\">DSLs in Compilers and Term-Rewriting<\/h2>\n<p>There is a long history of DSLs to specify compilers -- the\n\"metacompiler\" or \"compiler-compiler\" concept -- going back to at\nleast the 1970s. The general idea of a DSL-based instruction selection\nstage is to declaratively describe a list of <em>patterns<\/em> --\ncombinations of operators in the program -- and for each pattern, when\nit matches, a series of instructions that can implement that\npattern. This makes it easier to reason about what the compiler is\ndoing, to modify and improve it, and to apply systematic optimizations\nacross the backend by changing how the DSL is used to generate the\ncompiler backend itself.<\/p>\n<p>The \"pattern rewriting\" approach to a compiler backend can be seen as\na kind of <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Rewriting#Term_rewriting_systems\">term-rewriting\nsystem<\/a>:\nthat is, a formal framework in which rules operate on a data\nrepresentation (in this case, the program to be\ncompiled). Term-rewriting is an old idea: the <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Lambda_calculus\">lambda\ncalculus<\/a>, one of the\noriginal mathematical models of computation, operates via\nterm-rewriting. It is general enough to be useful yet also concise and\nexpressive enough to be very productive when the goal is to transform\nstructured data.<\/p>\n<p>One reason why term-rewriting is such a good fit for a compiler in\nparticular is that \"terms\", or nodes in an AST, represent values in\nthe program being compiled; given this, a rewrite rule is an\nexpression of an <em>equivalence<\/em>. An \"integer addition\" operator in a\ncompiler IR is <em>equivalent<\/em> to (or produces an equivalent result to)\nthe integer addition instruction in a given CPU's instruction set; so\nwe can replace one with the other. One might write this rule as\nsomething like <code>(add x y) =&gt; (x86_add x y)<\/code>, for example. Likewise,\nmany compiler optimizations can be expressed as rules, in a way that\nis familiar to any student of algebra: for example, <code>x + 0 == x<\/code>, or\nin an AST notation, <code>(add x 0) =&gt; x<\/code>.<\/p>\n<p>Examples of such pattern-rules abound in production compilers. For\nexample, in the Go compiler, a set of\n<a rel=\"external\" href=\"https:\/\/github.com\/golang\/go\/blob\/e870de9936a7efa42ac1915ff4ffb16017dbc819\/src\/cmd\/compile\/internal\/ssa\/_gen\/AMD64.rules\">rules<\/a>\ndefine how the IR's operators are converted into x86-64 instructions:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>\/\/ Lowering arithmetic<\/span><\/span>\n<span class=\"giallo-l\"><span>(Add(64|32|16|8) ...) =&gt; (ADD(Q|L|L|L) ...)<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>\/\/ combine add\/shift into LEAQ\/LEAL<\/span><\/span>\n<span class=\"giallo-l\"><span>(ADD(L|Q) x (SHL(L|Q)const [3] y)) =&gt; (LEA(L|Q)8 x y)<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>\/\/ Merge load and op<\/span><\/span>\n<span class=\"giallo-l\"><span>((ADD|SUB|AND|OR|XOR)Q x l:(MOVQload [off] {sym} ptr mem)) &amp;&amp;<\/span><\/span>\n<span class=\"giallo-l\"><span>    canMergeLoadClobber(v, l, x) &amp;&amp;<\/span><\/span>\n<span class=\"giallo-l\"><span>    clobber(l) =&gt;<\/span><\/span>\n<span class=\"giallo-l\"><span>((ADD|SUB|AND|OR|XOR)Qload x [off] {sym} ptr mem)<\/span><\/span><\/code><\/pre>\n<p>Similar kinds of descriptions exist in the <a rel=\"external\" href=\"https:\/\/github.com\/llvm-mirror\/llvm\/blob\/2c4ca6832fa6b306ee6a7010bfb80a3f2596f824\/lib\/Target\/X86\/X86InstrArithmetic.td\">LLVM x86\nbackend<\/a>\nand the <a rel=\"external\" href=\"https:\/\/github.com\/gcc-mirror\/gcc\/blob\/31ec203247413f150d5244198efd586fc6d2ef5e\/gcc\/config\/i386\/i386.md\">GCC x86\nbackend<\/a>,\nusing the <a rel=\"external\" href=\"https:\/\/llvm.org\/docs\/TableGen\/\">TableGen<\/a> language (LLVM)\nand the <a rel=\"external\" href=\"https:\/\/gcc.gnu.org\/onlinedocs\/gcc-4.3.2\/gccint\/Machine-Desc.html#Machine-Desc\">Machine Description\nDSL<\/a>\n(gcc), respectively.<\/p>\n<p>There is a large and well-explored design-space for auto-generated\ncompiler backends from such rules. A classical design is the\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/BURS\">BURS<\/a> (bottom-up rewrite system)\ntechnique. I won't attempt a deeper introduction here; further\ndescriptions can be found in e.g. the Dragon Book<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup> or Muchnick's\ntextbook<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup>. For this post, it suffices to know that these systems\nfind a \"covering\" of tree patterns such as the above over an input\nexpression tree.<\/p>\n<h2 id=\"term-rewriting-for-lowering-maybe\">Term Rewriting for Lowering... Maybe?<\/h2>\n<p>Given the above precedent -- several mainstream compilers adopting a\npattern-matching-based scheme, with clear benefits -- it would seem\nthat our path ahead is well-defined. Why, then, is there so much of\nthis post remaining? What more could be said?<\/p>\n<p>It turns out that three main problems arise when considering how to\nadopt a typical term-rewriting scheme. First, there is a basic\nquestion: do we actually reify the input tree as a tree? For example,\nif we have a pattern<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>(add x y) =&gt; (x86_add x y)<\/span><\/span><\/code><\/pre>\n<p>meaning that an <code>add<\/code> operator on two operands is lowered to an x86\n<code>ADD<\/code> instruction, does that imply that our IR literally contains a\nnode for the <code>add<\/code>?<\/p>\n<p>That may seem like a silly question to raise, as CLIF, Cranelift's IR,\ndoes indeed have an operator for <code>add<\/code> (actually <code>iadd<\/code> for \"integer\nadd\") that takes two arguments.<\/p>\n<p>But directly matching on a \"tree of operators\" implies several\nproperties of the IR that have deep impact. One of these properties is\nthat the \"value\" or \"result\" of the operator is its unique\nidentifier. In CLIF, this isn't the case: each instruction has its own\nidentifier (<code>Inst<\/code>) and each <code>Inst<\/code> can have any number of <code>Value<\/code>\nresult. Another is that it implies that the tree that the matching\nprocess <em>should<\/em> see is exactly what is in the IR. However, <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/ff995d910b64ea7937ccfd982dd431b1487a1ec8\/cranelift\/codegen\/src\/machinst\/lower.rs#L1216-L1236\">quite a\nfew\nconsiderations<\/a>\nare involved when a backend wants to \"merge\" the handling of operators\nby looking deeper into the tree. None of these impedance mismatches\nare fatal to the approach, but they do imply extra work to build the\ntree in memory \"as matched\".<\/p>\n<p>Second, there is the problem of how to incorporate <em>additional\ninformation<\/em> beyond the raw tree of operators. One could see this as a\nquestion of \"side-tables\" or \"auxiliary information\", or of supporting\nvarious \"queries\" on the input tree.<\/p>\n<p>For example, we might want to represent the ability to encode an\ninteger immediate in certain ISA-specific forms as a term. AArch64 has\nseveral such forms: a \"regular\" 12-bit immediate, and a <a rel=\"external\" href=\"https:\/\/dinfuehr.github.io\/blog\/encoding-of-immediate-values-on-aarch64\/\">rather clever\n\"logical\nimmediate\"<\/a>\nformat designed to efficiently encode common kinds of bitmasks in only\n13 bits. We might represent these with terms, and have rules that\ntranslate <code>(iconst K)<\/code> into <code>(aarch64_imm12 bits)<\/code> or\n<code>(aarch64_logicalimm bits)<\/code> and subsequent rules that match on these\nterms to encode immediate-using instruction forms. The problem then\ncomes: how do we know which of these intermediate rewrites to do\nbefore we attempt to match any instruction forms? Do we do both, and\nrepresent both forms?<\/p>\n<p>The net effect of this requirement is that the matching pattern for a\nrewrite rule starts to look less like a tree of terms and more like a\nsequence of custom queries. The <a rel=\"external\" href=\"https:\/\/github.com\/golang\/go\/blob\/e870de9936a7efa42ac1915ff4ffb16017dbc819\/src\/cmd\/compile\/internal\/ssa\/_gen\/AMD64.rules\">Go compiler's\nrules<\/a>\nadd predicates to rewrite rules to handle these cases, but this is\nawkward and makes the language harder to reason about. It would be\nbetter if we could represent the ISA concepts at the term-rewrite\nlevel as well.<\/p>\n<p>Third, there is the question of how to interact with the rest of the\ncompiler as we make these queries on the input representation. In the\nmost straightforward implementation, a rewrite system has knowledge of\nthe \"tree nodes\" that terms in the pattern match and that terms in the\nrewrite expression produce. But building the glue between the rewrite\nsystem and the IR data structures may be nontrivial, especially if\ncustom queries (as above) are also involved.<\/p>\n<p>All of this raises the question: is there a better way to think about\nthe execution semantics of the rewrite rules? What if the DSL were not\ninvolved in ASTs or rewrites in a direct way at all?<\/p>\n<h2 id=\"sequential-execution-semantics-and-external-extractors-constructors\">Sequential Execution Semantics and \"External Extractors\/Constructors\"<\/h2>\n<p>At this point, I hit upon ISLE's first key idea: what if all\ninteractions with the rest of the compiler were via \"virtual\" terms,\nimplemented by Rust functions? In other words, rather than build a\nsystem that matches on literal AST data structures and rewrites or\nproduces new output ASTs, all of the pattern matching would \"bottom\nout\" in a sort of FFI that would invoke the rest of the compiler, in\nhandwritten Rust. The DSL itself knows nothing about the rest of the\ncompiler, or \"ASTs\", or any other IR-specific concept. (This is ISLE's\nmain secret: it is not actually an instruction-selection DSL, but\nactually (or at least aspirationally) a more general language.)<\/p>\n<h3 id=\"sequential-semantics-for-matching\">Sequential Semantics for Matching<\/h3>\n<p>One could imagine a rule like <code>(iadd (imul a b) c) =&gt; (aarch64_madd a b c)<\/code> to \"compile\" to a series of \"match operations\" like the\nfollowing invented operations for some matching engine:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>$t0, $t1 := match_op $root, iadd<\/span><\/span>\n<span class=\"giallo-l\"><span>$t2, $t3 := match_op $t0, imul<\/span><\/span>\n<span class=\"giallo-l\"><span>$t4 = create_op aarch64_madd, $t2, $t3, $t1<\/span><\/span>\n<span class=\"giallo-l\"><span>return $t4<\/span><\/span><\/code><\/pre>\n<p>In this \"matching VM\", we execute <code>match_op<\/code> operations by trying to\nunpack a tree node into its children (arguments), given the expected\noperator. Any step in this sequence of match operators might \"fail\",\nwhich causes us to try the next rewrite rule instead. If we can match\nthe <code>iadd<\/code> from the input tree root, and the <code>imul<\/code> from its first\nargument, then the compiled form of this rule builds the\n<code>aarch64_madd<\/code> (\"multiply-add\") term.<\/p>\n<h3 id=\"programmable-matching\">Programmable Matching<\/h3>\n<p>Rather than a fixed set of operators like <code>match_op<\/code>, what if we\nallowed for environment-defined operators? What if operators like the\n<code>aarch64_logicalimm<\/code> above were \"match operators\" as well, such that\nthey \"matched\" if the given <code>u64<\/code> could be encoded in the desired form\nand failed to match otherwise?<\/p>\n<p>This is the essence of the \"external extractor\" idea (and the dual to\nit, the \"external constructor\") in ISLE. Once we allow user-defined\noperators for the left-hand side (\"matching pattern\") of a rule, we\nactually no longer need any built-in notion of AST matching at all;\nthis becomes just another thing we can define in the \"standard\nlibrary\" or \"prelude\" of our DSL!<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup><\/p>\n<p>The basic idea is to introduce the ability to <em>define<\/em> a term like\n<code>iadd<\/code> or <code>imul<\/code> and associate an external Rust function with it. When\nappearing on the left-hand side of a rewrite rule, terms match\n\"outside in\": that is, <code>(iadd (imul a b) c)<\/code> takes the root of the\ninput, tries to use <code>iadd<\/code> to destructure it to two arguments, and\ntries to use <code>imul<\/code> to destructure it further. (This outside-in,\nreversed order is the opposite of what one might expect if this were a\ntree of function calls, because we are <em>destructuring<\/em> (extracting)\nrather than <em>constructing<\/em>. We'll explore the analogy to functions\nmore below.)<\/p>\n<p>So, skipping straight to the real ISLE syntax now, we can define the\nterm like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(decl (iadd Value Value) Value)<\/span><\/span><\/code><\/pre>\n<p>and then associate a Rust function with it like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(extern extractor iadd my_iadd_impl)<\/span><\/span><\/code><\/pre>\n<p>and this implies the existence of a Rust function that the generated\nmatching code will invoke:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> my_iadd_impl<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator z-storage z-variable z-language\">&amp;mut self<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> input<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Value<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> Option<\/span><span class=\"z-punctuation\">&lt;(<\/span><span class=\"z-entity z-name z-type\">Value<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-entity z-name z-type\"> Value<\/span><span class=\"z-punctuation\">)&gt;;<\/span><\/span><\/code><\/pre>\n<p>Likewise, we can define a term to be used on the right-hand side and\nassociate an implementation like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(decl (aarch64_madd Value Value Value) Value)<\/span><\/span>\n<span class=\"giallo-l\"><span>(extern constructor aarch64_madd my_madd_impl)<\/span><\/span><\/code><\/pre>\n<p>with the Rust function<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">fn<\/span><span class=\"z-entity z-name z-function\"> my_madd_impl<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-keyword z-operator z-storage z-variable z-language\">&amp;mut self<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> a<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Value<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> b<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Value<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> c<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Value<\/span><span class=\"z-punctuation\">)<\/span><span class=\"z-keyword z-operator\"> -&gt;<\/span><span class=\"z-entity z-name z-type\"> Value<\/span><span class=\"z-punctuation\">;<\/span><\/span><\/code><\/pre>\n<p>and then use it like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(rule (iadd (imul a b) c)<\/span><\/span>\n<span class=\"giallo-l\"><span>      (aarch64_madd a b c))<\/span><\/span><\/code><\/pre>\n<p>and the generated code will invoke the <em>external extractor<\/em>\n<code>my_iadd_impl<\/code>; if it returns <code>Some<\/code> (matches), will invoke whatever\nexternal extractor is associated with <code>imul<\/code>; if it also returns\n<code>Some<\/code>, then invoke <code>aarch64_madd<\/code> to \"construct\" the result. These\nRust functions can do <em>whatever they like<\/em>: we have abstracted away\nthe need to actually query a reified AST and mutate it.<\/p>\n<h3 id=\"extractors-and-the-execution-driven-view\">Extractors and the Execution-Driven View<\/h3>\n<p>Another important consequence of this design is that we can define\n<em>arbitrary<\/em> extractors and constructors, and they can have <em>arbitrary<\/em>\ntypes. (ISLE is strongly-typed, with sum types that lower 1:1 to Rust\nenums.) This neatly addresses the \"metadata or side-table\" question\nabove: we don't need to generate auxiliary nodes in a real AST to\nrepresent information about a value, and we don't need to know when to\ncompute them; such computations can be driven by demand on the\nmatching side, and don't need to reify any actual node.<\/p>\n<p>Let's take a step back and understand what we have done here. We have\ntaken a rule that expresses a tree rewrite -- like <code>(iadd (imul a b) c) =&gt; (madd a b c)<\/code> -- and allowed for the terms in the left-hand side\nand right-hand side to compile to Rust function calls, with\nwell-defined semantics. The DSL itself deals only with\npattern-matching; ASTs and compiler IRs are wholly outside of its\nunderstanding. Nevertheless, if we ascribe formal semantics to <code>iadd<\/code>,\n<code>imul<\/code> and <code>madd<\/code>, we can still reason about the rewrite at the\nterm-rewriting system level, and this is essential to formal\nverification efforts for our compiler backends (see below!). We have\nthus allowed for integration with existing, handwritten Rust code\nwhile raising the abstraction level to allow for more declarative\nreasoning.<sup class=\"footnote-reference\"><a href=\"#4\">4<\/a><\/sup><\/p>\n<p>It's worth dwelling for a moment on the shift from an explicit AST\ndata structure, traversed and built by a rule-matching engine, to\ncalls to external extractors and constructors. As a consequence of\nthis shift, the term nodes corresponding to extractor matches need not\never actually exist in memory. The ISLE rewrite flow thus works\nsomething like the \"<a rel=\"external\" href=\"https:\/\/wiki.c2.com\/?VisitorPattern\">visitor\npattern<\/a>\": it introduces a level\nof abstraction that decouples the consumption and production of data\nfrom its representation, allowing more flexibility.<\/p>\n<p>The execution-driven view of term rewriting gives us our rewrite\nprocedure for free, as well: rather than an engine that takes some\nspecification of patterns and rewrites, we compile rules to sequential\ncode that invokes extractors and constructors. \"Rewriting\" a top-level\nterm is equivalent to invoking a Rust function. If we define a term,\n<code>lower<\/code>, corresponding to instruction lowering, then <code>(lower (iadd ...))<\/code> is a term that will be rewritten to whatever ISA-specific terms\nthe rules specify. This rewriting is done by a <em>Rust function<\/em> that\n<em>implements<\/em> <code>lower<\/code>; we invoke it with the term's arguments, and the\nbody will match on extractors as needed, then invoke constructors to\nbuild the rewritten expression.<\/p>\n<h2 id=\"explicit-types-and-implicit-conversions-in-isle\">Explicit Types (and Implicit Conversions) in ISLE<\/h2>\n<p>The second key differentiator in ISLE as compared to most other\nterm-rewriting systems is its <em>strong type system<\/em>. It might not be\ntoo surprising that a DSL that compiles to Rust would incorporate a\ntype system that mirrors Rust's type system to some degree, e.g. with\nsum types (enum variants). But this is actually a bit of a departure\nfrom conventional compiler-backend rule systems, and allows\nsignificant expressivity and safe-encapsulation wins, as we'll see\nbelow.<\/p>\n<h3 id=\"why-types\">Why Types?<\/h3>\n<p>A conventional rewrite system operates on an AST whose nodes all\nrepresent values in the program. In other words, every term has the\nsame type (at the DSL level): we can replace <code>iadd<\/code> with <code>x86_add<\/code>\nbecause both have type <code>Value<\/code>.<\/p>\n<p>While this works fine for simple substitutions, it quickly breaks down\nwhen various ISA complexities are considered. For example, how do we\nmodel an addressing mode? We might wish to have a node <code>x86_add<\/code> that\naccepts a \"register or memory\" operand, as x86 allows; and if a memory\noperand, the memory address can have one of several different forms\n(\"addressing modes\"): <code>[reg]<\/code>, <code>[reg + offset]<\/code>, <code>[reg + reg + offset]<\/code>, <code>[reg + scale*reg + offset]<\/code>.<\/p>\n<p>We could impose some ad-hoc structure on the AST in order to model\nthis: for example, an <code>x86_add<\/code> with an <code>x86_load<\/code> in its second\nargument (or alternately, a separate opcode <code>x86_add_from_memory<\/code>)\ncould represent this case. Then we would need to have rules for the\naddress expression: if another <code>x86_add<\/code> node (but only with register\narguments!), we could absorb that into the instruction's addressing\nmode.<\/p>\n<p>Ad-hoc structure like this is fragile, though, especially when\ntransformed by optimization passes. As a general guideline, as well,\nthe more we can put program invariants into the type system (at any\nlevel), the more likely we are to be able to maintain the structure\nacross refactors or unexpected interactions.<\/p>\n<h3 id=\"types-in-isle\">Types in ISLE<\/h3>\n<p>ISLE thus allows terms to have types that resemble function types. One\ncan define a term<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(decl lower_amode (Value) AMode)<\/span><\/span><\/code><\/pre>\n<p>that takes one argument, a <code>Value<\/code>, and produces an <code>AMode<\/code>. This type\ncan then be defined as<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(<\/span><span class=\"z-storage\">type<\/span><span> AMode (enum (Reg (reg Reg))<\/span><\/span>\n<span class=\"giallo-l\"><span>                  (RegReg (ra Reg)<\/span><\/span>\n<span class=\"giallo-l\"><span>                          (rb Reg))<\/span><\/span>\n<span class=\"giallo-l\"><span>                  (RegOffset (base Reg)<\/span><\/span>\n<span class=\"giallo-l\"><span>                             (offset u32))))<\/span><\/span><\/code><\/pre>\n<p>and so on. The <code>AMode<\/code> compiles to an enum in Rust with the specified\nenum variants, making interop with Rust code (in external extractors\nand constructors) via these rich types straightforward. In our machine\nbackends, machine instructions are defined as enums and constructed\ndirectly in the ISLE.<\/p>\n<h3 id=\"typed-terms-as-functions\">Typed Terms as Functions<\/h3>\n<p>Now that we have seen how to give a \"signature\" to a term, it's worth\ndiscussing how one can see terms -- both extractors and constructors\n-- as functions, albeit in opposite directions. In particular, with a term<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(decl F (A B C) R)<\/span><\/span><\/code><\/pre>\n<p>that has arguments (or AST child nodes) of types <code>A, B, C<\/code>, with a\ntype <code>R<\/code> for the term itself, one can see:<\/p>\n<ul>\n<li><code>F<\/code> as a function <em>from<\/em> <code>A, B, C<\/code> <em>to<\/em> <code>R<\/code>, when used as a\nconstructor; or<\/li>\n<li><code>F<\/code> as a function <em>from<\/em> <code>R<\/code> <em>to<\/em> <code>A, B, C<\/code>, when used as an\nextractor.<\/li>\n<\/ul>\n<p>In other words, given a tree of terms in a pattern (left-hand side),\nwhere terms are interpreted as extractors, we can see each term as a\nfunction invocation from the \"outer\" type (the thing being\ndestructured) to the \"inner\" types (the pieces that are the result of\nthe destructuring). Conversely, given a tree of terms in an expression\n(right-hand side), we can see each term as a function from the \"inner\"\ntypes (the arguments of the new thing being constructed) to the\n\"outer\" type (the return value). This is another way of seeing the\n\"execution-driven\" view of ISLE semantics described above.<\/p>\n<p>Note that the extractor form of <code>F<\/code> above is ordinarily a <em>partial<\/em>\nfunction -- that is, it is allowed to have <em>no<\/em> mapping for a particular\nvalue. This is how we formally think about the \"doesn't match\" case\nwhen searching for a particular kind of node in an AST, or any other\nmatcher on the left-hand side of a pattern. In contrast, the constructor\nis normally <em>total<\/em> -- cannot fail -- unless explicitly declared to be\npartial. (Partial constructors are useful for <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/blob\/main\/accepted\/isle-extended-patterns.md\"><code>if-let<\/code>\nclauses<\/a>.)<\/p>\n<h3 id=\"types-for-invariants\">Types for Invariants<\/h3>\n<p>Support for arbitrary types lets us much more richly capture\ninvariants as well, by encapsulating values (on the input side) or\nmachine instructions (on the output side) so that they can only be\nused or combined in legal ways. For example:<\/p>\n<ul>\n<li>\n<p>Many instruction-set architectures have a \"flags\" register that is\nset by certain operations with bits that correspond to conditions\n(result was zero, result was negative, etc.) and used by conditional\nbranches and conditional moves. This is \"global\" or \"ambient\" state\nand one has to be careful to use the flags after computing them,\nwithout overwriting them in the meantime. To ensure exact\ncorrespondence of a particular flag-producer and flag-consumer,\ncertain instruction constructors create <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/0c615365c645776c1abf42be89edc1d5292163c8\/cranelift\/codegen\/src\/prelude_lower.isle#L319-L358\"><code>ProducesFlags<\/code> and\n<code>ConsumesFlags<\/code>\nvalues<\/a>\nrather than raw instructions. These can then be emitted <em>together<\/em>,\nwith no clobber in the middle, with the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/0c615365c645776c1abf42be89edc1d5292163c8\/cranelift\/codegen\/src\/prelude_lower.isle#L389-L399\"><code>with_flags<\/code><\/a>\nconstructor.<\/p>\n<\/li>\n<li>\n<p>There is a distinction between an IR-level <code>Value<\/code> and a\nmachine-level value in a register, which we denote with <code>Reg<\/code>. When\na lowering rule requires an input to be in a register, it can use\nthe <code>put_in_reg<\/code> constructor, which takes a <code>Value<\/code> and produces\n(rewrites to) a <code>Reg<\/code>. This provides a way for us to do bookkeeping\n(note that the value was used, and ensure that we codegen its\nproducer), but also allows us to distinguish <em>how<\/em> to place the\nvalue in the register: one may wish to <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/0c615365c645776c1abf42be89edc1d5292163c8\/cranelift\/codegen\/src\/isa\/aarch64\/inst.isle#L2780-L2812\">sign- or\nzero-extend<\/a>\nthe values.<\/p>\n<\/li>\n<li>\n<p>There is a distinction between an IR-level <code>Value<\/code> and the\ninstruction that produces it. Not every <code>Value<\/code> is defined by an\ninstruction; some are <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Static_single-assignment_form#Block_arguments\">block\nparameters<\/a>. Furthermore,\nat a given point in the lowering process, we may not be <em>allowed<\/em> to\nsee the producer of a value, if we cannot \"sink\" its effect (merge\nit into an instruction generated at the current point). Thus,\ninstructions have <code>Value<\/code>s as operands, but one goes from a <code>Value<\/code>\nto an <code>Inst<\/code> (instruction ID) with\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/0c615365c645776c1abf42be89edc1d5292163c8\/cranelift\/codegen\/src\/prelude_lower.isle#L220-L222\"><code>def_inst<\/code><\/a>,\nwhich may or may not match depending on whether there is an <code>Inst<\/code>\nand we can see\/merge it.<\/p>\n<\/li>\n<\/ul>\n<h3 id=\"implicit-conversions\">Implicit Conversions<\/h3>\n<p>Type-safe abstractions allow for well-defined and safe interfaces, but\ncan lead to verbose code. After several months of experience with\nISLE, we were finding that we wrote rules like:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(rule (lower (iadd (def_inst (imul x y)) z))<\/span><\/span>\n<span class=\"giallo-l\"><span>  (madd<\/span><\/span>\n<span class=\"giallo-l\"><span>   (put_in_reg x)<\/span><\/span>\n<span class=\"giallo-l\"><span>   (put_in_reg y)<\/span><\/span>\n<span class=\"giallo-l\"><span>   (put_in_reg z)))<\/span><\/span><\/code><\/pre>\n<p>when we would prefer to write more natural rules like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(rule (lower (iadd (imul x y) z))<\/span><\/span>\n<span class=\"giallo-l\"><span>      (madd x y z))<\/span><\/span><\/code><\/pre>\n<p>as in our original term-rewriting examples above. At first it seemed\nwe had to choose one or the other: the shorter form would require\nabandoning some of the type distinctions we were making. But in\nactuality, there is some redundancy. Consider: when typechecking the\nleft-hand side pattern, we know that <code>iadd<\/code>'s arguments (which the\nextractor will produce, if it can destructure the <code>Inst<\/code> type) have\ntype <code>Value<\/code>, but the inner <code>imul<\/code> expects an <code>Inst<\/code>. In our prelude\nwe have one canonical term that converts from one to the other:\n<code>def_inst<\/code>. Likewise, in the right-hand side, bindings <code>x<\/code>, <code>y<\/code> and\n<code>z<\/code> have type <code>Value<\/code>, but the <code>madd<\/code> constructor that builds a\nmachine instruction requires <code>Reg<\/code> types (which are virtual registers\nin pre-regalloc machine code). We likewise have one term,\n<code>put_in_reg<\/code>, that can do this conversion. If there is only <em>one<\/em>\ncanonical way, or usual way, to make the conversion, and if the types\non <em>both<\/em> sides of the conversion are already known, why can't we\nrectify the types by inserting necessary conversions automatically?<\/p>\n<p>ISLE thus has one final trick up its sleeve to improve ease-of-use:\nimplicit conversions. By specifying converter terms for pairs of types\nlike<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"common-lisp\"><span class=\"giallo-l\"><span>(convert Inst Value def_inst)<\/span><\/span>\n<span class=\"giallo-l\"><span>(convert Value Reg put_in_reg)<\/span><\/span><\/code><\/pre>\n<p>the typechecking pass can expand the pattern and rewrite expression\nASTs as necessary. This makes writing lowering rules much more natural\nwith less boilerplate, and we have a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/0c615365c645776c1abf42be89edc1d5292163c8\/cranelift\/codegen\/src\/prelude_lower.isle#L712-L723\">fairly rich set of implicit\nconversions<\/a>\ndefined in our prelude to facilitate this.<\/p>\n<h2 id=\"putting-it-all-together-ast-patterns-in-isle\">Putting it All Together: AST Patterns in ISLE<\/h2>\n<p>Now that we've taken a tour of the various features of ISLE, let's\nreview what we have built. We have started with a desire to express\nhigh-level rewrite rules that equate AST nodes -- such as <code>(iadd (imul x y) z)<\/code> and <code>(madd x y z)<\/code> -- and to have a system that performs such\nrewrites, in a way that interoperates with the existing compiler\ninfrastructure and has predictable and comprehensible behavior.<\/p>\n<p>ISLE allows high-level patterns to be compiled to straightforward Rust\npattern-matching code. A strong type system with sum types (enums)\nensures that terms in patterns and rewrites match the expected schema,\nand allows for expressing high-level invariants. Implicit conversions\nleverage these types to remove redundancy in the patterns, allowing\nfor more natural high-level forms while retaining the useful\ntype-level distinctions. The ability to arbitrarily define\n\"extractors\" to use in patterns allows us to build up a rich\npattern-language in our prelude, matching trees of operators, values,\nand pieces of the input program with various properties in a\nprogrammable way. And the well-defined mapping to Rust and an\nexecution scheme that maps terms directly to function calls, rather\nthan an incrementally-rewritten AST allows for code as efficient as\nwhat one would write by hand.<\/p>\n<p>We have thus gone from <code>(iadd (imul x y) z) =&gt; (madd x y z)<\/code> to\nsomething like:<\/p>\n<ul>\n<li>Match the input <code>Inst<\/code> against the <code>iadd<\/code> enum variant, getting\nargument <code>Value<\/code>s if so;<\/li>\n<li>Get the <code>Inst<\/code> that produced the first <code>Value<\/code>, if we're allowed to\nmerge it, via <code>def_inst<\/code>;<\/li>\n<li>Match that <code>Inst<\/code> against the <code>imul<\/code> enum variant, getting its\nargument <code>Value<\/code>s if so;<\/li>\n<li>Put all three remaining <code>Value<\/code>s in registers with calls to\n<code>put_in_reg<\/code>; and<\/li>\n<li>Emit an <code>madd<\/code> instruction with these registers<\/li>\n<\/ul>\n<p>all <em>via bindings defined in ISLE itself<\/em> and <em>without any knowledge\nof \"instruction lowering\" in the ISLE DSL compiler<\/em>. As concrete\nevidence that the last point is valuable, we have been able to use\nISLE for CLIF-to-CLIF rewrite rules in our new mid-end optimization\nframework as well, simply by defining a new prelude (see below!).<\/p>\n<h2 id=\"ongoing-and-future-benefits-of-declarative-rules\">Ongoing and Future Benefits of Declarative Rules<\/h2>\n<p>The next most exciting thing about writing lowering rules\ndeclaratively -- after the expressivity and productivity win while\ndeveloping the compiler itself -- is that being able to reason about\nthe rules <em>as data<\/em> lets us analyze them in various ways and check\nthat they satisfy desirable properties.<\/p>\n<h3 id=\"correctness-and-formal-verification\">Correctness and Formal Verification<\/h3>\n<p>As an example, during the development of our rulesets, we found that\nit was sometimes unintuitive which rule would fire first. We have a\npriority mechanism to allow this to be controlled in an arbitrary way,\nbut the default heuristics (roughly, \"more specific rule first\") were\nsometimes counter-intuitive.<\/p>\n<p>We thus invented the idea of an <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/4906\">overlap\nchecker<\/a>,\ninitially implemented by my brilliant colleague <a rel=\"external\" href=\"https:\/\/www.linkedin.com\/in\/elliotttrevor\/\">Trevor\nElliott<\/a> and subsequently\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/5195\">redesigned with a new internal representation and\nalgorithm<\/a> by\nmy other brilliant colleague <a rel=\"external\" href=\"https:\/\/jamey.thesharps.us\/\">Jamey\nSharp<\/a>. The key idea is to define \"rule\noverlap\" such that two rules overlap if a given input to the\npattern-matching could cause either rule to fire. In these (and only\nthese) cases, priority and\/or default ordering heuristics determine\nwhich rule actually does fire. Then we decided that in such cases, we\nwould <em>require<\/em> the ISLE author to use the priority mechanism to\nexplicitly choose one of the rules. In other words, no two overlapping\nrules can have the same priority. Through a series of PRs to fix up\nour existing rules, we were able to actually find several cases where\nrules were fully \"shadowed\", or unreachable because some other more\ngeneral rule was always firing first. We turned on enforcing mode for\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/5011\">non-overlap among same-priority\nrules<\/a> and\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/5322\">non-shadowing of rules by higher-priority\nrules<\/a> after\nfixing several cases, and as a result, we now have more confidence in\nthe correctness of our rulesets.<\/p>\n<p>On a broader scale, writing rules as equivalences from one AST to\nanother lets us verify that the two sides are, well, actually\nequivalent! There is an ongoing collaboration with some\nformal-verification researchers who are adding <em>annotations<\/em> to\nexternal extractors and constructors that describe their semantics\n(mostly in terms of an SMT checker's theory-of-bitvectors\nprimitives). Given these \"specs\", one could lower each ISLE rule to\nSMT clauses rather than executable Rust code, and search for cases\nwhere it is incorrect. I won't steal any thunder here -- the work is\nreally exciting (also still in progress) and the researchers will\npresent it in due time -- but it's an example of what declarative DSLs\nallow.<\/p>\n<p>The ISLE-to-Rust compiler (metacompiler) is also a fairly complex tool\nin its own right, and has had bugs in the past. What makes us\nconfident that we are generating code that correctly implements the\nrules -- even if the rules themselves are verified? To answer that\nquestion, we <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/5435#pullrequestreview-1228022447\">have\nconsidered<\/a>\nhow to verify <em>the translation of the ISLE rules<\/em>. Our current plan is\nto modify the ISLE compiler to generate both the production backend,\nwith intelligently-scheduled matching operations, and a \"naive\"\nversion that runs through rules sequentially in priority order. If the\nlatter picks the same rule as the former, then we know we have a\nfaithful implementation of the left-hand-side matching (and the\ntranslation of the right-hand-side expression to constructor calls is\nstraightforward in comparison, so we trust it already). Then we can\ntrust that our verified-correct rules are being applied as written.<\/p>\n<h3 id=\"optimizing-the-instruction-selector\">Optimizing the Instruction Selector<\/h3>\n<p>Next, my colleague Jamey is working on <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/5435\">a new ISLE metacompiler\nbackend<\/a> that\nlowers ISLE rules to a planned sequence of matching ops more\nefficiently. The ability to change the way that compiler-backend code\nmatches the IR was a theoretical benefit of a DSL-based approach,\nrecognized and evaluated as we weighed the pros and cons of ISLE, but\nadmittedly was a bit speculative -- no one knew if we would actually\nfind a better way to generate code from the rules than the initial\nISLE compiler and its \"trie\"-based approach. I am very excited that\nJamey actually <em>did<\/em> manage to do this (and we should look forward to\na hopeful future blog post in which he can describe this in more\ndetail!).<\/p>\n<h3 id=\"mid-end-optimizations\">Mid-end Optimizations<\/h3>\n<p>Finally, as mentioned above, we were able to find a\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/opts\/algebraic.isle\">second<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/opts\/cprop.isle\">use<\/a>\nfor ISLE as part of the <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/blob\/main\/accepted\/cranelift-isel-isle-peepmatic.md\">egraph-based mid-end\noptimizer<\/a>\nwork, which we <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/5587\">just enabled by\ndefault<\/a>. (I\nhope to write a blog post about this soon too!) This was excellent and\nvery satisfying validation to me personally that ISLE is more general\nthan just Cranelift backends: we were able to write a new prelude (and\nactually share a bunch of extractors too) so that rules could specify\nIR-to-IR rewrites, in addition to IR-to-machine-instruction\nlowerings. This will allow us to iterate on and improve the compiler's\nsuite of optimizations more easily in the future, and it will also\nhave follow-on benefits in terms of shared infrastructure:\nverification tools that we build for backend lowering rules can also\nbe adapted to verify mid-end rules. In addition, there are potentially\nother ways that putting all of the compiler's core program-transform\nlogic into the same DSL will allow us to blur the lines, combine\nstages, or move logic around in ways we can't yet anticipate today;\nbut it seems like a worthwhile investment. In any case, ISLE proved to\nbe a quite useful tool in developing pattern-matching Rust code with\nless boilerplate and tedium than before!<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>ISLE has been great fun to design, build, and use; while we have learned a lot\nand made several language adjustments and extensions over the past year, I\nthink that there is general consensus that it has made the compiler backends\neasier to work on. I'm excited to see how the ongoing work (verification, new\nISLE codegen strategy) turns out, and how the language evolves in general. And\nas noted above, ISLE's secret is that it is actually more general than\ninstruction selection, or Cranelift: if you find another way to use it, I'd be\nvery interested in hearing about it!<\/p>\n<hr \/>\n<p><em>Thanks to Jamey Sharp and Nick Fitzgerald for reading and providing\nvery helpful feedback on a draft of this blog post, and to bjorn3 and\nAdrian Sampson for feedback and typo fixes after publication.<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>A Aho, R Sethi, J Ullman, M S Lam. Compilers: Principles,\nTechniques, and Tools. 2006.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>S Muchnick. Advanced Compiler Design and Implementation. 1997.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>ISLE <em>does<\/em> have special knowledge about Rust enums, and the\nability to match on them efficiently with <code>match<\/code> expressions in\nthe generated Rust code, because to miss this optimization would\nbe very costly. But in principle it could have been built\nwithout this, involving only Rust function calls and control\nflow around them.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"4\"><sup class=\"footnote-definition-label\">4<\/sup>\n<p>There is a parallel to the\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Prolog\">Prolog<\/a> language here, in\nthat it also allows for high-level, declarative expression of\nrule-matching with backtracking while also having a well-defined\nsequential execution semantics with FFI to an imperative\nworld. In fact Prolog was a central inspiration for ISLE's\ndesign. The key difference(s) are that (i) ISLE does not have\nfull backtracking -- once a left-hand side matches, we cannot\nbacktrack, as the right-hand sides are infallible -- and (ii)\nthere is no unification, and all dataflow in a term is\nunidirectional, from input (value to be destructured) to output\n(arguments). We used to have \"argument polarity\", which was\ncloser to unification in that it allowed a configurable (but\nfixed) input\/output direction for each argument to an\nextractor. We discarded this feature, however, in favor of a\nmore general <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/blob\/main\/accepted\/isle-extended-patterns.md\"><code>if-let<\/code>\nclause<\/a>.<\/p>\n<\/div>\n"},{"title":"Cranelift, Part 4: A New Register Allocator","published":"2022-06-09T00:00:00+00:00","updated":"2022-06-09T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2022\/06\/09\/cranelift-regalloc2\/"}},"id":"https:\/\/cfallin.org\/blog\/2022\/06\/09\/cranelift-regalloc2\/","content":"<p>This post is the fourth part of a three-part series<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup> describing\nwork that I have been doing to improve the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\">Cranelift<\/a>\ncompiler. In this post, I'll describe the work that went into\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\">regalloc2<\/a>, a new\nregister allocator I developed over the past year. The allocator\nstarted as an effort to port <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/source\/js\/src\/jit\/BacktrackingAllocator.cpp\">IonMonkey's register\nallocator<\/a>\nto Rust and a standalone form usable by Cranelift (\"how hard could it\nbe?\"), but quickly evolved during a focused optimization effort to\nhave its own unique design and implementation aspects.<\/p>\n<p><a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Register_allocation\">Register\nallocation<\/a> is a\nclassically hard\n(<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/NP-hardness\">NP-hard!<\/a>) problem, and a\ngood solution is mainly a question of concocting a suitable\ncombination of heuristics and engineering high-performance data\nstructures and algorithms such that <em>most<\/em> cases are <em>good enough<\/em>\nwith <em>few enough<\/em> exceptions. As I've found, this rabbithole goes\ninfinitely deep and there is always more to improve, but for now we're\nin a fairly good place.<\/p>\n<p>We <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/3989\">recently switched over to\nregalloc2<\/a>,\nand Cranelift 0.84 and the concurrently released Wasmtime 0.37 use it\nby default. Some\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/3942\">measurements<\/a>\nshow that it generally improves overall compiler speed (which was and\nis dominated by regalloc time) by 20%-ish, and generated code\nperformance improves on register pressure-impacted benchmarks up to\n10-20% in Wasmtime. In Cranelift's use as a backend for rustc via\n<a rel=\"external\" href=\"https:\/\/github.com\/bjorn3\/rustc_codegen_cranelift\">rustc_codegen_cranelift<\/a>\nruntime performance improved by <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/3989#issuecomment-1092110720\">up to\n7%<\/a>. The\nallocator seems to have generally fewer compile-time outliers than our\nprevious allocator, which in many cases is a more important property\nthan 10-20% improvements. Overall, it seems to be a reasonable\nperformance win with few downsides. Of course, this work benefits\nhugely from the lessons learned in developing that prior allocator,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/\">regalloc.rs<\/a>, which\nwas work primarily done by Julian Seward and Benjamin Bouvier; I\nlearned enormous amounts talking to them and watching their work on\nregalloc.rs in 2020, and this work stands on their shoulders as well\nas IonMonkey's.<\/p>\n<p>This post will make a whirlwind tour through several topics. After\nreviewing the register allocation problem and why it is important, we\nwill learn about regalloc2's approach: its abstractions, its key\nfeatures, and how its passes work. We'll then spend a good amount of\ntime on \"lessons learned\": how we attained reasonable performance; how\nwe managed to make anything work at all in reasonable development\ntime; how we migrated a large existing compiler codebase to new\nfoundational types and invariants; and some perspective on ongoing\ntuning and refinements.<\/p>\n<p>A <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/main\/doc\/DESIGN.md\">design\ndocument<\/a>\nfor the allocator exists as well, and this blogpost is meant to be\ncomplementary: we'll walk through some interesting bits of the\narchitecture, but anyone hoping to actually grok the thing in its\nentirety (and please talk to me if this is you!) is advised to dig\ninto the design doc and the source for the full story.<\/p>\n<p>Finally, a fair warning: this post has become a bit of a book chapter;\nif you're looking for a tl;dr, you can skip to the\n<a href=\"https:\/\/cfallin.org\/blog\/2022\/06\/09\/cranelift-regalloc2\/#four-lessons\">Lessons<\/a> section or the <a href=\"https:\/\/cfallin.org\/blog\/2022\/06\/09\/cranelift-regalloc2\/#conclusions\">Conclusions<\/a>.<\/p>\n<h2 id=\"register-allocation-recap\">Register Allocation: Recap<\/h2>\n<p>First, let's recap what register allocation is and why it's\nimportant.<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup><\/p>\n<p>The basic problem at hand is to assign <em>storage locations<\/em> to <em>program\ndataflow<\/em>. We can imagine our compiler input as a graph of operators,\neach of which consumes some values and produces others (let's ignore\ncontrol flow for the moment):<\/p>\n<p><img src=\"\/assets\/cranelift-regalloc2-fig1-web.svg\" alt=\"Figure: A dataflow graph\" \/><\/p>\n<p>Some compilers directly represent the program in this way (called a\n\"sea of nodes\" IR) but most, including Cranelift, <em>linearize<\/em> the\noperators into a particular program order. And in fact, by the time\nthat the register allocator does its work, the \"operators\" are really\nmachine instructions, or something very close to them, so we will call\nthem that: we have a sequence of instructions, and <em>program points<\/em>\nbefore and after each one. Even in this new view, we still have the\ndataflow connectivity that we did above; now, each edge corresponds to\na particular <em>range of program points<\/em>:<\/p>\n<p><img src=\"\/assets\/cranelift-regalloc2-fig2-web.svg\" alt=\"Figure: Dataflow graph with liveranges\" \/><\/p>\n<p>We call each of these dataflow edges, representing a value that must\nflow from one instruction to another, a <em>liverange<\/em>. We say that\nvirtual registers -- the names we give the values before regalloc --\nhave a set of liveranges.<\/p>\n<p>With control flow, liveranges might be discontiguous from a\nlinear-instruction-order point of view, because of jumps; for example:<\/p>\n<p><img src=\"\/assets\/cranelift-regalloc2-fig3-web.svg\" alt=\"Figure: Control flow with discontiguous liveranges\" \/><\/p>\n<p>Each instruction requires its inputs to be in registers and produces\nits outputs in registers, usually.<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup> So, the job of the register\nallocator is to choose one of a finite set of machine registers to\nconvey each of the liveranges from its definition(s) to all of its\nuse(s).<\/p>\n<p>Why is this hard? In brief, because we may not have enough\nregisters. We thus enter a world of <em>compromise<\/em>: if more values are\n\"alive\" (need to be kept around for later use) than the number of\nregisters that the CPU has, then we have to put some of them somewhere\nelse, and bring them back into registers only when we actually need to\nuse them. That \"somewhere else\" is usually memory in the function's\nstack frame that the compiler reserves (a \"stackslot\"), and the\nprocess of saving values away to free up registers is called\n\"spilling\".<\/p>\n<p>One more concept before we go further: we may want to choose to place\na liverange in <em>different<\/em> places throughout its lifetime, depending\non the needs of the instruction sequence at certain points. For\nexample, if a value is produced at the top of a function, then dormant\n(but live) for a while, and then used frequently in a tight loop at\nthe bottom of the function, we don't <em>really<\/em> want to spill it, and\nreload it from memory every loop iteration. In other words, given this\nprogram:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    v0 := ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>    v1 := ...    \/\/ lots of intermediate defs that use all regs<\/span><\/span>\n<span class=\"giallo-l\"><span>    v2 := ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    vN := ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>loop:<\/span><\/span>\n<span class=\"giallo-l\"><span>    v100 := ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    v101 := add v0, v100<\/span><\/span>\n<span class=\"giallo-l\"><span>    store v101, ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    jmp loop<\/span><\/span><\/code><\/pre>\n<p>we ideally do not want to assign a stack slot to the value <code>v0<\/code> and\nthen produce machine code like<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    add rax, rbx      ;; `v0` stored in `rax`<\/span><\/span>\n<span class=\"giallo-l\"><span>    mov [rsp+16], rax ;; spill `v0` to a stack slot<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>loop:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    mov rax, [rsp+16] ;; load `v0` from stack on every iteration -- expensive!<\/span><\/span>\n<span class=\"giallo-l\"><span>    add rcx, rax<\/span><\/span>\n<span class=\"giallo-l\"><span>    mov [...], rcx<\/span><\/span>\n<span class=\"giallo-l\"><span>    jmp loop<\/span><\/span><\/code><\/pre>\n<p>but if we only choose <em>a location<\/em> per liverange, we either choose a\nregister, or a stackslot -- no middle ground. Intuitively, it seems\nlike we should be able to put the value in a different place while it\nis \"dormant\" (spill it to the stack, most likely), then pick an\noptimal location during the tight loop. To do so, we need to refer to\nthe two parts of the liverange separately, and assign each one a\nseparate location. This is called <em>liverange splitting<\/em>.<\/p>\n<p>If we split liveranges, we can then do something like:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    add rax, rbx      ;; `v0` stored in `r0`<\/span><\/span>\n<span class=\"giallo-l\"><span>    mov [rsp+16], rax ;; spill `v0` to a stack slot<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>    mov rdx, [rsp+16] ;; move `v0` from stackslot to a new liverange in `rdx`<\/span><\/span>\n<span class=\"giallo-l\"><span>loop:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    add rcx, rdx      ;; no load within loop<\/span><\/span>\n<span class=\"giallo-l\"><span>    mov [...], rcx<\/span><\/span>\n<span class=\"giallo-l\"><span>    jmp loop<\/span><\/span><\/code><\/pre>\n<p>This seems quite powerful and useful -- so why doesn't every register\nallocator do this? In brief, because it <em>makes the problem much much\nharder<\/em>. When we have a fixed number of liveranges, we have a fixed\namount of work, and we assign a register per liverange, probably in\nsome priority order. And then we are done.<\/p>\n<p>But as soon as we allow for splitting, we can <em>increase<\/em> the amount of\nwork we have almost arbitrarily: we could split every liverange into\nmany tiny pieces, greatly multiplying the cost of register\nallocation. A well-placed split reduces the constraints in the problem\nwe're solving, making it easier, but too many splits just increases\nwork and also the likelihood that we will unnecessarily move values\naround.<\/p>\n<p>Splitting is thus the kind of problem that requires finely-tuned\nheuristics. To be concrete, consider the example above: we showed a\nsplit outside of the tight inner loop. But a naive splitting\nimplementation might just split before the use, putting a move from\nstack to register inside the inner loop. Some sort of cost model is\nnecessary to put splits in \"cheap\" places.<\/p>\n<p>With all of that, hopefully you have some feel for the problem: we\ncompute liveranges, we might split them, and then we choose where to\nput them. That's (almost) it -- modulo many tiny details.<\/p>\n<h2 id=\"regalloc2-s-design\">regalloc2's Design<\/h2>\n<p>At a high level, regalloc2 is a <em>backtracking<\/em> register allocator that\ncomputes precise liveranges, performs <em>merging<\/em> according to some\nheuristics into \"bundles\", and then runs a main loop that assigns\nlocations to bundles, sometimes <em>splitting<\/em> them to make the\nallocation problem easier (or possible at all). Once every part of\nevery liverange has a location, it inserts move instructions to\nconnect all of the pieces.<\/p>\n<p>Let's break that down a bit:<\/p>\n<ul>\n<li>\n<p>regalloc2 starts with <em>precise liveranges<\/em>. These are computed\naccording to the input to the allocator, which is a program that\nrefers to <em>virtual registers<\/em> and may be in\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Static_single_assignment_form\">SSA<\/a>\nform (one definition per register) or non-SSA (multiple definitions\nper register).<\/p>\n<\/li>\n<li>\n<p>It then <em>merges<\/em> these liveranges into larger-than-liverange\n\"bundles\". If done correctly, this reduces work (fewer liverange\nbundles to process) and also gives better code (when merged, it is\nguaranteed that the related pieces will not need a move instruction\nto join them).<\/p>\n<\/li>\n<li>\n<p>It then builds a priority queue of bundles, and processes them until\nevery bundle has a location. (In simplified terms, regalloc2's\nentire job is to \"assign locations to bundles\".) This processing may\ninvolve <em>undoing<\/em>, or <em>backtracking<\/em>, earlier assignments, and may\nalso involve <em>splitting<\/em> bundles into smaller bundles when separate\npieces could attain better allocations.<\/p>\n<\/li>\n<\/ul>\n<p>We'll explain each of these design aspects in turn below.<\/p>\n<h3 id=\"input-instructions-with-operands\">Input: Instructions with Operands<\/h3>\n<p>First, let's talk about the <em>input<\/em> to the register allocator. To\nunderstand how regalloc2 works, we first need to understand how it\nsees the world. (Said another way, before we solve the problem, let's\ndefine it fully!)<\/p>\n<p>regalloc2 processes a \"program\" that consists of instructions that\nrefer to <em>virtual registers<\/em> rather than real machine registers. These\ninstructions are arranged in a <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Control-flow_graph\">control-flow\ngraph<\/a> of basic\nblocks.<sup class=\"footnote-reference\"><a href=\"#4\">4<\/a><\/sup><\/p>\n<p>The most important principle regarding the allocator's view of the\nprogram is: the <em>meaning<\/em> of instructions is mostly\nirrelevant. Instead, the allocator cares mainly how a particular\ninstruction <em>accesses program values<\/em> as registers: <em>which<\/em> values and\n<em>how<\/em> (read or written), and with <em>what constraints<\/em> on\nlocation. Let's look more at the implications of this principle.<\/p>\n<h4 id=\"constraints\">Constraints<\/h4>\n<p>The allocator views the input program as a sequence of instructions\nthat <em>use<\/em> and <em>define<\/em> virtual registers. Every access to a register\nmanaged by the regalloc (an \"allocatable register\") must be via a\nvirtual register.<\/p>\n<p>Already we see a divergence from common ISAs like x86: there are\ninstructions that implicitly access certain registers. (One form of\nthe x86 integer multiply instruction always places its output in <code>rax<\/code>\nand <code>rdx<\/code>, for example.) Since these registers are not mentioned by\nthe instruction explicitly, one might initially think that there is no\nneed to create regalloc operands or use virtual registers for\nthem. But these registers (e.g. <code>rax<\/code> and <code>rdx<\/code>) can also be used by\nexplicit inputs and outputs to instructions; so the regalloc at least\nneeds to know that the registers will be clobbered, and at some later\npoint presumably the results will be read and the registers become\nfree again.<\/p>\n<p>We solve this problem by allowing <em>constraints<\/em> on operands. An\ninstruction that always reads or writes a specific physical register\nstill names a virtual-register operand. The only difference from an\nordinary instruction that can use any register is that this operand is\n<em>constrained to a particular register<\/em>. This lets the allocator\nuniformly reason about virtual registers allocated to physical\nregisters as the basic way that space is reserved; the constraint\nbecomes only a detail of the allocation process.<\/p>\n<p>Let's take an x86 instruction <code>mul<\/code> (integer multiply) as an example\nto see how this works. Ordinarily, one would write the following in\nassembly:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    ;; Multiplicand is implicitly in rax.<\/span><\/span>\n<span class=\"giallo-l\"><span>    mul rcx  ; multiply by rcx<\/span><\/span>\n<span class=\"giallo-l\"><span>    ;; 128-bit wide result is implicitly placed rdx (high 64 bits)<\/span><\/span>\n<span class=\"giallo-l\"><span>    ;; and rax (low 64 bits).<\/span><\/span><\/code><\/pre>\n<p>The instruction <code>mul rcx<\/code> does not tell the whole story from\nregalloc2's point of view, so we would instead present an instruction\nlike so to the register allocator, with constraints annotating\nuses\/definitions of virtual registers:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    ;; Put inputs in v0 and v1.<\/span><\/span>\n<span class=\"giallo-l\"><span>    mul v0 [use, fixed rax], v1 [use, any reg], v2 [def, fixed rax], v3 [def, fixed rdx]<\/span><\/span>\n<span class=\"giallo-l\"><span>    ;; Use results in v2 and v3.<\/span><\/span><\/code><\/pre>\n<p>The allocator will \"do the right thing\" by either inserting moves or\ngenerating inputs directly into, and using outputs directly from, the\nappropriate registers. The advantage of this scheme is that aside from\nthe constraints, it makes <code>mul<\/code> behave like any other instruction: it\nisolates complexity in one place and presents a more uniform,\neasier-to-use abstraction for the rest of the compiler.<\/p>\n<h4 id=\"modify-operands-and-reused-input-constraints\">\"Modify\" Operands and Reused-Input Constraints<\/h4>\n<p>The next difference we might observe between a real ISA like x86 and a\ncompiler's view of the world is: an operator in a compiler IR usually\nproduces its result as a <em>completely new value<\/em>, but real machine\ninstructions often <em>modify existing values<\/em>.<\/p>\n<p>For example, on x86, arithmetic operations are written in <em>two-operand\nform<\/em>. They look like <code>add rax, rbx<\/code>, which means: compute the sum of\n<code>rax<\/code> and <code>rbx<\/code>, and store the result in <code>rax<\/code>, overwriting that input\nvalue.<\/p>\n<p>The register allocator reasons about segments of value dataflow from\ndefinitions (defs) to uses; but the use of <code>rax<\/code> in this example seems\nto be neither. Or rather, it is both: it consumes a value in <code>rax<\/code>,\nand it produces a value in <code>rax<\/code>.<\/p>\n<p>But we can't decompose it into a separate use and def either, because\nthen the allocator might choose different locations for each. The\nencoding of <code>add rax, rbx<\/code> only has slots for two register names: the\ninput in <code>rax<\/code> and output in <code>rax<\/code> must be in the same register!<\/p>\n<p>We solve this by introducing a new kind of constraint: the \"reused\ninput register\" constraint.<sup class=\"footnote-reference\"><a href=\"#5\">5<\/a><\/sup> At the regalloc level, we say that the\n<code>add<\/code> above has <em>three<\/em> operands: two inputs (uses) and an output (a\ndef). It exactly corresponds to the compiler IR-level operator, with\nnicely separated values in different virtual registers. But, we\nconstrain the output by indicating that it <em>must be placed in the same\nregister as the input<\/em>. We can assert that this is the case when we\nget final assignments from the regalloc, then emit that register\nnumber into the \"first source and also destination\" slot of the\ninstruction.<sup class=\"footnote-reference\"><a href=\"#6\">6<\/a><\/sup><\/p>\n<p>So, instead of <code>add rax, rbx<\/code> (or <code>add v0, v1<\/code> with <code>v0<\/code> a \"modify\"\noperand), we can present the following 3-operand instruction to the\nregister allocator:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    add v0 [use, any reg], v1 [use, any reg], v2 [def, reuse-input(0)]<\/span><\/span><\/code><\/pre>\n<p>This corresponds more closely to what the compiler IR describes,\nnamely a new value for the result of the add and non-destructive uses\nof both operands. All of the complexity of saving the destructive\nsource if needed is pushed to the allocator itself.<\/p>\n<h4 id=\"program-points-early-and-late-operands\">Program Points, \"Early\" and \"Late\" Operands<\/h4>\n<p>Finally, we need to go a bit deeper on what exactly it means to\nallocate a register \"at\" an instruction. To see why there may be some\nsubtlety, let's consider an example. Take the instruction <code>movzx<\/code> on\nx86: this instruction does a 16-to-64-bit zero-extend, with one input\nand one output. In pre-regalloc form with virtual registers, we could\nwrite <code>movzx v1, v0<\/code>, reading an input in <code>v0<\/code> and putting the output\nin <code>v1<\/code>.<\/p>\n<p>An intuitive understanding of liveranges and the allocation problem\nmight lead us to reason: both <code>v0<\/code> and <code>v1<\/code> are \"live\" at this\ninstruction, so they overlap, and have to be placed in different\nregisters.<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>                          v0    v1<\/span><\/span>\n<span class=\"giallo-l\"><span>                           :<\/span><\/span>\n<span class=\"giallo-l\"><span>    v1 := movzx v0         |     |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                 |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                 :<\/span><\/span><\/code><\/pre>\n<p>But an experienced assembly programmer, knowing that <code>v0<\/code> is not used\nagain after this instruction, might reuse its register for the\noutput. So for example, if it were initially in <code>r13<\/code>, one might write\n<code>movzx r13, r13w<\/code> (the <code>r13w<\/code> is x86's archaic way of writing \"the 16\nbit version of <code>r13<\/code>\").<\/p>\n<p>But isn't this an invalid assignment, because we have put two\nliveranges in the same register <code>r13<\/code> when they are both \"live\" at\nthis particular instruction?<\/p>\n<p>It turns out that this will work fine, for a subtle reason: generally\ninstructions read all of their inputs, <em>then<\/em> write all of their\noutputs. In other words, there is a sort of two-phase semantics to\nmost instructions. So we could say that the input <code>v0<\/code> is live up to,\nand including, the \"read\" or \"early\" phase of this instruction, and\nthe output <code>v1<\/code> is live starting at the \"write\" or \"late\" phase of\nthis instruction. These two liveranges don't conflict at all! So the\nabove figure showing liveranges overlapping becomes:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>                          v0    v1<\/span><\/span>\n<span class=\"giallo-l\"><span>                           :<\/span><\/span>\n<span class=\"giallo-l\"><span>                   EARLY   |<\/span><\/span>\n<span class=\"giallo-l\"><span>    v1 := movzx v0               <\/span><\/span>\n<span class=\"giallo-l\"><span>                   LATE         |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                :<\/span><\/span><\/code><\/pre>\n<p>regalloc2 (along with most other register allocators) thus has a\nnotion of \"when\" an operand occurs -- the \"operand position\" -- and it\ncalls these two points in an instruction <code>Early<\/code> and <code>Late<\/code>. Along\nwith this, throughout the allocator, we name program points (distinct\npoints at which allocations are made) as <code>Before<\/code> or <code>After<\/code> a given\ninstruction.<\/p>\n<p>One final bit of subtlety: when a single instruction from a regalloc\npoint of view actually emits multiple instructions at the machine\nlevel, sometimes the usual phasing of reads and writes breaks\ndown. For example, maybe a pseudoinstruction becomes a sequence that\nstarts to write outputs before it has read all of its inputs. In such\na case, reusing one of the inputs (which is no longer live at <code>Late<\/code>)\nas an output register could be catastrophic. For this reason,\nregalloc2 <em>decouples<\/em> an operand's position from its kind (use or\ndef). One could have an \"early def\" or a \"late use\". Temporary\nregisters are also possible: these are live during both early and late\npoints on an instruction so they do not conflict with any input or\noutput, and can be used in sequences emitted from one\npseudoinstruction.<\/p>\n<h4 id=\"regalloc2-s-view-of-operands\">regalloc2's View of Operands<\/h4>\n<p>To summarize, each instruction can have a sequence of \"operands\", each\nof which:<\/p>\n<ul>\n<li>Names a <em>virtual register<\/em> that corresponds to a value in the\noriginal program;<\/li>\n<li>Indicates whether this value is read (\"used\") or written\n(\"defined\"),<\/li>\n<li>Indicates when during the instruction execution the value is\naccessed (\"early\", before the instruction executes; or \"late\", after\nit does);<\/li>\n<li>Indicates where the value should be placed: in a machine register of\na certain kind, or a specific machine register, or in the same\nregister that another operand took, or in a slot in the stack frame.<\/li>\n<\/ul>\n<h3 id=\"stage-1-live-ranges\">Stage 1: Live Ranges<\/h3>\n<p>We've described what the register allocator expects as its input. Now\nlet's talk about how the input is processed into an \"allocation\nproblem\" that can be solved by the main algorithm.<\/p>\n<p>The input is described as a graph of blocks of instructions, with\noperands; but most of the allocator works in terms of <em>liveranges<\/em> and\n<em>bundles of liveranges<\/em> instead.<\/p>\n<p>In brief, a <em>liverange<\/em> (originally \"live range\", but we say it so\noften it has become one word!) is a span of program points -- that is,\na range of \"before\" and \"after\" points on instructions -- that\nconnects a program value in a virtual register from a definition to\none or more uses. A liverange represents one unit of needed storage,\neither as a register or a slot in the stackframe.<\/p>\n<p>The basic strategy of regalloc2 is to reduce the input into liveranges\nas soon as possible, and then operate mostly on liveranges,\ntranslating back to program terms (inserted moves and assigned\nregisters per instruction) only at the very end of the process. This\nlets us reason about a simpler \"core problem\" that is actually quite\nconcisely specified:<\/p>\n<ul>\n<li>A liverange is a span of program points, which can be numbered\nconsecutively;<\/li>\n<li>A liverange has constraints at certain points that arise from\nprogram uses\/defs;<\/li>\n<li>We must assign locations to liveranges, such that:\n<ul>\n<li>At any point, at most one liverange lives in a given location;<\/li>\n<li>At all points, a liverange's constraints are satisfied;<\/li>\n<\/ul>\n<\/li>\n<li>We are allowed to split a liverange into pieces and assign different\nlocations to each piece. However, moves between pieces have a cost,\nand we must minimize cost.<\/li>\n<\/ul>\n<p>And that's it! No need to reason about ISA specifics, or the way that\nregalloc2 generates moves, or anything else. We'll worry about\ngenerating moves to \"reify\" (make real) the assignments later. For\nnow, we just need to slot the liveranges into locations and avoid\nconflicts.<\/p>\n<h4 id=\"computing-liveness\">Computing Liveness<\/h4>\n<p>To build up our set of liveranges, we first need to compute\n<em>liveness<\/em>. This is a property of any particular virtual register at a\nprogram point indicating that it has a value that will eventually be\nused.<\/p>\n<p><a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Live_variable_analysis\">Liveness\nanalysis<\/a> is an\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Data-flow_analysis\">iterative dataflow\nanalysis<\/a> that is\ncomputed in the backward direction: any use of a virtual register\npropagates liveness backward (\"upward\" in the program), and a\ndefinition of that virtual register's value ends the liveness (when\nscanning upward), because the old value (from above) is no longer\nrelevant.<\/p>\n<p>Thus the first thing that regalloc2 does with the input program is to\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/33611a68b90e40869bba52934449315a8f4e5477\/src\/ion\/liveranges.rs#L318-L319\">run a worklist algorithm to compute precise\nliveness<\/a>. This\nproduces a bitset<sup class=\"footnote-reference\"><a href=\"#7\">7<\/a><\/sup> that, at each basic block entry and exit, gives\nus the set of live virtual registers.<\/p>\n<p>Once we know which registers are live into and out of each basic\nblock, we can <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/33611a68b90e40869bba52934449315a8f4e5477\/src\/ion\/liveranges.rs#L413-L419\">perform block-local\nprocessing<\/a>\nto compute actual liveranges with each use of the register properly\nnoted. This is another backward scan, but this time we <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/33611a68b90e40869bba52934449315a8f4e5477\/src\/ion\/liveranges.rs#L176-L195\">build the data\nstructures<\/a>\nwe'll use for the rest of the allocation program.<\/p>\n<h4 id=\"normalization-and-saving-fixups-for-later\">Normalization, and Saving Fixups for Later<\/h4>\n<p>We mentioned above that one way to see the liverange-building step is\nas a simplification of the problem to its core essence, in order to\nmore easily solve it. \"Ranges that may overlap\" is certainly simpler\nthan \"instructions that access registers with certain\nsemantics\". However, even the constraints on the liveranges can be\nmade simpler in several ways.<\/p>\n<p>A good example of a complex set of constraints is the following:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    inst v0 [use, fixed r0], v0 [use, fixed r1], v1 [def, any reg]<\/span><\/span><\/code><\/pre>\n<p>This is an instruction that has two inputs, and takes the inputs in\nfixed physical registers <code>r0<\/code> and <code>r1<\/code>. This is completely reasonable\nand such instructions exist in real ISAs (see, e.g., x86's integer\ndivide instruction, with inputs in <code>rdx<\/code> and <code>rax<\/code>, or a call with an\nABI that passes arguments in fixed registers). If the two inputs\nhappen to be given the same program value, here virtual register <code>v0<\/code>,\nthen we have created an impossible constraint: we require <code>v0<\/code> to be\nin <em>both<\/em> <code>r0<\/code> and <code>r1<\/code> at the same time.<\/p>\n<p>As we have formulated the problem, a liverange is in only one place at\na time; and in fact this is a very useful simplifying invariant, and a\nsimpler model than \"there are N copies of the virtual register at\nonce\" (which one(s) are up-to-date, if we allow multiple defs?).<\/p>\n<p>We can \"simplify to a previously solved problem\" in this case with a\nneat trick: we keep a side-list of \"fixup moves\" to add back in, after\nwe complete allocation, and we insert such a fixup move from <code>r0<\/code> to\n<code>r1<\/code> just before this instruction. Then we <em>delete the constraint<\/em> on\nthe second operand that uses <code>v0<\/code>. The rest of the allocation will\nproceed as if <code>v0<\/code> were only required in <code>r0<\/code>; it will end up in that\nlocation; and the fixup move will copy it to <code>r1<\/code> as well.<\/p>\n<p>We perform a similar rewrite for reused-input constraints. These seem\nas if they would be fairly fundamental to the core allocation loop,\nbecause they tie one decision to another; now we have to deal with\ndependent allocation decisions. But we can do a simpler thing: we\n<em>edit the liveranges<\/em> so that (i) the output that reuses the input has\na liverange that starts at the <em>early<\/em> (input) phase, and (ii) the\ninput has a liverange that ends just before the instruction, not\noverlapping. (In other words, we shift both back by one program\npoint.) Then we insert a fixup move from input to output. The figure\nbelow illustrates this rewrite.<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>      INITIAL<\/span><\/span>\n<span class=\"giallo-l\"><span>                         v0   v1    v2<\/span><\/span>\n<span class=\"giallo-l\"><span>                         :    :<\/span><\/span>\n<span class=\"giallo-l\"><span>                         |    |<\/span><\/span>\n<span class=\"giallo-l\"><span>                  EARLY use  use<\/span><\/span>\n<span class=\"giallo-l\"><span>      add v2, v0, v1<\/span><\/span>\n<span class=\"giallo-l\"><span>                  LATE             def reuse(0)<\/span><\/span>\n<span class=\"giallo-l\"><span>                                     |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                     :<\/span><\/span>\n<span class=\"giallo-l\"><span>                                     <\/span><\/span>\n<span class=\"giallo-l\"><span>      REWRITTEN<\/span><\/span>\n<span class=\"giallo-l\"><span>                         v0   v1    v2<\/span><\/span>\n<span class=\"giallo-l\"><span>                         :    :<\/span><\/span>\n<span class=\"giallo-l\"><span>                         |    |<\/span><\/span>\n<span class=\"giallo-l\"><span>                         |    |<\/span><\/span>\n<span class=\"giallo-l\"><span>                  LATE   |    |<\/span><\/span>\n<span class=\"giallo-l\"><span>                              |    (implicit copy: v2 := v0)<\/span><\/span>\n<span class=\"giallo-l\"><span>                  EARLY      use   def<\/span><\/span>\n<span class=\"giallo-l\"><span>      add v2, v0, v1                 |<\/span><\/span>\n<span class=\"giallo-l\"><span>                  LATE               |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                     |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                     :<\/span><\/span><\/code><\/pre>\n<p>One may object that this pessimizes all reused-input allocations --\nhaven't we removed all knowledge of the constraint, so we will almost\nalways get different registers at input and output, and cause many new\nmoves to be inserted? The answer to this issue comes in the <em>bundle\nmerging<\/em>, which we discuss below (basically, we try to rejoin the two\nparts if no overlap would result).<\/p>\n<p>In general, this is a powerful technique: whenever some complexity\narises from a constraint or feature, it is best if the complexity can\nbe kept as close to the <em>outer boundary<\/em> of the system as\npossible. Rewrites or lowerings into a simpler \"core form\" are common\nin compilers, and it so happens that considering regalloc constraints\nin this light is useful too.<sup class=\"footnote-reference\"><a href=\"#8\">8<\/a><\/sup><\/p>\n<h3 id=\"step-2-bundles-and-merging\">Step 2: Bundles and Merging<\/h3>\n<p>Once we have created a list of liveranges with constraints, we could\nin theory begin to assign locations right away, finding available\nlocations that fulfill constraints and splitting where necessary to do\nso. However, such an approach would almost certainly run more slowly,\nand produce worse code, than most state-of-the-art allocators\ntoday. Why is that?<\/p>\n<p>A key observation about liveranges in real programs is that there are\n<em>clusters of related liveranges connected by moves<\/em>. Several examples\nare the liveranges on either side of an SSA block parameter (or\nphi-node), or on either side of a move instruction, or the input and\nreused-register-constrained output of an instruction.<sup class=\"footnote-reference\"><a href=\"#9\">9<\/a><\/sup> These\nliveranges often would benefit if they were in the same register: in\nall three cases, it would mean one fewer move instruction in the final\nprogram.<\/p>\n<p>Processing such related liveranges together, as one unit of\nallocation, would guarantee that they would be assigned the same\nlocation. (If impossible, the merged liveranges could always be split\nagain.) Attaining this result some other way would require reasoning\nabout \"affinity\" for locations between related liveranges, which is a\nmuch more complex question.<\/p>\n<p>Furthermore, processing multiple liveranges together brings all the\nusual efficiency benefits of batching: the more progress we can make\nwith a single decision, the faster the register allocator runs.<\/p>\n<p>We thus define a \"bundle\" of liveranges as the unit of\nallocation. After computing liveranges in the initial input program\nscan, we merge liveranges into bundles according to a few simple\nheuristics: across SSA block parameters, across move instructions, and\nfrom inputs to outputs of instructions with \"reused-input\" constraints.<\/p>\n<p>The one key invariant is: all liveranges in a bundle <em>must not\noverlap<\/em>. We greedily grow a bundle with the above heuristics, testing\nat each step whether another liverange can join.<\/p>\n<p>Beyond this point in the allocation process, we will reason about\nbundles: we enqueue them in the priority workqueue, we process them\none at a time and assign locations or split. At the end of the\nprocess, we'll scan the liveranges in the bundle and assign each the\nlocation that the bundle received.<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    CORE ALLOCATION PROBLEM:<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>         bundle0              bundle1               bundle2          bundle3<\/span><\/span>\n<span class=\"giallo-l\"><span>           |<\/span><\/span>\n<span class=\"giallo-l\"><span>           |                    |                                       |<\/span><\/span>\n<span class=\"giallo-l\"><span>           |                    |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                |                     |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                |                     |<\/span><\/span>\n<span class=\"giallo-l\"><span>           |<\/span><\/span>\n<span class=\"giallo-l\"><span>           |                                                            |<\/span><\/span>\n<span class=\"giallo-l\"><span>           <\/span><\/span>\n<span class=\"giallo-l\"><span>    ==&gt;<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>           bundle0: r0<\/span><\/span>\n<span class=\"giallo-l\"><span>           bundle1: r1<\/span><\/span>\n<span class=\"giallo-l\"><span>           bundle2: r0<\/span><\/span>\n<span class=\"giallo-l\"><span>           bundle3: r2<\/span><\/span><\/code><\/pre><h3 id=\"step-3-assignment-loop-and-splitting-heuristics\">Step 3: Assignment Loop and Splitting Heuristics<\/h3>\n<p>The heart of the allocator is the main loop that <em>allocates locations\nto bundles<\/em>. This is at least conceptually simple: pull a bundle off\nof a queue, \"probe\" potential locations one at a time to see if it\nwill fit (has no overlap with points in time for which that location\nis already reserved), assign it the first place it fits. But there is\nsignificant complexity in the details, as always.<\/p>\n<p>The key data structures are: (i) an \"allocation map\" for each physical\nregister, kept as a BTree for fast lookups, that indicates whether the\nregister is free or occupied at any program point and the liverange\nthat occupies it; and (ii) a queue of bundles to process. (The <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/main\/doc\/DESIGN.md\">design\ndocument<\/a>\ndescribes several others, such as the second-chance allocation queue\nand the structures used for stackslots, which we skip here for\nsimplicity.)<\/p>\n<p>The core part of the allocator's processing occurs here: we pull one\nbundle at a time from the queue and attempt to place it in one of the\nregisters (again we're ignoring stackslot constraints for simplicity).<\/p>\n<p>For each bundle, we can perform one of the following actions:<\/p>\n<ul>\n<li>\n<p>If we find a register with no overlapping allocations already in\nplace, we can allocate the bundle to the register; then we're done!\nThis is the best case.<\/p>\n<\/li>\n<li>\n<p>Otherwise, we can pick a register where some bundles with a lower\n\"spill cost\" (determined as a sum of some heuristic values for each\nuse of a liverange in a bundle) and <em>evict<\/em> those already-allocated\nbundles, punting them back to the queue, then put our present bundle\nin this register instead. We do this only if the present bundle has\na higher spill cost.<\/p>\n<\/li>\n<li>\n<p>If this is also not an option, we can split our present bundle into\npieces and try again. Heuristically, we find it works well to split\nat the first conflict point; in other words, allocate as much as\nwould have fit in any register, and then put the remainder back in\nthe queue.<\/p>\n<\/li>\n<\/ul>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>   TO ALLOCATE:                  GIVEN:<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>       bundle0                     r0     r1    r2       r3<\/span><\/span>\n<span class=\"giallo-l\"><span>         |                          |b1          |b4<\/span><\/span>\n<span class=\"giallo-l\"><span>         |                          |            |<\/span><\/span>\n<span class=\"giallo-l\"><span>         |                                       |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |b2          |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |            |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                                 |<\/span><\/span>\n<span class=\"giallo-l\"><span>         |                               |b3     |<\/span><\/span>\n<span class=\"giallo-l\"><span>         |                               |       |<\/span><\/span>\n<span class=\"giallo-l\"><span>         <\/span><\/span>\n<span class=\"giallo-l\"><span>         <\/span><\/span>\n<span class=\"giallo-l\"><span>    OPTION 1: Take a free register (r3)<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>        - Possible if no overlap. Easiest option!<\/span><\/span>\n<span class=\"giallo-l\"><span>        <\/span><\/span>\n<span class=\"giallo-l\"><span>    OPTION 2: Evict, if bundle0&#39;s spill cost is higher than evicted bundles<\/span><\/span>\n<span class=\"giallo-l\"><span>              and if no completely free register exists:<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>       bundle1  bundle2            r0     r1    r2<\/span><\/span>\n<span class=\"giallo-l\"><span>         |                          |b0          |b4<\/span><\/span>\n<span class=\"giallo-l\"><span>         |                          |            |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |            |<\/span><\/span>\n<span class=\"giallo-l\"><span>                 |                               |<\/span><\/span>\n<span class=\"giallo-l\"><span>                 |                               |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                                 |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |b0  |b3     |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |    |       |<\/span><\/span>\n<span class=\"giallo-l\"><span>                 <\/span><\/span>\n<span class=\"giallo-l\"><span>        (b1 and b2 are re-enqueued)<\/span><\/span>\n<span class=\"giallo-l\"><span>         <\/span><\/span>\n<span class=\"giallo-l\"><span>    OPTION 3: Split!<\/span><\/span>\n<span class=\"giallo-l\"><span>                                      <\/span><\/span>\n<span class=\"giallo-l\"><span>                                       <\/span><\/span>\n<span class=\"giallo-l\"><span>                                   r0     r1    r2 <\/span><\/span>\n<span class=\"giallo-l\"><span>                              --&gt;   |b1   |b0    |b4<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |     |      |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                          |      |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |b2          |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |            |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                                 |<\/span><\/span>\n<span class=\"giallo-l\"><span>                              --&gt;   |b0  |b3     |<\/span><\/span>\n<span class=\"giallo-l\"><span>                                    |    |       |<\/span><\/span><\/code><\/pre>\n<p>The presence of <em>eviction<\/em> as an option is what makes regalloc2 a\n<em>backtracking<\/em> allocator. It's not clear why the allocator should\nalways finish its job, if it is allowed to undo work. In fact <em>many<\/em>\nbundles may be evicted in order to place just <em>one<\/em> bundle instead --\nisn't this backward progress?<\/p>\n<p>The key to maintaining forward progress is that we <em>only evict bundles\nof lower spill weight<\/em>, together with the fact that <em>spill weight\nmonotonically decreases when splitting<\/em>. Eventually, if bad luck\ncontinues far enough, a bundle will be split into individual pieces\naround each use, and these can always be allocated because (if the\ninput program does not have fundamentally conflicting constraints on\none instruction) these single-use bundles have the lowest possible\nspill weight.<\/p>\n<h3 id=\"step-4-move-handling\">Step 4: Move Handling<\/h3>\n<p>Finally, once we have a series of locations assigned to each bundle,\nwe have \"solved the problem\", but... we still need to convey our\nsolution back to the real world, where a compiler is waiting for us to\nprovide a series of move, load, and store instructions to place values\ninto the right spots.<\/p>\n<p>We split the overall problem into two pieces for the usual simplicity\nreasons: first, we allow ourselves to cut liveranges into as many\npieces as needed, and put each piece in a different place, at a single\ninstruction granularity. We assume that we can edit the program\nsomehow to connect these pieces back up. That allowed the above\nliverange\/bundle processing to become a tractable problem for a solver\ncore to handle. Now, need to connect those liverange fragments. This\nis the second half of the problem: generating moves.<\/p>\n<h4 id=\"all-in-one-liverange-connectors-program-moves-and-edge-moves\">All-in-One: Liverange Connectors, Program Moves, and Edge Moves<\/h4>\n<p>The abstract model for the input to this stage of the allocator is\nthat between each pair of instructions, we perform some <em>arbitrary\npermutation<\/em> of liveranges in locations. One way to see this\npermutation is as a <em>parallel move<\/em>: a data-movement action that reads\nvalues in all of their old locations (inputs of the permutation), then\nin parallel, writes the values to all of their new locations (outputs\nof the permutation).<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>        EARLY<\/span><\/span>\n<span class=\"giallo-l\"><span>    inst1      r2, r0, r1<\/span><\/span>\n<span class=\"giallo-l\"><span>        LATE<\/span><\/span>\n<span class=\"giallo-l\"><span>        <\/span><\/span>\n<span class=\"giallo-l\"><span>          { r4 := r0 }              &lt;--- regalloc-inserted moves<\/span><\/span>\n<span class=\"giallo-l\"><span>        <\/span><\/span>\n<span class=\"giallo-l\"><span>        EARLY<\/span><\/span>\n<span class=\"giallo-l\"><span>    inst2      r0, r2, r3<\/span><\/span>\n<span class=\"giallo-l\"><span>        LATE<\/span><\/span>\n<span class=\"giallo-l\"><span>        <\/span><\/span>\n<span class=\"giallo-l\"><span>          { r6 := r5, r5 := r6 }    &lt;--- multiple moves in parallel!<\/span><\/span>\n<span class=\"giallo-l\"><span>                                         (arbitrary permutations)<\/span><\/span>\n<span class=\"giallo-l\"><span>          <\/span><\/span>\n<span class=\"giallo-l\"><span>        EARLY<\/span><\/span>\n<span class=\"giallo-l\"><span>    inst3      r5, r4, r2<\/span><\/span>\n<span class=\"giallo-l\"><span>        LATE<\/span><\/span><\/code><\/pre>\n<p>This is why we make a distinction between the \"After\" point of\ninstruction <em>i<\/em> and the \"Before\" point of instruction <em>i+1<\/em>, though a\ntraditional compiler textbook would tell you that there is only one\nprogram point between a pair of instructions. We have two, and between\nthese two program points lies the parallel move.<sup class=\"footnote-reference\"><a href=\"#10\">10<\/a><\/sup><\/p>\n<p>The process for generating these moves is: we scan liveranges, finding\npoints at which they have been split into pieces where the value must\nflow from one piece to the next. We also account for CFG edges and\nblock parameters at this point, as well as for move instructions in\nthe input program. Once we have accumulated the set of moves that must\nhappen, in parallel, at a given priority at a given location, we\nresolve these into a sequence of individual move\/load\/store\ninstructions using the algorithm we describe in the next section.<\/p>\n<p>One thing to note about this design is that we are handling <em>all<\/em>\nvalue movement in the program with a single resolution mechanism:\nregalloc-induced movement but also movement that was present in the\noriginal program. This is valuable because it allows the moves to be\nhandled more efficiently. In contrast, we have observed issues in the\npast in allocators that lower moves in stages -- e.g., SSA block\nparameters to moves prior to regalloc, then regalloc-induced moves\nduring regalloc -- where chains of moves occur because each level of\nabstraction is not aware of what other levels below or above it are\ndoing.<\/p>\n<h4 id=\"parallel-move-resolution\">Parallel Move Resolution<\/h4>\n<p>The actual problem of resolving a permutation such as:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    { r0 := r1 ; r1 := r2 ; r2 := r0 }<\/span><\/span><\/code><\/pre>\n<p>into a sequence of moves<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    scratch := r0<\/span><\/span>\n<span class=\"giallo-l\"><span>    r0 := r1<\/span><\/span>\n<span class=\"giallo-l\"><span>    r1 := r2<\/span><\/span>\n<span class=\"giallo-l\"><span>    r2 := scratch<\/span><\/span><\/code><\/pre>\n<p>is a well-studied one, and is known as the \"parallel moves\nproblem\". The crux of the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/moves.rs\">solution<\/a>\nis to understand the permutation as a kind of dependency graph, and\nsort its moves so that we pull an old value out of a given register\nbefore overwriting it. When we encounter a cycle, we can use a scratch\nregister as above.<\/p>\n<p>One might think that something like <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Tarjan%27s_strongly_connected_components_algorithm\">Tarjan's\nalgorithm<\/a>\nfor finding strongly-connected components is needed, but in fact there\nis a nice property of the problem that greatly simplifies it. Because\nany valid permutation has at most one writer for any given register,\nwe can <em>only have simple cycles<\/em> of moves, with other uses of old\nvalues in the cycle handled before realizing the cyclic move. Some\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/moves.rs#L92-L103\">more\ndescription<\/a>\nis available in our implementation. In fact, this is such a nice\nobservation that we later discovered <a rel=\"external\" href=\"https:\/\/hal.inria.fr\/inria-00289709\/document\">a\npaper<\/a> by Rideau et\nal. that names the resulting dependency graphs \"windmills\" for their\nshape (see figure below -- there can be a simple cycle in the middle,\nand only acyclic outward moves from cycle elements in a tree of\noutward shifts) and, delightfully, describes more or less the same\nalgorithm to \"tilt at windmills\" and resolve the moves.<\/p>\n<p><img src=\"\/assets\/cranelift-regalloc2-fig4.svg\" alt=\"Figure: &quot;Windmills&quot; in a register movement graph\" \/><\/p>\n<h4 id=\"scratch-registers-and-cycles\">Scratch Registers and Cycles<\/h4>\n<p>The above algorithm works, but has one serious drawback: it requires a\nscratch register whenever we have a cyclic move. The simplest approach\nto this requirement is to set aside one register permanently (or\nactually, one per \"register class\": e.g., an integer register and a\nfloat\/vector register). Especially on ISAs with relatively few\nregisters, like x86-64 with 16 each of integer and float registers,\nthis can impact performance by increasing register pressure and\nforcing more spills.<\/p>\n<p>We thus came up with a\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/51\">scheme<\/a> to\nallow use of all registers but still find a scratch when needed for a\ncyclic move. The approach begins with an idea <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/de15f9c109f9c474d00faf8032f559c236067c06\/js\/src\/jit\/BacktrackingAllocator.cpp#2582\">borrowed from\nIonMonkey<\/a>,\nnamely to look for a free register to use as a scratch by actually\nprobing the allocation maps. This often works: the need for a cyclic\nmove doesn't necessarily imply that we will have high register\npressure, and so there are often plenty of free registers available.<\/p>\n<p>What if that doesn't work, though? In the above PR, we take another\nseemingly-simplistic approach: we use a stackslot as the scratch\ninstead! This means that we will resolve the cyclic move into a\nsequence including stores and loads, but this is fine, because we're\nalready in a situation where all registers are full and we need to\nspill <em>something<\/em>.<\/p>\n<p>We're not quite done, though: there is another very important use of\nthe scratch register in a simplistic design, namely to resolve\nmemory-to-memory moves! This arises because our move resolution\nhandles both registers and stackslots in a uniform way, so some cycle\nelements may be stackslots (memory locations). Using a stackslot as a\nscratch above just compounds the problem. So we translate, in a\nseparate second phase, memory-to-memory moves into a <em>pair<\/em> of a load\n(from memory into scratch) and a store (from scratch into memory).<\/p>\n<p>So to recap, we may find a cyclic move permutation to be necessary,\nand no registers to be free to use as scratch; so we use a stackslot\ninstead. But some of the original move cycle may have been between\nstackslots, so we need <em>another<\/em> scratch to do make these\nstackslot-to-stackslot moves possible. But we're already out of\nscratch registers!<\/p>\n<p>The solution to this last issue is that we can do a last-ditch\nemergency spill of <em>any<\/em> register, just for the duration of one\nmove. So we pick a \"victim\" register of the right kind (integer or\nfloat), spill it to a second stackslot, use this victim register for a\nmemory-to-memory move (a load and store pair), then reload the victim.<\/p>\n<p>This cascading series of solutions, each a little more complex but a\nlittle rarer, is an example of a complexity-for-performance\ntradeoff. Overall, it is far better to allow the program to use all\nregisters; this will reduce spills. And most parallel moves are <em>not<\/em>\ncyclic, so scratch registers are rarely needed anyway. And when a\ncyclic move <em>is<\/em> needed, we often have a free register, because this\ncondition is mostly orthogonal to high register pressure. It is only\nwhen all of the bad cases line up -- cycle, no free registers, and\nmemory-to-memory moves -- that we reach for the highest-cost approach\n(decomposing one move into four), and so the most important aspect of\nthis fallback is not that it is fast but that it is correct and can\nhandle all cases.<\/p>\n<h3 id=\"everything-else\">Everything Else<\/h3>\n<p>This has been a not-so-whirlwind tour of the allocator pipeline in\nregalloc2, but despite my longwindedness, we had to skip many details!\nFor example, the way in which stackslots are allocated for spilled\nvalues, the way in which split pieces of a single original bundle\nshare a single spill location (\"spill bundles\"), the way in which we\nclean up after move insertion with Redundant Move Elimination (a sort\nof abstract interpretation that tracks symbolic locations of values),\nand more, are skipped here but are all described in the design\ndocument. One could truly write a book on the engineering of a\nregister allocator, but the above will have to suffice; now, we must\nmove on and draw some lessons!<\/p>\n<h2 id=\"four-lessons\">Four Lessons<\/h2>\n<h3 id=\"performance\">Performance<\/h3>\n<h4 id=\"cache-locality-and-scans\">Cache Locality and Scans<\/h4>\n<p>One enduring theme in the regalloc2 architecture is <em>data structure\ndesign for performance<\/em>. As I began the project by transliterating\nIonMonkey code, building Rust equivalents to the data structures in\nthe original C++, I found several things:<\/p>\n<ul>\n<li>\n<p>The original data structures were heavily <em>pointer-linked<\/em>. For\nexample, liveranges within bundles and uses within liveranges were\nkept as linked lists, to allow for fast insertion and removal in the\nmiddle, and splicing. A linked list is the classical CS answer to\nthese requirements.<\/p>\n<\/li>\n<li>\n<p>There were quite a few linear-time queries of these data\nstructures. For example, when generating moves between liveranges of\na virtual register, a scan would traverse the linked list of these\nliveranges, observe the range covering one end of a control-flow\ntransition, and do a <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/7751fef9eeb3db0a07ae4680daa2a62bd8f49882\/js\/src\/jit\/BacktrackingAllocator.cpp#2196\"><em>linear-time\nscan<\/em><\/a>\n(through the linked list) for the liverange at the other end!<\/p>\n<\/li>\n<\/ul>\n<p>These two design trends combine to make CPU caches exceptionally\nunhappy. First there is the algorithmic inefficiency, then there is\nthe cache-unfriendly demand access to random liveranges, each of which\nis a pointer-chasing scan.<\/p>\n<p>regalloc2 adopts two general themes that work against these problems:<\/p>\n<ul>\n<li>\n<p>The overall data structure design consists of <em>contiguous-in-memory\ninline structs<\/em> rather than linked lists. For example, the list of\nliveranges in a bundle is a <code>SmallVec&lt;[LiveRangeListEntry; 4]&gt;<\/code>,\ni.e. a list with up to four entries inline and otherwise\nheap-allocated, and the entry struct contains the program-point\nrange inline. Combining this more compact layout with certain\n<em>invariants<\/em> -- usually, some sort of sorted-order invariant --\nallows for efficient lookups and list merges even without\nlinked-list splicing.<\/p>\n<\/li>\n<li>\n<p>At a higher level, regalloc2 tries to <em>avoid random lookups as much\nas possible<\/em>. Sometimes this is unavoidable, but where it is not, a\nlinear scan that produces some output as it goes is much more\ncache-friendly.<\/p>\n<\/li>\n<\/ul>\n<p>It is worth examining the particular technique we use to resolve moves\nacross control-flow edges. This requires looking up where a virtual\nregister is allocated at either end of the edge -- two arbitrary\npoints in the linear sequence of instructions. The problem is solved\nin IonMonkey (as we linked above) by scanning over ranges to find\nbasic block ends and then doing a linear-time linked-list traversal to\nfind the \"other end\", for overall quadratic time.<\/p>\n<p>Instead we scan the liveranges for a virtual register once and\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/ion\/moves.rs#L126-L164\">produce \"half-moves\" into a\n<code>Vec<\/code>.<\/a>\nThese \"half-moves\" are records of either the \"source\" side of a move,\nat the origin point of a CFG edge, or the \"destination\" side of a\nmove, at the destination point of a CFG edge. After our single scan,\nwe sort the list of half-moves by a key (the vreg and destination\nblock) so that the source and destination(s) appear together. We can\nthen scan <em>this<\/em> list once and generate all moves in bulk.<\/p>\n<p>If that sounds something like\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/MapReduce\">MapReduce<\/a>, that is not an\naccident: the technique of leveraging a sort with a well-chosen key\nwas invented to allow for efficient parallel computation, and here\nallows the two \"ends\" of the move to be processed independently.<\/p>\n<p>This technique provides better algorithmic efficiency, much better\ncache residency (we have two steps that boil down to \"scan input list\nlinearly and produce output list linearly\"), and leans on the\nstandard-library implementation of <code>sort()<\/code>, which is likely to be\nfaster than anything we can come up with. Profiling of regalloc2 runs\nshows sometimes up to 10% or so of runtime spent in <code>sort()<\/code>, but this\nis far better than the alternative, in which we do a random\npointer-chasing lookup at every step.<\/p>\n<h4 id=\"compact-data\">Compact Data<\/h4>\n<p>Another lesson learned over and over during regalloc2 optimization is\nthis: data compactness matters! A single <code>struct<\/code> growing from 16 to\n24 bytes could lead to significant slowdowns if a large input leads to\nallocation and traversals over an array of 10,000 such structs. Every\nimprovement in memory footprint is a reduction in cache misses.<\/p>\n<p>We play many games with bitpacking to achieve this. For example,\nregalloc2 puts its <code>Operand<\/code> in <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/lib.rs#L407-L422\">32\nbits<\/a>,\nand this includes a virtual register number, a constraint, a physical\nregister number to possibly go with that constraint, the position\n(early\/late), kind (def\/use), and register class of the operand. Some\nof this optimization requires compromise: as a result of our encoding\nscheme, for example, we can allow only 2M (2<sup>21<\/sup>) virtual\nregisters per function body. But in practice most applications will have\nother limits that take effect before this matters. (And in any case,\nmany compilers play these same sorts of tricks, so megabytes-large\nfunction bodies are problematic in all sorts of ways.) And we sometimes\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/13\">find ways to pack a few more\nbits<\/a> (more such\nPRs are always welcome!).<\/p>\n<p>We play similar tricks with program points, spill weights (we <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/ion\/liveranges.rs#L55-L70\">store\nthem as\nbfloat16<\/a>\nbecause spill weights need not be too precise, only relatively\ncomparable, and using only 16 bits lets us pack some flags in the\nupper 16 and save a <code>u32<\/code>), and more.<\/p>\n<p>Finally, trading off indirection and data-inlining is important: e.g.,\na\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/ion\/data_structures.rs#L89-L94\"><code>LiveRangeList<\/code><\/a>\nkeeps the program-point range (32 + 32 bits) inline, then a 32-bit\nindex to indirect to everything else about the liverange, because\nchecking for bundle overlap is the most common reason for traversing\nthis list and reducing cache misses in this inner loop is paramount.<\/p>\n<h4 id=\"reducing-work\">Reducing Work<\/h4>\n<p>One final performance technique that at once both sounds completely\nobvious and superficial, yet is quite powerful, is: \"simply do less\nwork!\"<\/p>\n<p>One can often get lost in profiler results, wondering how to shave off\nsome hotspots by compacting some data or reworking some inner-loop\nlogic, only to miss that one is implicitly assuming that the actual\ncomputation to be done is invariant. In other words, one might look\nfor the fastest way to compute a particular subproblem or framing of\nthe problem, rather than the ultimate problem at hand (in this case,\nthe register allocation).<\/p>\n<p>In the case of regalloc2, this primarily means that we can improve\nperformance by <em>reducing the number of bundles and liveranges<\/em>. In\nturn, this means that we can get outsized wins by improving our\nmerging and splitting heuristics.<\/p>\n<p>Early in the optimization push, I realized that regalloc2 was often\nfinding an abnormally large number of conflicts between bundles, and\nsplitting far too aggressively. It turned out that the liveness\nanalysis was initially <em>approximate<\/em>, in an intentional, if premature,\nefficiency tradeoff to avoid a fixpoint loop in favor of a single-pass\nloop-backedge-based algorithm that overapproximated liveness (which is\nfine for correctness). The time that this saved was more than offset\nby the large increase in iterations of the bundle processing loop. So\nI reworked this into a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/b78ccbce6e5700bc1ed2356bbb1d3221de49a353\/src\/ion\/liveranges.rs#L327-L401\">precise\nanalysis<\/a>\nthat iterates until fixpoint. It is worthwhile to pay that extra\nanalysis cost upfront to get exact liveness in order to make our lives\n(and our runtime) better later.<\/p>\n<p>The way in which we compute that precise liveness itself also raises\nan interesting way of reducing work: by carefully choosing\ninvariants. We perform the liverange-building scan in such a way that\nwe <em>always observe liveranges in (reverse) program order<\/em>. This lets\nus build the liverange data structures, which are normally sorted,\nwith simple appends, merging with contiguous sections from adjacent\nblocks. This is in contrast to the <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/70cf6863bd85af2a3188ec1fe5209a3ec1b2de86\/js\/src\/jit\/BacktrackingAllocator.cpp#263-340\">original IonMonkey allocator's\nequivalent\nfunction<\/a>\nto add liveranges during analysis, which essentially does an insertion\nsort and merge, leading to O(n\u00b2) behavior. Note that the IonMonkey\ncode has a <code>CoalesceLimit<\/code> constant that caps the O(n\u00b2) behavior at\nsome fixed limit. In contrast our liverange build in regalloc2 is\nalways linear-time.<\/p>\n<p>The final way in which one can reduce work, related to data-structure\nand invariant choice, is by designing the input (API or data format)\ncorrectly in order to efficiently encode the problem. The register\nallocator that preceded regalloc2, regalloc.rs, did not have a notion\nof register constraints in instructions' use of virtual\nregisters. Instead, it required the user to use move instructions:\nreused-input constraints become a move prior to the instruction, and\nfixed-register constraints become moves to\/from physical registers. It\nthen relied on a separate move-elision analysis to try to eliminate\nthese moves. regalloc2 has a smaller input because constraints are\ncarried on every operand. It can still generate these moves when\nneeded, but they often are not. This results in faster allocation as\nwell as often better generated code.<\/p>\n<h3 id=\"correctness-design-for-test-and-fuzzing-first-development\">Correctness: \"Design for Test\" and Fuzzing-First Development<\/h3>\n<p>The next set of lessons to come from regalloc2 have to do with <em>how to\nattain correctness in complex programs<\/em>.<\/p>\n<p>I believe that regalloc2 is maybe the most <em>intrinsically complex<\/em>\nprogram I have written: its operation relies on many interlocking\ninvariants across the allocation process, and there are many, many\nedge cases to get right. It is &gt;10K lines of very dense Rust\ncode. There should be approximately zero chance for any human to get\nthis correct, working on real inputs, in any reasonable timeframe. And\nrelying on something this complex to uphold security guarantees that\nrely on correct compilation should be terrifying.<\/p>\n<p>And yet somehow it seems to work, and we haven't found any miscompiles\ncaused by RA2 itself since we switched Cranelift to use regalloc2 in\nApril. More broadly, there was\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/54\">one<\/a> issue\nwhere constraints generated by Cranelift could not be handled in some\ncases, resulting in a panic<sup class=\"footnote-reference\"><a href=\"#11\">11<\/a><\/sup>; and\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/56\">another<\/a> where\nspillslots were not reused as they should be, resulting in worse\nperformance; neither could result in incorrect generated code. In the\nintegration of RA2 into Cranelift, there were\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/4042\">two<\/a>\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/4044\">bugs<\/a> that\ncould, but both were found within 24 hours by the fuzzers. (That\ndoesn't mean there won't be any more of course -- but things have been\nsurprisingly boring and quiet!)<\/p>\n<p>The main superpower, if one can call it that, that enabled this to\nwork out is <em>fuzzing<\/em>. And in particular, a step-by-step approach to\nfuzzing in which I built fuzzing oracles, test harnesses, and fuzz\ntargets as I built the allocator itself, and drove development with\nit. Until about 4 months in when I wired up the first version of the\nCranelift integration, regalloc2 had <em>only<\/em> ever performed register\nallocation for fuzz-target-generated inputs. It still doesn't have a\ntest harness for manually-written tests; there seems to be no need, as\nthe fuzzer is remarkably prescient at finding bugs.<\/p>\n<p>I find it helpful to think of this philosophy in terms of the\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Design_for_testing\">design-for-test<\/a>\nidea from digital hardware design. In brief, the idea is that one\nbuilds additional features or interfaces into the hardware\nspecifically so its internal state is visible and it can be tested in\ncontrolled, systematic ways.<\/p>\n<p>The first thing that I built in the regalloc2 tree was a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/fuzzing\/func.rs#L300-L308\">function\nbody\ngenerator<\/a>\nthat produces arbitrary control flow, either reducible or irreducible,\nand arbitrary uses and defs according to what SSA allows. I then built\nan <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/main\/src\/ssa.rs\">SSA\nvalidator<\/a>,\nand finally, <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/main\/fuzz\/fuzz_targets\/ssagen.rs\">fuzzed one against the\nother<\/a>. This\nway I built confidence that I had fuzzing input that included\ninteresting edge cases. This would become an important tool for\ntesting the whole allocator, but it was important to \"test the tester\"\nfirst and cross-check it against SSA's requirements. Of course,\nchecking SSA requires one to compute flowgraph dominance on the CFG,\nand <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/main\/fuzz\/fuzz_targets\/domtree.rs\">that can be fuzzed\ntoo<\/a>,\nusing a from-first-principles definition of graph dominance. So the\ntest-tester has itself been tested in this additional way.<\/p>\n<p>Once I had built enough tools with the lower-level tools, and\nsharpened them all against each other, it was time to write the\nregister allocator itself. Once each major piece was implemented, I\nfirst fuzzed it with the SSA function generator to check for panics\n(assertion failures, mostly). Getting a clean run, given the\nrelatively generous spread of asserts throughout the codebase, gave\nsome confidence that the allocator was doing <em>something<\/em>\nreasonable. But to truly be confident that the results were\nsemantically correct answers, we needed to lean more heavily on some\nprogram analysis techniques.<\/p>\n<p>In <a href=\"\/blog\/2021-03\/15\/cranelift-isel-3\/\">another blog post<\/a> I detailed\nour \"register allocator checker\". In brief, this is a <em>symbolic\nverification<\/em> engine that checks that the resulting register\nallocations produce the same dataflow connectivity as the original,\npre-regalloc program. To fully verify regalloc2, I ported the checker\nover, and drove the whole pipeline -- SSA function generator,\nallocator, and checker -- with a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/main\/fuzz\/fuzz_targets\/ion_checker.rs\">fuzz\ntarget<\/a>.<\/p>\n<p>This workflow was remarkably (sometimes maddeningly!) effective. I\nstarted with a supposedly complete allocator, and ran the\nfuzzer. Within a few seconds it found a \"counterexample\" where,\naccording to the checker, regalloc2 produced an incorrect\nallocation. I built\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/main\/src\/ion\/dump.rs\">annotation<\/a>\ntooling to produce <a rel=\"external\" href=\"https:\/\/gist.github.com\/cfallin\/38d80aac45da75ce9eb142f5e28c0648\">views of the allocator's liveranges and other\nmetadata<\/a>\nover the original program. I pored over this and debug-log output of\nthe allocator's various stages, eventually worked out the bug (often\nsome corner-case I had not considered, or sometimes an unexpected\ninteraction between two different parts of the system) and came up\nwith a fix. With the particular fuzz-bug fixed, I started up the main\nfuzzer again. libFuzzer's startup seems to run over the entire corpus\nbefore generating new inputs, so sometimes my bugfixes would quickly\ncause regressions in other cases I had already handled before. After\njuggling solutions and finding some way to maintain correctness in all\ncases, I would let the fuzzer run again, usually finding my next novel\nfuzzbug within a few minutes.<\/p>\n<p>This was my life for a month or so. Fuzzers, especially over complex\nprograms with strict oracles, are <em>relentless<\/em>: they leave no rock\nunturned, they find every bug you could imagine and some you can't,\nand they accept no excuses. But one day... you run the fuzzer and you\nfind that it keeps running. And running. Three hours later, it's still\nrunning. There is no better feeling in the software-engineering\nuniverse, and frankly fuzzing with a strong oracle (like symbolic\nchecking or differential execution fuzzing) is probably the\nsecond-strongest assurance one will get that one's code is <em>correct<\/em>\n(with respect to the \"spec\" implied by the testcase generator and\noracles, mind!) short of actual formal verification. This was the\nproject that changed my opinion on fuzzing from \"nice to have\nsupplemental correctness technique\" to \"the only way to develop\ncomplex software\".<\/p>\n<h3 id=\"compatibility-and-migration-path\">Compatibility and Migration Path<\/h3>\n<p>The last lesson I want to draw from my regalloc2 experience is how one\nmight think about compatibility and migrations, in the context of\nlarge \"replace a whole unit\" updates to software projects.<\/p>\n<p>The regalloc2 effort occurred within the context of the Cranelift\nproject, and was designed primarily for use in Cranelift (though it\ncan be used, and apparently is being used, as a standalone library\nelsewhere as well). As such, a primary design directive for regalloc2\ncould be \"do whatever is needed to fit into Cranelift's assumptions\nabout the register allocator\".<\/p>\n<p>On the other hand, conforming to the imprint left by the last register\nallocator is a good way to sacrifice a rare chance to explore\ndifferent corners of the design space. The design of the API of\nregalloc.rs made in 2020 was quite good for the time -- simple, easy\nto use, and purpose-built for Cranelift -- but we subsequently learned\nseveral lessons. For example, regalloc.rs required the program to be\nalready lowered out of SSA, resulting in somewhat inefficient\ninteractions between blockparam-generated moves and regalloc-generated\nmoves. Ideally we wanted to do something better here.<\/p>\n<p>A timeline for context: regalloc2 proper was working, with its fuzzer\nas its only client, after about 6 weeks of initial implementation\n(late March to early May 2021). I cheerfully dove into a refactoring\nof Cranelift at that point to adapt to the new abstractions.<\/p>\n<p>Less cheerfully after a few weeks of effort, I stopped this\ndirect-port effort at around 547 type errors remaining (having never\ngotten past a full typecheck). There was simply too much changing all\nat once, and it was clearly not going to be a reasonable single diff\nto review or to trust for correctness. I had underestimated how much\nwould have to change; pulling one string loosened three others.<\/p>\n<p>It was clear that some sort of transition would need to happen in\nmultiple stages, so I next built a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/pull\/127\">compatibility\nshim<\/a> as a\nnew \"algorithm\" in regalloc.rs that was a thin wrapper around\nregalloc2. This involved significant work in regalloc2 to expand its\nrange of accepted inputs: support for non-SSA code, support for\n\"modify\" operands as well as uses and defs, and explicit handling of\nprogram-level moves with integration into the move generation\nlogic. This was working by August of 2021. Performance results were\nnot as good as initially expected with \"native\" regalloc2 API usage,\nbut were a promising intermediate step nonetheless.<\/p>\n<p>However, for somewhat complicated reasons, review of that PR stalled,\nand I spent time in other parts of Cranelift (the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/isle\/docs\/language-reference.md\">ISLE<\/a>\nDSL and instruction-selector backends using it). When I eventually\ncame back to RA2, in February 2022, several things had changed: some\nrefactoring (as a result of ISLE) made adaptation to \"SSA-like\" form\nin x86 instructions easier, and the enhancements to regalloc2 as part\nof the regalloc.rs compatibility shim also let us use RA2 directly and\nmigrate away from \"modify\" operands, moves, etc., in an incremental\nway.<\/p>\n<p>So I made a second attempt at porting Cranelift to use regalloc2\ndirectly, this time\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/3989\">succeeding<\/a>,\nto <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/3942\">fairly good\nresults<\/a>. We've\nbeen using RA2 since that PR merged in mid-April 2022, about a year\nafter RA2 began.<\/p>\n<p>I learned a few valuable lessons from this saga, but the main one is:\nincremental migration paths are everything. The above PR may look\nhorribly scary but much of the churn was \"semantically boring\": RA2\nsupported, in the end, most of the same abstractions as regalloc.rs,\nwith only blockparam handling changing fundamentally. This is a sort\nof hybrid of the \"compatibility shim\" and \"direct use of new API\"\napproaches: new API, but supporting a superset of the semantic demands\nof the old API. One can then migrate single API use-sites at a time\naway from \"legacy semantics\" and eventually delete the warts (e.g.,\n\"modify\" operands in addition to pure uses\/defs) if one desires, but\nthat is decoupled from the main atomic switchover. I indeed hope to do\nsuch cleanup in Cranelift, in due time.<\/p>\n<p>Along with that, it is useful to think of finite budget for\nsemantic\/design-level cleanup per change. Rewrites are opportune times\nto push a project into a better design-space and benefit from lessons\nlearned, sometimes in ways that would be hard or impossible to do with\na truly incremental approach. However, at the margins where the\nrewrite connects to the outside world, this shift causes tension and\nso is fundamentally constrained or else has to pull the whole world\nalong with it. I am happy that regalloc2 pulls responsibility for SSA\nlowering into the allocator; it can be handled more efficiently\nthere. Likewise I am happy that the compatibility-shim effort filled\nin support for regalloc.rs features that made the rest of the\ntransition easier.<\/p>\n<h3 id=\"unending-and-unwinnable-nature-of-heuristic-tuning\">Unending and Unwinnable Nature of Heuristic-Tuning<\/h3>\n<p>The final lesson I wish to pull out of this experience is one that has\nbecome apparent in the time since the initial transition to RA2: any\nprogram that solves an NP-complete problem in a complex way, with a\nhybridized ball of hundreds of individual heuristics and techniques\nthat somehow works most of the time, is <em>always<\/em> going to make someone\nunhappy in some case and at some point unambiguous wins become very\nhard to find. That is not at all to say that it's not worth continuing\nattempts at optimization; sometimes improvements do become\napparent. But they become much rarer after an initial hillclimb to the\ntop of a \"competent implementation of one point in design-space\" local\nmaximum.<\/p>\n<p>While looking for more performance, I experimented with many different\nsplit heuristics. Especially difficult are splits' relationship to\nloops: when one has a hot inner loop, one <em>really<\/em> wants to place a\nsplit-point that implies an expensive move (load or store) <em>outside<\/em>\nthe inner loop. But getting this right in all cases is subtle, because\nthe winning tradeoff depends on register pressure inside the loop, how\nmany values are live across the loop and to the following code, how\nmany uses occur in the loop and how frequently (rare path vs. common\npath), and so on. In the end, I actually abandoned a number of more\ncomplex cost heuristics (an example is in this <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/commit\/428e6a41f7b37697196e3a82e8326f22839307b5\">never-merged\ncommit<\/a>)\nand went with several simple heuristics: <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/b78ccbce6e5700bc1ed2356bbb1d3221de49a353\/src\/ion\/process.rs#L896-L903\">minimize the cost of the\nimplied move at a\nsplit<\/a>,\nand <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/b78ccbce6e5700bc1ed2356bbb1d3221de49a353\/src\/ion\/process.rs#L1036-L1052\">explicitly hoist split-points outside of\nloops<\/a>. This\nworked best overall, but did leave a little performance unclaimed in\nsome microbenchmarks.<\/p>\n<p>Sometimes clearer improvements are still possible. One example of a\nrecent investigation: in\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/3785\">#3785<\/a>, we\nnoticed that switching to RA2 had caused an extra move instruction to\nappear in a particular sequence. This seems minor, but it is always\ngood to understand <em>why<\/em> it might have occurred and if it points to\nsome deeper issue. After some\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/pull\/49\">investigation<\/a>\nit became apparent that the splitting heuristics were suboptimal in\nthe particular case of a liverange that spans from a\nregister-constrained use to a stack-constrained use. The details are\nbeyond the scope of this post (thank goodness, it's long enough\nalready!); but empirically I found that trimming liveranges around a\nsplit-site in a slightly different way tended to improve results.<\/p>\n<p>So, some changes will be an unmitigated win, but not every tradeoff is\nso. At the very least, the nature of a register allocator is that one\nwill likely have an unending stream of \"could work better in this\ncase\" sorts of issues. Can't win 'em all (but keep trying\nnonetheless!).<\/p>\n<h2 id=\"conclusions\">Conclusions<\/h2>\n<p>We're finally at the conclusions -- thanks to all who have persisted\nin reading this far!<\/p>\n<p>regalloc2 has been an immensely rewarding project for me, despite (or\nperhaps because of) the ups-and-downs inherent in building an\nhonest-to-goodness, actually-works,\nsomewhat-competitive-with-peer-compilers register allocator. It was a\nfar larger project than I had anticipated: when I began, I told my\nmanager it would probably be a few weeks to evaluate scope, maybe a\nmonth of work total. Witness <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Hofstadter%27s_law\">Hofstadter's\nLaw<\/a> in action: that\nis, it will always take longer than you think it will, even when\naccounting for Hofstadter's Law.<\/p>\n<p>I hope some of the above lessons have been illuminating, and perhaps\nthis post has given some sense of how many interesting problems the\nregister-allocator space contains. It's a well-studied area for at\nleast 40 years now, with countless approaches and clever tricks to\nlearn and to combine in new ways; the work is far from over!<\/p>\n<h2 id=\"acknowledgments\">Acknowledgments<\/h2>\n<p>Many, many thanks to: <a rel=\"external\" href=\"https:\/\/github.com\/julian-seward1\">Julian\nSeward<\/a> and <a rel=\"external\" href=\"https:\/\/github.com\/bnjbvr\">Benjamin\nBouvier<\/a> for numerous discussions about\nregister allocation throughout 2020, and Julian for several followup\ndiscussions after regalloc2 started to exist; Julian Seward and\n<a rel=\"external\" href=\"https:\/\/github.com\/Amanieu\">Amanieu d'Antras<\/a> for initial code-review\nof regalloc2 proper; Amanieu for a number of really high-quality PRs\nto improve RA2 and add feature support; and <a rel=\"external\" href=\"https:\/\/github.com\/fitzgen\">Nick\nFitzgerald<\/a> for code-review of the (quite\nextensive) Cranelift refactoring to use regalloc2. Enormous thanks to\nNick for reading over this entire post and providing feedback as well.<\/p>\n<hr \/>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>which is to say, the original\n<a href=\"\/blog\/2020\/09\/18\/cranelift-isel-1\/\">three<\/a>-<a href=\"\/blog\/2021\/01\/22\/cranelift-isel-2\/\">part<\/a>\n<a href=\"\/blog\/2021\/03\/15\/cranelift-isel-3\/\">series<\/a> covered a range of\ntopics summarizing the goals and ideas of Cranelift's new\nbackend design, but we haven't stopped working to improve things\nsince then! The series is now four-thirds complete; by the time\nI'm done it may be five-thirds or more...<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>In fact, it is perhaps the most important problem to solve for a\nfast Wasm-focused compiler, because most other common compiler\noptimizations will have been done at least to some degree to the\nWasm bytecode; register allocation is the main transform that\nbridges the semantic gap from stack bytecode to machine code.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>Other sorts of constraints are possible too; in general, a\nliverange is constrained by all of the \"register mentions\" in\ninstructions that touch the liverange's vreg, and we have to\nsatisfy all of these constraints at once. A constraint may be\n\"any register of this kind\", or \"this particular physical\nregister\", or \"a slot on the stack\", or \"the same register as\ngiven to another liverange\", for example. And beyond\nconstraints, we may have soft \"hints\" as well, which if\nfollowed, reduce the need to move values around.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"4\"><sup class=\"footnote-definition-label\">4<\/sup>\n<p>regalloc2 supports arbitrary control flow (i.e., does not impose\nany restrictions on\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Control-flow_graph#Reducibility\">reducibility<\/a>);\nits only requirement is that <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Control-flow_graph#Special_edges\">critical\nedges<\/a>\nare split, which Cranelift ensures by construction during\nlowering.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"5\"><sup class=\"footnote-definition-label\">5<\/sup>\n<p>Full credit for this idea, as well as most of the constraint\ndesign in regalloc2, goes to IonMonkey's register allocator.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"6\"><sup class=\"footnote-definition-label\">6<\/sup>\n<p>As we'll note under \"Lessons\" below, during development of a\ncompatibility layer that allowed regalloc2 to emulate\nregalloc.rs, an earlier register allocator, we actually added a\n\"modify\" kind of operand that directly corresponds to the\nsemantics of <code>rax<\/code> above, namely read-then-written all in one\nregister. We subsequently used it in several places while\nmigrating Cranelift. But for simplicity we hope to eventually\nremove this (once all uses of it are gone).<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"7\"><sup class=\"footnote-definition-label\">7<\/sup>\n<p>It's actually a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/33611a68b90e40869bba52934449315a8f4e5477\/src\/indexset.rs#L13-L26\">sparse\nbitset<\/a>\nthat, when large enough, stores a hashmap whose values are\n<em>contiguous 64-bit chunks<\/em> of the whole bitset. This is because,\nfor large functions with thousands of virtual registers, keeping\nthousands of bits per basic block would be impractical. However,\nthe naive sparse approach, where we keep a <code>HashSet&lt;VReg&gt;<\/code> or\nequivalent, is also costly because it spends 32 bits per set\nelement (plus load-factor overhead). We observed that the live\nregisters at a given point are often \"clustered\": there are some\nlong-range live values from early in the function, and then a\nbunch of recently-defined registers. (This depends also on\nvirtual registers being numbered roughly in program order, which\nis generally a good heuristic to rely on.) So we have a few\n<code>u64<\/code>s and pay the sparse map cost for those, then have a dense\nmap within each 64-bit chunk.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"8\"><sup class=\"footnote-definition-label\">8<\/sup>\n<p>Credit must go to IonMonkey for <a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/70cf6863bd85af2a3188ec1fe5209a3ec1b2de86\/js\/src\/jit\/BacktrackingAllocator.cpp#643-647\">this\ntrick<\/a>\nas well, though the details of how to edit the liveranges\nappropriately to get the right interference semantics were <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/33611a68b90e40869bba52934449315a8f4e5477\/src\/ion\/moves.rs#L762-L805\">far\nfrom\nclear<\/a>\nand the path to our current approach was \"paved by fuzzbug\nfailures\", so to speak.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"9\"><sup class=\"footnote-definition-label\">9<\/sup>\n<p>Some literature on SSA form calls the connected set of\nliveranges via phi-nodes or block parameters \"webs\". Our notion\nof a bundle encompasses this case but is a bit more general; in\nprinciple we can merge any liveranges into a bundle as long as\nthey don't overlap.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"10\"><sup class=\"footnote-definition-label\">10<\/sup>\n<p>Actually, there are <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc2\/blob\/0395614545da5ccc45866bfa50dcdbe9cc37c253\/src\/ion\/data_structures.rs#L561-L570\">up to\nseven<\/a>\nparallel moves between instructions, at priorities according to\nthe way that various constraint edge-cases are lowered. For\nexample, when a single vreg must be placed in multiple physical\nregisters due to multiple uses with different fixed-register\nconstraints, the move that makes this happen occurs at\n<code>MultiFixedReg<\/code> priority, which comes after the main\ninter-instruction permutation (it is logically part of the input\nsetup for the following instruction). And <code>ReusedInput<\/code> moves\nhappen after that, because any one of the fixed-register inputs\ncould be reused as an input. The detailed reasoning for the\norder here is beyond the scope of this blogpost, but suffice it\nto say that the fuzzer helped immensely in getting this ordering\nright!<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"11\"><sup class=\"footnote-definition-label\">11<\/sup>\n<p>Pertinent to the broader point about fuzzing, this combination\nof constraints was not generated by RA2's fuzz target, which is\nwhy the resulting corner cases were not seen during\ndevelopment. As soon as the fuzzing testcase generator was\nextended to do so, the fuzzer found a counterexample within a\nfew seconds, and helped to verify the constraint rewrites in\nRA2's frontend that fixed this issue.<\/p>\n<\/div>\n"},{"title":"Cranelift, Part 3: Correctness in Register Allocation","published":"2021-03-15T00:00:00+00:00","updated":"2021-03-15T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2021\/03\/15\/cranelift-isel-3\/"}},"id":"https:\/\/cfallin.org\/blog\/2021\/03\/15\/cranelift-isel-3\/","content":"<p>This post is the last in a three-part series about\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\">Cranelift<\/a>.\nIn the <a href=\"\/blog\/2020\/09\/18\/cranelift-isel-1\/\">first post<\/a>, I covered\noverall context and the instruction-selection problem; in the <a href=\"\/blog\/2021\/01\/22\/cranelift-isel-2\/\">second\npost<\/a>, I took a deep dive into\ncompiler performance via careful algorithmic design.<\/p>\n<p>In this post, I want to dive into how we engineer for and work to\nensure <em>correctness<\/em>, which is perhaps the most important aspect of\nany compiler project. A compiler is usually a complex beast: to obtain\nreasonable performance, one must perform quite complex analyses and\ncarefully transform an arbitrary program in ways that preserve its\nmeaning. It is likely that one will make mistakes and miss subtle\ncorner cases, especially in the cracks and crevices between\ncomponents. Despite all of that, correct code generation is <em>vital<\/em>\nbecause the consequences of miscompilation are potentially so severe:\nbasically any guarantee (security-related or otherwise) that we make\nat a higher level of the system stack relies on the (quite\nreasonable!) assumption that the computer will execute the source code\nwe have written faithfully. If the compiler translates our code to\nsomething else, then all bets are off.<\/p>\n<p>There are ways that one can apply good engineering principles to\nreduce this risk. An extremely powerful technique derives from the\ninsight that <em>checking a result<\/em> is usually easier than <em>computing<\/em>\nit, and if we randomly generate many inputs, run our compiler (or\nother program) on these inputs, and check its output, we can get to a\n<em>statistical approximation<\/em> of the claim \"for all inputs, the compiler\ngenerates the correct output\". The more random inputs we try, the\nstronger this statement becomes. This technique is known as\n<em><a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Fuzzing\">fuzzing<\/a><\/em> with a\n<em>program-specific\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Test_oracle\">oracle<\/a><\/em>, and I could\nwrite a lengthy ode to its uncanny power to find bugs (many others\nhave, already).<\/p>\n<p>In this post, I will cover how we worked to ensure correctness in our\nregister allocator,\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\">regalloc.rs<\/a>, by\ndeveloping a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/tree\/main\/lib\/src\/checker.rs\">symbolic\nchecker<\/a>\nthat uses abstract interpretation to prove correctness for a specific\nregister allocation result. By using this checker as a fuzzing oracle,\nand driving just the register allocator with a focused fuzzing target,\nwe have been able to uncover some very interesting and subtle bugs,\nand achieve a fairly high confidence in the allocator's robustness.<\/p>\n<h2 id=\"what-is-register-allocation\">What is Register Allocation?<\/h2>\n<p>Before we dive in, we need to cover a few basics. Most importantly:\nwhat is the <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Register_allocation\">register allocation\nproblem<\/a>, and what\nmakes it hard?<\/p>\n<p>In a typical programming language, a program can have an arbitrary\nnumber of variables or values in scope. This is a very useful\nabstraction: it is easiest to describe an algorithm when one does not\nhave to worry about where to store the values.<\/p>\n<p>For example, one could write the program:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>void f() {<\/span><\/span>\n<span class=\"giallo-l\"><span>    int x0 = compute(0);<\/span><\/span>\n<span class=\"giallo-l\"><span>    int x1 = compute(1);<\/span><\/span>\n<span class=\"giallo-l\"><span>    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    int x99 = compute(99);<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>    \/\/ ---<\/span><\/span>\n<span class=\"giallo-l\"><span>    <\/span><\/span>\n<span class=\"giallo-l\"><span>    consume(x0);<\/span><\/span>\n<span class=\"giallo-l\"><span>    consume(x1);<\/span><\/span>\n<span class=\"giallo-l\"><span>    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    consume(x99);<\/span><\/span>\n<span class=\"giallo-l\"><span>}<\/span><\/span><\/code><\/pre>\n<p>At the midpoint of the program (the <code>---<\/code> mark), there are 100\n<code>int<\/code>-sized values that have been computed and are later used. When\nthe compiler produces machine code for this function, where are those\nvalues stored?<\/p>\n<p>For small functions with only a few values, it is easy to place every\nvalue in a CPU register. But most CPUs do not have 100 general-purpose\nregisters for storing integers<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>; and in general, most languages\neither do not place limits on the number of local variables or else\nimpose limits that are much, much higher than the typical number of\nCPU registers. So we need some approach that scales beyond, say, about\n16 values (x86-64) or about 32 values (aarch64) in use at once.<\/p>\n<p>A very simple answer is to allocate a <em>memory<\/em> location for each local\nvariable. In fact this is exactly what the C programming model\nprovides: all of the <code>xN<\/code> variables above <em>semantically<\/em> live in\nmemory, and we can take the address <code>&amp;xN<\/code>. If one does this, one will\nfind that the addresses are part of the <em>stack<\/em>. When the function is\ncalled, it allocates a new area on the stack called the <em>stack frame<\/em>\nand uses it to store local variables.<\/p>\n<p>This is far from the best we can do, though! Consider what this means\nwhen we actually perform some operation on the locals. If we read two\nlocals, perform an addition, and store the result in a third, like so:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>x0 = x1 + x2;<\/span><\/span><\/code><\/pre>\n<p>then in machine code, because most CPUs do not have instructions that\ncan read two in-memory values and write back a third in-memory result,\nwe would need to emit something like the following:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>ld r0, [address of x1]<\/span><\/span>\n<span class=\"giallo-l\"><span>ld r1, [address of x2]<\/span><\/span>\n<span class=\"giallo-l\"><span>add r0, r0, r1  \/\/ r0 := r0 + r1<\/span><\/span>\n<span class=\"giallo-l\"><span>st r0, [address of x0]<\/span><\/span><\/code><\/pre>\n<p>Compiling code in this way is very <em>fast<\/em> because we need to make\nalmost no decisions: a variable reference <em>always<\/em> becomes a memory\nload, for example. This is how a \"baseline <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Just-in-time_compilation\">JIT\ncompiler<\/a>\"\ntypically works, actually: for example, in the SpiderMonkey JS and\nWasm JIT compiler, the baseline JIT tier -- which is meant to produce\npassable code very, very quickly -- actually keeps a stack of values\nin memory that correspond one-to-one to the JS bytecode or Wasm\nbytecode's value stack. (You can read the code\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/rev\/38ed718a101aca27db25984413c052ccd8c0ceda\/js\/src\/jit\/CacheIRCompiler.h#301\">here<\/a>:\nit actually keeps a few of the most recent values, at the top of\noperand stack, in fixed registers and the rest in memory.)<\/p>\n<p>Unfortunately, accessing memory multiple times for every operation is\nvery slow. What's more, it is often the case that values are <em>reused\nsoon after being produced<\/em>: for example, we might have<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>x0 = x1 + x2;<\/span><\/span>\n<span class=\"giallo-l\"><span>x3 = x0 * 2;<\/span><\/span><\/code><\/pre>\n<p>When we compute <code>x3<\/code> using <code>x0<\/code>, do we reload <code>x0<\/code>'s value from memory\nimmediately after storing it? A smarter compiler should be able to\nremember that it had just computed the value, and should keep it in a\nregister, avoiding the round-trip through memory altogether.<\/p>\n<p>This is <em>register allocation<\/em>: it is assigning a value in the program\nto a register for storage. What makes register allocation interesting\nis that (as noted above) there are fewer CPU registers than the number\nof allowable program values, so we have to choose some subset of\nvalues to keep in registers. This is often constrained in certain\nways: for example, an <code>add<\/code> instruction on RISC-like CPUs can only\nread from, and write to, registers, so a value's storage location must\nbe a register immediately before it is used by a <code>+<\/code>\noperator. Fortunately, the location assignments can change over time,\nso that at different points in the machine code, a register can be\nassigned to hold different values. The job of the register allocator\nis to decide how to shuffle values between memory and registers, and\nbetween registers, so that at any given time the values that need to\nbe in registers are so.<\/p>\n<p>In our design, the register allocator will accept as input a type of\nalmost-machine-code called \"virtual-register code\", or <code>VCode<\/code>. This\nhas a sequence of machine instructions, but registers named in the\ninstructions are <em>virtual<\/em> registers: the compiler can use as many of\nthem as it needs. The register allocator will (i) rewrite the register\nreferences in the instructions to be actual machine register names,\nand (ii) insert instructions to shuffle data as needed. These\ninstructions are called <em>spills<\/em> when they move a value from a\nregister to memory; <em>reloads<\/em> when the move a value from memory back\nto a register; and <em>moves<\/em> when they move values between\nregisters. The memory locations where values are stored when not in\nregisters are called <em>spill slots<\/em>.<\/p>\n<p>An example of the register-allocation problem is shown below on a\nprogram with four instructions:<\/p>\n<p><img src=\"\/assets\/2020-12-23-regalloc-web.svg\" alt=\"Figure: Register allocation\" \/><\/p>\n<p>This allocation is performed onto a machine with two registers (<code>r0<\/code>\nand <code>r1<\/code>). On the left, the original program is written in an\nassembly-like form with <em>virtual registers<\/em>. On the right, the program\nhas been modified to use only <em>real registers<\/em>.<\/p>\n<p>Between each instruction, we have written a mapping from virtual\nregisters to real registers. The register allocator's task is just\n(\"just\"!) to compute these mappings and then edit the instructions,\ntaking their register references through these mappings.<\/p>\n<p>Note that the program, at one point, has <em>three<\/em> live values, or\nvalues that still must be preserved because they will be used later:\nbetween the first and second instructions, all of <code>v0<\/code>, <code>v1<\/code> and <code>v2<\/code>\nare live.  The machine has only two registers, so it cannot hold all\nlive values in them; it must spill at least one. This is the reason\nfor the <em>spill instruction<\/em>, written as a store to the stack slot\n<code>[sp+0]<\/code>.<\/p>\n<h2 id=\"how-hard-is-register-allocation\">How Hard is Register Allocation?<\/h2>\n<p>In general, the register allocator will first analyze the program to\nwork out which values are live at which program points. This liveness\ninformation and related constraints specify a <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Combinatorial_optimization\">combinatorial\noptimization<\/a>\nproblem: certain values must be stored <em>somewhere<\/em> at each point,\nconstraints limit which choices can be made and some choices will\nconflict with some others (e.g., two values cannot occupy a register\nat the same time), and a set of choices implies some cost (in data\nmovement). The allocator will solve this optimization problem as well\nas it can using heuristics of some sort, depending on the register\nallocator.<\/p>\n<p>Is this a hard problem? In fact, it is not only hard in a colloquial sense,\nbut <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/NP-completeness\">NP-complete<\/a>: this\nmeans that it is as hard as any other NP problem, for which we know only\nexponential-time brute-force algorithms in the worst case.<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup> <sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup> The\nreason is that the problem does not have <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Optimal_substructure\">optimal\nsubstructure<\/a>: it\ncannot be decomposed into non-interacting parts that can each be solved\nseparately and then built up into an overall solution; rather, decisions at\none point affect decisions elsewhere, potentially anywhere else in the\nfunction body. Thus, in the worst case, we can't do better than a\nbrute-force search if we want an optimal solution.<\/p>\n<p>There are many good <em>approximations<\/em> to optimal register allocation. A\ncommon one is <a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/10.1145\/330249.330250\">linear-scan register\nallocation<\/a>, which can run in\nalmost-linear time (with respect to the code size). Allocators that can\nafford to spend more time are more complex: for example, in\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\">regalloc.rs<\/a>, in addition\nto the <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/tree\/main\/lib\/src\/linear_scan\">linear-scan\nimplementation<\/a>\n(written by my brilliant colleague Benjamin Bouvier), we have a\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/blob\/main\/lib\/src\/bt_main.rs\">\"backtracking\"\nalgorithm<\/a>\n(written by my other brilliant colleague Julian Seward) that can edit and\nimprove its choices as it discovers higher-priority uses for registers.<\/p>\n<p>The details of how these algorithms work do not really matter here,\nexcept to say that they are <em>very<\/em> complicated and hard to get\nright. An algorithm that appears relatively simple at the conceptual\nlevel or in pseudocode quickly runs into interesting and subtle\nconsiderations as real-world constraints creep in. The regalloc.rs\ncodebase is about 25K lines of deeply-algorithmic Rust code; any\nreasonable engineer would expect this to include at least several\nbugs! Compounding the urgency here, a register-allocation bug can\nresult in <em>arbitrary<\/em> incorrect results, because the register\nallocator is in charge of \"wiring up\" all of the dataflow in the\nprogram. If we exchange one arbitrary value with another arbitrary\nvalue in the program, anything could happen.<\/p>\n<h2 id=\"how-to-verify-correctness\">How to Verify Correctness<\/h2>\n<p>So we want to write a correct register allocator. How do we even start\non a task like this?<\/p>\n<p>It might help to break down what we mean by \"correct\". Note that the\nregister allocation problem has a nice property: the programs both\n<em>before<\/em> and <em>after<\/em> allocation have a well-defined semantics. In\nparticular, we can think of register allocation as a transformation\nthat converts programs running on an <em>infinite-register machine<\/em>\n(where we can use as many virtual registers as we want) to a\n<em>finite-register machine<\/em> (where the CPU has a fixed set of\nregisters). If the original program on the infinite-register machine\nyields the same result as the transformed (register-allocated) program\non the finite-register machine, then we have achieved a correct\nregister allocation.<\/p>\n<p>How do we test this equivalence?<\/p>\n<h3 id=\"single-program-single-input-equivalence\">Single-Program, Single-Input Equivalence<\/h3>\n<p>The simplest way to test whether two programs are equivalent is to run\nthem and compare the results! Let's say we do this: for a single\nprogram, choose some random inputs, and run the virtual-registerized\nprogram alongside its register-allocated version on the appropriate\ninterpreters. Compare register and memory state at the end.<\/p>\n<p>What does it mean if the final machine states match? It means that\n<em>for this one program<\/em>, our register allocator produces a transformed\nprogram that is correct <em>for this one program input<\/em>. Note the two\nqualifications here. First, we have not necessarily shown that the\nregister allocation is correct given another program input. Perhaps a\ndifferent input causes a branch to go down another program path, and\nthe register allocator introduced an error on that path. Second, we\nhave not shown anything for any other program; we have only tested a\nsingle program and its register-allocated output.<\/p>\n<p>We can attempt to address the first limitation -- correctness only\nunder one input -- by taking more sample points. For example, we could\nchoose a thousand random program inputs, and even drive this random\nchoice with some sort of feedback that tries to maximize control-flow\ncoverage or other \"interesting\" behavior (as fuzzers do). We could\nprobably achieve reasonable confidence that this single register\nallocation result is correct, given enough test cases.<\/p>\n<p><img src=\"\/assets\/2021-03-10-single-program-single-input-web.svg\" alt=\"Figure: Checking a program with concrete inputs\" \/><\/p>\n<p>However, this is still very <em>expensive<\/em>: we are asking to run the\nwhole program N times to get a sample size of N. Even a single\nexecution may be expensive: the program on which we have performed\nregister allocation might be a compiler, or a videogame, for example.<\/p>\n<h3 id=\"single-program-for-all-input-equivalence\">Single-Program, For-all-Input Equivalence<\/h3>\n<p>Can we avoid the need to run the program <em>at all<\/em> to test that its\nregister-allocated version is correct?<\/p>\n<p>The answer is surprisingly simple: yes, we can, by simply altering the\n<em>domain<\/em> that the program executes on. Ordinarily we think of CPU\nregisters as containing concrete numbers -- say, 64-bit values. What\nif they contained <em>symbols<\/em> instead?<\/p>\n<p>By generalizing over program values with symbols, we can often\nrepresent the state of the system in terms of inputs without caring\nwhat those inputs are. For example, given the program:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>ld v0, [A]<\/span><\/span>\n<span class=\"giallo-l\"><span>ld v1, [B]<\/span><\/span>\n<span class=\"giallo-l\"><span>ld v2, [C]<\/span><\/span>\n<span class=\"giallo-l\"><span>add v3, v0, v1<\/span><\/span>\n<span class=\"giallo-l\"><span>add v4, v2, v3<\/span><\/span>\n<span class=\"giallo-l\"><span>return v4<\/span><\/span><\/code><\/pre>\n<p>register-allocated to:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>ld r0, [A]<\/span><\/span>\n<span class=\"giallo-l\"><span>ld r1, [B]<\/span><\/span>\n<span class=\"giallo-l\"><span>ld r2, [C]<\/span><\/span>\n<span class=\"giallo-l\"><span>add r0, r0, r1<\/span><\/span>\n<span class=\"giallo-l\"><span>add r0, r2, r0<\/span><\/span>\n<span class=\"giallo-l\"><span>return r0<\/span><\/span><\/code><\/pre>\n<p>without symbolic reasoning, we could store arbitrary integers to\nmemory locations <code>A<\/code>, <code>B<\/code> and <code>C<\/code> and simulate the program's execution\nbefore and after register allocation, never seeing a mismatch, but\nthis would not prove anything unless we iterated through all possible\nvalues. However, if we suppose that after the three loads, <code>r0<\/code>\ncontains <code>v0<\/code> (as a symbolic value, whatever it is), <code>r1<\/code> contains\n<code>v1<\/code>, and <code>r2<\/code> contains <code>v2<\/code>, and that <code>r0<\/code> contains <code>v3<\/code> after the\nfirst add and <code>v4<\/code> after the second add, we can see the correspondence\nby matching up the symbols.<\/p>\n<p><img src=\"\/assets\/2021-03-10-single-program-symbolic-web.svg\" alt=\"Figure: Checking a program with symbolic values\" \/><\/p>\n<p>This is a very simple example, and perhaps under-sells the insight and\npower of this approach; we will come back to it later when we talk\nabout <em>Abstract Interpretation<\/em> below.<\/p>\n<p>In any case, what we have shown is that for a single instance of the\nregister-allocation problem, we can <em>prove<\/em> that it transformed the\nprogram in a correct way. Concretely, this means that the machine code\nthat we generate will execute just as if we were interpreting the\nvirtual-register code; if we can correctly generate virtual-register\ncode, then our compiler is correct. That's excellent! Can we go\nfurther?<\/p>\n<h3 id=\"for-all-programs-equivalence\">For-all-Programs Equivalence<\/h3>\n<p>We could prove a-priori that the register allocator will <em>always<\/em>\ntransform <em>any<\/em> program in a way that is correct. In other words, we\ncould abstract not only over the input values to the program, but over\nthe <em>program<\/em> itself.<\/p>\n<p>If we can prove this, then we have no need to run any sort of check at\nruntime. Abstracting over program inputs lets us avoid the need to run\nthe program; we know the register allocation is correct for all\ninputs. In an analogous way, abstracting over the program to be\nregister-allocated would let us avoid the need to run the register\nallocator; we know the register <em>allocator<\/em> is correct for all\n<em>programs<\/em> and for all <em>inputs<\/em> to those programs.<\/p>\n<p>One can imagine that this is much harder. In fact, it has been done,\nbut is a significant proof-engineering effort, and is a realm of\nactive research: this basically requires writing a machine-verifiable\nproof that one's compiler algorithms are correct. Such proven-correct\ncompilers exist: e.g., <a rel=\"external\" href=\"https:\/\/compcert.org\/\">CompCert<\/a> has been\nproven to compile C correctly to machine code for several\nplatforms. Unfortunately, such efforts are strongly limited by the\nproof-engineering effort that is required, and thus this approach is\nunlikely to be feasible for a compiler unless it is their primary\ngoal.<\/p>\n<h3 id=\"our-approach-allocator-with-checker\">Our Approach: Allocator with Checker<\/h3>\n<p>Given all of the above, we choose what we believe is the most\nreasonable tradeoff: we build a <em>symbolic checker<\/em> for the <em>output<\/em> of\nthe register allocator. This does not let us make a static claim that\nour register allocator is correct, but it <em>does<\/em> let us <em>prove<\/em> that\nit is correct for any given compiler run; and if we use this as a\nfuzzing oracle, we can build <em>statistical confidence<\/em> that it is\ncorrect for all compiler runs.<\/p>\n<h2 id=\"checking-the-register-allocator\">Checking the Register Allocator<\/h2>\n<p>Our overall flow is pictured below:<\/p>\n<p><img src=\"\/assets\/2021-03-10-checker-web.svg\" alt=\"Figure: Augmenting register allocation with a checker\" \/><\/p>\n<p>There are two ways in which we can add a\nregister-allocator checker into the system. The first, on the left, we call\n\"runtime checking\": in this mode, every register allocator execution is checked\nand the machine code using the allocations is not permitted to execute (i.e.\nthe compiler does not return a result) until the checker verifies equivalence.\nThis is the safest mode: it provides the same guarantees as a proven-correct\nallocator (\"for-all-programs equivalence\" above). However, it imposes some\noverhead on every compilation, which may not be desirable. For this reason,\nwhile running the register allocator with the checker is a supported option in\nCranelift, it is not the default.<\/p>\n<p>The second mode is one in which we apply the checker to a <em>fuzzing<\/em> workflow,\nand is the approach we have generally preferred (we have a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/blob\/main\/fuzz\/fuzz_targets\/bt.rs\">fuzz\ntarget<\/a>\nin regalloc.rs that generates arbitrary input programs and runs the\nchecker on each one; and we are <a rel=\"external\" href=\"https:\/\/github.com\/google\/oss-fuzz\/blob\/4a6021021043bddfa017df7d0aea26ad76edbba0\/projects\/wasmtime\/build.sh#L59\">running this\ncontinuously<\/a>\nas part of Wasmtime's membership in Google's\n<a rel=\"external\" href=\"https:\/\/github.com\/google\/oss-fuzz\/\">oss-fuzz<\/a> continuous-fuzzing\ninitiative). In this mode, we use the checker as an\napplication-specific oracle for a fuzzing engine: as the fuzzing engine\ngenerates random programs (test cases), we run the register allocator over\nthese programs, run the checker on the result, and tell the engine whether the\nregister allocator passed or failed.  The fuzzer will flag any failing test\ncases for a human developer to debug. If the fuzzer runs for a long time\nwithout finding any issues, we can then have more confidence that the register\nallocator is correct, even without running the checker; and the longer the\nfuzzer runs, the greater our confidence becomes. The application-specific\noracle sigificantly improves over more generic fuzzer feedback mechanisms, such\nas program crashes or incorrect output: a register-allocator bug may not\nimmediately manifest in incorrect execution, or when it does, the resulting\ncrash may have no obvious connection to the actual mis-allocated register. The\nchecker is able to point to a specific register use at a specific instruction\nand say \"this register is wrong\". Such a result makes for much smoother\ndebugging!<\/p>\n<p>Let's now walk through how we build the \"checker\" whose goal is to\nverify a particular register allocation is correct. We will come at\nthe solution in stages, first reasoning about the easiest case --\nstraight-line code -- and then introducing control flow. At the end,\nwe'll have a simple algorithm that runs in linear time (relative to\ncode size) and whose simplicity allows us to be reasonably confident\nin its guarantees.<\/p>\n<h3 id=\"symbolic-equivalence-and-abstract-interpretation\">Symbolic Equivalence and Abstract Interpretation<\/h3>\n<p>Recall that we described above a sort of symbolic interpretation of\nexecution: one can reason about CPU registers containing \"symbolic\"\nvalues, where each symbol represents a virtual register in the\noriginal code. For example, we can take the code<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>mov v0, 1<\/span><\/span>\n<span class=\"giallo-l\"><span>mov v1, 2<\/span><\/span>\n<span class=\"giallo-l\"><span>add v2, v0, v1<\/span><\/span>\n<span class=\"giallo-l\"><span>return v2<\/span><\/span><\/code><\/pre>\n<p>and a register-allocated form of that code<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>mov r0, 1<\/span><\/span>\n<span class=\"giallo-l\"><span>mov r1, 2<\/span><\/span>\n<span class=\"giallo-l\"><span>add r0, r0, r1<\/span><\/span>\n<span class=\"giallo-l\"><span>return r0<\/span><\/span><\/code><\/pre>\n<p>and <em>somehow<\/em> find a set of substitutions that makes them equivalent:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>mov r0, 1<\/span><\/span>\n<span class=\"giallo-l\"><span>      [ r0 = v0 ]<\/span><\/span>\n<span class=\"giallo-l\"><span>mov r1, 2<\/span><\/span>\n<span class=\"giallo-l\"><span>      [ r1 = v1 ]<\/span><\/span>\n<span class=\"giallo-l\"><span>add r0, r0, r1<\/span><\/span>\n<span class=\"giallo-l\"><span>      [ r0 = v2 ]<\/span><\/span>\n<span class=\"giallo-l\"><span>return r0<\/span><\/span><\/code><\/pre>\n<p>But how do we solve for these substitutions? Recall that above we\nhinted at a form of execution that operates on symbols rather than\nvalues. We can simply take the semantics of the original instruction\nset, and reformulate it to operate on symbolic values instead, and\nthen step through the code to find a representation of <em>all\nexecutions<\/em> at once. This is called symbolic execution, and\nwith some enhancements described below, is the basis of <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Abstract_interpretation\">abstract\ninterpretation<\/a><sup class=\"footnote-reference\"><a href=\"#4\">4<\/a><\/sup>.\nIt is a very powerful technique!<\/p>\n<p>What are the semantics of the instruction set that are relevant here?\nIt turns out, because the register allocator does not modify any of\nthe program's original instructions<sup class=\"footnote-reference\"><a href=\"#5\">5<\/a><\/sup>, we can understand each\ninstruction as <em>mostly<\/em> an arbitrary, opaque operator. The only\nimportant pieces of information are which registers it <em>reads<\/em> (before\nits operation) and which it <em>writes<\/em> (after its operation).<sup class=\"footnote-reference\"><a href=\"#6\">6<\/a><\/sup><\/p>\n<p>It turns out that to verify the output of the register allocator when\nit <em>spills<\/em> values, and when it <em>moves<\/em> values between registers, we\nneed to have special knowledge of spills, reloads, and moves. Hence,\nwe can reduce the input program to a sort of minimal ISA that captures\nonly what is important for symbolic reasoning (the real definition is\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/blob\/109455ce4cea07a6e8d87e06d200c1318605c0ea\/lib\/src\/checker.rs#L393-L430\">here<\/a>;\nwe simplify a bit for this post):<\/p>\n<ul>\n<li>\n<p><code>Spill &lt;spillslot&gt;, &lt;CPU register&gt;<\/code>: copy data (symbol representing\nvirtual register) from a register to a spill slot.<\/p>\n<\/li>\n<li>\n<p><code>Reload &lt;CPU register&gt;, &lt;spillslot&gt;<\/code>: copy data from a spill slot to\na register.<\/p>\n<\/li>\n<li>\n<p><code>Move &lt;CPU register&gt;, &lt;CPU register&gt;<\/code>: move data from one CPU\nregister to another (N.B.: <em>only<\/em> regalloc-inserted moves are\nrecognized as a <code>Move<\/code>, not moves in the original input program.)<\/p>\n<\/li>\n<li>\n<p><code>Op read:&lt;CPU register list&gt;, read_orig:&lt;virtual register list&gt; write:&lt;CPU register list&gt; write_orig:&lt;virtual register list&gt;<\/code>: some\narbitrary operation that reads some registers and writes some other\nregisters.<\/p>\n<\/li>\n<\/ul>\n<p>The last instruction is the most interesting: notice that it carries\nthe <em>original<\/em> virtual registers as well as the\n<em>post-register-allocation CPU registers<\/em> for the instruction. The need\nfor this will become clearer below, but the intuition is that we\nneed to see <em>both<\/em> in order to establish the <em>correspondence<\/em> between\nthe two.<\/p>\n<p>We can produce the above instructions while the register allocator is\nscanning over the code and editing it; that part is a <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/blob\/d1956e08b5a3d6759bccf067c9739cd1a8c23be9\/lib\/src\/checker.rs#L16-L42\">straightforward\ntranslation<\/a>. Once\nwe have the <em>abstracted<\/em> program, we can \"execute\" it over the domain\nof symbols. How do we do this? With the following rules:<\/p>\n<ul>\n<li>\n<p>We maintain some <em>state<\/em>, just as a real CPU does: for each CPU\nregister, and for each location in the stack frame, we track a\n<em>symbol<\/em> (rather than an integer value). This symbol can be a\nvirtual-register name, if we know that the storage location\ncurrently contains that register's value. It can also be <code>Unknown<\/code>,\nif the checker doesn't know, or <code>Conflicted<\/code>, if the value could be\none of several virtual registers. (The difference between the latter\ntwo will become clear when we discuss control-flow below. For now\nit's enough to see that we abstract the state to: either we know the\nslot contains a program value, symbolically, or we know nothing.)<\/p>\n<\/li>\n<li>\n<p>When we see a <code>Spill<\/code>, <code>Reload<\/code>, or <code>Move<\/code>, we copy the symbolic\nstate from the source location (register or spill slot) to the\ndestination location. In other words, we know that these\ninstructions always move the integer value of a register or memory\nword, whatever it may be; so if we have knowledge about the source\nlocation, symbolically for all possible executions, then we can\nextend that knowledge to the destination as well.<\/p>\n<\/li>\n<li>\n<p>When we see an <code>Op<\/code>, we do some checks then some updates:<\/p>\n<ul>\n<li>\n<p>For each <em>read<\/em> (input) register, we examine the symbolic value\nstored in the given CPU register (post-allocation location). If\nthat symbol matches the virtual register that the original\ninstruction used, then the allocator has properly conveyed the\nvirtual register's value to its use here, and thus the allocation\nis <em>correct<\/em> (preserves program dataflow). If not, we can signal a\nchecker error, and look for the bug in our register allocator. We\nknow <em>for sure<\/em> it must be a bug (i.e., there are no false\npositives), because we only track a symbol for a storage location\nwhen we have proven (for all executions!) that that storage must\ncontain that virtual register.<\/p>\n<\/li>\n<li>\n<p>For each <em>write<\/em> (output) register, we set the symbolic value\nstored in the given CPU register to be the given (pre-allocation)\nvirtual register. In other words, each write <em>produces<\/em> a\nsymbol. This symbol then flows through the program, moving via\nspills\/reloads\/moves, until it reaches consumers.<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>And that's it! We can prove in a fairly straightforward way that this\nis exactly correct -- produces no false positives or false negatives\n-- for straight-line code (code with no jumps). We can do this by\ninduction: if the symbolic state is correct before an instruction,\nthen the above rules just encode the data movement that the concrete\nprogram performs, and the symbolic state will be updated in the same\nway, so the symbolic state after the instruction is also correct.<\/p>\n<p>Note that this is <em>linear<\/em> as well -- so it's very fast, with a single\nscan over straight-line code. This is possible because we have <em>help<\/em>\nfrom the register allocator: we know about spills, reloads, and\nregister allocator-inserted moves, and we have pre- and\npost-allocation registers for all other instructions. Consider what we\nwould have to do if we did not know about these, but only saw machine\ninstructions. In that case, any load, store or move instruction could\nhave come from the allocator or from the original program. We would\nhave nothing but a graph of operators with connectivity between them,\nand we would have to solve a <em>graph isomorphism<\/em> problem. That is much\nharder, and much slower!<\/p>\n<p>So are we done? Not quite: we have only considered straight-line\ncode. What happens when we encounter a jump?<\/p>\n<h3 id=\"control-flow-joins-lattices-and-iterative-dataflow-analysis\">Control-Flow Joins, Lattices, and Iterative Dataflow Analysis<\/h3>\n<p>Control-flow makes analysis interesting because it allows for\n<em>multiple possibilities<\/em>. Consider a simple program with an\nif-then-else pattern (a \"control-flow diamond\", as it is sometimes\ncalled, due to its shape):<\/p>\n<p><img src=\"\/assets\/2021-03-10-diamond-predicate-web.svg\" alt=\"Figure: A control-flow diamond and symbolic analysis\" \/><\/p>\n<p>Let's say that a symbolic analysis decides that on the left branch,\n<code>r0<\/code> has symbolic state <code>A<\/code>, and on the right branch, it has symbolic\nstate <code>B<\/code>. What state does it have in the lower block, after the two\npaths re-join?<\/p>\n<p>We can give a precise answer if we are allowed to \"predicate\", or make\nthe answer conditional on some other program state. For example, if we\nknew that the if-condition were represented by some symbol <code>C<\/code> that\nhas a boolean type, we could invent an abstract expression language\nand then write <code>if C { A } else { B }<\/code>.<\/p>\n<p>However, this quickly becomes untenable. We will find that programs\nwith loops lead to <em>unbounded<\/em> symbolic expressions. (To see this,\nconsider that a symbolic representation can have a size larger than\nits inputs. Any cyclic data dependency around a loop could thus\ngenerate an infinitely-large symbolic representation.) Even with only\nacyclic control flow, path-sensitive symbolic expressions can grow\nexponentially with program size: consider that a program with <code>N<\/code>\nbasic blocks and no loops can have <code>O(2^N)<\/code> paths through those\nblocks, and fully precise symbolic expressions would need to capture\nthe effects of each of those paths.<\/p>\n<p>We thus need some way to <em>approximate<\/em>. Note that an abstract\ninterpretation of a program need not precisely capture all of the\nprogram's behavior losslessly. For example, we might perform a simple\nabstract interpretation analysis that only tracks possible numeric\nsigns (positive, negative, unknown) for integer variables. So it is\nalways fine to \"summarize\" and drop detail to remain tractable. Let us\nthus consider how we might \"merge\" state when multiple possibilities\nexist.<\/p>\n<p>It turns out that there is a very nice mathematical object that\ncaptures the notion of \"merging\" in a way that is very useful: the\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Lattice_(order)\">lattice<\/a>.  A lattice\nconsists of a set of elements and a <em>partial order<\/em> between them,\ntogether with a least element \"bottom\" and a greatest element \"top\",\nan operator called \"meet\" that finds the \"greatest lower bound\" for\nany two elements (the largest element that is less than its two\noperands) and a \"join\" that finds the \"least upper bound\" (the dual of\nthe above).<\/p>\n<p><img src=\"\/assets\/Hasse_diagram_of_powerset_of_3.svg\" alt=\"Figure: A lattice\" \/><\/p>\n<p>(Figure credit:\n<a rel=\"external\" href=\"https:\/\/commons.wikimedia.org\/wiki\/File:Hasse_diagram_of_powerset_of_3.svg\">Wikimedia<\/a>,\n<a rel=\"external\" href=\"https:\/\/creativecommons.org\/licenses\/by-sa\/3.0\/deed.en\">CC BY-SA\n3.0<\/a>)<\/p>\n<p>An extremely useful property of lattices is that their merging operations\nof meet and join are <em>commutative, associative and reflexive<\/em>. This is a\nformal way of saying that the result only depends on the set of elements\n\"thrown into the mix\", in any order and with any repetition. In other\nwords, the meet of many elements is a function only of the set of elements,\nnot of the order in which we process them.<\/p>\n<p>How is this useful? If we define particular analysis states -- and as\na reminder, in our specific case, these are maps from CPU registers\nand spillslots to symbolic virtual registers -- to be lattice\nelements, and define a \"meet function\" that somehow merges the states\n-- then we can use this merging behavior to implement a sort of\nprogram analysis over all programs, <em>including<\/em> those with loops,\nwithout unbounded analysis growth! This is called the\n\"meet-over-all-paths\" solution and is a standard way that compilers\nperform <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Data-flow_analysis\">dataflow\nanalysis<\/a> today.<sup class=\"footnote-reference\"><a href=\"#7\">7<\/a><\/sup><\/p>\n<p>To understand how a lattice describes \"merging\" in a program analysis in a\nuseful way, one can see the lattice ordering relation (the arrows in the\nfigure above) as denoting that one state is more or less refined (contains\nmore or less knowledge) than another. One starts at the \"greatest\" or\n\"top\" element: anything could be true; we know nothing. We then move to\nprogressively more refined states.  One analysis state is ordered \"less\nthan\" another if it captures all the constraints we have learned in the\nother state, plus some new ones. The \"meet\" operator, which computes the\ngreatest lower bound, will thus give us an analysis state that captures all\nof the knowledge in both inputs, and no more.<sup class=\"footnote-reference\"><a href=\"#8\">8<\/a><\/sup><\/p>\n<p>The general approach to performing an analysis on an arbitrary CFG\nis as follows:<\/p>\n<ol>\n<li>\n<p>We define our analysis state as a <em>lattice<\/em>.<\/p>\n<\/li>\n<li>\n<p>We trace the current analysis state at each <em>program point<\/em>, or\npoint between instructions. Initially, the state at every program\npoint is the \"top\" lattice element; as values meet, they move\n\"down\" the lattice, toward the \"bottom\" element.<\/p>\n<\/li>\n<li>\n<p>We process the effect of each instruction, computing the state at\nits post-program-point from its pre-program-point.<\/p>\n<\/li>\n<li>\n<p>When analysis state reaches a control-flow edge, we propagate the\nstate across the edge, and <em>meet<\/em> it with the incoming state from\nall other edges. This may then lead us to recompute states in the\ndestination block.<\/p>\n<\/li>\n<li>\n<p>We run a \"fixpoint\" loop, processing updates as analysis states at\nblock entries change, until no more changes occur.<\/p>\n<\/li>\n<\/ol>\n<p>In this way, we find a solution to the dataflow problem that satisfies\nall of the instruction semantics for <em>any<\/em> path through the\nprogram. It may not be fully precise (i.e., it may not answer every\nquestion) -- because it is often impossible to capture a fully precise\nanswer for executions that include loops, and impractical for programs\nwith significant control-flow -- but it is <em>sound<\/em>, in the sense that\nany claims we make from the analysis result will be <em>correct<\/em>.<\/p>\n<h2 id=\"a-register-checker-as-a-dataflow-analysis-problem\">A Register Checker as a Dataflow Analysis Problem<\/h2>\n<p>We now have all of the pieces that we need in order to check the\nregister-allocator output for any program. We saw above that we could\nmodel the machine state symbolically for any straight-line code, which\nallows us to detect register allocator errors exactly (no false\nnegatives and no false positives) as long as there is no control\nflow. We then discussed the usual static analysis approach to control\nflow. How can we combine the two?<\/p>\n<p>The answer is that we define a <em>lattice<\/em> of <em>symbolic register state<\/em>,\nand then walk through the same per-instruction semantics as above in a\nfixpoint dataflow analysis. Put simply, for each storage location (CPU\nregister or spill slot), we have a lattice:<\/p>\n<p><img src=\"\/assets\/2020-12-23-lattice.svg\" alt=\"Figure: Abstract value lattice\" \/><\/p>\n<p>The \"unknown\" state is the \"top\" lattice value. This means simply that\nwe don't know what is in the register because the analysis hasn't\nconverged yet (or no write has occurred).<\/p>\n<p>The \"conflicted\" state is the \"bottom\" lattice value. This means that\ntwo or more symbolic definitions have merged. Rather than try to\nrepresent a superposition of both with some sort of predication or\nloop summary, we simply give up and move to a state that indicates\n\"bad value\". This is not a checker error <em>as long as it is never\nused<\/em>, and it can be overwritten with a good value at any time; but if\nthe value is used as an instruction source, then we flag an error.<\/p>\n<p>The meet function, then, is <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/blob\/109455ce4cea07a6e8d87e06d200c1318605c0ea\/lib\/src\/checker.rs#L144-L155\">very\nsimple<\/a>:\ntwo registers meet to \"conflicted\" unless they are the same register;\n\"unknown\" meets with anything to produce that anything; and\n\"conflicted\" is contagious, in the sense that meeting any other state\nwith \"conflicted\" remains \"conflicted\".<\/p>\n<p>Note that we said above that our analysis state is a <em>map<\/em> from\nregisters and spill slots to symbolic states; not just a single\nsymbolic state. So our lattice is actually a\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Product_order\">product<\/a> of each\nindividual storage location's state, and we meet symbols\npiecewise. (The resulting map contains entries only for keys that\nappear in all meet-inputs; i.e. we take the intersection of the\ndomains.)<\/p>\n<p>With the analysis state and its meet-function defined, we run a\ndataflow analysis loop, allow it to converge, and look for errors; and\nwe're done!<\/p>\n<p>And that's it!<sup class=\"footnote-reference\"><a href=\"#9\">9<\/a><\/sup><\/p>\n<h2 id=\"effectiveness-can-it-find-bugs\">Effectiveness: Can it Find Bugs?<\/h2>\n<p>The short answer is that yes, it can find some <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/regalloc.rs\/pull\/86\">pretty subtle\nbugs<\/a>!<\/p>\n<p>The benefit of the regalloc.rs checker is twofold:<\/p>\n<ul>\n<li>\n<p>It has found real bugs. In the above example, there was a conceptual\nerror in the reference-types (precise GC rooting) support: in\ncertain cases where a spillslot was allocated for a pointer-typed\nvalue but never used, it could be added to the stackmap (list of\npointer-typed spillslots) provided to the GC. This bug needs a\nspecific set of circumstances to happen: we have to have enough\nregister pressure that we decide to allocate a spillslot for a\nvirtual register, but then hit the (rare) code-path in which we\ndon't actually need to do the spill because a register became\navailable. We never hit this in our other, hand-written tests of GC\n(Wasm reference types), despite some\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/source\/js\/src\/jit-test\/tests\/wasm\/ref-types\/stackmaps1.js\">pretty<\/a>\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/source\/js\/src\/jit-test\/tests\/wasm\/ref-types\/stackmaps2.js\">extensive<\/a>\n<a rel=\"external\" href=\"https:\/\/searchfox.org\/mozilla-central\/source\/js\/src\/jit-test\/tests\/wasm\/ref-types\/stackmaps3.js\">tests<\/a>\nat least in SpiderMonkey's WebAssembly test suite driving the\nCranelift backend. The fuzzer was able to drive toward full\ncoverage, hit this rare code-path, and then allow the checker to\ndiscover the error.<\/p>\n<\/li>\n<li>\n<p>It serves as a gold-standard test while developing <em>new<\/em> register\nallocators. Feedback while developing the linear-scan allocator\n(whose reference-type \/ precise GC rooting support came a bit later\nthan the backtracking allocator's) indicated that the checker found\nmany real issues and allowed for faster and more confident progress.<\/p>\n<\/li>\n<\/ul>\n<h2 id=\"related-work\">Related Work<\/h2>\n<p>It's surprisingly difficult to find prior work on checkers that\nvalidate individual runs of a register allocator.  There are several\nfully-verified compilers in existence;\n<a rel=\"external\" href=\"https:\/\/compcert.org\/\">CompCert<\/a> and <a rel=\"external\" href=\"https:\/\/cakeml.org\/\">CakeML<\/a>\nare two that can compile realistic languages (C and ML,\nrespectively). These compilers have fully verified register allocators\nin the sense that the algorithm itself is proven correct; there is no\nneed to run a checker on an individual compilation result. The\nengineering effort to achieve this is much higher than to write a\nchecker, however (in the latter case, ~700 lines of Rust).<\/p>\n<p>CakeML's approach to proving the register allocator correct is\ndescribed by Tan et al. in \"<a rel=\"external\" href=\"https:\/\/kar.kent.ac.uk\/71304\/1\/paper.pdf\">The Verified CakeML Compiler\nBackend<\/a>\" (J. Func Prog 29,\n2019). They appear to have nicely factored the problem so that the\ncompilation is correct as long as a valid graph coloring or\n\"permutation\" (mapping of program values to storage slots) is\nprovided. This allows reasoning about the core issue (dataflow\nequivalence before and after allocation) separately from the details\nof the allocator (graph coloring algorithm).<\/p>\n<p>Proof-producing compilers exist as well: for example,\n<a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/abs\/10.1145\/3192366.3192377\">Crellvm<\/a> is a\nrecent extension of several LLVM passes that generates a\n(machine-checkable) correctness proof alongside the transformed\nprogram. This approach is conceptually at the same level as our\nregister-allocator checker: it results in the validation of a single\ncompiler run, but is much easier to build than a full a-priori\ncorrectness proof. This effort does not yet appear to address register\nallocation, however.<\/p>\n<p>Rideau and Leroy in \"<a rel=\"external\" href=\"https:\/\/xavierleroy.org\/publi\/validation-regalloc.pdf\">Validating Register Allocation and\nSpilling<\/a>\" (CC\n2010) describe a similar taxonomy to ours, separating \"once and for\nall\" correctness proofs from \"translation validation checks\" and\nproviding the latter. Their validator, however, defines a fairly\ncomplex transfer function that builds a set of equality constraints\nthat must be solved. It appears that the validator does not leverage\nhints from the allocator, specifically w.r.t. spills, reloads and\ninserted moves as distinguished from stores, loads and moves in the\noriginal program; without these hints, a much more general and complex\ndataflow-equivalence scheme is needed.<\/p>\n<p>Nandivada et al. in \"<a rel=\"external\" href=\"https:\/\/homepages.dcc.ufmg.br\/~fernando\/publications\/papers\/SAS07.pdf\">A Framework for End-to-End Verification andEvaluation of\nRegister\nAllocators<\/a>\"\n(SAS 2007) describe a system very similar to our checker in which physical\nregister contents (as virtual-register or \"pseudo\" symbols) are encoded into a\npost-regalloc IR that is then typechecked. Their typechecker can uncover the\nsame sorts of regalloc errors that our checker can. Thus, their approach is\nlargely equivalent to ours; the main difference is that we do not encode the\nproblem as typechecking on a dedicated IR but rather a standalone static\nanalysis.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>This post concludes the three-post series\n(<a href=\"\/blog\/2020\/09\/18\/cranelift-isel-1\/\">one<\/a>,\n<a href=\"\/blog\/2021\/01\/22\/cranelift-isel-2\/\">two<\/a>) describing the work we've\ndone to develop all the pieces of Cranelift's new backend over the\npast year!  It has been a very interesting and educational ride for me\npersonally; I discovered an entirely new world of interesting problems\nto solve in the compiler backend, as distinct from the \"middle end\"\n(IR-level optimizations) that is more commonly taught and\nstudied. Additionally, the focus on <em>fast<\/em> compilation is an\ninteresting twist, and one that I believe is not studied enough. It is\neasy to justify higher analysis precision and better generated code\nthrough ever-more-complex techniques; the benefit to be found in\ndesign tradeoffs for fast compilation is more subjective and more\ndependent on workload.<\/p>\n<p>It is my hope that these writeups have illuminated some of the\nthinking that went into our design decisions. Our work is by no means\ndone, however! The <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/rfcs\/pull\/8\">roadmap for Cranelift work in\n2021<\/a> lists a number\nof ideas that we've discussed to achieve higher compiler performance\nand better code quality. I am excited to explore these more in the\ncoming year; they may even result in more blog posts. Until then,\nhappy compiling!<\/p>\n<p><em>For discussions about this post, please feel free to join us on our Zulip\ninstance in <a rel=\"external\" href=\"https:\/\/bytecodealliance.zulipchat.com\/#narrow\/stream\/217117-cranelift\/topic\/blog.20post.20on.20regalloc.20checker\">this\nthread<\/a>.<\/em><\/p>\n<hr \/>\n<p><em>Thanks to <a rel=\"external\" href=\"https:\/\/www.reddit.com\/u\/po8\">\/u\/po8<\/a> on Reddit for <a rel=\"external\" href=\"https:\/\/www.reddit.com\/r\/rust\/comments\/m5w3y4\/cranelift_part_3_correctness_in_register\/gr2w3i5\/\">several\nsuggestions<\/a>\nwhich I have incorporated. Thanks also to bjorn3 for several suggestions.\nFinally, thanks to <a rel=\"external\" href=\"https:\/\/homepages.dcc.ufmg.br\/~fernando\/\">Fernando M Q\nPereira<\/a> for bringing my attention to\nhis\n<a rel=\"external\" href=\"https:\/\/homepages.dcc.ufmg.br\/~fernando\/publications\/papers\/SAS07.pdf\">paper<\/a>\nin SAS 2007 that proposes a very similar idea, which I've added to the related\nwork section. Any and all feedback is welcome!<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>Why do CPUs have a limited number of registers? The bound is\nmostly due to <em>ISA encoding limitations<\/em>: there are only so many\nbits in an instruction to name a particular register source or\ndestination. When the CPU designer chooses how many registers to\ndefine, providing more will improve performance (up to a point)\nbecause the CPU can hold more state at one time, but will also\nimpose an increasing cost in code size and CPU\ncomplexity.<\/p>\n<p>Computer architect's tangent: due to <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Register_renaming\">register\nrenaming<\/a>, a\nmodern high-performance out-of-order CPU will have many more\n<em>physical<\/em> registers, with architectural register names mapped\nto physical registers at any given program point by the\nregister-renaming hardware (in common parlance, the register\nallocation table or RAT), but the ISA encoding restrictions\nlimit the number that have architectural names at any time. The\nexistence of register renaming sometimes causes confusion in\ndiscussions of register allocation -- why rename onto so few\nregisters when we have so many? -- well, we could do much better\nif we had more bits to refer to them all! Architectural\nstandardization is another reason for this: we would not want to\nrecompile code every time the PRF (physical register file)\nbecame larger. Simpler to say \"x86-64 has 16 integer registers\"\nand be done with it.<sup class=\"footnote-reference\"><a href=\"#10\">10<\/a><\/sup><\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>We don't know if exponential time is the <em>best<\/em> we can do in the\nworst case, though most computer scientists suspect so. This is\nthe famous <code>P=NP<\/code> problem, and if you can solve it, you <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Millennium_Prize_Problems#P_versus_NP\">win a\nmillion\ndollars<\/a>.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>A slight correction from <a rel=\"external\" href=\"https:\/\/www.reddit.com\/r\/rust\/comments\/m5w3y4\/cranelift_part_3_correctness_in_register\/gr2w3i5\/\">\/u\/po8's\ncomment<\/a>:\nregister allocation on <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Structured_programming\">structured\nprograms<\/a> <a rel=\"external\" href=\"https:\/\/link.springer.com\/chapter\/10.1007\/978-3-642-37051-9_1\">can be\ndone<\/a> in\npolynomial time, i.e., better than an exponential brute-force search.\nHowever, the problem remains quite complex!<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"4\"><sup class=\"footnote-definition-label\">4<\/sup>\n<p>Abstract interpretation was introduced by Radhia and Patrick\nCousot in their seminal 1977 POPL paper \"Abstract\ninterpretation: A Unified Lattice Model for Static Analysis of\nPrograms by Construction or Approximation of Fixpoints\"\n(<a rel=\"external\" href=\"https:\/\/www.di.ens.fr\/~cousot\/publications.www\/CousotCousot-POPL-77-ACM-p238--252-1977.pdf\">pdf<\/a>).<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"5\"><sup class=\"footnote-definition-label\">5<\/sup>\n<p>Except for move elimination, but we can ignore that for now --\nit is possible to adapt the abstract interpretation rules to\naccount for it later.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"6\"><sup class=\"footnote-definition-label\">6<\/sup>\n<p>In regalloc.rs we also have a notion of an instruction that\n\"modifies\" a register, which is like a combined read and write\nexcept that the value must be mapped to the <em>same<\/em> register for\nboth. This isn't fundamental to the point we're illustrating so\nwe'll skip over it for now.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"7\"><sup class=\"footnote-definition-label\">7<\/sup>\n<p>This dataflow analysis approach was proposed by <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Gary_Kildall\">Gary\nKildall<\/a> in the POPL\n1973 paper \"<a rel=\"external\" href=\"https:\/\/dl.acm.org\/doi\/10.1145\/512927.512945\">A unified approach to global program\noptimization<\/a>\". (He\nis perhaps better-known for writing the microcomputer OS\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/CP\/M\">CP\/M<\/a>, a predecessor to\nDOS.) Kildall's Dataflow analysis builds on the control-flow\ngraph ideas invented several years prior by <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Frances_Allen\">Fran\nAllen<\/a>; in her 1970\npaper <a rel=\"external\" href=\"https:\/\/www.cs.columbia.edu\/~suman\/secure_sw_devel\/p1-allen.pdf\">Control Flow\nAnalysis<\/a>,\nshe proposes interval-based dataflow analysis, which is the\nother main approach known and used today.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"8\"><sup class=\"footnote-definition-label\">8<\/sup>\n<p>Note that we have been somewhat vague here about directionality.\nWhat does \"more constrained\" or \"more refined\" mean? There are actually\ntwo directions an analysis may work, and these have to do with how it\nhandles imprecision. A \"may-analysis\", or \"widening analysis\", computes\nwhat the program may do. It generally begins with an \"empty set\" of sorts\n-- a variable has no possible values, a statement has no side-effects, a\nregister contains nothing -- and then uses a <em>union<\/em>-like meet operator\nto aggregate all <em>possibilities<\/em>. The real program behavior will be some\nsubset of these possibilities. In contrast, a \"must-analysis\", or\n\"narrowing analysis\", computes only what we know the program <em>must<\/em> do.\nIt generally begins with the \"universe set\" and then uses\n<em>intersection<\/em>-like meet operators. The real program's behavior is a\nsuperset of this analysis's description. We can't have both, usually,\nbecause an analysis cannot generally be fully precise.<\/p>\n<p>By convention, we always start analysis values at \"top\" and use\n\"meet\" to move down the lattice as the analysis converges, though\nwe could just as well start at \"bottom\" and move up with \"join\",\nsince flipping the lattice's order relation and swapping meet and\njoin produces another lattice.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"9\"><sup class=\"footnote-definition-label\">9<\/sup>\n<p>Well, not quite, as you might have guessed. One significant\ndetail I've omitted is how we handle <em>reference types<\/em> and\n<em>precise garbage collection<\/em>. Precise GC rooting entails tracking\na specific kind of <em>type information<\/em> for each register and\nspillslot: specifically, whether each storage location contains a\n<em>pointer<\/em> that the GC should observe when it performs a garbage\ncollection. It is important in many applications for this to be\n\"precise\", which means that we can only say that a register\ncontains a pointer if it <em>actually<\/em> does, and we <em>must<\/em> include\nall registers that contain pointers. Precision is important\nbecause the GC will assume any root pointer it traces points to a\nvalid object (so false positives are bad); and must know about\nevery pointer in case it is a moving GC and relocates an object\n(so false negatives are bad).<\/p>\n<p>In our particular variant of the problem, we need this information at\n<em>safepoints<\/em>: these are points at which the GC could be invoked. (It\nwould be too expensive to plan for a GC invocation at every point in\nthe program.) Furthermore, we needed to support GCs that could only\ntrace pointers on the stack (hence, spillslots), not in registers. So\nwe needed to induce <em>additional spills<\/em> around safepoints to ensure\npointers were only live on the stack, not in registers.<\/p>\n<p>To check this, we extended the abstract value lattice to note whether\neach virtual register is a pointer-typed value or not. Then, at every\nsafepoint, we (i) ensure that every actual pointer-typed value in a\nspillslot is listed in the stackmap provided to the GC, and (ii)\n<em>clear<\/em> any other pointer-typed stack location not listed in the\nstack map to an <code>Unknown<\/code> state. Why the latter? Because an actual\npointer-typed value in a stack slot might be \"dead\" (not used again),\nand so is legal to omit from the stackmap; instead of immediately\nflagging an error when one is excluded, we simply ensure that a\nlater use of it is invalid.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"10\"><sup class=\"footnote-definition-label\">10<\/sup>\n<p>Note that some computer architectures <em>do<\/em> task the compiler\nwith some form of register renaming. For example, the Intel\nItanium (<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/IA-64\">IA-64<\/a>) had a\nnovel sort of \"rotating register reference\" feature for loops,\nand trusted the compiler with managing a full 128 integer and\n128 floating-point registers. Modern GPUs also have thousands\nof \"registers\" managed by the compiler.<\/p>\n<\/div>\n"},{"title":"Cranelift, Part 2: Compiler Efficiency, CFGs, and a Branch Peephole Optimizer","published":"2021-01-22T00:00:00+00:00","updated":"2021-01-22T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2021\/01\/22\/cranelift-isel-2\/"}},"id":"https:\/\/cfallin.org\/blog\/2021\/01\/22\/cranelift-isel-2\/","content":"<p>This post is the second in a three-part series about\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\">Cranelift<\/a>.\nIn the <a href=\"\/blog\/2020\/09\/18\/cranelift-isel-1\/\">first post<\/a>, I described\nthe context around Cranelift and our project to replace its backend\ncode-generation infrastructure, and detailed the instruction-selection\nproblem and how we solve it. The remaining two posts will be\ndeep-dives into some interesting engineering problems.<\/p>\n<p>In this post, I want to dive into the <em>compiler performance<\/em> aspect of\nour work more deeply. (In the next post we'll explore correctness.)\nThere are many interesting aspects of compilation speed I could talk\nabout, but one particularly difficult problem is the handling of\n<em>control flow<\/em>: how do we translate structured control flow at the\nWasm level into control-flow graphs at the IR level, and finally to\nbranches in a linear stream of instructions at the machine-code level?<\/p>\n<p>Doing this translation efficiently requires careful attention to the\noverall pass structure, with the largest wins coming when one can\ncompletely eliminate a category of work. We'll see this in how we\ncombine several passes in a traditional lowering design (critical-edge\nsplitting, block ordering, redundant-block elimination, branch\nrelaxation, branch target resolution) into <em>inline transforms<\/em> that\nhappen during other passes (lowering of the CLIF, or Cranelift IR,\ninto machine-specific IR; and later, binary emission).<\/p>\n<p>This post basically describes the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/machinst\/buffer.rs\"><code>MachBuffer<\/code><\/a>,\na \"smart machine-code buffer\" that knows about branches and edits them\non-the-fly as we emit them, and the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/machinst\/blockorder.rs\"><code>BlockLoweringOrder<\/code><\/a>,\nwhich allows us to lower code in final basic-block order, with split\ncritical edges inserted implicitly, by traversing a never-materialized\nimplicit graph. The work was done mostly in <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/1718\">Cranelift PR\n#1718<\/a>, which\nresulted in a ~10% compile-time improvement and a ~25%\ncompile+run-time improvement on a CPU-intensive benchmark (<code>bz2<\/code>).<\/p>\n<h2 id=\"control-flow-graphs\">Control-Flow Graphs<\/h2>\n<p>Before we discuss any of that, we need to review control-flow graphs\n(CFGs)! The CFG is a fundamental data structure used in almost all\nmodern compilers. In brief, it represents how execution (i.e., program\ncontrol) may flow through instructions, using graph nodes to represent\nlinear sequences of instructions and graph edges to represent all\npossible control-flow transfers at branch instructions.<\/p>\n<p>At the end of the instruction selection process, which we learned\nabout in the <a href=\"\/blog\/2020\/09\/18\/cranelift-isel-1\/\">previous post<\/a>, we\nhave a function body lowered into VCode that consists of <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Basic_block\"><em>basic\nblocks<\/em><\/a>. A basic block is\na contiguous sequence of instructions that has no outbound branches\nexcept at the end, and has no inbound branches except at the\nbeginning. In other words, it is \"straight-line\" code: execution\nalways starts at the top and proceeds to the end. An example\ncontrol-flow graph (CFG) consisting of four basic blocks is shown\nbelow:<\/p>\n<p><img src=\"\/assets\/2020-10-08-cfg-web.svg\" alt=\"Figure: Control-flow graph with four basic blocks in a diamond\" \/><\/p>\n<p>Control-flow graphs are excellent data structures for compilers to\nuse. By making the flow of execution explicit as graph edges, rather\nthan reasoning about instructions in order in memory as the processor\nsees them, many analyses can be performed more easily. For example,\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Data-flow_analysis\">dataflow analysis<\/a>\nproblems can be solved easily because the CFG makes traversal of\npossible control-flow transfers easy. Graph-based representations of\nthe program also allow easier <em>moving and insertion of code<\/em>: it is\nless error-prone to manipulate an explicit graph than to reason about\nimplicit control-flow (e.g. fallthrough from a not-taken conditional\nbranch). Finally, the graph representation factors out the question of\n<em>block ordering<\/em>, which can be important for performance; we can\naddress this problem separately by choosing how we serialize the graph\nnodes (blocks). For these reasons, most compiler IRs, including\nCranelift's CLIF and <code>VCode<\/code>, are CFG-based.<\/p>\n<p>(Historical note: control-flow graphs were invented by the late\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Frances_Allen\">Frances Allen<\/a>, who\nlargely established the algorithmic foundations that modern compilers\nuse. Her paper <a rel=\"external\" href=\"https:\/\/www.clear.rice.edu\/comp512\/Lectures\/Papers\/1971-allen-catalog.pdf\">A catalogue of optimizing\ntransformations<\/a><sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>\ncovers essentially all of the important optimizations used today and\nis well worth a read.)<\/p>\n<h2 id=\"cpus-and-branch-instructions\">CPUs and Branch Instructions<\/h2>\n<p>To represent a CFG's end-of-block branches at the instruction level,\nwe can use <em>two-way branches<\/em>: these are instructions that branch\neither to one basic-block target if some condition is true, or another\nif the condition is false. (Basic blocks can also end in simple\nunconditional single-target branches.) We wrote such a branch as <code>if r0, L1, L2<\/code> above; this means that the block <code>L0<\/code> will be followed in\nexecution either by <code>L1<\/code> or <code>L2<\/code>, depending on the value in <code>r0<\/code>.<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup><\/p>\n<h3 id=\"branches-with-fallthrough\">Branches with Fallthrough<\/h3>\n<p>However, CPUs rarely have such two-way branch instructions. Instead,\nconditional control-flow in common ISAs is almost always provided with\na <em>conditional branch with fallthrough<\/em>. This is an instruction that,\nif some condition is true, branches to another location; otherwise,\ndoes nothing, and allows execution to continue sequentially. This is a\nbetter fit for a hardware implementation for a number of reasons: it's\neasier to encode one target than two (the destination of the jump\nmight be quite far away for some branches, and instructions have\nlimited bits available), and it's usually the case that the compiler\ncan place one of the successor blocks immediately afterward anyway.<\/p>\n<p>Now, this isn't much of a problem if we just want a working compiler;\ninstead of a two-way branch<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    if r0, L1, L2<\/span><\/span><\/code><\/pre>\n<p>We can write a sequence of branches<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    br_if r0, L1<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto L2<\/span><\/span><\/code><\/pre>\n<p>where <code>br_if<\/code> branches to <code>L1<\/code> or falls through to the unconditional\n<code>goto<\/code>. But this is not so efficient in many cases. Consider what\nwould happen if we laid out basic blocks in the order <code>L0<\/code>, <code>L2<\/code>,\n<code>L1<\/code>, <code>L3<\/code>:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    L0:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      br_if r0, L1<\/span><\/span>\n<span class=\"giallo-l\"><span>      goto L2<\/span><\/span>\n<span class=\"giallo-l\"><span>    L2:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      goto L3<\/span><\/span>\n<span class=\"giallo-l\"><span>    L1:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      goto L3<\/span><\/span>\n<span class=\"giallo-l\"><span>    L3:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      return<\/span><\/span><\/code><\/pre>\n<p>There are two redundant unconditional branches (<code>goto<\/code> instructions),\neach of which uselessly branches to the following instruction. We can\nremove both of them with no ill effects, taking advantage instead of\n<em>fallthrough<\/em>, or allowing execution to proceed directly from the end\nof one block to the start of the next one:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    L0:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      br_if r0, L1<\/span><\/span>\n<span class=\"giallo-l\"><span>      \/\/ ** Otherwise, fall through to L2 **<\/span><\/span>\n<span class=\"giallo-l\"><span>    L2:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      goto L3<\/span><\/span>\n<span class=\"giallo-l\"><span>    L1:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      \/\/ ** Always fall through to L3 **<\/span><\/span>\n<span class=\"giallo-l\"><span>    L3:<\/span><\/span>\n<span class=\"giallo-l\"><span>      ...<\/span><\/span>\n<span class=\"giallo-l\"><span>      return<\/span><\/span><\/code><\/pre>\n<p>This seems like an easy enough problem to solve: we just need to\nrecognize when a branch is redundant and remove it, right? Well, yes,\nbut we can do much better than that in some cases; we'll dig into this\nproblem in significantly more depth below!<\/p>\n<h3 id=\"machine-code-encoding-branch-offsets\">Machine-code Encoding: Branch Offsets<\/h3>\n<p>So far, we've written our machine instructions in a way that humans\ncan read, using <em>labels<\/em> to refer to locations in the instruction\nstream. At the hardware level, however, these labels do not exist;\ninstead, the machine code branches contain target <em>addresses<\/em> (usually\nencoded as relative <em>offsets<\/em> from the branch instruction). In other\nwords, we do not see <code>goto L3<\/code>, but rather <code>goto +32<\/code>.<\/p>\n<p>This gives rise to several complications when emitting machine code\nfrom a list of instruction <code>struct<\/code>s.  At the most basic level, we\nhave to resolve labels to offsets and then patch the branches\nappropriately. This is analogous to (but at a lower level than) the\njob of a <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Linker_(computing)\">linker<\/a>:\nwe resolve symbols to concrete values after deciding placement, and\nthen edit the code according to <em>relocations<\/em> to refer to those\nsymbols. In other words, whenever we emit a branch, we make a note (a\nrelocation, or \"label use\" in our <code>MachBackend<\/code>) to go back later and\npatch it with the resolved label offset.<\/p>\n<p>The second, and more interesting, problem arises because not all\nbranch instructions can necessarily refer to all possible labels! As a\nconcrete example, on AArch64, conditional branches have a \u00b11 MB range,\nand unconditional branches have a \u00b1128 MB range. This arises out of\ninstruction-encoding considerations: particularly in\nfixed-instruction-size ISAs (such as ARM, MIPS, and RISC V), less than\na full machine word of bits are available for the immediate jump\noffset that is embedded in the instruction word. (The instruction\nitself is always a machine-word wide, and we need some bits for the\nopcode and condition code too!) On x86, we have limits for a different\nreason: the variable-width encoding allows either a one-byte offset\n(allowing a \u00b1128 byte range) or four-byte offset (allowing a \u00b12 GB\nrange).<\/p>\n<p>To make a branch to a far-off label, then, on some machines we need to\neither use a different sort of branch than the default choice for the\ninstruction selector, or we need to use a form of <em>indirection<\/em>, by\ntargetting the original branch to <em>another branch<\/em>, the latter in a\nspecial form. The former is tricky because we do not know whether a\ntarget will be in-range until all code is lowered and placement is\ncomputed; so we need to either optimistically or pessimistically lower\nbranches to the shortest or longest form (respectively) and possibly\nswitch later. To make matters worse, as we edit branches to use a\nshorter or longer form, their length may change, moving <em>other<\/em>\ntargets into or out of range; in the most general solution, this is a\n\"fixpoint problem\", where we iterate until no more changes occur.<\/p>\n<h2 id=\"challenges-in-lowering-cfgs-to-machine-code\">Challenges in Lowering CFGs to Machine Code<\/h2>\n<p>So far, we have a way to produce <em>correct<\/em> machine code. To emit the\nfinal code for a two-target branch, we can emit a conditional-\nfollowed by unconditional-branch machine instruction. To resolve\nbranch targets correctly, we can assume that any target could be\nanywhere in memory, and always use the long form of a branch; then we\njust need to come back in one final pass and fill in the offsets when\nwe know them.<\/p>\n<p>We can do much better than this, though! Below I'll describe four\nproblems and the ways that they are traditionally solved.<\/p>\n<h3 id=\"problem-1-efficient-use-of-fallthroughs\">Problem 1: Efficient use of Fallthroughs<\/h3>\n<p>We described above how <em>branch fallthroughs<\/em> allow us to omit some\nsome unconditional branches once we know for sure the order that basic\nblocks will appear in the final binary. In particular, the simple\nlowering of a two-way branch <code>if r0, label_if_true, label_if_false<\/code> to\ntwo one-way branches<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    br_if r0, label_if_true<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto label_if_false<\/span><\/span>\n<span class=\"giallo-l\"><span>label_if_false:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>label_if_true:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span><\/code><\/pre>\n<p>has a completely redundant and useless <code>goto<\/code>! In general, if a branch\ntarget is the very next instruction, we can delete that branch.<\/p>\n<p>However, there are slightly more complex cases where we can also find\nsome improvements. Consider the inverted version of the above:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    br_if r0, label_if_false<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto label_if_true<\/span><\/span>\n<span class=\"giallo-l\"><span>label_if_false:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>label_if_true:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span><\/code><\/pre>\n<p>No branch here branches to its fallthrough, so one might think that\nboth branches are necessary. But in practice, on most CPU\narchitectures, all conditional branches have <em>inverted forms<\/em>. For\nexample, the x86 instruction <code>JE<\/code> (jump if equal) can be inverted to\n<code>JNE<\/code> (jump if not equal). If we are allowed to edit branch conditions\nas well, then we can rewrite the above as:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    br_if_not r0, label_if_true<\/span><\/span>\n<span class=\"giallo-l\"><span>label_if_false:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>label_if_true:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span><\/code><\/pre>\n<p>This turns out to remove many additional branches in practice.<\/p>\n<h3 id=\"problem-2-empty-blocks\">Problem 2: Empty Blocks<\/h3>\n<p>It is sometimes the case that after optimizations, a basic block is\ncompletely <em>empty<\/em> aside from a final unconditional branch. This can\noccur when all of the code in an if- or else-block is optimized away\nor moved elsewhere in the function body. It can also occur when a\nblock was inserted to <em>split a critical edge<\/em> (see below).<\/p>\n<p>Thus, a common optimization is <em><a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Jump_threading\">jump\nthreading<\/a><\/em>: when one\nbranch points directly to another, we can just edit the first branch\nto point to the final target. Generalized, we can \"chase through\" any\nnumber of branches to eliminate intermediate steps. For example:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto L1<\/span><\/span>\n<span class=\"giallo-l\"><span>L1:<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto L2<\/span><\/span>\n<span class=\"giallo-l\"><span>L2:<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto L3<\/span><\/span>\n<span class=\"giallo-l\"><span>L3:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span><\/code><\/pre>\n<p>can become:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>    ...<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto L3   \/\/ &lt;--- edited branch<\/span><\/span>\n<span class=\"giallo-l\"><span>L1:<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto L2<\/span><\/span>\n<span class=\"giallo-l\"><span>L2:<\/span><\/span>\n<span class=\"giallo-l\"><span>    goto L3<\/span><\/span>\n<span class=\"giallo-l\"><span>L3:<\/span><\/span>\n<span class=\"giallo-l\"><span>    ...<\/span><\/span><\/code><\/pre>\n<p>note that the intermediate branches <em>were not removed<\/em>: they may still\nbe the targets of <em>other branches<\/em>. We skip over them when starting\nfrom the first branch. However, if we know some other way that these\nbranches are unused, we can then delete them, reducing code size.<\/p>\n<h3 id=\"problem-3-branch-relaxation\">Problem 3: Branch Relaxation<\/h3>\n<p>As we noted above, the \"branch relaxation\" problem is that we must\nchoose one of <em>multiple forms<\/em> for each branch instruction, each of\nwhich may have a different range (maximal distance from current\nprogram-counter location). This is complex because the needed range\ndepends on the final locations of the branch and its target, which in\nturn depends on the size of instructions in the machine code; but some\nof those instructions are themselves branches. We thus have a circular\ndependency.<\/p>\n<p>There will always be <em>some<\/em> way to branch to an arbitrary location in\nthe processor's address space, so there is always the trivial but\ninefficient solution of using worst-case branch forms. However, we can\nusually do much better, because the majority of branches will be to\nrelatively small offsets.<\/p>\n<p>The usual approach to solving this problem involves a \"fixpoint\ncomputation\": an iterative loop that continues to make improvements\nuntil none are left. This is where the \"relaxation\" of branch\nrelaxation comes from: we modify branch instructions to have more\noptimal forms as we discover that targets are within range; and as we\ndo this, we recompute code offsets and see if this enables any other\nrelaxations. As long as the relationship between branch range and\nbranch instruction size is monotonic (smaller required range allows\nfor shorter instruction), this will always converge to a unique\nfixpoint; but it is potentially expensive, and involves sticky\ndata-structure design questions if we want the code editing and\/or\noffset recomputation to be fast.<\/p>\n<h3 id=\"problem-4-critical-edges\">Problem 4: Critical Edges<\/h3>\n<p>For a number of reasons, we usually want to <em>split <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Control-flow_graph#Special_edges\">critical\nedges<\/a><\/em>\nin the control-flow graph. A critical edge is any control-flow\ntransfer edge that comes <em>from<\/em> a block with multiple out-edges, and\ngoes <em>to<\/em> a block with multiple in-edges. We sometimes need to insert\nsome code to run whenever the program follows a critical edge: e.g.,\nthe register allocator may need to \"fix up\" the machine state, moving\nvalues around in registers as expected by the target block. Consider\nwhere we might insert such code: we can't insert it prior to the jump,\nbecause this would execute no matter what out-edge is\ntaken. Similarly, we can't insert it at the target of the jump,\nbecause this would execute for any entry into the target block, not\njust transfers over the particular edge.<\/p>\n<p>The solution is to \"split\" the critical edge: that is, create a new\nbasic block, edit the branch to point to this block, and then create\nan unconditional branch in the block to the original target. This\nbasic block is a place where we can insert whatever fixup code we\nneed, and it will execute <em>only<\/em> when control flow transfers from the\none specific block to the other. A critical-edge split is illustrated\nin the following figure:<\/p>\n<p><img src=\"\/assets\/2020-12-19-critical-edges-web.svg\" alt=\"Figure: Splitting a critical edge\" \/><\/p>\n<p>There are multiple ways in which we could handle this problem: we\ncould preemptively split every critical edge; or we could split them\non demand, only when we need to insert code. The latter would require\nediting the CFG in place, and for various reasons, we would prefer to\navoid doing this: it invalidates many analysis results, and\ncomplicates data structures. It is also much simpler to reason about\nmany algorithms if we can assume that edges are already\nsplit. However, splitting every edge will leave many empty blocks,\nbecause we <em>usually<\/em> do not need to insert any fixup code on an edge.\nIn addition, splitting an edge raises the question of <em>where<\/em> to\ninsert the split-block. If we take the simplest approach and append it\nto the end of the function, we probably significantly reduce the\nnumber of branch-fallthrough simplifications we can make; a smarter\nheuristic that placed the block near its predecessor or successor\nwould be better.<\/p>\n<h2 id=\"traditional-approach-in-place-edits\">Traditional Approach: In-Place Edits<\/h2>\n<p>The traditional approach to all of these problems is to decompose the\ntask into a number of <em>passes<\/em> and perform <em>in-place edits<\/em> with those\npasses. For example, in LLVM, IR is lowered into a machine-specific\nform (<code>MachineFunction<\/code> of <code>MachineBasicBlock<\/code>s) with an explicit\nnotion of layout and with machine-level branch instructions; then\nedits are made, taking care to update branches when the layout\nchanges.<\/p>\n<p>For example, the following sequence of passes should handle most of\nthe above issues:<\/p>\n<ol>\n<li>\n<p>Split all critical edges, placing the split-block after the\npredecessor. (In LLVM, the\n<a rel=\"external\" href=\"https:\/\/github.com\/llvm\/llvm-project\/blob\/3fa2d37eb3f8acddcfde749ca822f2cc7d900cbb\/llvm\/lib\/Transforms\/Utils\/BreakCriticalEdges.cpp\"><code>SplitCriticalEdges<\/code><\/a>\npass.)<\/p>\n<\/li>\n<li>\n<p>Perform other optimizations, and register allocation; these may use\nthe split-blocks.<\/p>\n<\/li>\n<li>\n<p>Perform jump-threading transform; this will remove control-flow\ntransfers through empty blocks. (In LLVM, the\n<a rel=\"external\" href=\"https:\/\/github.com\/llvm\/llvm-project\/blob\/3fa2d37eb3f8acddcfde749ca822f2cc7d900cbb\/llvm\/lib\/CodeGen\/BranchFolding.cpp\"><code>BranchFolding<\/code><\/a>\npass.)<\/p>\n<\/li>\n<li>\n<p>Compute reachability, and delete \"dead blocks\" (blocks that are no\nlonger reachable). (Also done by <code>BranchFolding<\/code> in LLVM.)<\/p>\n<\/li>\n<li>\n<p>Compute a block order that tries to minimize jump distances and\nplaces at least one successor directly after every block when\npossible. (In LLVM, the\n<a rel=\"external\" href=\"https:\/\/github.com\/llvm\/llvm-project\/blob\/3fa2d37eb3f8acddcfde749ca822f2cc7d900cbb\/llvm\/lib\/CodeGen\/MachineBlockPlacement.cpp\"><code>MachineBlockPlacement<\/code><\/a>\npass.)<\/p>\n<\/li>\n<li>\n<p>Linearize the code from the CFG nodes into a single stream of\nmachine instructions using this block order. (In LLVM, blocks are\ninitially lowered into the <code>MachineFunction<\/code> and then reordered by\n<code>MachineBlockPlacement<\/code>.)<\/p>\n<\/li>\n<li>\n<p>Remove branches to fallthrough blocks, and invert conditionals that\ncreate additional fallthroughs.<\/p>\n<\/li>\n<li>\n<p>Compute block offsets based on machine-code size of current\ninstruction sequence, assuming worst-case size for every branch.<\/p>\n<\/li>\n<li>\n<p>Scan over branches, checking whether block locations allow for\nshorter forms due to nearer targets. Update branches and recompute\nblock offsets if so. Continue until fixpoint. (In LLVM, the\n<a rel=\"external\" href=\"https:\/\/github.com\/llvm\/llvm-project\/blob\/3fa2d37eb3f8acddcfde749ca822f2cc7d900cbb\/llvm\/lib\/CodeGen\/BranchRelaxation.cpp\"><code>BranchRelaxation<\/code><\/a>\ndoes this.)<\/p>\n<\/li>\n<li>\n<p>Fill in branch targets using final offsets. Branches are now in a\nform ready for machine-code emission.<\/p>\n<\/li>\n<\/ol>\n<p>Clearly this will work, and with some care (especially in the\nblock-placement heuristics), it will produce very good code. But the\nabove steps require <em>many<\/em> in-place edits. This is both slow (we are\nre-doing some work every time we edit the code) and forces us to use\ndata structures that allow for such edits (e.g., linked list), which\nimposes a tax on every other operation on the IR. Is there a better\nway?<\/p>\n<h2 id=\"cranelift-s-new-approach-streaming-edits\">Cranelift's New Approach: Streaming Edits<\/h2>\n<p>It would be ideal if we could avoid some of the code-transform passes\ndescribed above; can we? It turns out that one can actually do the\nfunctional equivalent of <em>all<\/em> of the above as part of other,\npre-existing work:<\/p>\n<ol>\n<li>\n<p>We can decide the final block order ahead of time, and do our\nCLIF-to-VCode lowering in this order, so VCode never needs to be\nreordered; it is already linearized. We can also insert\ncritical-edge splits as part of this lowering.<\/p>\n<\/li>\n<li>\n<p>We can do <em>all<\/em> of the other work -- inverting conditionals,\nthreading jumps, removing dead blocks, and handling various branch\nsizes -- in a streaming approach during machine-code emission! The\nkey insight is that we can do a sort of \"peephole optimization\": we\ncan immediately delete and re-emit branches at the <em>tail<\/em> of the\nemission buffer. By tracking some auxiliary state during the single\nemission scan, such as reachability, labels at current emission\npoint, a list of unresolved label-refs earlier in code, and a\n\"deadline\" for short-range branches, we can do everything we need\nto do without ever backing up more than a few contiguous branches\nat the end of the buffer.<\/p>\n<\/li>\n<\/ol>\n<p>Let's go into each of those in more detail!<\/p>\n<h3 id=\"step-1-decide-final-order-and-split-edges-while-lowering\">Step 1: Decide Final order and Split Edges while Lowering<\/h3>\n<p>As part of the instruction-selection pipeline described in the\n<a href=\"\/blog\/2020\/09\/18\/cranelift-isel-1\">previous post<\/a>), we need\nto iterate over the basic blocks in the CLIF and, for each block,\nlower its code to VCode instructions. We would like to do this\niteration in the same order as our final machine code layout so that\nwe don't need to reorder the VCode later.<\/p>\n<p>The only constraint that the lowering algorithm imposes is that we\nexamine <em>value uses<\/em> before <em>value defs<\/em>, which we can ensure by\nvisiting a block before any of its dominators. That leaves a lot of\nfreedom in how we do the lowering.<\/p>\n<p>If that were the whole problem, we could just do a\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Depth-first_search#Vertex_orderings\">postorder<\/a>\ntraversal and be done with it. In fact, the problem is complicated by\none other factor: critical-edge splitting!<\/p>\n<p>Recall that we described above that we must either preemptively split\nall critical edges or else find a way to edit-in-place later. To avoid\nthe complexities of edit-in-place, we choose to split them all. Note\nthat this is cheap as far as our CFG lowering is concerned, because\nour later branch optimizations will remove empty blocks almost for\nfree. (The register allocator's analyses may become more expensive\nwith a higher block count, but in practice we have not found this to\nbe much of a problem.)<\/p>\n<p>The challenge is in generating these blocks in the correct place <em>on\nthe fly<\/em>. To generate the lowering order, we define a <em>virtual graph<\/em>\nthat is never actually materialized, whose nodes are implied by the\nCLIF blocks and edges (every CLIF edge becomes a split-edge block) and\nwhose edges are defined only by a successor function. To generate the\nlowering order, we perform a <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Depth-first_search\">depth-first\nsearch<\/a> over the\nvirtual graph, recording the\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Depth-first_search#Vertex_orderings\">postorder<\/a>. This\npostorder is guaranteed to see uses before defs, as required. The DFS\nitself is a pretty good heuristic for block placement: it will tend to\ngroup structured-control-flow code together into its hierarchical\nunits.<\/p>\n<p>There are additional details in <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/machinst\/blockorder.rs\">the\nimplementation<\/a>\nthat ensure we split only critical edges rather than all edges, that\nrecord block-successor information directly as we produce lowered\nblocks so that the subsequent backend stages do not need to recompute\nit, and some other small optimizations.<\/p>\n<p>This is illustrated in the following figure, showing a CLIF-level CFG\ntransformed with split edges and merged edge-blocks then linearized at\na conceptual level; and the successor function actually defined to\ndrive the DFS. Note that the na\u00efve lowering of the split-edge CFG\nwould result in 14 branches (due to 14 CFG edges); the final lowered\nmachine code contains only 4 branches, while providing a slot for any\nneeded fixup instructions on any CFG edge.<\/p>\n<p><img src=\"\/assets\/2020-12-19-cfg-lowering-web.svg\" alt=\"Figure: CFG lowering with edge-splitting and merging using implicit DFS\" \/><\/p>\n<h3 id=\"step-2-edit-branches-while-emitting\">Step 2: Edit Branches while Emitting<\/h3>\n<p>Once we have lowered <code>VCode<\/code>, we need to emit machine code! In a\nconventional design, this would require linearization, a bunch of\nbranch optimizations, and branch-target resolution before we ever\nproduced a byte of machine-code. But we can do much better.<\/p>\n<p>In Cranelift's design, a machine backend just <em>emits every conditional\nbranch na\u00efvely as a two-way combination<\/em> into a machine-code buffer we\ncall the <code>MachBuffer<\/code>. Critically, however, this <code>MachBuffer<\/code> is not\nmerely a <code>Vec&lt;u8&gt;<\/code>: it knows (many) things about its content,\nincluding where its branches are, what the branches' targets are, and\nhow to invert the branches if necessary.<\/p>\n<p>The <code>MachBuffer<\/code> will perform <em>streaming edits<\/em> on the code as it is\nemitted, editing only a <em>suffix<\/em> of the buffer (contiguous bytes up to\nthe end, or current emission point), in order to convert two-way\nbranch combos when possible into simpler forms.<\/p>\n<p>The abstraction that the machine backend sees is:<\/p>\n<ul>\n<li>\n<p>The <code>MachBuffer<\/code> allows us to emit machine-code bytes.<\/p>\n<\/li>\n<li>\n<p>We can tell the <code>MachBuffer<\/code> that a certain range of machine-code\nbytes that we just emitted are a <em>branch<\/em>, either conditional or\nunconditional, how to invert it if conditional, and a <em>label<\/em> as the\nbranch target.<\/p>\n<\/li>\n<li>\n<p>We can <em>bind<\/em> a label to the current emission point.<\/p>\n<\/li>\n<li>\n<p>We parameterize the <code>MachBuffer<\/code> on a <code>LabelUse<\/code> trait\nimplementation which defines all the different kinds of\nbranch-target references, how to patch the machine code with a\nresolved offset, and how to emit a <em>veneer<\/em>, i.e., a longer-form\nbranch that the original branch can indirect through in order to\nreach further.<\/p>\n<\/li>\n<\/ul>\n<p>And that's it! The <code>MachBuffer<\/code> does all of the work behind the\nscenes: when we emit a branch, it sometimes chomps some bytes; and\nwhen we define a label, it sometimes scans through a list of deferred\nfixups to patch earlier machine code.<\/p>\n<p>A (simplified) illustrated example is shown below. The machine backend\nemits two-way branches na\u00efvely by always emitting a conditional and\nunconditional branch (e.g. at the end of basic block <code>L0<\/code>). It also\nprovides metadata to the <code>MachBuffer<\/code> that describes where the labels\nare, where the branches are, where the branches are targetted (as\nlabels), and how to invert conditional branches. The <code>MachBuffer<\/code> is\nable to perform the listed streaming edits <em>as code is emitted<\/em>,\nproducing the final machine code at the right with no intermediate\nbuffering or additional passes. We'll describe how this editing occurs\nin more detail below.<\/p>\n<p><img src=\"\/assets\/2020-12-19-machbuffer-web.svg\" alt=\"Figure: MachBuffer emission and edits\" \/><\/p>\n<h4 id=\"branch-peephole-optimizations\">Branch Peephole Optimizations<\/h4>\n<p>The key insight of the <code>MachBuffer<\/code> design is that we can edit\nbranches <em>at the tail of the buffer<\/em> as code is emitted by tracking\nthe \"most recent\" branches: specifically, the branches that are\n<em>contiguous to the tail of the buffer<\/em>.<\/p>\n<p>The first optimization that we do is <em>branch inversion<\/em>, which can\nsometimes eliminate unconditional branches. In the example above, when\nthe backend has emitted the machine-code bytes for all of the <code>L0<\/code>\nbasic block, the <code>MachBuffer<\/code> will know that the last two branches,\ncontiguous to the tail, are a conditional branch to <code>L1<\/code> and an\nunconditional branch to <code>L2<\/code>. When we then see the label <code>L1<\/code> (which\nis the first branch's target) bound to this offset, we can apply a\nsimple rule: a conditional that jumps over an immediately following\nunconditional can be inverted, and the unconditional branch\nremoved. Note that, critically, because these branches are <em>contiguous\nto the tail<\/em>, the edit will not affect any offsets that have already\nbeen resolved; we are free to contract the code-size here, and offsets\nof subsequently-emitted code will be correct without further fixups.<\/p>\n<p>Said another way, our approach <em>never moves code<\/em>. Rather, it only\nsometimes <em>chomps or adjusts a just-emitted branch<\/em>, right away,\nbefore code-emission carries on past the branch.<\/p>\n<p>The next optimizations we do are <em>jump threading<\/em> and <em>dead-block\nremoval<\/em>. Recall from above that jump threading means that\nintermediate steps in a chain of jumps can be removed: a jump to a\njump to X becomes just a jump to X. We resolve this by keeping an\n<em>up-to-date alias table<\/em> that tracks label-to-label aliases. The table\nis updated whenever the <code>MachBuffer<\/code> is informed that an unconditional\njump was emitted and a label was bound to its address. We then\nindirect through the alias table when resolving labels to final\noffsets. The second task, dead-block removal, occurs as a side-effect\nof tracking <em>reachability<\/em> of the current buffer tail. Any offset that\n(i) immediately follows an unconditional jump, <em>and<\/em> (ii) has no\nlabels bound to it, is unreachable; an unconditional jump at an\nunreachable offset can be elided. (Actually, any code at an\nunreachable offset can be removed, but for simplicity and to make it\neasier to reason about correctness, we restrict the <code>MachBuffer<\/code>'s\nedits to code explicitly marked as branch instructions only.)<\/p>\n<p>In order for this to work correctly, we need to track all labels that\nhave been bound to the current buffer tail and adjust them if we chomp\n(truncate) the buffer or redirect a label. For this reason, the\nlabel-binding, label-use resolution, and branch-chomping are all\ntightly integrated into a set of interacting data structures:<\/p>\n<p><img src=\"\/assets\/2020-12-19-machbuffer-structures-rewrites-web.svg\" alt=\"Figure: MachBuffer data structures and rewrites\" \/><\/p>\n<p>To summarize, we track:<\/p>\n<ul>\n<li>Emitted bytes;<\/li>\n<li>All labels bound to the current offset;<\/li>\n<li>A table of all labels to a bound offset or \"unbound\";<\/li>\n<li>A table of all labels to another label as an alias or \"unaliased\";<\/li>\n<li>A list of the <em>last contiguous branches<\/em>, each of which is\nconditional or not, with inverted form if so, and label-use, and\nlabels that are bound <em>to<\/em> this branch instruction;<\/li>\n<li>A list of other label-uses for fixup.<\/li>\n<\/ul>\n<p>As we emit code, we append to the emitted-bytes buffer. We lazily\ninvalidate the \"labels at current offset\" set by tracking the offset\nfor which that set is valid; appending new code implicitly clears it.<\/p>\n<p>As the machine backend tells the <code>MachBuffer<\/code> about branches, we\nappend to the list of the last contiguous branches. This, too, is\ninvalidated when code is emitted that is not a branch.<\/p>\n<p>When a label-use is noted and the label is already resolved, we fix up\nthe buffer right away. Note that once a label resolves to an offset,\nthat offset cannot change; so this fixup can be done once and the\nmetadata discarded.<\/p>\n<p>All branch simplification happens when a label is <em>bound<\/em>: this is\nwhen new actions become possible. We perform the following algorithm\n(see\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/ce6e967eebc7e293950394ba212689190e2cf0ed\/cranelift\/codegen\/src\/machinst\/buffer.rs#L753\"><code>MachBuffer::optimize_branches()<\/code><\/a>\nfor details):<\/p>\n<ul>\n<li>Loop as long as there are some branches in the latest-branches list:\n<ul>\n<li>If the current buffer tail is beyond the end of the latest branch,\ndone (and clear list).<\/li>\n<li>If the latest branch (which ends at current tail) has a target\nthat resolves to current tail, chomp it and restart loop.<\/li>\n<li>If the latest branch is unconditional <em>and does not branch to\nitself<\/em>:\n<ul>\n<li>Update any labels pointing <em>at<\/em> this branch to point <em>at its\ntarget<\/em> instead.<\/li>\n<li>Restart loop if any labels were moved.<\/li>\n<\/ul>\n<\/li>\n<li>If latest branch is unconditional, follows another unconditional\nbranch, and no labels are bound at this branch, then chomp it\n(unreachable) and restart loop.<\/li>\n<li>If latest branch is unconditional, follows a conditional branch,\nand conditional branch target is current tail, then invert\nconditional and chomp the unconditional, and restart loop.<\/li>\n<\/ul>\n<\/li>\n<li>When loop is done, clear latest-branches list; no more can be\nsimplified.<\/li>\n<\/ul>\n<p>This may look to have undesirable algorithmic complexity, but in fact\nit is tightly bounded: we make a forward-progress argument as labels\nonly move down alias chains and fixed work is done per branch (each is\nonly examined once and acted upon or purged). Overall, the algorithm\nruns in linear time.<\/p>\n<p>This linear-time algorithm that edits locally, avoids any\ncode-movement, and streams into a buffer in final form, is far better\nthan the multi-pass edit-in-place design of a traditional backend,\nboth asymptotically and in practice (CPUs love streaming algorithms\nand minimized data movement). It seems to produce code nearly as good\nas a much more complex branch simplifier at a much lower cost.<\/p>\n<h3 id=\"correctness\">Correctness<\/h3>\n<p>The algorithm for simplifying branches is one of the most critical to\ncorrectness in the (post-optimizer) compiler backend; probably only\nsecond to the register allocator. It is very subtle, and bugs can be\ndisastrous: incorrect control flow could cause <em>anything<\/em> to happen,\nfrom impossible-to-debug incorrect program results (ask me <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/1729\">how I\nknow<\/a>! And\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/2083\">here too<\/a>!)\nto serious security vulnerabilities.<\/p>\n<p>Because of this, we have taken <em>extensive<\/em> care to ensure\ncorrectness. In fact, more than a third of the lines in the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/ce6e967eebc7e293950394ba212689190e2cf0ed\/cranelift\/codegen\/src\/machinst\/buffer.rs\"><code>MachBuffer<\/code>\nimplementation<\/a>\nare a proof of correctness, based on several core invariants\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/ce6e967eebc7e293950394ba212689190e2cf0ed\/cranelift\/codegen\/src\/machinst\/buffer.rs#L109-L141\">described\nhere<\/a>. At\neach data-structure mutation, we show that (i) the invariants still\nhold, and (ii) the code mutation did not alter execution semantics.<\/p>\n<p>Because there is significant wisdom in the Knuth quote \"I have only\nproved it correct, not tried it\" (there are always gaps between\nspecification and reality, and unless one generates an implementation\nfrom a machine-checked proof, then the English prose or its\ntranslation to code may have bugs too<sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup>), all invariants are also\nfully checked on each label-bind event in debug builds of\nCranelift. The various fuzzing harnesses that hammer on the new\nbackend will thus be driving these checks continuously.<\/p>\n<h3 id=\"other-concerns\">Other Concerns<\/h3>\n<p>There are many subtleties to the branch-simplification and code layout\nproblems that were not discussed here! Most prominently, we have not\ncovered <em>branch veneers<\/em> or the topic of branch ranges at all, though\nwe saw the <em>problem<\/em> of \"branch relaxation\" above. The <code>MachBuffer<\/code>\nhandles out-of-range branches by tracking a \"deadline\" (the last point\nat which any currently outstanding label may be bound without causing\na branch to go out of range); if we hit the deadline, we emit an\n<em>island<\/em> of <em>branch veneers<\/em>, which are commonly just long-range\nunconditional branches, for each unresolved label and resolve the\nlabels to those branches. This extends the deadlines. In practice\nisland emission will almost never occur, so it is acceptable to\npessimize this case (add an extra indirection) to avoid the need to go\nback and edit the original branch into a longer-range form.<\/p>\n<p>We also haven't covered constant pools; these are handled with the\nsame \"island\" mechanism, allowing emitted machine code to refer to\nnearby constant data.<\/p>\n<h2 id=\"conclusion-and-next-time\">Conclusion, and Next Time<\/h2>\n<p>This has been a deep dive into the world of branch simplification,\nwith an emphasis on how we engineered Cranelift's new backend to\nprovide very good compilation speed taking control-flow handling and\nbranch lowering\/simplification as an example. We believe that there\nmay be other significant opportunities to rethink, and carefully\nengineer, core algorithms in the compiler backend with specific\nattention to <em>maximizing streaming behavior<\/em>, <em>minimizing\nindirection<\/em>, and <em>minimizing passes over data<\/em>. This is an\ninteresting and exciting engineering pursuit largely because it goes\nbeyond the world of \"theoretical standard compiler-book algorithms\"\nand calls on problem solving to find clever new design tricks.<\/p>\n<p>As we described near the end of this post, <em>correctness<\/em> is also an\nimportant focus -- perhaps <em>the<\/em> most important focus -- of any\ncompiler engineering effort. Given that, I plan to write the next (and\nfinal) post in this series about how we engineered for correctness by\ntaking a deep-dive into the <em>register allocator checker<\/em>, which is a\nnovel symbolic checker (which can be seen as an application of\nabstract interpretation) that allows us to <em>prove<\/em> that any particular\nregister-allocator execution gave a correct allocation result.  I'll\ntalk about how this checker, driven by a fuzzing frontend, found some\n<em>really subtle and interesting bugs<\/em> that we likely never would have\nfound in production otherwise. With that, until next time, happy\ncompiling!<\/p>\n<p><em>For discussions about this post, please feel free to join us on our Zulip\ninstance in <a rel=\"external\" href=\"https:\/\/bytecodealliance.zulipchat.com\/#narrow\/stream\/217117-cranelift\/topic\/blog.20post.20on.20branch.20optimizations.20in.20new.20backend\">this\nthread<\/a>.<\/em><\/p>\n<hr \/>\n<p><em>Thanks to Benjamin Bouvier for reviewing this post and providing very helpful\nfeedback! Thanks also to bjorn3 for correcting a typo in a figure.<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>Frances E. Allen and John Cocke. <em>A catalogue of optimizing\ntransformations.<\/em> In <em>Design and Optimization of Compilers<\/em>\n(Prentice-Hall, 1972), pp. 1--30.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>This is a bit of a simplification of branches in the IR: in CLIF\n(and in most other CFG-based compiler IRs), there are several\nbranch types. Another is the \"switch\" or \"branch table\" branch\nthat chooses between N possible targets with an integer\nindex. There are also simple single-target unconditional\nbranches; and a return instruction is also a \"branch\" of sorts\nin that it ends a basic block, though it has no successors. The\nimportant takeaway is that IR-level branches are an abstraction\nlevel above machine-code control flow, allowing for a direct\nchoice between several or many targets as one operation.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>See <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/pull\/2083\">PR\n#2083<\/a>\nabove, which is a bug that arose <em>after<\/em> I wrote the correctness\nproof, because the proof assumed target-aliasing supported\narbitrarily-long branch chains but it actually followed only one\nlevel. This was a deliberate earlier implementation choice to\navoid infinite loops on branch cycles. It turns out that it's\npossible to just avoid cycles in the alias table by\nconstruction; we carefully prove that this is so and then allow\nredirect-chasing through chains of branches. For extra paranoia,\nbecause a non-terminating compiler is bad and we are merely\nhuman, we <em>still<\/em> limit redirect-chasing to 1 million branches\nand panic beyond that (because one can never be too careful);\nthis is a limit that will never be hit when using the Wasm\nfrontend (due to limits on function size) and is extremely\nunlikely to be hit elsewhere.<\/p>\n<\/div>\n"},{"title":"A New Backend for Cranelift, Part 1: Instruction Selection","published":"2020-09-18T00:00:00+00:00","updated":"2020-09-18T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/2020\/09\/18\/cranelift-isel-1\/"}},"id":"https:\/\/cfallin.org\/blog\/2020\/09\/18\/cranelift-isel-1\/","content":"<p>This post is the first in a three-part series about my recent work on\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\">Cranelift<\/a>\nas part of my day job at Mozilla. In this first post, I will set some context\nand describe the instruction selection problem. In particular, I'll talk about\na revamp to the instruction selector and backend framework in general that\nwe've been working on for the last nine months or so. This work has been\nco-developed with my brilliant colleagues Julian Seward and <a rel=\"external\" href=\"https:\/\/benj.me\">Benjamin\nBouvier<\/a>, with significant early input from <a rel=\"external\" href=\"https:\/\/github.com\/sunfishcode\">Dan\nGohman<\/a> as well, and help from all of the\nwonderful Cranelift hackers.<\/p>\n<h2 id=\"background-cranelift\">Background: Cranelift<\/h2>\n<p>So what is Cranelift? The project is a compiler framework written in\n<a rel=\"external\" href=\"https:\/\/www.rust-lang.org\/\">Rust<\/a> that is designed especially (but not\nexclusively) for <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Just-in-time_compilation\">just-in-time\ncompilation<\/a>. It's a\ngeneral-purpose compiler: its most popular use-case is to compile\n<a rel=\"external\" href=\"https:\/\/www.webassembly.org\/\">WebAssembly<\/a>, though several other frontends\nexist, for example,\n<a rel=\"external\" href=\"https:\/\/github.com\/bjorn3\/rustc_codegen_cranelift\">cg_clif<\/a>, which adapts the\nRust compiler itself to use Cranelift. Folks at Mozilla and several other\nplaces have been developing the compiler for a few years now.  It is the\ndefault compiler backend for\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\">wasmtime<\/a>, a runtime for\nWebAssembly outside the browser, and is used in production in several other\nplaces as well. We recently flipped the switch to turn on Cranelift-based\nWebAssembly support in nightly Firefox on <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/AArch64\">ARM64\n(AArch64)<\/a> machines, including most\nsmartphones, and if all goes well, it will eventually go out in a stable\nFirefox release. Cranelift is developed under the umbrella of the <a rel=\"external\" href=\"https:\/\/bytecodealliance.org\/\">Bytecode\nAlliance<\/a>.<\/p>\n<p>In the past nine months, we have built a new framework in Cranelift for the\n\"machine backends\", or the parts of the compiler that support particular CPU\ninstruction sets. We also added a new backend for AArch64, mentioned above, and\nfilled out features as needed until Cranelift was ready for production use in\nFirefox. This blog post sets some context and describes the design process that\nwent into the backend-framework revamp.<\/p>\n<p>It can be a bit confusing to keep all of the moving parts straight. Here's a\nvisual overview of Cranelift's place among various other components, focusing\non two of the major Rust crates (the Wasm frontend and the codegen backend) and\nseveral of the other programs that make use of Cranelift:<\/p>\n<p><img src=\"\/assets\/2020-09-10-cranelift-components.svg\" alt=\"Figure: Cranelift and other components\" \/><\/p>\n<h2 id=\"old-backend-design-instruction-legalizations\">Old Backend Design: Instruction Legalizations<\/h2>\n<p>To understand the work that we've done recently on Cranelift, we'll need to\nzoom into the <code>cranelift_codegen<\/code> crate above and talk about how it <em>used to<\/em>\nwork. What is this \"CLIF\" input, and how does the compiler translate it to\nmachine code that the CPU can execute?<\/p>\n<p>Cranelift makes use of\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/docs\/ir.md\">CLIF<\/a>,\nor the Cranelift IR (Intermediate Representation) Format, to represent the code\nthat it is compiling. Every compiler that performs program optimizations uses\nsome form of an <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Intermediate_representation\">Intermediate Representation\n(IR)<\/a>: you can think\nof this like a virtual instruction set that can represent all the operations a\nprogram is allowed to do. The IR is typically simpler than real instruction\nsets, designed to use a small set of well-defined instructions so that the\ncompiler can easily reason about what a program means. The IR is also\nindependent of the CPU architecture that the compiler eventually targets; this\nlets much of the compiler (such as the part that generates IR from the input\nprogramming language, and the parts that optimize the IR) be reused whenever\nthe compiler is adapted to target a new CPU architecture.  CLIF is in <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Static_single_assignment_form\">Static\nSingle Assignment\n(SSA)<\/a> form, and\nuses a conventional <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Control-flow_graph\">control-flow\ngraph<\/a> with basic blocks\n(though it previously allowed extended basic blocks, these have been phased\nout). Unlike many SSA IRs, it represents \u03c6-nodes with block parameters\nrather than explicit \u03c6-instructions.<\/p>\n<p>Within <code>cranelift_codegen<\/code>, before we revamped the backend design, the program\nremained in CLIF throughout compilation and up until the compiler emitted the\nfinal machine code. This might seem to contradict what we just said: how can\nthe IR be machine-independent, but also be the final form from which we emit\nmachine code?<\/p>\n<p>The answer is that the old backends were built around the concept of\n\"legalization\" and \"encodings\". At a high level, the idea is that every\n<em>Cranelift<\/em> instruction either corresponds to one <em>machine<\/em> instruction, or can\nbe replaced by a sequence of other <em>Cranelift<\/em> instructions. Given such a\nmapping, we can refine the CLIF in steps, starting from arbitrary\nmachine-independent instructions from earlier compiler stages, performing edits\nuntil the CLIF corresponds 1-to-1 with machine code. Let's visualize this\nprocess:<\/p>\n<p><img src=\"\/assets\/2020-09-10-cranelift-legalization.svg\" alt=\"Figure: legalization by repeated instruction expansion\" \/><\/p>\n<p>A very simple example of a CLIF instruction that has a direct \"encoding\" to a\nmachine instruction is <code>iadd<\/code>, which just adds two integers. On essentially any\nmodern architecture, this should map to a simple ALU instruction that adds two\nregisters.<\/p>\n<p>On the other hand, many CLIF instructions do not map cleanly. Some arithmetic\ninstructions fall into this category: for example, there is a CLIF instruction\nto count the number of set bits in an integer's binary representation\n(<code>popcount<\/code>); not every CPU has a single instruction for this, so it might be\nexpanded into a longer series of bit manipulations. There are operations that\nare defined at a higher semantic level, as well, that will necessarily be\nlowered with expansions: for example, accesses to Wasm memories are lowered\ninto operations that fetch the linear memory base and its size, bounds-check\nthe Wasm address against the limit, compute the real address for the Wasm\naddress, and perform the access.<\/p>\n<p>To compile a function, then, we iterate over the CLIF and find instructions\nwith no direct machine encodings; for each, we simply expand into the legalized\nsequence, and then recursively consider the instructions in that sequence. We\nloop until all instructions have machine encodings. At that point, we can emit\nthe bytes corresponding to each instruction's encoding<sup class=\"footnote-reference\"><a href=\"#1\">1<\/a><\/sup>.<\/p>\n<h2 id=\"growing-pains-and-a-new-backend-framework\">Growing Pains, and a New Backend Framework?<\/h2>\n<p>There are a number of advantages to the legacy Cranelift backend design, which\nperforms expansion-based legalization with a single IR throughout. As one might\nexpect, though, there are also a number of drawbacks. Let's discuss a few of\neach.<\/p>\n<h3 id=\"single-ir-and-legalization-pros\">Single IR and Legalization: Pros<\/h3>\n<ol>\n<li>\n<p>By operating on a single IR all the way to machine-code emission, the same\noptimizations can be applied at multiple stages. For example, consider a\nlegalization expansion that turns a high-level \"access Wasm memory\"\ninstruction into a sequence of loads, adds and bounds-checks. If many such\nsequences occur in one function, we might be able to factor out common\nportions (e.g.: computing the base of the Wasm memory).  Thus the\nlegalization scheme exposes as much code as possible, at as many stages as\npossible, to opportunities for optimization. The legacy Cranelift pipeline\nin fact works in this way: it runs \"pre-opt\" and \"post-opt\" optimization\npasses, before and after legalization respectively.<\/p>\n<\/li>\n<li>\n<p>If <em>most<\/em> of the Cranelift instructions become one machine instruction, and\nfew legalizations are necessary, then this scheme can be very fast: it\nbecomes simply a single traversal to fill in \"encodings\", which were\nrepresented by small indices into a table.<\/p>\n<\/li>\n<\/ol>\n<h3 id=\"single-ir-and-legalization-cons\">Single IR and Legalization: Cons<\/h3>\n<ol>\n<li>\n<p>Expansion-based legalization may not always result in\noptimal code. So far we've seen that legalization can convert from CLIF to\nmachine instructions with one-to-one or one-to-many mappings. However, there\nare sometimes also <em>single<\/em> machine instructions that implement the behavior of\n<em>multiple<\/em> CLIF instructions, i.e. a many-to-one mapping. In order to generate\nefficient code, we want to be able to make use of these instructions.<\/p>\n<p>For example, on x86, an instruction that references memory can compute an\naddress like <code>base + scale * index<\/code>, where <code>base<\/code> and <code>index<\/code> are registers\nand <code>scale<\/code> is 1, 2, 4, or 8. There is no notion of such an address mode in\nCLIF, so we would want to pattern-match the raw <code>iadd<\/code> (add) and <code>ishl<\/code>\n(shift) or <code>imul<\/code> (multiply) operations when they occur in the address\ncomputation. Then, we would want to somehow select the encoding on the\n<code>load<\/code> instruction based on the fact that its input is some specific\ncombination of adds and shifts\/multiplies.  This seems to break the\nabstraction that the encoding represents only that instruction's operation.<\/p>\n<p>In principle, we could implement more general pattern matching for legalization\nrules to allow many-to-one mappings. However, this would be a significant\nrefactor; and as long as we were reconsidering the design in whole, there were\nother reasons to avoid patching the problem in this way.<\/p>\n<\/li>\n<li>\n<p>There is a conceptual difficulty with the single-IR approach: there is\nno static representation of which instructions are expanded into which others\nand it is difficult to reason about the correctness and termination properties\nof legalization as a whole.<\/p>\n<p>Specifically, the expansion-based legalization rules must obey a partial\norder among instructions: if A expands into a sequence including B, then B\ncannot later expand into A. In practice, mappings were mostly one-to-one,\nand for those that weren't, there was a clear domain separation between the\n\"input\" high-level instructions and the \"machine-level\" instructions.\nHowever, for more complex machines, or more complex matching schemes that\nattempt to make better use of the target instruction set, this could become\na real difficulty for the machine-backend author to keep straight.<\/p>\n<\/li>\n<li>\n<p>There are efficiency concerns with expansion-based legalization. At\nan algorithmic level, we prefer to avoid fixpoint loops (in this case,\n\"continue expanding until no more expansions exist\") whenever possible. The\nruntime is bounded, but the bound is somewhat difficult to reason about,\nbecause it depends on the maximum depth of chained expansions.<\/p>\n<p>The data structures that enable in-place editing are also much slower than\nwe would like. Typically, compilers store IR instructions in linked lists to\nallow for in-place editing. While this is asymptotically as fast as an\narray-based solution (we never need to perform random access), it is much\nless cache-friendly or\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Instruction-level_parallelism\">ILP<\/a>-friendly\non modern CPUs. We'd prefer instead to store arrays of instructions and\nperform single passes over them whenever possible.<\/p>\n<\/li>\n<li>\n<p>Our particular implementation of the legalization scheme grew to be\nsomewhat unwieldy over time. Witness this GitHub issue, in which my eloquent\ncolleague <a rel=\"external\" href=\"https:\/\/benj.me\/\">Benjamin Bouvier<\/a> describes all the reasons\nwe'd like to fix the design: <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/issues\/1141\">#1141: Kill Recipes With\nFire<\/a>. This is no\nslight to the engineers who built it; the complexity was managed as well as\ncould be, with a very nice DSL-based code generation step to produce the\nlegalizer from high-level rule specifications.  However, reasoning through\nlegalizations and encodings become more cumbersome than we would prefer, and\nthe compiler backends were not very accessible to contributors. Adding a new\ninstruction required learning about \"recipes\", \"encodings\", and\n\"legalizations\" as well as mere instructions and opcodes, and finding one's\nway through the DSL to put the pieces together properly. A more conventional\ncode-lowering approach would avoid much of this complexity.<\/p>\n<\/li>\n<li>\n<p>A single-level IR has a fundamental tension: for analyses and optimizations\nto work well, an IR should have only one way to represent any particular\noperation, i.e. should consist of a small set of canonical instructions. On\nthe other hand, a machine-level representation should represent all of the\nrelevant details of the target ISA. For example, an address computation\nmight occur in many different ways (with different addressing modes) on the\nmachine, but we would prefer not to have to analyze a special\naddress-computation opcode in all of our analyses. An implicit rule at\nemission time (\"a load with an add instruction as input always becomes this\naddressing mode\") is not ideal, either.<\/p>\n<p>A single IR simply cannot serve both ends of this spectrum properly, and\ndifficulties arose as CLIF strayed from either end. To resolve this\nconflict, it is best to have a two-level representation, connected by an\nexplicit instruction selector. It allows CLIF itself to be as simple and as\nnormalized as possible, while allowing all the details we need in\nmachine-specific instructions.<\/p>\n<\/li>\n<\/ol>\n<p>For all of these reasons, as part of our revamp of Cranelift and a prerequisite\nto our new AArch64 backend, we built a new framework for machine backends and\ninstruction selection. The framework allows machine backends to define their\nown instructions, separately from CLIF; rather than legalizing with expansions\nand running until a fixpoint, we define a single lowering pass; and everything\nis built around more efficient data-structures, carefully optimizing passes\nover data and avoiding linked lists entirely. We now describe this new design!<\/p>\n<h2 id=\"a-new-ir-vcode\">A New IR: VCode<\/h2>\n<p>The main idea of the new Cranelift backend is to <em>add a machine-specific IR<\/em>,\nwith several properties that are chosen specifically to represent machine-code\nwell (i.e., the IR is very close to machine code). We call this <code>VCode<\/code>, which\ncomes from \"virtual-register code\", and the VCode contains <code>MachInst<\/code>s, or\nmachine instructions. The key design choices we made are:<\/p>\n<ul>\n<li>\n<p>VCode is a linear sequence of instructions. There is control-flow\ninformation that allows traversal over basic blocks, but the data structures\nare not designed to easily allow inserting or removing instructions or\nreordering code. Instead, we lower into VCode with a single pass,\ngenerating instructions in their final (or near-final) order. I'll write more\nabout how we make this efficient in a follow-up post.<\/p>\n<p>This design aspect avoids the inefficiencies of linked-list data structures,\nallowing fast passes over arrays of instructions instead. We've kept the\n<code>MachInst<\/code> size relatively small (16 bytes per instruction for AArch64)\nwhich aids code generation and iteration speed as well.<\/p>\n<\/li>\n<li>\n<p>VCode is <em>not<\/em> SSA-based; instead, its instructions operate on registers.\nWhile lowering, we allocate virtual registers. After the VCode is generated,\nthe register allocator computes appropriate register assignments and edits\nthe instructions in-place, replacing virtual registers with real registers.\n(Both are packed into a 32-bit representation space, using the high bit to\ndistinguish virtual from real.)<\/p>\n<p>Eschewing SSA at this level allows us to avoid the overhead of maintaining\nits invariants, and maps more closely to the real machine. Lowerings for\ninstructions are allowed to, e.g., use a destination register as a temporary\nbefore performing a final write into it. If we required SSA form, we would\nhave to allocate a temporary in this case and rely on the register allocator\nto coalesce it back to the same register, which adds compile-time overhead.<\/p>\n<\/li>\n<li>\n<p>VCode is a container for <code>MachInst<\/code>s, but there is a separate <code>MachInst<\/code>\ntype for each machine backend. The machine-independent part is parameterized\non <code>MachInst<\/code> (which is a trait in Rust) and is statically monomorphized to\nthe particular target for which the compiler is built.<\/p>\n<p>Modeling a machine instruction with Rust's excellent facilities for\nstrongly-typed data structures, such as <code>enum<\/code>s, avoids the issue of muddled\ninstruction domain (is a CLIF instruction machine-independent,\nmachine-dependent, or both?) and allows each backend to store the appropriate\ninformation for its encoding.<\/p>\n<\/li>\n<\/ul>\n<p>One can visualize a VCode function body as consisting of the following\ninformation (simplified; a real example is further below):<\/p>\n<p><img src=\"\/assets\/2020-09-10-vcode.svg\" alt=\"Figure: VCode is an array of instructions with block information\" \/><\/p>\n<p>Note that the instructions are simply stored in an array, and the basic blocks\nare recorded separately as ranges of array (instruction) indices. As we\ndescribed above, we designed this data structure for fast iteration, but not\nfor editing. We always ensure that the first block (<code>b0<\/code>) is the entry block,\nand that consecutive block indices have contiguous instruction-index ranges\n(i.e., are placed next to each other).<\/p>\n<p>Each instruction is mostly opaque from the point of view of the VCode\ncontainer, with a few exceptions: every instruction exposes its (i) register\nreferences, and (ii) basic-block targets, if a branch. Register references are\ncategorized into the usual \"uses\" and \"defs\" (reads and writes).<sup class=\"footnote-reference\"><a href=\"#2\">2<\/a><\/sup><\/p>\n<p>Note as well that the instructions can refer to <em>either<\/em> virtual registers\n(here denoted <code>v0<\/code>..<code>vN<\/code>) <em>or<\/em> real machine registers (here denoted\n<code>r0<\/code>..<code>rN<\/code>). This design choice allows the machine backend to make use of\nspecific registers where required by particular instructions, or by the ABI\n(parameter-passing conventions). The semantics of VCode are such that the\nregister allocator recognizes <em>live ranges<\/em> of the real registers, from defs to\nuses, and avoids allocating virtual registers to those particular real\nregisters for their live ranges. After allocation, all machine instructions are\nedited in place to refer only to real registers.<\/p>\n<p>Aside from registers and branch targets, an instruction contained in the VCode\nmay contain whatever other information is necessary to emit machine code. Each\nmachine backend defines its own type to store this information. For example, on\nAArch64, here are several of the instruction formats, simplified:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword z-storage\">pub enum<\/span><span class=\"z-entity z-name z-type\"> Inst<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ An ALU operation with two register sources and a register destination.<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-type\">    AluRRR<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        alu_op<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> ALUOp<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        rd<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Writable<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">Reg<\/span><span class=\"z-punctuation\">&gt;,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        rn<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Reg<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        rm<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Reg<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">    },<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ An ALU operation with a register source and an immediate-12 source, and a register<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ destination.<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-type\">    AluRRImm12<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        alu_op<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> ALUOp<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        rd<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Writable<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">Reg<\/span><span class=\"z-punctuation\">&gt;,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        rn<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Reg<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        imm12<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Imm12<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">    },<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ A MOVZ with a 16-bit immediate.<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-type\">    MovZ<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        rd<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> Writable<\/span><span class=\"z-punctuation\">&lt;<\/span><span class=\"z-entity z-name z-type\">Reg<\/span><span class=\"z-punctuation\">&gt;,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        imm<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> MoveWideConst<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        size<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> OperandSize<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">    },<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ A two-way conditional branch. Contains two targets; at emission time, a conditional<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ branch instruction followed by an unconditional branch instruction is emitted, but<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ the emission buffer will usually collapse this to just one branch. See a follow-up<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/\/ blog post for more!<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-type\">    CondBr<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        taken<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> BranchTarget<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        not_taken<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> BranchTarget<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        kind<\/span><span class=\"z-keyword z-operator\">:<\/span><span class=\"z-entity z-name z-type\"> CondBrKind<\/span><span class=\"z-punctuation\">,<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">    },<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>These enum arms could be considered similar to \"encodings\" in the old backend,\nexcept that they are defined in a much more straightforward way. Whereas old\nCranelift backends had to define instruction encodings using a DSL, and these\nencodings were assigned a numeric index and a special bit-packed encoding for\nadditional instruction parameters, here the instructions are simply stored in\ntype-safe and easy-to-use Rust data structures.<\/p>\n<p>We will not discuss the VCode data-structure design or instruction interface\nmuch further, except to note that the relevant instruction-emission\nfunctionality for a new machine backend can be implemented by providing\na <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/cb306fd514f34e7dd818bb17658b93fba98e2567\/cranelift\/codegen\/src\/machinst\/mod.rs#L140\"><code>MachInst<\/code> trait\nimplementation<\/a>\nfor one's instruction type (and then lowering into it; see below). We believe,\nand early experience seems to indicate, that this is a much easier task\nthan what was required to develop a backend in Cranelift's old DSL-based\nframework.<\/p>\n<h2 id=\"lowering-from-clif-to-vcode\">Lowering from CLIF to VCode<\/h2>\n<p>We've now come to the most interesting design question: how do we lower from\nCLIF instructions, which are machine-independent, into VCode with the\nappropriate type of CPU instructions? In other words, what have we replaced the\nexpansion-based legalization and encoding scheme with?<\/p>\n<p>In short, the scheme is a <em>single pass<\/em> over the CLIF instructions, and at each\ninstruction, we invoke a function provided by the machine backend to lower the\nCLIF instruction into VCode instruction(s). The backend is given a\n\"<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/cb306fd514f34e7dd818bb17658b93fba98e2567\/cranelift\/codegen\/src\/machinst\/lower.rs#L58\">lowering\ncontext<\/a>\"\nby which it can examine the instruction and the values that flow into it,\nperforming \"tree matching\" as desired (see below). This naturally allows\n1-to-1, 1-to-many, or many-to-1 translations. We incorporate a\nreference-counting scheme into this pass to ensure that instructions are only\ngenerated if their values are actually used; this is necessary to eliminate\ndead code when many-to-1 matches occur.<\/p>\n<h3 id=\"tree-matching\">Tree Matching<\/h3>\n<p>Recall that the old design allowed for 1-to-1 and 1-to-many mappings from CLIF\ninstructions to machine instructions, but not many-to-1. This is particularly\nproblematic when it comes to pattern-matching for things like addressing modes,\nwhere we want to recognize particular combinations of operations and choose a\nspecific instruction that covers all of those operations at once.<\/p>\n<p>Let's start by defining a \"tree\" that is rooted at a particular CLIF\ninstruction. For each argument to the instruction, we can look \"up\" the program\nto find its producer (def). Because CLIF is in SSA form, either the instruction\nargument is an ordinary value, which must have exactly one definition, or it is\na block parameter (\u03c6-node in conventional SSA formulations) that represents\nmultiple possible definitions. We will say that if we reach a block parameter\n(\u03c6-node), we simply end at a tree leaf -- it is perfectly alright to\npattern-match on a tree that is a <em>subset<\/em> of the true dataflow (we might get\nsuboptimal code, but it will still be correct). For example, given the CLIF\ncode:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>block0(v0: i64, v1: i64, v2: b1):<\/span><\/span>\n<span class=\"giallo-l\"><span>  brnz v2, block1(v0)<\/span><\/span>\n<span class=\"giallo-l\"><span>  jump block1(v1)<\/span><\/span>\n<span class=\"giallo-l\"><\/span>\n<span class=\"giallo-l\"><span>block1(v2: i64):<\/span><\/span>\n<span class=\"giallo-l\"><span>  v3 = iconst.i64 64<\/span><\/span>\n<span class=\"giallo-l\"><span>  v4 = iadd.i64 v2, v3<\/span><\/span>\n<span class=\"giallo-l\"><span>  v5 = iadd.i64 v4, v0<\/span><\/span>\n<span class=\"giallo-l\"><span>  v6 = load.i64 v5<\/span><\/span>\n<span class=\"giallo-l\"><span>  return v6<\/span><\/span><\/code><\/pre>\n<p>let's consider the load instruction: <code>v6 = load.i64 v5<\/code>. A simple code\ngenerator could map this 1-to-1 to the CPU's ordinary load instruction, using\nthe register holding <code>v5<\/code> as an address. This would certainly be correct.\nHowever, we might be able to do better: for example, on AArch64, the available\naddressing modes include a two-register sum <code>ldr x0, [x1, x2]<\/code> or a register\nwith a constant offset <code>ldr x0, [x1, #64]<\/code>.<\/p>\n<p>The \"operand tree\" might be drawn like this:<\/p>\n<p><img src=\"\/assets\/2020-09-10-load-operands.svg\" alt=\"Figure: operand tree for load instruction\" \/><\/p>\n<p>We stop at <code>v2<\/code> and <code>v0<\/code> because they are block parameters; we don't know with\ncertainty which instruction will produce these values. We can replace <code>v3<\/code> with\nthe constant <code>64<\/code>. Given this view, the lowering process for the load\ninstruction can fairly easily choose an addressing mode. (On AArch64, the code\nto make this choice is\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/cb306fd514f34e7dd818bb17658b93fba98e2567\/cranelift\/codegen\/src\/isa\/aarch64\/lower.rs#L653\">here<\/a>;\nin this case it would choose the register + constant immediate form, generating\na separate add instruction for <code>v0 + v2<\/code>.)<\/p>\n<p>Note that we do not actually explicitly construct an operand tree during\nlowering. Instead, the machine backend can query each instruction input, and\nthe lowering framework will provide <a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/cb306fd514f34e7dd818bb17658b93fba98e2567\/cranelift\/codegen\/src\/machinst\/lower.rs#L176\">a\nstruct<\/a>\ngiving the producing instruction if known, the constant value if known, and the\nregister that will hold the value if needed. The backend may traverse up the\ntree (via the \"producing instruction\") as many times as needed. If it cannot\ncombine the operation of an instruction further up the tree into the root\ninstruction, it can simply use the value in the register at that point instead;\nit is always safe (though possibly suboptimal) to generate machine instructions\nfor only the root instruction.<\/p>\n<h3 id=\"lowering-an-instruction\">Lowering an Instruction<\/h3>\n<p>Given this matching strategy, then, how do we actually do the translation?\nBasically, the backend provides a function that is called once per CLIF\ninstruction, at the \"root\" of the operand tree, and can produce as many machine\ninstructions as it likes. This function is essentially just a large <code>match<\/code>\nstatement over the opcode of the root CLIF instruction, with the match-arms\nlooking deeper as needed.<\/p>\n<p>Here is a simplified version of the match-arm for an integer add operation\nlowered to AArch64 (the full version is\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/cb306fd514f34e7dd818bb17658b93fba98e2567\/cranelift\/codegen\/src\/isa\/aarch64\/lower_inst.rs#L75\">here<\/a>):<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"rust\"><span class=\"giallo-l\"><span class=\"z-keyword\">match<\/span><span class=\"z-variable z-other\"> op<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-entity z-name z-type\">    Opcode<\/span><span class=\"z-keyword z-operator\">::<\/span><span class=\"z-entity z-name z-type\">Iadd<\/span><span class=\"z-keyword z-operator\"> =&gt;<\/span><span class=\"z-punctuation\"> {<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">        let<\/span><span class=\"z-variable z-other\"> rd<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-function\"> get_output_reg<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">ctx<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> outputs<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">0<\/span><span class=\"z-punctuation\">]);<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">        let<\/span><span class=\"z-variable z-other\"> rn<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-function\"> put_input_in_reg<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">ctx<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> inputs<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">0<\/span><span class=\"z-punctuation\">]);<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">        let<\/span><span class=\"z-variable z-other\"> rm<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-function\"> put_input_in_rse_imm12<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">ctx<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> inputs<\/span><span class=\"z-punctuation\">[<\/span><span class=\"z-constant z-numeric\">1<\/span><span class=\"z-punctuation\">]);<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-storage\">        let<\/span><span class=\"z-variable z-other\"> alu_op<\/span><span class=\"z-keyword z-operator\"> =<\/span><span class=\"z-entity z-name z-function\"> choose_32_64<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">ty<\/span><span class=\"z-punctuation\">,<\/span><span> ALUOp<\/span><span class=\"z-keyword z-operator\">::<\/span><span class=\"z-entity z-name z-type\">Add32<\/span><span class=\"z-punctuation\">,<\/span><span> ALUOp<\/span><span class=\"z-keyword z-operator\">::<\/span><span class=\"z-entity z-name z-type\">Add64<\/span><span class=\"z-punctuation\">);<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-variable z-other\">        ctx<\/span><span class=\"z-keyword z-operator\">.<\/span><span class=\"z-entity z-name z-function\">emit<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-entity z-name z-function\">alu_inst_imm12<\/span><span class=\"z-punctuation\">(<\/span><span class=\"z-variable z-other\">alu_op<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> rd<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> rn<\/span><span class=\"z-punctuation\">,<\/span><span class=\"z-variable z-other\"> rm<\/span><span class=\"z-punctuation\">));<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">    }<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation z-definition z-comment z-comment\">    \/\/ ...<\/span><\/span>\n<span class=\"giallo-l\"><span class=\"z-punctuation\">}<\/span><\/span><\/code><\/pre>\n<p>There is some magic that happens in several helper functions here.\n<code>put_input_in_reg()<\/code> invokes the proper methods on the <code>ctx<\/code> to look up the\nregister that holds an input value. <code>put_input_in_rse_imm12()<\/code> is more\ninteresting: it returns a\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/cb306fd514f34e7dd818bb17658b93fba98e2567\/cranelift\/codegen\/src\/isa\/aarch64\/lower.rs#L63\"><code>ResultRSEImm12<\/code><\/a>,\nwhich is a \"register, shifted register, extended register, or 12-bit\nimmediate\". This set of choices captures all of the options we have for the\nsecond argument of an add instruction on AArch64. The helper looks at the node\nin the operand tree and attempts to match either a shift or zero\/sign-extend\noperator, which can be incorporated directly into the add. It also checks\nwhether the operand is a constant and if so, could fit into a 12-bit immediate\nfield. If not, it falls back to simply using the register input.\n<code>alu_inst_imm12()<\/code> then breaks down this enum and chooses the appropriate\n<code>Inst<\/code> arm (<code>AluRRR<\/code>, <code>AluRRRShift<\/code>, <code>AluRRRExtend<\/code>, or <code>AluRRImm12<\/code>\nrespectively).<\/p>\n<p>And that's it! No need for legalization and repeated code editing to match\nseveral operations and produce a machine instruction. We have found this way of\nwriting lowering logic to be quite straightforward and easy to understand.<\/p>\n<h3 id=\"backward-pass-with-use-counts\">Backward Pass with Use-Counts<\/h3>\n<p>Now that we can lower a single instruction, how do we lower a function body\nwith many instructions? This is not quite as straightforward as looping over\nthe instructions and invoking the match-over-opcode function described above\n(though that would actually work). In particular, we want to handle the\nmany-to-1 case more efficiently. Consider what happens when the\nadd-instruction logic above is able to incorporate, say, a left-shift\noperator into the add instruction. The <code>add<\/code> machine instruction would then use\nthe <em>shift<\/em>'s input register, and completely ignore the shift's output. If the\nshift operator has no other uses, we should avoid doing the computation\nentirely; otherwise, there was no point in merging the operation into the add.<\/p>\n<p>We implement a sort of reference counting to solve this problem. In particular,\nwe track whether any given SSA value is actually used, and we only generate\ncode for a CLIF instruction if any of its results are used (or if it has a\nside-effect that must occur). This is a form of <a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Dead_code_elimination\">dead-code\nelimination<\/a> but\nintegrated into the single lowering pass.<\/p>\n<p>To know whether a value is used, we simply track a counter per value,\ninitialized to zero. Whenever the machine backend uses a register input (as\nopposed to using a constant value directly, or incorporating the producing\ninstruction's operation), it notifies the lowering driver that this register\nhas been used.<\/p>\n<p>We must see uses before defs for this to work. Thus, we iterate over\nthe function body \"backward\". Specifically, we iterate in\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Depth-first_search#Vertex_orderings\">postorder<\/a>;\nthis way, all instructions are seen before instructions that\n<a rel=\"external\" href=\"https:\/\/en.wikipedia.org\/wiki\/Dominator_(graph_theory)\">dominate<\/a>\nthem, so given SSA form, we see uses before defs.<\/p>\n<p>Finally, we have to consider side-effects carefully. This matters in two ways.\nFirst, if an instruction has a side-effect, then we must lower it into VCode\neven if its result(s) have no uses. Second, we cannot allow an operation to be\nmerged into another if this would move a side-effecting operation over another\nor alter whether it might execute. We ensure side-effect correctness with a\n\"coloring\" scheme (in a forward pass, assign a color to every instruction, and\nupdate the color on every side effect and on every new basic block); the\nproducing instruction is only considered for possible merging with its\nconsuming instruction if it has no side-effects (hence can always be moved) or\nif it has the same color as the consuming instruction (hence would not move\nover another side effect).<\/p>\n<p>The lowering procedure is as follows (<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/machinst\/lower.rs#L693\">full version\nhere<\/a>):<\/p>\n<ol>\n<li>Compute instruction colors based on side-effects.<\/li>\n<li>Allocate virtual registers to all SSA values. It's OK if we don't use some;\nan unused virtual register will not be allocated any real register.<\/li>\n<li>Iterate in postorder over instructions. If the instruction has a\nside-effect, or if any of its results are used, call into the\nmachine backend to lower it.<\/li>\n<li>Reverse the VCode instructons so that they appear in forward order. <sup class=\"footnote-reference\"><a href=\"#3\">3<\/a><\/sup><\/li>\n<\/ol>\n<p>Easy!<\/p>\n<h3 id=\"examples\">Examples<\/h3>\n<p>Let's see how this works in real life! Consider the following CLIF code:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>function %f25(i32, i32) -&gt; i32 {<\/span><\/span>\n<span class=\"giallo-l\"><span>block0(v0: i32, v1: i32):<\/span><\/span>\n<span class=\"giallo-l\"><span>  v2 = iconst.i32 21<\/span><\/span>\n<span class=\"giallo-l\"><span>  v3 = ishl.i32 v0, v2<\/span><\/span>\n<span class=\"giallo-l\"><span>  v4 = isub.i32 v1, v3<\/span><\/span>\n<span class=\"giallo-l\"><span>  return v4<\/span><\/span>\n<span class=\"giallo-l\"><span>}<\/span><\/span><\/code><\/pre>\n<p>We expect that the left-shift (<code>ishl<\/code>) operation should be merged into the\nsubtract operation on AArch64, using the reg-reg-shift form of ALU instruction,\nand indeed this happens (here I am showing the debug-dump format one can see\nwith <code>RUST_LOG=debug<\/code> when running <code>clif-util compile -d --target aarch64<\/code>):<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>VCode {<\/span><\/span>\n<span class=\"giallo-l\"><span>  Entry block: 0<\/span><\/span>\n<span class=\"giallo-l\"><span>Block 0:<\/span><\/span>\n<span class=\"giallo-l\"><span>  (original IR block: block0)<\/span><\/span>\n<span class=\"giallo-l\"><span>  (instruction range: 0 .. 6)<\/span><\/span>\n<span class=\"giallo-l\"><span>  Inst 0:   mov %v0J, x0<\/span><\/span>\n<span class=\"giallo-l\"><span>  Inst 1:   mov %v1J, x1<\/span><\/span>\n<span class=\"giallo-l\"><span>  Inst 2:   sub %v4Jw, %v1Jw, %v0Jw, LSL 21<\/span><\/span>\n<span class=\"giallo-l\"><span>  Inst 3:   mov %v5J, %v4J<\/span><\/span>\n<span class=\"giallo-l\"><span>  Inst 4:   mov x0, %v5J<\/span><\/span>\n<span class=\"giallo-l\"><span>  Inst 5:   ret<\/span><\/span>\n<span class=\"giallo-l\"><span>}<\/span><\/span><\/code><\/pre>\n<p>This then passes through the register allocator, has a prologue and epilogue\nattached (we cannot generate these until we know which registers are clobbered),\nhas redundant moves elided, and becomes:<\/p>\n<pre class=\"giallo z-code\"><code data-lang=\"plain\"><span class=\"giallo-l\"><span>stp fp, lr, [sp, #-16]!<\/span><\/span>\n<span class=\"giallo-l\"><span>mov fp, sp<\/span><\/span>\n<span class=\"giallo-l\"><span>sub w0, w1, w0, LSL 21<\/span><\/span>\n<span class=\"giallo-l\"><span>mov sp, fp<\/span><\/span>\n<span class=\"giallo-l\"><span>ldp fp, lr, [sp], #16<\/span><\/span>\n<span class=\"giallo-l\"><span>ret<\/span><\/span><\/code><\/pre>\n<p>which is a perfectly valid function, correct and callable from C, on\nAArch64! (We could do better if we knew that this were a leaf function and\navoided the stack-frame setup and teardown! Alas, many optimization\nopportunities remain.)<\/p>\n<p>There are many other examples of interesting instruction-selection cases in our\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\/filetests\/filetests\/vcode\/aarch64\">filetests<\/a>.\nOne of our favorite pastimes lately is to stare at disassemblies and find inefficient\ntranslations, improving the pattern-matching as required, so these are slowly\ngetting better (my brilliant colleague Julian Seward has built a custom tool\nthat dumps the hottest basic blocks from a given JIT execution and has found\nquite a number of improvements in our AArch64 and x86-64 backends).<\/p>\n<h2 id=\"next-efficient-code-generation-passes-and-checking-the-register-allocator\">Next: Efficient Code-Generation Passes, and Checking the Register Allocator<\/h2>\n<p>I've covered a lot of ground in this post, but there's still a lot more to say\nabout the new Cranelift backend framework!<\/p>\n<p>In the second post, I'd like to talk about how we designed the passes <em>after<\/em>\nVCode lowering to be as efficient as possible. In particular this will involve\nthe way in which we simplify branches, which avoids the more usual step-by-step\nprocess of removing empty basic blocks and flipping branch conditions and\ntaking advantage of fallthrough paths, instead doing last-minute edits as the\nbinary code is being emitted (see the\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/blob\/main\/cranelift\/codegen\/src\/machinst\/buffer.rs\"><code>MachBuffer<\/code><\/a>\nimplementation for all the details).<\/p>\n<p>Then, in the third post, I'll talk about how I've used abstract interpretation\nto build a symbolic checker for our register allocator, which has been\neffective at finding several interesting bugs while fuzzing.<\/p>\n<p>Stay tuned!<\/p>\n<p>In the meantime, for any and all discussions about Cranelift, please feel free\nto join us on our <a rel=\"external\" href=\"https:\/\/bytecodealliance.zulipchat.com\/\">Bytecode Alliance Zulip\nchat<\/a> (here's a\n<a rel=\"external\" href=\"https:\/\/bytecodealliance.zulipchat.com\/#narrow\/stream\/217117-cranelift\/topic\/blog.20post.20on.20new.20backend\">topic<\/a>\nfor this post)!<\/p>\n<hr \/>\n<p><em>Thanks to Julian Seward and Benjamin Bouvier for reviewing this post and\nsuggesting several additions and corrections.<\/em><\/p>\n<div class=\"footnote-definition\" id=\"1\"><sup class=\"footnote-definition-label\">1<\/sup>\n<p>Note that this description skips several quite important steps that come\nafter instructions have encodings. Most importantly, we still must perform\n<em>register allocation<\/em>, which chooses machine registers to hold each value in\nthe IR. This may involve inserting instructions as well, when values need to\nbe spilled to or reloaded from the stack or simply moved between registers.\nThen, after several other housekeeping tasks (such as resolving branches and\noptimizing their forms for the actual machine-code offsets), we can actually\nuse the encodings to emit machine code.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"2\"><sup class=\"footnote-definition-label\">2<\/sup>\n<p>We also support a \"mod\" (modify) type of register reference that is both\na use and def, while ensuring that the same register is allocated for the\nuse- and the def-points. This replaces an earlier mechanism known as \"tied\noperands\" that introduced an ad-hoc constraint to the register allocator.\nMods instead are handled by simply extending the live-range through the\ninstruction.<\/p>\n<\/div>\n<div class=\"footnote-definition\" id=\"3\"><sup class=\"footnote-definition-label\">3<\/sup>\n<p>The reversal scheme is actually a bit more subtle than this. We want to\nemit instructions in forward order within the lowering for a single CLIF\ninstruction, but we visit CLIF instructions backward. To make this work, we\nkeep a buffer of lowered VCode instructions per CLIF instruction in forward\norder; at the end of a single CLIF instruction, these are copied in reverse\norder to a buffer of lowered VCode instructions for the basic block. Because\nwe visit instructions within the block backward, this buffer contains the\nVCode sequence for the basic block in reverse order. Then, at the end of the\nblock, we reverse it again onto the tail of the VCode buffer. The end result\nis that we see VCode instructions in forward order for each CLIF instruction\nin forward order, contained within basic blocks in forward order (phew!).<\/p>\n<\/div>\n"},{"title":"blog.cfallin is live!","published":"2020-09-17T00:00:00+00:00","updated":"2020-09-17T00:00:00+00:00","author":{"name":{}},"link":{"@attributes":{"rel":"alternate","type":"text\/html","href":"https:\/\/cfallin.org\/blog\/first-post\/"}},"id":"https:\/\/cfallin.org\/blog\/first-post\/","content":"<p>Hello, and welcome to <a rel=\"external\" href=\"https:\/\/cfallin.org\/blog\/\">blog.cfallin<\/a>! I've thought for\na while that it might be nice to share, occasionally, some thoughts on\nwhatever technical tidbits interest me. This blog will likely be home to\nassorted ramblings on compilers, runtimes, and the like; you can find a bit\nmore about my background at <a href=\"\/about\/\">'About'<\/a>.<\/p>\n<p>My first post, coming soon, will be about the new compiler backend framework\nI've developed (along with extremely capable co-conspirators) for\n<a rel=\"external\" href=\"https:\/\/github.com\/bytecodealliance\/wasmtime\/tree\/main\/cranelift\">Cranelift<\/a>,\na compiler in Rust that will soon be used in production in Firefox, among other\nplaces. Stay tuned.<\/p>\n"}]}