fix(asm): materialise RISC-V MOV immediates
This commit is contained in:
@@ -68,6 +68,13 @@ Unreleased changes on the `development` branch.
|
||||
`C.LUI`/`C.ADDIW` when their immediate fits six signed bits. The
|
||||
byte-exact ground-truth test covers positive, negative, and out-of-range
|
||||
immediates against `GOARCH=riscv64 go tool asm`.
|
||||
- **RISC-V `MOV $imm, rd` materialisation.** The immediate-loading
|
||||
pseudo-instruction now uses the toolchain's `Split32BitImmediate` split
|
||||
(previously it rounded the upper 20 bits, producing wrong results for
|
||||
negative and bit-11-set immediates) and compresses the emitted
|
||||
`ADDI`/`LUI`/`ADDIW` to `C.LI`/`C.LUI`/`C.ADDIW` when their immediate
|
||||
fits six signed bits. A byte-exact ground-truth test covers zero, small,
|
||||
negative, and 32-bit immediates against `GOARCH=riscv64 go tool asm`.
|
||||
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
|
||||
hardware watchpoint slot 0, so a second `watch` call silently overwrote
|
||||
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
|
||||
|
||||
+53
-20
@@ -122,7 +122,9 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Most instructions are 4 bytes; MOV with a large immediate and I-type
|
||||
// arithmetic with a large immediate expand to several (possibly compressed)
|
||||
// instructions.
|
||||
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||
mnem := instr.Mnemonic.Text
|
||||
ops := instr.Operands
|
||||
@@ -142,12 +144,9 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||
if isMemOperand(ops[1]) && ops[1].Addr.Sym != nil && ops[1].Addr.Sym.Pseudo == "SB" {
|
||||
return 8
|
||||
}
|
||||
// MOV $imm, rd → large immediate needs LUI+ADDIW.
|
||||
if isImmOperand(ops[0]) {
|
||||
imm := immFromOperand(ops[0])
|
||||
if imm < -2048 || imm > 2047 {
|
||||
return 8
|
||||
}
|
||||
// MOV $imm, rd → size depends on the immediate and RVC compression.
|
||||
if isImmOperand(ops[0]) && ops[0].Imm.Sym == nil {
|
||||
return riscvMovImmSize(regFromOperand(ops[1]), immFromOperand(ops[0]))
|
||||
}
|
||||
}
|
||||
// I-type arithmetic with a large immediate expands to several instructions.
|
||||
@@ -636,28 +635,62 @@ func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// encodeRISCVLoadImm encodes loading an immediate into a register (MOV $imm,
|
||||
// rd), matching the toolchain's instructionsForMOVConst. For 12-bit
|
||||
// immediates it emits ADDI $imm, ZERO, rd (compressed to C.LI when it fits
|
||||
// six signed bits); for larger immediates it emits LUI + [ADDIW], with the LUI
|
||||
// and ADDIW compressed to C.LUI / C.ADDIW when their immediate fits.
|
||||
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)}
|
||||
if rd != 0 && imm >= -32 && imm <= 31 {
|
||||
return word16(rvcCI(0x2, uint32(rd), uint32(imm)&0x3F)) // C.LI
|
||||
}
|
||||
return wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, 0, imm))
|
||||
}
|
||||
|
||||
// LUI + ADDIW for larger constants.
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
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))
|
||||
if rd != 0 && rd != 2 && high >= -32 && high <= 31 {
|
||||
out = append(out, word16(rvcCI(0x3, uint32(rd), uint32(high)&0x3F))...) // C.LUI
|
||||
} else {
|
||||
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, rd, high<<12))...)
|
||||
}
|
||||
if low != 0 {
|
||||
if low >= -32 && low <= 31 {
|
||||
out = append(out, word16(rvcCI(0x1, uint32(rd), uint32(low)&0x3F))...) // C.ADDIW
|
||||
} else {
|
||||
out = append(out, wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, rd, rd, low))...)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// riscvMovImmSize returns the encoded byte length of MOV $imm, rd, mirroring
|
||||
// encodeRISCVLoadImm's expansion and compression.
|
||||
func riscvMovImmSize(rd int, imm int32) int {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
if rd != 0 && imm >= -32 && imm <= 31 {
|
||||
return 2 // C.LI
|
||||
}
|
||||
return 4 // ADDI
|
||||
}
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
size := 0
|
||||
if rd != 0 && rd != 2 && high >= -32 && high <= 31 {
|
||||
size += 2 // C.LUI
|
||||
} else {
|
||||
size += 4 // LUI
|
||||
}
|
||||
if low != 0 {
|
||||
if low >= -32 && low <= 31 {
|
||||
size += 2 // C.ADDIW
|
||||
} else {
|
||||
size += 4 // ADDIW
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// splitRISCV32Imm splits a signed 32-bit immediate into a signed 12-bit low
|
||||
// part and a signed 20-bit high part, mirroring cmd/internal/obj/riscv's
|
||||
// Split32BitImmediate. The high part is returned unshifted; callers place it
|
||||
|
||||
@@ -122,7 +122,7 @@ done:
|
||||
}
|
||||
|
||||
func TestRISCV_MOV_imm_small(t *testing.T) {
|
||||
// MOV $42, rd → ADDI (fits in 12 bits). Not RVC-compressed (treated as MOV, not ADDI).
|
||||
// MOV $42, rd → ADDI (fits in 12 bits, but not C.LI's 6-bit immediate).
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·small(SB), NOSPLIT, $0
|
||||
MOV $42, X10
|
||||
@@ -136,16 +136,16 @@ TEXT ·small(SB), NOSPLIT, $0
|
||||
}
|
||||
|
||||
func TestRISCV_MOV_imm_large(t *testing.T) {
|
||||
// MOV $0x12345, rd → LUI + ADDIW (8 bytes total)
|
||||
// MOV $0x12345, rd → C.LUI $18 (2B) + ADDIW $837 (4B).
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·large(SB), NOSPLIT, $0
|
||||
MOV $0x12345, X10
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
// LUI (4B) + ADDIW (4B) + JALR (4B) = 12
|
||||
if len(code) != 12 {
|
||||
t.Errorf("expected 12 bytes, got %d", len(code))
|
||||
// C.LUI (2B) + ADDIW (4B) + JALR (4B) = 10
|
||||
if len(code) != 10 {
|
||||
t.Errorf("expected 10 bytes, got %d", len(code))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·movimm(SB), NOSPLIT, $0
|
||||
MOV $0, X10
|
||||
MOV $5, X10
|
||||
MOV $42, X10
|
||||
MOV $-1, X10
|
||||
MOV $-2048, X10
|
||||
MOV $-2049, X10
|
||||
MOV $2047, X10
|
||||
MOV $2048, X10
|
||||
MOV $4095, X10
|
||||
MOV $-4096, X10
|
||||
MOV $0x12345, X10
|
||||
MOV $2147483647, X10
|
||||
RET
|
||||
@@ -22,6 +22,7 @@ func TestGroundTruthRISCV(t *testing.T) {
|
||||
"../testdata/verify/rvc_riscv64.s",
|
||||
"../testdata/verify/loadstore_riscv64.s",
|
||||
"../testdata/verify/largeimm_riscv64.s",
|
||||
"../testdata/verify/movimm_riscv64.s",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
testGroundTruthRISCVFile(t, path)
|
||||
|
||||
Reference in New Issue
Block a user