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) + } + } +}