// Copyright (c) 2026 Petr Balvín (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. 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 forms. func TestLOONG64_movImmToFp(t *testing.T) { fn := firstTextLOONG64(t, `#include "textflag.h" TEXT ·fpmov(SB), NOSPLIT, $0 MOVV $0x1, F0 MOVW $0x2, F4 RET `) code := assembleLOONG64Helper(t, fn) want := []byte{ 0x00, 0x04, 0x80, 0x03, // ori f0, r0, 1 0x04, 0x08, 0x80, 0x03, // ori f4, r0, 2 0x20, 0x00, 0x00, 0x4c, // jirl r0, r1, 0 } if !bytes.Equal(code, want) { t.Errorf("code = % x\nwant % x", code, want) } }