submilli-wasm 0.2.0

A WebAssembly interpreter with a wasmtime-compatible API, built for fast compilation and startup
Documentation
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
//! The internal instruction set (`Op`) and `CompiledFunc` layout.
//!
//! A flat, one-variant-per-opcode enum so the interpreter hot loop is a single
//! `match`. Structured control flow is lowered away by the compile pass — branches
//! carry their resolved target plus the operand-stack fixup inline (the folded
//! sidetable, see ARCHITECTURE §5).

use crate::canon::IrHeap;

pub(crate) use super::op_types::*;

/// One internal instruction. 16 bytes and **not** a drop type: wide immediates
/// live out of line (`br_table`/`br_on_cast` edges and rare 64-bit memarg offsets in per-function
/// pools), `BranchTarget` packs its stack fixup into two `u16`s, and nothing inline is `Box`ed —
/// a `Vec<Op>` frees in one shot at teardown, and the op stream is 33% smaller resident than the
/// previous 24-byte layout at unchanged decode shape (fixed width, aligned fields).
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub(crate) enum Op {
    // --- control ---
    Unreachable,
    Nop,
    // Every branch carries the slot of the operand-stack top after its operands are consumed
    // (the condition's slot for a conditional branch): the base of its `keep`/`pop` fixup.
    Br(BranchTarget, Slot),
    BrIf(BranchTarget, Slot),
    /// Branch when the i32 condition is zero (used to lower `if`).
    BrIfNot(BranchTarget, Slot),
    /// Fused i32 compare-and-branch: branches to `ip` when `cmp(lhs, rhs) != negate` — an
    /// adjacent relop + `br_if`/`br_if_not` pair collapsed at compile time (one dispatch, no
    /// intermediate bool). Fused only when nothing can jump between the pair (see `control`) and
    /// the branch keeps no values (so it needs no operand fixup).
    BrIfCmp {
        kind: CmpKind,
        negate: bool,
        ip: u32,
        lhs: Slot,
        rhs: Slot,
    },
    /// [`Op::BrIfCmp`] against an immediate right operand (a folded constant, step 2c).
    BrIfCmpImm {
        kind: CmpKind,
        negate: bool,
        ip: u32,
        lhs: Slot,
        imm: i32,
    },
    /// Targets live in [`CompiledFunc::br_tables`]; see [`BrTableRange`].
    BrTable(BrTableRange, Slot),
    // Calls: a direct call's slot is its first argument (the callee's frame starts there; results
    // land there). Indirect/ref calls carry their table-index / funcref operand's slot instead
    // (read before the callee is known); the arguments sit just below it.
    Call(FuncIdx, Slot),
    /// `call` of a function this module defines, by defined-function index ([`Code::index`]): it
    /// runs in the caller's instance from the caller's module, so nothing resolves through the
    /// store.
    ///
    /// [`Code::index`]: crate::module::code::Code::index
    CallInternal(u32, Slot),
    CallIndirect {
        type_idx: TypeIdx,
        table: TableIdx,
        s: Slot,
    },
    /// `call_ref`: call the funcref operand (null traps); type index is validation-only.
    CallRef(TypeIdx, Slot),
    /// Tail calls (#39): replace the current frame with the callee, which returns to the caller's caller.
    ReturnCall(FuncIdx, Slot),
    ReturnCallIndirect {
        type_idx: TypeIdx,
        table: TableIdx,
        s: Slot,
    },
    ReturnCallRef(TypeIdx, Slot),
    /// Branch when the popped reference is null, dropping it; on fall-through it stays (non-null).
    BrOnNull(BranchTarget, Slot),
    /// Branch when the popped reference is non-null, keeping it on the target; on null it is dropped.
    BrOnNonNull(BranchTarget, Slot),
    /// `throw $tag`: take the tag's args (from the slot), build an exception, and unwind
    /// (module-relative tag index).
    Throw(u32, Slot),
    /// `throw_ref`: re-throw the `exnref` operand (null traps; stack-polymorphic).
    ThrowRef(Slot),

    // --- parametric ---
    Select(Slot),

    // --- variable ---
    /// `(dst, src)`: copy one frame slot to another — `local.get`/`local.set`/`local.tee` (a local's
    /// slot is its index).
    Copy(Slot, Slot),
    GlobalGet(Slot, GlobalIdx),
    GlobalSet(Slot, GlobalIdx),

    // --- memory loads ---
    I32Load(LoadArg),
    I64Load(LoadArg),
    F32Load(LoadArg),
    F64Load(LoadArg),
    I32Load8S(LoadArg),
    I32Load8U(LoadArg),
    I32Load16S(LoadArg),
    I32Load16U(LoadArg),
    I64Load8S(LoadArg),
    I64Load8U(LoadArg),
    I64Load16S(LoadArg),
    I64Load16U(LoadArg),
    I64Load32S(LoadArg),
    I64Load32U(LoadArg),

    // --- memory stores ---
    I32Store(StoreArg),
    I64Store(StoreArg),
    F32Store(StoreArg),
    F64Store(StoreArg),
    I32Store8(StoreArg),
    I32Store16(StoreArg),
    I64Store8(StoreArg),
    I64Store16(StoreArg),
    I64Store32(StoreArg),

    // --- memory management (carry an explicit memory index, #41) ---
    /// `(memory, dst)`.
    MemorySize(u32, Slot),
    /// `(memory, delta)`: the old size (or -1) replaces the delta.
    MemoryGrow(u32, Slot),
    /// `(segment, memory, dst)`: operands `dst, src, len` from the slot.
    MemoryInit(DataIdx, u32, Slot),
    DataDrop(DataIdx),
    /// `memory.copy dst_mem src_mem` — the two indices may differ; operands `dst, src, len`.
    MemoryCopy(u32, u32, Slot),
    /// `(memory, dst)`: operands `dst, val, len`.
    MemoryFill(u32, Slot),

    // --- table management (bulk-memory subset) ---
    /// Operands `dst, src, len` from `s`.
    TableInit {
        elem: ElemIdx,
        table: TableIdx,
        s: Slot,
    },
    /// Operands `dst, src, len` from `s`.
    TableCopy {
        dst_table: TableIdx,
        src_table: TableIdx,
        s: Slot,
    },
    ElemDrop(ElemIdx),

    // --- references + table ref-ops (reference-types) ---
    RefNull(IrHeap, Slot),
    RefFunc(FuncIdx, Slot),
    RefIsNull(Slot),
    /// `ref.as_non_null`: trap if the reference is null, else leave it in place.
    RefAsNonNull(Slot),
    /// `(table, s)`: the index at `s` is replaced by the element.
    TableGet(TableIdx, Slot),
    /// `(table, s)`: operands `index, value`.
    TableSet(TableIdx, Slot),
    TableSize(TableIdx, Slot),
    /// `(table, s)`: operands `init, delta`; the old size (or -1) replaces them.
    TableGrow(TableIdx, Slot),
    /// `(table, s)`: operands `dst, value, len`.
    TableFill(TableIdx, Slot),

    // --- i32 with an immediate right operand (step 2c): `(dst, lhs, k)`, a constant folded out
    // of the op stream ---
    /// `lhs + k` (also `lhs - c`, as `k = -c`).
    I32AddImm(Slot, Slot, i32),
    I32MulImm(Slot, Slot, i32),
    I32AndImm(Slot, Slot, i32),
    I32OrImm(Slot, Slot, i32),
    I32XorImm(Slot, Slot, i32),
    I32ShlImm(Slot, Slot, i32),
    I32ShrSImm(Slot, Slot, i32),
    I32ShrUImm(Slot, Slot, i32),
    /// An i32 comparison against an immediate: `dst` = `lhs <kind> imm`.
    I32CmpImm {
        kind: CmpKind,
        dst: Slot,
        lhs: Slot,
        imm: i32,
    },

    // --- constants (floats as raw bits) ---
    I32Const(Slot, i32),
    I64Const(Slot, i64),
    F32Const(Slot, u32),
    F64Const(Slot, u64),

    // --- i32 comparisons / numeric ---
    I32Eqz(Slot, Slot),
    I32Eq(Slot, Slot, Slot),
    I32Ne(Slot, Slot, Slot),
    I32LtS(Slot, Slot, Slot),
    I32LtU(Slot, Slot, Slot),
    I32GtS(Slot, Slot, Slot),
    I32GtU(Slot, Slot, Slot),
    I32LeS(Slot, Slot, Slot),
    I32LeU(Slot, Slot, Slot),
    I32GeS(Slot, Slot, Slot),
    I32GeU(Slot, Slot, Slot),
    I32Clz(Slot, Slot),
    I32Ctz(Slot, Slot),
    I32Popcnt(Slot, Slot),
    I32Add(Slot, Slot, Slot),
    I32Sub(Slot, Slot, Slot),
    I32Mul(Slot, Slot, Slot),
    I32DivS(Slot, Slot, Slot),
    I32DivU(Slot, Slot, Slot),
    I32RemS(Slot, Slot, Slot),
    I32RemU(Slot, Slot, Slot),
    I32And(Slot, Slot, Slot),
    I32Or(Slot, Slot, Slot),
    I32Xor(Slot, Slot, Slot),
    I32Shl(Slot, Slot, Slot),
    I32ShrS(Slot, Slot, Slot),
    I32ShrU(Slot, Slot, Slot),
    I32Rotl(Slot, Slot, Slot),
    I32Rotr(Slot, Slot, Slot),

    // --- i64 comparisons / numeric ---
    I64Eqz(Slot, Slot),
    I64Eq(Slot, Slot, Slot),
    I64Ne(Slot, Slot, Slot),
    I64LtS(Slot, Slot, Slot),
    I64LtU(Slot, Slot, Slot),
    I64GtS(Slot, Slot, Slot),
    I64GtU(Slot, Slot, Slot),
    I64LeS(Slot, Slot, Slot),
    I64LeU(Slot, Slot, Slot),
    I64GeS(Slot, Slot, Slot),
    I64GeU(Slot, Slot, Slot),
    I64Clz(Slot, Slot),
    I64Ctz(Slot, Slot),
    I64Popcnt(Slot, Slot),
    I64Add(Slot, Slot, Slot),
    I64Sub(Slot, Slot, Slot),
    I64Mul(Slot, Slot, Slot),
    I64DivS(Slot, Slot, Slot),
    I64DivU(Slot, Slot, Slot),
    I64RemS(Slot, Slot, Slot),
    I64RemU(Slot, Slot, Slot),
    I64And(Slot, Slot, Slot),
    I64Or(Slot, Slot, Slot),
    I64Xor(Slot, Slot, Slot),
    I64Shl(Slot, Slot, Slot),
    I64ShrS(Slot, Slot, Slot),
    I64ShrU(Slot, Slot, Slot),
    I64Rotl(Slot, Slot, Slot),
    I64Rotr(Slot, Slot, Slot),

    // --- f32 ---
    F32Eq(Slot, Slot, Slot),
    F32Ne(Slot, Slot, Slot),
    F32Lt(Slot, Slot, Slot),
    F32Gt(Slot, Slot, Slot),
    F32Le(Slot, Slot, Slot),
    F32Ge(Slot, Slot, Slot),
    F32Abs(Slot, Slot),
    F32Neg(Slot, Slot),
    F32Ceil(Slot, Slot),
    F32Floor(Slot, Slot),
    F32Trunc(Slot, Slot),
    F32Nearest(Slot, Slot),
    F32Sqrt(Slot, Slot),
    F32Add(Slot, Slot, Slot),
    F32Sub(Slot, Slot, Slot),
    F32Mul(Slot, Slot, Slot),
    F32Div(Slot, Slot, Slot),
    F32Min(Slot, Slot, Slot),
    F32Max(Slot, Slot, Slot),
    F32Copysign(Slot, Slot, Slot),

    // --- f64 ---
    F64Eq(Slot, Slot, Slot),
    F64Ne(Slot, Slot, Slot),
    F64Lt(Slot, Slot, Slot),
    F64Gt(Slot, Slot, Slot),
    F64Le(Slot, Slot, Slot),
    F64Ge(Slot, Slot, Slot),
    F64Abs(Slot, Slot),
    F64Neg(Slot, Slot),
    F64Ceil(Slot, Slot),
    F64Floor(Slot, Slot),
    F64Trunc(Slot, Slot),
    F64Nearest(Slot, Slot),
    F64Sqrt(Slot, Slot),
    F64Add(Slot, Slot, Slot),
    F64Sub(Slot, Slot, Slot),
    F64Mul(Slot, Slot, Slot),
    F64Div(Slot, Slot, Slot),
    F64Min(Slot, Slot, Slot),
    F64Max(Slot, Slot, Slot),
    F64Copysign(Slot, Slot, Slot),
    // --- conversions ---
    I32WrapI64(Slot, Slot),
    I32TruncF32S(Slot, Slot),
    I32TruncF32U(Slot, Slot),
    I32TruncF64S(Slot, Slot),
    I32TruncF64U(Slot, Slot),
    I64ExtendI32S(Slot, Slot),
    I64ExtendI32U(Slot, Slot),
    I64TruncF32S(Slot, Slot),
    I64TruncF32U(Slot, Slot),
    I64TruncF64S(Slot, Slot),
    I64TruncF64U(Slot, Slot),
    F32ConvertI32S(Slot, Slot),
    F32ConvertI32U(Slot, Slot),
    F32ConvertI64S(Slot, Slot),
    F32ConvertI64U(Slot, Slot),
    F32DemoteF64(Slot, Slot),
    F64ConvertI32S(Slot, Slot),
    F64ConvertI32U(Slot, Slot),
    F64ConvertI64S(Slot, Slot),
    F64ConvertI64U(Slot, Slot),
    F64PromoteF32(Slot, Slot),
    I32ReinterpretF32(Slot, Slot),
    I64ReinterpretF64(Slot, Slot),
    F32ReinterpretI32(Slot, Slot),
    F64ReinterpretI64(Slot, Slot),
    // --- sign-extension ops ---
    I32Extend8S(Slot, Slot),
    I32Extend16S(Slot, Slot),
    I64Extend8S(Slot, Slot),
    I64Extend16S(Slot, Slot),
    I64Extend32S(Slot, Slot),
    // --- saturating float-to-int ---
    I32TruncSatF32S(Slot, Slot),
    I32TruncSatF32U(Slot, Slot),
    I32TruncSatF64S(Slot, Slot),
    I32TruncSatF64U(Slot, Slot),
    I64TruncSatF32S(Slot, Slot),
    I64TruncSatF32U(Slot, Slot),
    I64TruncSatF64S(Slot, Slot),
    I64TruncSatF64U(Slot, Slot),

    // GC ops are *stack-window* ops: each carries `top`, the operand-stack top (one past its last
    // operand) before the op — their handlers pop/push relative to it.
    // --- GC aggregates: structs ---
    StructNew(TypeIdx, Slot),
    StructNewDefault(TypeIdx, Slot),
    StructGet {
        ty: TypeIdx,
        field: u32,
        top: Slot,
    },
    StructGetS {
        ty: TypeIdx,
        field: u32,
        top: Slot,
    },
    StructGetU {
        ty: TypeIdx,
        field: u32,
        top: Slot,
    },
    StructSet {
        ty: TypeIdx,
        field: u32,
        top: Slot,
    },

    // --- GC aggregates: arrays ---
    ArrayNew(TypeIdx, Slot),
    ArrayNewDefault(TypeIdx, Slot),
    ArrayNewFixed {
        ty: TypeIdx,
        n: u32,
        top: Slot,
    },
    ArrayNewData {
        ty: TypeIdx,
        data: DataIdx,
        top: Slot,
    },
    ArrayNewElem {
        ty: TypeIdx,
        elem: ElemIdx,
        top: Slot,
    },
    ArrayGet(TypeIdx, Slot),
    ArrayGetS(TypeIdx, Slot),
    ArrayGetU(TypeIdx, Slot),
    ArraySet(TypeIdx, Slot),
    ArrayLen(Slot),
    ArrayFill(TypeIdx, Slot),
    ArrayCopy {
        dst: TypeIdx,
        src: TypeIdx,
        top: Slot,
    },
    ArrayInitData {
        ty: TypeIdx,
        data: DataIdx,
        top: Slot,
    },
    ArrayInitElem {
        ty: TypeIdx,
        elem: ElemIdx,
        top: Slot,
    },

    // --- GC i31 ---
    RefI31(Slot),
    I31GetS(Slot),
    I31GetU(Slot),

    // --- GC casts / equality ---
    RefTest {
        ty: IrHeap,
        nullable: bool,
        top: Slot,
    },
    RefCast {
        ty: IrHeap,
        nullable: bool,
        top: Slot,
    },
    RefEq(Slot),
    /// `any.convert_extern` / `extern.convert_any`: externref ↔ anyref.
    AnyConvertExtern(Slot),
    ExternConvertAny(Slot),
    /// `br_on_cast` / `br_on_cast_fail`: the branch edge lives out of line in
    /// `CompiledFunc::br_tables` (shared with `br_table` cases) — `target`'s bit 31 carries the
    /// cast's `nullable` flag, the low 31 bits index the pool. Keeps the variant inside 16 bytes.
    /// `heap` indexes the module's `cast_heaps` pool (inline, the `IrHeap` would push the variant
    /// past 16 bytes).
    BrOnCast {
        heap: u32,
        target: u32,
        top: Slot,
    },
    BrOnCastFail {
        heap: u32,
        target: u32,
        top: Slot,
    },
    /// Fixed-width SIMD (`v128`, #37); the ~236 vector ops live in [`SimdOp`](super::op_simd::SimdOp).
    /// A stack-window op like the GC ones: `(op, top)`.
    #[cfg(feature = "simd")]
    Simd(super::op_simd::SimdOp, Slot),
}

