Files
gasm-sdk/asm/riscv_assemble.go
T

1363 lines
42 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"fmt"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
)
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
// It handles the full RV64IMAFDC instruction set including RVC compression.
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
fi := riscvComputeFrame(t)
prologue := riscvPrologue(fi)
var relocs []Reloc
var spadj []SpadjStep
// The prologue raises the SP delta by autosize; the boundary is reported
// at the pc just past its ADDI, exactly as the toolchain's pctospadj does.
if fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: riscvPrologueSpadjPC(fi), Value: fi.autosize})
}
// 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{}
pos := len(prologue)
for _, stmt := range t.Body {
switch s := stmt.(type) {
case *ast.Label:
offsets[s.Name.Text] = pos
case *ast.Instr:
recs = append(recs, instrRec{instr: s})
pos += riscvInstrSize(s, 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, nil) // no relocs in Pass 2
if err != nil {
return nil, nil, nil, 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. Record relocations
// during this final pass (relocation offsets are relative to instruction start).
out := append([]byte(nil), prologue...)
pc = len(prologue)
preCount := len(relocs)
var lines []LineEntry
for _, r := range recs {
lines = append(lines, LineEntry{Offset: pc, Line: r.instr.Pos().Line})
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
out = append(out, r.code...)
pc += len(r.code)
} else {
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
if err != nil {
return nil, nil, nil, nil, nil, err
}
if c16, ok := tryCompressRVC(r.instr, fi); ok {
code = []byte{byte(c16), byte(c16 >> 8)}
}
2026-08-13 18:12:22 +02:00
// Make newly added relocation offsets function-relative. Each
// instruction records its reloc offset relative to its own start;
// the current pc is that instruction's offset from the function
// start (which includes the prologue). After is the address just
// past the relocated field, shifted by the same amount.
for j := preCount; j < len(relocs); j++ {
2026-08-13 18:12:22 +02:00
relocs[j].Off += pc
relocs[j].After += pc
}
preCount = len(relocs)
// The RET's epilogue closes the frame: the SP delta returns to zero
// after its ADDI (restore LR + ADDI).
if strings.ToUpper(r.instr.Mnemonic.Text) == "RET" && fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: pc + riscvReturnEpilogueLen(fi), Value: 0})
}
out = append(out, code...)
pc += len(code)
}
}
return out, offsets, relocs, lines, spadj, nil
}
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
2026-08-13 17:41:16 +02:00
// Most instructions are 4 bytes; MOV with a large immediate and I-type
// arithmetic with a large immediate expand to several (possibly compressed)
// instructions.
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
mnem := instr.Mnemonic.Text
ops := instr.Operands
if mnem == "RET" {
return len(riscvReturn(fi))
}
if mnem == "MOV" && len(ops) == 2 {
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
return 8
}
// MOV sym(SB), rd → 8 bytes (AUIPC + LD).
if isMemOperand(ops[0]) && ops[0].Addr.Sym != nil && ops[0].Addr.Sym.Pseudo == "SB" {
return 8
}
// MOV rd, sym(SB) → 8 bytes (AUIPC + SD).
if isMemOperand(ops[1]) && ops[1].Addr.Sym != nil && ops[1].Addr.Sym.Pseudo == "SB" {
return 8
}
2026-08-13 17:41:16 +02:00
// MOV $imm, rd → size depends on the immediate and RVC compression.
if isImmOperand(ops[0]) && ops[0].Imm.Sym == nil {
return riscvMovImmSize(regFromOperand(ops[1]), immFromOperand(ops[0]))
}
}
// I-type arithmetic with a large immediate expands to several instructions.
if (mnem == "ADDI" || mnem == "ANDI" || mnem == "ORI" || mnem == "XORI") && len(ops) >= 1 && isImmOperand(ops[0]) {
return riscvItypeImmediateSize(mnem, immFromOperand(ops[0]))
}
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, relocs *[]Reloc) ([]byte, error) {
mnem := instr.Mnemonic.Text
ops := instr.Operands
var word uint32
// Handle pseudo-instructions and special cases first.
switch mnem {
case "RET":
2026-08-13 18:12:22 +02:00
// RET = epilogue (restore LR and close the frame when present) +
// uncompressed JALR X0, 0(X1) (the toolchain never compresses RET).
return riscvReturn(fi), nil
case "CALL":
2026-08-13 18:12:22 +02:00
// CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL
// relocation. The Go assembler rejects CALL to a local branch label.
if len(ops) != 1 {
return nil, fmt.Errorf("CALL expects 1 operand, got %d", len(ops))
}
2026-08-13 18:12:22 +02:00
op := ops[0]
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
}
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: op.Addr.Sym.Name, Kind: RelRISCVJal, Addend: op.Addr.Sym.Offset})
}
word = riscvJType(1, 0) // JAL X1, 0, the linker fills the offset
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
case "JMP":
// JMP = JAL X0, target. The Go assembler never compresses this to
// C.J, so always emit the 32-bit JAL.
var target string
if len(ops) >= 1 {
target = labelFromOperand(ops[0])
}
targetOff, ok := offsets[target]
if !ok {
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
}
offset := int32(targetOff - pc)
word = riscvJType(0, offset)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
case "JAL":
rd := 0
var target string
if len(ops) >= 2 {
rd = regFromOperand(ops[0])
target = labelFromOperand(ops[1])
} else if len(ops) == 1 {
target = labelFromOperand(ops[0])
}
targetOff, ok := offsets[target]
if !ok {
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
}
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, fi, relocs)
// JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1).
case "JALR":
return encodeRISCVJALR(instr, fi)
// System instructions with no operands.
case "FENCE", "ECALL", "EBREAK":
enc, ok := riscvInstrTable[mnem]
if !ok {
return nil, fmt.Errorf("unsupported system instruction %q", mnem)
}
word = riscvIType(enc, 0, 0, 0)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
// FP conversion / move instructions use a separate table (rs2 encodes
// the conversion type, not a register). Handle them before the main
// table lookup.
if cvtEnc, ok := riscvCvtTable[mnem]; ok {
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
rs1 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word := riscvCvtType(cvtEnc, rd, rs1)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
// R4-type fused multiply-add: INSTR rs1, rs2, rs3, rd (destination last).
if fmaEnc, ok := riscvFmaTable[mnem]; ok {
if len(ops) != 4 {
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
}
rs1 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1])
rs3 := regFromOperand(ops[2])
rd := regFromOperand(ops[3])
if rd < 0 || rs1 < 0 || rs2 < 0 || rs3 < 0 {
return nil, fmt.Errorf("invalid FP register in %s", mnem)
}
word := riscvFmaType(fmaEnc, rd, rs1, rs2, rs3)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
// CSR instructions: INSTR csr, rs1|uimm, rd (destination last).
if csrEnc, ok := riscvCsrTable[mnem]; ok {
if len(ops) != 3 {
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
}
csr := immFromOperand(ops[0]) // CSR address (12-bit)
rd := regFromOperand(ops[2]) // destination register
if rd < 0 {
return nil, fmt.Errorf("invalid destination register in %s", mnem)
}
var src int
if csrEnc.imm {
// Immediate variant: ops[1] is a 5-bit unsigned immediate.
src = int(immFromOperand(ops[1]))
if src < 0 || src > 31 {
return nil, fmt.Errorf("%s: uimm out of range 0-31", mnem)
}
} else {
// Register variant: ops[1] is a register.
src = regFromOperand(ops[1])
if src < 0 {
return nil, fmt.Errorf("invalid source register in %s", mnem)
}
}
word := riscvCsrType(csrEnc, rd, src, csr)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
enc, ok := riscvInstrTable[mnem]
if !ok {
return nil, fmt.Errorf("unsupported RISC-V instruction %q", mnem)
}
switch {
// R-type: Go reverses the ISA order, writing rs2, rs1, rd (destination
// last); the two-operand form INSTR rs2, rd uses rd as rs1.
case len(ops) == 3 && isRTypeInstr(mnem):
rs2 := regFromOperand(ops[0]) // first operand = rs2
rs1 := regFromOperand(ops[1]) // second operand = rs1
rd := regFromOperand(ops[2]) // destination (last operand)
if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvRType(enc, rd, rs1, rs2)
case len(ops) == 2 && isRTypeInstr(mnem):
rs2 := regFromOperand(ops[0]) // source (first operand)
rd := regFromOperand(ops[1]) // destination (second operand)
if rd < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvRType(enc, rd, rd, rs2)
// I-type shift (SLLI, SRLI, SRAI): INSTR $shamt, rs1, rd; the two-operand
// form INSTR $shamt, rd uses rd as the source.
case len(ops) == 3 && isShiftImmInstr(mnem):
shamt := int(immFromOperand(ops[0]))
rs1 := regFromOperand(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)
case len(ops) == 2 && isShiftImmInstr(mnem):
shamt := int(immFromOperand(ops[0]))
rd := regFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvRType(enc, rd, rd, shamt)
// AMO atomics: Plan 9 order is INSTR src, (addr), dst.
case len(ops) == 3 && isAMOInstr(mnem):
rs2 := regFromOperand(ops[0]) // source value
rs1, _ := memFromOperandWithFrame(ops[1], fi) // memory address
rd := regFromOperand(ops[2]) // destination (old value)
if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvAMOType(enc, rd, rs1, rs2)
// FP arithmetic: Go reverses the ISA order, writing rs2, rs1, rd.
case len(ops) == 3 && isFPArithInstr(mnem):
rs2 := regFromOperand(ops[0])
rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2])
if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid FP register in %s", mnem)
}
word = riscvRType(enc, rd, rs1, rs2)
// FP arithmetic (2-operand): FSQRT src, dst.
case len(ops) == 2 && isFPArithInstr(mnem):
rs1 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid FP register in %s", mnem)
}
word = riscvRType(enc, rd, rs1, 0)
// FP loads: INSTR addr, freg (Plan 9: source first).
case len(ops) == 2 && isFPLoadInstr(mnem):
rd := regFromOperand(ops[1])
rs1, imm := memFromOperandWithFrame(ops[0], fi)
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvIType(enc, rd, rs1, imm)
// FP stores: INSTR freg, addr (Plan 9: source first).
case len(ops) == 2 && isFPStoreInstr(mnem):
rs2 := regFromOperand(ops[0])
rs1, imm := memFromOperandWithFrame(ops[1], fi)
if rs2 < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvSType(enc, rs1, rs2, imm)
// LR (load-reserved): INSTR (addr), dst, 2 operands.
case len(ops) == 2 && isLRInstr(mnem):
rs1, _ := memFromOperandWithFrame(ops[0], fi)
rd := regFromOperand(ops[1])
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvAMOType(enc, rd, rs1, 0) // rs2=0 for LR
// SC (store-conditional): INSTR src, (addr), dst, 3 operands.
case len(ops) == 3 && isSCInstr(mnem):
rs2 := regFromOperand(ops[0])
rs1, _ := memFromOperandWithFrame(ops[1], fi)
rd := regFromOperand(ops[2])
if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvAMOType(enc, rd, rs1, rs2)
// FP compare: Go reverses the ISA order, writing rs2, rs1, rd.
case len(ops) == 3 && isFPCmpInstr(mnem):
rs2 := regFromOperand(ops[0])
rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2])
if rd < 0 || rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvRType(enc, rd, rs1, rs2)
// I-type with immediate: Plan 9 order is INSTR $imm, rs1, rd; the
// two-operand form INSTR $imm, rd uses rd as the source.
case len(ops) == 3 && isITypeInstr(mnem):
imm := immFromOperand(ops[0]) // immediate
rs1 := regFromOperand(ops[1]) // source register
rd := regFromOperand(ops[2]) // destination
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
return encodeRISCVItypeImmediate(mnem, enc, rd, rs1, imm)
case len(ops) == 2 && isITypeInstr(mnem):
imm := immFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
return encodeRISCVItypeImmediate(mnem, enc, rd, rd, imm)
// Loads: rd, offset(rs1), Plan 9 order is LD src, dst.
case len(ops) == 2 && isLoadInstr(mnem):
rd := regFromOperand(ops[1]) // destination (last operand)
rs1, imm := memFromOperandWithFrame(ops[0], fi) // memory source (first operand)
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvIType(enc, rd, rs1, imm)
// Stores: Plan 9 order is SD src, dst (src=register, dst=memory).
case len(ops) == 2 && isStoreInstr(mnem):
rs2 := regFromOperand(ops[0]) // source register (first operand)
rs1, imm := memFromOperandWithFrame(ops[1], fi) // memory dest (last operand)
if rs2 < 0 || rs1 < 0 {
return nil, fmt.Errorf("invalid operand in %s", mnem)
}
word = riscvSType(enc, rs1, rs2, imm)
// Branches: rs1, rs2, label.
case len(ops) == 3 && isBranchInstr(mnem):
rs1 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1])
target := labelFromOperand(ops[2])
targetOff, ok := offsets[target]
if !ok {
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
}
offset := int32(targetOff - pc)
if rs1 < 0 || rs2 < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
// The Go assembler never compresses branches to C.BEQZ/C.BNEZ.
word = riscvBType(enc, rs1, rs2, offset)
// U-type: rd, imm.
case len(ops) == 2 && isUTypeInstr(mnem):
rd := regFromOperand(ops[0])
imm := immFromOperand(ops[1])
if rd < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
word = riscvUType(enc, rd, imm)
default:
return nil, fmt.Errorf("cannot encode %s with %d operands", mnem, len(ops))
}
// Emit as little-endian 32-bit word.
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, fi riscvFrameInfo, relocs *[]Reloc) ([]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) {
// MOV $sym(SB), rd, load address of a static symbol or external.
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
rd := regFromOperand(dst)
if rd < 0 {
return nil, fmt.Errorf("MOV $sym(SB): invalid destination register")
}
return encodeRISCVSBAddr(src.Imm.Sym, rd, relocs), nil
}
// MOV $sym(FP/SP), rd, not supported: immediate symbol references
// other than SB cannot be encoded as a simple immediate.
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo != "" {
return nil, fmt.Errorf("MOV $%s(%s): unsupported immediate symbol reference (only SB is supported)", src.Imm.Sym.Name, src.Imm.Sym.Pseudo)
}
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)
// MOV sym(SB), rd, load from static data.
if src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB" {
if rd < 0 {
return nil, fmt.Errorf("MOV sym(SB): invalid destination register")
}
return encodeRISCVSBLoad(src.Addr.Sym, rd, relocs), nil
}
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)
// MOV rd, sym(SB), store to static data.
if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" {
if rs2 < 0 {
return nil, fmt.Errorf("MOV rd, sym(SB): invalid source register")
}
return encodeRISCVSBStore(dst.Addr.Sym, rs2, relocs), nil
}
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
}
}
2026-08-13 17:41:16 +02:00
// encodeRISCVLoadImm encodes loading an immediate into a register (MOV $imm,
// rd), matching the toolchain's instructionsForMOVConst. For 12-bit
// immediates it emits ADDI $imm, ZERO, rd (compressed to C.LI when it fits
// six signed bits); for larger immediates it emits LUI + [ADDIW], with the LUI
// and ADDIW compressed to C.LUI / C.ADDIW when their immediate fits.
func encodeRISCVLoadImm(rd int, imm int32) []byte {
if imm >= -2048 && imm <= 2047 {
2026-08-13 17:41:16 +02:00
if rd != 0 && imm >= -32 && imm <= 31 {
return word16(rvcCI(0x2, uint32(rd), uint32(imm)&0x3F)) // C.LI
}
return wordLE(riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, 0, imm))
}
2026-08-13 17:41:16 +02:00
low, high := splitRISCV32Imm(imm)
var out []byte
2026-08-13 17:41:16 +02:00
if rd != 0 && rd != 2 && high >= -32 && high <= 31 {
out = append(out, word16(rvcCI(0x3, uint32(rd), uint32(high)&0x3F))...) // C.LUI
} else {
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, rd, high<<12))...)
}
if low != 0 {
if low >= -32 && low <= 31 {
out = append(out, word16(rvcCI(0x1, uint32(rd), uint32(low)&0x3F))...) // C.ADDIW
} else {
out = append(out, wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, rd, rd, low))...)
}
}
return out
}
2026-08-13 17:41:16 +02:00
// riscvMovImmSize returns the encoded byte length of MOV $imm, rd, mirroring
// encodeRISCVLoadImm's expansion and compression.
func riscvMovImmSize(rd int, imm int32) int {
if imm >= -2048 && imm <= 2047 {
if rd != 0 && imm >= -32 && imm <= 31 {
return 2 // C.LI
}
return 4 // ADDI
}
low, high := splitRISCV32Imm(imm)
size := 0
if rd != 0 && rd != 2 && high >= -32 && high <= 31 {
size += 2 // C.LUI
} else {
size += 4 // LUI
}
if low != 0 {
if low >= -32 && low <= 31 {
size += 2 // C.ADDIW
} else {
size += 4 // ADDIW
}
}
return size
}
// splitRISCV32Imm splits a signed 32-bit immediate into a signed 12-bit low
// part and a signed 20-bit high part, mirroring cmd/internal/obj/riscv's
// Split32BitImmediate. The high part is returned unshifted; callers place it
// in the upper bits of LUI (or its compressed C.LUI form).
func splitRISCV32Imm(imm int32) (low, high int32) {
if imm >= -2048 && imm <= 2047 {
return imm, 0
}
h := int64(imm) >> 12
if imm&(1<<11) != 0 {
h++
}
low = int32((int64(imm) << 52) >> 52) // sign extend 12 bits
high = int32((h << 44) >> 44) // sign extend 20 bits
return low, high
}
// encodeRISCVItypeImmediate encodes an I-type arithmetic instruction, expanding
// large immediates for ADDI/ANDI/ORI/XORI into LUI+ADDIW+op (or two ADDIs for
// ADDI), matching the Go assembler.
func encodeRISCVItypeImmediate(mnem string, enc riscvEnc, rd, rs1 int, imm int32) ([]byte, error) {
if imm >= -2048 && imm <= 2047 {
return wordLE(riscvIType(enc, rd, rs1, imm)), nil
}
var opMn string
switch mnem {
case "ADDI":
opMn = "ADD"
case "ANDI":
opMn = "AND"
case "ORI":
opMn = "OR"
case "XORI":
opMn = "XOR"
default:
return nil, fmt.Errorf("%s: immediate %d does not fit 12 bits", mnem, imm)
}
// ADDI with a small-ish immediate splits into two ADDIs.
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
imm0 := imm / 2
imm1 := imm - imm0
var out []byte
out = append(out, wordLE(riscvIType(enc, rd, rs1, imm0))...)
out = append(out, wordLE(riscvIType(enc, rd, rd, imm1))...)
return out, nil
}
// LUI $high, TMP; [ADDIW $low, TMP, TMP]; op TMP, rs1, rd. The LUI and
// ADDIW compress to their RVC forms (C.LUI / C.ADDIW) when the immediate
// fits 6 signed bits, matching the toolchain's compress pass.
low, high := splitRISCV32Imm(imm)
tmp := 31 // X31 = T6 = TMP
var out []byte
if high != 0 && high >= -32 && high <= 31 {
out = append(out, word16(rvcCI(0x3, uint32(tmp), uint32(high)&0x3F))...)
} else {
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, tmp, high<<12))...)
}
if low != 0 {
if low >= -32 && low <= 31 {
out = append(out, word16(rvcCI(0x1, uint32(tmp), uint32(low)&0x3F))...)
} else {
out = append(out, wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, tmp, tmp, low))...)
}
}
opEnc, ok := riscvInstrTable[opMn]
if !ok {
return nil, fmt.Errorf("%s: unsupported operation %q", mnem, opMn)
}
out = append(out, wordLE(riscvRType(opEnc, rd, rs1, tmp))...)
return out, nil
}
// riscvItypeImmediateSize returns the encoded byte length of an I-type
// immediate instruction, accounting for the large-immediate expansion.
func riscvItypeImmediateSize(mnem string, imm int32) int {
if imm >= -2048 && imm <= 2047 {
return 4
}
switch mnem {
case "ADDI", "ANDI", "ORI", "XORI":
default:
return 4
}
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
return 8
}
low, high := splitRISCV32Imm(imm)
size := 4 // the R-type op (TMP is X31, never compressed)
if high != 0 && high >= -32 && high <= 31 {
size += 2 // C.LUI
} else {
size += 4 // LUI
}
if low != 0 {
if low >= -32 && low <= 31 {
size += 2 // C.ADDIW
} else {
size += 4 // ADDIW
}
}
return size
}
// encodeRISCVSBAddr emits AUIPC + ADDI to load the address of a static
// symbol into rd, recording the single R_RISCV_PCREL_ITYPE relocation the Go
// toolchain uses for the pair (the object-file emitters expand or map it).
func encodeRISCVSBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
name := sym.Name
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
}
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
addi := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rd, 0)
return append(wordLE(auipc), wordLE(addi)...)
}
// encodeRISCVSBLoad emits AUIPC + LD to load from a static symbol into rd,
// recording the single R_RISCV_PCREL_ITYPE relocation for the pair.
func encodeRISCVSBLoad(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
name := sym.Name
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
}
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
ld := riscvIType(riscvEnc{0x03, 0x3, 0x00}, rd, rd, 0)
return append(wordLE(auipc), wordLE(ld)...)
}
// encodeRISCVSBStore emits AUIPC + SD to store a register into a static symbol,
// recording the single R_RISCV_PCREL_STYPE relocation for the pair.
func encodeRISCVSBStore(sym *ast.Symbol, rs2 int, relocs *[]Reloc) []byte {
tmp := 31 // X31 = T6
name := sym.Name
if relocs != nil {
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELSType, Addend: sym.Offset})
}
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, tmp, 0)
sd := riscvSType(riscvEnc{0x23, 0x3, 0x00}, tmp, rs2, 0)
var out []byte
out = append(out, wordLE(auipc)...)
out = append(out, wordLE(sd)...)
return out
}
// wordLE encodes a uint32 as 4 little-endian bytes.
func wordLE(w uint32) []byte {
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
}
// word16 encodes a uint16 as 2 little-endian bytes.
func word16(w uint16) []byte {
return []byte{byte(w), byte(w >> 8)}
}
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
ops := instr.Operands
if len(ops) == 2 {
rs1 := regFromOperand(ops[0])
rd := regFromOperand(ops[1])
if rd < 0 || rs1 < 0 {
return nil, fmt.Errorf("JALR: invalid register operand")
}
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)
return wordLE(word), nil
}
if len(ops) == 1 {
rs1, imm := memFromOperandWithFrame(ops[0], fi)
if rs1 < 0 {
return nil, fmt.Errorf("JALR: invalid memory operand")
}
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)
return wordLE(word), nil
}
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
}
// 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 "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 rvcLSP(0x3, uint32(rd), uint32(imm)), true
}
// Register-relative C.LD: both in prime regs, 8-byte scaled offset.
if rs1 != -1 && rd != -1 && isRVCIntReg(rd) && isRVCIntReg(rs1) && imm >= 0 && imm < 256 && imm%8 == 0 {
return rvcCL(0x3, rvcReg3(rd), rvcReg3(rs1), uint32(imm)), 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 rvcSSP(0x7, uint32(rs2), uint32(imm)), 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 {
return rvcSSP(0x7, uint32(rs2), uint32(imm)), true
}
// Register-relative C.SD: base and source in prime regs.
if rs1 != -1 && rs2 != -1 && isRVCIntReg(rs1) && isRVCIntReg(rs2) && imm >= 0 && imm < 256 && imm%8 == 0 {
return rvcCS(0x7, rvcReg3(rs2), rvcReg3(rs1), uint32(imm)), true
}
case "LW":
rd, rs1, imm := extractLDParams(instr, fi)
if rs1 == 2 && rd != 0 && rd != -1 && imm >= 0 && imm < 256 && imm%4 == 0 {
return rvcLSP(0x2, uint32(rd), uint32(imm)), true
}
if rs1 != -1 && rd != -1 && isRVCIntReg(rd) && isRVCIntReg(rs1) && imm >= 0 && imm < 128 && imm%4 == 0 {
return rvcCL(0x2, rvcReg3(rd), rvcReg3(rs1), uint32(imm)), true
}
case "SW":
rs2, rs1, imm := extractSDParams(instr, fi)
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 256 && imm%4 == 0 {
return rvcSSP(0x6, uint32(rs2), uint32(imm)), true
}
if rs1 != -1 && rs2 != -1 && isRVCIntReg(rs1) && isRVCIntReg(rs2) && imm >= 0 && imm < 128 && imm%4 == 0 {
return rvcCS(0x6, rvcReg3(rs2), rvcReg3(rs1), uint32(imm)), true
}
case "ADDI":
rd, rs1, imm := extractITypeParams(instr)
if rd == -1 || rs1 == -1 {
return 0, false
}
if rd == 2 && rs1 == 2 && imm != 0 && imm%16 == 0 && imm >= -512 && imm <= 511 {
// C.ADDI16SP: ADDI to SP by a nonzero 16-byte multiple.
return rvcADDI16SP(2, imm), true
}
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 isRVCIntReg(rd) && rs1 == 2 && imm != 0 && imm >= 0 && imm < 1024 && imm%4 == 0 {
// C.ADDI4SPN: ADDI $imm, SP, rd for a prime rd.
return rvcCIW(0x0, rvcReg3(rd), uint32(imm)), 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
}
if rd == 0 && rs1 == 0 && imm == 0 {
// C.NOP
return 0x0001, true
}
case "JAL":
// JAL/JMP are never compressed to C.J by the Go assembler.
return 0, false
case "JMP":
// JAL/JMP are never compressed to C.J by the Go assembler.
return 0, false
case "BEQ":
// Branches are never compressed to C.BEQZ/C.BNEZ.
return 0, false
case "BNE":
// Branches are never compressed to C.BEQZ/C.BNEZ.
return 0, false
case "ADD":
// ADD rs2, rs1, rd → C.ADD (CR-type, funct4=0x9) when rd == rs1; ADD
// is commutative, so if rd == rs2, swap. ADD rs2, X0, rd is C.MV.
if len(ops) == 3 {
rs2 := regFromOperand(ops[0])
rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2])
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
if rd == rs1 && rs2 != 0 {
return rvcCR(0x9, uint32(rd), uint32(rs2)), true
}
if rd == rs2 && rs1 != 0 {
return rvcCR(0x9, uint32(rd), uint32(rs1)), true
}
if rs1 == 0 && rs2 != 0 {
// ADD rs2, X0, rd → C.MV rd, rs2.
return rvcCR(0x8, uint32(rd), uint32(rs2)), true
}
}
}
case "SUB", "XOR", "OR", "AND":
// C.SUB (0x23,0), C.XOR (0x23,1), C.OR (0x23,2), C.AND (0x23,3)
if len(ops) == 3 {
var funct2 uint32
switch mnem {
case "SUB":
funct2 = 0x0
case "XOR":
funct2 = 0x1
case "OR":
funct2 = 0x2
case "AND":
funct2 = 0x3
}
rs2 := regFromOperand(ops[0])
rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2])
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
if rd == rs1 && isRVCIntReg(rd) && isRVCIntReg(rs2) && rs2 != 0 {
return rvcCA(0x23, funct2, rvcReg3(rd), rvcReg3(rs2)), true
}
// AND/OR/XOR are commutative; SUB is not.
if mnem != "SUB" && rd == rs2 && isRVCIntReg(rd) && isRVCIntReg(rs1) && rs1 != 0 {
return rvcCA(0x23, funct2, rvcReg3(rd), rvcReg3(rs1)), true
}
}
}
case "ADDW", "SUBW":
// C.ADDW (0x27,1) / C.SUBW (0x27,0), CA-type, prime regs.
if len(ops) == 3 {
funct2 := uint32(0x0)
if mnem == "ADDW" {
funct2 = 0x1
}
rs2 := regFromOperand(ops[0])
rs1 := regFromOperand(ops[1])
rd := regFromOperand(ops[2])
if rd != -1 && rs1 != -1 && rs2 != -1 && isRVCIntReg(rd) {
if rd == rs1 && isRVCIntReg(rs2) {
return rvcCA(0x27, funct2, rvcReg3(rd), rvcReg3(rs2)), true
}
// ADDW is commutative; SUBW is not.
if mnem == "ADDW" && isRVCIntReg(rs1) && rd == rs2 {
return rvcCA(0x27, funct2, rvcReg3(rd), rvcReg3(rs1)), true
}
}
}
case "FLD":
// FLD rd, imm(SP) → C.FLDSP (CI-type, funct3=0x1).
rd, rs1, imm := extractLDParams(instr, fi)
if rs1 == 2 && rd != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
return rvcLSP(0x1, uint32(rd), uint32(imm)), true
}
// Register-relative C.FLD: rd in F8-F15, base in X8-X15.
if rs1 != -1 && rd != -1 && rd >= 8 && rd <= 15 && isRVCIntReg(rs1) && imm >= 0 && imm < 256 && imm%8 == 0 {
return rvcCL(0x1, uint32(rd-8), rvcReg3(rs1), uint32(imm)), true
}
case "FSD":
// FSD rs2, imm(SP) → C.FSDSP (CSS-type, funct3=0x5).
rs2, rs1, imm := extractSDParams(instr, fi)
if rs1 == 2 && rs2 != -1 && imm >= 0 && imm < 512 && imm%8 == 0 {
return rvcSSP(0x5, uint32(rs2), uint32(imm)), true
}
// Register-relative C.FSD: source in F8-F15, base in X8-X15.
if rs1 != -1 && rs2 != -1 && rs2 >= 8 && rs2 <= 15 && isRVCIntReg(rs1) && imm >= 0 && imm < 256 && imm%8 == 0 {
return rvcCS(0x5, uint32(rs2-8), rvcReg3(rs1), uint32(imm)), true
}
case "LUI":
// LUI rd, imm → C.LUI when rd≠0, rd≠SP, imm nonzero and fits in six
// signed bits (matching the toolchain's compress pass).
if len(ops) == 2 {
rd := regFromOperand(ops[0])
imm := immFromOperand(ops[1])
if rd != -1 && rd != 0 && rd != 2 && imm != 0 && imm >= -32 && imm <= 31 {
return rvcCI(0x3, uint32(rd), uint32(imm)&0x3F), true
}
}
case "ADDIW":
rd, rs1, imm := extractITypeParams(instr)
if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 {
return rvcCI(0x1, uint32(rd), uint32(imm)&0x3F), true
}
case "SLLI", "SRLI", "SRAI":
rd, rs1, imm := extractITypeParams(instr)
if rd == rs1 && rd != 0 && imm != 0 && imm >= 1 && imm <= 63 {
if mnem == "SLLI" {
// C.SLLI: funct3=0, op=10 quadrant, shamt in bits [12|6:2].
return rvcSLLI(uint32(rd), uint32(imm)&0x3F), true
}
if isRVCIntReg(rd) {
funct2 := uint32(0x0)
if mnem == "SRAI" {
funct2 = 0x1
}
// C.SRLI/C.SRAI: CB-type, funct3=0x4.
return rvcCBShift(funct2, rvcReg3(rd), uint32(imm)&0x3F), true
}
}
case "ANDI":
rd, rs1, imm := extractITypeParams(instr)
if isRVCIntReg(rd) && rd == rs1 && imm >= -32 && imm <= 31 {
// C.ANDI: CB-type, funct3=0x4, funct2=0x2.
return rvcCBShift(0x2, rvcReg3(rd), uint32(imm)&0x3F), true
}
case "EBREAK":
// C.EBREAK: CR-type, funct4=0x9, rd=0, rs2=0.
return rvcCR(0x9, 0, 0), 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. The Plan 9 order is INSTR $imm, rs1, rd (3 operands) or
// INSTR $imm, rd (2 operands, rd is also the source).
func extractITypeParams(instr *ast.Instr) (rd, rs1 int, imm int32) {
ops := instr.Operands
switch len(ops) {
case 3:
imm = immFromOperand(ops[0])
rs1 = regFromOperand(ops[1])
rd = regFromOperand(ops[2])
case 2:
imm = immFromOperand(ops[0])
rd = regFromOperand(ops[1])
rs1 = rd
default:
return -1, -1, 0
}
return
}
// Instruction type classifiers.
func isRTypeInstr(m string) bool {
switch m {
case "ADD", "SUB", "SLL", "SLT", "SLTU", "XOR", "SRL", "SRA", "OR", "AND",
"ADDW", "SUBW", "SLLW", "SRLW", "SRAW",
"MUL", "MULH", "MULHSU", "MULHU", "DIV", "DIVU", "REM", "REMU",
"MULW", "DIVW", "DIVUW", "REMW", "REMUW":
return true
}
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":
return true
}
return false
}
func isLoadInstr(m string) bool {
switch m {
case "LB", "LH", "LW", "LD", "LBU", "LHU", "LWU":
return true
}
return false
}
func isStoreInstr(m string) bool {
switch m {
case "SB", "SH", "SW", "SD":
return true
}
return false
}
func isBranchInstr(m string) bool {
switch m {
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU":
return true
}
return false
}
func isUTypeInstr(m string) bool {
return m == "LUI" || m == "AUIPC"
}
func isAMOInstr(m string) bool {
switch m {
case "AMOSWAPW", "AMOSWAPD", "AMOADDW", "AMOADDD",
"AMOANDW", "AMOANDD", "AMOORW", "AMOORD",
"AMOXORW", "AMOXORD", "AMOMAXW", "AMOMAXD",
"AMOMINW", "AMOMIND", "AMOMAXUW", "AMOMAXUD",
"AMOMINUW", "AMOMINUD":
return true
}
return false
}
func isFPArithInstr(m string) bool {
switch m {
case "FADDS", "FSUBS", "FMULS", "FDIVS",
"FADDD", "FSUBD", "FMULD", "FDIVD",
"FSQRTS", "FSQRTD", "FMINS", "FMAXS", "FMIND", "FMAXD":
return true
}
return false
}
func isFPLoadInstr(m string) bool {
return m == "FLW" || m == "FLD"
}
func isFPStoreInstr(m string) bool {
return m == "FSW" || m == "FSD"
}
func isLRInstr(m string) bool {
return m == "LRW" || m == "LRD"
}
func isSCInstr(m string) bool {
return m == "SCW" || m == "SCD"
}
func isFPCmpInstr(m string) bool {
switch m {
case "FEQS", "FLTS", "FLES", "FEQD", "FLTD", "FLED":
return true
}
return false
}
// Operand helpers.
func regFromOperand(op *ast.Operand) int {
// Register is in Addr.Base (from (base) syntax) or Addr.Sym.Name (bare ident).
if op.Addr.Base != "" {
return riscvRegNum(op.Addr.Base)
}
if op.Addr.Sym != nil && op.Addr.Sym.Name != "" {
return riscvRegNum(op.Addr.Sym.Name)
}
return -1
}
func immFromOperand(op *ast.Operand) int32 {
if op.Imm.HasVal {
v := op.Imm.Val
if op.Imm.Neg {
v = -v
}
return int32(v)
}
return 0
}
func memFromOperand(op *ast.Operand) (rs1 int, imm int32) {
rs1 = riscvRegNum(op.Addr.Base)
imm = int32(op.Addr.Offset)
return
}
// memFromOperandWithFrame resolves a memory operand, handling FP/SP
// pseudo-registers via the frame mapping.
func memFromOperandWithFrame(op *ast.Operand, fi riscvFrameInfo) (rs1 int, imm int32) {
// Check for a pseudo-register reference (name+offset(FP) or name+offset(SP)).
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo != "" {
return riscvResolvePseudo(op.Addr.Sym, fi)
}
// Plain register+offset memory reference.
return memFromOperand(op)
}
func labelFromOperand(op *ast.Operand) string {
if op.Addr.Sym != nil {
return op.Addr.Sym.Name
}
return op.Raw
}
// suggestLabel returns a "did you mean" suggestion for an undefined label.
func suggestLabel(target string, offsets map[string]int) string {
if len(offsets) == 0 {
return ""
}
// Find the closest matching label using Levenshtein distance.
bestDist := len(target) + 1
var best string
for name := range offsets {
dist := levenshtein(target, name)
if dist < bestDist {
bestDist = dist
best = name
}
}
// Only suggest if the distance is small enough.
if bestDist <= 3 && bestDist < len(target)/2+1 {
return fmt.Sprintf("; did you mean %q?", best)
}
return ""
}
// levenshtein computes the Levenshtein distance between two strings.
func levenshtein(a, b string) int {
la, lb := len(a), len(b)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
// Create a matrix of distances.
prev := make([]int, lb+1)
curr := make([]int, lb+1)
for j := 0; j <= lb; j++ {
prev[j] = j
}
for i := 1; i <= la; i++ {
curr[0] = i
for j := 1; j <= lb; j++ {
cost := 1
if a[i-1] == b[j-1] {
cost = 0
}
curr[j] = min3(curr[j-1]+1, prev[j]+1, prev[j-1]+cost)
}
prev, curr = curr, prev
}
return prev[lb]
}
func min3(a, b, c int) int {
if a < b {
if a < c {
return a
}
return c
}
if b < c {
return b
}
return c
}