From 31a2cee38206edb3883be75a432e12a671aa8920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Thu, 13 Aug 2026 17:41:16 +0200 Subject: [PATCH] fix(asm): materialise RISC-V MOV immediates --- CHANGELOG.md | 7 +++ asm/riscv_assemble.go | 73 +++++++++++++++++++++++--------- asm/riscv_encode_test.go | 10 ++--- testdata/verify/movimm_riscv64.s | 19 +++++++++ verify/riscv_groundtruth_test.go | 1 + 5 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 testdata/verify/movimm_riscv64.s diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b5768..efcfd23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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); diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index 59bb0e0..d5fad7f 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -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 diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go index 3c4d316..a68d09c 100644 --- a/asm/riscv_encode_test.go +++ b/asm/riscv_encode_test.go @@ -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)) } } diff --git a/testdata/verify/movimm_riscv64.s b/testdata/verify/movimm_riscv64.s new file mode 100644 index 0000000..cb196e7 --- /dev/null +++ b/testdata/verify/movimm_riscv64.s @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Petr Balvín (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 diff --git a/verify/riscv_groundtruth_test.go b/verify/riscv_groundtruth_test.go index 54236f2..cb3fece 100644 --- a/verify/riscv_groundtruth_test.go +++ b/verify/riscv_groundtruth_test.go @@ -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)