// Copyright (c) 2026 Petr BalvĂ­n (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}, } { 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) != 183 { t.Errorf("the amd64 layer registers %d instructions, want 183", 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"}, } { 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) } }