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.
+105
View File
@@ -0,0 +1,105 @@
// 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"
)
// TestRISCVBitManipDifferential proves the Zba, Zbb, Zbc and Zbs families
// against the toolchain: the toolchain's own testdata spellings (every
// operand form each section carries) assembled by gasm and by go tool asm
// must agree byte for byte. The lines are the oracle's own, so a wrong
// funct6, a swapped operand pair or a missed two-operand collapse names
// itself through the first differing word.
func TestRISCVBitManipDifferential(t *testing.T) {
src := `#include "textflag.h"
TEXT ·bitmanip(SB), NOSPLIT, $0
ADDUW X10, X11, X12
ADDUW X10, X11
SH1ADD X11, X12, X13
SH1ADD X11, X12
SH1ADDUW X12, X13, X14
SH1ADDUW X12, X13
SH2ADD X13, X14, X15
SH2ADD X13, X14
SH2ADDUW X14, X15, X16
SH2ADDUW X14, X15
SH3ADD X15, X16, X17
SH3ADD X15, X16
SH3ADDUW X16, X17, X18
SH3ADDUW X16, X17
SLLIUW $31, X17, X18
SLLIUW $63, X17
SLLIUW $63, X17, X18
SLLIUW $1, X18, X19
ANDN X19, X20, X21
ANDN X19, X20
CLZ X20, X21
CLZW X21, X22
CPOP X22, X23
CPOPW X23, X24
CTZ X24, X25
CTZW X25, X26
MAX X26, X28, X29
MAX X26, X28
MAXU X28, X29, X30
MAXU X28, X29
MIN X29, X30, X5
MIN X29, X30
MINU X30, X5, X6
MINU X30, X5
ORN X6, X7, X8
ORN X6, X7
SEXTB X16, X17
SEXTH X17, X18
XNOR X18, X19, X20
XNOR X18, X19
ZEXTH X19, X20
ROL X8, X9, X10
ROL X8, X9
ROLW X9, X10, X11
ROLW X9, X10
ROR X10, X11, X12
ROR X10, X11
ROR $63, X11
RORI $63, X11, X12
RORI $1, X12, X13
RORIW $31, X13, X14
RORIW $1, X14, X15
RORW X15, X16, X17
RORW X15, X16
RORW $31, X13
ORCB X5, X6
REV8 X7, X8
CLMUL X5, X6, X7
CLMUL X5, X6
CLMULH X5, X6, X7
CLMULH X5, X6
CLMULR X5, X6, X7
CLMULR X5, X6
BCLR X23, X24, X25
BCLR $63, X24
BCLRI $1, X25, X26
BEXT X26, X28, X29
BEXT $63, X28
BEXTI $1, X29, X30
BINV X30, X5, X6
BINV $63, X6
BINVI $1, X7, X8
BSET X8, X9, X10
BSET $63, X9
BSETI $1, X10, X11
RET
`
dir := t.TempDir()
path := filepath.Join(dir, "bitmanip_riscv64.s")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
assertRISCVDifferential(t, path, src, "bitmanip")
}
+38
View File
@@ -204,6 +204,27 @@ var riscvInstrTable = map[string]riscvEnc{
"SLLIW": {0x1B, 0x1, 0x00},
"SRLIW": {0x1B, 0x5, 0x00},
"SRAIW": {0x1B, 0x5, 0x20},
// Zba/Zbs shift-immediate forms: the shamt spans bits [25:20], so the
// funct7 field carries the operation's funct6 and bit 25 comes from the
// amount. SLLIUW (Zba) zeroes the upper 32 bits before the shift.
"BCLRI": {0x13, 0x1, 0x24},
"BEXTI": {0x13, 0x5, 0x24},
"BINVI": {0x13, 0x1, 0x34},
"BSETI": {0x13, 0x1, 0x14},
"SLLIUW": {0x1B, 0x1, 0x04},
// Zbb unary bit operations: one source register, the rs2 field fixed
// (the count or the position the operation works on).
"CLZ": {0x13, 0x1, 0x30},
"CLZW": {0x1B, 0x1, 0x30},
"CPOP": {0x13, 0x1, 0x30},
"CPOPW": {0x1B, 0x1, 0x30},
"CTZ": {0x13, 0x1, 0x30},
"CTZW": {0x1B, 0x1, 0x30},
"SEXTB": {0x13, 0x1, 0x30},
"SEXTH": {0x13, 0x1, 0x30},
"ORCB": {0x13, 0x5, 0x14},
"REV8": {0x13, 0x5, 0x35},
"ZEXTH": {0x3B, 0x4, 0x04},
// RV64M, multiply/divide.
"MUL": {0x33, 0x0, 0x01},
"MULH": {0x33, 0x1, 0x01},
@@ -222,6 +243,23 @@ var riscvInstrTable = map[string]riscvEnc{
// Zicond conditional zeroing.
"CZEROEQZ": {0x33, 0x5, 0x07},
"CZERONEZ": {0x33, 0x7, 0x07},
// Zba address generation and Zbc carry-less multiplication.
"ADDUW": {0x3B, 0x0, 0x04},
"SH1ADD": {0x33, 0x2, 0x10},
"SH1ADDUW": {0x3B, 0x2, 0x10},
"SH2ADD": {0x33, 0x4, 0x10},
"SH2ADDUW": {0x3B, 0x4, 0x10},
"SH3ADD": {0x33, 0x6, 0x10},
"SH3ADDUW": {0x3B, 0x6, 0x10},
"CLMUL": {0x33, 0x1, 0x05},
"CLMULH": {0x33, 0x3, 0x05},
"CLMULR": {0x33, 0x2, 0x05},
// Zbs single-bit: the register forms; the immediate spellings lower to
// the shift-immediate entries below (BCLR $n is BCLRI $n).
"BCLR": {0x33, 0x1, 0x24},
"BEXT": {0x33, 0x5, 0x24},
"BINV": {0x33, 0x1, 0x34},
"BSET": {0x33, 0x1, 0x14},
// RV64I, I-type arithmetic.
"ADDI": {0x13, 0x0, 0x00},
"ADDIW": {0x1B, 0x0, 0x00},