452 lines
13 KiB
Go
452 lines
13 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package asm
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
|
)
|
|
|
|
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
|
|
// It handles the core RV64I/RV64M instruction set with fixed 32-bit encoding.
|
|
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, error) {
|
|
fi := riscvComputeFrame(t)
|
|
prologue := riscvPrologue(fi)
|
|
|
|
// First pass: compute label offsets (all RISC-V instructions are 4 bytes).
|
|
// Labels are offset by the prologue length.
|
|
offsets := map[string]int{}
|
|
var instrs []*ast.Instr
|
|
pos := len(prologue)
|
|
for _, stmt := range t.Body {
|
|
switch s := stmt.(type) {
|
|
case *ast.Label:
|
|
offsets[s.Name.Text] = pos
|
|
case *ast.Instr:
|
|
instrs = append(instrs, s)
|
|
pos += 4 // all RISC-V instructions are 4 bytes
|
|
}
|
|
}
|
|
|
|
// Second pass: encode instructions.
|
|
out := append([]byte(nil), prologue...)
|
|
for _, instr := range instrs {
|
|
code, err := encodeRISCVInstr(instr, pos, offsets, fi)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s: %w", instr.Mnemonic.Text, err)
|
|
}
|
|
out = append(out, code...)
|
|
pos -= 4 // track remaining
|
|
}
|
|
return out, offsets, nil
|
|
}
|
|
|
|
// encodeRISCVInstr encodes a single RISC-V instruction.
|
|
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo) ([]byte, error) {
|
|
mnem := instr.Mnemonic.Text
|
|
ops := instr.Operands
|
|
var word uint32
|
|
|
|
// Handle pseudo-instructions and special cases first.
|
|
switch mnem {
|
|
case "RET":
|
|
// RET = JALR X0, 0(X1)
|
|
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0)
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
case "CALL":
|
|
// CALL is a pseudo-instruction; encode as NOP placeholder.
|
|
word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0)
|
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
|
case "JMP":
|
|
// JMP = JAL X0, target
|
|
var target string
|
|
if len(ops) >= 1 {
|
|
target = labelFromOperand(ops[0])
|
|
}
|
|
targetOff, ok := offsets[target]
|
|
if !ok {
|
|
return nil, fmt.Errorf("undefined label %q", target)
|
|
}
|
|
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", target)
|
|
}
|
|
offset := int32(targetOff - pc)
|
|
word = riscvJType(rd, offset)
|
|
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: Plan 9 order is INSTR src1, src2, dst (destination last).
|
|
case len(ops) == 3 && isRTypeInstr(mnem):
|
|
rs1 := regFromOperand(ops[0]) // source 1 (first operand)
|
|
rs2 := regFromOperand(ops[1]) // source 2 (second operand)
|
|
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)
|
|
|
|
// 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: Plan 9 order is INSTR src1, src2, dst.
|
|
case len(ops) == 3 && isFPArithInstr(mnem):
|
|
rs1 := regFromOperand(ops[0])
|
|
rs2 := 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: INSTR src1, src2, dst(int) — result in integer register.
|
|
case len(ops) == 3 && isFPCmpInstr(mnem):
|
|
rs1 := regFromOperand(ops[0])
|
|
rs2 := 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 src, imm, dst.
|
|
case len(ops) == 3 && isITypeInstr(mnem):
|
|
rs1 := regFromOperand(ops[0]) // source register
|
|
imm := immFromOperand(ops[1]) // immediate
|
|
rd := regFromOperand(ops[2]) // destination
|
|
if rd < 0 || rs1 < 0 {
|
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
|
}
|
|
word = riscvIType(enc, rd, rs1, 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", target)
|
|
}
|
|
rs1Off := 0 // placeholder
|
|
_ = rs1Off
|
|
offset := int32(targetOff - pc)
|
|
if rs1 < 0 || rs2 < 0 {
|
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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 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
|
|
}
|
|
|
|
func isFPCvtInstr(m string) bool {
|
|
_, ok := riscvCvtTable[m]
|
|
return ok
|
|
}
|
|
|
|
// 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
|
|
}
|