// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm // RISC-V register encoding: maps register names to their 5-bit numbers. // The Go assembler uses the standard RISC-V ABI naming. // riscvRegNum returns the 5-bit register number for a RISC-V register name. // Returns -1 if the register is not recognized. func riscvRegNum(name string) int { switch name { // Numbered integer registers. case "X0", "ZERO": return 0 case "X1", "RA", "LR": return 1 case "X2", "SP": return 2 case "X3", "GP": return 3 case "X4", "TP": return 4 case "X5", "T0": return 5 case "X6", "T1": return 6 case "X7", "T2": return 7 case "X8", "S0", "FP": return 8 case "X9", "S1": return 9 case "X10", "A0": return 10 case "X11", "A1": return 11 case "X12", "A2": return 12 case "X13", "A3": return 13 case "X14", "A4": return 14 case "X15", "A5": return 15 case "X16", "A6": return 16 case "X17", "A7": return 17 case "X18", "S2": return 18 case "X19", "S3": return 19 case "X20", "S4": return 20 case "X21", "S5": return 21 case "X22", "S6": return 22 case "X23", "S7": return 23 case "X24", "S8": return 24 case "X25", "S9": return 25 case "X26", "S10": return 26 case "X27", "S11": return 27 case "X28", "T3": return 28 case "X29", "T4": return 29 case "X30", "T5": return 30 case "X31", "T6", "TMP": return 31 // Floating-point registers (F0-F31). case "F0", "FT0": return 0 case "F1", "FT1": return 1 case "F2", "FT2": return 2 case "F3", "FT3": return 3 case "F4", "FT4": return 4 case "F5", "FT5": return 5 case "F6", "FT6": return 6 case "F7", "FT7": return 7 case "F8", "FS0": return 8 case "F9", "FS1": return 9 case "F10", "FA0": return 10 case "F11", "FA1": return 11 case "F12", "FA2": return 12 case "F13", "FA3": return 13 case "F14", "FA4": return 14 case "F15", "FA5": return 15 case "F16", "FA6": return 16 case "F17", "FA7": return 17 case "F18", "FS2": return 18 case "F19", "FS3": return 19 case "F20", "FS4": return 20 case "F21", "FS5": return 21 case "F22", "FS6": return 22 case "F23", "FS7": return 23 case "F24", "FS8": return 24 case "F25", "FS9": return 25 case "F26", "FS10": return 26 case "F27", "FS11": return 27 case "F28", "FT8": return 28 case "F29", "FT9": return 29 case "F30", "FT10": return 30 case "F31", "FT11": return 31 default: return -1 } } // RISC-V instruction encoding parameters. type riscvEnc struct { opcode uint32 // bits [6:0] funct3 uint32 // bits [14:12] funct7 uint32 // bits [31:25] } // riscvInstrTable maps RISC-V mnemonics to their encoding. var riscvInstrTable = map[string]riscvEnc{ // RV64I — R-type arithmetic/logic. "ADD": {0x33, 0x0, 0x00}, "SUB": {0x33, 0x0, 0x20}, "SLL": {0x33, 0x1, 0x00}, "SLT": {0x33, 0x2, 0x00}, "SLTU": {0x33, 0x3, 0x00}, "XOR": {0x33, 0x4, 0x00}, "SRL": {0x33, 0x5, 0x00}, "SRA": {0x33, 0x5, 0x20}, "OR": {0x33, 0x6, 0x00}, "AND": {0x33, 0x7, 0x00}, // RV64I — 32-bit variants (W suffix). "ADDW": {0x3B, 0x0, 0x00}, "SUBW": {0x3B, 0x0, 0x20}, "SLLW": {0x3B, 0x1, 0x00}, "SRLW": {0x3B, 0x5, 0x00}, "SRAW": {0x3B, 0x5, 0x20}, // RV64I — I-type shift-immediate (shamt in rs2 field). "SLLI": {0x13, 0x1, 0x00}, "SRLI": {0x13, 0x5, 0x00}, "SRAI": {0x13, 0x5, 0x20}, "SLLIW": {0x1B, 0x1, 0x00}, "SRLIW": {0x1B, 0x5, 0x00}, "SRAIW": {0x1B, 0x5, 0x20}, // RV64M — multiply/divide. "MUL": {0x33, 0x0, 0x01}, "MULH": {0x33, 0x1, 0x01}, "MULHSU": {0x33, 0x2, 0x01}, "MULHU": {0x33, 0x3, 0x01}, "DIV": {0x33, 0x4, 0x01}, "DIVU": {0x33, 0x5, 0x01}, "REM": {0x33, 0x6, 0x01}, "REMU": {0x33, 0x7, 0x01}, // RV64M — 32-bit variants. "MULW": {0x3B, 0x0, 0x01}, "DIVW": {0x3B, 0x4, 0x01}, "DIVUW": {0x3B, 0x5, 0x01}, "REMW": {0x3B, 0x6, 0x01}, "REMUW": {0x3B, 0x7, 0x01}, // RV64I — I-type arithmetic. "ADDI": {0x13, 0x0, 0x00}, "ADDIW": {0x1B, 0x0, 0x00}, "SLTI": {0x13, 0x2, 0x00}, "SLTIU": {0x13, 0x3, 0x00}, "XORI": {0x13, 0x4, 0x00}, "ORI": {0x13, 0x6, 0x00}, "ANDI": {0x13, 0x7, 0x00}, // Loads (I-type). "LB": {0x03, 0x0, 0x00}, "LH": {0x03, 0x1, 0x00}, "LW": {0x03, 0x2, 0x00}, "LD": {0x03, 0x3, 0x00}, "LBU": {0x03, 0x4, 0x00}, "LHU": {0x03, 0x5, 0x00}, "LWU": {0x03, 0x6, 0x00}, // Stores (S-type). "SB": {0x23, 0x0, 0x00}, "SH": {0x23, 0x1, 0x00}, "SW": {0x23, 0x2, 0x00}, "SD": {0x23, 0x3, 0x00}, // Branches (B-type). "BEQ": {0x63, 0x0, 0x00}, "BNE": {0x63, 0x1, 0x00}, "BLT": {0x63, 0x4, 0x00}, "BGE": {0x63, 0x5, 0x00}, "BLTU": {0x63, 0x6, 0x00}, "BGEU": {0x63, 0x7, 0x00}, // U-type. "LUI": {0x37, 0x0, 0x00}, "AUIPC": {0x17, 0x0, 0x00}, // System. "ECALL": {0x73, 0x0, 0x00}, "EBREAK": {0x73, 0x0, 0x00}, "FENCE": {0x0F, 0x0, 0x00}, // JALR — indirect jump/call (I-type). "JALR": {0x67, 0x0, 0x00}, // RV64A — atomics (AMO opcode 0x2F). // funct3: 0x2 = word, 0x3 = doubleword. funct5 in bits [31:27]. "AMOSWAPW": {0x2F, 0x2, 0x01 << 2}, "AMOSWAPD": {0x2F, 0x3, 0x01 << 2}, "AMOADDW": {0x2F, 0x2, 0x00 << 2}, "AMOADDD": {0x2F, 0x3, 0x00 << 2}, "AMOANDW": {0x2F, 0x2, 0x0C << 2}, "AMOANDD": {0x2F, 0x3, 0x0C << 2}, "AMOORW": {0x2F, 0x2, 0x06 << 2}, "AMOORD": {0x2F, 0x3, 0x06 << 2}, "AMOXORW": {0x2F, 0x2, 0x04 << 2}, "AMOXORD": {0x2F, 0x3, 0x04 << 2}, "AMOMAXW": {0x2F, 0x2, 0x14 << 2}, "AMOMAXD": {0x2F, 0x3, 0x14 << 2}, "AMOMINW": {0x2F, 0x2, 0x10 << 2}, "AMOMIND": {0x2F, 0x3, 0x10 << 2}, "AMOMAXUW": {0x2F, 0x2, 0x1C << 2}, "AMOMAXUD": {0x2F, 0x3, 0x1C << 2}, "AMOMINUW": {0x2F, 0x2, 0x18 << 2}, "AMOMINUD": {0x2F, 0x3, 0x18 << 2}, // RV64F/D — floating-point arithmetic. "FADDS": {0x53, 0x0, 0x00}, "FSUBS": {0x53, 0x0, 0x04}, "FMULS": {0x53, 0x0, 0x08}, "FDIVS": {0x53, 0x0, 0x0C}, "FADDD": {0x53, 0x0, 0x01}, "FSUBD": {0x53, 0x0, 0x05}, "FMULD": {0x53, 0x0, 0x09}, "FDIVD": {0x53, 0x0, 0x0D}, "FSQRTS": {0x53, 0x0, 0x2C}, "FSQRTD": {0x53, 0x0, 0x2D}, // FP loads/stores. "FLW": {0x07, 0x2, 0x00}, "FLD": {0x07, 0x3, 0x00}, "FSW": {0x27, 0x2, 0x00}, "FSD": {0x27, 0x3, 0x00}, // FP min/max. "FMINS": {0x53, 0x0, 0x14}, "FMAXS": {0x53, 0x1, 0x14}, "FMIND": {0x53, 0x0, 0x15}, "FMAXD": {0x53, 0x1, 0x15}, // RV64A — load-reserved / store-conditional (funct5 0x02 / 0x03). "LRW": {0x2F, 0x2, 0x02 << 2}, "LRD": {0x2F, 0x3, 0x02 << 2}, "SCW": {0x2F, 0x2, 0x03 << 2}, "SCD": {0x2F, 0x3, 0x03 << 2}, // FP compare — result in integer register (funct7 0x50/0x51). "FEQS": {0x53, 0x2, 0x50}, "FLTS": {0x53, 0x1, 0x50}, "FLES": {0x53, 0x0, 0x50}, "FEQD": {0x53, 0x2, 0x51}, "FLTD": {0x53, 0x1, 0x51}, "FLED": {0x53, 0x0, 0x51}, } // riscvRType encodes an R-type instruction: funct7 | rs2 | rs1 | funct3 | rd | opcode. func riscvRType(enc riscvEnc, rd, rs1, rs2 int) uint32 { return (enc.funct7 << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) | (enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode } // riscvAMOType encodes an atomic (AMO) instruction. // Layout: funct5 | aq | rl | rs2 | rs1 | funct3 | rd | opcode. // The funct5 is stored in the upper bits of enc.funct7 (shifted left by 2). func riscvAMOType(enc riscvEnc, rd, rs1, rs2 int) uint32 { funct5 := enc.funct7 >> 2 // extract funct5 from the stored value return (funct5 << 27) | (uint32(rs2) << 20) | (uint32(rs1) << 15) | (enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode } // FP conversion instructions (FCVT, FMV). These use the rs2 field to // encode the conversion type rather than a register, so they are handled // separately from the general instruction table. type riscvCvtEnc struct { funct7 uint32 // bits [31:25] rs2 uint32 // conversion-type code in bits [24:20] opcode uint32 // always 0x53 (OP-FP) } var riscvCvtTable = map[string]riscvCvtEnc{ // float → int (rs2 selects the integer width/sign). "FCVTWS": {0x60, 0x0, 0x53}, // float32 → int32 "FCVTWUS": {0x60, 0x1, 0x53}, // float32 → uint32 "FCVTLS": {0x60, 0x2, 0x53}, // float32 → int64 "FCVTLUS": {0x60, 0x3, 0x53}, // float32 → uint64 "FCVTWD": {0x61, 0x0, 0x53}, // float64 → int32 "FCVTWUD": {0x61, 0x1, 0x53}, // float64 → uint32 "FCVTLD": {0x61, 0x2, 0x53}, // float64 → int64 "FCVTLUD": {0x61, 0x3, 0x53}, // float64 → uint64 // int → float (rs2 selects the integer width/sign). "FCVTSW": {0x68, 0x0, 0x53}, // int32 → float32 "FCVTSWU": {0x68, 0x1, 0x53}, // uint32 → float32 "FCVTSL": {0x68, 0x2, 0x53}, // int64 → float32 "FCVTSLU": {0x68, 0x3, 0x53}, // uint64 → float32 "FCVTDW": {0x69, 0x0, 0x53}, // int32 → float64 "FCVTDWU": {0x69, 0x1, 0x53}, // uint32 → float64 "FCVTDL": {0x69, 0x2, 0x53}, // int64 → float64 "FCVTDLU": {0x69, 0x3, 0x53}, // uint64 → float64 // float → float width conversion. "FCVTSD": {0x20, 0x1, 0x53}, // float64 → float32 "FCVTDS": {0x21, 0x0, 0x53}, // float32 → float64 // Bit moves between integer and FP registers (no conversion). "FMVXD": {0x71, 0x0, 0x53}, // float64 → int64 (bit move) "FMVDX": {0x79, 0x0, 0x53}, // int64 → float64 (bit move) "FMVXW": {0x70, 0x0, 0x53}, // float32 → int32 (bit move) "FMVWX": {0x78, 0x0, 0x53}, // int32 → float32 (bit move) } // riscvCvtType encodes an FP conversion instruction. // Layout: funct7 | rs2(convtype) | rs1 | funct3(0) | rd | opcode. func riscvCvtType(enc riscvCvtEnc, rd, rs1 int) uint32 { return (enc.funct7 << 25) | (enc.rs2 << 20) | (uint32(rs1) << 15) | (uint32(rd) << 7) | enc.opcode } // R4-type fused multiply-add instructions (FMADD/FMSUB/FNMSUB/FNMADD). // These take 4 register operands: rs1, rs2, rs3, rd. // Layout: rs3 | fmt | rs2 | rs1 | rm | rd | opcode. type riscvFmaEnc struct { fmt uint32 // bits [26:25]: 0x0 = single, 0x1 = double opcode uint32 // bits [6:0] } var riscvFmaTable = map[string]riscvFmaEnc{ "FMADDS": {0x0, 0x43}, // rd = rs1*rs2 + rs3 "FMADDD": {0x1, 0x43}, "FMSUBS": {0x0, 0x47}, // rd = rs1*rs2 - rs3 "FMSUBD": {0x1, 0x47}, "FNMSUBS": {0x0, 0x4B}, // rd = -(rs1*rs2) + rs3 "FNMSUBD": {0x1, 0x4B}, "FNMADDS": {0x0, 0x4F}, // rd = -(rs1*rs2) - rs3 "FNMADDD": {0x1, 0x4F}, } // riscvFmaType encodes an R4-type fused multiply-add instruction. func riscvFmaType(enc riscvFmaEnc, rd, rs1, rs2, rs3 int) uint32 { return (uint32(rs3) << 27) | (enc.fmt << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) | (0x0 << 12) /* rm=dynamic */ | (uint32(rd) << 7) | enc.opcode } // CSR (Control and Status Register) instructions. // Format: csr[11:0] | rs1/zimm | funct3 | rd | opcode (0x73). type riscvCsrEnc struct { funct3 uint32 // bits [14:12] imm bool // true for CSRRWI/CSRRSI/CSRRCI (5-bit uimm variant) } var riscvCsrTable = map[string]riscvCsrEnc{ "CSRRW": {0x1, false}, // rd=CSR, CSR=rs1 "CSRRS": {0x2, false}, // rd=CSR, CSR |= rs1 "CSRRC": {0x3, false}, // rd=CSR, CSR &= ~rs1 "CSRRWI": {0x5, true}, // rd=CSR, CSR=uimm "CSRRSI": {0x6, true}, // rd=CSR, CSR |= uimm "CSRRCI": {0x7, true}, // rd=CSR, CSR &= ~uimm } // riscvCsrType encodes a CSR instruction. // csr is the 12-bit CSR address; src is either a register number or a 5-bit // unsigned immediate (depending on enc.imm). func riscvCsrType(enc riscvCsrEnc, rd, src int, csr int32) uint32 { return (uint32(csr&0xFFF) << 20) | (uint32(src&0x1F) << 15) | (enc.funct3 << 12) | (uint32(rd) << 7) | 0x73 } // riscvIType encodes an I-type instruction: imm[11:0] | rs1 | funct3 | rd | opcode. func riscvIType(enc riscvEnc, rd, rs1 int, imm int32) uint32 { return (uint32(imm&0xFFF) << 20) | (uint32(rs1) << 15) | (enc.funct3 << 12) | (uint32(rd) << 7) | enc.opcode } // riscvSType encodes an S-type instruction: imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode. func riscvSType(enc riscvEnc, rs1, rs2 int, imm int32) uint32 { immU := uint32(imm) & 0xFFF return ((immU >> 5) << 25) | (uint32(rs2) << 20) | (uint32(rs1) << 15) | (enc.funct3 << 12) | ((immU & 0x1F) << 7) | enc.opcode } // riscvBType encodes a B-type instruction (branches). func riscvBType(enc riscvEnc, rs1, rs2 int, offset int32) uint32 { imm := uint32(offset) & 0x1FFE // bits [12:1], bit 0 is always 0 return (((imm >> 12) & 1) << 31) | // imm[12] (((imm >> 5) & 0x3F) << 25) | // imm[10:5] (uint32(rs2) << 20) | (uint32(rs1) << 15) | (enc.funct3 << 12) | (((imm >> 1) & 0xF) << 8) | // imm[4:1] (((imm >> 11) & 1) << 7) | // imm[11] enc.opcode } // riscvUType encodes a U-type instruction: imm[31:12] | rd | opcode. func riscvUType(enc riscvEnc, rd int, imm int32) uint32 { return (uint32(imm) & 0xFFFFF000) | (uint32(rd) << 7) | enc.opcode } // riscvJType encodes a J-type instruction (JAL). func riscvJType(rd int, offset int32) uint32 { imm := uint32(offset) & 0x1FFFFE // bits [20:1] return (((imm >> 20) & 1) << 31) | // imm[20] (((imm >> 1) & 0x3FF) << 21) | // imm[10:1] (((imm >> 11) & 1) << 20) | // imm[11] (((imm >> 12) & 0xFF) << 12) | // imm[19:12] (uint32(rd) << 7) | 0x6F // JAL opcode } // ---- RVC (compressed) encoding helpers ---- // isRVCIntReg reports whether a register number can be encoded in the 3-bit // prime register field used by compressed instructions (x8–x15). func isRVCIntReg(r int) bool { return r >= 8 && r <= 15 } // rvcReg3 returns the 3-bit encoding for registers x8–x15 (0–7). func rvcReg3(r int) uint32 { return uint32(r - 8) } // rvcCR encodes a CR-type (register) compressed instruction. // Format: funct4 | rd/rs1 | rs2 | op=2. func rvcCR(funct4, rd, rs2 uint32) uint16 { return uint16((funct4 << 12) | (rd << 7) | (rs2 << 2) | 0x2) } // rvcCI encodes a CI-type (immediate) compressed instruction. // Used for C.ADDI, C.LI, C.LUI, C.ADDIW — linear 6-bit immediate. func rvcCI(funct3, rd uint32, imm uint32) uint16 { return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x1) } // rvcLSP encodes a CI-type stack-relative load: C.LDSP (funct3=3) or // C.FLDSP (funct3=1). offset is the full byte offset; the immediate bits // are interleaved per the RISC-V spec: [5:3|8:6]. func rvcLSP(funct3, rd uint32, offset uint32) uint16 { // Bit interleave offset bits [5,4,3,8,7,6] → packed value. packed := uint32(0) for i, b := range []int{5, 4, 3, 8, 7, 6} { packed |= ((offset >> b) & 1) << (5 - i) } return uint16((funct3 << 13) | ((packed>>5)&1)<<12 | (rd << 7) | (packed&0x1F)<<2 | 0x2) } // rvcSSP encodes a CSS-type stack-relative store: C.SDSP (funct3=7) or // C.FSDSP (funct3=5). offset is the full byte offset; the immediate bits // are interleaved per the RISC-V spec: [5:3|8:6]. func rvcSSP(funct3, rs2 uint32, offset uint32) uint16 { // Bit interleave offset bits [5,4,3,8,7,6] → packed value. packed := uint32(0) for i, b := range []int{5, 4, 3, 8, 7, 6} { packed |= ((offset >> b) & 1) << (5 - i) } return uint16((funct3 << 13) | (packed << 7) | (rs2 << 2) | 0x2) } // rvcCSS encodes a CSS-type (stack store) compressed instruction. func rvcCSS(funct3, rs2 uint32, imm uint32) uint16 { return uint16((funct3 << 13) | (imm << 7) | (rs2 << 2) | 0x2) } // rvcCL encodes a CL-type (load) compressed instruction. // imm layout: [5:3] in bits [12:10], [2|6] in bits [6:5]. func rvcCL(funct3, rd, rs1 uint32, imm uint32) uint16 { bits := uint16((funct3 << 13) | ((imm>>3)&0x7)<<10 | (rs1 << 7) | ((imm & 0x7) << 5) | (rd << 2) | 0x0) return bits } // rvcCS encodes a CS-type (store) compressed instruction. func rvcCS(funct3, rs2, rs1 uint32, imm uint32) uint16 { return uint16((funct3 << 13) | ((imm>>3)&0x7)<<10 | (rs1 << 7) | ((imm & 0x7) << 5) | (rs2 << 2) | 0x0) } // rvcCJ encodes a CJ-type (jump) compressed instruction. // offset is a 12-bit signed offset (bit 0 is always 0). func rvcCJ(funct3 uint32, offset int32) uint16 { uoff := uint32(offset) & 0xFFE bits := ((uoff >> 11) & 1) << 10 bits |= ((uoff >> 4) & 1) << 9 bits |= ((uoff >> 9) & 0x3) << 7 bits |= ((uoff >> 10) & 1) << 6 bits |= ((uoff >> 6) & 1) << 5 bits |= ((uoff >> 7) & 1) << 4 bits |= ((uoff >> 1) & 0x7) << 1 bits |= ((uoff >> 5) & 1) return uint16((funct3 << 13) | (bits << 2) | 0x1) } // rvcCA encodes a CA-type (arithmetic) compressed instruction. // Format: funct6[15:10] | rd'/rs1'[9:7] | funct2[6:5] | rs2'[4:2] | op=01. func rvcCA(funct6, funct2, rd, rs2 uint32) uint16 { return uint16((funct6 << 10) | (rd << 7) | (funct2 << 5) | (rs2 << 2) | 0x1) } // rvcCB encodes a CB-type (branch) compressed instruction. // Format: funct3[15:13] | offset[8|4:3] | rs1'[9:7] | offset[7:6|2:1|5] | op=01. // Bit pattern for offset: [8|4:3|7:6|2:1|5] func rvcCB(funct3, rs1 uint32, offset int32) uint16 { uoff := uint32(offset) & 0x1FE // bits [8:1] offBits := uint32(0) offBits |= ((uoff >> 8) & 1) << 10 // bit 10 = offset[8] offBits |= ((uoff >> 3) & 0x3) << 8 // bits 9:8 = offset[4:3] offBits |= ((uoff >> 6) & 0x3) << 6 // bits 7:6 = offset[7:6] offBits |= ((uoff >> 1) & 0x3) << 3 // bits 4:3 = offset[2:1] offBits |= ((uoff >> 5) & 1) << 2 // bit 2 = offset[5] return uint16((funct3 << 13) | offBits | (rs1 << 7) | 0x1) }