// 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", 1}, {"VMOVW", 2}, {"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}, } { 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) != 66 { t.Errorf("the amd64 layer registers %d instructions, want 66", 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"}, } { 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) } }