feat(asm): encode the riscv64 bit-manipulation families

The Zba address generation, Zbb unary bit operations, Zbc carry-less
multiplication and Zbs single-bit families were names the table carried
and the encoder refused: thirty spellings plus RORI and XNOR fell over.
The register and immediate forms now encode as the toolchain does, the
unary operations carry their fixed rs2 constant, RORI lowers to ROR's
expansion (its reverse shift compressing like ROR's), XNOR XORs and
inverts in place, and ROL/ROLW rotate left through the same temporary
the toolchain uses, taking a register amount only as its own expansion
requires.  The toolchain's whole testdata block for these families is
now a differential test: every word must agree byte for byte.

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 00:47:27 +02:00
1 parent d82ef33fa9
commit ddfa33ccc1
3 files changed
+266 -19

No files matched your search

+123 -19
View File
@@ -437,7 +437,7 @@ func riscvExtendedSize(mnem string, ops []*ast.Operand) int {
case "NOP":
// The toolchain drops a bare NOP entirely.
return 0
case "ANDN", "ORN":
case "ANDN", "ORN", "XNOR":
return 8
case "MAX", "MAXU", "MIN", "MINU":
if riscvIdenticalMinMax(mnem, ops) {
@@ -451,7 +451,7 @@ func riscvExtendedSize(mnem string, ops []*ast.Operand) int {
return 4
}
return 20
case "ROR", "RORW":
case "ROL", "ROLW", "ROR", "RORI", "RORW":
if len(ops) >= 1 && isImmOperand(ops[0]) {
// SRL + [compressed] SLL of the reverse shift + OR.
return 4 + riscvRevShiftSize(mnem, ops) + 4
@@ -487,11 +487,11 @@ func riscvIdenticalMinMax(mnem string, ops []*ast.Operand) bool {
}
// riscvRevShiftSize returns the size of the reverse-shift instruction inside
// a ROR/RORW immediate expansion: the SLLI of the complementary amount, which
// a ROR/RORI immediate expansion: the SLLI of the complementary amount, which
// compresses to C.SLLI only in the 64-bit form when rd == rs1, both non-zero,
// and the amount lands in 1-63. The W forms have no compressed shift.
func riscvRevShiftSize(mnem string, ops []*ast.Operand) int {
if mnem != "ROR" {
if mnem != "ROR" && mnem != "RORI" {
return 4 // SLLIW has no compressed form
}
imm := int(immFromOperand(ops[0]))
@@ -1501,6 +1501,18 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
}
word = riscvAMOType(enc, rd, rs1, rs2)
// Zbb unary bit operations: INSTR rs, rd, exactly two operands as the
// toolchain spells them. The rs2 field is fixed, not zero: the table
// below carries the constant each operation reads (CLZ counts leading
// zeros with an empty field, REV8 works on bytes at position 24).
case len(ops) == 2 && isZbUnaryInstr(mnem):
rs1 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvRType(enc, rd, rs1, riscvZbUnaryRS2[mnem])
// FP arithmetic: Go reverses the ISA order, writing rs2, rs1, rd.
case len(ops) == 3 && isFPArithInstr(mnem):
rs2 := regFromOperand(ops[0])
@@ -2749,6 +2761,24 @@ func encodeRISCVExtended(mnem string, instr *ast.Instr, pc int, offsets map[stri
}
return append(out, wordLE(riscvRType(op, rd, rs1, notReg))...), true, nil
case "XNOR":
// ~(rs1 ^ rs2): the toolchain XORs into the destination and inverts
// it in place, no temporary.
if len(ops) != 2 && len(ops) != 3 {
return nil, true, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
rs2 := regFromOperand(ops[0])
rs1 := regFromOperand(ops[1])
rd := rs1
if len(ops) == 3 {
rd = regFromOperand(ops[2])
}
if rs1 < 0 || rs2 < 0 || rd < 0 {
return nil, true, fmt.Errorf("%s: invalid register", mnem)
}
out := wordLE(riscvRType(riscvInstrTable["XOR"], rd, rs1, rs2))
return append(out, wordLE(riscvIType(riscvInstrTable["XORI"], rd, rd, -1))...), true, nil
case "MAX", "MAXU", "MIN", "MINU":
if len(ops) != 2 && len(ops) != 3 {
return nil, true, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
@@ -2788,17 +2818,47 @@ func encodeRISCVExtended(mnem string, instr *ast.Instr, pc int, offsets map[stri
out = append(out, wordLE(riscvRType(riscvInstrTable["XOR"], rd, rs1, rd))...)
return out, true, nil
case "ROR", "RORW", "RORIW":
case "BCLR", "BEXT", "BINV", "BSET":
// The immediate spelling lowers to the shift-immediate entry, as the
// toolchain does: BCLR $63, X24 is BCLRI $63, X24, X24. The register
// spelling falls through to the main table's R-type path.
if len(ops) == 0 || !isImmOperand(ops[0]) {
return nil, false, nil
}
if len(ops) != 2 && len(ops) != 3 {
return nil, true, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
shamt, ok := riscvRawImm(ops[0])
if !ok || shamt < 0 || shamt > 63 {
return nil, true, fmt.Errorf("%s: immediate out of range 0 to 63", mnem)
}
rs1 := regFromOperand(ops[1])
rd := rs1
if len(ops) == 3 {
rd = regFromOperand(ops[2])
}
if rs1 < 0 || rd < 0 {
return nil, true, fmt.Errorf("%s: invalid register", mnem)
}
immForm := map[string]string{"BCLR": "BCLRI", "BEXT": "BEXTI", "BINV": "BINVI", "BSET": "BSETI"}[mnem]
return wordLE(riscvRType(riscvInstrTable[immForm], rd, rs1, int(shamt))), true, nil
case "ROL", "ROLW", "ROR", "RORI", "RORW", "RORIW":
if len(ops) != 2 && len(ops) != 3 {
return nil, true, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
}
if isImmOperand(ops[0]) {
// Immediate rotate: SRLI the amount, SLLI the complement, OR.
// The immediate spellings are ROR's: ROL takes a register amount
// only, as the toolchain's own expansion requires.
if mnem == "ROL" || mnem == "ROLW" {
return nil, true, fmt.Errorf("%s takes a register shift amount", mnem)
}
imm := int(immFromOperand(ops[0]))
shiftW := 63
srlEnc := riscvInstrTable["SRLI"]
sllEnc := riscvInstrTable["SLLI"]
if mnem != "ROR" {
if mnem == "RORW" || mnem == "RORIW" {
shiftW = 31
srlEnc = riscvInstrTable["SRLIW"]
sllEnc = riscvInstrTable["SLLIW"]
@@ -2817,16 +2877,17 @@ func encodeRISCVExtended(mnem string, instr *ast.Instr, pc int, offsets map[stri
var out []byte
out = append(out, wordLE(riscvRType(srlEnc, 31, rs1, imm))...)
sll := (-imm) & shiftW
if mnem == "ROR" && rd == rs1 && rd != 0 && sll >= 1 && sll <= 63 {
if mnem != "RORW" && mnem != "RORIW" && rd == rs1 && rd != 0 && sll >= 1 && sll <= 63 {
out = append(out, word16(rvcSLLI(uint32(rd), uint32(sll)))...) // C.SLLI
} else {
out = append(out, wordLE(riscvRType(sllEnc, rd, rs1, sll))...)
}
return append(out, wordLE(riscvRType(riscvInstrTable["OR"], rd, 31, rd))...), true, nil
}
// Register rotate: OR of the two opposite shifts through TMP.
if mnem == "RORIW" {
return nil, true, fmt.Errorf("RORIW takes an immediate shift amount")
// Register rotate: OR of the two opposite shifts through TMP. RORI
// and RORIW are the immediate spellings and take no register amount.
if mnem == "RORIW" || mnem == "RORI" {
return nil, true, fmt.Errorf("%s takes an immediate shift amount", mnem)
}
rs2 := regFromOperand(ops[0])
rs1 := regFromOperand(ops[1])
@@ -2837,16 +2898,27 @@ func encodeRISCVExtended(mnem string, instr *ast.Instr, pc int, offsets map[stri
if rs1 < 0 || rs2 < 0 || rd < 0 {
return nil, true, fmt.Errorf("%s: invalid register", mnem)
}
sllEnc := riscvInstrTable["SLL"]
srlEnc := riscvInstrTable["SRL"]
if mnem == "RORW" {
sllEnc = riscvInstrTable["SLLW"]
srlEnc = riscvInstrTable["SRLW"]
// ROR shifts right by the amount and left by its complement; ROL
// swaps the two.
wide := mnem == "ROL" || mnem == "ROR"
shiftLeft := riscvInstrTable["SLL"]
shiftRight := riscvInstrTable["SRL"]
shiftLeftW := riscvInstrTable["SLLW"]
shiftRightW := riscvInstrTable["SRLW"]
tmpShift, rdShift := shiftLeft, shiftRight
if mnem == "ROL" || mnem == "ROLW" {
tmpShift, rdShift = shiftRight, shiftLeft
}
if !wide {
tmpShift, rdShift = shiftLeftW, shiftRightW
if mnem == "ROLW" {
tmpShift, rdShift = shiftRightW, shiftLeftW
}
}
var out []byte
out = append(out, wordLE(riscvRType(riscvInstrTable["SUB"], 31, 0, rs2))...) // NEG
out = append(out, wordLE(riscvRType(sllEnc, 31, rs1, 31))...)
out = append(out, wordLE(riscvRType(srlEnc, rd, rs1, rs2))...)
out = append(out, wordLE(riscvRType(tmpShift, 31, rs1, 31))...)
out = append(out, wordLE(riscvRType(rdShift, rd, rs1, rs2))...)
out = append(out, wordLE(riscvRType(riscvInstrTable["OR"], rd, 31, rd))...)
return out, true, nil
@@ -3150,7 +3222,12 @@ func isRTypeInstr(m string) bool {
"ADDW", "SUBW", "SLLW", "SRLW", "SRAW",
"MUL", "MULH", "MULHSU", "MULHU", "DIV", "DIVU", "REM", "REMU",
"MULW", "DIVW", "DIVUW", "REMW", "REMUW",
"CZEROEQZ", "CZERONEZ":
"CZEROEQZ", "CZERONEZ",
// Zba address generation, Zbc carry-less multiplication and the
// Zbs single-bit register forms.
"ADDUW", "SH1ADD", "SH1ADDUW", "SH2ADD", "SH2ADDUW", "SH3ADD", "SH3ADDUW",
"CLMUL", "CLMULH", "CLMULR",
"BCLR", "BEXT", "BINV", "BSET":
return true
}
return false
@@ -3158,12 +3235,39 @@ func isRTypeInstr(m string) bool {
func isShiftImmInstr(m string) bool {
switch m {
case "SLLI", "SRLI", "SRAI", "SLLIW", "SRLIW", "SRAIW":
case "SLLI", "SRLI", "SRAI", "SLLIW", "SRLIW", "SRAIW",
"BCLRI", "BEXTI", "BINVI", "BSETI", "SLLIUW":
return true
}
return false
}
// isZbUnaryInstr reports whether m is a Zbb one-source bit operation: a
// single source register with the rs2 field fixed, spelled INSTR rs, rd.
func isZbUnaryInstr(m string) bool {
switch m {
case "CLZ", "CLZW", "CPOP", "CPOPW", "CTZ", "CTZW",
"SEXTB", "SEXTH", "ORCB", "REV8", "ZEXTH":
return true
}
return false
}
// riscvZbUnaryRS2 carries the constant each Zbb unary operation fixes in the
// rs2 field: the population counts, sign extensions and byte operations
// address a width or a position, not a second register. CLZ, CLZW and ZEXTH
// leave the field empty and are absent from the map.
var riscvZbUnaryRS2 = map[string]int{
"CPOP": 2,
"CPOPW": 2,
"CTZ": 1,
"CTZW": 1,
"SEXTB": 4,
"SEXTH": 5,
"ORCB": 7,
"REV8": 24,
}
// riscvShiftMax bounds a shift immediate at the instruction's width: the
// doubleword forms shift 0-63, the word forms 0-31, the toolchain's own
// validation boundary.