fix(asm): correct RISC-V frame layout and RVC encodings

Assisted-by: GLM 5.2
This commit is contained in:
2026-08-13 14:41:57 +02:00
parent 9b5c878f9e
commit b78b6c5004
11 changed files with 489 additions and 249 deletions
+32 -21
View File
@@ -5,17 +5,25 @@ package asm
import (
"fmt"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
)
// assembleRISCV assembles a RISC-V TEXT function body into machine code.
// It handles the full RV64IMAFDC instruction set including RVC compression.
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
fi := riscvComputeFrame(t)
prologue := riscvPrologue(fi)
var relocs []Reloc
var spadj []SpadjStep
// The prologue raises the SP delta by autosize; the boundary is reported
// at the pc just past its ADDI, exactly as the toolchain's pctospadj does.
if fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: riscvPrologueSpadjPC(fi), Value: fi.autosize})
}
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
// per instruction (or 8 for MOV $large-imm). No encoding yet.
@@ -33,7 +41,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
offsets[s.Name.Text] = pos
case *ast.Instr:
recs = append(recs, instrRec{instr: s})
pos += riscvInstrSize(s)
pos += riscvInstrSize(s, fi)
}
}
@@ -42,7 +50,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
for i := range recs {
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil) // no relocs in Pass 2
if err != nil {
return nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
}
recs[i].code = code
pc += len(code)
@@ -78,14 +86,16 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
out := append([]byte(nil), prologue...)
pc = len(prologue)
preCount := len(relocs)
var lines []LineEntry
for _, r := range recs {
lines = append(lines, LineEntry{Offset: pc, Line: r.instr.Pos().Line})
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
out = append(out, r.code...)
pc += len(r.code)
} else {
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, nil, err
}
if c16, ok := tryCompressRVC(r.instr, fi); ok {
code = []byte{byte(c16), byte(c16 >> 8)}
@@ -99,18 +109,26 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, error) {
relocs[j].After = relocs[j].Off + 8
}
preCount = len(relocs)
// The RET's epilogue closes the frame: the SP delta returns to zero
// after its ADDI (restore LR + ADDI).
if strings.ToUpper(r.instr.Mnemonic.Text) == "RET" && fi.autosize != 0 {
spadj = append(spadj, SpadjStep{PC: pc + riscvReturnEpilogueLen(fi), Value: 0})
}
out = append(out, code...)
pc += len(code)
}
}
return out, offsets, relocs, nil
return out, offsets, relocs, lines, spadj, nil
}
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
// Most instructions are 4 bytes; MOV with a large immediate is 8 (LUI+ADDIW).
func riscvInstrSize(instr *ast.Instr) int {
func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
mnem := instr.Mnemonic.Text
ops := instr.Operands
if mnem == "RET" {
return len(riscvReturn(fi))
}
if mnem == "MOV" && len(ops) == 2 {
// MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI).
if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" {
@@ -154,9 +172,8 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
// 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
// RET = epilogue (restore LR and close the frame when present) + C.JR ra.
return riscvReturn(fi), nil
case "CALL":
// CALL target → AUIPC X1, %pcrel_hi + JALR X1, %pcrel_lo(X1).
// For now, emit AUIPC X1, 0 + JALR X1, 0(X1) with zero offsets.
@@ -687,10 +704,6 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
ops := instr.Operands
switch mnem {
case "RET":
// RET = JALR X0, 0(X1) → C.JR RA (CR-type: funct4=0x8, rs1=ra, rs2=0).
return rvcCR(0x8, 1, 0), true
case "LD", "MOV":
// LD rd, offset(SP) → C.LDSP when rd≠0 and uimm[8:3] fits.
// MOV name+off(FP), rd → load, same compression.
@@ -764,20 +777,18 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
return 0, false
case "ADD":
// ADD rd, rs2 → C.ADD when rd == rs1 and both in prime regs (rd ≠ 0).
// ADD is commutative: if rd == rs2, swap.
// ADD rd, rs2 → C.ADD (CR-type, funct4=0x9) when rd == rs1; ADD is
// commutative, so if rd == rs2, swap.
if len(ops) == 3 {
rs1 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1])
rd := regFromOperand(ops[2])
if rd != -1 && rs1 != -1 && rs2 != -1 && rd != 0 {
if rd == rs1 && isRVCIntReg(rd) && isRVCIntReg(rs2) && rs2 != 0 {
// C.ADD: funct6=0x27, funct2=0x0 (CA-type)
return rvcCA(0x27, 0x0, rvcReg3(rd), rvcReg3(rs2)), true
if rd == rs1 && rs2 != 0 {
return rvcCR(0x9, uint32(rd), uint32(rs2)), true
}
if rd == rs2 && isRVCIntReg(rd) && isRVCIntReg(rs1) && rs1 != 0 {
// Swap: C.ADD rd, rs1
return rvcCA(0x27, 0x0, rvcReg3(rd), rvcReg3(rs1)), true
if rd == rs2 && rs1 != 0 {
return rvcCR(0x9, uint32(rd), uint32(rs1)), true
}
}
}