feat(asm): take the loong64 register-pair spellings the toolchain parses

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin authored and Petr Balvín committed 2026-10-07 21:42:14 +02:00
1 parent 1031cd9ae7
commit a3eaa82f87
5 files changed
+237 -18

No files matched your search

+105 -18
View File
@@ -360,6 +360,59 @@ func l64SubToAdd(mnem string, ops []*ast.Operand) (string, bool) {
return mnem, false
}
// l64PairRegister reports whether name names a register the pair sugar may
// carry: the general, FP, FCC and FCSR spellings loong64RegNum resolves, or
// a bare LSX/LASX vector register (V0-V31, X0-X31), which lives outside
// that table and parses only through the vector reader.
func l64PairRegister(name string) bool {
if loong64RegNum(name) >= 0 {
return true
}
v, ok := l64ParseVecOperand(&ast.Operand{Kind: ast.OpAddr, Raw: name})
return ok && !v.hasSuf
}
// l64ExpandPairs rewrites the toolchain's register-pair spellings into the
// operand list its own parser produces: a top-level colon between two
// registers splits the operand in two with the halves swapped (the old x86
// "register pair" syntax the shared grammar keeps on every GOARCH), so
// INSTR R4:R5, R6 encodes exactly as INSTR R5, R4, R6, bytes included.
// The sugar is pure syntax: every acceptance question the reordered list
// raises is answered by the ordinary operand matching. changed reports
// whether any operand carried it; the operand objects are copied, never
// edited in the shared syntax tree.
func l64ExpandPairs(ops []*ast.Operand) ([]*ast.Operand, bool) {
changed := false
var out []*ast.Operand
for i, op := range ops {
sfx := strings.Join(strings.Fields(op.Addr.Shift), "")
if op.Addr.Base != "" || !strings.HasPrefix(sfx, ":") || !l64PairRegister(sfx[1:]) {
if changed {
out = append(out, op)
}
continue
}
if !changed {
out = make([]*ast.Operand, 0, len(ops)+1)
out = append(out, ops[:i]...)
changed = true
}
name := sfx[1:]
// The second register lands first. Both halves keep the operand
// shape a plain register spelling parses to (a bare symbol
// reference), which is what every operand consumer reads.
second := *op
second.Addr.Shift = ""
second.Addr.Sym = &ast.Symbol{Raw: name, Name: name}
second.Raw = name
first := *op
first.Addr.Shift = ""
first.Raw = operandRegName(op)
out = append(out, &second, &first)
}
return out, changed
}
// loong64InstrSize returns the encoded size of an instruction: 4 bytes for
// most, more for the multi-instruction expansions.
func loong64InstrSize(instr *ast.Instr, fi loong64FrameInfo) int {
@@ -367,6 +420,11 @@ func loong64InstrSize(instr *ast.Instr, fi loong64FrameInfo) int {
ops := instr.Operands
var neg bool
mnem, neg = l64SubToAdd(mnem, ops)
// The register-pair sugar widens the operand list exactly as the encode
// pass sees it, so both passes count the same instruction.
if pairOps, changed := l64ExpandPairs(ops); changed {
ops = pairOps
}
if mnem == "RET" {
return len(loong64Return(fi))
@@ -485,6 +543,13 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
copy(ops2[1:], ops[1:])
ops = ops2
}
// The toolchain's register-pair sugar: INSTR R4:R5, R6 encodes exactly
// as INSTR R5, R4, R6. The expansion is a fresh operand list; the
// instruction pointer stays the original one, because the PC-relative
// layout tables are keyed on it.
if pairOps, changed := l64ExpandPairs(ops); changed {
ops = pairOps
}
// Pseudo-instructions and the branches first.
// Before any of them: the toolchain's loong64 operand grammar has no
@@ -494,13 +559,28 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
// the composition and emit the bare register, so every operand whose
// verbatim suffix is not the element-selector index (V1.B[3] records
// the name as V1.B and the suffix "[3]", which is a real loong64
// form) is rejected outright.
// form) or the register-pair sugar expanded above is rejected
// outright, with the toolchain's own wording where the shape is one
// it diagnoses.
for _, op := range ops {
sfx := strings.Join(strings.Fields(op.Addr.Shift), "")
if sfx != "" && !strings.HasPrefix(sfx, "[") {
return nil, fmt.Errorf("%s: shifted register operand %s%s is not a loong64 form",
mnem, operandRegName(op), sfx)
if sfx == "" || strings.HasPrefix(sfx, "[") {
continue
}
if strings.HasPrefix(sfx, ":") {
if op.Addr.Base != "" {
// (Rj:Rk) inside an address: the pair never splits there.
return nil, fmt.Errorf("%s: indirect through register pair", mnem)
}
// A register-pair spelling with a register right half was
// expanded above, so whatever survives carries a right half
// outside the register table: the toolchain's parse-stage
// objection (R4:label).
return nil, fmt.Errorf("%s: illegal or missing addressing mode for symbol %s",
mnem, strings.TrimPrefix(sfx, ":"))
}
return nil, fmt.Errorf("%s: shifted register operand %s%s is not a loong64 form",
mnem, operandRegName(op), sfx)
}
switch mnem {
case "RET":
@@ -629,11 +709,11 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
l64rrr(preldx, 30, rj, hint),
), nil
case "JMP", "B":
return encodeLOONG64Branch(instr, mnem, pc, offsets, false, resolve, relocs, pcRelPcs)
return encodeLOONG64Branch(instr, ops, mnem, pc, offsets, false, resolve, relocs, pcRelPcs)
case "JAL", "CALL", "BL":
return encodeLOONG64Branch(instr, mnem, pc, offsets, true, resolve, relocs, pcRelPcs)
return encodeLOONG64Branch(instr, ops, mnem, pc, offsets, true, resolve, relocs, pcRelPcs)
case "MOV", "MOVB", "MOVH", "MOVW", "MOVV", "MOVBU", "MOVHU", "MOVWU", "MOVF", "MOVD":
return encodeLOONG64Mov(instr, mnem, fi, relocs)
return encodeLOONG64Mov(ops, mnem, fi, relocs)
}
// 16-bit branches (BEQ/BNE/BLT/BGE/BLTU/BGEU) and JIRL.
@@ -703,7 +783,7 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
// The LSX/LASX vector slice and the VMOVQ/XVMOVQ move family, before
// the integer/FP table (their mnemonics overlap the table's 2R format
// but resolve vector-bank registers).
if code, handled, err := encodeLOONG64Vector(instr, mnem, fi); handled {
if code, handled, err := encodeLOONG64Vector(mnem, ops, fi); handled {
if err != nil {
return nil, err
}
@@ -960,11 +1040,14 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
//
// JMP/B label → b label JMP/B (rj) → jirl r0, rj, 0
// JAL/CALL/BL label → bl label JAL/CALL/BL (rj) → jirl r1, rj, 0
func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[string]int, link bool, resolve func(string) string, relocs *[]Reloc, pcRelPcs map[*ast.Instr]int) ([]byte, error) {
if len(instr.Operands) != 1 {
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(instr.Operands))
//
// instr is the original instruction, the key the PC-relative layout table
// is keyed on; ops are the operands to encode, the pair expansion included.
func encodeLOONG64Branch(instr *ast.Instr, ops []*ast.Operand, mnem string, pc int, offsets map[string]int, link bool, resolve func(string) string, relocs *[]Reloc, pcRelPcs map[*ast.Instr]int) ([]byte, error) {
if len(ops) != 1 {
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
}
op := instr.Operands[0]
op := ops[0]
// PC-relative displacement: N(PC) resolves to the instruction N slots
// away in source order (the toolchain's parse-time count), and the field
// carries the final pc distance in instruction units.
@@ -1376,8 +1459,7 @@ func l64ImmMem(op *ast.Operand) (off int32, base int, ok bool) {
// MOVx $sym(SB), rd address of a static symbol (pcalau12i+addi.d)
// MOVx sym(SB), rd load from a static symbol (pcalau12i+ld)
// MOVx rd, sym(SB) store to a static symbol (pcalau12i+st)
func encodeLOONG64Mov(instr *ast.Instr, mnem string, fi loong64FrameInfo, relocs *[]Reloc) ([]byte, error) {
ops := instr.Operands
func encodeLOONG64Mov(ops []*ast.Operand, mnem string, fi loong64FrameInfo, relocs *[]Reloc) ([]byte, error) {
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
@@ -2008,8 +2090,14 @@ func operandRegName(op *ast.Operand) string {
return ""
}
// l64Reg returns the register number of an operand, or -1.
// l64Reg returns the register number of a register operand, or -1. A
// memory reference is not a register, however register-shaped its base:
// the toolchain's class match refuses one wherever a C_REG is required,
// and reading the base's number here would encode it silently.
func l64Reg(op *ast.Operand) int {
if isMemOperand(op) {
return -1
}
return loong64RegNum(operandRegName(op))
}
@@ -2228,16 +2316,15 @@ func l64VecElementBase(lasx bool, v l64VecOperand) (int, bool) {
// vector plus the VMOVQ/XVMOVQ move family. handled reports whether the
// mnemonic belongs to the vector slice; the operand shapes and opcode
// constants reproduce GOARCH=loong64 `go tool asm` exactly.
func encodeLOONG64Vector(instr *ast.Instr, mnem string, fi loong64FrameInfo) ([]byte, bool, error) {
func encodeLOONG64Vector(mnem string, ops []*ast.Operand, fi loong64FrameInfo) ([]byte, bool, error) {
if mnem == "VMOVQ" || mnem == "XVMOVQ" {
code, err := encodeLOONG64Vmovq(mnem == "XVMOVQ", instr.Operands, fi)
code, err := encodeLOONG64Vmovq(mnem == "XVMOVQ", ops, fi)
return code, true, err
}
lasx, ok := l64VecBank[mnem]
if !ok {
return nil, false, nil
}
ops := instr.Operands
bank := "V"
if lasx {
bank = "X"