feat(asm): add RISC-V encoder with RV64I/RV64M instruction formats
Assisted-by: Kimi K3
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
// 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) {
|
||||
// First pass: compute label offsets (all RISC-V instructions are 4 bytes).
|
||||
offsets := map[string]int{}
|
||||
var instrs []*ast.Instr
|
||||
pos := 0
|
||||
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 := make([]byte, 0, pos)
|
||||
for _, instr := range instrs {
|
||||
code, err := encodeRISCVInstr(instr, pos, offsets)
|
||||
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) ([]byte, error) {
|
||||
mnem := instr.Mnemonic.Text
|
||||
enc, ok := riscvInstrTable[mnem]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported RISC-V instruction %q", mnem)
|
||||
}
|
||||
|
||||
ops := instr.Operands
|
||||
var word uint32
|
||||
|
||||
switch {
|
||||
// R-type: rd, rs1, rs2 (3 register operands).
|
||||
case len(ops) == 3 && isRTypeInstr(mnem):
|
||||
rd := regFromOperand(ops[0])
|
||||
rs1 := regFromOperand(ops[1])
|
||||
rs2 := regFromOperand(ops[2])
|
||||
if rd < 0 || rs1 < 0 || rs2 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvRType(enc, rd, rs1, rs2)
|
||||
|
||||
// I-type with immediate: rd, rs1, imm.
|
||||
case len(ops) == 3 && isITypeInstr(mnem):
|
||||
rd := regFromOperand(ops[0])
|
||||
rs1 := regFromOperand(ops[1])
|
||||
imm := immFromOperand(ops[2])
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
word = riscvIType(enc, rd, rs1, imm)
|
||||
|
||||
// Loads: rd, offset(rs1) — parsed as rd, (rs1)+offset.
|
||||
case len(ops) == 2 && isLoadInstr(mnem):
|
||||
rd := regFromOperand(ops[0])
|
||||
rs1, imm := memFromOperand(ops[1])
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
||||
}
|
||||
word = riscvIType(enc, rd, rs1, imm)
|
||||
|
||||
// Stores: rs2, offset(rs1).
|
||||
case len(ops) == 2 && isStoreInstr(mnem):
|
||||
rs2 := regFromOperand(ops[0])
|
||||
rs1, imm := memFromOperand(ops[1])
|
||||
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)
|
||||
|
||||
// JAL: rd, label (or just label for JMP).
|
||||
case mnem == "JAL" || mnem == "JMP":
|
||||
rd := 0 // JMP uses rd=0 (X0)
|
||||
if len(ops) >= 1 && mnem == "JAL" {
|
||||
rd = regFromOperand(ops[0])
|
||||
}
|
||||
var target string
|
||||
if len(ops) >= 2 {
|
||||
target = labelFromOperand(ops[len(ops)-1])
|
||||
} else if len(ops) == 1 && mnem == "JMP" {
|
||||
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)
|
||||
|
||||
// RET: JALR X0, 0(X1).
|
||||
case mnem == "RET":
|
||||
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, 1, 0)
|
||||
|
||||
// CALL: AUIPC + JALR sequence (simplified: just encode as NOP placeholder).
|
||||
case mnem == "CALL":
|
||||
// CALL is a pseudo-instruction that expands to AUIPC + JALR.
|
||||
// For now, encode as a NOP (ADDI X0, X0, 0).
|
||||
word = riscvIType(riscvEnc{0x13, 0x0, 0x00}, 0, 0, 0)
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func labelFromOperand(op *ast.Operand) string {
|
||||
if op.Addr.Sym != nil {
|
||||
return op.Addr.Sym.Name
|
||||
}
|
||||
return op.Raw
|
||||
}
|
||||
Reference in New Issue
Block a user