Files
gasm-sdk/asm/arm64_ext_asm_test.go
T

426 lines
15 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"encoding/binary"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
)
// assembleArm64Words parses src, assembles it for arm64 and returns the
// first function's body as little-endian instruction words. Every statement
// must encode: a failure is the test's.
func assembleArm64Words(t *testing.T, src string) []uint32 {
t.Helper()
f, errs := parser.Parse("ext_arm64.s", src)
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
img, err := AssembleFileARM64(f)
if err != nil {
t.Fatalf("assemble: %v", err)
}
if len(img.Funcs) != 1 {
t.Fatalf("got %d functions, want 1", len(img.Funcs))
}
body := img.Code[img.Funcs[0].Offset:][:img.Funcs[0].Size]
if len(body)%4 != 0 {
t.Fatalf("body is %d bytes, not a whole number of instructions", len(body))
}
words := make([]uint32, 0, len(body)/4)
for i := 0; i < len(body); i += 4 {
words = append(words, binary.LittleEndian.Uint32(body[i:]))
}
return words
}
// assembleArm64SrcError parses and assembles src and returns the assembler's
// error text.
func assembleArm64SrcError(t *testing.T, src string) string {
t.Helper()
f, errs := parser.Parse("ext_arm64.s", src)
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
_, err := AssembleFileARM64(f)
if err == nil {
t.Fatal("assembled, want an error")
}
return err.Error()
}
const arm64ExtProbeHead = "#include \"textflag.h\"\nTEXT ·t(SB), NOSPLIT, $0\n"
// TestArm64AssembleExtensionGolden drives the wired layer through the full
// assembler: text in, instruction word out. Each want is the encoding the
// ARM Architecture Reference Manual's field layout gives for the statement:
// the fixed class word, the size field from the arrangement, and the
// register and immediate fields in the class's own places. The arch-level
// golden vectors in arch/arm64_ext_test.go pin the same bytes at the
// metadata layer; these pin the text-to-bytes path.
func TestArm64AssembleExtensionGolden(t *testing.T) {
tests := []struct {
stmt string
want uint32
}{
// Unpredicated three-vector: Zn, Zm, Zd, one shared arrangement.
{"ADD Z1.S, Z2.S, Z0.S", 0x04a20020},
{"ADD Z0.B, Z1.B, Z2.B", 0x04210002},
{"SUB Z31.D, Z30.D, Z29.D", 0x04fe07fd},
{"SQADD Z5.H, Z6.H, Z7.H", 0x046610a7},
{"UQADD Z8.S, Z9.S, Z10.S", 0x04a9150a},
{"SQSUB Z5.H, Z6.H, Z7.H", 0x046618a7},
{"UQSUB Z8.S, Z9.S, Z10.S", 0x04a91d0a},
{"MUL Z0.B, Z1.B, Z2.B", 0x04216002},
{"SMULH Z11.D, Z12.D, Z13.D", 0x04ec696d},
{"UMULH Z0.B, Z1.B, Z2.B", 0x04216c02},
// Governed destructive, merging: Zm, Pg/M, Zdn; the governing
// predicate is a 3-bit field, so P0-P7 alone.
{"ADD Z1.S, P0/M, Z0.S", 0x04800020},
{"SUBR Z1.S, P7/M, Z0.S", 0x04831c20},
{"MUL Z3.D, P2/M, Z5.D", 0x04d00865},
{"SUBR Z0.B, P5/M, Z31.B", 0x0403141f},
// Immediate classes: imm{, LSL #8}, Zdn. A bare multiple of 256
// derives the shift bit, the spelling the layer canonicalises.
{"ADD $255, Z0.S", 0x25a0dfe0},
{"ADD $65280, Z0.H", 0x2560ffe0},
{"ADD $255, LSL #8, Z0.S", 0x25a0ffe0},
{"ADD $(255<<8), Z0.S", 0x25a0ffe0},
{"MUL $-128, Z0.B", 0x2530d000},
}
for _, tt := range tests {
words := assembleArm64Words(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if len(words) != 2 {
t.Fatalf("%s: got %d words, want the statement and RET", tt.stmt, len(words))
}
if words[0] != tt.want {
t.Errorf("%s:\n got %08x\n want %08x", tt.stmt, words[0], tt.want)
}
if words[1] != 0xd65f03c0 {
t.Errorf("%s: RET encoded %08x", tt.stmt, words[1])
}
}
}
// TestArm64AssembleExtensionRefusals pins the diagnostics a pinned statement
// gets from the layer instead of a scalar path's register complaint.
func TestArm64AssembleExtensionRefusals(t *testing.T) {
tests := []struct {
stmt string
want string
}{
{"ADD Z0, Z1.S, Z2.S", "carries no arrangement suffix"},
{"ADD Z33.S, Z1.S, Z2.S", "outside Z0-Z31"},
{"ADD Z1.S, P0/Z, Z0.S", "/M"},
{"ADD Z1.S, P9/M, Z0.S", "outside P0-P7"},
{"ADD $300, Z0.S", "immediate 300"},
{"ADD $255<<8, Z0.S", "not an immediate the layer can read"},
{"ADD Z1.S, P0/M, R0", "wants a scalable vector register"},
{"ADD Z1.S, P0/B, Z0.S", "not an extended-layer operand"},
{"MUL $200, Z0.B", "outside the signed 8-bit range"},
{"ADD Z1.S, LSL #8, Z0.S", "LSL belongs straight after an immediate"},
}
for _, tt := range tests {
got := assembleArm64SrcError(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if !strings.Contains(got, tt.want) {
t.Errorf("%s: error %q does not name %q", tt.stmt, got, tt.want)
}
}
}
// TestArm64AssembleExtensionPredicateGolden drives the predicate family
// through the full assembler: corpus spellings in, corpus words out. The
// want words are the same arm64sveenc.s lines the arch-level golden test
// pins; these prove the text-to-bytes path parses the dot and slash
// qualifiers, the general registers and the counter spelling the statements
// write.
func TestArm64AssembleExtensionPredicateGolden(t *testing.T) {
tests := []struct {
stmt string
want uint32
}{
// The logical operations, .B alone, the governing predicate with the
// zeroing qualifier in its dot spelling.
{"PAND P4.B, P2.B, P1.Z, P14.B", 0x2504444e},
{"PANDS P4.B, P2.B, P1.Z, P14.B", 0x2544444e},
{"PBIC P4.B, P2.B, P1.Z, P14.B", 0x2504445e},
{"PEOR P4.B, P2.B, P1.Z, P14.B", 0x2504464e},
{"PNAND P4.B, P2.B, P1.Z, P14.B", 0x2584465e},
{"PORR P4.B, P2.B, P1.Z, P14.B", 0x2584444e},
// The slash qualifier spells the same operand the dot spelling does.
{"PAND P4.B, P2.B, P1/Z, P14.B", 0x2504444e},
// The select and the breaks.
{"PSEL P4.B, P2.B, P1, P14.B", 0x2504465e},
{"PBRKA P5.B, P9.Z, P2.B", 0x251064a2},
{"PBRKAS P5.B, P9.Z, P4.B", 0x255064a4},
{"PBRKN P4.B, P2.B, P1.Z, P4.B", 0x25184444},
{"PBRKPA P4.B, P2.B, P1.Z, P14.B", 0x2504c44e},
// The permutations carry the arrangement into the size field.
{"PTRN1 P5.D, P4.D, P2.D", 0x05e55082},
{"PUZP2 P5.D, P4.D, P2.D", 0x05e54c82},
{"PZIP1 P5.H, P4.H, P2.H", 0x05654082},
// The singles and the first-fault group.
{"PPFALSE P13.B", 0x2518e40d},
{"PPFIRST P5.B, P9, P5.B", 0x2558c125},
{"PPNEXT P5.D, P4, P5.D", 0x25d9c485},
{"PPTEST P14.B, P0", 0x2550c1c0},
{"PPUNPKHI P14.B, P0.H", 0x053141c0},
{"PRDFFR P13.B", 0x2519f00d},
{"PRDFFR P14.Z, P0.B", 0x2518f1c0},
{"PRDFFRS P14.Z, P0.B", 0x2558f1c0},
{"PWRFFR P13.B", 0x252891a0},
{"PREV P14.S, P13.S", 0x05b441cd},
{"SETFFR", 0x252c9000},
// The while compares: general registers in, a sized predicate out.
{"PWHILEGE R2, R10, P10.H", 0x2562114a},
{"PWHILELT R2, R10, P10.H", 0x2562154a},
{"PWHILELS R2, R10, P10.B", 0x25221d5a},
{"PWHILERW R2, R10, P10.H", 0x2562315a},
}
for _, tt := range tests {
words := assembleArm64Words(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if len(words) != 2 {
t.Fatalf("%s: got %d words, want the statement and RET", tt.stmt, len(words))
}
if words[0] != tt.want {
t.Errorf("%s:\n got %08x\n want %08x", tt.stmt, words[0], tt.want)
}
if words[1] != 0xd65f03c0 {
t.Errorf("%s: RET encoded %08x", tt.stmt, words[1])
}
}
}
// TestArm64AssembleExtensionPredicateRefusals pins the diagnostics the
// predicate statements get from the layer.
func TestArm64AssembleExtensionPredicateRefusals(t *testing.T) {
tests := []struct {
stmt string
want string
}{
{"PAND P4.B, P2.B, P1.M, P14.B", "zeroing qualifier"},
{"PAND P4.B, P2.B, P9.Z, P14.B", "outside P0-P7"},
{"PAND P8.B, P2.B, P1.Z, P14.B", "outside P0-P7"},
{"PAND P4.S, P2.B, P1.Z, P14.B", "want .B"},
{"PBRKN P4.B, P2.B, P1.Z, P9.B", "same register Pdm"},
{"PBRKAS P5.B, P9.M, P4.B", "zeroing qualifier"},
{"PSEL P4.B, P2.B, P1.Z, P14.B", "takes no qualifier"},
{"PTRN1 P5.D, P4.S, P2.D", "want .D"},
{"PPFALSE P13.S", "want .B"},
{"PPTRUE P6.S", "predicate-as-counter register"},
{"PPTRUE PN6.S", "outside PN8-PN15"},
{"PWHILELT R2, R31, P10.H", "outside R0-R30"},
{"PWHILELT R2, R10, P10.Q", "no size encoding"},
{"SETFFR P0.B", "takes 0 operands"},
}
for _, tt := range tests {
got := assembleArm64SrcError(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if !strings.Contains(got, tt.want) {
t.Errorf("%s: error %q does not name %q", tt.stmt, got, tt.want)
}
}
}
// TestArm64AssembleExtensionPredicateLeavesScalarsAlone pins the non-
// invasion promise across the new operand kinds: general-register
// statements whose mnemonics the layer also carries for predicates keep
// their scalar behaviour whenever no vector, predicate or counter operand
// appears.
func TestArm64AssembleExtensionPredicateLeavesScalarsAlone(t *testing.T) {
tests := []struct {
stmt string
want uint32
}{
{"ADD R0, R1, R2", 0x8b000022},
{"SUB R0, R1, R2", 0xcb000022},
}
for _, tt := range tests {
words := assembleArm64Words(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if len(words) != 2 {
t.Fatalf("%s: got %d words, want the statement and RET", tt.stmt, len(words))
}
if words[0] != tt.want {
t.Errorf("%s:\n got %08x\n want %08x", tt.stmt, words[0], tt.want)
}
}
}
// TestArm64AssembleExtensionPredicateLabelOffsets proves pass 1 and pass 2
// agree on a function mixing the predicate family with the vector family:
// PWHILELT (4 bytes) and SETFFR (4 bytes) shift the label by exactly the
// words pass 2 lays down.
func TestArm64AssembleExtensionPredicateLabelOffsets(t *testing.T) {
src := arm64ExtProbeHead + `
PWHILELT R2, R10, P10.H
loop:
SETFFR
B loop
PPFALSE P13.B
RET
`
words := assembleArm64Words(t, src)
want := []uint32{0x2562154a, 0x252c9000, 0x17ffffff, 0x2518e40d, 0xd65f03c0}
if len(words) != len(want) {
t.Fatalf("got %d words, want %d", len(words), len(want))
}
for i := range want {
if words[i] != want[i] {
t.Errorf("word %d: got %08x, want %08x", i, words[i], want[i])
}
}
}
// TestArm64AssembleExtensionLabelOffsets proves pass 1 and pass 2 agree on a
// function that mixes the layer with ordinary statements: the label after an
// SVE instruction lands on the 4 bytes the encoder laid down, and the branch
// back to it encodes the distance in words.
func TestArm64AssembleExtensionLabelOffsets(t *testing.T) {
src := arm64ExtProbeHead + `
ADD Z0.S, Z1.S, Z2.S
loop:
ADD $255, Z0.S
B loop
MUL $-128, Z0.B
RET
`
words := assembleArm64Words(t, src)
want := []uint32{0x04a10002, 0x25a0dfe0, 0x17ffffff, 0x2530d000, 0xd65f03c0}
if len(words) != len(want) {
t.Fatalf("got %d words, want %d", len(words), len(want))
}
for i := range want {
if words[i] != want[i] {
t.Errorf("word %d: got %08x, want %08x", i, words[i], want[i])
}
}
}
// TestArm64AssembleExtensionLeavesScalarsAlone pins the non-invasion
// promise: statements whose operands the scalar paths already read keep
// their exact encodings, scalar and NEON alike, with the layer wired in.
func TestArm64AssembleExtensionLeavesScalarsAlone(t *testing.T) {
tests := []struct {
stmt string
want uint32
}{
{"ADD R0, R1, R2", 0x8b000022},
{"ADD $255, R0", 0x9103fc00},
{"SUB R0, R1, R2", 0xcb000022},
}
for _, tt := range tests {
words := assembleArm64Words(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if len(words) != 2 {
t.Fatalf("%s: got %d words, want the statement and RET", tt.stmt, len(words))
}
if words[0] != tt.want {
t.Errorf("%s:\n got %08x\n want %08x", tt.stmt, words[0], tt.want)
}
}
}
// TestArm64AssembleExtensionStage2Golden drives the SVE2.1 Z-alias
// statements through the full assembler: corpus text in, corpus word out.
// The statements carry the spellings the layer reads: dot and slash
// qualifiers, bare predicates, bare vectors, general registers and the
// immediate-with-source alias.
func TestArm64AssembleExtensionStage2Golden(t *testing.T) {
tests := []struct {
stmt string
want uint32
}{
{"ZABS Z7.D, P4.M, Z13.D", 0x04d6b0ed},
{"ZABS Z7.D, P4.Z, Z13.D", 0x04c6b0ed},
{"ZADD Z15.B, Z0.B, P3.M, Z0.B", 0x04000de0},
{"ZADD Z7.D, Z23.D, Z13.D", 0x04e702ed},
{"ZAND Z15.B, Z0.B, P3.M, Z0.B", 0x041a0de0},
{"ZAND Z7.D, Z6.D, Z23.D", 0x042730d7},
{"ZBIC Z15.B, Z0.B, P3.M, Z0.B", 0x041b0de0},
{"ZBIC Z7.D, Z6.D, Z23.D", 0x04e730d7},
{"ZCLS Z7.D, P4.M, Z13.D", 0x04d8b0ed},
{"ZCLS Z7.D, P4.Z, Z13.D", 0x04c8b0ed},
{"ZCLZ Z7.D, P4.M, Z13.D", 0x04d9b0ed},
{"ZCLZ Z7.D, P4.Z, Z13.D", 0x04c9b0ed},
{"ZCOMPACT Z7.D, P4, Z13.D", 0x05e190ed},
{"ZCOMPACT Z7.D, P4, Z13.D", 0x05e190ed},
{"ZEOR Z15.B, Z0.B, P3.M, Z0.B", 0x04190de0},
{"ZEOR Z7.D, Z6.D, Z23.D", 0x04a730d7},
{"ZEXPAND Z7.D, P4, Z13.D", 0x05f190ed},
{"ZMOVPRFX Z7.D, P4.Z, Z21.D", 0x04d030f5},
{"ZMOVPRFX Z11, Z6", 0x0420bd66},
{"ZNEG Z7.D, P4.M, Z13.D", 0x04d7b0ed},
{"ZNEG Z7.D, P4.Z, Z13.D", 0x04c7b0ed},
{"ZNOT Z7.D, P4.M, Z13.D", 0x04deb0ed},
{"ZNOT Z7.D, P4.Z, Z13.D", 0x04ceb0ed},
{"ZORR Z15.B, Z0.B, P3.M, Z0.B", 0x04180de0},
{"ZORR Z7.D, Z6.D, Z23.D", 0x046730d7},
{"ZREV Z1.S, Z26.S", 0x05b8383a},
{"ZREVB Z7.D, P4.M, Z13.D", 0x05e490ed},
{"ZREVB Z7.D, P4.Z, Z13.D", 0x05e4b0ed},
{"ZREVD Z13.Q, P1.M, Z22.Q", 0x052e85b6},
{"ZREVD Z13.Q, P1.Z, Z22.Q", 0x052ea5b6},
{"ZREVH Z7.D, P4.M, Z13.D", 0x05e590ed},
{"ZREVH Z7.D, P4.Z, Z13.D", 0x05e5b0ed},
{"ZREVW Z13.D, P1.M, Z22.D", 0x05e685b6},
{"ZREVW Z13.D, P1.Z, Z22.D", 0x05e6a5b6},
{"ZSEL Z23.B, Z21.B, P14, Z2.B", 0x0537faa2},
{"ZSQABS Z7.D, P4.M, Z13.D", 0x44c8b0ed},
{"ZSQABS Z7.D, P4.Z, Z13.D", 0x44cab0ed},
{"ZSQNEG Z7.D, P4.M, Z13.D", 0x44c9b0ed},
{"ZSQNEG Z7.D, P4.Z, Z13.D", 0x44cbb0ed},
{"ZSUB Z15.B, Z0.B, P3.M, Z0.B", 0x04010de0},
{"ZSUB Z7.D, Z23.D, Z13.D", 0x04e706ed},
{"ZSUBR Z15.B, Z0.B, P3.M, Z0.B", 0x04030de0},
{"ZSUNPKHI Z15.B, Z0.H", 0x057139e0},
{"ZSUNPKLO Z15.B, Z0.H", 0x057039e0},
{"ZSXTB Z7.D, P4.M, Z13.D", 0x04d0b0ed},
{"ZSXTB Z7.D, P4.Z, Z13.D", 0x04c0b0ed},
{"ZSXTH Z7.D, P4.M, Z13.D", 0x04d2b0ed},
{"ZSXTH Z7.D, P4.Z, Z13.D", 0x04c2b0ed},
{"ZSXTW Z13.D, P1.M, Z22.D", 0x04d4a5b6},
{"ZSXTW Z13.D, P1.Z, Z22.D", 0x04c4a5b6},
{"ZTBX Z7.D, Z23.D, Z13.D", 0x05e72eed},
{"ZTBXQ Z7.D, Z23.D, Z13.D", 0x05e736ed},
{"ZTRN1 Z7.D, Z23.D, Z13.D", 0x05e772ed},
{"ZTRN1 Z7.Q, Z6.Q, Z23.Q", 0x05a718d7},
{"ZTRN2 Z7.D, Z23.D, Z13.D", 0x05e776ed},
{"ZTRN2 Z7.Q, Z6.Q, Z23.Q", 0x05a71cd7},
{"ZUUNPKHI Z15.B, Z0.H", 0x057339e0},
{"ZUUNPKLO Z15.B, Z0.H", 0x057239e0},
{"ZUXTB Z7.D, P4.M, Z13.D", 0x04d1b0ed},
{"ZUXTB Z7.D, P4.Z, Z13.D", 0x04c1b0ed},
{"ZUXTH Z7.D, P4.M, Z13.D", 0x04d3b0ed},
{"ZUXTH Z7.D, P4.Z, Z13.D", 0x04c3b0ed},
{"ZUXTW Z13.D, P1.M, Z22.D", 0x04d5a5b6},
{"ZUXTW Z13.D, P1.Z, Z22.D", 0x04c5a5b6},
{"ZUZP1 Z7.D, Z23.D, Z13.D", 0x05e76aed},
{"ZUZP1 Z7.Q, Z6.Q, Z23.Q", 0x05a708d7},
{"ZUZP2 Z7.D, Z23.D, Z13.D", 0x05e76eed},
{"ZUZP2 Z7.Q, Z6.Q, Z23.Q", 0x05a70cd7},
{"ZUZPQ1 Z7.D, Z23.D, Z13.D", 0x44c7eaed},
{"ZUZPQ2 Z7.D, Z23.D, Z13.D", 0x44c7eeed},
{"ZZIP1 Z7.D, Z23.D, Z13.D", 0x05e762ed},
{"ZZIP1 Z7.Q, Z6.Q, Z23.Q", 0x05a700d7},
{"ZZIP2 Z7.D, Z23.D, Z13.D", 0x05e766ed},
{"ZZIP2 Z7.Q, Z6.Q, Z23.Q", 0x05a704d7},
{"ZZIPQ1 Z7.D, Z23.D, Z13.D", 0x44c7e2ed},
{"ZZIPQ2 Z7.D, Z23.D, Z13.D", 0x44c7e6ed},
{"ZDUP R2, Z10.D", 0x05e0384a},
{"ZINSR R2, Z10.D", 0x05e4384a},
{"ZADD $6, Z7.D, Z7.D", 0x25e0c0c7},
{"ZSUB $6, Z7.D, Z7.D", 0x25e1c0c7},
{"ZSUBR $6, Z7.D, Z7.D", 0x25e3c0c7},
}
for _, tt := range tests {
words := assembleArm64Words(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if len(words) != 2 {
t.Fatalf("%s: got %d words, want the statement and RET", tt.stmt, len(words))
}
if words[0] != tt.want {
t.Errorf("%s:\n got %08x\n want %08x", tt.stmt, words[0], tt.want)
}
}
}