From e43fa39dc9b47ffdeec6a44a3aa3b1af12c067a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Tue, 6 Oct 2026 20:55:34 +0200 Subject: [PATCH] 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 --- asm/riscv_assemble.go | 582 ++++++++++++++++++++++++++++++++++- asm/riscv_compressed_test.go | 142 +++++++++ 2 files changed, 716 insertions(+), 8 deletions(-) create mode 100644 asm/riscv_compressed_test.go diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index 51f9b43..ecfe8ff 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -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 - // jump the offsets prove overlong encodes to a 4-byte placeholder: the - // relaxation pass rewrites it before the final encoding. pcRelPcs is - // unavailable this early, so the N(PC) forms take the same placeholder - // path. + // jump the offsets prove overlong encodes to a placeholder of the + // instruction's own size: the relaxation pass rewrites it before the + // final encoding. pcRelPcs is unavailable this early, so the N(PC) + // forms take the same placeholder path. pc := len(prologue) for i := range recs { 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) } if err != nil { - code = make([]byte, 4) + code = make([]byte, riscvInstrSize(recs[i].instr, fi)) } recs[i].code = code pc += len(code) @@ -460,6 +460,9 @@ func riscvExtendedSize(mnem string, ops []*ast.Operand) int { case "RORIW": return 12 } + if isRVCInstr(mnem) { + return 2 + } return 4 } @@ -511,7 +514,8 @@ func riscvRevShiftSize(mnem string, ops []*ast.Operand) int { // recalculated offsets after compression. func isBranchLike(mnem string) bool { 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 false @@ -895,6 +899,14 @@ func riscvPCRelOffset(instr *ast.Instr) (int, bool) { if len(instr.Operands) < 2 { 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: 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 // encodings the main table does not carry. FSGNJD is a plain table // 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 { 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. // Each expansion reproduces the toolchain's instruction-for-instruction // 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 + switch { + case isRVCInstr(mnem): + code, err := encodeRISCVCompressed(mnem, instr, pc, offsets, pcRelPcs) + return code, true, err + } switch mnem { case "NOP": 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 // are accepted in either spelling: the vector V registers and the integer // registers share their 5-bit numbers, and the superset keeps hand-written diff --git a/asm/riscv_compressed_test.go b/asm/riscv_compressed_test.go new file mode 100644 index 0000000..bfdea4e --- /dev/null +++ b/asm/riscv_compressed_test.go @@ -0,0 +1,142 @@ +// Copyright (c) 2026 Petr Balvín (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) + } + } +}