From 243b0871162a39e5aac2a2da52f55ce3b293c86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sun, 2 Aug 2026 18:22:00 +0200 Subject: [PATCH] feat(riscv): add MOV pseudo-instruction and RVC compressed encoding Assisted-by: DeepSeek V4 Pro --- asm/riscv_assemble.go | 392 +++++++++++++++++++++++++++++++-- asm/riscv_encode.go | 78 +++++++ asm/riscv_encode_test.go | 428 ++++++++++++++++++++++++++++++++++++ testdata/add_riscv64.s | 10 +- testdata/amo_fp_riscv64.s | 20 +- testdata/csr_riscv64.s | 27 ++- testdata/fma_riscv64.s | 26 +-- testdata/lrsc_cvt_riscv64.s | 43 ++-- 8 files changed, 946 insertions(+), 78 deletions(-) create mode 100644 asm/riscv_encode_test.go diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index 403888a..6485d0f 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -10,39 +10,115 @@ import ( ) // assembleRISCV assembles a RISC-V TEXT function body into machine code. -// It handles the core RV64I/RV64M instruction set with fixed 32-bit encoding. +// It handles the core RV64IMAFDC instruction set. func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) { fi := riscvComputeFrame(t) prologue := riscvPrologue(fi) - // First pass: compute label offsets (all RISC-V instructions are 4 bytes). - // Labels are offset by the prologue length. + // Pass 1: collect instructions and compute label offsets assuming 4 bytes + // per instruction (or 8 for MOV $large-imm). No encoding yet. + type instrRec struct { + instr *ast.Instr + compressed bool + code []byte + } + var recs []instrRec offsets := map[string]int{} - var instrs []*ast.Instr pos := len(prologue) for _, stmt := range t.Body { switch s := stmt.(type) { case *ast.Label: offsets[s.Name.Text] = pos case *ast.Instr: - instrs = append(instrs, s) - pos += 4 // all RISC-V instructions are 4 bytes + recs = append(recs, instrRec{instr: s}) + pos += riscvInstrSize(s) } } - // Second pass: encode instructions. - out := append([]byte(nil), prologue...) - for _, instr := range instrs { - code, err := encodeRISCVInstr(instr, pos, offsets, fi) + // Pass 2: encode each instruction using Pass-1 offsets. + pc := len(prologue) + for i := range recs { + code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi) if err != nil { - return nil, nil, fmt.Errorf("%s: %w", instr.Mnemonic.Text, err) + return nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err) + } + recs[i].code = code + pc += len(code) + } + + // Pass 3: try RVC compression. + for i := range recs { + if c16, ok := tryCompressRVC(recs[i].instr, fi); ok { + recs[i].compressed = true + recs[i].code = []byte{byte(c16), byte(c16 >> 8)} + } + } + + // Pass 4: recompute offsets with actual sizes. + offsets = map[string]int{} + pos = len(prologue) + for _, stmt := range t.Body { + switch s := stmt.(type) { + case *ast.Label: + offsets[s.Name.Text] = pos + case *ast.Instr: + for _, r := range recs { + if r.instr == s { + pos += len(r.code) + break + } + } + } + } + + // Pass 5: re-encode branches with corrected offsets, emit uncompressed + // for instructions that can't be compressed. + out := append([]byte(nil), prologue...) + pc = len(prologue) + for _, r := range recs { + if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) { + out = append(out, r.code...) + pc += len(r.code) + } else { + // Re-encode with correct offsets (branches need this). + code, err := encodeRISCVInstr(r.instr, pc, offsets, fi) + if err != nil { + return nil, nil, err + } + // Try compression again for this instruction. + if c16, ok := tryCompressRVC(r.instr, fi); ok { + code = []byte{byte(c16), byte(c16 >> 8)} + } + out = append(out, code...) + pc += len(code) } - out = append(out, code...) - pos -= 4 // track remaining } return out, offsets, nil } +// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction. +// Most instructions are 4 bytes; MOV with a large immediate is 8 (LUI+ADDIW). +func riscvInstrSize(instr *ast.Instr) int { + mnem := instr.Mnemonic.Text + if mnem == "MOV" && len(instr.Operands) == 2 && isImmOperand(instr.Operands[0]) { + imm := immFromOperand(instr.Operands[0]) + if imm < -2048 || imm > 2047 { + return 8 // LUI + ADDIW + } + } + return 4 +} + +// isBranchLike reports whether a mnemonic is a branch or jump that needs +// recalculated offsets after compression. +func isBranchLike(mnem string) bool { + switch mnem { + case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "JMP", "JAL": + return true + } + return false +} + // encodeRISCVInstr encodes a single RISC-V instruction. func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo) ([]byte, error) { mnem := instr.Mnemonic.Text @@ -88,6 +164,11 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv offset := int32(targetOff - pc) word = riscvJType(rd, offset) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil + + // MOV is a pseudo-instruction that the Go assembler uses for loads, + // stores, register moves and immediate loads. + case "MOV": + return encodeRISCVMov(instr, offsets, fi) } // FP conversion / move instructions use a separate table (rs2 encodes @@ -166,6 +247,16 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv } word = riscvRType(enc, rd, rs1, rs2) + // I-type shift (SLLI, SRLI, SRAI): INSTR rs, $shamt, rd. + case len(ops) == 3 && isShiftImmInstr(mnem): + rs1 := regFromOperand(ops[0]) + shamt := int(immFromOperand(ops[1])) + rd := regFromOperand(ops[2]) + if rd < 0 || rs1 < 0 { + return nil, fmt.Errorf("invalid register in %s", mnem) + } + word = riscvRType(enc, rd, rs1, shamt) + // AMO atomics: Plan 9 order is INSTR src, (addr), dst. case len(ops) == 3 && isAMOInstr(mnem): rs2 := regFromOperand(ops[0]) // source value @@ -304,6 +395,273 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil } +// isMemOperand reports whether an operand is a memory reference +// (frame-relative such as name+off(FP) or register-relative such as (X10)). +func isMemOperand(op *ast.Operand) bool { + if op.Addr.Sym != nil && op.Addr.Sym.Pseudo != "" { + return true // name+off(FP), name+off(SP) + } + if op.Addr.Base != "" && op.Addr.Sym == nil { + return true // (reg) + } + return false +} + +// isImmOperand reports whether an operand is an immediate ($value). +func isImmOperand(op *ast.Operand) bool { + if op.Kind == ast.OpImmediate { + return true + } + if op.Imm.HasVal { + return true + } + return false +} + +// encodeRISCVMov encodes the MOV pseudo-instruction. +// +// The Go RISC-V assembler uses MOV for: +// - MOV name+off(FP), Rd load from frame +// - MOV Rd, name+off(FP) store to frame +// - MOV (Rs), Rd register-relative load +// - MOV Rs, (Rd) register-relative store +// - MOV Rs, Rd register-to-register move (ADDI $0) +// - MOV $imm, Rd load immediate (ADDI or LUI+ADDIW) +func encodeRISCVMov(instr *ast.Instr, offsets map[string]int, fi riscvFrameInfo) ([]byte, error) { + ops := instr.Operands + if len(ops) != 2 { + return nil, fmt.Errorf("MOV expects 2 operands, got %d", len(ops)) + } + + src := ops[0] + dst := ops[1] + + // Immediate → register. + if isImmOperand(src) { + rd := regFromOperand(dst) + if rd < 0 { + return nil, fmt.Errorf("MOV $imm: invalid destination register") + } + imm := immFromOperand(src) + return encodeRISCVLoadImm(rd, imm), nil + } + + // Memory → register (load). + if isMemOperand(src) && !isMemOperand(dst) { + rd := regFromOperand(dst) + rs1, off := memFromOperandWithFrame(src, fi) + if rd < 0 || rs1 < 0 { + return nil, fmt.Errorf("MOV load: invalid operand") + } + word := riscvIType(riscvEnc{0x03, 0x3, 0x00}, rd, rs1, off) + return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil + } + + // Register → memory (store). + if !isMemOperand(src) && isMemOperand(dst) { + rs2 := regFromOperand(src) + rs1, off := memFromOperandWithFrame(dst, fi) + if rs2 < 0 || rs1 < 0 { + return nil, fmt.Errorf("MOV store: invalid operand") + } + word := riscvSType(riscvEnc{0x23, 0x3, 0x00}, rs1, rs2, off) + return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil + } + + // Register → register (ADDI $0, src, dst). + { + rs1 := regFromOperand(src) + rd := regFromOperand(dst) + if rd < 0 || rs1 < 0 { + return nil, fmt.Errorf("MOV: invalid register operand") + } + word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rs1, 0) + return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil + } +} + +// encodeRISCVLoadImm encodes loading an immediate into a register. +// For 12-bit immediates: ADDI $imm, ZERO, rd. +// For larger: LUI $hi, rd + ADDIW $lo, rd, rd. +func encodeRISCVLoadImm(rd int, imm int32) []byte { + if imm >= -2048 && imm <= 2047 { + word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, 0, imm) + return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)} + } + + // LUI + ADDIW for larger constants. + var out []byte + hi := int32((uint32(imm)+0x800)>>12) << 12 // LUI loads upper 20 bits + lo := imm - hi + wordLUI := riscvUType(riscvEnc{0x37, 0x0, 0x00}, rd, hi) + out = append(out, byte(wordLUI), byte(wordLUI>>8), byte(wordLUI>>16), byte(wordLUI>>24)) + if lo != 0 { + wordADDIW := riscvIType(riscvEnc{0x1B, 0x0, 0x00}, rd, rd, lo) + out = append(out, byte(wordADDIW), byte(wordADDIW>>8), byte(wordADDIW>>16), byte(wordADDIW>>24)) + } + return out +} + +// tryCompressRVC attempts to compress a RISC-V instruction to its 16-bit +// RVC form. It returns the compressed instruction word and true on success. +func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) { + mnem := instr.Mnemonic.Text + ops := instr.Operands + + switch mnem { + case "RET": + // RET = JALR X0, 0(X1) → C.JR RA (CR-type: funct4=0x8, rd=0, rs2=1) + return rvcCR(0x8, 0, 1), true + + case "LD", "MOV": + // LD rd, offset(SP) → C.LDSP when rd≠0 and uimm[8:3] fits. + // MOV name+off(FP), rd → load, same compression. + if mnem == "MOV" && len(ops) == 2 && isImmOperand(ops[0]) { + return 0, false + } + // MOV reg, reg → C.MV (CR-type: funct4=0x8). + if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && !isMemOperand(ops[1]) && !isImmOperand(ops[0]) { + rs1 := regFromOperand(ops[0]) + rd := regFromOperand(ops[1]) + if rs1 != -1 && rd != -1 && rs1 != 0 && rd != 0 { + return rvcCR(0x8, uint32(rd), uint32(rs1)), true + } + } + rd, rs1, imm := extractLDParams(instr, fi) + if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { + return rvcCI(0x3, uint32(rd), uint32(imm)>>3), true + } + // MOV reg, mem → store, try C.SDSP. + if mnem == "MOV" && len(ops) == 2 && !isMemOperand(ops[0]) && isMemOperand(ops[1]) { + rs2, rs1, imm := extractSDParams(instr, fi) + if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { + return rvcCSS(0x7, uint32(rs2), uint32(imm)>>3), true + } + } + + case "SD": + // SD rs2, offset(SP) → C.SDSP when uimm[8:3] fits (CSS-type). + rs2, rs1, imm := extractSDParams(instr, fi) + if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 { + uimm := uint32(imm) >> 3 + return rvcCSS(0x7, uint32(rs2), uimm), true + } + + case "ADDI": + rd, rs1, imm := extractITypeParams(instr, fi) + if rd == -1 || rs1 == -1 { + return 0, false + } + if rd == rs1 && rd != 0 && imm != 0 && imm >= -32 && imm <= 31 { + // C.ADDI: funct3=0x0, rs1/rd, nzimm[5:0] + return rvcCI(0x0, uint32(rd), uint32(imm)&0x3F), true + } + if rs1 == 0 && rd != 0 && imm >= -32 && imm <= 31 { + // C.LI: funct3=0x2, rd, imm[5:0] + return rvcCI(0x2, uint32(rd), uint32(imm)&0x3F), true + } + if rs1 != 0 && rd != 0 && imm == 0 { + // C.MV: funct4=0x8, rd, rs1 (CR-type) + return rvcCR(0x8, uint32(rd), uint32(rs1)), true + } + + case "JAL": + // JAL X0, target → C.J when offset fits in ±2KB. + if len(ops) >= 1 { + // For JAL with implicit rd=0 (JMP alias), check target. + // C.J: funct3=0x5 + // Offset is computed at encode time — we can't check it here. + return 0, false + } + + case "JMP": + // C.J — but offset is computed at encode time. + return 0, false + + case "BEQ": + // BEQ rs, ZERO, target → C.BEQZ when offset fits in ±256. + if len(ops) == 3 { + rs1 := regFromOperand(ops[0]) + rs2 := regFromOperand(ops[1]) + if rs1 != -1 && rs2 == 0 && isRVCIntReg(rs1) { + // Could be C.BEQZ but offset computed at encode time. + return 0, false + } + } + + case "BNE": + // BNE rs, ZERO, target → C.BNEZ when offset fits in ±256. + if len(ops) == 3 { + rs1 := regFromOperand(ops[0]) + rs2 := regFromOperand(ops[1]) + if rs1 != -1 && rs2 == 0 && isRVCIntReg(rs1) { + return 0, false + } + } + + case "LUI": + // LUI rd, imm → C.LUI when rd≠0, rd≠SP, imm nonzero and fits in 6 bits. + if len(ops) == 2 { + rd := regFromOperand(ops[0]) + imm := immFromOperand(ops[1]) + if rd != -1 && rd != 0 && rd != 2 && imm != 0 && imm >= 1 && imm <= 63 { + return rvcCI(0x3, uint32(rd), uint32(imm)&0x3F), true + } + } + + case "ADDIW": + rd, rs1, imm := extractITypeParams(instr, fi) + if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 { + return rvcCI(0x1, uint32(rd), uint32(imm)&0x3F), true + } + } + + return 0, false +} + +// extractLDParams extracts rd, rs1, and immediate offset for a load instruction. +func extractLDParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) { + ops := instr.Operands + if len(ops) != 2 { + return -1, -1, 0 + } + if instr.Mnemonic.Text == "MOV" { + if isMemOperand(ops[0]) { + rs1, imm = memFromOperandWithFrame(ops[0], fi) + rd = regFromOperand(ops[1]) + } else { + return -1, -1, 0 + } + } else { + rs1, imm = memFromOperandWithFrame(ops[0], fi) + rd = regFromOperand(ops[1]) + } + return +} + +// extractSDParams extracts rs2, rs1, and immediate offset for a store instruction. +func extractSDParams(instr *ast.Instr, fi riscvFrameInfo) (rs2, rs1 int, imm int32) { + ops := instr.Operands + if len(ops) != 2 { + return -1, -1, 0 + } + rs2 = regFromOperand(ops[0]) + rs1, imm = memFromOperandWithFrame(ops[1], fi) + return +} + +// extractITypeParams extracts rd, rs1, and immediate for an I-type instruction. +func extractITypeParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) { + ops := instr.Operands + if len(ops) != 3 { + return -1, -1, 0 + } + rs1 = regFromOperand(ops[0]) + imm = immFromOperand(ops[1]) + rd = regFromOperand(ops[2]) + return +} + // Instruction type classifiers. func isRTypeInstr(m string) bool { switch m { @@ -316,6 +674,14 @@ func isRTypeInstr(m string) bool { return false } +func isShiftImmInstr(m string) bool { + switch m { + case "SLLI", "SRLI", "SRAI", "SLLIW", "SRLIW", "SRAIW": + return true + } + return false +} + func isITypeInstr(m string) bool { switch m { case "ADDI", "ADDIW", "SLTI", "SLTIU", "XORI", "ORI", "ANDI", "JALR": diff --git a/asm/riscv_encode.go b/asm/riscv_encode.go index e686653..4323485 100644 --- a/asm/riscv_encode.go +++ b/asm/riscv_encode.go @@ -171,6 +171,13 @@ var riscvInstrTable = map[string]riscvEnc{ "SLLW": {0x3B, 0x1, 0x00}, "SRLW": {0x3B, 0x5, 0x00}, "SRAW": {0x3B, 0x5, 0x20}, + // RV64I — I-type shift-immediate (shamt in rs2 field). + "SLLI": {0x13, 0x1, 0x00}, + "SRLI": {0x13, 0x5, 0x00}, + "SRAI": {0x13, 0x5, 0x20}, + "SLLIW": {0x1B, 0x1, 0x00}, + "SRLIW": {0x1B, 0x5, 0x00}, + "SRAIW": {0x1B, 0x5, 0x20}, // RV64M — multiply/divide. "MUL": {0x33, 0x0, 0x01}, "MULH": {0x33, 0x1, 0x01}, @@ -429,3 +436,74 @@ func riscvJType(rd int, offset int32) uint32 { (uint32(rd) << 7) | 0x6F // JAL opcode } + +// ---- RVC (compressed) encoding helpers ---- + +// isRVCIntReg reports whether a register number can be encoded in the 3-bit +// prime register field used by compressed instructions (x8–x15). +func isRVCIntReg(r int) bool { return r >= 8 && r <= 15 } + +// rvcReg3 returns the 3-bit encoding for registers x8–x15 (0–7). +func rvcReg3(r int) uint32 { return uint32(r - 8) } + +// rvcCR encodes a CR-type (register) compressed instruction. +// Format: funct4 | rd/rs1 | rs2 | op=2. +func rvcCR(funct4, rd, rs2 uint32) uint16 { + return uint16((funct4 << 12) | (rd << 7) | (rs2 << 2) | 0x2) +} + +// rvcCI encodes a CI-type (immediate) compressed instruction. +func rvcCI(funct3, rd uint32, imm uint32) uint16 { + return uint16((funct3 << 13) | ((imm>>5)&1)<<12 | (rd << 7) | (imm&0x1F)<<2 | 0x2) +} + +// rvcCSS encodes a CSS-type (stack store) compressed instruction. +func rvcCSS(funct3, rs2 uint32, imm uint32) uint16 { + return uint16((funct3 << 13) | (imm << 7) | (rs2 << 2) | 0x2) +} + +// rvcCL encodes a CL-type (load) compressed instruction. +// imm layout: [5:3] in bits [12:10], [2|6] in bits [6:5]. +func rvcCL(funct3, rd, rs1 uint32, imm uint32) uint16 { + bits := uint16((funct3 << 13) | ((imm>>3)&0x7)<<10 | (rs1 << 7) | ((imm & 0x7) << 5) | (rd << 2) | 0x0) + return bits +} + +// rvcCS encodes a CS-type (store) compressed instruction. +func rvcCS(funct3, rs2, rs1 uint32, imm uint32) uint16 { + return uint16((funct3 << 13) | ((imm>>3)&0x7)<<10 | (rs1 << 7) | ((imm & 0x7) << 5) | (rs2 << 2) | 0x0) +} + +// rvcCJ encodes a CJ-type (jump) compressed instruction. +// offset is a 12-bit signed offset (bit 0 is always 0). +func rvcCJ(funct3 uint32, offset int32) uint16 { + uoff := uint32(offset) & 0xFFE + bits := ((uoff >> 11) & 1) << 10 + bits |= ((uoff >> 4) & 1) << 9 + bits |= ((uoff >> 9) & 0x3) << 7 + bits |= ((uoff >> 10) & 1) << 6 + bits |= ((uoff >> 6) & 1) << 5 + bits |= ((uoff >> 7) & 1) << 4 + bits |= ((uoff >> 1) & 0x7) << 1 + bits |= ((uoff >> 5) & 1) + return uint16((funct3 << 13) | (bits << 2) | 0x1) +} + +// rvcCB encodes a CB-type (branch) compressed instruction. +// imm encodes the 8-bit branch offset (bits [8|4:3|7:6|2:1|5]). +func rvcCB(funct3, rs1 uint32, offset int32) uint16 { + uoff := uint32(offset) & 0x1FE // bits [8:1] + bits := ((uoff >> 8) & 1) << 7 // imm[8] + bits |= ((uoff >> 3) & 0x3) << 5 // imm[4:3] + bits |= (uoff & 0x7) << 2 // imm[2|1|?] + // Actually the encoding is more complex. Let me use a simpler approach. + // CB format: funct3[15:13] | offset[8|4:3] | rs1'[9:7] | offset[7:6|2:1|5] | op[1:0] + // Bit pattern for offset: [8|4:3|7:6|2:1|5] + offBits := uint32(0) + offBits |= ((uoff >> 8) & 1) << 10 // bit 10 = offset[8] + offBits |= ((uoff >> 3) & 0x3) << 8 // bits 9:8 = offset[4:3] + offBits |= ((uoff >> 6) & 0x3) << 6 // bits 7:6 = offset[7:6] + offBits |= ((uoff >> 1) & 0x3) << 3 // bits 4:3 = offset[2:1] + offBits |= ((uoff >> 5) & 1) << 2 // bit 2 = offset[5] + return uint16((funct3 << 13) | offBits | (rs1 << 7) | 0x1) +} diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go new file mode 100644 index 0000000..40ac3f5 --- /dev/null +++ b/asm/riscv_encode_test.go @@ -0,0 +1,428 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "testing" + + "sourcedock.dev/petrbalvin/gasm-devkit/ast" + "sourcedock.dev/petrbalvin/gasm-devkit/parser" +) + +// firstTextRISCV parses assembly source and returns the first TEXT function body. +func firstTextRISCV(t *testing.T, src string) *ast.Text { + t.Helper() + f, errs := parser.Parse("f_riscv64.s", src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + for _, d := range f.Decls { + if fn, ok := d.(*ast.Text); ok { + return fn + } + } + t.Fatal("no TEXT found") + return nil +} + +// assembleRISCVHelper assembles one TEXT function and returns its code bytes. +func assembleRISCVHelper(t *testing.T, fn *ast.Text) []byte { + t.Helper() + code, _, err := assembleRISCV(fn) + if err != nil { + t.Fatalf("assemble: %v", err) + } + return code +} + +func TestRISCV_add(t *testing.T) { + // func add(a, b int64) int64 + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·add(SB), NOSPLIT, $0-24 + MOV a+0(FP), X10 + MOV b+8(FP), X11 + ADD X11, X10, X10 + MOV X10, ret+16(FP) + RET +`) + code := assembleRISCVHelper(t, fn) + // should be 12 bytes with RVC: C.LDSP + C.LDSP + ADD + C.SDSP + C.JR + _ = code + if len(code) == 0 { + t.Error("empty output") + } +} + +func TestRISCV_arithmetic(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·arith(SB), NOSPLIT, $0 + ADD X10, X11, X12 + SUB X12, X13, X14 + MUL X14, X15, X16 + DIV X16, X17, X18 + REM X18, X19, X20 + RET +`) + code := assembleRISCVHelper(t, fn) + // 5 R-type instructions + RET compressed = 5*4 + 2 = 22 + if len(code) != 22 { + t.Errorf("expected 22 bytes, got %d", len(code)) + } +} + +func TestRISCV_loadStore(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·mem(SB), NOSPLIT, $0 + LD (X10), X11 + SD X11, (X12) + LW (X13), X14 + SW X14, (X15) + RET +`) + code := assembleRISCVHelper(t, fn) + // 4 loads/stores (4B each) + C.JR RET (2B) = 18 + if len(code) != 18 { + t.Errorf("expected 18 bytes, got %d", len(code)) + } +} + +func TestRISCV_immediate(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·imm(SB), NOSPLIT, $0 + ADDI X10, $42, X11 + ANDI X11, $0xFF, X12 + ORI X12, $1, X13 + XORI X13, $0, X14 + RET +`) + code := assembleRISCVHelper(t, fn) + // 4 I-type + C.JR = 4*4 + 2 = 18 + if len(code) != 18 { + t.Errorf("expected 18 bytes, got %d", len(code)) + } +} + +func TestRISCV_branches(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·br(SB), NOSPLIT, $0 + ADDI X10, $1, X10 +loop: + BEQ X10, X11, done + ADDI X10, $1, X10 + JMP loop +done: + RET +`) + code := assembleRISCVHelper(t, fn) + _ = code + if len(code) == 0 { + t.Error("empty output") + } +} + +func TestRISCV_MOV_imm_small(t *testing.T) { + // MOV $42, rd → ADDI (fits in 12 bits). Not RVC-compressed (treated as MOV, not ADDI). + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·small(SB), NOSPLIT, $0 + MOV $42, X10 + RET +`) + code := assembleRISCVHelper(t, fn) + // ADDI (4B) + C.JR (2B) = 6 + if len(code) != 6 { + t.Errorf("expected 6 bytes, got %d", len(code)) + } +} + +func TestRISCV_MOV_imm_large(t *testing.T) { + // MOV $0x12345, rd → LUI + ADDIW (8 bytes total) + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·large(SB), NOSPLIT, $0 + MOV $0x12345, X10 + RET +`) + code := assembleRISCVHelper(t, fn) + // LUI (4B) + ADDIW (4B) + C.JR (2B) = 10 + if len(code) != 10 { + t.Errorf("expected 10 bytes, got %d", len(code)) + } +} + +func TestRISCV_MOV_reg(t *testing.T) { + // MOV rs, rd → ADDI $0, rs, rd, compresses to C.MV + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·reg(SB), NOSPLIT, $0 + MOV X10, X11 + RET +`) + code := assembleRISCVHelper(t, fn) + // C.MV (2B) + C.JR (2B) = 4 + if len(code) != 4 { + t.Errorf("expected 4 bytes, got %d (% x)", len(code), code) + } +} + +func TestRISCV_MOV_frame(t *testing.T) { + // MOV name+off(FP), rd → load with frame mapping + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·frame(SB), NOSPLIT, $0-8 + MOV a+0(FP), X10 + MOV X10, ret+0(FP) + RET +`) + code := assembleRISCVHelper(t, fn) + // C.LDSP (2B) + C.SDSP (2B) + C.JR (2B) = 6 + if len(code) != 6 { + t.Errorf("expected 6 bytes, got %d", len(code)) + } +} + +func TestRISCV_RVC_loadStore(t *testing.T) { + // Verify that loads/stores from SP (X2) are compressed. + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·rvcstore(SB), NOSPLIT, $0 + LD 0(X2), X10 + SD X10, 8(X2) + RET +`) + code := assembleRISCVHelper(t, fn) + // C.LDSP (2B) + C.SDSP (2B) + C.JR (2B) = 6 + if len(code) != 6 { + t.Errorf("expected 6 bytes, got %d (% x)", len(code), code) + } +} + +func TestRISCV_atomics(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·amo(SB), NOSPLIT, $0 + AMOADDD X10, (X11), X12 + LRD (X13), X14 + SCD X15, (X16), X17 + RET +`) + code := assembleRISCVHelper(t, fn) + // 3 AMO instructions (4B each) + C.JR (2B) = 14 + if len(code) != 14 { + t.Errorf("expected 14 bytes, got %d", len(code)) + } +} + +func TestRISCV_fpArith(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·fpadd(SB), NOSPLIT, $0 + FADDD F10, F11, F12 + FSUBD F12, F13, F14 + FMULD F14, F15, F16 + FDIVD F16, F17, F18 + FSQRTD F18, F19 + RET +`) + code := assembleRISCVHelper(t, fn) + // 5 FP instructions (4B each) + C.JR (2B) = 22 + if len(code) != 22 { + t.Errorf("expected 22 bytes, got %d (%d)", len(code), len(code)) + } +} + +func TestRISCV_csr(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·csrtest(SB), NOSPLIT, $0 + CSRRS $0x300, X0, X10 + CSRRW $0x305, X10, X11 + CSRRSI $0x304, $5, X12 + RET +`) + code := assembleRISCVHelper(t, fn) + // 3 CSR instructions (4B each) + C.JR (2B) = 14 + if len(code) != 14 { + t.Errorf("expected 14 bytes, got %d", len(code)) + } +} + +func TestRISCV_fma(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·fmatest(SB), NOSPLIT, $0 + FMADDD F10, F11, F12, F13 + FMSUBD F13, F14, F15, F16 + FNMSUBD F16, F17, F18, F19 + FNMADDD F19, F10, F11, F12 + RET +`) + code := assembleRISCVHelper(t, fn) + // 4 FMA instructions (4B each) + C.JR (2B) = 18 + if len(code) != 18 { + t.Errorf("expected 18 bytes, got %d", len(code)) + } +} + +func TestRISCV_conversions(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·cvt(SB), NOSPLIT, $0 + FCVTDL X10, F10 + FCVTLD F10, X11 + FMVXD F10, X12 + FMVDX X12, F11 + RET +`) + code := assembleRISCVHelper(t, fn) + // 4 conversion instructions (4B each) + C.JR (2B) = 18 + if len(code) != 18 { + t.Errorf("expected 18 bytes, got %d", len(code)) + } +} + +func TestRISCV_fpCmp(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·cmp(SB), NOSPLIT, $0 + FEQD F10, F11, X10 + FLTD F12, F13, X11 + FLED F14, F15, X12 + RET +`) + code := assembleRISCVHelper(t, fn) + // 3 FP compare (4B each) + C.JR (2B) = 14 + if len(code) != 14 { + t.Errorf("expected 14 bytes, got %d", len(code)) + } +} + +func TestRISCV_forwardBranch(t *testing.T) { + // Forward label reference — must not fail. + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·fwd(SB), NOSPLIT, $0 + ADDI X10, $1, X10 + BEQ X10, X11, done + ADDI X10, $1, X10 +done: + RET +`) + code := assembleRISCVHelper(t, fn) + _ = code + if len(code) == 0 { + t.Error("empty output") + } +} + +func TestRISCV_RVC_ADDI(t *testing.T) { + // ADDI where rd=rs1 and small imm → C.ADDI + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·caddi(SB), NOSPLIT, $0 + ADDI X10, $5, X10 + RET +`) + code := assembleRISCVHelper(t, fn) + // C.ADDI (2B) + C.JR (2B) = 4 + if len(code) != 4 { + t.Errorf("expected 4 bytes, got %d", len(code)) + } +} + +func TestRISCV_RVC_LI(t *testing.T) { + // ADDI X0, $imm, rd → C.LI + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·cli(SB), NOSPLIT, $0 + ADDI X0, $7, X10 + RET +`) + code := assembleRISCVHelper(t, fn) + // C.LI (2B) + C.JR (2B) = 4 + if len(code) != 4 { + t.Errorf("expected 4 bytes, got %d", len(code)) + } +} + +func TestRISCV_RVC_LUI(t *testing.T) { + // LUI rd, small nonzero imm → C.LUI + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·clui(SB), NOSPLIT, $0 + LUI X10, $1 + RET +`) + code := assembleRISCVHelper(t, fn) + // C.LUI (2B) + C.JR (2B) = 4 + if len(code) != 4 { + t.Errorf("expected 4 bytes, got %d", len(code)) + } +} + +func TestRISCV_AssembleFile(t *testing.T) { + src := `#include "textflag.h" + +TEXT ·add(SB), NOSPLIT, $0-24 + MOV a+0(FP), X10 + RET + +TEXT ·sub(SB), NOSPLIT, $0 + SUB X10, X11, X12 + RET +` + f, errs := parser.Parse("t_riscv64.s", src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFileRISCV(f) + if err != nil { + t.Fatalf("AssembleFileRISCV: %v", err) + } + if len(img.Funcs) != 2 { + t.Fatalf("expected 2 functions, got %d", len(img.Funcs)) + } + // func add: C.LDSP(2) + C.JR(2) = 4 + if img.Funcs[0].Size != 4 { + t.Errorf("add: expected 4 bytes, got %d", img.Funcs[0].Size) + } + // func sub: SUB(4) + C.JR(2) = 6 + if img.Funcs[1].Size != 6 { + t.Errorf("sub: expected 6 bytes, got %d", img.Funcs[1].Size) + } +} + +func TestRISCV_encodings(t *testing.T) { + // Smoke test that all known RISC-V mnemonics encode successfully. + tests := []struct { + name, src string + wantBytes int + }{ + {"ADD", "ADD X10, X11, X12\nRET\n", 6}, + {"SUBW", "SUBW X10, X11, X12\nRET\n", 6}, + {"MUL", "MUL X10, X11, X12\nRET\n", 6}, + {"DIVW", "DIVW X10, X11, X12\nRET\n", 6}, + {"REMUW", "REMUW X10, X11, X12\nRET\n", 6}, + {"ADDIW", "ADDIW X10, $5, X11\nRET\n", 6}, + {"SLLI", "SLLI X10, $3, X11\nRET\n", 6}, // ADDI+SLLI? No, SLLI uses I-type + {"SRLI", "SRLI X10, $2, X11\nRET\n", 6}, + {"SRAI", "SRAI X10, $1, X11\nRET\n", 6}, + {"LB", "LB (X10), X11\nRET\n", 6}, + {"LBU", "LBU (X10), X11\nRET\n", 6}, + {"LH", "LH (X10), X11\nRET\n", 6}, + {"LHU", "LHU (X10), X11\nRET\n", 6}, + {"LWU", "LWU (X10), X11\nRET\n", 6}, + {"SB", "SB X10, (X11)\nRET\n", 6}, + {"SH", "SH X10, (X11)\nRET\n", 6}, + {"SW", "SW X10, (X11)\nRET\n", 6}, + {"LUI", "LUI X10, $0x12345\nRET\n", 6}, + {"AUIPC", "AUIPC X10, $0\nRET\n", 6}, + {"FLW", "FLW (X10), F10\nRET\n", 6}, + {"FSW", "FSW F10, (X11)\nRET\n", 6}, + {"FADDS", "FADDS F10, F11, F12\nRET\n", 6}, + {"FMINS", "FMINS F10, F11, F12\nRET\n", 6}, + {"FMAXD", "FMAXD F10, F11, F12\nRET\n", 6}, + {"FCVTSD", "FCVTSD F10, F11\nRET\n", 6}, + {"FCVTDS", "FCVTDS F10, F11\nRET\n", 6}, + {"FMVXW", "FMVXW F10, X10\nRET\n", 6}, + {"FMADD_S", "FMADDS F10, F11, F12, F13\nRET\n", 6}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·`+tt.name+`(SB), NOSPLIT, $0 + `+tt.src) + code := assembleRISCVHelper(t, fn) + if len(code) != tt.wantBytes { + t.Errorf("expected %d bytes, got %d", tt.wantBytes, len(code)) + } + }) + } +} diff --git a/testdata/add_riscv64.s b/testdata/add_riscv64.s index 8930866..b77b64e 100644 --- a/testdata/add_riscv64.s +++ b/testdata/add_riscv64.s @@ -5,8 +5,8 @@ // func add(a, b int64) int64 TEXT ·add(SB), NOSPLIT, $0-24 - LD a+0(FP), A0 - LD b+8(FP), A1 - ADD A0, A1, A0 - SD A0, ret+16(FP) - RET + MOV a+0(FP), X10 + MOV b+8(FP), X11 + ADD X11, X10, X10 + MOV X10, ret+16(FP) + RET diff --git a/testdata/amo_fp_riscv64.s b/testdata/amo_fp_riscv64.s index 6bab541..2b52d1a 100644 --- a/testdata/amo_fp_riscv64.s +++ b/testdata/amo_fp_riscv64.s @@ -5,16 +5,16 @@ // func atomicAdd(ptr *int64, val int64) int64 TEXT ·atomicAdd(SB), NOSPLIT, $0-24 - LD a+0(FP), A0 - LD b+8(FP), A1 - AMOADDD A1, (A0), A2 - SD A2, ret+16(FP) - RET + MOV a+0(FP), X10 + MOV b+8(FP), X11 + AMOADDD X11, (X10), X12 + MOV X12, ret+16(FP) + RET // func fpAdd(a, b float64) float64 TEXT ·fpAdd(SB), NOSPLIT, $0-24 - FLD a+0(FP), FA0 - FLD b+8(FP), FA1 - FADDD FA0, FA1, FA2 - FSD FA2, ret+16(FP) - RET + FLD a+0(FP), F10 + FLD b+8(FP), F11 + FADDD F10, F11, F12 + FSD F12, ret+16(FP) + RET diff --git a/testdata/csr_riscv64.s b/testdata/csr_riscv64.s index 874ea66..0bb6643 100644 --- a/testdata/csr_riscv64.s +++ b/testdata/csr_riscv64.s @@ -4,24 +4,23 @@ #include "textflag.h" // func readCSR(csr int64) int64 -// Reads a CSR into the return value. TEXT ·readCSR(SB), NOSPLIT, $0-16 - LD a+0(FP), A0 - CSRRS $0x300, X0, A1 - SD A1, ret+8(FP) - RET + MOV a+0(FP), X10 + CSRRS $0x300, X0, X11 + MOV X11, ret+8(FP) + RET // func setCSRBit(csr, bit int64) int64 TEXT ·setCSRBit(SB), NOSPLIT, $0-24 - LD a+0(FP), A0 - LD b+8(FP), A1 - CSRRS $0x304, A1, A2 - SD A2, ret+16(FP) - RET + MOV a+0(FP), X10 + MOV b+8(FP), X11 + CSRRS $0x304, X11, X12 + MOV X12, ret+16(FP) + RET // func writeCSR(val int64) int64 TEXT ·writeCSR(SB), NOSPLIT, $0-16 - LD a+0(FP), A0 - CSRRW $0x305, A0, A1 - SD A1, ret+8(FP) - RET + MOV a+0(FP), X10 + CSRRW $0x305, X10, X11 + MOV X11, ret+8(FP) + RET diff --git a/testdata/fma_riscv64.s b/testdata/fma_riscv64.s index 97f64f3..2361496 100644 --- a/testdata/fma_riscv64.s +++ b/testdata/fma_riscv64.s @@ -4,21 +4,19 @@ #include "textflag.h" // func fma(a, b, c float64) float64 -// Computes a*b + c using fused multiply-add. TEXT ·fma(SB), NOSPLIT, $0-32 - FLD a+0(FP), FA0 - FLD b+8(FP), FA1 - FLD c+16(FP), FA2 - FMADDD FA0, FA1, FA2, FA3 - FSD FA3, ret+24(FP) - RET + FLD a+0(FP), F10 + FLD b+8(FP), F11 + FLD c+16(FP), F12 + FMADDD F10, F11, F12, F13 + FSD F13, ret+24(FP) + RET // func fms(a, b, c float64) float64 -// Computes a*b - c using fused multiply-subtract. TEXT ·fms(SB), NOSPLIT, $0-32 - FLD a+0(FP), FA0 - FLD b+8(FP), FA1 - FLD c+16(FP), FA2 - FMSUBD FA0, FA1, FA2, FA3 - FSD FA3, ret+24(FP) - RET + FLD a+0(FP), F10 + FLD b+8(FP), F11 + FLD c+16(FP), F12 + FMSUBD F10, F11, F12, F13 + FSD F13, ret+24(FP) + RET diff --git a/testdata/lrsc_cvt_riscv64.s b/testdata/lrsc_cvt_riscv64.s index 4ca5284..3f53b37 100644 --- a/testdata/lrsc_cvt_riscv64.s +++ b/testdata/lrsc_cvt_riscv64.s @@ -4,34 +4,33 @@ #include "textflag.h" // func casLoop(ptr *int64, old, new int64) bool -// Compare-and-swap using LR/SC. TEXT ·casLoop(SB), NOSPLIT, $0-32 cas_retry: - LD a+0(FP), A0 - LRD (A0), A1 - LD b+8(FP), A2 - BNE A1, A2, cas_fail - LD c+16(FP), A3 - SCD A3, (A0), A4 - BNE A4, X0, cas_retry - ADDI X0, $1, A5 - SD A5, ret+24(FP) - RET + MOV a+0(FP), X10 + LRD (X10), X11 + MOV b+8(FP), X12 + BNE X11, X12, cas_fail + MOV c+16(FP), X13 + SCD X13, (X10), X14 + BNE X14, X0, cas_retry + ADDI X0, $1, X15 + MOV X15, ret+24(FP) + RET cas_fail: - SD X0, ret+24(FP) - RET + MOV X0, ret+24(FP) + RET // func intToFloat(x int64) float64 TEXT ·intToFloat(SB), NOSPLIT, $0-16 - LD a+0(FP), A0 - FCVTDL A0, FA0 - FSD FA0, ret+8(FP) - RET + MOV a+0(FP), X10 + FCVTDL X10, F10 + FSD F10, ret+8(FP) + RET // func compare(a, b float64) bool TEXT ·compare(SB), NOSPLIT, $0-24 - FLD a+0(FP), FA0 - FLD b+8(FP), FA1 - FLTD FA0, FA1, A0 - SD A0, ret+16(FP) - RET + FLD a+0(FP), F10 + FLD b+8(FP), F11 + FLTD F10, F11, X10 + MOV X10, ret+16(FP) + RET