lib/std/arch/rv64/isel.rad 48.7 KiB raw
1
//! RV64 instruction selection.
2
//!
3
//! Walks IL and selects RV64 instructions for each operation.
4
//!
5
//! *Register resolution hierarchy*
6
//!
7
//!   getReg(ssa) -> Reg
8
//!     Primitive physical register lookup. Panics if the register is spilled.
9
//!     Used as a building block by the functions below.
10
//!
11
//!   getSrcReg(ssa, scratch) -> Reg
12
//!     Source register for an [`il::Reg`] operand. Returns the physical register,
13
//!     or loads a spilled value into `scratch`. Used for instruction fields
14
//!     typed as [`il::Reg`] (e.g. base addresses in Load/Store/Blit).
15
//!
16
//!   getDstReg(ssa, scratch) -> Reg
17
//!     Destination register for an instruction result. Returns the physical
18
//!     register, or records a pending spill and returns `scratch`. The pending
19
//!     spill is flushed by [`selectBlock`] after each instruction.
20
//!
21
//!   resolveVal(scratch, val) -> Reg
22
//!     Resolve an [`il::Val`] to whatever register holds it. Delegates to [`getSrcReg`]
23
//!     for register values; materializes immediates and symbols into `scratch`.
24
//!     Used for operands that can be consumed from any register.
25
//!
26
//!   loadVal(rd, val) -> Reg
27
//!     Force an [`il::Val`] into a specific register `rd`. Built on [`resolveVal`] + [`emitMv`].
28
//!     Used when the instruction requires the value in `rd` (e.g. `sub rd, rd, rs2`).
29
30
use std::mem;
31
use std::lang::il;
32
use std::lang::gen;
33
use std::lang::gen::regalloc;
34
use std::lang::gen::labels;
35
use std::lang::gen::data;
36
37
use super::encode;
38
use super::emit;
39
40
///////////////
41
// Constants //
42
///////////////
43
44
/// Shift amount for byte sign/zero extension.
45
const SHIFT_W8: i32 = 64 - 8;
46
/// Shift amount for halfword sign/zero extension.
47
const SHIFT_W16: i32 = 64 - 16;
48
/// Shift amount for word sign/zero extension.
49
const SHIFT_W32: i32 = 64 - 32;
50
/// Mask for extracting byte value.
51
const MASK_W8: i32 = 0xFF;
52
/// Maximum number of block arguments supported.
53
const MAX_BLOCK_ARGS: u32 = 16;
54
55
/// Signed integer range limits.
56
const I8_MIN: i64 = -128;
57
const I8_MAX: i64 = 127;
58
const I16_MIN: i64 = -32768;
59
const I16_MAX: i64 = 32767;
60
const I32_MIN: i64 = -2147483648;
61
const I32_MAX: i64 = 2147483647;
62
63
/// Unsigned integer range limits.
64
const U8_MAX: i64 = 255;
65
const U16_MAX: i64 = 65535;
66
const U32_MAX: i64 = 4294967295;
67
68
/// Binary operation.
69
union BinOp { Add, And, Or, Xor }
70
/// Shift operation.
71
union ShiftOp { Sll, Srl, Sra }
72
/// Compare operation.
73
union CmpOp { Slt, Ult }
74
75
/// Selector errors.
76
pub union SelectorError {
77
    Internal,
78
}
79
80
/// A pending spill store to be flushed after instruction selection.
81
record PendingSpill {
82
    /// The SSA register that was spilled.
83
    ssa: il::Reg,
84
    /// The physical register holding the value to store.
85
    rd: gen::Reg,
86
}
87
88
////////////////////
89
// Selector State //
90
////////////////////
91
92
/// Instruction selector state.
93
pub record Selector {
94
    /// Emitter for outputting instructions.
95
    e: *mut emit::Emitter,
96
    /// Register allocation result.
97
    ralloc: *regalloc::AllocResult,
98
    /// Hash-indexed data symbol map.
99
    dataSymMap: *data::DataSymMap,
100
    /// Total stack frame size.
101
    frameSize: i32,
102
    /// Running offset into the reserve region of the frame.
103
    /// Tracks current position within the pre-allocated reserve slots.
104
    reserveOffset: i32,
105
    /// Pending spill store, auto-committed after each instruction.
106
    pendingSpill: ?PendingSpill,
107
    /// Next synthetic block index for skip-branch targets.
108
    nextSynthBlock: u32,
109
    /// Whether dynamic allocations exist.
110
    isDynamic: bool,
111
}
112
113
/////////////////////////
114
// Register Allocation //
115
/////////////////////////
116
117
/// Get the physical register for an already-allocated SSA register.
118
fn getReg(s: *Selector, ssa: il::Reg) -> gen::Reg {
119
    let phys = s.ralloc.assignments[ssa.n] else {
120
        panic "getReg: spilled register has no physical assignment";
121
    };
122
    return phys;
123
}
124
125
/// Compute the offset for a spill slot.
126
/// When using FP (dynamic): offset from FP = `slot - totalSize`.
127
/// When using SP: offset from SP = `slot`.
128
fn spillOffset(s: *Selector, slot: i32) -> i32 {
129
    if s.isDynamic {
130
        return slot - s.frameSize;
131
    }
132
    return slot;
133
}
134
135
/// Get the base register for spill slot addressing (FP or SP).
136
fn spillBase(s: *Selector) -> gen::Reg {
137
    if s.isDynamic {
138
        return super::FP;
139
    }
140
    return super::SP;
141
}
142
143
/// Get the destination register for an SSA register.
144
/// If the register is spilled, records a pending spill and returns the scratch
145
/// register. The pending spill is auto-committed by [`selectBlock`] after each
146
/// instruction. If not spilled, returns the physical register.
147
fn getDstReg(s: *mut Selector, ssa: il::Reg, scratch: gen::Reg) -> gen::Reg {
148
    if let _ = regalloc::spill::spillSlot(&s.ralloc.spill, ssa) {
149
        s.pendingSpill = PendingSpill { ssa, rd: scratch };
150
        return scratch;
151
    }
152
    return getReg(s, ssa);
153
}
154
155
/// Get the source register for an SSA register.
156
/// If the register is spilled, loads the value from the spill slot into the
157
/// scratch register and returns it. Otherwise returns the physical register.
158
fn getSrcReg(s: *mut Selector, ssa: il::Reg, scratch: gen::Reg) -> gen::Reg {
159
    if let slot = regalloc::spill::spillSlot(&s.ralloc.spill, ssa) {
160
        emit::emitLd(s.e, scratch, spillBase(s), spillOffset(s, slot));
161
        return scratch;
162
    }
163
    return getReg(s, ssa);
164
}
165
166
/// Look up symbol address in data map.
167
fn lookupDataSym(s: *Selector, name: *[u8]) -> u32 throws (SelectorError) {
168
    let addr = data::lookupAddr(s.dataSymMap, name) else {
169
        throw SelectorError::Internal;
170
    };
171
    return addr;
172
}
173
174
/// Resolve an IL value to the physical register holding it.
175
/// For non-spilled register values, returns the physical register directly.
176
/// For immediates, symbols, and spilled registers, materializes into `scratch`.
177
fn resolveVal(s: *mut Selector, scratch: gen::Reg, val: il::Val) -> gen::Reg {
178
    match val {
179
        case il::Val::Reg(r) => {
180
            return getSrcReg(s, r, scratch);
181
        },
182
        case il::Val::Imm(imm) => {
183
            if imm == 0 {
184
                return super::ZERO;
185
            }
186
            emit::loadImm(s.e, scratch, imm);
187
            return scratch;
188
        },
189
        case il::Val::DataSym(name) => {
190
            let addr = try lookupDataSym(s, name) catch {
191
                panic "resolveVal: data symbol not found";
192
            };
193
            emit::loadImm(s.e, scratch, addr as i64);
194
            return scratch;
195
        },
196
        case il::Val::FnAddr(name) => {
197
            emit::recordAddrLoad(s.e, name, scratch);
198
            return scratch;
199
        },
200
        case il::Val::Undef => {
201
            return scratch;
202
        }
203
    }
204
}
205
206
/// Load an IL value into a specific physical register.
207
/// Like [`resolveVal`], but ensures the value ends up in `rd`.
208
fn loadVal(s: *mut Selector, rd: gen::Reg, val: il::Val) -> gen::Reg {
209
    let rs = resolveVal(s, rd, val);
210
    emitMv(s, rd, rs);
211
    return rd;
212
}
213
214
/// Emit a move instruction if source and destination differ.
215
fn emitMv(s: *mut Selector, rd: gen::Reg, rs: gen::Reg) {
216
    if *rd != *rs {
217
        emit::emit(s.e, encode::mv(rd, rs));
218
    }
219
}
220
221
/// Emit zero-extension from a sub-word type to the full register width.
222
fn emitZext(e: *mut emit::Emitter, rd: gen::Reg, rs: gen::Reg, typ: il::Type) {
223
    match typ {
224
        case il::Type::W8 => emit::emit(e, encode::andi(rd, rs, MASK_W8)),
225
        case il::Type::W16 => {
226
            emit::emit(e, encode::slli(rd, rs, SHIFT_W16));
227
            emit::emit(e, encode::srli(rd, rd, SHIFT_W16));
228
        },
229
        case il::Type::W32 => {
230
            emit::emit(e, encode::slli(rd, rs, SHIFT_W32));
231
            emit::emit(e, encode::srli(rd, rd, SHIFT_W32));
232
        },
233
        case il::Type::W64,
234
             il::Type::Ptr => {}
235
    }
236
}
237
238
/// Emit sign-extension from a sub-word type to the full register width.
239
fn emitSext(e: *mut emit::Emitter, rd: gen::Reg, rs: gen::Reg, typ: il::Type) {
240
    match typ {
241
        case il::Type::W8 => {
242
            emit::emit(e, encode::slli(rd, rs, SHIFT_W8));
243
            emit::emit(e, encode::srai(rd, rd, SHIFT_W8));
244
        },
245
        case il::Type::W16 => {
246
            emit::emit(e, encode::slli(rd, rs, SHIFT_W16));
247
            emit::emit(e, encode::srai(rd, rd, SHIFT_W16));
248
        },
249
        case il::Type::W32 => {
250
            emit::emit(e, encode::addiw(rd, rs, 0));
251
        },
252
        case il::Type::W64,
253
             il::Type::Ptr => {}
254
    }
255
}
256
257
/// Resolve a value, trap if zero (unless known non-zero), and return the register.
258
fn resolveAndTrapIfZero(s: *mut Selector, b: il::Val) -> gen::Reg {
259
    let rs2 = resolveVal(s, super::SCRATCH2, b);
260
    let mut knownNonZero = false;
261
    if let case il::Val::Imm(imm) = b {
262
        knownNonZero = imm != 0;
263
    }
264
    if not knownNonZero {
265
        emit::emit(s.e, encode::bne(rs2, super::ZERO, super::INSTR_SIZE * 2));
266
        emit::emit(s.e, encode::ebreak());
267
    }
268
    return rs2;
269
}
270
271
////////////////////////
272
// Instruction Select //
273
////////////////////////
274
275
/// Pre-scan result for reserve analysis.
276
record ReserveInfo {
277
    /// Total size needed for constant-sized reserves.
278
    size: i32,
279
    /// Whether any dynamic-sized reserves exist.
280
    isDynamic: bool,
281
}
282
283
/// Pre-scan all blocks for constant-sized reserve instructions.
284
/// Returns the total size needed for all static reserves, respecting alignment.
285
fn computeReserveInfo(func: *il::Fn) -> ReserveInfo {
286
    let mut offset: i32 = 0;
287
    let mut isDynamic = false;
288
289
    for b in 0..func.blocks.len {
290
        let block = &func.blocks[b];
291
        for instr in block.instrs {
292
            match instr {
293
                case il::Instr::Reserve { size, alignment, .. } => {
294
                    if let case il::Val::Imm(sz) = size {
295
                        offset = mem::alignUpI32(offset, alignment as i32);
296
                        offset += sz as i32;
297
                    } else {
298
                        isDynamic = true;
299
                    }
300
                },
301
                else => {},
302
            }
303
        }
304
    }
305
    return ReserveInfo { size: offset, isDynamic };
306
}
307
308
/// Select instructions for a function.
309
pub fn selectFn(
310
    e: *mut emit::Emitter,
311
    dataSymMap: *data::DataSymMap,
312
    ralloc: *regalloc::AllocResult,
313
    func: *il::Fn
314
) {
315
    // Reset block offsets for this function.
316
    labels::resetBlocks(&mut e.labels);
317
    // Pre-scan for constant-sized reserves to promote to fixed frame slots.
318
    let reserveInfo = computeReserveInfo(func);
319
    let isLeaf = func.isLeaf;
320
    // Compute frame layout from spill slots, reserve slots, and used callee-saved registers.
321
    let frame = emit::computeFrame(
322
        ralloc.spill.frameSize + reserveInfo.size,
323
        ralloc.usedCalleeSaved,
324
        func.blocks.len,
325
        isLeaf,
326
        reserveInfo.isDynamic
327
    );
328
    // Synthetic block indices start after real blocks and the epilogue block.
329
    let mut s = Selector {
330
        e, ralloc, dataSymMap, frameSize: frame.totalSize,
331
        reserveOffset: 0, pendingSpill: nil,
332
        nextSynthBlock: func.blocks.len + 1,
333
        isDynamic: frame.isDynamic,
334
    };
335
    // Record function name for printing.
336
    emit::recordFunc(s.e, func.name);
337
    // Record function code offset for call patching.
338
    emit::recordFuncOffset(s.e, func.name);
339
    // Emit prologue.
340
    emit::emitPrologue(s.e, &frame);
341
342
    // Move function params from arg registers to assigned registers.
343
    // Cross-call params may have been assigned to callee-saved registers
344
    // instead of their natural arg registers. Spilled params are stored
345
    // directly to their spill slots.
346
    for funcParam, i in func.params {
347
        if i < super::ARG_REGS.len {
348
            let param = funcParam.value;
349
            let argReg = super::ARG_REGS[i];
350
351
            if let slot = regalloc::spill::spillSlot(&ralloc.spill, param) {
352
                // Spilled parameter: store arg register to spill slot.
353
                emit::emitSd(s.e, argReg, spillBase(&s), spillOffset(&s, slot));
354
            } else if let assigned = ralloc.assignments[param.n] {
355
                emitMv(&mut s, assigned, argReg);
356
            }
357
        }
358
    }
359
360
    // Emit each block.
361
    for i in 0..func.blocks.len {
362
        selectBlock(&mut s, i, &func.blocks[i], &frame, func);
363
    }
364
    // Emit epilogue.
365
    emit::emitEpilogue(s.e, &frame);
366
    // Patch local branches now that all blocks are emitted.
367
    emit::patchLocalBranches(s.e);
368
}
369
370
/// Select instructions for a block.
371
fn selectBlock(s: *mut Selector, blockIdx: u32, block: *il::Block, frame: *emit::Frame, func: *il::Fn) {
372
    // Record block address for branch patching.
373
    emit::recordBlock(s.e, blockIdx);
374
375
    // Block parameters are handled at jump sites (in `Jmp`/`Br`).
376
    // By the time we enter the block, the arguments have already been
377
    // moved to the parameter registers by the predecessor's terminator.
378
379
    // Process each instruction, auto-committing any pending spill after each.
380
    let hasLocs = block.locs.len > 0;
381
    for instr, i in block.instrs {
382
        // Record debug location before emitting machine instructions.
383
        if hasLocs {
384
            emit::recordSrcLoc(s.e, block.locs[i]);
385
        }
386
        s.pendingSpill = nil;
387
        selectInstr(s, blockIdx, instr, frame, func);
388
389
        // Flush the pending spill store, if any.
390
        if let p = s.pendingSpill {
391
            if let slot = regalloc::spill::spillSlot(&s.ralloc.spill, p.ssa) {
392
                emit::emitSd(s.e, p.rd, spillBase(s), spillOffset(s, slot));
393
            }
394
            s.pendingSpill = nil;
395
        }
396
    }
397
}
398
399
/// Select instructions for a single IL instruction.
400
fn selectInstr(s: *mut Selector, blockIdx: u32, instr: il::Instr, frame: *emit::Frame, func: *il::Fn) {
401
    match instr {
402
        case il::Instr::BinOp { op, typ, dst, a, b } => {
403
            let rd = getDstReg(s, dst, super::SCRATCH1);
404
            let rs1 = resolveVal(s, super::SCRATCH1, a);
405
            selectAluBinOp(s, op, typ, rd, rs1, b);
406
        },
407
        case il::Instr::UnOp { op, typ, dst, a } => {
408
            let rd = getDstReg(s, dst, super::SCRATCH1);
409
            let rs = resolveVal(s, super::SCRATCH1, a);
410
            selectAluUnOp(s, op, typ, rd, rs);
411
        },
412
        case il::Instr::Load { typ, dst, src, offset } => {
413
            let rd = getDstReg(s, dst, super::SCRATCH1);
414
            let base = getSrcReg(s, src, super::SCRATCH2);
415
            emit::emitLoad(s.e, rd, base, offset, typ);
416
        },
417
        case il::Instr::Sload { typ, dst, src, offset } => {
418
            let rd = getDstReg(s, dst, super::SCRATCH1);
419
            let base = getSrcReg(s, src, super::SCRATCH2);
420
            emit::emitSload(s.e, rd, base, offset, typ);
421
        },
422
        case il::Instr::Store { typ, src, dst, offset } => {
423
            let base = getSrcReg(s, dst, super::SCRATCH2);
424
            let rs = resolveVal(s, super::SCRATCH1, src);
425
            emit::emitStore(s.e, rs, base, offset, typ);
426
        },
427
        case il::Instr::Copy { dst, val } => {
428
            let rd = getDstReg(s, dst, super::SCRATCH1);
429
            let rs = resolveVal(s, super::SCRATCH1, val);
430
            emitMv(s, rd, rs);
431
        },
432
        case il::Instr::Reserve { dst, size, alignment } => {
433
            match size {
434
                case il::Val::Imm(sz) => {
435
                    // Constant-sized reserve: use pre-allocated frame slot.
436
                    let rd = getDstReg(s, dst, super::SCRATCH1);
437
                    let aligned: i32 = mem::alignUpI32(s.reserveOffset, alignment as i32);
438
                    let base = spillBase(s);
439
                    let offset = s.ralloc.spill.frameSize + aligned
440
                        - (s.frameSize if s.isDynamic else 0);
441
442
                    emit::emitAddImm(s.e, rd, base, offset);
443
                    s.reserveOffset = aligned + (sz as i32);
444
                },
445
                case il::Val::Reg(r) => {
446
                    // Dynamic-sized reserve: runtime SP adjustment.
447
                    let rd = getDstReg(s, dst, super::SCRATCH1);
448
                    let rs = getSrcReg(s, r, super::SCRATCH2);
449
450
                    emit::emit(s.e, encode::sub(super::SP, super::SP, rs));
451
452
                    if alignment > 1 {
453
                        let mask = 0 - alignment as i32;
454
                        assert encode::isSmallImm(mask);
455
456
                        emit::emit(s.e, encode::andi(super::SP, super::SP, mask));
457
                    }
458
                    emit::emit(s.e, encode::mv(rd, super::SP));
459
                },
460
                else =>
461
                    panic "selectInstr: invalid reserve operand",
462
            }
463
        },
464
        case il::Instr::Blit { dst, src, size } => {
465
            let case il::Val::Imm(staticSize) = size
466
                else panic "selectInstr: blit requires immediate size";
467
468
            let bothSpilled = regalloc::spill::isSpilled(&s.ralloc.spill, dst)
469
                and regalloc::spill::isSpilled(&s.ralloc.spill, src);
470
471
            // When both are spilled, offsets must fit 12-bit immediates
472
            // since we can't advance base registers (they live in spill
473
            // slots, not real registers we can mutate).
474
            assert not (bothSpilled and staticSize as i32 > super::MAX_IMM), "selectInstr: blit both-spilled with large size";
475
476
            // Resolve dst/src base registers.
477
            let mut rdst = super::SCRATCH2;
478
            let mut rsrc = super::SCRATCH1;
479
            let mut srcReload: ?i32 = nil;
480
481
            if bothSpilled {
482
                let dstSlot = regalloc::spill::spillSlot(&s.ralloc.spill, dst) else {
483
                    panic "selectInstr: blit dst not spilled";
484
                };
485
                let srcSlot = regalloc::spill::spillSlot(&s.ralloc.spill, src) else {
486
                    panic "selectInstr: blit src not spilled";
487
                };
488
                emit::emitLd(s.e, super::SCRATCH2, spillBase(s), spillOffset(s, dstSlot));
489
                srcReload = spillOffset(s, srcSlot);
490
            } else {
491
                rdst = getSrcReg(s, dst, super::SCRATCH2);
492
                rsrc = getSrcReg(s, src, super::SCRATCH2);
493
            }
494
            let mut offset: i32 = 0;
495
            let mut remaining = staticSize as i32;
496
497
            // For large blits where both pointers are in real registers,
498
            // use an inline loop instead of unrolled LD/SD pairs.
499
            let dwordBytes = remaining & ~(super::DWORD_SIZE - 1);
500
            let canLoop = not bothSpilled
501
                and *rsrc != *super::SCRATCH1 and *rsrc != *super::SCRATCH2
502
                and *rdst != *super::SCRATCH1 and *rdst != *super::SCRATCH2;
503
504
            if canLoop and dwordBytes >= super::BLIT_LOOP_THRESHOLD {
505
                emit::emitAddImm(s.e, super::SCRATCH1, rsrc, dwordBytes);
506
507
                let loopStart = s.e.codeLen;
508
509
                emit::emitLd(s.e, super::SCRATCH2, rsrc, 0);
510
                emit::emitSd(s.e, super::SCRATCH2, rdst, 0);
511
                emit::emit(s.e, encode::addi(rsrc, rsrc, super::DWORD_SIZE));
512
513
                if *rdst != *rsrc {
514
                    emit::emit(s.e, encode::addi(rdst, rdst, super::DWORD_SIZE));
515
                }
516
                let brOff = (loopStart as i32 - s.e.codeLen as i32) * super::INSTR_SIZE;
517
518
                emit::emit(s.e, encode::bne(rsrc, super::SCRATCH1, brOff));
519
                remaining -= dwordBytes;
520
            }
521
522
            // Copy remaining: 8 bytes, then 4 bytes, then 1 byte at a time.
523
            // Before each load/store pair, check whether the offset is
524
            // about to exceed the 12-bit signed immediate range. When
525
            // it does, advance the base registers by the accumulated
526
            // offset and reset to zero.
527
            while remaining >= super::DWORD_SIZE {
528
                if offset > super::MAX_IMM - super::DWORD_SIZE {
529
                    emit::emitAddImm(s.e, rsrc, rsrc, offset);
530
                    if *rdst != *rsrc {
531
                        emit::emitAddImm(s.e, rdst, rdst, offset);
532
                    }
533
                    offset = 0;
534
                }
535
                if let off = srcReload {
536
                    emit::emitLd(s.e, super::SCRATCH1, spillBase(s), off);
537
                    emit::emitLd(s.e, super::SCRATCH1, super::SCRATCH1, offset);
538
                } else {
539
                    emit::emitLd(s.e, super::SCRATCH1, rsrc, offset);
540
                }
541
                emit::emitSd(s.e, super::SCRATCH1, rdst, offset);
542
                offset += super::DWORD_SIZE;
543
                remaining -= super::DWORD_SIZE;
544
            }
545
            if remaining >= super::WORD_SIZE {
546
                if offset > super::MAX_IMM - super::WORD_SIZE {
547
                    emit::emitAddImm(s.e, rsrc, rsrc, offset);
548
                    if *rdst != *rsrc {
549
                        emit::emitAddImm(s.e, rdst, rdst, offset);
550
                    }
551
                    offset = 0;
552
                }
553
                if let off = srcReload {
554
                    emit::emitLd(s.e, super::SCRATCH1, spillBase(s), off);
555
                    emit::emitLw(s.e, super::SCRATCH1, super::SCRATCH1, offset);
556
                } else {
557
                    emit::emitLw(s.e, super::SCRATCH1, rsrc, offset);
558
                }
559
                emit::emitSw(s.e, super::SCRATCH1, rdst, offset);
560
                offset += super::WORD_SIZE;
561
                remaining -= super::WORD_SIZE;
562
            }
563
            while remaining > 0 {
564
                if offset > super::MAX_IMM - 1 {
565
                    emit::emitAddImm(s.e, rsrc, rsrc, offset);
566
                    if *rdst != *rsrc {
567
                        emit::emitAddImm(s.e, rdst, rdst, offset);
568
                    }
569
                    offset = 0;
570
                }
571
                if let off = srcReload {
572
                    emit::emitLd(s.e, super::SCRATCH1, spillBase(s), off);
573
                    emit::emitLb(s.e, super::SCRATCH1, super::SCRATCH1, offset);
574
                } else {
575
                    emit::emitLb(s.e, super::SCRATCH1, rsrc, offset);
576
                }
577
                emit::emitSb(s.e, super::SCRATCH1, rdst, offset);
578
                offset += 1;
579
                remaining -= 1;
580
            }
581
            // Restore base registers if they were advanced (never happens
582
            // in the both-spilled case since size <= MAX_IMM).
583
            if not bothSpilled {
584
                let advanced = staticSize as i32 - offset;
585
                if advanced != 0 {
586
                    emit::emitAddImm(s.e, rsrc, rsrc, 0 - advanced);
587
                    if *rdst != *rsrc {
588
                        emit::emitAddImm(s.e, rdst, rdst, 0 - advanced);
589
                    }
590
                }
591
            }
592
        },
593
        case il::Instr::Zext { typ, dst, val } => {
594
            let rd = getDstReg(s, dst, super::SCRATCH1);
595
            let rs = resolveVal(s, super::SCRATCH1, val);
596
            emitZext(s.e, rd, rs, typ);
597
        },
598
        case il::Instr::Sext { typ, dst, val } => {
599
            let rd = getDstReg(s, dst, super::SCRATCH1);
600
            let rs = resolveVal(s, super::SCRATCH1, val);
601
            emitSext(s.e, rd, rs, typ);
602
        },
603
        // MakePtr is a no-op on RV64: both are 64-bit register values.
604
        case il::Instr::MakePtr { dst, val } => {
605
            let rd = getDstReg(s, dst, super::SCRATCH1);
606
            let rs = resolveVal(s, super::SCRATCH1, val);
607
            emitMv(s, rd, rs);
608
        },
609
        case il::Instr::Ret { val } => {
610
            if let v = val {
611
                let rs = resolveVal(s, super::SCRATCH1, v);
612
                emitMv(s, super::A0, rs);
613
            }
614
            // Skip the jump to epilogue if this RET is in the last block,
615
            // since the epilogue immediately follows.
616
            if frame.totalSize != 0 and blockIdx + 1 == frame.epilogueBlock {
617
                // Epilogue is the next block; fallthrough is sufficient.
618
            } else {
619
                emit::emitReturn(s.e, frame);
620
            }
621
        },
622
        case il::Instr::Jmp { target, args } => {
623
            // Move arguments to target block's parameter registers.
624
            emitBlockArgs(s, func, target, args);
625
            // Skip branch if target is the next block (fallthrough).
626
            if target != blockIdx + 1 {
627
                emit::recordBranch(s.e, target, emit::BranchKind::Jump);
628
            }
629
        },
630
        case il::Instr::Br { op, typ, a, b, thenTarget, thenArgs, elseTarget, elseArgs } => {
631
            // Use zero register directly for immediate `0` operands.
632
            let aIsZero = isZeroImm(a);
633
            let bIsZero = isZeroImm(b);
634
635
            let rs1 = super::ZERO if aIsZero else resolveVal(s, super::SCRATCH1, a);
636
            let rs2 = super::ZERO if bIsZero else resolveVal(s, super::SCRATCH2, b);
637
638
            // Normalize sub-word operands so that both registers have the same
639
            // canonical representation. Without this, eg. `-1 : i8 ` loaded as
640
            // `0xFFFFFFFFFFFFFFFF` and `255 : i8` loaded as `0xFF` would compare
641
            // unequal even though they are the same 8-bit pattern.
642
            //
643
            // For SLT: sign-extension needed (signed comparison).
644
            // For ULT: zero-extension needed (unsigned magnitude comparison).
645
            // For EQ/NE with W32: sign-extension is cheaper.
646
            // For EQ/NE with W8/W16: keep zero-extension.
647
            // Skip extension for zero register.
648
            // Determine extension mode: sign-extend for W32 or SLT,
649
            // zero-extend otherwise.
650
            let mut useSext: bool = undefined;
651
            if let case il::CmpOp::Slt = op {
652
                useSext = true;
653
            } else {
654
                useSext = typ == il::Type::W32;
655
            }
656
            if useSext {
657
                if not aIsZero and not isExtendedImm(a, typ, true) {
658
                    emitSext(s.e, rs1, rs1, typ);
659
                }
660
                if not bIsZero and not isExtendedImm(b, typ, true) {
661
                    emitSext(s.e, rs2, rs2, typ);
662
                }
663
            } else {
664
                if not aIsZero and not isExtendedImm(a, typ, false) {
665
                    emitZext(s.e, rs1, rs1, typ);
666
                }
667
                if not bIsZero and not isExtendedImm(b, typ, false) {
668
                    emitZext(s.e, rs2, rs2, typ);
669
                }
670
            }
671
            // Block-argument moves must only execute on the taken path.
672
            // When `thenArgs` is non-empty, invert the branch so that the
673
            // then-moves land on the fall-through (taken) side.
674
            //
675
            // When one target is the next block in layout order, we can
676
            // eliminate the trailing unconditional jump by arranging the
677
            // conditional branch to skip to the *other* target and letting
678
            // execution fall through.
679
            if thenArgs.len > 0 and elseArgs.len > 0 {
680
                panic "selectInstr: both `then` and `else` have block arguments";
681
            } else if thenArgs.len > 0 {
682
                emit::recordBranch(s.e, elseTarget, emit::BranchKind::InvertedCond { op, rs1, rs2 });
683
                emitBlockArgs(s, func, thenTarget, thenArgs);
684
                // Skip trailing jump if then is the next block (fallthrough).
685
                if thenTarget != blockIdx + 1 {
686
                    emit::recordBranch(s.e, thenTarget, emit::BranchKind::Jump);
687
                }
688
            } else if thenTarget == blockIdx + 1 and elseArgs.len == 0 {
689
                // Then is the next block and no else args: invert the
690
                // condition to branch to else and fall through to then.
691
                emit::recordBranch(s.e, elseTarget, emit::BranchKind::InvertedCond { op, rs1, rs2 });
692
            } else {
693
                emit::recordBranch(s.e, thenTarget, emit::BranchKind::Cond { op, rs1, rs2 });
694
                emitBlockArgs(s, func, elseTarget, elseArgs);
695
                // Skip trailing jump if else is the next block (fallthrough).
696
                if elseTarget != blockIdx + 1 {
697
                    emit::recordBranch(s.e, elseTarget, emit::BranchKind::Jump);
698
                }
699
            }
700
        },
701
        case il::Instr::Switch { val, defaultTarget, defaultArgs, cases } => {
702
            let rs1 = resolveVal(s, super::SCRATCH1, val);
703
            // When a case has block args, invert the branch to skip past
704
            // the arg moves.
705
            for c in cases {
706
                emit::loadImm(s.e, super::SCRATCH2, c.value);
707
708
                if c.args.len > 0 {
709
                    let skip = s.nextSynthBlock;
710
                    s.nextSynthBlock = skip + 1;
711
712
                    emit::recordBranch(s.e, skip, emit::BranchKind::InvertedCond {
713
                        op: il::CmpOp::Eq, rs1, rs2: super::SCRATCH2,
714
                    });
715
                    emitBlockArgs(s, func, c.target, c.args);
716
                    emit::recordBranch(s.e, c.target, emit::BranchKind::Jump);
717
                    emit::recordBlock(s.e, skip);
718
                } else {
719
                    emit::recordBranch(s.e, c.target, emit::BranchKind::Cond {
720
                        op: il::CmpOp::Eq, rs1, rs2: super::SCRATCH2,
721
                    });
722
                }
723
            }
724
            // Fall through to default.
725
            emitBlockArgs(s, func, defaultTarget, defaultArgs);
726
            emit::recordBranch(s.e, defaultTarget, emit::BranchKind::Jump);
727
        },
728
        case il::Instr::Unreachable => {
729
            emit::emit(s.e, encode::ebreak());
730
        },
731
        case il::Instr::Call { retTy, dst, func, args } => {
732
            // For indirect calls, save target to scratch register before arg
733
            // setup can clobber it.
734
            if let case il::Val::Reg(r) = func {
735
                let target = getSrcReg(s, r, super::SCRATCH2);
736
                emitMv(s, super::SCRATCH2, target);
737
            }
738
            // Move arguments to A0-A7 using parallel move resolution.
739
            assert args.len <= super::ARG_REGS.len, "selectInstr: too many call arguments";
740
            emitParallelMoves(s, &super::ARG_REGS[..], args);
741
742
            // Emit call.
743
            match func {
744
                case il::Val::FnAddr(name) => {
745
                    emit::recordCall(s.e, name);
746
                },
747
                case il::Val::Reg(_) => {
748
                    emit::emit(s.e, encode::jalr(super::RA, super::SCRATCH2, 0));
749
                },
750
                else => {
751
                    panic "selectInstr: invalid call target";
752
                }
753
            }
754
            // Move result from A0.
755
            if let d = dst {
756
                let rd = getDstReg(s, d, super::SCRATCH1);
757
                emitMv(s, rd, super::A0);
758
            }
759
        },
760
        case il::Instr::Ecall { dst, num, a0, a1, a2, a3 } => {
761
            // Move arguments using parallel move.
762
            // TODO: Can't use slice literals here because the lowerer doesn't
763
            // support const-evaluating struct/union values in them.
764
            let ecallDsts: [gen::Reg; 5] = [super::A7, super::A0, super::A1, super::A2, super::A3];
765
            let ecallArgs: [il::Val; 5] = [num, a0, a1, a2, a3];
766
767
            emitParallelMoves(s, &ecallDsts[..], &ecallArgs[..]);
768
            emit::emit(s.e, encode::ecall());
769
770
            // Result in A0.
771
            let ecallRd = getDstReg(s, dst, super::SCRATCH1);
772
            emitMv(s, ecallRd, super::A0);
773
        },
774
        case il::Instr::Ebreak => {
775
            emit::emit(s.e, encode::ebreak());
776
        },
777
        case il::Instr::Elem { dst, base, idx, len, stride } => {
778
            // Bounds check: trap if idx >= len.
779
            let rsIdx = resolveVal(s, super::SCRATCH1, idx);
780
            let rsLen = resolveVal(s, super::SCRATCH2, len);
781
            emit::emit(s.e, encode::bltu(rsIdx, rsLen, 8));
782
            emit::emit(s.e, encode::ebreak());
783
784
            // Compute byte offset into SCRATCH1: idx * stride.
785
            let rs = resolveVal(s, super::SCRATCH1, idx);
786
            if stride == 1 {
787
                emitMv(s, super::SCRATCH1, rs);
788
            } else {
789
                emit::loadImm(s.e, super::SCRATCH2, stride as i64);
790
                emit::emit(s.e, encode::mul(super::SCRATCH1, rs, super::SCRATCH2));
791
            }
792
            // dst = base + SCRATCH1.
793
            let rsBase = getSrcReg(s, base, super::SCRATCH2);
794
            let rd = getDstReg(s, dst, super::SCRATCH2);
795
            emit::emit(s.e, encode::add(rd, rsBase, super::SCRATCH1));
796
        },
797
    }
798
}
799
800
/// Check if a value is an immediate that's already correctly extended.
801
/// `loadImm` produces the exact 64-bit value; this checks whether that value
802
/// already matches what sign/zero-extension to the given type would produce.
803
fn isExtendedImm(val: il::Val, typ: il::Type, signed: bool) -> bool {
804
    if let case il::Val::Imm(imm) = val {
805
        if signed {
806
            // Sign-extension truncates to the type width and sign-extends.
807
            // The 64-bit value is already correctly sign-extended if it
808
            // fits in the signed range of the target type.
809
            match typ {
810
                case il::Type::W8 => return imm >= I8_MIN and imm <= I8_MAX,
811
                case il::Type::W16 => return imm >= I16_MIN and imm <= I16_MAX,
812
                case il::Type::W32 => return imm >= I32_MIN and imm <= I32_MAX,
813
                case il::Type::W64,
814
                     il::Type::Ptr => return true,
815
            }
816
        } else {
817
            // Zero-extension: value must be non-negative and within unsigned range.
818
            match typ {
819
                case il::Type::W8 => return imm >= 0 and imm <= U8_MAX,
820
                case il::Type::W16 => return imm >= 0 and imm <= U16_MAX,
821
                case il::Type::W32 => return imm >= 0 and imm <= U32_MAX,
822
                case il::Type::W64,
823
                     il::Type::Ptr => return true,
824
            }
825
        }
826
    }
827
    return false;
828
}
829
830
/// Check if a value is an immediate zero.
831
fn isZeroImm(val: il::Val) -> bool {
832
    if let case il::Val::Imm(imm) = val {
833
        return imm == 0;
834
    }
835
    return false;
836
}
837
838
/// Select a binary ALU operation, dispatching to the appropriate
839
/// instruction pattern based on the operation kind and type.
840
fn selectAluBinOp(s: *mut Selector, op: il::BinOp, typ: il::Type, rd: gen::Reg, rs1: gen::Reg, b: il::Val) {
841
    match op {
842
        case il::BinOp::Add => {
843
            if typ == il::Type::W32 {
844
                // Inline W32 ADD with immediate optimization.
845
                if let case il::Val::Imm(imm) = b {
846
                    if encode::isSmallImm64(imm) {
847
                        emit::emit(s.e, encode::addiw(rd, rs1, imm as i32));
848
                        return;
849
                    }
850
                }
851
                let rs2 = resolveVal(s, super::SCRATCH2, b);
852
                emit::emit(s.e, encode::addw(rd, rs1, rs2));
853
            } else {
854
                selectBinOp(s, rd, rs1, b, BinOp::Add, super::SCRATCH2);
855
            }
856
        }
857
        case il::BinOp::Sub => {
858
            // Optimize subtraction by small immediate: use ADDI with negated value.
859
            if let case il::Val::Imm(imm) = b {
860
                let neg = -imm;
861
                if neg >= super::MIN_IMM as i64 and neg <= super::MAX_IMM as i64 {
862
                    emit::emit(s.e,
863
                        encode::addiw(rd, rs1, neg as i32)
864
                            if typ == il::Type::W32 else
865
                        encode::addi(rd, rs1, neg as i32));
866
                    return;
867
                }
868
            }
869
            let rs2 = resolveVal(s, super::SCRATCH2, b);
870
871
            emit::emit(s.e,
872
                encode::subw(rd, rs1, rs2)
873
                    if typ == il::Type::W32 else
874
                encode::sub(rd, rs1, rs2));
875
        }
876
        case il::BinOp::Mul => {
877
            // Strength-reduce multiplication by known constants.
878
            if let case il::Val::Imm(imm) = b {
879
                if imm == 0 {
880
                    emit::emit(s.e, encode::mv(rd, super::ZERO));
881
                    return;
882
                } else if imm == 1 {
883
                    emitMv(s, rd, rs1);
884
                    return;
885
                } else if imm == 2 {
886
                    emit::emit(s.e, encode::slli(rd, rs1, 1));
887
                    return;
888
                } else if imm == 4 {
889
                    emit::emit(s.e, encode::slli(rd, rs1, 2));
890
                    return;
891
                } else if imm == 8 {
892
                    emit::emit(s.e, encode::slli(rd, rs1, 3));
893
                    return;
894
                }
895
            }
896
            let rs2 = resolveVal(s, super::SCRATCH2, b);
897
            emit::emit(s.e,
898
                encode::mulw(rd, rs1, rs2)
899
                    if typ == il::Type::W32 else
900
                encode::mul(rd, rs1, rs2));
901
        }
902
        case il::BinOp::Sdiv => {
903
            let rs2 = resolveAndTrapIfZero(s, b);
904
            emit::emit(s.e,
905
                encode::divw(rd, rs1, rs2)
906
                    if typ == il::Type::W32 else
907
                encode::div(rd, rs1, rs2));
908
        }
909
        case il::BinOp::Udiv => {
910
            let rs2 = resolveAndTrapIfZero(s, b);
911
            emit::emit(s.e,
912
                encode::divuw(rd, rs1, rs2)
913
                    if typ == il::Type::W32 else
914
                encode::divu(rd, rs1, rs2));
915
        }
916
        case il::BinOp::Srem => {
917
            let rs2 = resolveAndTrapIfZero(s, b);
918
            emit::emit(s.e,
919
                encode::remw(rd, rs1, rs2)
920
                    if typ == il::Type::W32 else
921
                encode::rem(rd, rs1, rs2));
922
        }
923
        case il::BinOp::Urem => {
924
            let rs2 = resolveAndTrapIfZero(s, b);
925
            emit::emit(s.e,
926
                encode::remuw(rd, rs1, rs2)
927
                    if typ == il::Type::W32 else
928
                encode::remu(rd, rs1, rs2));
929
        }
930
        case il::BinOp::And =>
931
            selectBinOp(s, rd, rs1, b, BinOp::And, super::SCRATCH2),
932
        case il::BinOp::Or =>
933
            selectBinOp(s, rd, rs1, b, BinOp::Or, super::SCRATCH2),
934
        case il::BinOp::Xor =>
935
            selectBinOp(s, rd, rs1, b, BinOp::Xor, super::SCRATCH2),
936
        case il::BinOp::Shl =>
937
            selectShift(s, rd, rs1, b, ShiftOp::Sll, typ, super::SCRATCH2),
938
        case il::BinOp::Sshr =>
939
            selectShift(s, rd, rs1, b, ShiftOp::Sra, typ, super::SCRATCH2),
940
        case il::BinOp::Ushr =>
941
            selectShift(s, rd, rs1, b, ShiftOp::Srl, typ, super::SCRATCH2),
942
        case il::BinOp::Eq, il::BinOp::Ne => {
943
            let rs2 = resolveVal(s, super::SCRATCH2, b);
944
            // Canonicalize both operands to the declared width.
945
            if typ == il::Type::W32 {
946
                emitSext(s.e, rs1, rs1, typ);
947
                if not isExtendedImm(b, typ, true) {
948
                    emitSext(s.e, rs2, rs2, typ);
949
                }
950
            } else {
951
                emitZext(s.e, rs1, rs1, typ);
952
                if not isExtendedImm(b, typ, false) {
953
                    emitZext(s.e, rs2, rs2, typ);
954
                }
955
            }
956
            emit::emit(s.e, encode::xor(rd, rs1, rs2));
957
            if let case il::BinOp::Eq = op {
958
                emit::emit(s.e, encode::sltiu(rd, rd, 1));
959
            } else {
960
                emit::emit(s.e, encode::sltu(rd, super::ZERO, rd));
961
            }
962
        }
963
        case il::BinOp::Slt =>
964
            selectCmp(s, rd, rs1, b, CmpOp::Slt, super::SCRATCH2),
965
        case il::BinOp::Ult =>
966
            selectCmp(s, rd, rs1, b, CmpOp::Ult, super::SCRATCH2),
967
        case il::BinOp::Sge, il::BinOp::Uge => {
968
            let rs2 = resolveVal(s, super::SCRATCH2, b);
969
            if let case il::BinOp::Sge = op {
970
                emit::emit(s.e, encode::slt(rd, rs1, rs2));
971
            } else {
972
                emit::emit(s.e, encode::sltu(rd, rs1, rs2));
973
            }
974
            emit::emit(s.e, encode::xori(rd, rd, 1));
975
        }
976
    }
977
}
978
979
/// Select a unary ALU operation.
980
fn selectAluUnOp(s: *mut Selector, op: il::UnOp, typ: il::Type, rd: gen::Reg, rs: gen::Reg) {
981
    match op {
982
        case il::UnOp::Neg => {
983
            if typ == il::Type::W32 {
984
                emit::emit(s.e, encode::subw(rd, super::ZERO, rs));
985
            } else {
986
                emit::emit(s.e, encode::neg(rd, rs));
987
            }
988
        }
989
        case il::UnOp::Not =>
990
            emit::emit(s.e, encode::not_(rd, rs)),
991
    }
992
}
993
994
/// Select binary operation with immediate optimization.
995
fn selectBinOp(s: *mut Selector, rd: gen::Reg, rs1: gen::Reg, b: il::Val, op: BinOp, scratch: gen::Reg) {
996
    // Try immediate optimization first.
997
    if let case il::Val::Imm(imm) = b {
998
        if encode::isSmallImm64(imm) {
999
            let simm = imm as i32;
1000
            match op {
1001
                case BinOp::Add => emit::emit(s.e, encode::addi(rd, rs1, simm)),
1002
                case BinOp::And => emit::emit(s.e, encode::andi(rd, rs1, simm)),
1003
                case BinOp::Or  => emit::emit(s.e, encode::ori(rd, rs1, simm)),
1004
                case BinOp::Xor => emit::emit(s.e, encode::xori(rd, rs1, simm)),
1005
            }
1006
            return;
1007
        }
1008
    }
1009
    // Fallback: load into register.
1010
    let rs2 = resolveVal(s, scratch, b);
1011
    match op {
1012
        case BinOp::Add => emit::emit(s.e, encode::add(rd, rs1, rs2)),
1013
        case BinOp::And => emit::emit(s.e, encode::and_(rd, rs1, rs2)),
1014
        case BinOp::Or  => emit::emit(s.e, encode::or_(rd, rs1, rs2)),
1015
        case BinOp::Xor => emit::emit(s.e, encode::xor(rd, rs1, rs2)),
1016
    }
1017
}
1018
1019
/// Select shift operation with immediate optimization.
1020
/// For 32-bit operations, uses the `*w` variants that operate on the lower 32 bits
1021
/// and sign-extend the result.
1022
fn selectShift(s: *mut Selector, rd: gen::Reg, rs1: gen::Reg, b: il::Val, op: ShiftOp, typ: il::Type, scratch: gen::Reg) {
1023
    let isW32: bool = typ == il::Type::W32;
1024
1025
    // Try immediate optimization first.
1026
    if let case il::Val::Imm(shamt) = b {
1027
        // Keep immediate forms only for encodable shift amounts.
1028
        // Otherwise fall back to register shifts, which naturally mask the count.
1029
        if shamt >= 0 and ((isW32 and shamt < 32) or (not isW32 and shamt < 64)) {
1030
            let sa = shamt as i32;
1031
            if isW32 {
1032
                match op {
1033
                    case ShiftOp::Sll => emit::emit(s.e, encode::slliw(rd, rs1, sa)),
1034
                    case ShiftOp::Srl => emit::emit(s.e, encode::srliw(rd, rs1, sa)),
1035
                    case ShiftOp::Sra => emit::emit(s.e, encode::sraiw(rd, rs1, sa)),
1036
                }
1037
            } else {
1038
                match op {
1039
                    case ShiftOp::Sll => emit::emit(s.e, encode::slli(rd, rs1, sa)),
1040
                    case ShiftOp::Srl => emit::emit(s.e, encode::srli(rd, rs1, sa)),
1041
                    case ShiftOp::Sra => emit::emit(s.e, encode::srai(rd, rs1, sa)),
1042
                }
1043
            }
1044
            return;
1045
        }
1046
    }
1047
    // Fallback: load into register.
1048
    let rs2 = resolveVal(s, scratch, b);
1049
    if isW32 {
1050
        match op {
1051
            case ShiftOp::Sll => emit::emit(s.e, encode::sllw(rd, rs1, rs2)),
1052
            case ShiftOp::Srl => emit::emit(s.e, encode::srlw(rd, rs1, rs2)),
1053
            case ShiftOp::Sra => emit::emit(s.e, encode::sraw(rd, rs1, rs2)),
1054
        }
1055
    } else {
1056
        match op {
1057
            case ShiftOp::Sll => emit::emit(s.e, encode::sll(rd, rs1, rs2)),
1058
            case ShiftOp::Srl => emit::emit(s.e, encode::srl(rd, rs1, rs2)),
1059
            case ShiftOp::Sra => emit::emit(s.e, encode::sra(rd, rs1, rs2)),
1060
        }
1061
    }
1062
}
1063
1064
/// Resolve parallel moves from IL values to physical destination registers.
1065
///
1066
/// The parallel move problem arises when moving values between registers where
1067
/// there may be dependencies (e.g. moving A0 to A1 and A1 to A0 simultaneously).
1068
///
1069
/// This algorithm:
1070
/// 1. Identifies "ready" moves.
1071
/// 2. Executes ready moves.
1072
/// 3. Breaks cycles using scratch register.
1073
///
1074
/// Entries with `ZERO` destination are skipped, as they are handled by caller.
1075
fn emitParallelMoves(s: *mut Selector, dsts: *[gen::Reg], args: *[il::Val]) {
1076
    let n: u32 = args.len;
1077
    if n == 0 {
1078
        return;
1079
    }
1080
    assert n <= MAX_BLOCK_ARGS, "emitParallelMoves: too many arguments";
1081
    // Source registers for each arg.
1082
    let mut srcRegs: [gen::Reg; MAX_BLOCK_ARGS] = [super::ZERO; MAX_BLOCK_ARGS];
1083
    // If this is a register-to-register move.
1084
    let mut isRegMove: [bool; MAX_BLOCK_ARGS] = [false; MAX_BLOCK_ARGS];
1085
    // If this move still needs to be executed.
1086
    let mut pending: [bool; MAX_BLOCK_ARGS] = [false; MAX_BLOCK_ARGS];
1087
    // Number of pending moves.
1088
    let mut numPending: u32 = 0;
1089
1090
    for i in 0..n {
1091
        let dst = dsts[i];
1092
        if dst != super::ZERO { // Skip entries with no destination.
1093
            match args[i] {
1094
                case il::Val::Reg(r) => {
1095
                    if let _ = regalloc::spill::spillSlot(&s.ralloc.spill, r) {
1096
                        // Spilled value needs load, not a register move.
1097
                        pending[i] = true;
1098
                        numPending += 1;
1099
                    } else {
1100
                        let src = getReg(s, r);
1101
                        if src != dst {
1102
                            // Register-to-register move needed.
1103
                            srcRegs[i] = src;
1104
                            isRegMove[i] = true;
1105
                            pending[i] = true;
1106
                            numPending += 1;
1107
                        } else {
1108
                            // No move needed.
1109
                        }
1110
                    }
1111
                },
1112
                case il::Val::Imm(_), il::Val::DataSym(_), il::Val::FnAddr(_) => {
1113
                    pending[i] = true;
1114
                    numPending += 1;
1115
                },
1116
                case il::Val::Undef => {
1117
                    // Undefined values don't need any move.
1118
                }
1119
            }
1120
        } else {
1121
            // Nothing to do.
1122
        }
1123
    }
1124
1125
    // Execute parallel move algorithm.
1126
    while numPending > 0 {
1127
        let mut found = false;
1128
1129
        // Find a ready move: one whose destination is not a source of any
1130
        // pending register move.
1131
        for i in 0..n {
1132
            if pending[i] {
1133
                let dst = dsts[i];
1134
                let mut isReady = true;
1135
1136
                // Check if `dst` is used as source by any other pending register move.
1137
                for j in 0..n {
1138
                    if j != i and pending[j] and isRegMove[j] and srcRegs[j] == dst {
1139
                        isReady = false;
1140
                        break;
1141
                    }
1142
                }
1143
                if isReady {
1144
                    // Execute this move.
1145
                    if isRegMove[i] {
1146
                        emitMv(s, dst, srcRegs[i]);
1147
                    } else {
1148
                        // Load immediate, symbol, or spilled value.
1149
                        loadVal(s, dst, args[i]);
1150
                    }
1151
                    found = true;
1152
                    pending[i] = false;
1153
                    numPending -= 1;
1154
1155
                    break;
1156
                }
1157
            }
1158
        }
1159
1160
        if not found {
1161
            // No ready move, we have a cycle among register moves.
1162
            // Break it by saving one source to scratch.
1163
            for i in 0..n {
1164
                if pending[i] and isRegMove[i] {
1165
                    let src = srcRegs[i];
1166
                    // Save this source to scratch.
1167
                    emitMv(s, super::SCRATCH1, src);
1168
                    // Update all pending moves that use this source.
1169
                    for j in 0..n {
1170
                        if pending[j] and isRegMove[j] and srcRegs[j] == src {
1171
                            srcRegs[j] = super::SCRATCH1;
1172
                        }
1173
                    }
1174
                    break;
1175
                }
1176
            }
1177
        }
1178
    }
1179
}
1180
1181
/// Emit moves from block arguments to target block's parameter registers.
1182
///
1183
/// Handles spilled destinations directly, then delegates to [`emitParallelMoves`]
1184
/// for the remaining register-to-register parallel move resolution.
1185
fn emitBlockArgs(s: *mut Selector, func: *il::Fn, target: u32, args: *mut [il::Val]) {
1186
    if args.len == 0 {
1187
        return;
1188
    }
1189
    let block = &func.blocks[target];
1190
    assert args.len == block.params.len, "emitBlockArgs: argument/parameter count mismatch";
1191
    assert args.len <= MAX_BLOCK_ARGS, "emitBlockArgs: too many block arguments";
1192
1193
    // Destination registers for each arg.
1194
    // Zero means the destination is spilled or skipped.
1195
    let mut dsts: [gen::Reg; MAX_BLOCK_ARGS] = [super::ZERO; MAX_BLOCK_ARGS];
1196
1197
    for arg, i in args {
1198
        let param = block.params[i].value;
1199
1200
        // Spilled destinations: store directly to spill slot.
1201
        // These don't participate in the parallel move algorithm.
1202
        if let slot = regalloc::spill::spillSlot(&s.ralloc.spill, param) {
1203
            if let case il::Val::Undef = arg {
1204
                // Undefined values don't need any move.
1205
            } else {
1206
                let rs = resolveVal(s, super::SCRATCH1, arg);
1207
                emit::emitSd(s.e, rs, spillBase(s), spillOffset(s, slot));
1208
            }
1209
        } else {
1210
            dsts[i] = getReg(s, param);
1211
        }
1212
    }
1213
    emitParallelMoves(s, &dsts[..], args);
1214
}
1215
1216
/// Select comparison with immediate optimization.
1217
fn selectCmp(s: *mut Selector, rd: gen::Reg, rs1: gen::Reg, b: il::Val, op: CmpOp, scratch: gen::Reg) {
1218
    // Try immediate optimization first.
1219
    if let case il::Val::Imm(imm) = b {
1220
        if encode::isSmallImm64(imm) {
1221
            let simm = imm as i32;
1222
            match op {
1223
                case CmpOp::Slt => emit::emit(s.e, encode::slti(rd, rs1, simm)),
1224
                case CmpOp::Ult => emit::emit(s.e, encode::sltiu(rd, rs1, simm)),
1225
            }
1226
            return;
1227
        }
1228
    }
1229
    // Fallback: load into register.
1230
    let rs2 = resolveVal(s, scratch, b);
1231
    match op {
1232
        case CmpOp::Slt => emit::emit(s.e, encode::slt(rd, rs1, rs2)),
1233
        case CmpOp::Ult => emit::emit(s.e, encode::sltu(rd, rs1, rs2)),
1234
    }
1235
}