diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f78d1..96b5768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`+`` 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); diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index 5df261f..59bb0e0 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -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) { diff --git a/testdata/verify/largeimm_riscv64.s b/testdata/verify/largeimm_riscv64.s new file mode 100644 index 0000000..fffa627 --- /dev/null +++ b/testdata/verify/largeimm_riscv64.s @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Petr Balvín (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 diff --git a/verify/riscv_groundtruth_test.go b/verify/riscv_groundtruth_test.go index d252bc8..54236f2 100644 --- a/verify/riscv_groundtruth_test.go +++ b/verify/riscv_groundtruth_test.go @@ -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)