feat(riscv): add MOV pseudo-instruction and RVC compressed encoding

Assisted-by: DeepSeek V4 Pro
This commit is contained in:
2026-08-02 18:22:00 +02:00
parent eee7a6d4a4
commit 243b087116
8 changed files with 946 additions and 78 deletions
+379 -13
View File
@@ -10,39 +10,115 @@ import (
)
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
// It handles the core RV64I/RV64M instruction set with fixed 32-bit encoding.
// It handles the core RV64IMAFDC instruction set.
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) {
fi := riscvComputeFrame(t)
prologue := riscvPrologue(fi)
// First pass: compute label offsets (all RISC-V instructions are 4 bytes).
// Labels are offset by the prologue length.
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
// per instruction (or 8 for MOV $large-imm). No encoding yet.
type instrRec struct {
instr *ast.Instr
compressed bool
code []byte
}
var recs []instrRec
offsets := map[string]int{}
var instrs []*ast.Instr
pos := len(prologue)
for _, stmt := range t.Body {
switch s := stmt.(type) {
case *ast.Label:
offsets[s.Name.Text] = pos
case *ast.Instr:
instrs = append(instrs, s)
pos += 4 // all RISC-V instructions are 4 bytes
recs = append(recs, instrRec{instr: s})
pos += riscvInstrSize(s)
}
}
// Second pass: encode instructions.
out := append([]byte(nil), prologue...)
for _, instr := range instrs {
code, err := encodeRISCVInstr(instr, pos, offsets, fi)
// Pass 2: encode each instruction using Pass-1 offsets.
pc := len(prologue)
for i := range recs {
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi)
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", instr.Mnemonic.Text, err)
return nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
}
recs[i].code = code
pc += len(code)
}
// Pass 3: try RVC compression.
for i := range recs {
if c16, ok := tryCompressRVC(recs[i].instr, fi); ok {
recs[i].compressed = true
recs[i].code = []byte{byte(c16), byte(c16 >> 8)}
}
}
// Pass 4: recompute offsets with actual sizes.
offsets = map[string]int{}
pos = len(prologue)
for _, stmt := range t.Body {
switch s := stmt.(type) {
case *ast.Label:
offsets[s.Name.Text] = pos
case *ast.Instr:
for _, r := range recs {
if r.instr == s {
pos += len(r.code)
break
}
}
}
}
// Pass 5: re-encode branches with corrected offsets, emit uncompressed
// for instructions that can't be compressed.
out := append([]byte(nil), prologue...)
pc = len(prologue)
for _, r := range recs {
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
out = append(out, r.code...)
pc += len(r.code)
} else {
// Re-encode with correct offsets (branches need this).
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi)
if err != nil {
return nil, nil, err
}
// Try compression again for this instruction.
if c16, ok := tryCompressRVC(r.instr, fi); ok {
code = []byte{byte(c16), byte(c16 >> 8)}
}
out = append(out, code...)
pc += len(code)
}
out = append(out, code...)
pos -= 4 // track remaining
}
return out, offsets, nil
}
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
// Most instructions are 4 bytes; MOV with a large immediate is 8 (LUI+ADDIW).
func riscvInstrSize(instr *ast.Instr) int {
mnem := instr.Mnemonic.Text
if mnem == "MOV" && len(instr.Operands) == 2 && isImmOperand(instr.Operands[0]) {
imm := immFromOperand(instr.Operands[0])
if imm < -2048 || imm > 2047 {
return 8 // LUI + ADDIW
}
}
return 4
}
// isBranchLike reports whether a mnemonic is a branch or jump that needs
// recalculated offsets after compression.
func isBranchLike(mnem string) bool {
switch mnem {
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "JMP", "JAL":
return true
}
return false
}
// encodeRISCVInstr encodes a single RISC-V instruction.
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo) ([]byte, error) {
mnem := instr.Mnemonic.Text
@@ -88,6 +164,11 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
offset := int32(targetOff - pc)
word = riscvJType(rd, offset)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
// MOV is a pseudo-instruction that the Go assembler uses for loads,
// stores, register moves and immediate loads.
case "MOV":
return encodeRISCVMov(instr, offsets, fi)
}
// FP conversion / move instructions use a separate table (rs2 encodes
@@ -166,6 +247,16 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
}
word = riscvRType(enc, rd, rs1, rs2)
// I-type shift (SLLI, SRLI, SRAI): INSTR rs, $shamt, rd.
case len(ops) == 3 && isShiftImmInstr(mnem):
rs1 := regFromOperand(ops[0])
shamt := int(immFromOperand(ops[1]))
rd := regFromOperand(ops[2])
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvRType(enc, rd, rs1, shamt)
// AMO atomics: Plan 9 order is INSTR src, (addr), dst.
case len(ops) == 3 && isAMOInstr(mnem):
rs2 := regFromOperand(ops[0]) // source value
@@ -304,6 +395,273 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
// isMemOperand reports whether an operand is a memory reference
// (frame-relative such as name+off(FP) or register-relative such as (X10)).
func isMemOperand(op *ast.Operand) bool {
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo != "" {
return true // name+off(FP), name+off(SP)
}
if op.Addr.Base != "" && op.Addr.Sym == nil {
return true // (reg)
}
return false
}
// isImmOperand reports whether an operand is an immediate ($value).
func isImmOperand(op *ast.Operand) bool {
if op.Kind == ast.OpImmediate {
return true
}
if op.Imm.HasVal {
return true
}
return false
}
// encodeRISCVMov encodes the MOV pseudo-instruction.
//
// The Go RISC-V assembler uses MOV for:
// - MOV name+off(FP), Rd load from frame
// - MOV Rd, name+off(FP) store to frame
// - MOV (Rs), Rd register-relative load
// - MOV Rs, (Rd) register-relative store
// - MOV Rs, Rd register-to-register move (ADDI $0)
// - MOV $imm, Rd load immediate (ADDI or LUI+ADDIW)
func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo) ([]byte, error) {
ops := instr.Operands
if len(ops) != 2 {
return nil, fmt.Errorf("MOV expects 2 operands, got %d", len(ops))
}
src := ops[0]
dst := ops[1]
// Immediate → register.
if isImmOperand(src) {
rd := regFromOperand(dst)
if rd < 0 {
return nil, fmt.Errorf("MOV $imm: invalid destination register")
}
imm := immFromOperand(src)
return encodeRISCVLoadImm(rd, imm), nil
}
// Memory → register (load).
if isMemOperand(src) && !isMemOperand(dst) {
rd := regFromOperand(dst)
rs1, off := memFromOperandWithFrame(src, fi)
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("MOV load: invalid operand")
}
word := riscvIType(riscvEnc{0x03, 0x3, 0x00}, rd, rs1, off)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
// Register → memory (store).
if !isMemOperand(src) && isMemOperand(dst) {
rs2 := regFromOperand(src)
rs1, off := memFromOperandWithFrame(dst, fi)
if rs2 < 0 || rs1 < 0 {
return nil, fmt.Errorf("MOV store: invalid operand")
}
word := riscvSType(riscvEnc{0x23, 0x3, 0x00}, rs1, rs2, off)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
// Register → register (ADDI $0, src, dst).
{
rs1 := regFromOperand(src)
rd := regFromOperand(dst)
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("MOV: invalid register operand")
}
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rs1, 0)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
}
// encodeRISCVLoadImm encodes loading an immediate into a register.
// For 12-bit immediates: ADDI $imm, ZERO, rd.
// For larger: LUI $hi, rd + ADDIW $lo, rd, rd.
func encodeRISCVLoadImm(rd int, imm int32) []byte {
if imm >= -2048 && imm <= 2047 {
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, 0, imm)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
}
// LUI + ADDIW for larger constants.
var out []byte
hi := int32((uint32(imm)+0x800)>>12) << 12 // LUI loads upper 20 bits
lo := imm - hi
wordLUI := riscvUType(riscvEnc{0x37, 0x0, 0x00}, rd, hi)
out = append(out, byte(wordLUI), byte(wordLUI>>8), byte(wordLUI>>16), byte(wordLUI>>24))
if lo != 0 {
wordADDIW := riscvIType(riscvEnc{0x1B, 0x0, 0x00}, rd, rd, lo)
out = append(out, byte(wordADDIW), byte(wordADDIW>>8), byte(wordADDIW>>16), byte(wordADDIW>>24))
}
return out
}
// tryCompressRVC attempts to compress a RISC-V instruction to its 16-bit
// RVC form. It returns the compressed instruction word and true on success.
func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
mnem := instr.Mnemonic.Text
ops := instr.Operands
switch mnem {
case "RET":
// RET = JALR X0, 0(X1) → C.JR RA (CR-type: funct4=0x8, rd=0, rs2=1)
return rvcCR(0x8, 0, 1), true
case "LD", "MOV":
// LD rd, offset(SP) → C.LDSP when rd≠0 and uimm[8:3] fits.
// MOV name+off(FP), rd → load, same compression.
if mnem == "MOV" && len(ops) == 2 && isImmOperand(ops[0]) {
return 0, false
}
// MOV reg, reg → C.MV (CR-type: funct4=0x8).
if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && !isMemOperand(ops[1]) && !isImmOperand(ops[0]) {
rs1 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rs1 != -1 && rd != -1 && rs1 != 0 && rd != 0 {
return rvcCR(0x8, uint32(rd), uint32(rs1)), true
}
}
rd, rs1, imm := extractLDParams(instr, fi)
if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
return rvcCI(0x3, uint32(rd), uint32(imm)>>3), true
}
// MOV reg, mem → store, try C.SDSP.
if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && isMemOperand(ops[1]) {
rs2, rs1, imm := extractSDParams(instr, fi)
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
return rvcCSS(0x7, uint32(rs2), uint32(imm)>>3), true
}
}
case "SD":
// SD rs2, offset(SP) → C.SDSP when uimm[8:3] fits (CSS-type).
rs2, rs1, imm := extractSDParams(instr, fi)
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
uimm := uint32(imm) >> 3
return rvcCSS(0x7, uint32(rs2), uimm), true
}
case "ADDI":
rd, rs1, imm := extractITypeParams(instr, fi)
if rd == -1 || rs1 == -1 {
return 0, false
}
if rd == rs1 && rd != 0 && imm != 0 && imm >= -32 && imm <= 31 {
// C.ADDI: funct3=0x0, rs1/rd, nzimm[5:0]
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
}
if rs1 == 0 && rd != 0 && imm >= -32 && imm <= 31 {
// C.LI: funct3=0x2, rd, imm[5:0]
return rvcCI(0x2, uint32(rd), uint32(imm)&0x3F), true
}
if rs1 != 0 && rd != 0 && imm == 0 {
// C.MV: funct4=0x8, rd, rs1 (CR-type)
return rvcCR(0x8, uint32(rd), uint32(rs1)), true
}
case "JAL":
// JAL X0, target → C.J when offset fits in ±2KB.
if len(ops) >= 1 {
// For JAL with implicit rd=0 (JMP alias), check target.
// C.J: funct3=0x5
// Offset is computed at encode time — we can't check it here.
return 0, false
}
case "JMP":
// C.J — but offset is computed at encode time.
return 0, false
case "BEQ":
// BEQ rs, ZERO, target → C.BEQZ when offset fits in ±256.
if len(ops) == 3 {
rs1 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1])
if rs1 != -1 && rs2 == 0 && isRVCIntReg(rs1) {
// Could be C.BEQZ but offset computed at encode time.
return 0, false
}
}
case "BNE":
// BNE rs, ZERO, target → C.BNEZ when offset fits in ±256.
if len(ops) == 3 {
rs1 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1])
if rs1 != -1 && rs2 == 0 && isRVCIntReg(rs1) {
return 0, false
}
}
case "LUI":
// LUI rd, imm → C.LUI when rd≠0, rd≠SP, imm nonzero and fits in 6 bits.
if len(ops) == 2 {
rd := regFromOperand(ops[0])
imm := immFromOperand(ops[1])
if rd != -1 && rd != 0 && rd != 2 && imm != 0 && imm >= 1 && imm <= 63 {
return rvcCI(0x3, uint32(rd), uint32(imm)&0x3F), true
}
}
case "ADDIW":
rd, rs1, imm := extractITypeParams(instr, fi)
if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 {
return rvcCI(0x1, uint32(rd), uint32(imm)&0x3F), true
}
}
return 0, false
}
// extractLDParams extracts rd, rs1, and immediate offset for a load instruction.
func extractLDParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) {
ops := instr.Operands
if len(ops) != 2 {
return -1, -1, 0
}
if instr.Mnemonic.Text == "MOV" {
if isMemOperand(ops[0]) {
rs1, imm = memFromOperandWithFrame(ops[0], fi)
rd = regFromOperand(ops[1])
} else {
return -1, -1, 0
}
} else {
rs1, imm = memFromOperandWithFrame(ops[0], fi)
rd = regFromOperand(ops[1])
}
return
}
// extractSDParams extracts rs2, rs1, and immediate offset for a store instruction.
func extractSDParams(instr *ast.Instr, fi riscvFrameInfo) (rs2, rs1 int, imm int32) {
ops := instr.Operands
if len(ops) != 2 {
return -1, -1, 0
}
rs2 = regFromOperand(ops[0])
rs1, imm = memFromOperandWithFrame(ops[1], fi)
return
}
// extractITypeParams extracts rd, rs1, and immediate for an I-type instruction.
func extractITypeParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) {
ops := instr.Operands
if len(ops) != 3 {
return -1, -1, 0
}
rs1 = regFromOperand(ops[0])
imm = immFromOperand(ops[1])
rd = regFromOperand(ops[2])
return
}
// Instruction type classifiers.
func isRTypeInstr(m string) bool {
switch m {
@@ -316,6 +674,14 @@ func isRTypeInstr(m string) bool {
return false
}
func isShiftImmInstr(m string) bool {
switch m {
case "SLLI", "SRLI", "SRAI", "SLLIW", "SRLIW", "SRAIW":
return true
}
return false
}
func isITypeInstr(m string) bool {
switch m {
case "ADDI", "ADDIW", "SLTI", "SLTIU", "XORI", "ORI", "ANDI", "JALR":