feat(riscv64,loong64): PCALIGN, branch relaxation and operand shapes

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-20 14:25:47 +02:00
parent 9b238a525a
commit 9dc3987e02
8 changed files with 3230 additions and 100 deletions
+564 -45
View File
@@ -4,7 +4,9 @@
package asm
import (
"errors"
"fmt"
"slices"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
@@ -31,32 +33,54 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
}
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
// per instruction (or 8 for MOV $large-imm). No encoding yet.
// per instruction (or 8 for MOV $large-imm). No encoding yet. PCALIGN
// contributes only its padding, which is attached to the following
// instruction and emitted ahead of it. A relaxed branch carries the
// inverted condition and is followed by an inserted JMP rec (jmpTo set)
// that carries the original target.
type instrRec struct {
instr *ast.Instr
compressed bool
code []byte
pad int
relaxed bool
jmpTo string
}
var recs []instrRec
offsets := map[string]int{}
pos := guardLen + len(prologue)
pendingPad := 0
for _, stmt := range t.Body {
switch s := stmt.(type) {
case *ast.Label:
offsets[s.Name.Text] = pos
case *ast.Instr:
recs = append(recs, instrRec{instr: s})
if strings.ToUpper(s.Mnemonic.Text) == "PCALIGN" {
pendingPad += riscvPCAlignPad(pos, s)
pos += riscvPCAlignPad(pos, s)
continue
}
recs = append(recs, instrRec{instr: s, pad: pendingPad})
pendingPad = 0
pos += riscvInstrSize(s, fi)
}
}
// Pass 2: encode each instruction using Pass-1 offsets.
// Pass 2: encode each instruction using Pass-1 offsets. A branch or
// jump the offsets prove overlong encodes to a 4-byte placeholder: the
// relaxation pass rewrites it before the final encoding. pcRelPcs is
// unavailable this early, so the N(PC) forms take the same placeholder
// path.
pc := len(prologue)
for i := range recs {
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil) // no relocs in Pass 2
if err != nil {
branchLike := isBranchLike(recs[i].instr.Mnemonic.Text) || riscvIsCondBranch(recs[i].instr.Mnemonic.Text)
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil, nil) // no relocs in Pass 2
if err != nil && !(branchLike && riscvIsRangeError(err)) {
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
}
if err != nil {
code = make([]byte, 4)
}
recs[i].code = code
pc += len(code)
}
@@ -72,20 +96,100 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
// 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.
// whole slice per statement. PCALIGN padding is recomputed here, since
// compression has shifted instruction sizes since Pass 1.
offsets = map[string]int{}
pos = guardLen + len(prologue)
ri := 0
pendingPad = 0
for _, stmt := range t.Body {
switch s := stmt.(type) {
case *ast.Label:
offsets[s.Name.Text] = pos
case *ast.Instr:
if strings.ToUpper(s.Mnemonic.Text) == "PCALIGN" {
pad := riscvPCAlignPad(pos, s)
pendingPad += pad
pos += pad
continue
}
recs[ri].pad = pendingPad
pendingPad = 0
pos += len(recs[ri].code)
ri++
}
}
// Pass 4b: relax overlong conditional branches exactly as the toolchain
// does: invert the branch condition, point it at the instruction after an
// inserted JMP, let the JMP carry the original target, and re-layout until
// a pass inserts nothing. Inserted JMP recs share their branch's source
// line and trail it in emission order, so the body walk flushes them
// before every statement and at the end.
var pcRelPcs map[*ast.Instr]int
for {
offsets = map[string]int{}
pos = guardLen + len(prologue)
ri := 0
pcs := make([]int, len(recs))
flushJmps := func() {
for ri < len(recs) && recs[ri].jmpTo != "" {
pcs[ri] = pos
pos += 4
ri++
}
}
for _, stmt := range t.Body {
switch s := stmt.(type) {
case *ast.Label:
flushJmps()
offsets[s.Name.Text] = pos
case *ast.Instr:
flushJmps()
if ri >= len(recs) {
continue
}
pcs[ri] = pos + recs[ri].pad
pos += recs[ri].pad + len(recs[ri].code)
ri++
}
}
flushJmps()
changed := false
for i := range recs {
r := &recs[i]
if r.relaxed || r.jmpTo != "" {
continue
}
mnem := strings.ToUpper(r.instr.Mnemonic.Text)
if !riscvIsCondBranch(mnem) || len(r.instr.Operands) == 0 {
continue
}
target := labelFromOperand(r.instr.Operands[len(r.instr.Operands)-1])
targetOff, ok := offsets[target]
if !ok {
continue
}
if delta := int32(targetOff - pcs[i]); delta < -4096 || delta >= 4096 {
r.relaxed = true
recs = slices.Insert(recs, i+1, instrRec{instr: r.instr, jmpTo: target})
changed = true
}
}
if !changed {
// Capture the final pcs for the N(PC) branch forms: their target
// is the instruction N source slots away, resolved by index.
pcRelPcs = map[*ast.Instr]int{}
for i := range recs {
if _, ok := riscvPCRelOffset(recs[i].instr); ok {
pcRelPcs[recs[i].instr] = pcs[i]
}
}
break
}
}
// Pass 5: re-encode branches with corrected offsets. Record relocations
// during this final pass (relocation offsets are relative to instruction
// start). The guard prefix precedes the prologue; its branches target
@@ -104,12 +208,40 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
preCount := len(relocs)
var lines []LineEntry
for _, r := range recs {
// PCALIGN padding precedes the instruction it was attached to.
if r.pad > 0 {
out = append(out, riscvPadBytes(r.pad)...)
pc += r.pad
}
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)
var code []byte
switch {
case r.jmpTo != "":
// The JMP a relaxation inserted: JAL X0 to the original target.
targetOff, ok := offsets[r.jmpTo]
if !ok {
return nil, nil, nil, nil, nil, fmt.Errorf("undefined label %q", r.jmpTo)
}
offset := int32(targetOff - pc)
if err := riscvCheckJumpOffset(r.jmpTo, offset); err != nil {
return nil, nil, nil, nil, nil, err
}
word := riscvJType(0, offset)
code = []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
case r.relaxed:
// The inverted half of a relaxed branch: it targets the inserted
// JMP, always the very next instruction (offset 4).
enc, rs1, rs2, ok := riscvInvertedBranchEnc(strings.ToUpper(r.instr.Mnemonic.Text), r.instr.Operands)
if !ok {
return nil, nil, nil, nil, nil, fmt.Errorf("%s: cannot relax branch", r.instr.Mnemonic.Text)
}
word := riscvBType(enc, rs1, rs2, 4)
code = []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
case r.compressed && !isBranchLike(r.instr.Mnemonic.Text):
code = r.code
default:
var err error
code, err = encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs, pcRelPcs)
if err != nil {
return nil, nil, nil, nil, nil, err
}
@@ -131,9 +263,9 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
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)
}
out = append(out, code...)
pc += len(code)
}
if fi.needSplit {
relocs = append(relocs, guardReloc)
@@ -150,6 +282,8 @@ var riscvImmAlias = map[string]string{
"AND": "ANDI",
"OR": "ORI",
"XOR": "XORI",
"SLT": "SLTI",
"SLTU": "SLTIU",
"SLL": "SLLI",
"SRL": "SRLI",
"SRA": "SRAI",
@@ -178,6 +312,35 @@ func riscvNormaliseImmAlias(mnem string, ops []*ast.Operand) (string, bool) {
return mnem, false
}
// riscvPCAlignPad returns the padding PCALIGN inserts before the next
// instruction so that it starts at the requested boundary relative to the
// function start. The boundary must be a power of two between 8 and 2048, as
// the toolchain requires; anything else pads nothing.
func riscvPCAlignPad(pos int, instr *ast.Instr) int {
if len(instr.Operands) != 1 || !isImmOperand(instr.Operands[0]) {
return 0
}
align := int(immFromOperand(instr.Operands[0]))
if align < 8 || align > 2048 || align&(align-1) != 0 {
return 0
}
return (align - pos%align) % align
}
// riscvPadBytes renders PCALIGN padding: 4-byte NOPs (addi $0, X0, X0) with a
// trailing 2-byte compressed NOP when the pad is 2 mod 4, exactly as the
// toolchain lays the bytes down.
func riscvPadBytes(pad int) []byte {
out := make([]byte, 0, pad)
for ; pad >= 4; pad -= 4 {
out = append(out, 0x13, 0x00, 0x00, 0x00)
}
if pad == 2 {
out = append(out, 0x01, 0x00)
}
return out
}
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
// Most instructions are 4 bytes; MOV with a large immediate and I-type
// arithmetic with a large immediate expand to several (possibly compressed)
@@ -224,6 +387,10 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
}
return riscvItypeImmediateSize(mnem, imm)
}
// BYTE lays down one raw byte per operand.
if mnem == "BYTE" {
return len(ops)
}
// The toolchain's synthesised instructions: some emit one word, others
// expand to a fixed sequence.
return riscvExtendedSize(mnem, ops)
@@ -318,12 +485,171 @@ func isBranchLike(mnem string) bool {
return false
}
// riscvIsCondBranch reports whether m is a conditional branch, the only
// instruction class branch relaxation rewrites.
func riscvIsCondBranch(mnem string) bool {
switch mnem {
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "BGT", "BLE", "BGTU", "BLEU",
"BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ":
return true
}
return false
}
// riscvCSRNames maps the standard CSR mnemonics the assembler accepts onto
// their addresses.
var riscvCSRNames = map[string]int32{
"FFLAGS": 0x001,
"FRM": 0x002,
"FCSR": 0x003,
"VSTART": 0x008,
"VXSAT": 0x009,
"VXRM": 0x00A,
"VCSR": 0x00F,
"CYCLE": 0xC00,
"TIME": 0xC01,
"INSTRET": 0xC02,
"CYCLEH": 0xC80,
"TIMEH": 0xC81,
"INSTRETH": 0xC82,
"VL": 0xC20,
"VLENB": 0xC22,
}
// riscvCSRAddress resolves a CSR operand: an integer immediate or one of the
// standard CSR names.
func riscvCSRAddress(op *ast.Operand) (int32, bool) {
if isImmOperand(op) {
return immFromOperand(op), true
}
if op.Addr.Sym != nil {
if v, ok := riscvCSRNames[strings.ToUpper(op.Addr.Sym.Name)]; ok {
return v, true
}
}
return 0, false
}
// riscvPCRelOffset reports the N of a branch or jump operand spelled N(PC):
// the displacement counted in source instructions from the branch itself.
func riscvPCRelOffset(instr *ast.Instr) (int, bool) {
mnem := strings.ToUpper(instr.Mnemonic.Text)
switch mnem {
case "JMP":
if len(instr.Operands) != 1 {
return 0, false
}
case "JAL":
if len(instr.Operands) != 1 && len(instr.Operands) != 2 {
return 0, false
}
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU",
"BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ":
if len(instr.Operands) < 2 {
return 0, false
}
default:
return 0, false
}
op := instr.Operands[len(instr.Operands)-1]
if op.Kind == ast.OpAddr && op.Addr.Sym == nil && op.Addr.Base == "PC" {
return int(op.Addr.Offset), true
}
return 0, false
}
// riscvPCRelTargetOff resolves the target displacement of a branch whose last
// operand is N(PC): the toolchain's parser counts the source instructions at
// a uniform 4 bytes, so the target is the instruction N slots away, and the
// displacement tracks that instruction's final pc. A nil pcRelPcs (the
// layout passes) yields a placeholder range error; the caller tolerates it
// for branch-like instructions.
func riscvPCRelTargetOff(instr *ast.Instr, pc int, pcRelPcs map[*ast.Instr]int) (int, bool, error) {
off, ok := riscvPCRelOffset(instr)
if !ok {
return 0, false, nil
}
if pcRelPcs == nil {
return 0, true, &riscvRangeError{"pc-relative placeholder"}
}
targetPc, ok := pcRelPcs[instr]
if !ok {
return 0, true, fmt.Errorf("PC-relative target %d out of range", off)
}
return targetPc, true, nil
}
// riscvInvertedBranchEnc returns the encoding of mnem's inverted condition
// for the given operands: InvertBranch's table applied at the encoding level.
// The register operands are already in position for the inverted form.
func riscvInvertedBranchEnc(mnem string, ops []*ast.Operand) (riscvEnc, int, int, bool) {
reg := func(i int) int { return regFromOperand(ops[i]) }
switch mnem {
case "BEQ": // → BNE rs1, rs2
return riscvEnc{0x63, 0x1, 0x00}, reg(0), reg(1), true
case "BNE": // → BEQ rs1, rs2
return riscvEnc{0x63, 0x0, 0x00}, reg(0), reg(1), true
case "BLT": // → BGE rs1, rs2
return riscvEnc{0x63, 0x5, 0x00}, reg(0), reg(1), true
case "BGE": // → BLT rs1, rs2
return riscvEnc{0x63, 0x4, 0x00}, reg(0), reg(1), true
case "BLTU": // → BGEU rs1, rs2
return riscvEnc{0x63, 0x7, 0x00}, reg(0), reg(1), true
case "BGEU": // → BLTU rs1, rs2
return riscvEnc{0x63, 0x6, 0x00}, reg(0), reg(1), true
case "BEQZ": // → BNEZ rs, X0
return riscvEnc{0x63, 0x1, 0x00}, reg(0), 0, true
case "BNEZ": // → BEQZ rs, X0
return riscvEnc{0x63, 0x0, 0x00}, reg(0), 0, true
case "BLTZ": // → BGEZ rs, X0
return riscvEnc{0x63, 0x5, 0x00}, reg(0), 0, true
case "BGEZ": // → BLTZ rs, X0
return riscvEnc{0x63, 0x4, 0x00}, reg(0), 0, true
case "BLEZ": // → BGTZ: blt X0, rs
return riscvEnc{0x63, 0x4, 0x00}, 0, reg(0), true
case "BGTZ": // → BLEZ: bge X0, rs
return riscvEnc{0x63, 0x5, 0x00}, 0, reg(0), true
case "BGT": // → BLE: bge rs2, rs1
return riscvEnc{0x63, 0x5, 0x00}, reg(1), reg(0), true
case "BLE": // → BGT: blt rs2, rs1
return riscvEnc{0x63, 0x4, 0x00}, reg(1), reg(0), true
case "BGTU": // → BLEU: bgeu rs2, rs1
return riscvEnc{0x63, 0x7, 0x00}, reg(1), reg(0), true
case "BLEU": // → BGTU: bltu rs2, rs1
return riscvEnc{0x63, 0x6, 0x00}, reg(1), reg(0), true
}
return riscvEnc{}, 0, 0, false
}
// riscvRangeError reports a branch or jump displacement beyond its
// architecture limit. The layout passes tolerate it (the relaxation pass
// rewrites overlong conditional branches before the final encoding); a range
// error reaching the final pass is a real failure.
type riscvRangeError struct{ msg string }
func (e *riscvRangeError) Error() string { return e.msg }
// riscvIsRangeError reports whether err is a displacement-range rejection.
func riscvIsRangeError(err error) bool {
var re *riscvRangeError
return errors.As(err, &re)
}
// riscvRoundModes maps the rounding-mode suffixes onto their funct7 codes.
var riscvRoundModes = map[string]uint32{
"RNE": 0,
"RTZ": 1,
"RDN": 2,
"RUP": 3,
"RMM": 4,
}
// 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 &riscvRangeError{fmt.Sprintf("branch to %q too far (13-bit range)", target)}
}
return nil
}
@@ -332,13 +658,13 @@ func riscvCheckBranchOffset(target string, off int32) error {
// 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 &riscvRangeError{fmt.Sprintf("jump to %q too far (21-bit range)", target)}
}
return nil
}
// encodeRISCVInstr encodes a single RISC-V instruction.
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc, pcRelPcs map[*ast.Instr]int) ([]byte, error) {
mnem := instr.Mnemonic.Text
ops := instr.Operands
var immNeg bool
@@ -351,6 +677,27 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
// RET = epilogue (restore LR and close the frame when present) +
// uncompressed JALR X0, 0(X1) (the toolchain never compresses RET).
return riscvReturn(fi), nil
case "WORD":
// WORD $w lays down a raw 32-bit little-endian word.
if len(ops) != 1 {
return nil, fmt.Errorf("WORD expects 1 operand, got %d", len(ops))
}
w := int64(immFromOperand(ops[0]))
if w < 0 || w > 0xFFFFFFFF {
return nil, fmt.Errorf("WORD: immediate %d does not fit a 32-bit word", w)
}
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}, nil
case "BYTE":
// BYTE $b lays down one raw byte per operand.
var out []byte
for _, op := range ops {
b := int64(immFromOperand(op))
if b < 0 || b > 0xFF {
return nil, fmt.Errorf("BYTE: immediate %d does not fit a byte", b)
}
out = append(out, byte(b))
}
return out, nil
case "CALL":
// CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL
// relocation. The Go assembler rejects CALL to a local branch label.
@@ -404,6 +751,17 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, rs1, 0)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel {
if err != nil {
return nil, err
}
offset := int32(off - pc)
if err := riscvCheckJumpOffset("", offset); err != nil {
return nil, err
}
word = riscvJType(0, offset)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
}
targetOff, ok := offsets[target]
if !ok {
@@ -424,6 +782,18 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
} else if len(ops) == 1 {
target = labelFromOperand(ops[0])
}
if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel {
if err != nil {
return nil, err
}
targetOff := off
offset := int32(targetOff - pc)
if err := riscvCheckJumpOffset(target, offset); err != nil {
return nil, err
}
word = riscvJType(rd, offset)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
targetOff, ok := offsets[target]
if !ok {
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
@@ -456,10 +826,20 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
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))
targetOff := 0
target := ""
if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel {
if err != nil {
return nil, err
}
targetOff = off
} else {
target = labelFromOperand(ops[1])
var ok bool
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
@@ -484,18 +864,25 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
// System instructions with no operands.
case "FENCE", "ECALL", "EBREAK":
case "FENCE", "ECALL", "EBREAK", "FENCE.TSO", "PAUSE":
enc, ok := riscvInstrTable[mnem]
if !ok {
return nil, fmt.Errorf("unsupported system instruction %q", mnem)
}
// 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).
// (the toolchain's encodeFenceOperand TYPE_NONE default). FENCE.TSO
// carries the TSO fence mode with RW predecessor and successor.
imm := int32(0)
if mnem == "FENCE" {
imm = 0x0FF
}
if mnem == "FENCE.TSO" {
imm = 0x833
}
if mnem == "PAUSE" {
imm = 0x010
}
word = riscvIType(enc, 0, 0, imm)
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
@@ -516,6 +903,29 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
}
// FP conversions with an explicit rounding mode: FCVTWS.RNE and friends
// suffix the base mnemonic with RNE/RTZ/RDN/RUP/RMM, which lands in the
// low three bits of the funct7 field.
if i := strings.IndexByte(mnem, '.'); i > 0 {
if base, ok := riscvCvtTable[mnem[:i]]; ok {
rm, ok := riscvRoundModes[mnem[i+1:]]
if !ok {
return nil, fmt.Errorf("unsupported rounding mode in %q", mnem)
}
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)
}
base.funct7 = (base.funct7 &^ 7) | rm
word := riscvCvtType(base, 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 {
@@ -532,29 +942,103 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
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 {
// CSR instructions: INSTR csr, rs1|uimm, rd (destination last). The
// write-only pseudos (CSRS/CSRC/CSRW and their immediate forms) spell
// the source first, the CSR second, and read the destination as X0; the
// immediate or register variant follows the source operand's kind.
csrMnem := mnem
csrPseudo := false
csrRead := false
csrFix := int32(0)
switch mnem {
case "CSRS", "CSRW", "CSRC", "CSRSI", "CSRWI", "CSRCI":
csrMnem = map[string]string{
"CSRS": "CSRRS", "CSRW": "CSRRW", "CSRC": "CSRRC",
"CSRSI": "CSRRSI", "CSRWI": "CSRRWI", "CSRCI": "CSRRCI",
}[mnem]
csrPseudo = true
// CSRR csr, rd is CSRRS rd, csr, X0; the read pseudos RDCYCLE/RDTIME/
// RDINSTRET fix the CSR to cycle/time/instret.
case "CSRR":
csrMnem = "CSRRS"
csrPseudo = true
csrRead = true
case "RDCYCLE", "RDTIME", "RDINSTRET":
csrMnem = "CSRRS"
csrPseudo = true
csrRead = true
csrFix = map[string]int32{"RDCYCLE": 0xC00, "RDTIME": 0xC01, "RDINSTRET": 0xC02}[mnem]
}
if csrEnc, ok := riscvCsrTable[csrMnem]; ok {
if csrRead && len(ops) != 1 && len(ops) != 2 {
return nil, fmt.Errorf("%s expects 1 or 2 operands, got %d", mnem, len(ops))
}
if csrPseudo && !csrRead && len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
if !csrPseudo && len(ops) != 3 {
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
}
csr := immFromOperand(ops[0]) // CSR address (12-bit)
csrOp := ops[0]
srcOp := ops[0]
rdOp := ops[len(ops)-1]
switch {
case csrRead:
// CSRR csr, rd (or the fixed-CSR read pseudos with only rd).
case csrPseudo:
// src, csr.
if len(ops) > 1 {
csrOp, srcOp = ops[1], ops[0]
}
rdOp = nil
default:
// Either src, csr, rd or csr, src, rd: a CSR *name* in the
// second operand marks the toolchain's order.
srcOp = ops[1]
if op := ops[1]; op.Addr.Sym != nil {
if _, ok := riscvCSRNames[strings.ToUpper(op.Addr.Sym.Name)]; ok {
csrOp, srcOp = ops[1], ops[0]
}
}
}
csr, ok := riscvCSRAddress(csrOp)
if !ok && csrFix == 0 {
return nil, fmt.Errorf("%s: unknown CSR %q", mnem, csrOp.Raw)
}
if csrFix != 0 {
csr = csrFix
}
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
if rd < 0 {
return nil, fmt.Errorf("invalid destination register in %s", mnem)
rd := 0
if !csrPseudo {
rd = regFromOperand(rdOp) // destination register
if rd < 0 {
return nil, fmt.Errorf("invalid destination register in %s", mnem)
}
}
if csrRead {
rd = regFromOperand(rdOp)
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]))
switch {
case csrRead:
// CSRR reads with rs1 = X0: src stays zero.
case isImmOperand(srcOp):
// Immediate variant: the source is a 5-bit unsigned immediate.
src = int(immFromOperand(srcOp))
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])
case csrEnc.imm:
return nil, fmt.Errorf("%s expects an immediate source", mnem)
default:
// Register variant: the source is a register.
src = regFromOperand(srcOp)
if src < 0 {
return nil, fmt.Errorf("invalid source register in %s", mnem)
}
@@ -738,14 +1222,34 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
}
word = riscvSType(enc, rs1, rs2, imm)
// Branches: rs1, rs2, label.
// Branches: rs1, rs2, label. BGT/BLE/BGTU/BLEU are the swapped-spelling
// forms of BLT/BGE/BLTU/BGEU (bgt rs1, rs2 is blt rs2, rs1).
case len(ops) == 3 && isBranchInstr(mnem):
rs1 := regFromOperand(ops[0])
rs2 := regFromOperand(ops[1])
target := labelFromOperand(ops[2])
targetOff, ok := offsets[target]
if !ok {
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
switch mnem {
case "BGT":
enc, rs1, rs2 = riscvEnc{0x63, 0x4, 0x00}, rs2, rs1 // blt rs2, rs1
case "BLE":
enc, rs1, rs2 = riscvEnc{0x63, 0x5, 0x00}, rs2, rs1 // bge rs2, rs1
case "BGTU":
enc, rs1, rs2 = riscvEnc{0x63, 0x6, 0x00}, rs2, rs1 // bltu rs2, rs1
case "BLEU":
enc, rs1, rs2 = riscvEnc{0x63, 0x7, 0x00}, rs2, rs1 // bgeu rs2, rs1
}
targetOff := 0
if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel {
if err != nil {
return nil, err
}
targetOff = off
} else {
var ok bool
targetOff, ok = offsets[target]
if !ok {
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
}
}
offset := int32(targetOff - pc)
if rs1 < 0 || rs2 < 0 {
@@ -758,10 +1262,15 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
// The Go assembler never compresses branches to C.BEQZ/C.BNEZ.
word = riscvBType(enc, rs1, rs2, offset)
// U-type: rd, imm.
// U-type: rd, imm (or the toolchain testdata's INSTR $imm, rd).
case len(ops) == 2 && isUTypeInstr(mnem):
rd := regFromOperand(ops[0])
imm := immFromOperand(ops[1])
var rd int
var imm int32
if isImmOperand(ops[0]) {
imm, rd = immFromOperand(ops[0]), regFromOperand(ops[1])
} else {
rd, imm = regFromOperand(ops[0]), immFromOperand(ops[1])
}
if rd < 0 {
return nil, fmt.Errorf("invalid register in %s", mnem)
}
@@ -1226,6 +1735,11 @@ func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
mnem := riscvCompressMnem(instr)
ops := instr.Operands
// The immediate aliases fold onto their I-type mnemonics before
// compression: the toolchain compresses ADD $imm, rd as c.addi, exactly
// as it compresses the spelling ADDI.
var immNeg bool
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
switch mnem {
case "LD", "MOV":
@@ -1289,6 +1803,9 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
case "ADDI":
rd, rs1, imm := extractITypeParams(instr)
if immNeg {
imm = -imm
}
if rd == -1 || rs1 == -1 {
return 0, false
}
@@ -2045,7 +2562,8 @@ func isRTypeInstr(m string) bool {
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":
"MULW", "DIVW", "DIVUW", "REMW", "REMUW",
"CZEROEQZ", "CZERONEZ":
return true
}
return false
@@ -2085,7 +2603,7 @@ func isStoreInstr(m string) bool {
func isBranchInstr(m string) bool {
switch m {
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU":
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "BGT", "BLE", "BGTU", "BLEU":
return true
}
return false
@@ -2111,7 +2629,8 @@ func isFPArithInstr(m string) bool {
switch m {
case "FADDS", "FSUBS", "FMULS", "FDIVS",
"FADDD", "FSUBD", "FMULD", "FDIVD",
"FSQRTS", "FSQRTD", "FMINS", "FMAXS", "FMIND", "FMAXD", "FSGNJD":
"FSQRTS", "FSQRTD", "FMINS", "FMAXS", "FMIND", "FMAXD", "FSGNJD",
"FSGNJS", "FSGNJX", "FSGNJXD", "FSGNJXS", "FSGNJND", "FSGNJNS", "FSGNJNX":
return true
}
return false