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