feat(arch): add the amd64 extended-instruction layer with BF16 and VP2INTERSECT
Assisted-by: GLM 5.3 Flash
This commit is contained in:
1 parent
2d803e38d8
commit
28bea95128
6 files changed
+803
-8
No files matched your search
@@ -0,0 +1,122 @@
|
||||
// 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 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},
|
||||
} {
|
||||
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) != 15 {
|
||||
t.Errorf("the amd64 layer registers %d instructions, want 15", 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"},
|
||||
} {
|
||||
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)
|
||||
}
|
||||
}
|
||||
+23
-4
@@ -152,14 +152,15 @@ func TestExtensionEncodable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionArchIsolation is the architecture-binding negative case: the
|
||||
// extension layer is registered for arm64 alone, and no other architecture
|
||||
// answers its queries, not even for a mnemonic the amd64 base table carries.
|
||||
// TestExtensionArchIsolation is the architecture-binding negative case: no
|
||||
// architecture answers the arm64 mnemonics but arm64, and the amd64 layer
|
||||
// answers nothing of the arm64 family either (its own mnemonics live in
|
||||
// extension_amd64_test.go).
|
||||
func TestExtensionArchIsolation(t *testing.T) {
|
||||
ops := []arch.ExtOperand{
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB),
|
||||
}
|
||||
for _, a := range []arch.Arch{arch.AMD64, arch.RISCV, arch.LOONG64, arch.Unknown} {
|
||||
for _, a := range []arch.Arch{arch.RISCV, arch.LOONG64, arch.Unknown} {
|
||||
if cands, ok := LookupExtension(a, "ADD"); ok || cands != nil {
|
||||
t.Errorf("LookupExtension(%s, ADD) offered %d candidates", a, len(cands))
|
||||
}
|
||||
@@ -181,6 +182,24 @@ func TestExtensionArchIsolation(t *testing.T) {
|
||||
t.Errorf("arch.Extensions(%s) carries %d instructions", a, len(got))
|
||||
}
|
||||
}
|
||||
// The amd64 layer exists but stays silent about the arm64 family.
|
||||
if cands, ok := LookupExtension(arch.AMD64, "ADD"); ok || cands != nil {
|
||||
t.Errorf("LookupExtension(AMD64, ADD) offered %d candidates", len(cands))
|
||||
}
|
||||
if got, err := EncodeExtension(arch.AMD64, "ADD", ops...); err == nil {
|
||||
t.Errorf("EncodeExtension(AMD64, ADD) encoded %x, want a refusal", got)
|
||||
} else if !strings.Contains(err.Error(), string(arch.AMD64)) {
|
||||
t.Errorf("EncodeExtension(AMD64) error %q does not name the architecture", err)
|
||||
}
|
||||
if ExtensionEncodable(arch.AMD64, "ADD", ops...) {
|
||||
t.Error("ExtensionEncodable(AMD64, ADD) reported true")
|
||||
}
|
||||
if names := ExtensionNames(arch.AMD64); len(names) == 0 {
|
||||
t.Error("the amd64 layer registers no names")
|
||||
}
|
||||
if got := arch.Extensions(arch.AMD64); len(got) == 0 {
|
||||
t.Error("arch.Extensions(AMD64) is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionNamesARM64 checks the completion-facing name list: every
|
||||
|
||||
Reference in new issue
Block a user