feat(asm): encode the explicit riscv64 compressed instructions

The C extension's own spellings were names the table carried and the
encoder refused: CLWSP stopped the corpus audit's riscv64 file first.
Thirty-eight mnemonics now encode directly to their halfword, with the
toolchain's operand spellings and validation: the stack loads and stores
pin their base to SP, the register-based loads, stores and arithmetic
carry prime registers, CLUI refuses zero and SP, CADDI4SPN scales by
four, CADDI16SP by sixteen, and CJ, CBEQZ and CBNEZ resolve their N(PC)
targets against the final layout, taking a two-byte placeholder in the
early passes so the offsets stay honest.  CAND with an immediate is the
toolchain's C.ANDI spelling.  The toolchain's whole C extension testdata
block is a differential test, halfword for halfword, beside a range test
at the toolchain's own boundaries.

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 00:47:27 +02:00
1 parent ddfa33ccc1
commit e43fa39dc9
2 files changed
+716 -8

No files matched your search

+574 -8
View File
@@ -69,10 +69,10 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
} }
// Pass 2: encode each instruction using Pass-1 offsets. A branch or // Pass 2: encode each instruction using Pass-1 offsets. A branch or
// jump the offsets prove overlong encodes to a 4-byte placeholder: the // jump the offsets prove overlong encodes to a placeholder of the
// relaxation pass rewrites it before the final encoding. pcRelPcs is // instruction's own size: the relaxation pass rewrites it before the
// unavailable this early, so the N(PC) forms take the same placeholder // final encoding. pcRelPcs is unavailable this early, so the N(PC)
// path. // forms take the same placeholder path.
pc := len(prologue) pc := len(prologue)
for i := range recs { for i := range recs {
branchLike := isBranchLike(recs[i].instr.Mnemonic.Text) || riscvIsCondBranch(recs[i].instr.Mnemonic.Text) branchLike := isBranchLike(recs[i].instr.Mnemonic.Text) || riscvIsCondBranch(recs[i].instr.Mnemonic.Text)
@@ -81,7 +81,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err) return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
} }
if err != nil { if err != nil {
code = make([]byte, 4) code = make([]byte, riscvInstrSize(recs[i].instr, fi))
} }
recs[i].code = code recs[i].code = code
pc += len(code) pc += len(code)
@@ -460,6 +460,9 @@ func riscvExtendedSize(mnem string, ops []*ast.Operand) int {
case "RORIW": case "RORIW":
return 12 return 12
} }
if isRVCInstr(mnem) {
return 2
}
return 4 return 4
} }
@@ -511,7 +514,8 @@ func riscvRevShiftSize(mnem string, ops []*ast.Operand) int {
// recalculated offsets after compression. // recalculated offsets after compression.
func isBranchLike(mnem string) bool { func isBranchLike(mnem string) bool {
switch mnem { switch mnem {
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "JMP", "JAL": case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "JMP", "JAL",
"CJ", "CBEQZ", "CBNEZ":
return true return true
} }
return false return false
@@ -895,6 +899,14 @@ func riscvPCRelOffset(instr *ast.Instr) (int, bool) {
if len(instr.Operands) < 2 { if len(instr.Operands) < 2 {
return 0, false return 0, false
} }
case "CJ":
if len(instr.Operands) != 1 {
return 0, false
}
case "CBEQZ", "CBNEZ":
if len(instr.Operands) != 2 {
return 0, false
}
default: default:
return 0, false return 0, false
} }
@@ -1426,7 +1438,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
// The toolchain's synthesised instructions and the RVV slice: expanded // The toolchain's synthesised instructions and the RVV slice: expanded
// encodings the main table does not carry. FSGNJD is a plain table // encodings the main table does not carry. FSGNJD is a plain table
// entry and stays with the FP arithmetic path. // entry and stays with the FP arithmetic path.
if code, handled, err := encodeRISCVExtended(mnem, instr, pc, offsets); handled { if code, handled, err := encodeRISCVExtended(mnem, instr, pc, offsets, pcRelPcs); handled {
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -2693,8 +2705,13 @@ func extractITypeParams(instr *ast.Instr) (rd, rs1 int, imm int32) {
// this group; err carries the diagnostic when it does but cannot be encoded. // this group; err carries the diagnostic when it does but cannot be encoded.
// Each expansion reproduces the toolchain's instruction-for-instruction // Each expansion reproduces the toolchain's instruction-for-instruction
// sequence, including its use of X31 (TMP) and its RVC compression. // sequence, including its use of X31 (TMP) and its RVC compression.
func encodeRISCVExtended(mnem string, instr *ast.Instr, pc int, offsets map[string]int) ([]byte, bool, error) { func encodeRISCVExtended(mnem string, instr *ast.Instr, pc int, offsets map[string]int, pcRelPcs map[*ast.Instr]int) ([]byte, bool, error) {
ops := instr.Operands ops := instr.Operands
switch {
case isRVCInstr(mnem):
code, err := encodeRISCVCompressed(mnem, instr, pc, offsets, pcRelPcs)
return code, true, err
}
switch mnem { switch mnem {
case "NOP": case "NOP":
if len(ops) != 0 { if len(ops) != 0 {
@@ -2987,6 +3004,555 @@ func riscvFoldedMove(rd, rs int) []byte {
} }
} }
// isRVCInstr reports whether m is one of the explicit compressed-instruction
// mnemonics: the toolchain's own spellings, encoded directly rather than
// reached by compressing a 32-bit form.
func isRVCInstr(m string) bool {
switch m {
case "CLWSP", "CLDSP", "CFLDSP", "CSWSP", "CSDSP", "CFSDSP",
"CLW", "CLD", "CFLD", "CSW", "CSD", "CFSD",
"CJ", "CJR", "CJALR", "CBEQZ", "CBNEZ",
"CLI", "CLUI", "CADD", "CADDI", "CADDW", "CADDIW",
"CADDI16SP", "CADDI4SPN",
"CSLLI", "CSRLI", "CSRAI", "CANDI",
"CMV", "CAND", "COR", "CXOR", "CSUB", "CSUBW",
"CNOP", "CEBREAK":
return true
}
return false
}
// encodeRISCVCompressed encodes one explicit RVC mnemonic to its 16-bit
// halfword, with the toolchain's operand spellings and its validation:
// stack-relative loads and stores pin their base to SP, the register-based
// ones and the CA arithmetic to the prime registers x8-x15, and every
// immediate carries its instruction's own range and scale.
func encodeRISCVCompressed(mnem string, instr *ast.Instr, pc int, offsets map[string]int, pcRelPcs map[*ast.Instr]int) ([]byte, error) {
ops := instr.Operands
immOf := func(op *ast.Operand) (int64, error) {
v, ok := riscvRawImm(op)
if !ok {
return 0, fmt.Errorf("%s expects an immediate", mnem)
}
return v, nil
}
// stackMem accepts a bare offset(SP) reference: the explicit compressed
// stack instructions pin their base to the hardware SP, so a frame
// reference (name+off(SP)) is not one.
stackMem := func(op *ast.Operand) (int64, bool) {
if op.Addr.Sym != nil || op.Addr.Base != "SP" {
return 0, false
}
return op.Addr.Offset, true
}
// regMem accepts a bare offset(rs) reference with a prime base register.
regMem := func(op *ast.Operand) (int, int64, bool) {
if op.Addr.Sym != nil || op.Addr.Base == "" {
return 0, 0, false
}
rs1 := riscvRegNum(op.Addr.Base)
return rs1, op.Addr.Offset, true
}
prime := func(r int) bool { return r >= 8 && r <= 15 }
branchTarget := func(op *ast.Operand) (int, error) {
if op.Addr.Sym == nil && op.Addr.Base == "PC" {
n := int(op.Addr.Offset)
// The target lands in the final layout; pass 2 encodes ahead of
// it with a placeholder, so the missing map is a range error like
// any unresolved branch.
if pcRelPcs == nil {
return 0, &riscvRangeError{fmt.Sprintf("%s: PC-relative target %d out of range", mnem, n)}
}
target, ok := pcRelPcs[instr]
if !ok {
return 0, &riscvRangeError{fmt.Sprintf("%s: PC-relative target %d out of range", mnem, n)}
}
return target - pc, nil
}
target := labelFromOperand(op)
off, ok := offsets[target]
if !ok {
return 0, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
}
return off - pc, nil
}
switch {
// Compressed stack-pointer-based loads and stores: offset(SP), rd.
case mnem == "CLWSP" || mnem == "CLDSP" || mnem == "CFLDSP":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
off, ok := stackMem(ops[0])
if !ok {
return nil, fmt.Errorf("%s: rs2 must be SP/X2", mnem)
}
rd := regFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd position", mnem)
}
scale, hi := int64(4), int64(255)
funct3 := uint32(0x2)
if mnem != "CLWSP" {
scale, hi, funct3 = 8, 511, 0x3
}
if mnem == "CFLDSP" {
funct3 = 0x1
}
if off < 0 || off > hi {
return nil, fmt.Errorf("%s: offset %d must be in range [0, %d]", mnem, off, hi)
}
if off%scale != 0 {
return nil, fmt.Errorf("%s: offset %d must be a multiple of %d", mnem, off, scale)
}
return word16(rvcLSP(funct3, uint32(rd), uint32(off))), nil
case mnem == "CSWSP" || mnem == "CSDSP" || mnem == "CFSDSP":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
rs2 := regFromOperand(ops[0])
off, ok := stackMem(ops[1])
if !ok {
return nil, fmt.Errorf("%s: rd must be SP/X2", mnem)
}
if rs2 < 0 {
return nil, fmt.Errorf("%s: expected integer register in rs2 position", mnem)
}
scale, hi, funct3 := int64(4), int64(255), uint32(0x6)
if mnem != "CSWSP" {
scale, hi, funct3 = 8, 511, 0x7
}
if mnem == "CFSDSP" {
funct3 = 0x5
}
if off < 0 || off > hi {
return nil, fmt.Errorf("%s: offset %d must be in range [0, %d]", mnem, off, hi)
}
if off%scale != 0 {
return nil, fmt.Errorf("%s: offset %d must be a multiple of %d", mnem, off, scale)
}
return word16(rvcSSP(funct3, uint32(rs2), uint32(off))), nil
// Compressed register-based loads and stores: offset(rs), rd, all prime.
case mnem == "CLW" || mnem == "CLD" || mnem == "CFLD":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
rs1, off, ok := regMem(ops[0])
if !ok || !prime(rs1) {
return nil, fmt.Errorf("%s: expected integer prime register in rs1 position", mnem)
}
rd := regFromOperand(ops[1])
if rd < 0 || !prime(rd) {
return nil, fmt.Errorf("%s: expected integer prime register in rd position", mnem)
}
scale, hi, funct3 := int64(4), int64(127), uint32(0x2)
if mnem != "CLW" {
scale, hi, funct3 = 8, 255, 0x3
}
if mnem == "CFLD" {
funct3 = 0x1
}
if off < 0 || off > hi {
return nil, fmt.Errorf("%s: offset %d must be in range [0, %d]", mnem, off, hi)
}
if off%scale != 0 {
return nil, fmt.Errorf("%s: offset %d must be a multiple of %d", mnem, off, scale)
}
return word16(rvcCL(funct3, uint32(rvcReg3(rd)), uint32(rvcReg3(rs1)), uint32(off))), nil
case mnem == "CSW" || mnem == "CSD" || mnem == "CFSD":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
rs2 := regFromOperand(ops[0])
rs1, off, ok := regMem(ops[1])
if !ok || !prime(rs1) {
return nil, fmt.Errorf("%s: expected integer prime register in rs1 position", mnem)
}
if rs2 < 0 || !prime(rs2) {
return nil, fmt.Errorf("%s: expected integer prime register in rs2 position", mnem)
}
scale, hi, funct3 := int64(4), int64(127), uint32(0x6)
if mnem != "CSW" {
scale, hi, funct3 = 8, 255, 0x7
}
if mnem == "CFSD" {
funct3 = 0x5
}
if off < 0 || off > hi {
return nil, fmt.Errorf("%s: offset %d must be in range [0, %d]", mnem, off, hi)
}
if off%scale != 0 {
return nil, fmt.Errorf("%s: offset %d must be a multiple of %d", mnem, off, scale)
}
return word16(rvcCS(funct3, uint32(rvcReg3(rs2)), uint32(rvcReg3(rs1)), uint32(off))), nil
// Compressed control transfer.
case mnem == "CJ" || mnem == "CBEQZ" || mnem == "CBNEZ":
if mnem == "CJ" && len(ops) != 1 {
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
}
if mnem != "CJ" && len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
rs1 := 0
if mnem != "CJ" {
rs1 = regFromOperand(ops[0])
if rs1 < 0 || !prime(rs1) {
return nil, fmt.Errorf("%s: expected integer prime register in rs1", mnem)
}
}
off, err := branchTarget(ops[len(ops)-1])
if err != nil {
return nil, err
}
hi, lo := 2046, -2048
if mnem != "CJ" {
hi, lo = 254, -256
}
if off > hi || off < lo || off%2 != 0 {
return nil, fmt.Errorf("%s: branch target %d out of range [%d, %d]", mnem, off, lo, hi)
}
if mnem == "CJ" {
return word16(rvcCJ(int32(off))), nil
}
funct3 := uint32(0x6)
if mnem == "CBNEZ" {
funct3 = 0x7
}
return word16(rvcCB(funct3, uint32(rvcReg3(rs1)), int32(off))), nil
case mnem == "CJR" || mnem == "CJALR":
if len(ops) != 1 {
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
}
rs1 := regFromOperand(ops[0])
if rs1 < 0 {
return nil, fmt.Errorf("%s: expected integer register in rs1", mnem)
}
if rs1 == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rs1", mnem)
}
funct4 := uint32(0x8)
if mnem == "CJALR" {
funct4 = 0x9
}
return word16(rvcCR(funct4, uint32(rs1), 0)), nil
// Compressed constant generation.
case mnem == "CLI":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
imm, err := immOf(ops[0])
if err != nil {
return nil, err
}
if imm < -32 || imm > 31 {
return nil, fmt.Errorf("%s: immediate %d must be in range [-32, 31]", mnem, imm)
}
rd := regFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd position", mnem)
}
if rd == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rd", mnem)
}
return word16(rvcCI(0x2, uint32(rd), uint32(imm)&0x3F)), nil
case mnem == "CLUI":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
imm, err := immOf(ops[0])
if err != nil {
return nil, err
}
if imm == 0 {
return nil, fmt.Errorf("%s: immediate cannot be zero", mnem)
}
if imm < -32 || imm > 31 {
return nil, fmt.Errorf("%s: immediate %d must be in range [-32, 31]", mnem, imm)
}
rd := regFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd position", mnem)
}
if rd == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rd", mnem)
}
if rd == 2 {
return nil, fmt.Errorf("%s: cannot use register SP/X2 in rd", mnem)
}
return word16(rvcCI(0x3, uint32(rd), uint32(imm)&0x3F)), nil
// Compressed integer register-immediate operations.
case (mnem == "CADD" || mnem == "CADDI") && len(ops) >= 1 && isImmOperand(ops[0]),
(mnem == "CADDW" || mnem == "CADDIW") && len(ops) >= 1 && isImmOperand(ops[0]):
if len(ops) != 2 && len(ops) != 3 {
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
imm, err := immOf(ops[0])
if err != nil {
return nil, err
}
if imm < -32 || imm > 31 {
return nil, fmt.Errorf("%s: immediate %d must be in range [-32, 31]", mnem, imm)
}
if (mnem == "CADD" || mnem == "CADDI") && imm == 0 {
return nil, fmt.Errorf("%s: immediate cannot be zero", mnem)
}
rd := regFromOperand(ops[1])
if len(ops) == 3 {
if rd != regFromOperand(ops[2]) {
return nil, fmt.Errorf("%s: rd must be the same as rs1", mnem)
}
}
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd position", mnem)
}
funct3 := uint32(0x0)
if mnem == "CADDW" || mnem == "CADDIW" {
funct3 = 0x1
}
return word16(rvcCI(funct3, uint32(rd), uint32(imm)&0x3F)), nil
case mnem == "CADDI16SP":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
imm, err := immOf(ops[0])
if err != nil {
return nil, err
}
if imm == 0 {
return nil, fmt.Errorf("%s: immediate cannot be zero", mnem)
}
if imm < -512 || imm > 511 {
return nil, fmt.Errorf("%s: immediate %d must be in range [-512, 511]", mnem, imm)
}
if imm%16 != 0 {
return nil, fmt.Errorf("%s: immediate %d must be a multiple of 16", mnem, imm)
}
rd := regFromOperand(ops[1])
if rd != 2 {
return nil, fmt.Errorf("%s: rd must be SP/X2", mnem)
}
return word16(rvcADDI16SP(2, int32(imm))), nil
case mnem == "CADDI4SPN":
if len(ops) != 3 {
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
}
imm, err := immOf(ops[0])
if err != nil {
return nil, err
}
if imm == 0 {
return nil, fmt.Errorf("%s: immediate cannot be zero", mnem)
}
if imm < 0 || imm > 1023 {
return nil, fmt.Errorf("%s: immediate %d must be in range [0, 1023]", mnem, imm)
}
if imm%4 != 0 {
return nil, fmt.Errorf("%s: immediate %d must be a multiple of 4", mnem, imm)
}
if regFromOperand(ops[1]) != 2 {
return nil, fmt.Errorf("%s: SP/X2 must be in rs1", mnem)
}
rd := regFromOperand(ops[2])
if rd < 0 || !prime(rd) {
return nil, fmt.Errorf("%s: expected integer prime register in rd", mnem)
}
return word16(rvcCIW(0x0, uint32(rvcReg3(rd)), uint32(imm))), nil
// Compressed shifts and the immediate C.ANDI: rd is the source too.
// CAND with an immediate first operand is the toolchain's C.ANDI
// spelling (CANDI $imm and CAND $imm encode identically).
case mnem == "CSLLI" || mnem == "CSRLI" || mnem == "CSRAI" || mnem == "CANDI",
mnem == "CAND" && len(ops) >= 1 && isImmOperand(ops[0]):
if mnem == "CAND" {
mnem = "CANDI"
}
if len(ops) != 2 && len(ops) != 3 {
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
imm, err := immOf(ops[0])
if err != nil {
return nil, err
}
if imm == 0 && mnem != "CANDI" {
return nil, fmt.Errorf("%s: immediate cannot be zero", mnem)
}
if mnem == "CANDI" {
if imm < -32 || imm > 31 {
return nil, fmt.Errorf("%s: immediate %d must be in range [-32, 31]", mnem, imm)
}
} else {
if imm < 0 || imm > 63 {
return nil, fmt.Errorf("%s: immediate %d must be in range [0, 63]", mnem, imm)
}
}
rd := regFromOperand(ops[1])
if len(ops) == 3 {
if rd != regFromOperand(ops[2]) {
return nil, fmt.Errorf("%s: rd must be the same as rs1", mnem)
}
}
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd position", mnem)
}
if mnem == "CSLLI" {
if rd == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rd", mnem)
}
return word16(rvcSLLI(uint32(rd), uint32(imm)&0x3F)), nil
}
if !prime(rd) {
return nil, fmt.Errorf("%s: expected integer prime register in rd", mnem)
}
funct2 := uint32(0x0)
switch mnem {
case "CSRAI":
funct2 = 0x1
case "CANDI":
funct2 = 0x2
}
return word16(rvcCBShift(funct2, uint32(rvcReg3(rd)), uint32(imm)&0x3F)), nil
// Compressed integer register-register operations: destination last.
case mnem == "CMV":
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
rs2 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rs2 < 0 {
return nil, fmt.Errorf("%s: expected integer register in rs2", mnem)
}
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd", mnem)
}
if rs2 == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rs2", mnem)
}
if rd == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rd", mnem)
}
return word16(rvcCR(0x8, uint32(rd), uint32(rs2))), nil
case mnem == "CADD" || mnem == "CAND" || mnem == "COR" || mnem == "CXOR" || mnem == "CSUB" || mnem == "CSUBW":
if len(ops) != 2 && len(ops) != 3 {
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
rs2 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if len(ops) == 3 {
if rd != regFromOperand(ops[2]) {
return nil, fmt.Errorf("%s: rd must be the same as rs1", mnem)
}
}
if rs2 < 0 {
return nil, fmt.Errorf("%s: expected integer register in rs2", mnem)
}
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd", mnem)
}
if rs2 == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rs2", mnem)
}
if rd == 0 {
return nil, fmt.Errorf("%s: cannot use register X0 in rd", mnem)
}
if mnem == "CADD" {
return word16(rvcCR(0x9, uint32(rd), uint32(rs2))), nil
}
if !prime(rd) || !prime(rs2) {
return nil, fmt.Errorf("%s: expected integer prime registers", mnem)
}
funct6 := uint32(0x23)
funct2 := uint32(0x0)
switch mnem {
case "CAND":
funct2 = 0x3
case "COR":
funct2 = 0x2
case "CXOR":
funct2 = 0x1
case "CSUBW":
funct6 = 0x27
}
return word16(rvcCA(funct6, funct2, uint32(rvcReg3(rd)), uint32(rvcReg3(rs2)))), nil
case mnem == "CADDW":
if len(ops) < 1 {
return nil, fmt.Errorf("%s expects operands", mnem)
}
if isImmOperand(ops[0]) {
if len(ops) != 2 && len(ops) != 3 {
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
imm, err := immOf(ops[0])
if err != nil {
return nil, err
}
if imm < -32 || imm > 31 {
return nil, fmt.Errorf("%s: immediate %d must be in range [-32, 31]", mnem, imm)
}
rd := regFromOperand(ops[1])
if len(ops) == 3 && rd != regFromOperand(ops[2]) {
return nil, fmt.Errorf("%s: rd must be the same as rs1", mnem)
}
if rd < 0 {
return nil, fmt.Errorf("%s: expected integer register in rd position", mnem)
}
return word16(rvcCI(0x1, uint32(rd), uint32(imm)&0x3F)), nil
}
if len(ops) != 2 && len(ops) != 3 {
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
rs2 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if len(ops) == 3 && rd != regFromOperand(ops[2]) {
return nil, fmt.Errorf("%s: rd must be the same as rs1", mnem)
}
if rs2 < 0 || rd < 0 || !prime(rs2) || !prime(rd) {
return nil, fmt.Errorf("%s: expected integer prime registers", mnem)
}
return word16(rvcCA(0x27, 0x1, uint32(rvcReg3(rd)), uint32(rvcReg3(rs2)))), nil
case mnem == "CNOP":
if len(ops) != 0 {
return nil, fmt.Errorf("%s expects no operands", mnem)
}
return word16(0x0001), nil
case mnem == "CEBREAK":
if len(ops) != 0 {
return nil, fmt.Errorf("%s expects no operands", mnem)
}
return word16(0x9002), nil
}
return nil, fmt.Errorf("unsupported RISC-V instruction %q", mnem)
}
// rvcCJ encodes a CJ-type compressed jump: the 11-bit displacement in the
// order [11|4|9:8|10|6|7|3:1|5], funct3 5, op 01.
func rvcCJ(off int32) uint16 {
packed := encodeRVCPattern(uint32(off), []int{11, 4, 9, 8, 10, 6, 7, 3, 2, 1, 5})
return uint16((0x5 << 13) | packed<<2 | 0x1)
}
// rvcCB encodes a CB-type compressed branch: the 8-bit displacement in the
// order [8|4:3|7:6|2:1|5], funct3 6 (C.BEQZ) or 7 (C.BNEZ), op 01.
func rvcCB(funct3, rs1 uint32, off int32) uint16 {
packed := encodeRVCPattern(uint32(off), []int{8, 4, 3, 7, 6, 2, 1, 5})
return uint16((funct3 << 13) | ((packed>>5)&0x7)<<10 | rs1<<7 | (packed&0x1F)<<2 | 0x1)
}
// encodeRISCVVector encodes the RVV slice GOROOT's kernels use. Registers // encodeRISCVVector encodes the RVV slice GOROOT's kernels use. Registers
// are accepted in either spelling: the vector V registers and the integer // are accepted in either spelling: the vector V registers and the integer
// registers share their 5-bit numbers, and the superset keeps hand-written // registers share their 5-bit numbers, and the superset keeps hand-written
+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"os"
"path/filepath"
"testing"
)
// TestRISCVCompressedDifferential proves the explicit compressed-instruction
// mnemonics against the toolchain: the "C" extension block of the toolchain's
// own testdata (every stack, register, control-transfer, constant-generation,
// shift and register-register spelling it carries) assembled by gasm and by
// go tool asm must agree halfword for halfword. The lines are the oracle's
// own, so a wrong bit pattern, scale or register field names itself through
// the first differing halfword.
func TestRISCVCompressedDifferential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·compressed(SB), NOSPLIT, $0
CLWSP 20(SP), X10
CLDSP 24(SP), X10
CFLDSP 32(SP), F10
CSWSP X10, 20(SP)
CSDSP X10, 24(SP)
CFSDSP F10, 32(SP)
CLW 20(X10), X11
CLD 24(X10), X11
CFLD 32(X10), F11
CSW X11, 20(X10)
CSD X11, 24(X10)
CFSD F11, 32(X10)
CJ 1(PC)
CJR X5
CJALR X5
CBEQZ X10, 1(PC)
CBNEZ X10, 1(PC)
CLI $-32, X5
CLI $31, X5
CLUI $-32, X5
CLUI $31, X5
CADD $-32, X5
CADD $31, X5
CADDI $-32, X5
CADDI $31, X5
CADDW $-32, X5
CADDW $31, X5
CADDIW $-32, X5
CADDIW $31, X5
CADDI16SP $-512, SP
CADDI16SP $496, SP
CADDI4SPN $4, SP, X10
CADDI4SPN $1020, SP, X10
CSLLI $63, X5
CSRLI $63, X10
CSRAI $63, X10
CAND $-32, X10
CAND $31, X10
CANDI $-32, X10
CANDI $31, X10
CMV X6, X5
CADD X9, X8
CAND X9, X8
COR X9, X8
CXOR X9, X8
CSUB X9, X8
CADDW X9, X8
CSUBW X9, X8
CNOP
CEBREAK
RET
`
dir := t.TempDir()
path := filepath.Join(dir, "compressed_riscv64.s")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
assertRISCVDifferential(t, path, src, "compressed")
}
// TestRISCVCompressedRange pins the compressed immediate and offset ranges at
// the toolchain's own boundaries: a stack load off the scale or range, a
// non-prime register in the CL/CS and CA shapes, a zero immediate where the
// toolchain forbids one and a CLUI into SP are all rejected on sight.
func TestRISCVCompressedRange(t *testing.T) {
asmOne := func(t *testing.T, stmt string) error {
t.Helper()
fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·c(SB), NOSPLIT, $0\n\t"+stmt+"\n\tRET\n")
_, _, _, _, _, _, err := assembleRISCV(fn)
return err
}
for _, s := range []string{
"CLWSP $0(SP), X10", // never spelled; the parser rejects the shape
} {
if err := asmOne(t, s); err == nil {
t.Errorf("%s must be rejected", s)
}
}
for _, s := range []string{
"CLWSP 21(SP), X10", // not a multiple of 4
"CLWSP 256(SP), X10", // out of range
"CLDSP 25(SP), X10", // not a multiple of 8
"CFLDSP 33(SP), F10", // not a multiple of 8
"CLWSP 20(X10), X10", // base must be SP
"CLW 22(X10), X11", // not a multiple of 4
"CLW 128(X10), X11", // out of range
"CLW 20(X5), X11", // base must be prime
"CLW 20(X10), X5", // rd must be prime
"CLI $32, X5", // out of range
"CLI $-33, X5", // out of range
"CLUI $0, X5", // zero
"CLUI $3, X2", // SP as destination
"CSLLI $0, X5", // zero shift
"CSLLI $64, X5", // out of range
"CSRLI $63, X5", // rd must be prime
"CANDI $63, X10", // out of range
"CMV X0, X5", // X0 in rd
"CMV X5, X0", // X0 in rs2
"CADD X5, X0", // X0 in rs2
"CSUB X5, X5", // X0-free but rd prime required
"CADDI4SPN $4, X5, X10", /* base must be SP */
} {
if err := asmOne(t, s); err == nil {
t.Errorf("%s must be rejected, as go tool asm rejects it", s)
}
}
for _, s := range []string{
"CLWSP 20(SP), X10", "CLDSP 24(SP), X10",
"CLW 20(X10), X11", "CSD X11, 24(X10)",
"CLI $-32, X5", "CLUI $-32, X5",
"CADD $-32, X5", "CADDIW $31, X5",
"CSLLI $63, X5", "CSRLI $63, X10", "CANDI $-32, X10",
"CMV X6, X5", "CADD X9, X8", "CSUB X9, X8", "CADDW X9, X8",
"CADDI16SP $496, SP", "CADDI4SPN $1020, SP, X10",
} {
if err := asmOne(t, s); err != nil {
t.Errorf("%s must assemble: %v", s, err)
}
}
}