File: isle-integration.md

package info (click to toggle)
rust-wasmtime 26.0.1%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 48,492 kB
  • sloc: ansic: 4,003; sh: 561; javascript: 542; cpp: 254; asm: 175; ml: 96; makefile: 55
file content (298 lines) | stat: -rw-r--r-- 13,477 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# How ISLE is Integrated with Cranelift

This document contains an overview of and FAQ about how ISLE fits into
Cranelift.

## What is ISLE?

ISLE is a domain-specific language for authoring instruction selection and
rewrite rules. ISLE source text is [compiled down into Rust
code](https://github.com/bytecodealliance/wasmtime/tree/main/cranelift/isle#implementation).

Documentation on the ISLE language itself can be found
[here](../isle/docs/language-reference.md).

## How does ISLE integrate with the build system?

The build integration is inside of `cranelift/codegen/build.rs`.

The ISLE compiler is built as a build-dependency, and the build script then
uses it to compile ISLE source to generated Rust code. In other words, the ISLE
compiler behaves as an additional compile step, and ISLE source is rebuilt just
like any Rust source would be. Nothing special needs to be done when editing
ISLE.

Sometimes, it's desirable to see what code is actually generated. By default,
the generated code is placed in a Cargo-managed path in `target/`. If you want
to see the source instead, invoke Cargo with the optional feature
`isle-in-source-tree` as follows:

```shell
$ cargo check -p cranelift-codegen --features isle-in-source-tree
```

This will place the ISLE source in `cranelift/codegen/isle_generated_code/`,
where you can inspect it, debug by setting breakpoints in it, etc. Note that if
you later build without this feature, the build system will require you to
delete the directory. This is to ensure that no out-of-date copies exist, which
could cause significant confusion.

If there are any errors during ISLE compilation (e.g., a type mismatch), you
will see a basic error message with a file, line number, and one-line error. To
see a more detailed output with context, `--features isle-errors` can be used.
This will give pretty-printed errors with source context.

Additionally, the `cranelift-codegen-meta` crate will automatically generate
ISLE `extern` declarations and helpers for working with CLIF. The code that does
this is defined inside `cranelift/codegen/meta/src/gen_inst.rs` and it creates
several ISLE files in the `target/` output directory which are subsequently
read by the ISLE compiler as part of its prologue.

## Where are the relevant files?

* `cranelift/isle`: The ISLE compiler's source code.

* `cranelift/codegen/src/prelude.isle`: Common definitions and declarations for
  ISLE. This gets included in every ISLE compilation.

* `target/.../out/clif_lower.isle`: Auto-generated declarations and helpers
  for working with CLIF for instruction lowering inside ISLE. Generated by
  `cranelift/codegen/build.rs`, which builds it into every backend.
  
* `target/.../out/clif_opt.isle`: Auto-generated declarations and helpers for
  working with CLIF for mid-end optimizations. Generated by
  `cranelift/codegen/build.rs`, which builds it into the mid-end optimizer.

* `cranelift/codegen/src/machinst/isle.rs`: Common Rust code for gluing
  ISLE-generated code into a target architecture's backend. Contains
  implementations of ISA-agnostic `extern` helpers declared in ISLE.

* `cranelift/codegen/src/isa/<arch>/inst.isle`: ISA-specific ISLE
  helpers. Contains things like constructors for each instruction in the ISA, or
  helpers to get a specific register. Helps bridge the gap between the raw,
  non-SSA ISA and the pure, SSA view that the lowering rules have.

* `cranelift/codegen/src/isa/<arch>/lower.isle`: Instruction selection lowering
  rules for an ISA. These should be pure, SSA rewrite rules, that lend
  themselves to eventual verification.

* `cranelift/codegen/src/isa/<arch>/lower/isle.rs`: The Rust glue code for
  integrating this ISA's ISLE-generate Rust code into the rest of the backend
  for this ISA. Contains implementations of ISA-specific `extern` helpers
  declared in ISLE.

## Gluing ISLE's generated code into Cranelift

Each ISA-specific, ISLE-generated file is generic over a `Context` trait that
has a trait method for each `extern` helper defined in ISLE. There is one
concrete implementation of each of these traits, defined in
`cranelift/codegen/src/isa/<arch>/lower/isle.rs`. In general, the way that
ISLE-generated code is glued into the rest of the system is with these trait
implementations.

There may also be a `lower` function defined in `isle.rs` that encapsulates
creating the ISLE `Context` and calling into the generated code.

## Lowering rules are always pure, use SSA

The lowering rules themselves, defined in
`cranelift/codegen/src/isa/<arch>/lower.isle`, must always be a pure mapping
from a CLIF instruction to the target ISA's `MachInst`.

Examples of things that the lowering rules themselves shouldn't deal with or
talk about:

* Registers that are modified (both read and written to, violating SSA)
* Implicit uses of registers
* Maintaining use counts for each CLIF value or virtual register

Instead, these things should be handled by some combination of
`cranelift/codegen/src/isa/<arch>/inst.isle` and general Rust code (either in
`cranelift/codegen/src/isa/<arch>/lower/isle.rs` or elsewhere).

When an instruction modifies a register, both reading from it and writing to it,
we should build an SSA view of that instruction that gets legalized via "move
mitosis" by splitting a move out from the register.

For example, on x86 the `add` instruction reads and writes its first operand:

    add a, b    ==    a = a + b

So we present an SSA facade where `add` operates on three registers, instead of
two, and defines one of them, while reading the other two and leaving them
unmodified:

    add a, b, c    ==    a = b + c

Then, as an implementation detail of the facade, we emit moves as necessary:

    add a, b, c    ==>    mov a, b; add b, c

We call the process of emitting these moves "move mitosis". For ISAs with
ubiquitous use of modified registers and instructions in two-operand form, like
x86, we implement move mitosis with methods on the ISA's `MachInst`. For other
ISAs that are RISCier and where modified registers are pretty rare, such as
aarch64, we implement the handful of move mitosis special cases at the
`inst.isle` layer. Either way, the important thing is that the lowering rules
remain pure.

Finally, note that these moves are generally cleaned up by the register
allocator's move coalescing, and move mitosis will eventually go away completely
once we switch over to `regalloc2`, which takes instructions in SSA form
directly as input.

Instructions that implicitly operate on specific registers, or which require
that certain operands be in certain registers, are handled similarly: the
lowering rules use a pure paradigm that ignores these constraints and has
instructions that explicitly take implicit operands, and we ensure the
constraints are fulfilled a layer below the lowering rules (in `inst.isle` or in
Rust glue code).

## When are lowering rules allowed to have side effects?

Extractors (the matchers that appear on the left-hand sides of `rule`s) should
**never** have side effects. When evaluating a rule's extractors, we haven't yet
committed to evaluating that rule's right-hand side. If the extractors performed
side effects, we could get deeply confusing action-at-a-distance bugs where
rules we never fully match pull the rug out from under our feet.

Anytime you are tempted to perform side effects in an extractor, you should
instead just package up the things you would need in order to perform that side
effect, and then have a separate constructor that takes that package and
performs the side effect it describes. The constructor can only be called inside
a rule's right-hand side, which is only evaluated after we've committed to this
rule, which avoids the action-at-a-distance bugs described earlier.

For example, loads have a side effect in CLIF: they might trap. Therefore, even
if a loaded value is never used, we will emit code that implements that
load. But if we are compiling for x86 we can sink loads into the operand
for another operation depending on how the loaded value is used. If we sink that
load into, say, an `add` then we need to tell the lowering context *not* to
lower the CLIF `load` instruction anymore, because its effectively already
lowered as part of lowering the `add` that uses the loaded value. Marking an
instruction as "already lowered" is a side effect, and we might be tempted to
perform that side effect in the extractor that matches sinkable loads. But we
can't do that because although the load itself might be sinkable, there might be
a reason why we ultimately don't perform this load-sinking rule, and if that
happens we still need to lower the CLIF load.

Therefore, we make the `sinkable_load` extractor create a `SinkableLoad` type
that packages up everything we need to know about the load and how to tell the
lowering context that we've sunk it and the lowering context doesn't need to
lower it anymore, but *it doesn't actually tell that to the lowering context
yet*.

```lisp
;; inst.isle

;; A load that can be sunk into another operation.
(type SinkableLoad extern (enum))

;; Extract a `SinkableLoad` from a value if the value is defined by a compatible
;; load.
(decl sinkable_load (SinkableLoad) Value)
(extern extractor sinkable_load sinkable_load)
```

Then, we pair that with a `sink_load` constructor that takes the `SinkableLoad`,
performs the associated side effect of telling the lowering context not to lower
the load anymore, and returns the x86 operand with the load sunken into it.

```lisp
;; inst.isle

;; Sink a `SinkableLoad` into a `RegMemImm.Mem`.
;;
;; This is a side-effectful operation that notifies the context that the
;; instruction that produced the `SinkableImm` has been sunk into another
;; instruction, and no longer needs to be lowered.
(decl sink_load (SinkableLoad) RegMemImm)
(extern constructor sink_load sink_load)
```

Finally, we can use `sinkable_load` and `sink_load` inside lowering rules that
create instructions where an operand is loaded directly from memory:

```
;; lower.isle

(rule (lower (has_type (fits_in_64 ty)
                       (iadd x (sinkable_load y))))
      (value_reg (add ty
                      (put_in_reg x)
                      (sink_load y))))
```

See the `sinkable_load`, `SinkableLoad`, and `sink_load` declarations inside
`cranelift/codegen/src/isa/x64/inst.isle` as well as their external
implementations inside `cranelift/codegen/src/isa/x64/lower/isle.rs` for
details.

See also the "ISLE code should leverage types" section below.

## ISLE code should leverage types

ISLE is a typed language, and we should leverage that to prevent whole classes
of bugs where possible. Use newtypes liberally.

For example, use the `with_flags` family of helpers to pair flags-producing
instructions with flags-consuming instructions, ensuring that no errant
instructions are ever inserted between our flags-using instructions, clobbering
their flags. See `with_flags`, `ProducesFlags`, and `ConsumesFlags` inside
`cranelift/codegen/src/prelude.isle` for details.

## Implicit type conversions

ISLE supports implicit type conversions, and we will make use of these
where possible to simplify the lowering rules. For example, we have
`Value` and `ValueRegs`; the former denotes SSA values in CLIF, and
the latter denotes the register or registers that hold that value in
lowered code. Prior to introduction of implicit type conversions, we
had many occurrences of expressions like `(value_regs r1 r2)` or
`(value_reg r)`. Given that we have already defined a term
`value_reg`, we can define a conversion such as

```lisp
    (convert Reg ValueRegs value_reg)
```

and the ISLE compiler will automatically insert it where the types
imply that it is necessary. When properly defined, converters allow us
to write rules like

```lisp
    (rule (lower (has_type (fits_in_64 ty)
                           (iadd x y)))
          (add ty x y))
```

### Implicit type conversions and side-effects

While implicit conversions are very convenient, we take care to manage
how they introduce invisible side-effects.

Particularly important here is the fact that implicit conversions can
occur more than once. For example, if we define `(convert Value Reg
put_value_in_reg)`, and we emit an instruction `(add a a)` (say, to
double the value of `a`) where `a` is a `Value`, then we will invoke
`put_value_in_reg` twice.

If this were an external constructor that performed some unique action
per invocation, for example allocating a fresh register, then this
would not be desirable. So we follow a convention when defining
implicit type conversions: the conversion must be *idempotent*, i.e.,
must return the same value if invoked more than
once. `put_value_in_reg` in particular already has this property, so
we are safe to use it as an implicit converter.

Note that this condition is *not* the same as saying that the
converter cannot have side-effects. It is perfectly fine for this to
be the case, and can add significant convenience, even. The small loss
in efficiency from invoking the converter twice is tolerable, and we
could optimize it later if we needed to do so.

Even given this, there may be times where it is still clearer to be
explicit. For example, sinking a load is an explicit and important
detail of some lowering patterns; while we could define an implicit
conversion from `SinkableLoad` to `Reg`, it is probably better for now
to retain the `(sink_load ...)` form for clarity.