From a3eaa82f873457ad89fe6ed9e7aa3c4326141c0f Mon Sep 17 00:00:00 2001 From: petrbalvin Date: Wed, 7 Oct 2026 21:34:06 +0200 Subject: [PATCH] feat(asm): take the loong64 register-pair spellings the toolchain parses Assisted-by: GLM 5.3 Flash --- asm/loong64_assemble.go | 123 +++++++++++++++++++++++++++----- asm/loong64_encode_test.go | 47 ++++++++++++ asm/loong64_errorparity_test.go | 56 +++++++++++++++ testdata/verify/pairs_loong64.s | 28 ++++++++ verify/l64_groundtruth_test.go | 1 + 5 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 testdata/verify/pairs_loong64.s diff --git a/asm/loong64_assemble.go b/asm/loong64_assemble.go index 0fbc842..4fabc3c 100644 --- a/asm/loong64_assemble.go +++ b/asm/loong64_assemble.go @@ -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" diff --git a/asm/loong64_encode_test.go b/asm/loong64_encode_test.go index 15b6c0b..962074c 100644 --- a/asm/loong64_encode_test.go +++ b/asm/loong64_encode_test.go @@ -1058,3 +1058,50 @@ TEXT ·bytes(SB), NOSPLIT, $0 } } } + +// TestLOONG64PairSugar pins the toolchain's register-pair spellings: a +// top-level colon between two registers splits the operand in two with the +// halves swapped, so INSTR R4:R5, R6 encodes exactly as INSTR R5, R4, R6. +// Each row is pinned two ways: against the word GOARCH=loong64 go tool asm +// emits for the pair spelling, and against gasm's own encoding of the +// direct spelling, which must be the same bytes. +func TestLOONG64PairSugar(t *testing.T) { + for _, tt := range []struct { + pair, direct string + want uint32 // the word go tool asm emits for the pair + }{ + {"MULV R4:R5, R6", "MULV R5, R4, R6", 0x001D9486}, + {"MULV R4:R5", "MULV R5, R4", 0x001D9484}, + {"MULV R6, R4:R5", "MULV R6, R5, R4", 0x001D98A4}, + {"DIVV R4:R5, R6", "DIVV R5, R4, R6", 0x00221486}, + {"MULHV R4:R5, R6", "MULHV R5, R4, R6", 0x001E1486}, + {"ADDV R4:R5, R6", "ADDV R5, R4, R6", 0x00109486}, + {"AND R4:R5, R6", "AND R5, R4, R6", 0x00149486}, + {"MOVV R4:R5", "MOVV R5, R4", 0x001500A4}, + {"MULD F4:F5, F6", "MULD F5, F4, F6", 0x01051486}, + {"VADDW V1:V2, V3", "VADDW V2, V1, V3", 0x700B0823}, + } { + t.Run(tt.pair, func(t *testing.T) { + pair := assembleLOONG64Helper(t, firstTextLOONG64(t, `#include "textflag.h" +TEXT ·p(SB), NOSPLIT, $0 + `+tt.pair+` + RET +`)) + direct := assembleLOONG64Helper(t, firstTextLOONG64(t, `#include "textflag.h" +TEXT ·d(SB), NOSPLIT, $0 + `+tt.direct+` + RET +`)) + if len(pair) < 4 { + t.Fatalf("pair spelling produced %d bytes", len(pair)) + } + got := binary.LittleEndian.Uint32(pair) + if got != tt.want { + t.Errorf("pair word = %08x, want %08x (go tool asm)", got, tt.want) + } + if len(direct) < 4 || !bytes.Equal(pair[:4], direct[:4]) { + t.Errorf("pair bytes % x differ from the direct spelling's % x", pair[:4], direct[:4]) + } + }) + } +} diff --git a/asm/loong64_errorparity_test.go b/asm/loong64_errorparity_test.go index 1494a35..7ded6be 100644 --- a/asm/loong64_errorparity_test.go +++ b/asm/loong64_errorparity_test.go @@ -215,6 +215,62 @@ func TestLoong64ForeignErrorParity(t *testing.T) { // and the pair of rejections is the parity this catalogue asserts. var loong64RejectedNames = []string{"RFE", "DUFFCOPY", "DUFFZERO", "PCALIGNMAX"} +// loong64PairErrorShapes walks the register-pair sugar's negative shapes, +// every one measured against GOARCH=loong64 go tool asm. want carries the +// toolchain's diagnostic where gasm's wording matches it (checked through +// the same equivalence as the catalogue above); an empty want pins the +// rejection alone, and the row's comment records the toolchain's own words +// for the divergence. +var loong64PairErrorShapes = []struct { + body string + want string +}{ + // A pair never splits inside an address; the toolchain's parse stage + // refuses it outright. + {"MULV (R4:R5), R6", "indirect through register pair"}, + // The reordered list fails the instruction's operand shape. The + // toolchain words these "illegal combination ..." at the class match; + // gasm words them at the operand count. + {"MOVV R4:R5, R6", ""}, + {"BEQ R4:R5, R6", ""}, + {"JMP R4:R5", ""}, + {"MULV R4:R5, R7:R8, R6", ""}, + {"MULV R4:R5, (R6)", ""}, + {"BEQ R4:R5, 2(PC)", ""}, + // A right half outside the register table; the toolchain's parse stage + // again ("illegal or missing addressing mode for symbol label"). + {"MULV R4:label", "illegal or missing addressing mode for symbol label"}, + // An immediate half on either side. The toolchain reorders the pair + // first and rejects at the class match ("illegal combination MULV + // U3CON ...", "MULV: expected register; found $4"); gasm rejects the + // shape without the reorder. + {"MOVV $4:R5", ""}, + {"MULV R4:$5", ""}, +} + +// TestLoong64PairErrorParity requires gasm to refuse every register-pair +// spelling the toolchain refuses, with an equivalent diagnostic where the +// row carries one. The toolchain's accept side of the sugar is pinned +// byte for byte by TestLOONG64PairSugar; the empty-want rows are documented +// wording divergences, not acceptance divergences. +func TestLoong64PairErrorParity(t *testing.T) { + for _, tt := range loong64PairErrorShapes { + src := "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n\t" + tt.body + "\n\tRET\n" + f, perr := parser.Parse("pairparity.s", src) + if len(perr) > 0 { + continue // the parser already rejects the spelling + } + _, aerr := AssembleFileLOONG64(f) + if aerr == nil { + t.Errorf("gasm accepts what the toolchain rejects: %s", tt.body) + continue + } + if tt.want != "" && !loong64DiagEquivalent(tt.want, aerr.Error()) { + t.Errorf("gasm rejects %s with an inequivalent diagnostic:\n toolchain: %s\n gasm: %s", tt.body, tt.want, aerr) + } + } +} + // TestLoong64BacklogNameParity walks the audit's known-but-unencodable // backlog: the names go tool asm recognises on loong64 yet refuses to // encode must be refused by gasm as well, and the one name the audit diff --git a/testdata/verify/pairs_loong64.s b/testdata/verify/pairs_loong64.s new file mode 100644 index 0000000..62dae32 --- /dev/null +++ b/testdata/verify/pairs_loong64.s @@ -0,0 +1,28 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// Differential kernel for the loong64 register-pair spellings: a top-level +// colon between two registers splits the operand in two with the halves +// swapped, so R4:R5 encodes exactly as R5, R4. The integer multiply and +// divide families, the FP pair and the vector pair, each the way +// GOARCH=loong64 go tool asm takes them. + +#include "textflag.h" + +TEXT ·pairs(SB), NOSPLIT, $0-0 + MULV R4 : R5, R6 + MULV R4 : R5 + MULV R6, R4 : R5 + DIVV R4 : R5, R6 + DIVVU R4 : R5, R6 + MULHV R4 : R5, R6 + MULHVU R4 : R5, R6 + ADDV R4 : R5, R6 + ADDW R4 : R5, R6 + AND R4 : R5, R6 + OR R4 : R5, R6 + XOR R4 : R5, R6 + MOVV R4 : R5 + MULD F4 : F5, F6 + VADDW V1 : V2, V3 + RET diff --git a/verify/l64_groundtruth_test.go b/verify/l64_groundtruth_test.go index f45e4b8..e9d6e13 100644 --- a/verify/l64_groundtruth_test.go +++ b/verify/l64_groundtruth_test.go @@ -28,6 +28,7 @@ func TestGroundTruthLOONG64(t *testing.T) { "../testdata/verify/movwfp_loong64.s", "../testdata/verify/branchu_loong64.s", "../testdata/verify/atomics_loong64.s", + "../testdata/verify/pairs_loong64.s", "../testdata/verify/vector_loong64.s", "../testdata/verify/pcalign_loong64.s", "../testdata/verify/l64forms_loong64.s",