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:
1 parent
d82ef33fa9
commit
ddfa33ccc1
3 files changed
+266
-19
No files matched your search
+123
-19
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
Reference in new issue
Block a user