impl Op {
    /// A stack-window op's `top` (one past its last operand), or `None` for a slot-addressed op.
    pub(crate) fn window_top(&self) -> Option<Slot> {
        match *self {
            Op::StructNew(_, top)
            | Op::StructNewDefault(_, top)
            | Op::StructGet { top, .. }
            | Op::StructGetS { top, .. }
            | Op::StructGetU { top, .. }
            | Op::StructSet { top, .. }
            | Op::ArrayNew(_, top)
            | Op::ArrayNewDefault(_, top)
            | Op::ArrayNewFixed { top, .. }
            | Op::ArrayNewData { top, .. }
            | Op::ArrayNewElem { top, .. }
            | Op::ArrayGet(_, top)
            | Op::ArrayGetS(_, top)
            | Op::ArrayGetU(_, top)
            | Op::ArraySet(_, top)
            | Op::ArrayLen(top)
            | Op::ArrayFill(_, top)
            | Op::ArrayCopy { top, .. }
            | Op::ArrayInitData { top, .. }
            | Op::ArrayInitElem { top, .. }
            | Op::RefI31(top)
            | Op::I31GetS(top)
            | Op::I31GetU(top)
            | Op::RefTest { top, .. }
            | Op::RefCast { top, .. }
            | Op::RefEq(top)
            | Op::AnyConvertExtern(top)
            | Op::ExternConvertAny(top)
            | Op::BrOnCast { top, .. }
            | Op::BrOnCastFail { top, .. } => Some(top),
            #[cfg(feature = "simd")]
            Op::Simd(_, top) => Some(top),
            _ => None,
        }
    }
}

