2026-08-01 19:51:00 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package asm
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2026-08-13 14:41:57 +02:00
|
|
|
"strings"
|
2026-08-01 19:51:00 +02:00
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
|
2026-08-03 08:51:00 +02:00
|
|
|
// It handles the full RV64IMAFDC instruction set including RVC compression.
|
2026-08-13 14:41:57 +02:00
|
|
|
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
2026-08-02 00:18:00 +02:00
|
|
|
fi := riscvComputeFrame(t)
|
|
|
|
|
prologue := riscvPrologue(fi)
|
2026-09-19 23:49:13 +02:00
|
|
|
guardLen, err := riscvGuardLen(fi)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, nil, nil, nil, err
|
|
|
|
|
}
|
2026-08-02 00:18:00 +02:00
|
|
|
|
2026-08-03 08:51:00 +02:00
|
|
|
var relocs []Reloc
|
2026-08-13 14:41:57 +02:00
|
|
|
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.
|
2026-09-14 21:08:00 +02:00
|
|
|
// The guard prefix shifts its PC.
|
2026-08-13 14:41:57 +02:00
|
|
|
if fi.autosize != 0 {
|
2026-09-14 21:08:00 +02:00
|
|
|
spadj = append(spadj, SpadjStep{PC: guardLen + riscvPrologueSpadjPC(fi), Value: fi.autosize})
|
2026-08-13 14:41:57 +02:00
|
|
|
}
|
2026-08-03 08:51:00 +02:00
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
// 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
|
2026-08-01 19:51:00 +02:00
|
|
|
offsets := map[string]int{}
|
2026-09-14 21:08:00 +02:00
|
|
|
pos := guardLen + len(prologue)
|
2026-08-01 19:51:00 +02:00
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
switch s := stmt.(type) {
|
|
|
|
|
case *ast.Label:
|
|
|
|
|
offsets[s.Name.Text] = pos
|
|
|
|
|
case *ast.Instr:
|
2026-08-02 18:22:00 +02:00
|
|
|
recs = append(recs, instrRec{instr: s})
|
2026-08-13 14:41:57 +02:00
|
|
|
pos += riscvInstrSize(s, fi)
|
2026-08-01 19:51:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
// Pass 2: encode each instruction using Pass-1 offsets.
|
|
|
|
|
pc := len(prologue)
|
|
|
|
|
for i := range recs {
|
2026-08-03 08:51:00 +02:00
|
|
|
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil) // no relocs in Pass 2
|
2026-08-01 19:51:00 +02:00
|
|
|
if err != nil {
|
2026-08-13 14:41:57 +02:00
|
|
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
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)}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:13 +02:00
|
|
|
// Pass 4: recompute offsets with actual sizes. recs holds the
|
|
|
|
|
// instructions in emission order, so an index into it walks t.Body in
|
|
|
|
|
// lockstep (the same single pass Pass 1 uses) instead of rescanning the
|
|
|
|
|
// whole slice per statement.
|
2026-08-02 18:22:00 +02:00
|
|
|
offsets = map[string]int{}
|
2026-09-14 21:08:00 +02:00
|
|
|
pos = guardLen + len(prologue)
|
2026-09-19 23:49:13 +02:00
|
|
|
ri := 0
|
2026-08-02 18:22:00 +02:00
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
switch s := stmt.(type) {
|
|
|
|
|
case *ast.Label:
|
|
|
|
|
offsets[s.Name.Text] = pos
|
|
|
|
|
case *ast.Instr:
|
2026-09-19 23:49:13 +02:00
|
|
|
pos += len(recs[ri].code)
|
|
|
|
|
ri++
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 08:51:00 +02:00
|
|
|
// Pass 5: re-encode branches with corrected offsets. Record relocations
|
2026-09-14 21:08:00 +02:00
|
|
|
// during this final pass (relocation offsets are relative to instruction
|
|
|
|
|
// start). The guard prefix precedes the prologue; its branches target
|
|
|
|
|
// the morestack block at the end of the function, which the previous
|
|
|
|
|
// passes have sized.
|
|
|
|
|
var out []byte
|
2026-09-19 23:49:13 +02:00
|
|
|
guardBytes, guardReloc, err := riscvGuard(fi)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, nil, nil, nil, err
|
|
|
|
|
}
|
2026-09-14 21:08:00 +02:00
|
|
|
if fi.needSplit {
|
|
|
|
|
out = append(out, guardBytes...)
|
|
|
|
|
}
|
|
|
|
|
out = append(out, prologue...)
|
|
|
|
|
pc = guardLen + len(prologue)
|
2026-08-03 08:51:00 +02:00
|
|
|
preCount := len(relocs)
|
2026-08-13 14:41:57 +02:00
|
|
|
var lines []LineEntry
|
2026-08-02 18:22:00 +02:00
|
|
|
for _, r := range recs {
|
2026-08-13 14:41:57 +02:00
|
|
|
lines = append(lines, LineEntry{Offset: pc, Line: r.instr.Pos().Line})
|
2026-08-02 18:22:00 +02:00
|
|
|
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
|
|
|
|
|
out = append(out, r.code...)
|
|
|
|
|
pc += len(r.code)
|
|
|
|
|
} else {
|
2026-08-03 08:51:00 +02:00
|
|
|
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
|
2026-08-02 18:22:00 +02:00
|
|
|
if err != nil {
|
2026-08-13 14:41:57 +02:00
|
|
|
return nil, nil, nil, nil, nil, err
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
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.
|
2026-08-03 08:51:00 +02:00
|
|
|
for j := preCount; j < len(relocs); j++ {
|
2026-08-13 18:12:22 +02:00
|
|
|
relocs[j].Off += pc
|
|
|
|
|
relocs[j].After += pc
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
preCount = len(relocs)
|
2026-08-13 14:41:57 +02:00
|
|
|
// 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})
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
out = append(out, code...)
|
|
|
|
|
pc += len(code)
|
2026-08-01 19:51:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-14 21:08:00 +02:00
|
|
|
if fi.needSplit {
|
|
|
|
|
relocs = append(relocs, guardReloc)
|
|
|
|
|
}
|
2026-08-13 14:41:57 +02:00
|
|
|
return out, offsets, relocs, lines, spadj, nil
|
2026-08-01 19:51:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 19:58:43 +02:00
|
|
|
// riscvImmAlias maps the R-type ALU mnemonics onto their I-type immediate
|
|
|
|
|
// forms: the toolchain accepts ADD $imm, rj, rd and emits addi. Applied
|
|
|
|
|
// whenever the first operand is an immediate.
|
|
|
|
|
var riscvImmAlias = map[string]string{
|
|
|
|
|
"ADD": "ADDI",
|
|
|
|
|
"ADDW": "ADDIW",
|
|
|
|
|
"AND": "ANDI",
|
|
|
|
|
"OR": "ORI",
|
|
|
|
|
"XOR": "XORI",
|
|
|
|
|
"SLL": "SLLI",
|
|
|
|
|
"SRL": "SRLI",
|
|
|
|
|
"SRA": "SRAI",
|
|
|
|
|
"SLLW": "SLLIW",
|
|
|
|
|
"SRLW": "SRLIW",
|
|
|
|
|
"SRAW": "SRAIW",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// riscvNormaliseImmAlias rewrites the mnemonic to its immediate form when the
|
|
|
|
|
// first operand is an immediate: the toolchain accepts ADD $imm, rj, rd and
|
|
|
|
|
// emits addi, and SUB $imm becomes addi with the negated immediate. The
|
|
|
|
|
// second result reports that negation; the operand itself is left untouched
|
|
|
|
|
// because several passes normalise the same instruction.
|
|
|
|
|
func riscvNormaliseImmAlias(mnem string, ops []*ast.Operand) (string, bool) {
|
|
|
|
|
if len(ops) >= 2 && isImmOperand(ops[0]) {
|
|
|
|
|
switch strings.ToUpper(mnem) {
|
|
|
|
|
case "SUB":
|
|
|
|
|
return "ADDI", true
|
|
|
|
|
case "SUBW":
|
|
|
|
|
return "ADDIW", true
|
|
|
|
|
}
|
|
|
|
|
if alias, ok := riscvImmAlias[strings.ToUpper(mnem)]; ok {
|
|
|
|
|
return alias, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return mnem, false
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
// 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.
|
2026-08-13 14:41:57 +02:00
|
|
|
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
2026-08-02 18:22:00 +02:00
|
|
|
mnem := instr.Mnemonic.Text
|
2026-08-03 08:51:00 +02:00
|
|
|
ops := instr.Operands
|
2026-09-19 19:58:43 +02:00
|
|
|
var immNeg bool
|
|
|
|
|
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
|
2026-08-13 14:41:57 +02:00
|
|
|
if mnem == "RET" {
|
|
|
|
|
return len(riscvReturn(fi))
|
|
|
|
|
}
|
2026-09-19 19:58:43 +02:00
|
|
|
if strings.HasPrefix(mnem, "MOV") && len(ops) == 2 {
|
2026-08-03 08:51:00 +02:00
|
|
|
// 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]))
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
2026-09-14 23:25:14 +02:00
|
|
|
// Frame-relative loads and stores: a frame offset beyond the signed
|
|
|
|
|
// 12-bit range materialises the address in X31 first.
|
|
|
|
|
if isMemOperand(ops[0]) && !isMemOperand(ops[1]) {
|
|
|
|
|
return riscvFrameMemSize(ops[0], fi)
|
|
|
|
|
}
|
|
|
|
|
if isMemOperand(ops[1]) && !isMemOperand(ops[0]) {
|
|
|
|
|
return riscvFrameMemSize(ops[1], fi)
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
2026-08-13 16:04:08 +02:00
|
|
|
// 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]) {
|
2026-09-19 19:58:43 +02:00
|
|
|
imm := immFromOperand(ops[0])
|
|
|
|
|
if immNeg {
|
|
|
|
|
imm = -imm
|
|
|
|
|
}
|
|
|
|
|
return riscvItypeImmediateSize(mnem, imm)
|
2026-08-13 16:04:08 +02:00
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:13 +02:00
|
|
|
// riscvCheckBranchOffset rejects a B-type displacement outside its signed
|
|
|
|
|
// 13-bit span [-4096, 4094]; the encoder masks to 13 bits, so an
|
|
|
|
|
// out-of-range offset would otherwise wrap to a wrong target.
|
|
|
|
|
func riscvCheckBranchOffset(target string, off int32) error {
|
|
|
|
|
if off < -4096 || off > 4094 {
|
|
|
|
|
return fmt.Errorf("branch to %q too far (13-bit range)", target)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// riscvCheckJumpOffset rejects a J-type displacement outside its signed
|
|
|
|
|
// 21-bit span [-1048576, 1048574].
|
|
|
|
|
func riscvCheckJumpOffset(target string, off int32) error {
|
|
|
|
|
if off < -1048576 || off > 1048574 {
|
|
|
|
|
return fmt.Errorf("jump to %q too far (21-bit range)", target)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:51:00 +02:00
|
|
|
// encodeRISCVInstr encodes a single RISC-V instruction.
|
2026-08-03 08:51:00 +02:00
|
|
|
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
2026-08-01 19:51:00 +02:00
|
|
|
mnem := instr.Mnemonic.Text
|
2026-08-02 00:18:00 +02:00
|
|
|
ops := instr.Operands
|
2026-09-19 19:58:43 +02:00
|
|
|
var immNeg bool
|
|
|
|
|
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
|
2026-08-02 00:18:00 +02:00
|
|
|
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).
|
2026-08-13 14:41:57 +02:00
|
|
|
return riscvReturn(fi), nil
|
2026-08-02 00:18:00 +02:00
|
|
|
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-03 08:51:00 +02:00
|
|
|
}
|
2026-08-13 18:12:22 +02:00
|
|
|
op := ops[0]
|
|
|
|
|
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
|
2026-09-19 19:17:07 +02:00
|
|
|
// CALL (X5): an indirect call, the toolchain's JALR X1, 0(X5).
|
|
|
|
|
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
|
|
|
|
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
|
|
|
|
return nil, fmt.Errorf("CALL: invalid indirect operand %q", op.Raw)
|
|
|
|
|
}
|
|
|
|
|
rs1 := riscvRegNum(op.Addr.Base)
|
|
|
|
|
if rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("CALL: unknown branch register %q", op.Addr.Base)
|
|
|
|
|
}
|
|
|
|
|
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, 0)
|
|
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
|
|
|
}
|
2026-08-13 18:12:22 +02:00
|
|
|
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})
|
|
|
|
|
}
|
2026-09-14 18:22:18 +02:00
|
|
|
word = riscvJType(1, 0) // JAL X1, 0, the linker fills the offset
|
2026-08-02 00:18:00 +02:00
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
|
|
|
case "JMP":
|
2026-08-13 17:57:10 +02:00
|
|
|
// JMP = JAL X0, target. The Go assembler never compresses this to
|
|
|
|
|
// C.J, so always emit the 32-bit JAL.
|
2026-08-02 00:18:00 +02:00
|
|
|
var target string
|
|
|
|
|
if len(ops) >= 1 {
|
2026-09-14 23:25:14 +02:00
|
|
|
// JMP sym(SB): a tail call, JAL X0 against a symbol relocation.
|
|
|
|
|
if ops[0].Addr.Sym != nil && ops[0].Addr.Sym.Pseudo == "SB" {
|
|
|
|
|
if relocs != nil {
|
|
|
|
|
*relocs = append(*relocs, Reloc{Off: 0, After: 4, Name: ops[0].Addr.Sym.Name, Kind: RelRISCVJal, Addend: ops[0].Addr.Sym.Offset})
|
|
|
|
|
}
|
|
|
|
|
word = riscvJType(0, 0)
|
|
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
|
|
|
}
|
2026-08-02 00:18:00 +02:00
|
|
|
target = labelFromOperand(ops[0])
|
2026-09-19 19:17:07 +02:00
|
|
|
// JMP (X5): an indirect branch, the toolchain's JALR X0, 0(X5).
|
|
|
|
|
if ops[0].Addr.Sym == nil && ops[0].Addr.Base != "" {
|
|
|
|
|
if ops[0].Addr.Offset != 0 || ops[0].Addr.Index != "" {
|
|
|
|
|
return nil, fmt.Errorf("JMP: invalid indirect operand %q", ops[0].Raw)
|
|
|
|
|
}
|
|
|
|
|
rs1 := riscvRegNum(ops[0].Addr.Base)
|
|
|
|
|
if rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("JMP: unknown branch register %q", ops[0].Addr.Base)
|
|
|
|
|
}
|
|
|
|
|
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, rs1, 0)
|
|
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
|
|
|
}
|
2026-08-02 00:18:00 +02:00
|
|
|
}
|
|
|
|
|
targetOff, ok := offsets[target]
|
|
|
|
|
if !ok {
|
2026-08-05 18:52:00 +02:00
|
|
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
2026-08-02 00:18:00 +02:00
|
|
|
}
|
|
|
|
|
offset := int32(targetOff - pc)
|
2026-09-19 23:49:13 +02:00
|
|
|
if err := riscvCheckJumpOffset(target, offset); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-08-02 00:18:00 +02:00
|
|
|
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 {
|
2026-08-05 18:52:00 +02:00
|
|
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
2026-08-02 00:18:00 +02:00
|
|
|
}
|
|
|
|
|
offset := int32(targetOff - pc)
|
2026-09-19 23:49:13 +02:00
|
|
|
if err := riscvCheckJumpOffset(target, offset); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-08-02 00:18:00 +02:00
|
|
|
word = riscvJType(rd, offset)
|
|
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
2026-08-02 18:22:00 +02:00
|
|
|
|
|
|
|
|
// MOV is a pseudo-instruction that the Go assembler uses for loads,
|
2026-09-19 19:58:43 +02:00
|
|
|
// stores, register moves and immediate loads. The width suffixes
|
|
|
|
|
// (MOVB/MOVH/MOVW and unsigned forms) select the access width, and
|
|
|
|
|
// MOVD/MOVF address the FP registers.
|
|
|
|
|
case "MOV", "MOVB", "MOVBU", "MOVH", "MOVHU", "MOVW", "MOVWU", "MOVF", "MOVD":
|
2026-08-29 17:12:53 +02:00
|
|
|
return encodeRISCVMov(instr, fi, relocs)
|
2026-08-03 08:51:00 +02:00
|
|
|
|
|
|
|
|
// JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1).
|
|
|
|
|
case "JALR":
|
|
|
|
|
return encodeRISCVJALR(instr, fi)
|
|
|
|
|
|
2026-09-19 19:58:43 +02:00
|
|
|
// Branch-zero pseudos: BEQZ/BNEZ compare against X0, and BLTZ/BGEZ/
|
|
|
|
|
// BLEZ/BGTZ reorder the register operands of BLT/BGE accordingly.
|
|
|
|
|
case "BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ":
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rs := regFromOperand(ops[0])
|
|
|
|
|
if rs < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s: invalid register", mnem)
|
|
|
|
|
}
|
|
|
|
|
target := labelFromOperand(ops[1])
|
|
|
|
|
targetOff, ok := offsets[target]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
|
|
|
|
}
|
|
|
|
|
var enc riscvEnc
|
|
|
|
|
rs1, rs2 := rs, 0
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "BEQZ":
|
|
|
|
|
enc = riscvEnc{0x63, 0x0, 0x00} // beq rs, x0
|
|
|
|
|
case "BNEZ":
|
|
|
|
|
enc = riscvEnc{0x63, 0x1, 0x00} // bne rs, x0
|
|
|
|
|
case "BLTZ":
|
|
|
|
|
enc = riscvEnc{0x63, 0x4, 0x00} // blt rs, x0
|
|
|
|
|
case "BGEZ":
|
|
|
|
|
enc = riscvEnc{0x63, 0x5, 0x00} // bge rs, x0
|
|
|
|
|
case "BLEZ":
|
|
|
|
|
enc, rs1, rs2 = riscvEnc{0x63, 0x5, 0x00}, 0, rs // bge x0, rs
|
|
|
|
|
case "BGTZ":
|
|
|
|
|
enc, rs1, rs2 = riscvEnc{0x63, 0x4, 0x00}, 0, rs // blt x0, rs
|
|
|
|
|
}
|
2026-09-19 23:49:13 +02:00
|
|
|
if err := riscvCheckBranchOffset(target, int32(targetOff-pc)); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-09-19 19:58:43 +02:00
|
|
|
word = riscvBType(enc, rs1, rs2, int32(targetOff-pc))
|
|
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
|
|
|
|
2026-08-03 08:51:00 +02:00
|
|
|
// System instructions with no operands.
|
|
|
|
|
case "FENCE", "ECALL", "EBREAK":
|
|
|
|
|
enc, ok := riscvInstrTable[mnem]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("unsupported system instruction %q", mnem)
|
|
|
|
|
}
|
2026-09-19 23:49:13 +02:00
|
|
|
// The bare FENCE expands to fence iorw, iorw: the predecessor and
|
|
|
|
|
// successor fields both carry 0xF in the I-type immediate
|
|
|
|
|
// (the toolchain's encodeFenceOperand TYPE_NONE default).
|
|
|
|
|
imm := int32(0)
|
|
|
|
|
if mnem == "FENCE" {
|
|
|
|
|
imm = 0x0FF
|
|
|
|
|
}
|
|
|
|
|
word = riscvIType(enc, 0, 0, imm)
|
2026-08-03 08:51:00 +02:00
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
2026-08-02 00:18:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-02 11:45:00 +02:00
|
|
|
// 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)
|
2026-09-19 23:49:13 +02:00
|
|
|
if csr < 0 || csr > 0xFFF {
|
|
|
|
|
return nil, fmt.Errorf("%s: CSR address %d out of range 0-0xFFF", mnem, csr)
|
|
|
|
|
}
|
|
|
|
|
rd := regFromOperand(ops[2]) // destination register
|
2026-08-02 11:45:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:51:00 +02:00
|
|
|
enc, ok := riscvInstrTable[mnem]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("unsupported RISC-V instruction %q", mnem)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch {
|
2026-08-13 15:13:31 +02:00
|
|
|
// R-type: Go reverses the ISA order, writing rs2, rs1, rd (destination
|
|
|
|
|
// last); the two-operand form INSTR rs2, rd uses rd as rs1.
|
2026-08-01 19:51:00 +02:00
|
|
|
case len(ops) == 3 && isRTypeInstr(mnem):
|
2026-08-13 15:13:31 +02:00
|
|
|
rs2 := regFromOperand(ops[0]) // first operand = rs2
|
|
|
|
|
rs1 := regFromOperand(ops[1]) // second operand = rs1
|
2026-08-02 00:18:00 +02:00
|
|
|
rd := regFromOperand(ops[2]) // destination (last operand)
|
2026-08-01 19:51:00 +02:00
|
|
|
if rd < 0 || rs1 < 0 || rs2 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
word = riscvRType(enc, rd, rs1, rs2)
|
|
|
|
|
|
2026-08-13 15:13:31 +02:00
|
|
|
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.
|
2026-08-02 18:22:00 +02:00
|
|
|
case len(ops) == 3 && isShiftImmInstr(mnem):
|
2026-08-13 15:13:31 +02:00
|
|
|
shamt := int(immFromOperand(ops[0]))
|
|
|
|
|
rs1 := regFromOperand(ops[1])
|
2026-08-02 18:22:00 +02:00
|
|
|
rd := regFromOperand(ops[2])
|
|
|
|
|
if rd < 0 || rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
word = riscvRType(enc, rd, rs1, shamt)
|
|
|
|
|
|
2026-08-13 15:13:31 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-08-02 11:45:00 +02:00
|
|
|
// 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)
|
|
|
|
|
|
2026-08-13 15:13:31 +02:00
|
|
|
// FP arithmetic: Go reverses the ISA order, writing rs2, rs1, rd.
|
2026-08-02 11:45:00 +02:00
|
|
|
case len(ops) == 3 && isFPArithInstr(mnem):
|
2026-08-13 15:13:31 +02:00
|
|
|
rs2 := regFromOperand(ops[0])
|
|
|
|
|
rs1 := regFromOperand(ops[1])
|
2026-08-02 11:45:00 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-09-14 18:22:18 +02:00
|
|
|
// LR (load-reserved): INSTR (addr), dst, 2 operands.
|
2026-08-02 11:45:00 +02:00
|
|
|
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
|
|
|
|
|
|
2026-09-14 18:22:18 +02:00
|
|
|
// SC (store-conditional): INSTR src, (addr), dst, 3 operands.
|
2026-08-02 11:45:00 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-08-13 15:13:31 +02:00
|
|
|
// FP compare: Go reverses the ISA order, writing rs2, rs1, rd.
|
2026-08-02 11:45:00 +02:00
|
|
|
case len(ops) == 3 && isFPCmpInstr(mnem):
|
2026-08-13 15:13:31 +02:00
|
|
|
rs2 := regFromOperand(ops[0])
|
|
|
|
|
rs1 := regFromOperand(ops[1])
|
2026-08-02 11:45:00 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-08-13 15:13:31 +02:00
|
|
|
// I-type with immediate: Plan 9 order is INSTR $imm, rs1, rd; the
|
|
|
|
|
// two-operand form INSTR $imm, rd uses rd as the source.
|
2026-08-01 19:51:00 +02:00
|
|
|
case len(ops) == 3 && isITypeInstr(mnem):
|
2026-09-19 23:49:13 +02:00
|
|
|
imm, err := riscvImm32FromOperand(ops[0], immNeg) // immediate
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-09-19 19:58:43 +02:00
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
rs1 := regFromOperand(ops[1]) // source register
|
2026-08-02 00:18:00 +02:00
|
|
|
rd := regFromOperand(ops[2]) // destination
|
2026-08-01 19:51:00 +02:00
|
|
|
if rd < 0 || rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
|
|
|
|
}
|
2026-08-13 16:04:08 +02:00
|
|
|
return encodeRISCVItypeImmediate(mnem, enc, rd, rs1, imm)
|
2026-08-01 19:51:00 +02:00
|
|
|
|
2026-08-13 15:13:31 +02:00
|
|
|
case len(ops) == 2 && isITypeInstr(mnem):
|
2026-09-19 23:49:13 +02:00
|
|
|
imm, err := riscvImm32FromOperand(ops[0], immNeg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2026-09-19 19:58:43 +02:00
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
rd := regFromOperand(ops[1])
|
|
|
|
|
if rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
|
|
|
|
}
|
2026-08-13 16:04:08 +02:00
|
|
|
return encodeRISCVItypeImmediate(mnem, enc, rd, rd, imm)
|
2026-08-13 15:13:31 +02:00
|
|
|
|
2026-09-14 18:22:18 +02:00
|
|
|
// Loads: rd, offset(rs1), Plan 9 order is LD src, dst.
|
2026-08-01 19:51:00 +02:00
|
|
|
case len(ops) == 2 && isLoadInstr(mnem):
|
2026-08-02 00:18:00 +02:00
|
|
|
rd := regFromOperand(ops[1]) // destination (last operand)
|
|
|
|
|
rs1, imm := memFromOperandWithFrame(ops[0], fi) // memory source (first operand)
|
2026-08-01 19:51:00 +02:00
|
|
|
if rd < 0 || rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
word = riscvIType(enc, rd, rs1, imm)
|
|
|
|
|
|
2026-08-02 00:18:00 +02:00
|
|
|
// Stores: Plan 9 order is SD src, dst (src=register, dst=memory).
|
2026-08-01 19:51:00 +02:00
|
|
|
case len(ops) == 2 && isStoreInstr(mnem):
|
2026-08-02 00:18:00 +02:00
|
|
|
rs2 := regFromOperand(ops[0]) // source register (first operand)
|
|
|
|
|
rs1, imm := memFromOperandWithFrame(ops[1], fi) // memory dest (last operand)
|
2026-08-01 19:51:00 +02:00
|
|
|
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 {
|
2026-08-05 18:52:00 +02:00
|
|
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
2026-08-01 19:51:00 +02:00
|
|
|
}
|
|
|
|
|
offset := int32(targetOff - pc)
|
|
|
|
|
if rs1 < 0 || rs2 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
|
|
|
|
}
|
2026-09-19 23:49:13 +02:00
|
|
|
if err := riscvCheckBranchOffset(target, offset); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-08-03 01:08:00 +02:00
|
|
|
|
2026-08-13 17:57:10 +02:00
|
|
|
// The Go assembler never compresses branches to C.BEQZ/C.BNEZ.
|
2026-08-01 19:51:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
// 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)
|
2026-08-29 17:12:53 +02:00
|
|
|
func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
2026-08-02 18:22:00 +02:00
|
|
|
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) {
|
2026-09-14 18:22:18 +02:00
|
|
|
// MOV $sym(SB), rd, load address of a static symbol or external.
|
2026-08-03 08:51:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-09-14 18:22:18 +02:00
|
|
|
// MOV $sym(FP/SP), rd, not supported: immediate symbol references
|
2026-08-03 08:51:00 +02:00
|
|
|
// 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)
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
rd := regFromOperand(dst)
|
|
|
|
|
if rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("MOV $imm: invalid destination register")
|
|
|
|
|
}
|
2026-09-19 23:49:13 +02:00
|
|
|
imm, err := riscvImm32FromOperand(src, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
return encodeRISCVLoadImm(rd, imm), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Memory → register (load).
|
|
|
|
|
if isMemOperand(src) && !isMemOperand(dst) {
|
|
|
|
|
rd := regFromOperand(dst)
|
2026-09-14 18:22:18 +02:00
|
|
|
// MOV sym(SB), rd, load from static data.
|
2026-08-03 08:51:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
rs1, off := memFromOperandWithFrame(src, fi)
|
|
|
|
|
if rd < 0 || rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("MOV load: invalid operand")
|
|
|
|
|
}
|
2026-09-19 19:58:43 +02:00
|
|
|
return riscvFrameMemOp(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), false), false, rd, rs1, off), nil
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register → memory (store).
|
|
|
|
|
if !isMemOperand(src) && isMemOperand(dst) {
|
|
|
|
|
rs2 := regFromOperand(src)
|
2026-09-14 18:22:18 +02:00
|
|
|
// MOV rd, sym(SB), store to static data.
|
2026-08-03 08:51:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
rs1, off := memFromOperandWithFrame(dst, fi)
|
|
|
|
|
if rs2 < 0 || rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("MOV store: invalid operand")
|
|
|
|
|
}
|
2026-09-19 19:58:43 +02:00
|
|
|
return riscvFrameMemOp(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), true), true, rs2, rs1, off), nil
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 19:58:43 +02:00
|
|
|
// Register → register: MOVD/MOVF are FP moves (fsgnj with rs2 = rs1),
|
|
|
|
|
// everything else is ADDI $0, src, dst.
|
2026-08-02 18:22:00 +02:00
|
|
|
{
|
|
|
|
|
rs1 := regFromOperand(src)
|
|
|
|
|
rd := regFromOperand(dst)
|
|
|
|
|
if rd < 0 || rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("MOV: invalid register operand")
|
|
|
|
|
}
|
2026-09-19 19:58:43 +02:00
|
|
|
mnem := strings.ToUpper(instr.Mnemonic.Text)
|
|
|
|
|
if mnem == "MOVD" || mnem == "MOVF" {
|
|
|
|
|
op := uint32(0x20000053) // FSGNJ.S
|
|
|
|
|
if mnem == "MOVD" {
|
|
|
|
|
op = 0x22000053 // FSGNJ.D
|
|
|
|
|
}
|
|
|
|
|
return wordLE(op | uint32(rs1)<<15 | uint32(rs1)<<20 | uint32(rd)<<7), nil
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
word := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rs1, 0)
|
|
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 19:58:43 +02:00
|
|
|
// riscvMovEnc returns the load (store=false) or store (store=true) opcode for
|
|
|
|
|
// a MOV-family mnemonic: the suffix selects the access width, MOVD and MOVF
|
|
|
|
|
// select the FP load/store opcodes, and bare MOV is the 64-bit integer form.
|
|
|
|
|
func riscvMovEnc(mnem string, store bool) riscvEnc {
|
|
|
|
|
if store {
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "MOVB":
|
|
|
|
|
return riscvEnc{0x23, 0x0, 0x00} // SB
|
|
|
|
|
case "MOVH":
|
|
|
|
|
return riscvEnc{0x23, 0x1, 0x00} // SH
|
|
|
|
|
case "MOVW":
|
|
|
|
|
return riscvEnc{0x23, 0x2, 0x00} // SW
|
|
|
|
|
case "MOVF":
|
|
|
|
|
return riscvEnc{0x27, 0x2, 0x00} // FSW
|
|
|
|
|
case "MOVD":
|
|
|
|
|
return riscvEnc{0x27, 0x3, 0x00} // FSD
|
|
|
|
|
}
|
|
|
|
|
return riscvEnc{0x23, 0x3, 0x00} // SD
|
|
|
|
|
}
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "MOVB":
|
|
|
|
|
return riscvEnc{0x03, 0x0, 0x00} // LB
|
|
|
|
|
case "MOVBU":
|
|
|
|
|
return riscvEnc{0x03, 0x4, 0x00} // LBU
|
|
|
|
|
case "MOVH":
|
|
|
|
|
return riscvEnc{0x03, 0x1, 0x00} // LH
|
|
|
|
|
case "MOVHU":
|
|
|
|
|
return riscvEnc{0x03, 0x5, 0x00} // LHU
|
|
|
|
|
case "MOVW":
|
|
|
|
|
return riscvEnc{0x03, 0x2, 0x00} // LW
|
|
|
|
|
case "MOVWU":
|
|
|
|
|
return riscvEnc{0x03, 0x6, 0x00} // LWU
|
|
|
|
|
case "MOVF":
|
|
|
|
|
return riscvEnc{0x07, 0x2, 0x00} // FLW
|
|
|
|
|
case "MOVD":
|
|
|
|
|
return riscvEnc{0x07, 0x3, 0x00} // FLD
|
|
|
|
|
}
|
|
|
|
|
return riscvEnc{0x03, 0x3, 0x00} // LD
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 23:25:14 +02:00
|
|
|
// riscvFrameMemOp encodes a register-relative load (store=false, I-type
|
|
|
|
|
// width 0x03) or store (store=true, S-type width 0x23) of the 64-bit width
|
|
|
|
|
// at off(rs1). Offsets beyond the signed 12-bit range materialise the
|
|
|
|
|
// address in X31 first: LUI hi (the rounding split), then ADD X31, rs1,
|
|
|
|
|
// matching the toolchain's large-frame addressing; the access uses the
|
|
|
|
|
// sign-extended low part, which always fits.
|
|
|
|
|
func riscvFrameMemOp(enc riscvEnc, store bool, reg, rs1 int, off int32) []byte {
|
|
|
|
|
if fits12(off) {
|
|
|
|
|
var word uint32
|
|
|
|
|
if store {
|
|
|
|
|
word = riscvSType(enc, rs1, reg, off)
|
|
|
|
|
} else {
|
|
|
|
|
word = riscvIType(enc, reg, rs1, off)
|
|
|
|
|
}
|
|
|
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
|
|
|
|
|
}
|
|
|
|
|
lo := off - (splitHi(off) << 12)
|
|
|
|
|
out := riscvAddressInX31WithBase(off, rs1)
|
|
|
|
|
var word uint32
|
|
|
|
|
if store {
|
|
|
|
|
word = riscvSType(enc, 31, reg, lo)
|
|
|
|
|
} else {
|
|
|
|
|
word = riscvIType(enc, reg, 31, lo)
|
|
|
|
|
}
|
|
|
|
|
return append(out, wordLE(word)...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// riscvFrameMemSize returns the encoded size of a frame-relative MOV for the
|
|
|
|
|
// layout pass: 4 bytes when the offset fits, otherwise the X31
|
|
|
|
|
// materialisation plus the access.
|
|
|
|
|
func riscvFrameMemSize(op *ast.Operand, fi riscvFrameInfo) int {
|
|
|
|
|
rs1, off := memFromOperandWithFrame(op, fi)
|
|
|
|
|
if fits12(off) {
|
|
|
|
|
return 4
|
|
|
|
|
}
|
|
|
|
|
return len(riscvAddressInX31WithBase(off, rs1)) + 4
|
|
|
|
|
}
|
|
|
|
|
|
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.
|
2026-08-02 18:22:00 +02:00
|
|
|
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-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-13 17:41:16 +02:00
|
|
|
low, high := splitRISCV32Imm(imm)
|
2026-08-02 18:22:00 +02:00
|
|
|
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))...)
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 16:04:08 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 08:51:00 +02:00
|
|
|
// encodeRISCVSBAddr emits AUIPC + ADDI to load the address of a static
|
2026-08-13 12:07:29 +02:00
|
|
|
// 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).
|
2026-08-03 08:51:00 +02:00
|
|
|
func encodeRISCVSBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
|
|
|
|
|
name := sym.Name
|
|
|
|
|
if relocs != nil {
|
2026-08-13 12:07:29 +02:00
|
|
|
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
|
|
|
|
|
addi := riscvIType(riscvEnc{0x13, 0x0, 0x00}, rd, rd, 0)
|
|
|
|
|
return append(wordLE(auipc), wordLE(addi)...)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 12:07:29 +02:00
|
|
|
// encodeRISCVSBLoad emits AUIPC + LD to load from a static symbol into rd,
|
|
|
|
|
// recording the single R_RISCV_PCREL_ITYPE relocation for the pair.
|
2026-08-03 08:51:00 +02:00
|
|
|
func encodeRISCVSBLoad(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
|
|
|
|
|
name := sym.Name
|
|
|
|
|
if relocs != nil {
|
2026-08-13 12:07:29 +02:00
|
|
|
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELIType, Addend: sym.Offset})
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
auipc := riscvUType(riscvEnc{0x17, 0x0, 0x00}, rd, 0)
|
|
|
|
|
ld := riscvIType(riscvEnc{0x03, 0x3, 0x00}, rd, rd, 0)
|
|
|
|
|
return append(wordLE(auipc), wordLE(ld)...)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 12:07:29 +02:00
|
|
|
// encodeRISCVSBStore emits AUIPC + SD to store a register into a static symbol,
|
|
|
|
|
// recording the single R_RISCV_PCREL_STYPE relocation for the pair.
|
2026-08-03 08:51:00 +02:00
|
|
|
func encodeRISCVSBStore(sym *ast.Symbol, rs2 int, relocs *[]Reloc) []byte {
|
|
|
|
|
tmp := 31 // X31 = T6
|
|
|
|
|
name := sym.Name
|
|
|
|
|
if relocs != nil {
|
2026-08-13 12:07:29 +02:00
|
|
|
*relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: name, Kind: RelRISCVPCRELSType, Addend: sym.Offset})
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
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)}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 16:04:08 +02:00
|
|
|
// word16 encodes a uint16 as 2 little-endian bytes.
|
|
|
|
|
func word16(w uint16) []byte {
|
|
|
|
|
return []byte{byte(w), byte(w >> 8)}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 08:51:00 +02:00
|
|
|
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
2026-09-19 19:17:07 +02:00
|
|
|
// Plan 9: JALR rs1, rd (2 regs), JALR rd, offset(rs1) (the trampoline
|
|
|
|
|
// form), or JALR offset(rs1) (memory → rd=X1).
|
2026-08-03 08:51:00 +02:00
|
|
|
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
|
|
|
|
ops := instr.Operands
|
2026-09-19 19:17:07 +02:00
|
|
|
// JALR rd, offset(rs1): the memory operand's base is the jump-target
|
|
|
|
|
// register, not the destination.
|
|
|
|
|
if len(ops) == 2 && isMemOperand(ops[1]) {
|
|
|
|
|
rd := regFromOperand(ops[0])
|
|
|
|
|
rs1, imm := memFromOperandWithFrame(ops[1], fi)
|
|
|
|
|
if rd < 0 || rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("JALR: invalid register operand")
|
|
|
|
|
}
|
|
|
|
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, imm)), nil
|
|
|
|
|
}
|
2026-08-03 08:51:00 +02:00
|
|
|
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")
|
|
|
|
|
}
|
2026-09-19 19:17:07 +02:00
|
|
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)), nil
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
if len(ops) == 1 {
|
|
|
|
|
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
|
|
|
|
if rs1 < 0 {
|
|
|
|
|
return nil, fmt.Errorf("JALR: invalid memory operand")
|
|
|
|
|
}
|
2026-09-19 19:17:07 +02:00
|
|
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)), nil
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
// 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) {
|
2026-09-19 23:49:13 +02:00
|
|
|
mnem := riscvCompressMnem(instr)
|
2026-08-02 18:22:00 +02:00
|
|
|
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 {
|
2026-08-03 08:51:00 +02:00
|
|
|
return rvcLSP(0x3, uint32(rd), uint32(imm)), true
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
2026-08-13 15:42:38 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
// 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 {
|
2026-08-03 08:51:00 +02:00
|
|
|
return rvcSSP(0x7, uint32(rs2), uint32(imm)), true
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-08-03 08:51:00 +02:00
|
|
|
return rvcSSP(0x7, uint32(rs2), uint32(imm)), true
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
2026-08-13 15:42:38 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
|
|
|
|
|
case "ADDI":
|
2026-08-29 17:12:53 +02:00
|
|
|
rd, rs1, imm := extractITypeParams(instr)
|
2026-08-02 18:22:00 +02:00
|
|
|
if rd == -1 || rs1 == -1 {
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-13 15:42:38 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
if rd == 0 && rs1 == 0 && imm == 0 {
|
|
|
|
|
// C.NOP
|
|
|
|
|
return 0x0001, true
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
|
|
|
|
|
case "JAL":
|
2026-08-13 17:57:10 +02:00
|
|
|
// JAL/JMP are never compressed to C.J by the Go assembler.
|
|
|
|
|
return 0, false
|
2026-08-02 18:22:00 +02:00
|
|
|
|
|
|
|
|
case "JMP":
|
2026-08-13 17:57:10 +02:00
|
|
|
// JAL/JMP are never compressed to C.J by the Go assembler.
|
2026-08-02 18:22:00 +02:00
|
|
|
return 0, false
|
|
|
|
|
|
|
|
|
|
case "BEQ":
|
2026-08-13 17:57:10 +02:00
|
|
|
// Branches are never compressed to C.BEQZ/C.BNEZ.
|
2026-08-03 01:08:00 +02:00
|
|
|
return 0, false
|
|
|
|
|
|
|
|
|
|
case "BNE":
|
2026-08-13 17:57:10 +02:00
|
|
|
// Branches are never compressed to C.BEQZ/C.BNEZ.
|
2026-08-03 01:08:00 +02:00
|
|
|
return 0, false
|
|
|
|
|
|
|
|
|
|
case "ADD":
|
2026-08-13 15:13:31 +02:00
|
|
|
// 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.
|
2026-08-02 18:22:00 +02:00
|
|
|
if len(ops) == 3 {
|
2026-08-13 15:13:31 +02:00
|
|
|
rs2 := regFromOperand(ops[0])
|
|
|
|
|
rs1 := regFromOperand(ops[1])
|
2026-08-03 01:08:00 +02:00
|
|
|
rd := regFromOperand(ops[2])
|
|
|
|
|
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
|
2026-08-13 14:41:57 +02:00
|
|
|
if rd == rs1 && rs2 != 0 {
|
|
|
|
|
return rvcCR(0x9, uint32(rd), uint32(rs2)), true
|
2026-08-03 01:08:00 +02:00
|
|
|
}
|
2026-08-13 14:41:57 +02:00
|
|
|
if rd == rs2 && rs1 != 0 {
|
|
|
|
|
return rvcCR(0x9, uint32(rd), uint32(rs1)), true
|
2026-08-03 01:08:00 +02:00
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
if rs1 == 0 && rs2 != 0 {
|
|
|
|
|
// ADD rs2, X0, rd → C.MV rd, rs2.
|
|
|
|
|
return rvcCR(0x8, uint32(rd), uint32(rs2)), true
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 01:08:00 +02:00
|
|
|
case "SUB", "XOR", "OR", "AND":
|
|
|
|
|
// C.SUB (0x23,0), C.XOR (0x23,1), C.OR (0x23,2), C.AND (0x23,3)
|
2026-08-02 18:22:00 +02:00
|
|
|
if len(ops) == 3 {
|
2026-08-03 01:08:00 +02:00
|
|
|
var funct2 uint32
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "SUB":
|
|
|
|
|
funct2 = 0x0
|
|
|
|
|
case "XOR":
|
|
|
|
|
funct2 = 0x1
|
|
|
|
|
case "OR":
|
|
|
|
|
funct2 = 0x2
|
|
|
|
|
case "AND":
|
|
|
|
|
funct2 = 0x3
|
|
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
rs2 := regFromOperand(ops[0])
|
|
|
|
|
rs1 := regFromOperand(ops[1])
|
2026-08-03 01:08:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 15:42:38 +02:00
|
|
|
case "ADDW", "SUBW":
|
2026-09-14 18:22:18 +02:00
|
|
|
// C.ADDW (0x27,1) / C.SUBW (0x27,0), CA-type, prime regs.
|
2026-08-13 15:42:38 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 01:08:00 +02:00
|
|
|
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 {
|
2026-08-03 08:51:00 +02:00
|
|
|
return rvcLSP(0x1, uint32(rd), uint32(imm)), true
|
2026-08-03 01:08:00 +02:00
|
|
|
}
|
2026-08-13 15:42:38 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-08-03 01:08:00 +02:00
|
|
|
|
|
|
|
|
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 {
|
2026-08-03 08:51:00 +02:00
|
|
|
return rvcSSP(0x5, uint32(rs2), uint32(imm)), true
|
2026-08-03 01:08:00 +02:00
|
|
|
}
|
2026-08-13 15:42:38 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-08-03 01:08:00 +02:00
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
case "LUI":
|
2026-08-13 17:57:10 +02:00
|
|
|
// LUI rd, imm → C.LUI when rd≠0, rd≠SP, imm nonzero and fits in six
|
|
|
|
|
// signed bits (matching the toolchain's compress pass).
|
2026-08-02 18:22:00 +02:00
|
|
|
if len(ops) == 2 {
|
|
|
|
|
rd := regFromOperand(ops[0])
|
|
|
|
|
imm := immFromOperand(ops[1])
|
2026-08-13 17:57:10 +02:00
|
|
|
if rd != -1 && rd != 0 && rd != 2 && imm != 0 && imm >= -32 && imm <= 31 {
|
2026-08-02 18:22:00 +02:00
|
|
|
return rvcCI(0x3, uint32(rd), uint32(imm)&0x3F), true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "ADDIW":
|
2026-08-29 17:12:53 +02:00
|
|
|
rd, rs1, imm := extractITypeParams(instr)
|
2026-08-02 18:22:00 +02:00
|
|
|
if rd == rs1 && rd != 0 && imm >= -32 && imm <= 31 {
|
|
|
|
|
return rvcCI(0x1, uint32(rd), uint32(imm)&0x3F), true
|
|
|
|
|
}
|
2026-08-03 08:51:00 +02:00
|
|
|
|
|
|
|
|
case "SLLI", "SRLI", "SRAI":
|
2026-08-29 17:12:53 +02:00
|
|
|
rd, rs1, imm := extractITypeParams(instr)
|
2026-08-03 08:51:00 +02:00
|
|
|
if rd == rs1 && rd != 0 && imm != 0 && imm >= 1 && imm <= 63 {
|
|
|
|
|
if mnem == "SLLI" {
|
2026-08-13 15:13:31 +02:00
|
|
|
// C.SLLI: funct3=0, op=10 quadrant, shamt in bits [12|6:2].
|
|
|
|
|
return rvcSLLI(uint32(rd), uint32(imm)&0x3F), true
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
if isRVCIntReg(rd) {
|
|
|
|
|
funct2 := uint32(0x0)
|
|
|
|
|
if mnem == "SRAI" {
|
|
|
|
|
funct2 = 0x1
|
|
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
// C.SRLI/C.SRAI: CB-type, funct3=0x4.
|
|
|
|
|
return rvcCBShift(funct2, rvcReg3(rd), uint32(imm)&0x3F), true
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case "ANDI":
|
2026-08-29 17:12:53 +02:00
|
|
|
rd, rs1, imm := extractITypeParams(instr)
|
2026-08-03 08:51:00 +02:00
|
|
|
if isRVCIntReg(rd) && rd == rs1 && imm >= -32 && imm <= 31 {
|
2026-08-13 15:13:31 +02:00
|
|
|
// C.ANDI: CB-type, funct3=0x4, funct2=0x2.
|
|
|
|
|
return rvcCBShift(0x2, rvcReg3(rd), uint32(imm)&0x3F), true
|
2026-08-03 08:51:00 +02:00
|
|
|
}
|
2026-08-13 15:13:31 +02:00
|
|
|
|
|
|
|
|
case "EBREAK":
|
|
|
|
|
// C.EBREAK: CR-type, funct4=0x9, rd=0, rs2=0.
|
|
|
|
|
return rvcCR(0x9, 0, 0), true
|
2026-08-02 18:22:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0, false
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:13 +02:00
|
|
|
// riscvCompressMnem maps a MOV-family load or store onto the base mnemonic
|
|
|
|
|
// the toolchain lowers it to (MOVW 4(SP), X9 is LW under another name), so
|
|
|
|
|
// the width spellings compress exactly like their base forms. Register and
|
|
|
|
|
// immediate forms keep their own mnemonic: the C.MV path matches "MOV" and
|
|
|
|
|
// nothing else in the switch has a width case.
|
|
|
|
|
func riscvCompressMnem(instr *ast.Instr) string {
|
|
|
|
|
mnem := instr.Mnemonic.Text
|
|
|
|
|
ops := instr.Operands
|
|
|
|
|
if !strings.HasPrefix(mnem, "MOV") || len(ops) != 2 {
|
|
|
|
|
return mnem
|
|
|
|
|
}
|
|
|
|
|
load := isMemOperand(ops[0]) && !isMemOperand(ops[1])
|
|
|
|
|
store := !isMemOperand(ops[0]) && isMemOperand(ops[1])
|
|
|
|
|
if !load && !store {
|
|
|
|
|
return mnem
|
|
|
|
|
}
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "MOVW":
|
|
|
|
|
if load {
|
|
|
|
|
return "LW"
|
|
|
|
|
}
|
|
|
|
|
return "SW"
|
|
|
|
|
case "MOVF":
|
|
|
|
|
if load {
|
|
|
|
|
return "FLW"
|
|
|
|
|
}
|
|
|
|
|
return "FSW"
|
|
|
|
|
case "MOVD":
|
|
|
|
|
if load {
|
|
|
|
|
return "FLD"
|
|
|
|
|
}
|
|
|
|
|
return "FSD"
|
|
|
|
|
case "MOV":
|
|
|
|
|
if load {
|
|
|
|
|
return "LD"
|
|
|
|
|
}
|
|
|
|
|
return "SD"
|
|
|
|
|
}
|
|
|
|
|
// MOVB/MOVBU/MOVH/MOVHU/MOVWU have no compressed form; their base
|
|
|
|
|
// mnemonics (LB/LBU/LH/LHU/LWU, SB/SH) match no case either.
|
|
|
|
|
return mnem
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 15:13:31 +02:00
|
|
|
// 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).
|
2026-08-29 17:12:53 +02:00
|
|
|
func extractITypeParams(instr *ast.Instr) (rd, rs1 int, imm int32) {
|
2026-08-02 18:22:00 +02:00
|
|
|
ops := instr.Operands
|
2026-08-13 15:13:31 +02:00
|
|
|
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:
|
2026-08-02 18:22:00 +02:00
|
|
|
return -1, -1, 0
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:51:00 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 18:22:00 +02:00
|
|
|
func isShiftImmInstr(m string) bool {
|
|
|
|
|
switch m {
|
|
|
|
|
case "SLLI", "SRLI", "SRAI", "SLLIW", "SRLIW", "SRAIW":
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:51:00 +02:00
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 11:45:00 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:51:00 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:13 +02:00
|
|
|
// riscvImm32FromOperand reads an immediate for the MOV/I-type paths as a
|
|
|
|
|
// signed 32-bit value. The toolchain materialises wider constants through
|
|
|
|
|
// its SLLI expansion, which this assembler does not implement, so values
|
|
|
|
|
// outside the int32 span are diagnosed instead of silently truncated (MOV
|
|
|
|
|
// $0x123456789 must not assemble as $0x3456789). The neg flag carries the
|
|
|
|
|
// SUB $imm alias, whose negated value may fit when the written one does not.
|
|
|
|
|
func riscvImm32FromOperand(op *ast.Operand, neg bool) (int32, error) {
|
|
|
|
|
var v int64
|
|
|
|
|
if op.Imm.HasVal {
|
|
|
|
|
v = op.Imm.Val
|
|
|
|
|
if op.Imm.Neg {
|
|
|
|
|
v = -v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if neg {
|
|
|
|
|
v = -v
|
|
|
|
|
}
|
|
|
|
|
if int64(int32(v)) != v {
|
|
|
|
|
return 0, fmt.Errorf("immediate %d out of range; 64-bit materialisation not supported", v)
|
|
|
|
|
}
|
|
|
|
|
return int32(v), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:51:00 +02:00
|
|
|
func memFromOperand(op *ast.Operand) (rs1 int, imm int32) {
|
|
|
|
|
rs1 = riscvRegNum(op.Addr.Base)
|
|
|
|
|
imm = int32(op.Addr.Offset)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 00:18:00 +02:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 19:51:00 +02:00
|
|
|
func labelFromOperand(op *ast.Operand) string {
|
|
|
|
|
if op.Addr.Sym != nil {
|
|
|
|
|
return op.Addr.Sym.Name
|
|
|
|
|
}
|
|
|
|
|
return op.Raw
|
|
|
|
|
}
|
2026-08-05 18:52:00 +02:00
|
|
|
|
|
|
|
|
// 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 {
|
2026-09-14 18:22:18 +02:00
|
|
|
return fmt.Sprintf("; did you mean %q?", best)
|
2026-08-05 18:52:00 +02:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|