diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index f509030..51f9b43 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -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. diff --git a/asm/riscv_bitmanip_test.go b/asm/riscv_bitmanip_test.go new file mode 100644 index 0000000..cfca6d5 --- /dev/null +++ b/asm/riscv_bitmanip_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Petr Balvín (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") +} diff --git a/asm/riscv_encode.go b/asm/riscv_encode.go index 084c23d..b4d7e2b 100644 --- a/asm/riscv_encode.go +++ b/asm/riscv_encode.go @@ -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},