diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index f8cf3cf..bd2e787 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -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.