Files
gasm-sdk/asm/loong64_encode_test.go
T
2026-09-20 06:44:51 +02:00

592 lines
16 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"bytes"
"encoding/binary"
"testing"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// firstTextLOONG64 parses assembly source and returns the first TEXT body.
func firstTextLOONG64(t *testing.T, src string) *ast.Text {
t.Helper()
f, errs := parser.Parse("f_loong64.s", src)
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
for _, d := range f.Decls {
if fn, ok := d.(*ast.Text); ok {
return fn
}
}
t.Fatal("no TEXT found")
return nil
}
// assembleLOONG64Helper assembles one TEXT function and returns its bytes.
func assembleLOONG64Helper(t *testing.T, fn *ast.Text) []byte {
t.Helper()
code, _, _, _, _, err := assembleLOONG64(fn)
if err != nil {
t.Fatalf("assemble: %v", err)
}
return code
}
// wantWords checks that code matches the expected little-endian words.
func wantWords(t *testing.T, code []byte, want ...uint32) {
t.Helper()
got := make([]uint32, 0, len(code)/4)
for i := 0; i+4 <= len(code); i += 4 {
got = append(got, binary.LittleEndian.Uint32(code[i:]))
}
if len(got) != len(want) {
t.Fatalf("word count = %d, want %d\ncode: % x", len(got), len(want), code)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
}
}
}
func TestLOONG64_add(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·add(SB), NOSPLIT, $0-24
MOVV a+0(FP), R4
MOVV b+8(FP), R5
ADDV R5, R4, R4
MOVV R4, ret+16(FP)
RET
`)
code := assembleLOONG64Helper(t, fn)
// 5 instructions: two ld.d, add.d, st.d, jirl r0, r1, 0.
wantWords(t, code,
0x28C02064, // ld.d r4, 8(r3)
0x28C04065, // ld.d r5, 16(r3)
0x00109484, // add.d r4, r4, r5
0x29C06064, // st.d r4, 24(r3)
0x4C000020, // jirl r0, r1, 0
)
}
func TestLOONG64_arithmetic(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·arith(SB), NOSPLIT, $0
ADDV R4, R5, R6
SUBV R7, R8, R9
MULV R10, R11, R12
DIVV R13, R14, R15
AND R16, R17, R18
OR R18, R19, R20
XOR R20, R21, R2
SLLV R2, R23, R24
SRLV R24, R25, R26
SRAV R26, R27, R28
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x001090A6, // add.d r6, r5, r4
0x00119D09, // sub.d r9, r8, r7
0x001DA96C, // mul.d r12, r11, r10
0x002235CF, // div.d r15, r14, r13
0x0014C232, // and r18, r17, r16
0x00154A74, // or r20, r19, r18
0x0015D2A2, // xor r2, r21, r20
0x00188AF8, // sll.d r24, r23, r2
0x0019633A, // srl.d r26, r25, r24
0x0019EB7C, // sra.d r28, r27, r26
0x4C000020, // jirl r0, r1, 0
)
}
func TestLOONG64_immediates(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·imm(SB), NOSPLIT, $0
ADDV $42, R4, R5
ADDV $-8, R6
AND $0xff, R7, R8
OR $1, R9, R10
SGT $100, R13, R14
SLLV $4, R15, R16
MOVV $0x12345, R17
MOVV $0, R18
MOVW $0, R19
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x02C0A885, // addi.d r5, r4, 42
0x02FFE0C6, // addi.d r6, r6, -8
0x0343FCE8, // andi r8, r7, 0xff
0x0380052A, // ori r10, r9, 1
0x020191AE, // slti r14, r13, 100
0x004111F0, // slli.d r16, r15, 4
0x14000251, // lu12i.w r17, 0x12
0x038D1631, // ori r17, r17, 0x345
0x00150012, // or r18, r0, r0
0x00170013, // sll.w r19, r0, r0
0x4C000020, // jirl r0, r1, 0
)
}
func TestLOONG64_loadStore(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·mem(SB), NOSPLIT, $0
MOVV (R4), R5
MOVV R5, (R6)
MOVW 8(R7), R8
MOVB R9, -4(R10)
MOVV (R11)(R12), R13
MOVV R14, (R15)(R16)
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x28C00085, // ld.d r5, 0(r4)
0x29C000C5, // st.d r5, 0(r6)
0x288020E8, // ld.w r8, 8(r7)
0x293FF149, // st.b r9, -4(r10)
0x380C316D, // ldx.d r13, r11, r12
0x381C41EE, // stx.d r14, r15, r16
0x4C000020, // jirl r0, r1, 0
)
}
func TestLOONG64_branches(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·br(SB), NOSPLIT, $0
BEQ R4, R5, done
BNE R6, R7, skip
BLT R8, R9, done
BGE R10, R11, done
BLTU R12, R13, done
BGEU R14, R15, done
skip:
JMP done
done:
RET
`)
code := assembleLOONG64Helper(t, fn)
// skip is at 0x18 (6 words), done at 0x1c.
wantWords(t, code,
0x58001C85, // beq r5, r4, +7
0x5C0018C7, // bne r7, r6, +6
0x60001509, // blt r9, r8, +5
0x6400114B, // bge r11, r10, +4
0x68000D8D, // bltu r13, r12, +3
0x6C0009CF, // bgeu r15, r14, +2
0x50000400, // b done (+1, chain-folded through skip)
0x4C000020, // jirl r0, r1, 0
)
}
func TestLOONG64_frame(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·f(SB), NOSPLIT, $32-8
MOVV R4, R5
MOVV arg+0(FP), R6
MOVV R7, local-8(SP)
MOVV local-8(SP), R8
MOVV R9, ret+0(FP)
RET
`)
code := assembleLOONG64Helper(t, fn)
// autosize = align8(32+8) = 40; prologue stores LR at -40(SP),
// opens the frame, stores LR again at 0(SP). The function is a leaf
// (no calls), so the epilogue skips the LR restore. FP args are at
// autosize+8; SP locals at autosize+offset.
wantWords(t, code,
0x29FF6061, // st.d r1, -40(r3)
0x02FF6063, // addi.d r3, r3, -40
0x29C00061, // st.d r1, 0(r3)
0x00150085, // or r5, r4, r0
0x28C0C066, // ld.d r6, 48(r3) arg+0(FP) → 0+40+8
0x29C08067, // st.d r7, 32(r3) local-8(SP) → 40-8
0x28C08068, // ld.d r8, 32(r3)
0x29C0C069, // st.d r9, 48(r3) ret+0(FP) → 0+40+8
0x02C0A063, // addi.d r3, r3, 40
0x4C000020, // jirl r0, r1, 0
)
}
func TestLOONG64_jumpChain(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·jc(SB), NOSPLIT, $0
JMP a
a:
JMP b
b:
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x50000800, // b +2 (a, chain-folded to b)
0x50000400, // b +1 (b)
0x4C000020, // jirl r0, r1, 0
)
}
func TestLOONG64_dconClasses(t *testing.T) {
cases := []struct {
v int64
word int // expected word count
}{
{0x123456789, 3}, // lu12i.w + ori + lu32i.d
{-1, 2}, // addi.d + lu52i.d (the MOV path handles -1 earlier)
{0x1000000000000, 2}, // addi.w + lu32i.d
{0x123456789abcdef0, 4}, // full sequence
{0xFFFFFFFFF, 2}, // lu12i.w + ori
{0x1234567800000000, 3}, // addi.w + lu32i.d + lu52i.d
}
for _, c := range cases {
if n := len(l64DconMovWords(0, c.v)); n != c.word {
t.Errorf("0x%x: %d words, want %d", c.v, n, c.word)
}
}
}
func TestLOONG64_regNames(t *testing.T) {
cases := map[string]int{
"R0": 0, "R31": 31, "F0": 0, "F31": 31, "FCC0": 0, "FCC7": 7,
"FCSR0": 0, "FCSR3": 3, "ZERO": 0, "RA": 1, "SP": 3, "g": 22, "G": 22,
"R32": -1, "FCC8": -1, "X0": -1, "R": -1, "TMP": 30, "CTXT": 29,
}
for name, want := range cases {
if got := loong64RegNum(name); got != want {
t.Errorf("loong64RegNum(%q) = %d, want %d", name, got, want)
}
}
// The X/V spellings name the LSX/LASX vector banks, a register class of
// their own: the oracle (GOARCH=loong64 go tool asm) rejects `BEQZ X0`
// with "unrecognized instruction" while assembling `VADDV V0, V1, V2`
// and `XVADDV X0, X1, X2`, so loong64RegNum stays strict and the vector
// operands resolve through loong64VecRegNum only.
vecCases := map[string]int{
"V0": 0, "V31": 31, "X0": 0, "X31": 31,
"R4": -1, "F0": -1, "FCC0": -1, "V32": -1, "X32": -1, "V": -1, "X": -1,
}
for name, want := range vecCases {
if got := loong64VecRegNum(name); got != want {
t.Errorf("loong64VecRegNum(%q) = %d, want %d", name, got, want)
}
}
}
func TestLOONG64_bytesEqualGroundTruth(t *testing.T) {
// A spot-check that assembleLOONG64 emits the same bytes the Go
// toolchain does for a small kernel (the full comparison lives in
// verify's TestGroundTruthLOONG64).
src := `#include "textflag.h"
TEXT ·k(SB), NOSPLIT, $0-0
ADDV R4, R5, R6
MOVV $0x100000, R7
BEQ R6, R7, done
JMP done
done:
RET
`
fn := firstTextLOONG64(t, src)
code := assembleLOONG64Helper(t, fn)
want := []byte{
0xa6, 0x90, 0x10, 0x00, // add.d r6, r5, r4
0x07, 0x20, 0x00, 0x14, // lu12i.w r7, 0x100
0xc7, 0x08, 0x00, 0x58, // beq r7, r6, +2 (done)
0x00, 0x04, 0x00, 0x50, // b +1 (done)
0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0
}
if !bytes.Equal(code, want) {
t.Errorf("code = % x\nwant % x", code, want)
}
}
// TestLOONG64IndirectBranch pins the indirect branch encodings: JMP (Rj) and
// JAL (Rj) lower to jirl, and the raw JIRL spelling encodes the written
// offset (the Go loong64 assembler deletes raw JIRL instructions entirely,
// so this form is a gasm-only superset with faithful semantics).
func TestLOONG64IndirectBranch(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·f(SB), NOSPLIT, $0-0
JMP (R4)
JIRL R0, R4, 8
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x4C000080, // jirl r0, r4, 0
0x4C002080, // jirl r0, r4, 8
0x4C000020, // jirl r0, r1, 0 (RET)
)
// JAL (R5) links, so the toolchain gives the function its autosize-8
// prologue and epilogue around the call and the closing RET.
fn = firstTextLOONG64(t, `#include "textflag.h"
TEXT ·f(SB), NOSPLIT, $0-0
JAL (R5)
RET
`)
code = assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x29FFE061, // st.d r1, -8(r3) (prologue saves RA below the new SP)
0x02FFE063, // addi.d r3, r3, -8 (prologue opens the frame)
0x29C00061, // st.d r1, 0(r3) (prologue saves RA at SP)
0x4C0000A1, // jirl r1, r5, 0
0x28C00061, // ld.d r1, 0(r3) (epilogue restores RA)
0x02C02063, // addi.d r3, r3, 8
0x4C000020, // jirl r0, r1, 0 (RET)
)
}
// TestLOONG64_vector pins the LSX/LASX slice against words read off
// GOARCH=loong64 go tool asm (cross-checked against the toolchain's own
// loong64enc1.s): the three-register forms, the immediate forms with their
// biases, the vector-to-condition forms, lane popcount, the FP conversion,
// FSEL and the VMOVQ move family.
func TestLOONG64_vector(t *testing.T) {
t.Run("three-register and immediate forms", func(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·v(SB), NOSPLIT, $0
VADDV V1, V2, V3
VADDW V1, V2, V3
VADDV V2, V1
VANDV V1, V2
VXORV V1, V2, V3
VSEQB V1, V2, V3
VSEQV V1, V2, V3
VSRAB V1, V2, V3
VROTRW V1, V2, V3
VANDB $0, V2, V3
VANDB $255, V2
VSEQB $3, V2, V3
VSEQV $15, V2, V3
VSEQV $-15, V2, V3
VSRAB $7, V1, V2
VROTRW $16, V1, V2
VPCNTV V1, V2
XVADDV X1, X2, X3
XVXORV X1, X2, X3
XVSEQB X1, X2, X3
XVPCNTV X1, X2
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x700B8443, // vadd.v v3, v2, v1
0x700B0443, // vadd.w
0x700B8821, // vadd.v v1, v1, v2 (two-operand form)
0x71260442, // vand.v v2, v2, v1
0x71270443, // vxor.v
0x70000443, // vseq.b
0x70018443, // vseq.d
0x70EC0443, // vsra.b
0x70EF0443, // vrotr.w
0x73D00043, // vandi.b v3, v2, 0
0x73D3FC42, // vandi.b v2, v2, 255 (two-operand form)
0x72800C43, // vseqi.b v3, v2, 3
0x7281BC43, // vseqi.d v3, v2, 15
0x7281C443, // vseqi.d v3, v2, -15 (7-bit two's complement)
0x73343C22, // vsrai.b v2, v1, 7 (encoded as 7+8)
0x72A0C022, // vrotri.w v2, v1, 16
0x729C2C22, // vpcnt.d v2, v1
0x740B8443, // xvadd.d x3, x2, x1
0x75270443, // xvxor.d
0x74000443, // xvseq.b
0x769C2C22, // xvpcnt.d x2, x1
0x4C000020,
)
})
t.Run("vector-to-condition", func(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·v(SB), NOSPLIT, $0
VSETNEV V1, FCC0
VSETANYEQB V1, FCC0
VSETANYEQV V2, FCC0
VSETALLNEV V0, FCC0
XVSETNEV X1, FCC0
XVSETALLNEV X1, FCC0
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x729C9C20, // vsetnez.d fcc0, v1
0x729CA020, // vsetanyeqz.b
0x729CAC40, // vsetanyeqz.d
0x729CBC00, // vsetallnez.d
0x769C9C20, // xvsetnez.d
0x769CBC20, // xvsetallnez.d
0x4C000020,
)
})
t.Run("FP convert and FSEL", func(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·v(SB), NOSPLIT, $0
FFINTDV F0, F1
FSEL FCC0, F3, F4, F3
FSEL FCC1, F1, F2
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x011D2801, // ffint.d.v f1, f0
0x0D000C83, // fsel f3, f4, f3, fcc0
0x0D008442, // fsel f2, f2, f1, fcc1
0x4C000020,
)
})
t.Run("VMOVQ move family", func(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·v(SB), NOSPLIT, $0
VMOVQ V1, V9
VMOVQ (R4), V2
VMOVQ 16(R4), V2
VMOVQ V0, (R4)
VMOVQ V0, 32(R4)
VMOVQ (R4)(R7), V3
VMOVQ V3, (R4)(R7)
VMOVQ R6, V0.B16
VMOVQ R6, V12.W4
VMOVQ (R4), V4.W4
XVMOVQ X3, X7
XVMOVQ (R4), X2
XVMOVQ X0, (R4)
XVMOVQ (R4)(R7), X4
XVMOVQ X0, (R4)(R7)
XVMOVQ R6, X0.B32
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x732D0029, // vori.b v9, v1, 0 (register move)
0x2C000082, // vld v2, r4, 0
0x2C004082, // vld v2, r4, 16
0x2C400080, // vst v0, r4, 0
0x2C408080, // vst v0, r4, 32
0x38401C83, // vldx v3, r4, r7
0x38441C83, // vstx v3, r4, r7
0x729F00C0, // vreplgr2vr.b v0, r6
0x729F08CC, // vreplgr2vr.w v12, r6
0x30200084, // vldrepl.w v4, r4, 0
0x772D0067, // xvori.b x7, x3, 0
0x2C800082, // xvld x2, r4, 0
0x2CC00080, // xvst x0, r4, 0
0x38481C84, // xvldx x4, r4, r7
0x384C1C80, // xvstx x0, r4, r7
0x769F00C0, // xvreplgr2vr.b x0, r6
0x4C000020,
)
})
t.Run("element extract and insert", func(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·v(SB), NOSPLIT, $0
VMOVQ V0.V[0], R10
VMOVQ V6.V[1], R8
VMOVQ R9, V1.V[0]
XVMOVQ X0.V[0], R10
XVMOVQ X5.W[7], R7
XVMOVQ R4, X7.V[3]
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x72EFF00A, // vpickve2gr.d r10, v0, 0
0x72EFF4C8, // vpickve2gr.d r8, v6, 1
0x72EBF121, // vinsgr2vr.d v1, r9, 0
0x76EFE00A, // xvpickve2gr.d r10, x0, 0
0x76EFDCA7, // xvpickve2gr.w r7, x5, 7
0x76EBEC87, // xvinsgr2vr.d x7, r4, 3
0x4C000020,
)
})
}
// TestLOONG64_vectorErrors pins the register-class and range diagnostics of
// the vector slice; each shape is rejected by the oracle as well
// (GOARCH=loong64 go tool asm).
func TestLOONG64_vectorErrors(t *testing.T) {
cases := []string{
// Integer registers in vector positions.
`TEXT ·e(SB), NOSPLIT, $0
VADDV R4, R5, R6
RET
`,
// Crossed banks: LSX spellings take V, LASX spellings X.
`TEXT ·e(SB), NOSPLIT, $0
VADDV X1, X2, X3
RET
`,
`TEXT ·e(SB), NOSPLIT, $0
XVADDV V1, V2, V3
RET
`,
// The LASX bank has no .b/.h element forms.
`TEXT ·e(SB), NOSPLIT, $0
XVMOVQ R4, X2.B[0]
RET
`,
// Immediate ranges.
`TEXT ·e(SB), NOSPLIT, $0
VANDB $256, V2
RET
`,
`TEXT ·e(SB), NOSPLIT, $0
VSEQB $16, V2, V3
RET
`,
`TEXT ·e(SB), NOSPLIT, $0
VROTRW $32, V1, V2
RET
`,
// VSET* wants an FCC flag, not a vector register.
`TEXT ·e(SB), NOSPLIT, $0
VSETNEV V1, V2
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_dbarAtomics pins the _dbar (acquire/release) AMO variants.
// The oracle words come from GOARCH=loong64 go tool objdump of kernels
// assembled with go tool asm, and match the toolchain's loong64enc1.s.
func TestLOONG64_dbarAtomics(t *testing.T) {
fn := firstTextLOONG64(t, `#include "textflag.h"
TEXT ·atoms(SB), NOSPLIT, $0
AMADDDBW R14, (R13), R12
AMADDDBV R14, (R13), R12
AMANDDBW R5, (R4), R6
AMANDDBV R5, (R4), R6
AMORDBW R5, (R4), R0
AMORDBV R5, (R4), R6
AMSWAPDBW R5, (R4), R6
AMCASDBV R6, (R4), R5
RET
`)
code := assembleLOONG64Helper(t, fn)
wantWords(t, code,
0x386A39AC, // amadd_db.w r12, r13, r14
0x386AB9AC, // amadd_db.d
0x386B1486, // amand_db.w r6, r4, r5
0x386B9486, // amand_db.d
0x386C1480, // amor_db.w r0, r4, r5
0x386C9486, // amor_db.d
0x38691486, // amswap_db.w
0x385B9885, // amcas_db.w
0x4C000020,
)
}