fix(asm): route the riscv64 register moves through the toolchain forms

Assisted-by: GLM 5.3
This commit is contained in:
petrbalvin committed 2026-10-07 13:51:02 +02:00
1 parent da35883633
commit 36d1ac804a
1 file changed
+209 -9
+209 -9
View File
@@ -6,8 +6,10 @@ package asm
import (
"errors"
"fmt"
"math"
"math/bits"
"slices"
"strconv"
"strings"
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
@@ -368,6 +370,17 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
return len(riscvReturn(fi))
}
if strings.HasPrefix(mnem, "MOV") && len(ops) == 2 {
// FP constant: FMV from X0 for a zero bit pattern, otherwise the
// 8-byte AUIPC + FLW/FLD pool load.
if (mnem == "MOVF" || mnem == "MOVD") && isImmOperand(ops[0]) &&
!ops[0].Imm.HasVal && ops[0].Imm.Sym == nil && ops[0].Imm.Str == "" {
if pattern, _, err := riscvFPConstBits(mnem, ops[0]); err == nil {
if pattern == 0 {
return 4
}
return 8
}
}
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
return 8
@@ -410,6 +423,29 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
if isMemOperand(ops[1]) && !isMemOperand(ops[0]) {
return riscvFrameMemSize(ops[1], fi)
}
// Register-to-register width moves: the SLLI + SRAI/SRLI extension
// pairs carry their per-half compression; the single-word forms are
// four bytes before the RVC pass (bare MOV compresses to C.MV or
// C.LI, which the post-encoding pass resolves from real bytes).
if !isMemOperand(ops[0]) && !isMemOperand(ops[1]) {
rd := regFromOperand(ops[1])
rs1 := regFromOperand(ops[0])
switch mnem {
case "MOVB", "MOVH":
shamt := 56
if mnem == "MOVH" {
shamt = 48
}
return len(riscvExtendBytes(rd, rs1, shamt, true))
case "MOVHU", "MOVWU":
shamt := 48
if mnem == "MOVWU" {
shamt = 32
}
return len(riscvExtendBytes(rd, rs1, shamt, false))
}
return 4
}
}
// I-type arithmetic with a large immediate expands to several instructions.
if (mnem == "ADDI" || mnem == "ANDI" || mnem == "ORI" || mnem == "XORI") && len(ops) >= 1 && isImmOperand(ops[0]) {
@@ -1831,6 +1867,43 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *
// Immediate → register.
if isImmOperand(src) {
mnem := strings.ToUpper(instr.Mnemonic.Text)
// FP constant → FP register: a zero bit pattern moves through FMV
// from X0, anything else loads from the pooled $f32/$f64 constant
// symbol the toolchain synthesises (AUIPC + FLW/FLD through TMP).
// An integer constant is not an FP load source, exactly as the
// toolchain rejects the non-FCONST forms.
if (mnem == "MOVF" || mnem == "MOVD") && !src.Imm.HasVal && src.Imm.Sym == nil && src.Imm.Str == "" {
rd := regFromOperand(dst)
if rd < 0 || !riscvIsFloatRegOperand(dst) {
return nil, fmt.Errorf("%s $float: invalid destination register", mnem)
}
pattern, _, err := riscvFPConstBits(mnem, src)
if err != nil {
return nil, fmt.Errorf("%s: invalid floating-point constant %q", mnem, src.Imm.Float)
}
if pattern == 0 {
op := uint32(0x78) << 25 // FMV.W.X
if mnem == "MOVD" {
op = uint32(0x79) << 25 // FMV.D.X
}
return wordLE(op | uint32(rd)<<7 | 0x53), nil
}
var name string
var data []byte
double := mnem == "MOVD"
if double {
name = fmt.Sprintf("$f64.%016x", pattern)
data = riscvLiteralBytes(int64(pattern))
} else {
name = fmt.Sprintf("$f32.%08x", uint32(pattern))
data = []byte{byte(pattern), byte(pattern >> 8), byte(pattern >> 16), byte(pattern >> 24)}
}
if lits != nil {
lits.add(name, data)
}
return encodeRISCVSBFPLoad(name, rd, double, relocs), nil
}
// MOV $sym(SB), rd, load address of a static symbol or external.
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
rd := regFromOperand(dst)
@@ -1874,7 +1947,6 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *
// Memory → register (load).
if isMemOperand(src) && !isMemOperand(dst) {
rd := regFromOperand(dst)
// MOV sym(SB), rd, load from static data.
if src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB" {
if rd < 0 {
return nil, fmt.Errorf("MOV sym(SB): invalid destination register")
@@ -1891,7 +1963,6 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *
// Register → memory (store).
if !isMemOperand(src) && isMemOperand(dst) {
rs2 := regFromOperand(src)
// MOV rd, sym(SB), store to static data.
if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" {
if rs2 < 0 {
return nil, fmt.Errorf("MOV rd, sym(SB): invalid source register")
@@ -1905,8 +1976,12 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *
return riscvFrameMemOp(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), true), true, rs2, rs1, off), nil
}
// Register → register: MOVD/MOVF are FP moves (fsgnj with rs2 = rs1),
// everything else is ADDI $0, src, dst.
// Register → register. The width suffix selects the toolchain's
// synthesis: MOVF/MOVD between the integer and FP banks are FMV and
// inside the FP bank FSGNJ with rs2 = rs1; MOVW is ADDIW $0; MOVBU is
// ANDI $255; MOVB and MOVH sign-extend through SLLI+SRAI and MOVHU/
// MOVWU zero-extend through SLLI+SRLI; bare MOV is ADDI $0, which the
// RVC pass compresses.
{
rs1 := regFromOperand(src)
rd := regFromOperand(dst)
@@ -1914,12 +1989,50 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *
return nil, fmt.Errorf("MOV: invalid register operand")
}
mnem := strings.ToUpper(instr.Mnemonic.Text)
if mnem == "MOVD" || mnem == "MOVF" {
op := uint32(0x20000053) // FSGNJ.S
if mnem == "MOVD" {
op = 0x22000053 // FSGNJ.D
srcF, dstF := riscvIsFloatRegOperand(src), riscvIsFloatRegOperand(dst)
switch mnem {
case "MOVF", "MOVD":
switch {
case srcF && dstF:
op := uint32(0x20000053) // FSGNJ.S
if mnem == "MOVD" {
op = 0x22000053 // FSGNJ.D
}
return wordLE(op | uint32(rs1)<<15 | uint32(rs1)<<20 | uint32(rd)<<7), nil
case dstF && !srcF:
op := uint32(0x78) << 25 // FMV.W.X
if mnem == "MOVD" {
op = uint32(0x79) << 25 // FMV.D.X
}
return wordLE(op | uint32(rs1)<<15 | uint32(rd)<<7 | 0x53), nil
case srcF && !dstF:
op := uint32(0x70) << 25 // FMV.X.W
if mnem == "MOVD" {
op = uint32(0x71) << 25 // FMV.X.D
}
return wordLE(op | uint32(rs1)<<15 | uint32(rd)<<7 | 0x53), nil
}
return wordLE(op | uint32(rs1)<<15 | uint32(rs1)<<20 | uint32(rd)<<7), nil
return nil, fmt.Errorf("%s: both registers must be in the same bank", mnem)
case "MOVW":
// ADDIW $0, rs, rd; the two-operand-only form never has
// rd == rs1 in real sources, and the toolchain's C.ADDIW
// forbids a zero immediate, so the word stays uncompressed.
return wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, rd, rs1, 0)), nil
case "MOVBU":
// ANDI $255, rs, rd; 255 never fits C.ANDI's six signed bits.
return wordLE(riscvIType(riscvEnc{0x13, 0x7, 0x00}, rd, rs1, 0xFF)), nil
case "MOVB", "MOVH":
shamt := 56
if mnem == "MOVH" {
shamt = 48
}
return riscvExtendBytes(rd, rs1, shamt, true), nil
case "MOVHU", "MOVWU":
shamt := 48
if mnem == "MOVWU" {
shamt = 32
}
return riscvExtendBytes(rd, rs1, shamt, false), nil
}
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rs1, 0)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
@@ -2238,6 +2351,93 @@ func riscvLiteralBytes(v int64) []byte {
byte(v >> 32), byte(v >> 40), byte(v >> 48), byte(v >> 56)}
}
// riscvIsFloatRegOperand reports whether the operand spells a floating-point
// register: every F-bank spelling starts with F, and the one integer name
// that does (FP, the frame pointer alias of X8) is excluded.
func riscvIsFloatRegOperand(op *ast.Operand) bool {
name := ""
if op.Addr.Base != "" {
name = op.Addr.Base
} else if op.Addr.Sym != nil {
name = op.Addr.Sym.Name
}
return name != "FP" && strings.HasPrefix(name, "F")
}
// riscvFPConstBits resolves an FP constant operand to the bit pattern the
// toolchain moves or pools: MOVF narrows through float32 first, MOVD keeps
// the float64 bits. The bare spelling fills Imm.Float and the parenthesised
// one ($ (709.78…)) leaves only the raw text, exactly as on arm64.
func riscvFPConstBits(mnem string, src *ast.Operand) (uint64, bool, error) {
text := src.Imm.Float
if text == "" {
s := strings.Join(strings.Fields(src.Raw), "")
s = strings.TrimPrefix(s, "$")
if strings.HasPrefix(s, "(") && strings.HasSuffix(s, ")") &&
strings.ContainsAny(s[1:len(s)-1], ".eE") {
text = s[1 : len(s)-1]
}
}
if text == "" {
return 0, false, fmt.Errorf("not a floating-point constant")
}
f, err := strconv.ParseFloat(text, 64)
if err != nil {
return 0, false, err
}
if src.Imm.Neg {
f = -f
}
if mnem == "MOVF" {
return uint64(math.Float32bits(float32(f))), false, nil
}
return math.Float64bits(f), true, nil
}
// riscvExtendBytes emits the SLLI + SRAI/SRLI pair the toolchain synthesises
// for the MOVB/MOVH/MOVHU/MOVWU register moves, with the per-half compression
// its compress pass applies: the SLLI compresses only in place (rd == rs1),
// the SRAI/SRLI only for the prime registers X8 to X15.
func riscvExtendBytes(rd, rs1, shamt int, arithmetic bool) []byte {
var out []byte
if rd == rs1 && rd != 0 {
out = word16(rvcSLLI(uint32(rd), uint32(shamt)))
} else {
out = wordLE(riscvIType(riscvEnc{0x13, 0x1, 0x00}, rd, rs1, int32(shamt)))
}
if isRVCIntReg(rd) {
funct2 := uint32(0x0)
if arithmetic {
funct2 = 0x1
}
out = append(out, word16(rvcCBShift(funct2, rvcReg3(rd), uint32(shamt)))...)
} else {
imm := int32(shamt)
if arithmetic {
imm = 0x400 | int32(shamt) // funct6 010000, the SRAI half
}
out = append(out, wordLE(riscvIType(riscvEnc{0x13, 0x5, 0x00}, rd, rd, imm))...)
}
return out
}
// encodeRISCVSBFPLoad emits AUIPC X31 + FLW/FLD against a pooled constant
// symbol, the toolchain's form for an FP destination: the address lands in
// TMP because the destination register is not an integer one. The single
// R_RISCV_PCREL_ITYPE relocation covers the pair.
func encodeRISCVSBFPLoad(name string, rd int, double bool, relocs *[]Reloc) []byte {
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType})
}
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, 31, 0)
width := uint32(0x2) // FLW
if double {
width = 0x3 // FLD
}
fl := riscvIType(riscvEnc{0x07, width, 0x00}, rd, 31, 0)
return append(wordLE(auipc), wordLE(fl)...)
}
// RiscvLiteral is one pooled 64-bit constant: a MOV whose immediate sits
// beyond both the 32-bit span and the shift sequences loads its bits from a
// read-only data symbol named like the toolchain's $i64 pool.