fix(asm): materialise large RISC-V immediates
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
@@ -61,6 +61,13 @@ Unreleased changes on the `development` branch.
|
||||
`C.LDSP`/`C.SDSP`), plus `C.ADDI4SPN`, `C.ADDW` and `C.SUBW`. The
|
||||
byte-exact ground-truth test exercises these against `GOARCH=riscv64
|
||||
go tool asm`.
|
||||
- **RISC-V large-immediate materialisation.** `ADDI`/`ANDI`/`ORI`/`XORI`
|
||||
with a 32-bit immediate that does not fit 12 bits now expand exactly as
|
||||
`cmd/asm`: two `ADDI`s for the small `ADDI` split range, and
|
||||
`LUI`+`ADDIW`+`<op>` otherwise, with the `LUI` and `ADDIW` compressed to
|
||||
`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`.
|
||||
- **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);
|
||||
|
||||
+117
-2
@@ -150,6 +150,10 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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]) {
|
||||
return riscvItypeImmediateSize(mnem, immFromOperand(ops[0]))
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
@@ -447,7 +451,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvIType(enc, rd, rs1, imm)
|
||||
return encodeRISCVItypeImmediate(mnem, enc, rd, rs1, imm)
|
||||
|
||||
case len(ops) == 2 && isITypeInstr(mnem):
|
||||
imm := immFromOperand(ops[0])
|
||||
@@ -455,7 +459,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvIType(enc, rd, rd, imm)
|
||||
return encodeRISCVItypeImmediate(mnem, enc, rd, rd, imm)
|
||||
|
||||
// Loads: rd, offset(rs1) — Plan 9 order is LD src, dst.
|
||||
case len(ops) == 2 && isLoadInstr(mnem):
|
||||
@@ -654,6 +658,112 @@ func encodeRISCVLoadImm(rd int, imm int32) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
// 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
|
||||
// in the upper bits of LUI (or its compressed C.LUI form).
|
||||
func splitRISCV32Imm(imm int32) (low, high int32) {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
return imm, 0
|
||||
}
|
||||
h := int64(imm) >> 12
|
||||
if imm&(1<<11) != 0 {
|
||||
h++
|
||||
}
|
||||
low = int32((int64(imm) << 52) >> 52) // sign extend 12 bits
|
||||
high = int32((h << 44) >> 44) // sign extend 20 bits
|
||||
return low, high
|
||||
}
|
||||
|
||||
// encodeRISCVItypeImmediate encodes an I-type arithmetic instruction, expanding
|
||||
// large immediates for ADDI/ANDI/ORI/XORI into LUI+ADDIW+op (or two ADDIs for
|
||||
// ADDI), matching the Go assembler.
|
||||
func encodeRISCVItypeImmediate(mnem string, enc riscvEnc, rd, rs1 int, imm int32) ([]byte, error) {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
return wordLE(riscvIType(enc, rd, rs1, imm)), nil
|
||||
}
|
||||
|
||||
var opMn string
|
||||
switch mnem {
|
||||
case "ADDI":
|
||||
opMn = "ADD"
|
||||
case "ANDI":
|
||||
opMn = "AND"
|
||||
case "ORI":
|
||||
opMn = "OR"
|
||||
case "XORI":
|
||||
opMn = "XOR"
|
||||
default:
|
||||
return nil, fmt.Errorf("%s: immediate %d does not fit 12 bits", mnem, imm)
|
||||
}
|
||||
|
||||
// ADDI with a small-ish immediate splits into two ADDIs.
|
||||
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
|
||||
imm0 := imm / 2
|
||||
imm1 := imm - imm0
|
||||
var out []byte
|
||||
out = append(out, wordLE(riscvIType(enc, rd, rs1, imm0))...)
|
||||
out = append(out, wordLE(riscvIType(enc, rd, rd, imm1))...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// LUI $high, TMP; [ADDIW $low, TMP, TMP]; op TMP, rs1, rd. The LUI and
|
||||
// ADDIW compress to their RVC forms (C.LUI / C.ADDIW) when the immediate
|
||||
// fits 6 signed bits, matching the toolchain's compress pass.
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
tmp := 31 // X31 = T6 = TMP
|
||||
var out []byte
|
||||
if high != 0 && high >= -32 && high <= 31 {
|
||||
out = append(out, word16(rvcCI(0x3, uint32(tmp), uint32(high)&0x3F))...)
|
||||
} else {
|
||||
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, tmp, high<<12))...)
|
||||
}
|
||||
if low != 0 {
|
||||
if low >= -32 && low <= 31 {
|
||||
out = append(out, word16(rvcCI(0x1, uint32(tmp), uint32(low)&0x3F))...)
|
||||
} else {
|
||||
out = append(out, wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, tmp, tmp, low))...)
|
||||
}
|
||||
}
|
||||
opEnc, ok := riscvInstrTable[opMn]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: unsupported operation %q", mnem, opMn)
|
||||
}
|
||||
out = append(out, wordLE(riscvRType(opEnc, rd, rs1, tmp))...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// riscvItypeImmediateSize returns the encoded byte length of an I-type
|
||||
// immediate instruction, accounting for the large-immediate expansion.
|
||||
func riscvItypeImmediateSize(mnem string, imm int32) int {
|
||||
if imm >= -2048 && imm <= 2047 {
|
||||
return 4
|
||||
}
|
||||
switch mnem {
|
||||
case "ADDI", "ANDI", "ORI", "XORI":
|
||||
default:
|
||||
return 4
|
||||
}
|
||||
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
|
||||
return 8
|
||||
}
|
||||
low, high := splitRISCV32Imm(imm)
|
||||
size := 4 // the R-type op (TMP is X31, never compressed)
|
||||
if high != 0 && 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
|
||||
}
|
||||
|
||||
// encodeRISCVSBAddr emits AUIPC + ADDI to load the address of a static
|
||||
// symbol into rd, recording the single R_RISCV_PCREL_ITYPE relocation the Go
|
||||
// toolchain uses for the pair (the object-file emitters expand or map it).
|
||||
@@ -700,6 +810,11 @@ func wordLE(w uint32) []byte {
|
||||
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
|
||||
}
|
||||
|
||||
// word16 encodes a uint16 as 2 little-endian bytes.
|
||||
func word16(w uint16) []byte {
|
||||
return []byte{byte(w), byte(w >> 8)}
|
||||
}
|
||||
|
||||
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
||||
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
|
||||
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT ·largeimm(SB), NOSPLIT, $0
|
||||
ADDI $2048, X5
|
||||
ADDI $4095, X5, X6
|
||||
ANDI $4095, X5, X6
|
||||
ORI $-4096, X5, X6
|
||||
XORI $0x12345, X5, X6
|
||||
RET
|
||||
@@ -21,6 +21,7 @@ func TestGroundTruthRISCV(t *testing.T) {
|
||||
"../testdata/verify/basic_riscv64.s",
|
||||
"../testdata/verify/rvc_riscv64.s",
|
||||
"../testdata/verify/loadstore_riscv64.s",
|
||||
"../testdata/verify/largeimm_riscv64.s",
|
||||
} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
testGroundTruthRISCVFile(t, path)
|
||||
|
||||
Reference in New Issue
Block a user