fix(asm): correct RISC-V operand order and complete RVC compression

Assisted-by: DeepSeek V4 Pro
This commit is contained in:
2026-08-13 15:13:31 +02:00
parent b78b6c5004
commit 373c09f725
8 changed files with 212 additions and 77 deletions
+9
View File
@@ -46,6 +46,15 @@ Unreleased changes on the `development` branch.
are populated from the recorded stack-adjustment and source-line data, and are populated from the recorded stack-adjustment and source-line data, and
a byte-exact ground-truth test compares framed and leaf functions against a byte-exact ground-truth test compares framed and leaf functions against
`GOARCH=riscv64 go tool asm`. `GOARCH=riscv64 go tool asm`.
- **RISC-V operand ordering and RVC compression.** R-type instructions now
take `rs2, rs1, rd` and I-type arithmetic instructions take `imm12, rs1,
rd`, matching the Go assembler's documented operand order (previously both
were reversed, so non-commutative R-type instructions such as `SUB` encoded
the wrong operation). The two-operand ternary forms (`ADD rs2, rd`,
`ADDI $imm, rd`, `SLLI $shamt, rd`) are now accepted. RVC compression is
completed for `C.ADDI16SP`, `C.SLLI`, `C.SRLI`, `C.SRAI`, `C.ANDI`,
`C.NOP`, `C.EBREAK`, `C.MV` (from `ADDI`/`ADD`) and the commutative
`AND`/`OR`/`XOR` forms; the byte-exact ground-truth test now covers these.
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used - **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
hardware watchpoint slot 0, so a second `watch` call silently overwrote hardware watchpoint slot 0, so a second `watch` call silently overwrote
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3); the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
+87 -37
View File
@@ -324,26 +324,44 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
} }
switch { switch {
// R-type: Plan 9 order is INSTR src1, src2, dst (destination last). // R-type: Go reverses the ISA order, writing rs2, rs1, rd (destination
// last); the two-operand form INSTR rs2, rd uses rd as rs1.
case len(ops) == 3 && isRTypeInstr(mnem): case len(ops) == 3 && isRTypeInstr(mnem):
rs1 := regFromOperand(ops[0]) // source 1 (first operand) rs2 := regFromOperand(ops[0]) // first operand = rs2
rs2 := regFromOperand(ops[1]) // source 2 (second operand) rs1 := regFromOperand(ops[1]) // second operand = rs1
rd := regFromOperand(ops[2]) // destination (last operand) rd := regFromOperand(ops[2]) // destination (last operand)
if rd < 0 || rs1 < 0 || rs2 < 0 { if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem) return nil, fmt.Errorf("invalid register in %s", mnem)
} }
word = riscvRType(enc, rd, rs1, rs2) word = riscvRType(enc, rd, rs1, rs2)
// I-type shift (SLLI, SRLI, SRAI): INSTR rs, $shamt, rd. case len(ops) == 2 && isRTypeInstr(mnem):
rs2 := regFromOperand(ops[0]) // source (first operand)
rd := regFromOperand(ops[1]) // destination (second operand)
if rd < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvRType(enc, rd, rd, rs2)
// I-type shift (SLLI, SRLI, SRAI): INSTR $shamt, rs1, rd; the two-operand
// form INSTR $shamt, rd uses rd as the source.
case len(ops) == 3 && isShiftImmInstr(mnem): case len(ops) == 3 && isShiftImmInstr(mnem):
rs1 := regFromOperand(ops[0]) shamt := int(immFromOperand(ops[0]))
shamt := int(immFromOperand(ops[1])) rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2]) rd := regFromOperand(ops[2])
if rd < 0 || rs1 < 0 { if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem) return nil, fmt.Errorf("invalid register in %s", mnem)
} }
word = riscvRType(enc, rd, rs1, shamt) word = riscvRType(enc, rd, rs1, shamt)
case len(ops) == 2 && isShiftImmInstr(mnem):
shamt := int(immFromOperand(ops[0]))
rd := regFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvRType(enc, rd, rd, shamt)
// AMO atomics: Plan 9 order is INSTR src, (addr), dst. // AMO atomics: Plan 9 order is INSTR src, (addr), dst.
case len(ops) == 3 && isAMOInstr(mnem): case len(ops) == 3 && isAMOInstr(mnem):
rs2 := regFromOperand(ops[0]) // source value rs2 := regFromOperand(ops[0]) // source value
@@ -354,10 +372,10 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
} }
word = riscvAMOType(enc, rd, rs1, rs2) word = riscvAMOType(enc, rd, rs1, rs2)
// FP arithmetic: Plan 9 order is INSTR src1, src2, dst. // FP arithmetic: Go reverses the ISA order, writing rs2, rs1, rd.
case len(ops) == 3 && isFPArithInstr(mnem): case len(ops) == 3 && isFPArithInstr(mnem):
rs1 := regFromOperand(ops[0]) rs2 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1]) rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2]) rd := regFromOperand(ops[2])
if rd < 0 || rs1 < 0 || rs2 < 0 { if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid FP register in %s", mnem) return nil, fmt.Errorf("invalid FP register in %s", mnem)
@@ -410,26 +428,35 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
} }
word = riscvAMOType(enc, rd, rs1, rs2) word = riscvAMOType(enc, rd, rs1, rs2)
// FP compare: INSTR src1, src2, dst(int) — result in integer register. // FP compare: Go reverses the ISA order, writing rs2, rs1, rd.
case len(ops) == 3 && isFPCmpInstr(mnem): case len(ops) == 3 && isFPCmpInstr(mnem):
rs1 := regFromOperand(ops[0]) rs2 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1]) rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2]) rd := regFromOperand(ops[2])
if rd < 0 || rs1 < 0 || rs2 < 0 { if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem) return nil, fmt.Errorf("invalid operand in %s", mnem)
} }
word = riscvRType(enc, rd, rs1, rs2) word = riscvRType(enc, rd, rs1, rs2)
// I-type with immediate: Plan 9 order is INSTR src, imm, dst. // I-type with immediate: Plan 9 order is INSTR $imm, rs1, rd; the
// two-operand form INSTR $imm, rd uses rd as the source.
case len(ops) == 3 && isITypeInstr(mnem): case len(ops) == 3 && isITypeInstr(mnem):
rs1 := regFromOperand(ops[0]) // source register imm := immFromOperand(ops[0]) // immediate
imm := immFromOperand(ops[1]) // immediate rs1 := regFromOperand(ops[1]) // source register
rd := regFromOperand(ops[2]) // destination rd := regFromOperand(ops[2]) // destination
if rd < 0 || rs1 < 0 { if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem) return nil, fmt.Errorf("invalid register in %s", mnem)
} }
word = riscvIType(enc, rd, rs1, imm) word = riscvIType(enc, rd, rs1, imm)
case len(ops) == 2 && isITypeInstr(mnem):
imm := immFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvIType(enc, rd, rd, imm)
// Loads: rd, offset(rs1) — Plan 9 order is LD src, dst. // Loads: rd, offset(rs1) — Plan 9 order is LD src, dst.
case len(ops) == 2 && isLoadInstr(mnem): case len(ops) == 2 && isLoadInstr(mnem):
rd := regFromOperand(ops[1]) // destination (last operand) rd := regFromOperand(ops[1]) // destination (last operand)
@@ -742,6 +769,10 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
if rd == -1 || rs1 == -1 { if rd == -1 || rs1 == -1 {
return 0, false return 0, false
} }
if rd == 2 && rs1 == 2 && imm != 0 && imm%16 == 0 && imm >= -512 && imm <= 511 {
// C.ADDI16SP: ADDI to SP by a nonzero 16-byte multiple.
return rvcADDI16SP(2, imm), true
}
if rd == rs1 && rd != 0 && imm != 0 && imm >= -32 && imm <= 31 { if rd == rs1 && rd != 0 && imm != 0 && imm >= -32 && imm <= 31 {
// C.ADDI: funct3=0x0, rs1/rd, nzimm[5:0] // C.ADDI: funct3=0x0, rs1/rd, nzimm[5:0]
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
@@ -754,6 +785,10 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
// C.MV: funct4=0x8, rd, rs1 (CR-type) // C.MV: funct4=0x8, rd, rs1 (CR-type)
return rvcCR(0x8, uint32(rd), uint32(rs1)), true return rvcCR(0x8, uint32(rd), uint32(rs1)), true
} }
if rd == 0 && rs1 == 0 && imm == 0 {
// C.NOP
return 0x0001, true
}
case "JAL": case "JAL":
// JAL X0, target → C.J when offset fits in ±2KB. // JAL X0, target → C.J when offset fits in ±2KB.
@@ -777,11 +812,11 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
return 0, false return 0, false
case "ADD": case "ADD":
// ADD rd, rs2 → C.ADD (CR-type, funct4=0x9) when rd == rs1; ADD is // ADD rs2, rs1, rd → C.ADD (CR-type, funct4=0x9) when rd == rs1; ADD
// commutative, so if rd == rs2, swap. // is commutative, so if rd == rs2, swap. ADD rs2, X0, rd is C.MV.
if len(ops) == 3 { if len(ops) == 3 {
rs1 := regFromOperand(ops[0]) rs2 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1]) rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2]) rd := regFromOperand(ops[2])
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 { if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
if rd == rs1 && rs2 != 0 { if rd == rs1 && rs2 != 0 {
@@ -790,6 +825,10 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
if rd == rs2 && rs1 != 0 { if rd == rs2 && rs1 != 0 {
return rvcCR(0x9, uint32(rd), uint32(rs1)), true return rvcCR(0x9, uint32(rd), uint32(rs1)), true
} }
if rs1 == 0 && rs2 != 0 {
// ADD rs2, X0, rd → C.MV rd, rs2.
return rvcCR(0x8, uint32(rd), uint32(rs2)), true
}
} }
} }
@@ -807,13 +846,17 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
case "AND": case "AND":
funct2 = 0x3 funct2 = 0x3
} }
rs1 := regFromOperand(ops[0]) rs2 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1]) rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2]) rd := regFromOperand(ops[2])
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 { if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
if rd == rs1 && isRVCIntReg(rd) && isRVCIntReg(rs2) && rs2 != 0 { if rd == rs1 && isRVCIntReg(rd) && isRVCIntReg(rs2) && rs2 != 0 {
return rvcCA(0x23, funct2, rvcReg3(rd), rvcReg3(rs2)), true return rvcCA(0x23, funct2, rvcReg3(rd), rvcReg3(rs2)), true
} }
// AND/OR/XOR are commutative; SUB is not.
if mnem != "SUB" && rd == rs2 && isRVCIntReg(rd) && isRVCIntReg(rs1) && rs1 != 0 {
return rvcCA(0x23, funct2, rvcReg3(rd), rvcReg3(rs1)), true
}
} }
} }
@@ -848,33 +891,32 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
} }
case "SLLI", "SRLI", "SRAI": case "SLLI", "SRLI", "SRAI":
// C.SLLI (funct3=0x0), C.SRLI (funct3=0x4, funct2=0), C.SRAI (funct3=0x4, funct2=1).
rd, rs1, imm := extractITypeParams(instr, fi) rd, rs1, imm := extractITypeParams(instr, fi)
if rd == rs1 && rd != 0 && imm != 0 && imm >= 1 && imm <= 63 { if rd == rs1 && rd != 0 && imm != 0 && imm >= 1 && imm <= 63 {
if mnem == "SLLI" { if mnem == "SLLI" {
// C.SLLI: funct3=0, CI-type with shamt in bits [12|6:2]. // C.SLLI: funct3=0, op=10 quadrant, shamt in bits [12|6:2].
// For simplicity, use the standard CI format — the shamt is in imm[5:0]. return rvcSLLI(uint32(rd), uint32(imm)&0x3F), true
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
} }
if isRVCIntReg(rd) { if isRVCIntReg(rd) {
funct2 := uint32(0x0) funct2 := uint32(0x0)
if mnem == "SRAI" { if mnem == "SRAI" {
funct2 = 0x1 funct2 = 0x1
} }
// CB-format shift: funct3=0x4, shamt in bits [12|6:2]. // C.SRLI/C.SRAI: CB-type, funct3=0x4.
// Use simplified encoding for now. return rvcCBShift(funct2, rvcReg3(rd), uint32(imm)&0x3F), true
_ = funct2
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
} }
} }
case "ANDI": case "ANDI":
rd, rs1, imm := extractITypeParams(instr, fi) rd, rs1, imm := extractITypeParams(instr, fi)
if isRVCIntReg(rd) && rd == rs1 && imm >= -32 && imm <= 31 { if isRVCIntReg(rd) && rd == rs1 && imm >= -32 && imm <= 31 {
// C.ANDI: funct3=0x4, funct2=0x2 (CB-type). // C.ANDI: CB-type, funct3=0x4, funct2=0x2.
// Simplified encoding for now. return rvcCBShift(0x2, rvcReg3(rd), uint32(imm)&0x3F), true
return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true
} }
case "EBREAK":
// C.EBREAK: CR-type, funct4=0x9, rd=0, rs2=0.
return rvcCR(0x9, 0, 0), true
} }
return 0, false return 0, false
@@ -911,15 +953,23 @@ func extractSDParams(instr *ast.Instr, fi riscvFrameInfo) (rs2, rs1 int, imm int
return return
} }
// extractITypeParams extracts rd, rs1, and immediate for an I-type instruction. // extractITypeParams extracts rd, rs1, and immediate for an I-type
// instruction. The Plan 9 order is INSTR $imm, rs1, rd (3 operands) or
// INSTR $imm, rd (2 operands, rd is also the source).
func extractITypeParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) { func extractITypeParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) {
ops := instr.Operands ops := instr.Operands
if len(ops) != 3 { switch len(ops) {
case 3:
imm = immFromOperand(ops[0])
rs1 = regFromOperand(ops[1])
rd = regFromOperand(ops[2])
case 2:
imm = immFromOperand(ops[0])
rd = regFromOperand(ops[1])
rs1 = rd
default:
return -1, -1, 0 return -1, -1, 0
} }
rs1 = regFromOperand(ops[0])
imm = immFromOperand(ops[1])
rd = regFromOperand(ops[2])
return return
} }
+26
View File
@@ -460,6 +460,12 @@ func rvcCI(funct3, rd uint32, imm uint32) uint16 {
return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x1) return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x1)
} }
// rvcSLLI encodes C.SLLI, which shares funct3=0 with C.ADDI but lives in the
// op=10 quadrant (unlike C.ADDI's op=01).
func rvcSLLI(rd, shamt uint32) uint16 {
return uint16(((shamt>>5)&1)<<12 | (rd << 7) | (shamt&0x1F)<<2 | 0x2)
}
// rvcLSP encodes a CI-type stack-relative load: C.LDSP (funct3=3) or // rvcLSP encodes a CI-type stack-relative load: C.LDSP (funct3=3) or
// C.FLDSP (funct3=1). offset is the full byte offset; the immediate bits // C.FLDSP (funct3=1). offset is the full byte offset; the immediate bits
// are interleaved per the RISC-V spec: [5:3|8:6]. // are interleaved per the RISC-V spec: [5:3|8:6].
@@ -535,3 +541,23 @@ func rvcCB(funct3, rs1 uint32, offset int32) uint16 {
offBits |= ((uoff >> 5) & 1) << 2 // bit 2 = offset[5] offBits |= ((uoff >> 5) & 1) << 2 // bit 2 = offset[5]
return uint16((funct3 << 13) | offBits | (rs1 << 7) | 0x1) return uint16((funct3 << 13) | offBits | (rs1 << 7) | 0x1)
} }
// rvcCBShift encodes a CB-type shift/immediate compressed instruction
// (C.SRLI, C.SRAI, C.ANDI). rd is the 3-bit prime-register index; imm is
// the 6-bit shamt/immediate; funct2 selects the operation (0=SRLI, 1=SRAI,
// 2=ANDI).
func rvcCBShift(funct2, rd, imm uint32) uint16 {
return uint16((0x4 << 13) | ((imm>>5)&1)<<12 | (funct2 << 10) | (rd << 7) | (imm&0x1F)<<2 | 0x1)
}
// rvcADDI16SP encodes C.ADDI16SP: ADDI rd, imm, rd for the stack pointer
// with a 10-bit signed, 16-byte-scaled immediate. imm is the raw byte
// offset; the immediate bits are extracted in the order [9|4|6|8:7|5].
func rvcADDI16SP(rd uint32, imm int32) uint16 {
u := uint32(imm)
packed := uint32(0)
for _, bit := range []uint{9, 4, 6, 8, 7, 5} {
packed = packed<<1 | (u>>bit)&1
}
return uint16((0x3 << 13) | ((packed>>5)&1)<<12 | (rd << 7) | (packed&0x1F)<<2 | 0x1)
}
+23 -35
View File
@@ -90,10 +90,10 @@ TEXT ·mem(SB), NOSPLIT, $0
func TestRISCV_immediate(t *testing.T) { func TestRISCV_immediate(t *testing.T) {
fn := firstTextRISCV(t, `#include "textflag.h" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·imm(SB), NOSPLIT, $0 TEXT ·imm(SB), NOSPLIT, $0
ADDI X10, $42, X11 ADDI $42, X10, X11
ANDI X11, $0xFF, X12 ANDI $0xFF, X11, X12
ORI X12, $1, X13 ORI $1, X12, X13
XORI X13, $0, X14 XORI $0, X13, X14
RET RET
`) `)
code := assembleRISCVHelper(t, fn) code := assembleRISCVHelper(t, fn)
@@ -106,10 +106,10 @@ TEXT ·imm(SB), NOSPLIT, $0
func TestRISCV_branches(t *testing.T) { func TestRISCV_branches(t *testing.T) {
fn := firstTextRISCV(t, `#include "textflag.h" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·br(SB), NOSPLIT, $0 TEXT ·br(SB), NOSPLIT, $0
ADDI X10, $1, X10 ADDI $1, X10, X10
loop: loop:
BEQ X10, X11, done BEQ X10, X11, done
ADDI X10, $1, X10 ADDI $1, X10, X10
JMP loop JMP loop
done: done:
RET RET
@@ -291,9 +291,9 @@ func TestRISCV_forwardBranch(t *testing.T) {
// Forward label reference — must not fail. // Forward label reference — must not fail.
fn := firstTextRISCV(t, `#include "textflag.h" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·fwd(SB), NOSPLIT, $0 TEXT ·fwd(SB), NOSPLIT, $0
ADDI X10, $1, X10 ADDI $1, X10, X10
BEQ X10, X11, done BEQ X10, X11, done
ADDI X10, $1, X10 ADDI $1, X10, X10
done: done:
RET RET
`) `)
@@ -308,7 +308,7 @@ func TestRISCV_RVC_ADDI(t *testing.T) {
// ADDI where rd=rs1 and small imm → C.ADDI // ADDI where rd=rs1 and small imm → C.ADDI
fn := firstTextRISCV(t, `#include "textflag.h" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·caddi(SB), NOSPLIT, $0 TEXT ·caddi(SB), NOSPLIT, $0
ADDI X10, $5, X10 ADDI $5, X10, X10
RET RET
`) `)
code := assembleRISCVHelper(t, fn) code := assembleRISCVHelper(t, fn)
@@ -322,7 +322,7 @@ func TestRISCV_RVC_LI(t *testing.T) {
// ADDI X0, $imm, rd → C.LI // ADDI X0, $imm, rd → C.LI
fn := firstTextRISCV(t, `#include "textflag.h" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·cli(SB), NOSPLIT, $0 TEXT ·cli(SB), NOSPLIT, $0
ADDI X0, $7, X10 ADDI $7, X0, X10
RET RET
`) `)
code := assembleRISCVHelper(t, fn) code := assembleRISCVHelper(t, fn)
@@ -389,10 +389,10 @@ func TestRISCV_encodings(t *testing.T) {
{"MUL", "MUL X10, X11, X12\nRET\n", 8}, {"MUL", "MUL X10, X11, X12\nRET\n", 8},
{"DIVW", "DIVW X10, X11, X12\nRET\n", 8}, {"DIVW", "DIVW X10, X11, X12\nRET\n", 8},
{"REMUW", "REMUW X10, X11, X12\nRET\n", 8}, {"REMUW", "REMUW X10, X11, X12\nRET\n", 8},
{"ADDIW", "ADDIW X10, $5, X11\nRET\n", 8}, {"ADDIW", "ADDIW $5, X10, X11\nRET\n", 8},
{"SLLI", "SLLI X10, $3, X11\nRET\n", 8}, // ADDI+SLLI? No, SLLI uses I-type {"SLLI", "SLLI $3, X10, X11\nRET\n", 8}, // ADDI+SLLI? No, SLLI uses I-type
{"SRLI", "SRLI X10, $2, X11\nRET\n", 8}, {"SRLI", "SRLI $2, X10, X11\nRET\n", 8},
{"SRAI", "SRAI X10, $1, X11\nRET\n", 8}, {"SRAI", "SRAI $1, X10, X11\nRET\n", 8},
{"LB", "LB (X10), X11\nRET\n", 8}, {"LB", "LB (X10), X11\nRET\n", 8},
{"LBU", "LBU (X10), X11\nRET\n", 8}, {"LBU", "LBU (X10), X11\nRET\n", 8},
{"LH", "LH (X10), X11\nRET\n", 8}, {"LH", "LH (X10), X11\nRET\n", 8},
@@ -431,9 +431,9 @@ func TestRISCV_RVC_branch(t *testing.T) {
// BEQ rs, X0, target → C.BEQZ when rs is in prime regs and offset fits. // BEQ rs, X0, target → C.BEQZ when rs is in prime regs and offset fits.
fn := firstTextRISCV(t, `#include "textflag.h" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·cbeqz(SB), NOSPLIT, $0 TEXT ·cbeqz(SB), NOSPLIT, $0
ADDI X10, $1, X10 ADDI $1, X10, X10
BEQ X10, X0, done BEQ X10, X0, done
ADDI X10, $1, X10 ADDI $1, X10, X10
done: done:
RET RET
`) `)
@@ -488,28 +488,16 @@ func TestRISCV_RVC_CADD_commute(t *testing.T) {
} }
func TestRISCV_RVC_CSUB(t *testing.T) { func TestRISCV_RVC_CSUB(t *testing.T) {
// SUB where rd==rs1 and both in prime regs → C.SUB. // SUB rs2, rs1, rd → C.SUB when rd == rs1 and both in prime regs.
fn := firstTextRISCV(t, `#include "textflag.h" fn := firstTextRISCV(t, `#include "textflag.h"
TEXT ·csub(SB), NOSPLIT, $0 TEXT ·csub(SB), NOSPLIT, $0
SUB X11, X10, X10 SUB X11, X10, X10
RET RET
`) `)
code := assembleRISCVHelper(t, fn) code := assembleRISCVHelper(t, fn)
// SUB X11,X10,X10 → rd=X10, rs1=X11 ≠ rd → no C.SUB. // SUB X11, X10, X10 → rs2=X11, rs1=X10, rd=X10; rd==rs1 → C.SUB (2B) + JALR (4B) = 6.
// Plan9: INSTR src1, src2, dst. For C.SUB: rd must equal rs1. if len(code) != 6 {
// So: SUB X10, X11, X10 → rd=10, rs1=10, rs2=11 ✓ t.Errorf("expected 6 bytes with C.SUB, got %d (% x)", len(code), code)
if len(code) == 4 {
return // compressed
}
// Try with correct operand order.
fn2 := firstTextRISCV(t, `#include "textflag.h"
TEXT ·csub2(SB), NOSPLIT, $0
SUB X10, X11, X10
RET
`)
code2 := assembleRISCVHelper(t, fn2)
if len(code2) != 6 {
t.Errorf("expected 6 bytes with C.SUB, got %d (% x)", len(code2), code2)
} }
} }
@@ -712,9 +700,9 @@ TEXT ·sys(SB), NOSPLIT, $0
RET RET
`) `)
code := assembleRISCVHelper(t, fn) code := assembleRISCVHelper(t, fn)
// 3 system instructions × 4 bytes + JALR(4) = 16 // FENCE(4) + ECALL(4) + C.EBREAK(2) + JALR(4) = 14
if len(code) != 16 { if len(code) != 14 {
t.Errorf("expected 16 bytes, got %d (% x)", len(code), code) t.Errorf("expected 14 bytes, got %d (% x)", len(code), code)
} }
} }
+9 -2
View File
@@ -112,9 +112,13 @@ func riscvReturn(fi riscvFrameInfo) []byte {
} }
// riscvSPAdjust emits an ADDI rd, imm, rd for the stack pointer (rd = rs1 = // riscvSPAdjust emits an ADDI rd, imm, rd for the stack pointer (rd = rs1 =
// X2), compressed to C.ADDI when the immediate is a nonzero 6-bit signed // X2), compressed to C.ADDI16SP when the immediate is a nonzero 16-byte
// value. // multiple, else C.ADDI when it fits 6-bit signed.
func riscvSPAdjust(imm int32) []byte { func riscvSPAdjust(imm int32) []byte {
if imm != 0 && imm%16 == 0 && imm >= -512 && imm <= 511 {
c := rvcADDI16SP(2, imm)
return []byte{byte(c), byte(c >> 8)}
}
if riscvFitsCAddi(imm) { if riscvFitsCAddi(imm) {
c := rvcCI(0x0, 2, uint32(imm)&0x3F) c := rvcCI(0x0, 2, uint32(imm)&0x3F)
return []byte{byte(c), byte(c >> 8)} return []byte{byte(c), byte(c >> 8)}
@@ -149,6 +153,9 @@ func riscvReturnEpilogueLen(fi riscvFrameInfo) int {
} }
func riscvSPAdjustLen(imm int32) int { func riscvSPAdjustLen(imm int32) int {
if imm != 0 && imm%16 == 0 && imm >= -512 && imm <= 511 {
return 2
}
if riscvFitsCAddi(imm) { if riscvFitsCAddi(imm) {
return 2 return 2
} }
+32
View File
@@ -0,0 +1,32 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
TEXT ·shifts(SB), NOSPLIT, $0
SLLI $3, X10, X10
SRLI $2, X10, X10
SRAI $1, X10, X10
RET
TEXT ·logic(SB), NOSPLIT, $0
AND X11, X10, X10
OR X11, X10, X10
XOR X11, X10, X10
ANDI $7, X10, X10
RET
TEXT ·mv(SB), NOSPLIT, $0
ADDI $0, X11, X10
RET
TEXT ·nop(SB), NOSPLIT, $0
ADDI $0, X0
RET
TEXT ·bigframe(SB), NOSPLIT, $24-0
RET
TEXT ·ebreak(SB), NOSPLIT, $0
EBREAK
RET
+15 -2
View File
@@ -7,6 +7,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"strings"
"testing" "testing"
"sourcedock.dev/petrbalvin/gasm-devkit/asm" "sourcedock.dev/petrbalvin/gasm-devkit/asm"
@@ -81,11 +82,23 @@ func diffHex(a, b []byte) string {
} }
for i := 0; i < n; i += 4 { for i := 0; i < n; i += 4 {
ab, bb := "??", "??" ab, bb := "??", "??"
if i < len(a) { if i+4 <= len(a) {
ab = fmt.Sprintf("%02x%02x%02x%02x", a[i], a[i+1], a[i+2], a[i+3]) ab = fmt.Sprintf("%02x%02x%02x%02x", a[i], a[i+1], a[i+2], a[i+3])
} else if i < len(a) {
var sb strings.Builder
for j := i; j < len(a); j++ {
fmt.Fprintf(&sb, "%02x", a[j])
}
ab = sb.String()
} }
if i < len(b) { if i+4 <= len(b) {
bb = fmt.Sprintf("%02x%02x%02x%02x", b[i], b[i+1], b[i+2], b[i+3]) bb = fmt.Sprintf("%02x%02x%02x%02x", b[i], b[i+1], b[i+2], b[i+3])
} else if i < len(b) {
var sb strings.Builder
for j := i; j < len(b); j++ {
fmt.Fprintf(&sb, "%02x", b[j])
}
bb = sb.String()
} }
mark := " " mark := " "
if ab != bb { if ab != bb {
+11 -1
View File
@@ -17,7 +17,17 @@ import (
// relocation fields of static-symbol references are masked before the // relocation fields of static-symbol references are masked before the
// comparison, since the toolchain leaves them zero for the linker. // comparison, since the toolchain leaves them zero for the linker.
func TestGroundTruthRISCV(t *testing.T) { func TestGroundTruthRISCV(t *testing.T) {
path := "../testdata/verify/basic_riscv64.s" for _, path := range []string{
"../testdata/verify/basic_riscv64.s",
"../testdata/verify/rvc_riscv64.s",
} {
t.Run(path, func(t *testing.T) {
testGroundTruthRISCVFile(t, path)
})
}
}
func testGroundTruthRISCVFile(t *testing.T, path string) {
src, err := os.ReadFile(path) src, err := os.ReadFile(path)
if err != nil { if err != nil {
t.Fatalf("read: %v", err) t.Fatalf("read: %v", err)