/// The compiled-function record lives in [`super::code`]; re-exported here so the long-standing
/// `module::op::CompiledFunc` path keeps resolving.
pub(crate) use super::code::CompiledFunc;

// The two load-bearing layout properties (see the enum doc). 16 bytes holds with the `simd`
// feature too: `SimdOp`'s 16-byte immediates live in the `v128s` pool.
/// A `match` pattern for any of the listed tuple variants, binding its first field to `$b`.
macro_rules! first_field_of {
    ($b:ident; $($v:ident)*) => { $(Op::$v($b, ..))|* };
}

impl Op {
    /// The result slot of an op that computes into a free destination — reading every operand
    /// before writing it — so the compiler may retarget it at a local (`local.set` fusion).
    pub(crate) fn dst_mut(&mut self) -> Option<&mut Slot> {
        match self {
            first_field_of!(d;
                I32Eqz I32Eq I32Ne I32LtS I32LtU I32GtS I32GtU I32LeS I32LeU I32GeS I32GeU I32Clz
                I32Ctz I32Popcnt I32Add I32Sub I32Mul I32DivS I32DivU I32RemS I32RemU I32And I32Or
                I32Xor I32Shl I32ShrS I32ShrU I32Rotl I32Rotr I64Eqz I64Eq I64Ne I64LtS I64LtU
                I64GtS I64GtU I64LeS I64LeU I64GeS I64GeU I64Clz I64Ctz I64Popcnt I64Add I64Sub
                I64Mul I64DivS I64DivU I64RemS I64RemU I64And I64Or I64Xor I64Shl I64ShrS I64ShrU
                I64Rotl I64Rotr F32Eq F32Ne F32Lt F32Gt F32Le F32Ge F32Abs F32Neg F32Ceil F32Floor
                F32Trunc F32Nearest F32Sqrt F32Add F32Sub F32Mul F32Div F32Min F32Max F32Copysign
                F64Eq F64Ne F64Lt F64Gt F64Le F64Ge F64Abs F64Neg F64Ceil F64Floor F64Trunc
                F64Nearest F64Sqrt F64Add F64Sub F64Mul F64Div F64Min F64Max F64Copysign
                I32WrapI64 I32TruncF32S I32TruncF32U I32TruncF64S I32TruncF64U I64ExtendI32S
                I64ExtendI32U I64TruncF32S I64TruncF32U I64TruncF64S I64TruncF64U F32ConvertI32S
                F32ConvertI32U F32ConvertI64S F32ConvertI64U F32DemoteF64 F64ConvertI32S
                F64ConvertI32U F64ConvertI64S F64ConvertI64U F64PromoteF32 I32ReinterpretF32
                I64ReinterpretF64 F32ReinterpretI32 F64ReinterpretI64 I32Extend8S I32Extend16S
                I64Extend8S I64Extend16S I64Extend32S I32TruncSatF32S I32TruncSatF32U
                I32TruncSatF64S I32TruncSatF64U I64TruncSatF32S I64TruncSatF32U I64TruncSatF64S
                I64TruncSatF64U I32Const I64Const F32Const F64Const GlobalGet I32AddImm I32MulImm
                I32AndImm I32OrImm I32XorImm I32ShlImm I32ShrSImm I32ShrUImm
            ) => Some(d),
            // A pooled load keeps its address in place at `dst`.
            first_field_of!(m;
                I32Load I64Load F32Load F64Load I32Load8S I32Load8U I32Load16S I32Load16U
                I64Load8S I64Load8U I64Load16S I64Load16U I64Load32S I64Load32U
            ) => (m.offset != BIG_MEMARG).then_some(&mut m.dst),
            Op::I32CmpImm { dst, .. } => Some(dst),
            _ => None,
        }
    }
}

const _: () = assert!(std::mem::size_of::<Op>() == 16);
const _: () = assert!(!std::mem::needs_drop::<Op>());