444 lines
12 KiB
Go
444 lines
12 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package asm
|
||
|
||
import (
|
||
"bytes"
|
||
"testing"
|
||
|
||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||
)
|
||
|
||
// TestLOONG64_sys exercises the no-operand system instructions and the
|
||
// bare-data pseudo-instructions. The words match `go tool asm`
|
||
// (GOARCH=loong64) for the same source.
|
||
func TestLOONG64_sys(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·sys(SB), NOSPLIT, $0
|
||
NOOP
|
||
UNDEF
|
||
WORD $0x12345678
|
||
SYSCALL $0x10
|
||
BREAK $0x20
|
||
DBAR $1
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x03400000, // andi r0, r0, 0 (NOOP)
|
||
0x002A0000, // break 0 (UNDEF)
|
||
0x12345678, // WORD
|
||
0x002B0010, // syscall 0x10
|
||
0x002A0020, // break 0x20
|
||
0x38720001, // dbar 1
|
||
0x4C000020, // jirl r0, r1, 0
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_branches21 exercises the single-register branch forms: the
|
||
// 21-bit BEQZ/BNEZ/BLTZ/BGEZ and the rd-field BGTZ/BLEZ.
|
||
func TestLOONG64_branches21(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·b21(SB), NOSPLIT, $0
|
||
BEQZ R4, done
|
||
BNEZ R5, done
|
||
BLTZ R6, done
|
||
BGEZ R7, done
|
||
BGTZ R8, done
|
||
BLEZ R9, done
|
||
done:
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x40001880, // beqz r4, +6
|
||
0x440014A0, // bnez r5, +5
|
||
0x600010C0, // bltz r6, +4
|
||
0x64000CE0, // bgez r7, +3
|
||
0x60000808, // bgtz r8, +2 (register in the rd field)
|
||
0x64000409, // blez r9, +1
|
||
0x4C000020, // jirl r0, r1, 0
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_fma exercises the four fused multiply-add forms (4 and 3
|
||
// operand spellings).
|
||
func TestLOONG64_fma(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·fma(SB), NOSPLIT, $0
|
||
FMADDD F0, F1, F2, F3
|
||
FMSUBD F4, F5, F6
|
||
FNMADDD F7, F8, F9, F10
|
||
FNMSUBD F11, F12, F13
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x08200443, // fmadd.d f3, f2, f1, f0
|
||
0x086214C6, // fmsub.d f6, f5, f5, f4
|
||
0x08A3A12A, // fnmadd.d f10, f9, f8, f7
|
||
0x08E5B1AD, // fnmsub.d f13, f12, f12, f11
|
||
0x4C000020,
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_bitops exercises BSTRINS/BSTRPICK (the 6-bit msb/lsb fields)
|
||
// and ALSL (the sa−1 shift field).
|
||
func TestLOONG64_bitops(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·bits(SB), NOSPLIT, $0
|
||
BSTRINSW $3, R4, $0, R5
|
||
BSTRINSV $3, R4, $1, R6
|
||
BSTRPICKW $3, R4, $0, R5
|
||
BSTRPICKV $6, R7, $0, R8
|
||
ALSLW $1, R4, R5, R6
|
||
ALSLW $4, R7, R8, R9
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x00630085, // bstrins.w r5, r4, $3, $0
|
||
0x00830486, // bstrins.d r6, r4, $3, $1
|
||
0x00638085, // bstrpick.w r5, r4, $3, $0
|
||
0x00C600E8, // bstrpick.d r8, r7, $6, $0
|
||
0x00041486, // alsl.w r6, r5, r4, $1 (sa-1)
|
||
0x0005A0E9, // alsl.w r9, r8, r7, $4
|
||
0x4C000020,
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_ptr exercises the 14-bit-offset memory forms (LL/SC/MOVWP/
|
||
// MOVVP with the offset scaled by 4) and PRELD.
|
||
func TestLOONG64_ptr(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·ptr(SB), NOSPLIT, $0
|
||
LLW 8(R14), R15
|
||
SCW R16, -4(R17)
|
||
MOVWP 16(R18), R19
|
||
MOVVP R20, 24(R21)
|
||
PRELD 32(R22), $0
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x200009CF, // ll.w r15, 8(r14)
|
||
0x21FFFE30, // sc.w r16, -4(r17)
|
||
0x24001253, // ldptr.w r19, 16(r18)
|
||
0x27001AB4, // stptr.d r20, 24(r21)
|
||
0x2AC082C0, // preld 32(r22), 0
|
||
0x4C000020,
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_atomics exercises the AM* read-modify-write forms and
|
||
// RDTIME, plus the MOVV FP→GP move.
|
||
func TestLOONG64_atomics(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·atoms(SB), NOSPLIT, $0
|
||
AMADDW R4, (R5), R6
|
||
RDTIMED R7, R8
|
||
MOVV F1, R2
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x386110A6, // amadd.w r6, r5, r4
|
||
0x000068E8, // rdtime.d r8, r7
|
||
0x0114B822, // movfr2gr.d r2, f1
|
||
0x4C000020,
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_lu52 exercises the LU52I.D immediate form (a gasm extension
|
||
// the toolchain reaches only through its MOVV expansion).
|
||
func TestLOONG64_lu52(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·lu52(SB), NOSPLIT, $0
|
||
LU52ID $0x345, R10
|
||
LU52ID $0x123, R11, R12
|
||
ADDV16 $0x10000, R13
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x030D154A, // lu52i.d r10, r10, 0x345
|
||
0x03048D6C, // lu52i.d r12, r11, 0x123
|
||
0x100005AD, // addu16i.d r13, r13, 0x10000>>16
|
||
0x4C000020,
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_sbRefs checks the static-symbol reference forms through the
|
||
// full file assembly: each pcalau12i+addi.d/ld/st pair carries the
|
||
// R_LOONG64_ADDR_HI/LO relocation pair, and the immediate fields are left
|
||
// zero for the linker.
|
||
func TestLOONG64_sbRefs(t *testing.T) {
|
||
f, errs := parser.Parse("sb_loong64.s", `#include "textflag.h"
|
||
TEXT ·sb(SB), NOSPLIT, $0
|
||
MOVV $·table(SB), R4
|
||
MOVV ·table+8(SB), R5
|
||
MOVV R6, ·table(SB)
|
||
RET
|
||
|
||
GLOBL ·table(SB), RODATA, $8
|
||
DATA ·table+0(SB)/8, $42
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFileLOONG64(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||
}
|
||
fn := img.Funcs[0]
|
||
if fn.Size != 28 {
|
||
t.Fatalf("function size = %d, want 28", fn.Size)
|
||
}
|
||
var hi, lo int
|
||
// The three references: $·table (0), ·table+8 (8), ·table (0).
|
||
wantAdd := []int64{0, 0, 8, 8, 0, 0}
|
||
for i, r := range fn.Relocs {
|
||
wantKind := RelLoong64AddrHi
|
||
wantOff := (i / 2) * 8
|
||
if i%2 == 1 {
|
||
wantKind = RelLoong64AddrLo
|
||
wantOff += 4
|
||
}
|
||
if r.Kind != wantKind || r.Off != wantOff || r.Name != "table" || r.Addend != wantAdd[i] {
|
||
t.Errorf("reloc %d = {kind %v off %d name %q addend %d}", i, r.Kind, r.Off, r.Name, r.Addend)
|
||
}
|
||
if r.Kind == RelLoong64AddrHi {
|
||
hi++
|
||
} else {
|
||
lo++
|
||
}
|
||
}
|
||
if hi != 3 || lo != 3 {
|
||
t.Errorf("relocs = %d hi + %d lo, want 3 + 3", hi, lo)
|
||
}
|
||
// The image carries the zero-immediate pair encodings (the linker
|
||
// fills the immediate fields from the relocations).
|
||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||
wantWords(t, code,
|
||
0x1A000004, // pcalau12i r4, 0
|
||
0x02C00084, // addi.d r4, r4, 0
|
||
0x1A00001E, // pcalau12i r30, 0
|
||
0x28C003C5, // ld.d r5, 0(r30)
|
||
0x1A00001E, // pcalau12i r30, 0
|
||
0x29C003C6, // st.d r6, 0(r30)
|
||
0x4C000020, // jirl r0, r1, 0
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_errors checks the encoder's error paths: undefined labels,
|
||
// invalid register operands and operand-count mismatches. The X0 and
|
||
// AMADDW cases follow the oracle: GOARCH=loong64 go tool asm rejects
|
||
// `BEQZ X0` (the X bank is not an integer register) and the two-register
|
||
// `AMADDW R4, R5` (the AM* family is strictly `val, (addr), result`).
|
||
func TestLOONG64_errors(t *testing.T) {
|
||
cases := []string{
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
JMP nowhere
|
||
RET
|
||
`,
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
BEQZ X0, done
|
||
done:
|
||
RET
|
||
`,
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
ADDV R4
|
||
RET
|
||
`,
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
FMADDD F0, F1
|
||
RET
|
||
`,
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
AMADDW R4, R5
|
||
RET
|
||
`,
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
WORD
|
||
RET
|
||
`,
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
PRELD 32(R4)
|
||
RET
|
||
`,
|
||
`TEXT ·e(SB), NOSPLIT, $0
|
||
ALSLW $5, R4, R5, R6
|
||
RET
|
||
`,
|
||
}
|
||
for i, src := range cases {
|
||
fn := firstTextLOONG64(t, src)
|
||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||
t.Errorf("case %d: expected an error, got none", i)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestLOONG64_pcsp checks the stack-adjustment table of a framed function:
|
||
// the prologue raises the SP delta by autosize (in effect from the third
|
||
// instruction) and the RET's epilogue restores it to zero, with the pc deltas
|
||
// in MinLC (4) units; byte-identical to `go tool asm`.
|
||
func TestLOONG64_pcsp(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
src string
|
||
want []byte
|
||
}{
|
||
{
|
||
"leaf",
|
||
`#include "textflag.h"
|
||
TEXT ·leaf(SB), NOSPLIT, $8-0
|
||
MOVV R4, R5
|
||
RET
|
||
`,
|
||
[]byte{0x02, 0x02, 0x20, 0x03, 0x1f, 0x01, 0x00},
|
||
},
|
||
{
|
||
"nonleaf",
|
||
`#include "textflag.h"
|
||
TEXT ·nonleaf(SB), NOSPLIT, $8-0
|
||
MOVV R4, R5
|
||
JAL (R12)
|
||
RET
|
||
`,
|
||
[]byte{0x02, 0x02, 0x20, 0x05, 0x1f, 0x01, 0x00},
|
||
},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
f, errs := parser.Parse("pcsp_loong64.s", c.src)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFileLOONG64(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||
}
|
||
if got := pcspTable(img.Funcs[0], 4); !bytes.Equal(got, c.want) {
|
||
t.Errorf("pcsp = % x, want % x", got, c.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestLOONG64_sbRefsUndefined checks that a reference to a symbol no GLOBL
|
||
// defines assembles into a relocation and is rejected at object emission.
|
||
func TestLOONG64_sbRefsUndefined(t *testing.T) {
|
||
f, errs := parser.Parse("sb_loong64.s", `#include "textflag.h"
|
||
TEXT ·sb(SB), NOSPLIT, $0
|
||
MOVV missing(SB), R4
|
||
RET
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFileLOONG64(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFileLOONG64: %v", err)
|
||
}
|
||
if len(img.Funcs[0].Relocs) != 2 {
|
||
t.Fatalf("relocs = %d, want the HI/LO pair", len(img.Funcs[0].Relocs))
|
||
}
|
||
if _, err := img.GOObjectLOONG64("p", "sb_loong64.s"); err == nil {
|
||
t.Error("expected an unknown-symbol error at emission")
|
||
}
|
||
}
|
||
|
||
// TestLOONG64_movImmToFp checks the immediate-to-FP move: MOVW $c, Fd is the
|
||
// only spelling the toolchain accepts, expanding to ori (or addi.w for the
|
||
// negative span) into R30 plus movgr2fr.w. The pinned words are the
|
||
// toolchain's own bytes; the other widths and out-of-range constants are
|
||
// illegal combinations there and are diagnosed here.
|
||
func TestLOONG64_movImmToFp(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·fpmov(SB), NOSPLIT, $0
|
||
MOVW $0x1, F0
|
||
MOVW $0x2, F4
|
||
MOVW $-1, F4
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
want := []byte{
|
||
0x1e, 0x04, 0x80, 0x03, // ori r30, r0, 1
|
||
0xc0, 0xa7, 0x14, 0x01, // movgr2fr.w f0, r30
|
||
0x1e, 0x08, 0x80, 0x03, // ori r30, r0, 2
|
||
0xc4, 0xa7, 0x14, 0x01, // movgr2fr.w f4, r30
|
||
0x1e, 0xfc, 0xbf, 0x02, // addi.w r30, r0, -1
|
||
0xc4, 0xa7, 0x14, 0x01, // movgr2fr.w f4, r30
|
||
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
|
||
}
|
||
if !bytes.Equal(code, want) {
|
||
t.Errorf("code = % x\nwant % x", code, want)
|
||
}
|
||
}
|
||
|
||
// TestLOONG64_movImmToFpErrors checks the immediate-to-FP diagnostics: the
|
||
// widths the toolchain rejects as illegal combinations, and constants beyond
|
||
// the 12-bit ori/addi.w span (the toolchain never materialises a wider
|
||
// constant on this path).
|
||
func TestLOONG64_movImmToFpErrors(t *testing.T) {
|
||
cases := []string{
|
||
"MOVV $1, F0",
|
||
"MOVF $2, F4",
|
||
"MOVD $2, F4",
|
||
"MOVW $100000, F1",
|
||
"MOVW $-2049, F1",
|
||
"MOVW $4096, F1",
|
||
}
|
||
for _, src := range cases {
|
||
fn := firstTextLOONG64(t, "#include \"textflag.h\"\nTEXT ·e(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n")
|
||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||
t.Errorf("%s: expected an error, got none", src)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestLOONG64_branch16Unsigned pins the unsigned two-operand branches: with
|
||
// one register BLTU/BGEU keep the register-register form against R0 (never
|
||
// taken), the toolchain's encoding, where a beqz would test the wrong
|
||
// condition; the three-operand forms are unchanged.
|
||
func TestLOONG64_branch16Unsigned(t *testing.T) {
|
||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||
TEXT ·u(SB), NOSPLIT, $0
|
||
BLTU R4, done
|
||
BGEU R5, done
|
||
BLTU R6, R7, done
|
||
BGEU R8, R9, done
|
||
done:
|
||
RET
|
||
`)
|
||
code := assembleLOONG64Helper(t, fn)
|
||
wantWords(t, code,
|
||
0x68001080, // bltu r4, r0, +4
|
||
0x6C000CA0, // bgeu r5, r0, +3
|
||
0x680008C7, // bltu r6, r7, +2
|
||
0x6C000509, // bgeu r8, r9, +1
|
||
0x4C000020, // jirl r0, r1, 0
|
||
)
|
||
}
|
||
|
||
// TestLOONG64_bitFieldRange checks the BSTRINS/BSTRPICK bit-number
|
||
// validation, mirroring the toolchain's "illegal bit number" rule: 0..31 for
|
||
// the .w forms, 0..63 for the .d forms, and lsb <= msb.
|
||
func TestLOONG64_bitFieldRange(t *testing.T) {
|
||
cases := []string{
|
||
"BSTRINSW $32, R4, $0, R5",
|
||
"BSTRPICKW $31, R4, $32, R5",
|
||
"BSTRINSV $64, R4, $0, R5",
|
||
"BSTRPICKV $3, R4, $4, R5",
|
||
"BSTRINSW $-1, R4, $0, R5",
|
||
}
|
||
for _, src := range cases {
|
||
fn := firstTextLOONG64(t, "#include \"textflag.h\"\nTEXT ·e(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n")
|
||
if _, _, _, _, _, err := assembleLOONG64(fn); err == nil {
|
||
t.Errorf("%s: expected an error, got none", src)
|
||
}
|
||
}
|
||
}
|