Files
gasm-sdk/asm/extension_amd64_test.go
T
2026-10-07 19:49:15 +02:00

270 lines
9.7 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"encoding/hex"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
)
// TestAmd64ExtensionRegistry checks the mnemonic lookup for the amd64 layer:
// one mnemonic across several vector lengths or W bits resolves to every
// entry, the lookup is case-insensitive, and the counts match the registered
// families.
func TestAmd64ExtensionRegistry(t *testing.T) {
for _, tt := range []struct {
mnem string
forms int
}{
{"VCVTNE2PS2BF16", 3},
{"VCVTNEPS2BF16", 3},
{"VDPBF16PS", 3},
{"VP2INTERSECTD", 3},
{"VP2INTERSECTQ", 3},
{"VMOVSH", 3},
{"VMOVW", 4},
{"VADDSH", 1},
{"VSQRTSH", 1},
{"VCOMISH", 1},
{"VCVTSH2SS", 1},
{"VCVTSI2SH", 2},
{"VCVTSH2SI", 2},
{"VADDPH", 3},
{"VSQRTPH", 3},
{"VSCALEFSH", 1},
{"VGETEXPSH", 1},
{"VCMPSH", 1},
{"VGETMANTSH", 1},
{"VREDUCESH", 1},
{"VRNDSCALESH", 1},
{"VCVTPH2W", 3},
{"VCVTPH2UW", 3},
{"VCVTW2PH", 3},
{"VCVTUW2PH", 3},
{"VCVTPH2DQ", 3},
{"VCVTPH2UDQ", 3},
{"VCVTDQ2PH", 3},
{"VCVTUDQ2PH", 3},
{"VCVTPH2QQ", 3},
{"VCVTPH2UQQ", 3},
{"VCVTQQ2PH", 3},
{"VCVTUQQ2PH", 3},
{"VCVTPH2PD", 3},
{"VCVTPD2PH", 3},
{"VRNDSCALEPH", 3},
{"VREDUCEPH", 3},
{"VGETMANTPH", 3},
{"VFMADD132PH", 3},
{"VFMADD213PH", 3},
{"VFMADD231PH", 3},
{"VFMSUB132PH", 3},
{"VFMSUB213PH", 3},
{"VFMSUB231PH", 3},
{"VFMADDSUB132PH", 3},
{"VFMADDSUB213PH", 3},
{"VFMADDSUB231PH", 3},
{"VFMSUBADD132PH", 3},
{"VFMSUBADD213PH", 3},
{"VFMSUBADD231PH", 3},
{"VFMADD132SH", 1},
{"VFMADD213SH", 1},
{"VFMADD231SH", 1},
{"VFMSUB132SH", 1},
{"VFMSUB213SH", 1},
{"VFMSUB231SH", 1},
{"VFMULCPH", 3},
{"VFCMULCPH", 3},
{"VFMULCSH", 1},
{"VFCMULCSH", 1},
{"VFMADDCPH", 3},
{"VFCMADDCPH", 3},
{"VFMADDCSH", 1},
{"VFCMADDCSH", 1},
{"VMINMAXPH", 3},
{"VMINMAXSH", 1},
{"VPDPWSUD", 2},
{"VPDPWSUDS", 2},
{"VPDPWUSD", 2},
{"VPDPWUSDS", 2},
} {
cands, ok := LookupExtension(arch.AMD64, tt.mnem)
if !ok {
t.Fatalf("LookupExtension(AMD64, %s) found nothing", tt.mnem)
}
if len(cands) != tt.forms {
t.Errorf("%s registers %d forms, want %d", tt.mnem, len(cands), tt.forms)
}
lower, ok := LookupExtension(arch.AMD64, strings.ToLower(tt.mnem))
if !ok || len(lower) != tt.forms {
t.Errorf("the %s lookup is not case-insensitive", tt.mnem)
}
}
if got := arch.Extensions(arch.AMD64); len(got) != 191 {
t.Errorf("the amd64 layer registers %d instructions, want 191", len(got))
}
if _, ok := LookupExtension(arch.AMD64, "NOSUCHINSTR"); ok {
t.Error("a non-extended mnemonic resolved")
}
// VPOPCNTD and VPOPCNTQ are toolchain instructions today: they stay in
// the generated table and out of the extension layer.
if _, ok := LookupExtension(arch.AMD64, "VPOPCNTD"); ok {
t.Error("VPOPCNTD is an extension, want it in the generated table alone")
}
}
// TestAmd64ExtensionAboveGeneratedTable pins the layering twice over: no
// registered mnemonic sits in the generated amd64 table, and the encoder
// mirror asm.Encodable answers false for every one of them, so the layer
// stays out of the main encoders by test and not by promise.
func TestAmd64ExtensionAboveGeneratedTable(t *testing.T) {
for _, mnem := range ExtensionNames(arch.AMD64) {
if _, found := arch.ForArch(arch.AMD64).Lookup(mnem); found {
t.Errorf("%s leaked into the generated amd64 table", mnem)
}
if Encodable(mnem) {
t.Errorf("%s is encodable through the main encoder, the layer is not sealed", mnem)
}
}
}
// TestEncodeExtensionAmd64 encodes through the registry and pins the same
// golden words the arch table tests pin, proving the registry resolves to the
// right encoding.
func TestEncodeExtensionAmd64(t *testing.T) {
for _, tt := range []struct {
name string
mnem string
ops []arch.ExtOperand
want string
}{
{"bf16 convert", "VCVTNE2PS2BF16",
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtZmm(6)},
"62f2574872f4"},
{"bf16 narrow convert", "VCVTNEPS2BF16",
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtYmm(6)},
"62f27e4872f5"},
{"dot product", "VDPBF16PS",
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtXmm(4), arch.ExtXmm(6)},
"62f2560852f4"},
{"intersect into a mask", "VP2INTERSECTD",
[]arch.ExtOperand{arch.ExtYmm(2), arch.ExtYmm(1), arch.ExtMask(2)},
"62f26f2868d1"},
{"scalar fp16 add, high registers", "VADDSH",
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtXmm(30)},
"6205160058f4"},
{"scalar compare", "VCOMISH",
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtXmm(30)},
"62057c082ff5"},
{"integer into a scalar fp16", "VCVTSI2SH",
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtGpr64(12), arch.ExtXmm(30)},
"624596002af4"},
{"scalar fp16 into an integer", "VCVTSH2SI",
[]arch.ExtOperand{arch.ExtXmm(30), arch.ExtGpr32(2)},
"62957e082dd6"},
{"word move into an xmm", "VMOVW",
[]arch.ExtOperand{arch.ExtGpr64(12), arch.ExtXmm(30)},
"62457d086ef4"},
{"scalar compare into a mask", "VCMPSH",
[]arch.ExtOperand{arch.ExtImmediate(0x7b), arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtMask(5)},
"62931600c2ec7b"},
{"mantissa extract with a control byte", "VGETMANTSH",
[]arch.ExtOperand{arch.ExtImmediate(0x0b), arch.ExtXmm(29), arch.ExtXmm(28), arch.ExtXmm(30)},
"6203140027f40b"},
{"packed fp16 add under embedded rounding", "VADDPH",
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtRounded(arch.ExtZmm(6), arch.ExtRoundTruncate)},
"62f5547858f4"},
{"scalar fp16 minimum with exceptions suppressed", "VMINSH",
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtXmm(4), arch.ExtRounded(arch.ExtXmm(6), arch.ExtRoundSAE)},
"62f556185df4"},
{"fp16 to signed words", "VCVTPH2W",
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(6)},
"62f57d487df5"},
{"dwords to fp16 over a broadcast source", "VCVTDQ2PH",
[]arch.ExtOperand{arch.ExtBroadcast(9, 0), arch.ExtYmm(30)},
"62457c585b31"},
{"fp16 to signed qwords, rounded", "VCVTPH2QQ",
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtRounded(arch.ExtZmm(6), arch.ExtRoundTruncate)},
"62f57d787bf5"},
{"fp16 scalar out of memory into an integer", "VCVTSH2SI",
[]arch.ExtOperand{arch.ExtMemory(9, 0), arch.ExtGpr64(12)},
"6255fe082d21"},
{"fp16 to double-precision, widened", "VCVTPH2PD",
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtZmm(6)},
"62f57c485af5"},
{"packed fp16 rounding to fraction bits", "VRNDSCALEPH",
[]arch.ExtOperand{arch.ExtImmediate(0x7b), arch.ExtZmm(5), arch.ExtZmm(6)},
"62f37c4808f57b"},
{"packed fused multiply-add, high registers", "VFMADD132PH",
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtZmm(28), arch.ExtZmm(30)},
"6206154098f4"},
{"fused multiply-add out of a broadcast source", "VFMADD231PH",
[]arch.ExtOperand{arch.ExtYmm(5), arch.ExtBroadcast(1, 0), arch.ExtYmm(6)},
"62f65538b831"},
{"scalar multiply-subtract under embedded rounding", "VFMSUB231SH",
[]arch.ExtOperand{arch.ExtXmm(5), arch.ExtXmm(4), arch.ExtRounded(arch.ExtXmm(6), arch.ExtRoundTruncate)},
"62f65578bbf4"},
{"complex multiply, conjugating the first source", "VFCMULCPH",
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtZmm(28), arch.ExtZmm(30)},
"62061740d6f4"},
{"complex multiply-add, conjugating the second source", "VFMADDCPH",
[]arch.ExtOperand{arch.ExtZmm(29), arch.ExtZmm(28), arch.ExtZmm(30)},
"6206164056f4"},
{"complex multiply-add, conjugated first source, rounded", "VFCMADDCPH",
[]arch.ExtOperand{arch.ExtZmm(5), arch.ExtZmm(4), arch.ExtRounded(arch.ExtZmm(6), arch.ExtRoundNearest)},
"62f6571856f4"},
{"scalar complex multiply-add out of memory", "VFMADDCSH",
[]arch.ExtOperand{arch.ExtXmm(29), arch.ExtMemory(9, 0), arch.ExtXmm(30)},
"624616005731"},
{"minimum or maximum under a control byte", "VMINMAXPH",
[]arch.ExtOperand{arch.ExtImmediate(0x88), arch.ExtZmm(29), arch.ExtMemory(9, 0), arch.ExtZmm(30)},
"62431440523188"},
{"scalar minimum or maximum out of memory", "VMINMAXSH",
[]arch.ExtOperand{arch.ExtImmediate(0x88), arch.ExtXmm(28), arch.ExtMemory(9, 0), arch.ExtXmm(29)},
"62431c00532988"},
{"vnni dot product through the VEX word", "VPDPWSUD",
[]arch.ExtOperand{arch.ExtXmm(2), arch.ExtXmm(1), arch.ExtXmm(3)},
"c4e26ad2d9"},
{"saturating dot product, high registers", "VPDPWUSDS",
[]arch.ExtOperand{arch.ExtYmm(10), arch.ExtYmm(15), arch.ExtYmm(8)},
"c4422dd3c7"},
{"dot product out of memory", "VPDPWSUD",
[]arch.ExtOperand{arch.ExtXmm(2), arch.ExtMemory(1, 127), arch.ExtXmm(1)},
"c4e26ad2497f"},
} {
got, err := EncodeExtension(arch.AMD64, tt.mnem, tt.ops...)
if err != nil {
t.Errorf("%s: encode: %v", tt.name, err)
continue
}
if hex.EncodeToString(got) != tt.want {
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
}
}
}
// TestEncodeExtensionAmd64Errors checks the registry's diagnostics on the
// amd64 side: a wrong arity names the form's count and a mis-classed operand
// surfaces the entry's own message.
func TestEncodeExtensionAmd64Errors(t *testing.T) {
if _, err := EncodeExtension(arch.AMD64, "VP2INTERSECTD", arch.ExtZmm(1)); err == nil {
t.Error("one operand encoded, want an arity error")
} else if !strings.Contains(err.Error(), "3 operands") {
t.Errorf("arity error %q does not name the count", err)
}
_, err := EncodeExtension(arch.AMD64, "VCVTNEPS2BF16", arch.ExtZmm(1), arch.ExtZmm(2))
if err == nil {
t.Fatal("a ZMM destination encoded on the narrow convert, want an error")
}
if !strings.Contains(err.Error(), "YMM register") {
t.Errorf("error %q does not name the YMM destination", err)
}
if _, err := EncodeExtension(arch.AMD64, "VCVTNE2PS2BF16"); err == nil ||
!strings.Contains(err.Error(), "takes 3 operands, got 0") {
t.Errorf("zero-operand error = %v, want the operand-count diagnostic", err)
}
}