feat(arch): add the extended-instruction layer with SVE arithmetic
Test / test (push) Successful in 3m38s
Test / test (push) Successful in 3m38s
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The extended-instruction registry: the lookup over and above the generated
|
||||
// architecture tables. The generated tables (arch/*_gen.go) list the
|
||||
// mnemonics the Go toolchain knows; the extension layer carries the
|
||||
// instructions it does not, and this file indexes them per architecture so
|
||||
// the assembler and the linter can consult the layer without touching the
|
||||
// generated lists or the main encoders. A later hook wires
|
||||
// ExtensionEncodable into the Encodable mirror and EncodeExtension into the
|
||||
// per-architecture assembly paths; nothing existing changes until then.
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
)
|
||||
|
||||
// extensionIndex is the per-architecture index of the extension layer, keyed
|
||||
// by upper-case mnemonic. One mnemonic registers several forms (the SVE ADD
|
||||
// carries unpredicated, predicated and immediate shapes), so the value is the
|
||||
// full candidate list in table order.
|
||||
type extensionIndex struct {
|
||||
byName map[string][]arch.ExtInstr
|
||||
}
|
||||
|
||||
// extensionIndexes builds one index per known architecture. Architectures
|
||||
// whose extension layer is not built yet get an empty index, which keeps the
|
||||
// queries answering false rather than failing on a missing entry.
|
||||
var extensionIndexes = buildExtensionIndexes()
|
||||
|
||||
func buildExtensionIndexes() map[arch.Arch]*extensionIndex {
|
||||
m := make(map[arch.Arch]*extensionIndex)
|
||||
for _, a := range []arch.Arch{arch.AMD64, arch.ARM64, arch.RISCV, arch.LOONG64} {
|
||||
idx := &extensionIndex{byName: make(map[string][]arch.ExtInstr)}
|
||||
for _, in := range arch.Extensions(a) {
|
||||
key := strings.ToUpper(in.Name)
|
||||
idx.byName[key] = append(idx.byName[key], in)
|
||||
}
|
||||
m[a] = idx
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// LookupExtension returns the extended instructions registered for the
|
||||
// mnemonic on a, outside the generated architecture table. It reports false
|
||||
// when a carries no extended layer or the mnemonic is not in it; a mnemonic
|
||||
// the base table knows is not thereby covered, the layers stay independent.
|
||||
func LookupExtension(a arch.Arch, mnemonic string) ([]arch.ExtInstr, bool) {
|
||||
idx, ok := extensionIndexes[a]
|
||||
if !ok || idx == nil {
|
||||
return nil, false
|
||||
}
|
||||
cands, ok := idx.byName[strings.ToUpper(mnemonic)]
|
||||
return cands, ok && len(cands) > 0
|
||||
}
|
||||
|
||||
// ExtensionNames returns the mnemonics the extension layer of a registers,
|
||||
// in table order, without duplicates.
|
||||
func ExtensionNames(a arch.Arch) []string {
|
||||
var names []string
|
||||
seen := make(map[string]bool)
|
||||
for _, in := range arch.Extensions(a) {
|
||||
key := strings.ToUpper(in.Name)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
names = append(names, in.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// EncodeExtension encodes one extended instruction on a: it resolves the
|
||||
// mnemonic through the extension registry, picks the registered form whose
|
||||
// arity matches the operands and encodes against it. The first form that
|
||||
// encodes wins. When every matching form rejects the operands, the error
|
||||
// comes from the form whose operand kinds the list points at (the one with
|
||||
// the most matching positions), so a mis-spelled predicate qualifier is
|
||||
// diagnosed as one, not as the unpredicated form's register complaint.
|
||||
func EncodeExtension(a arch.Arch, mnemonic string, ops ...arch.ExtOperand) ([]byte, error) {
|
||||
cands, ok := LookupExtension(a, mnemonic)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s registers no extended instruction %q", a, mnemonic)
|
||||
}
|
||||
var bestErr error
|
||||
var bestScore int
|
||||
var tried int
|
||||
for _, in := range cands {
|
||||
if in.Form.Arity() != len(ops) {
|
||||
continue
|
||||
}
|
||||
tried++
|
||||
b, err := in.Encode(ops)
|
||||
if err == nil {
|
||||
return b, nil
|
||||
}
|
||||
if score := kindScore(in.Form, ops); bestErr == nil || score > bestScore {
|
||||
bestErr, bestScore = err, score
|
||||
}
|
||||
}
|
||||
if tried == 0 {
|
||||
return nil, fmt.Errorf("%s: extended %q takes %s, got %d operands",
|
||||
a, mnemonic, extensionAritySummary(cands), len(ops))
|
||||
}
|
||||
return nil, bestErr
|
||||
}
|
||||
|
||||
// kindScore counts the positions whose operand kind matches what the form
|
||||
// wants, the tie-break that picks the most specific rejection.
|
||||
func kindScore(form arch.ExtForm, ops []arch.ExtOperand) int {
|
||||
kinds := form.Kinds()
|
||||
score := 0
|
||||
for i, op := range ops {
|
||||
if i < len(kinds) && op.Kind == kinds[i] {
|
||||
score++
|
||||
}
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// ExtensionEncodable reports whether the extension layer of a encodes the
|
||||
// mnemonic with these operands. It mirrors asm.Encodable for the extension
|
||||
// layer: the predicate the linter consults once the hook wires it in.
|
||||
func ExtensionEncodable(a arch.Arch, mnemonic string, ops ...arch.ExtOperand) bool {
|
||||
_, err := EncodeExtension(a, mnemonic, ops...)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// extensionAritySummary describes the operand counts the candidate forms
|
||||
// take, "2 or 3" style, for the arity error.
|
||||
func extensionAritySummary(cands []arch.ExtInstr) string {
|
||||
counts := make([]int, 0, len(cands))
|
||||
seen := make(map[int]bool)
|
||||
for _, in := range cands {
|
||||
n := in.Form.Arity()
|
||||
if !seen[n] {
|
||||
seen[n] = true
|
||||
counts = append(counts, n)
|
||||
}
|
||||
}
|
||||
slices.Sort(counts)
|
||||
var b strings.Builder
|
||||
for i, n := range counts {
|
||||
if i > 0 {
|
||||
if i == len(counts)-1 {
|
||||
b.WriteString(" or ")
|
||||
} else {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "%d", n)
|
||||
}
|
||||
b.WriteString(" operands")
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// TestExtensionRegistryARM64 checks the mnemonic lookup over and above the
|
||||
// generated arm64 table: one mnemonic, several forms, case-insensitive, and
|
||||
// nothing offered for a spelling the layer does not carry.
|
||||
func TestExtensionRegistryARM64(t *testing.T) {
|
||||
add, ok := LookupExtension(arch.ARM64, "ADD")
|
||||
if !ok {
|
||||
t.Fatal("LookupExtension(ARM64, ADD) found nothing")
|
||||
}
|
||||
var forms []arch.ExtForm
|
||||
for _, in := range add {
|
||||
if in.Name != "ADD" {
|
||||
t.Errorf("candidate %q leaked into the ADD lookup", in.Name)
|
||||
}
|
||||
forms = append(forms, in.Form)
|
||||
}
|
||||
if len(forms) != 3 ||
|
||||
forms[0] != arch.ExtFormVectors ||
|
||||
forms[1] != arch.ExtFormPredicated ||
|
||||
forms[2] != arch.ExtFormImmediate {
|
||||
t.Errorf("ADD registers forms %v, want unpredicated, predicated and immediate", forms)
|
||||
}
|
||||
if _, ok := LookupExtension(arch.ARM64, "add"); !ok {
|
||||
t.Error("the lookup is case-sensitive")
|
||||
}
|
||||
if _, ok := LookupExtension(arch.ARM64, "NOSUCHINSTR"); ok {
|
||||
t.Error("a non-extended mnemonic resolved")
|
||||
}
|
||||
sqadd, ok := LookupExtension(arch.ARM64, "SQADD")
|
||||
if !ok || len(sqadd) != 2 {
|
||||
t.Errorf("SQADD registers %d forms, want the unpredicated and immediate pair", len(sqadd))
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionAboveGeneratedTable pins the layering: SQADD is nowhere in the
|
||||
// generated arm64 table (the toolchain knows only the NEON spelling VSQADD)
|
||||
// yet the extension layer carries it, while ADD sits in both layers
|
||||
// independently.
|
||||
func TestExtensionAboveGeneratedTable(t *testing.T) {
|
||||
if _, found := arch.ForArch(arch.ARM64).Lookup("SQADD"); found {
|
||||
t.Error("SQADD is in the generated table, the layering assumption broke")
|
||||
}
|
||||
if _, ok := LookupExtension(arch.ARM64, "SQADD"); !ok {
|
||||
t.Error("SQADD is missing from the extension layer")
|
||||
}
|
||||
if _, found := arch.ForArch(arch.ARM64).Lookup("ADD"); !found {
|
||||
t.Error("ADD vanished from the generated table")
|
||||
}
|
||||
if add, ok := LookupExtension(arch.ARM64, "ADD"); !ok || len(add) != 3 {
|
||||
t.Errorf("ADD carries %d extension forms, want 3", len(add))
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeExtensionGolden encodes through the registry and pins the same
|
||||
// golden words the arch table tests pin, proving the registry resolves to the
|
||||
// right encoding.
|
||||
func TestEncodeExtensionGolden(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []arch.ExtOperand
|
||||
want uint32
|
||||
}{
|
||||
{"unpredicated add", "ADD",
|
||||
[]arch.ExtOperand{
|
||||
arch.ExtVector(2, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrB),
|
||||
},
|
||||
0x04200040},
|
||||
{"predicated mul", "MUL",
|
||||
[]arch.ExtOperand{
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtPredicate(2, arch.ExtQualMerging), arch.ExtVector(0, arch.ExtArrB),
|
||||
},
|
||||
0x04100800},
|
||||
{"immediate add with derived shift", "ADD",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(32512), arch.ExtVector(0, arch.ExtArrH)},
|
||||
0x2560efe0},
|
||||
{"signed immediate mul", "MUL",
|
||||
[]arch.ExtOperand{arch.ExtImmediate(-1), arch.ExtVector(0, arch.ExtArrB)},
|
||||
0x2530dfe0},
|
||||
} {
|
||||
got, err := EncodeExtension(arch.ARM64, tt.mnem, tt.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: encode: %v", tt.name, err)
|
||||
continue
|
||||
}
|
||||
if want := hex.EncodeToString([]byte{
|
||||
byte(tt.want), byte(tt.want >> 8), byte(tt.want >> 16), byte(tt.want >> 24),
|
||||
}); hex.EncodeToString(got) != want {
|
||||
t.Errorf("%s:\n got %x\n want %s", tt.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeExtensionErrors checks the registry's diagnostics: a wrong arity
|
||||
// names every form's count, an operand the first candidate rejects surfaces
|
||||
// its own message once a later form takes over.
|
||||
func TestEncodeExtensionErrors(t *testing.T) {
|
||||
if _, err := EncodeExtension(arch.ARM64, "ADD", arch.ExtVector(0, arch.ExtArrB)); err == nil {
|
||||
t.Error("one operand encoded, want an arity error")
|
||||
} else if !strings.Contains(err.Error(), "2 or 3 operands") {
|
||||
t.Errorf("arity error %q does not name the counts", err)
|
||||
}
|
||||
// The predicated candidate must answer for its own operands: the /Z
|
||||
// qualifier is rejected with the merging message, not the unpredicated
|
||||
// form's register-kind complaint.
|
||||
_, err := EncodeExtension(arch.ARM64, "ADD",
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtPredicate(0, arch.ExtQualZeroing), arch.ExtVector(0, arch.ExtArrB))
|
||||
if err == nil {
|
||||
t.Fatal("/Z encoded, want an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/M") {
|
||||
t.Errorf("error %q does not name the merging qualifier", err)
|
||||
}
|
||||
if _, err := EncodeExtension(arch.ARM64, "NOSUCHINSTR", arch.ExtVector(0, arch.ExtArrB)); err == nil ||
|
||||
!strings.Contains(err.Error(), "registers no extended instruction") {
|
||||
t.Errorf("unknown mnemonic error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionEncodable checks the predicate the later Encodable hook will
|
||||
// call: true exactly when the registry encodes the operand list.
|
||||
func TestExtensionEncodable(t *testing.T) {
|
||||
if !ExtensionEncodable(arch.ARM64, "ADD",
|
||||
arch.ExtVector(0, arch.ExtArrS), arch.ExtVector(1, arch.ExtArrS), arch.ExtVector(2, arch.ExtArrS)) {
|
||||
t.Error("an encodable unpredicated add reported false")
|
||||
}
|
||||
if !ExtensionEncodable(arch.ARM64, "ADD",
|
||||
arch.ExtVector(1, arch.ExtArrS), arch.ExtPredicate(0, arch.ExtQualMerging), arch.ExtVector(0, arch.ExtArrS)) {
|
||||
t.Error("an encodable predicated add reported false")
|
||||
}
|
||||
if ExtensionEncodable(arch.ARM64, "ADD",
|
||||
arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(0, arch.ExtArrS), arch.ExtVector(0, arch.ExtArrB)) {
|
||||
t.Error("mismatched arrangements reported encodable")
|
||||
}
|
||||
if ExtensionEncodable(arch.ARM64, "ADD", arch.ExtVector(0, arch.ExtArrB)) {
|
||||
t.Error("a one-operand add reported encodable")
|
||||
}
|
||||
if ExtensionEncodable(arch.ARM64, "NOSUCHINSTR") {
|
||||
t.Error("an unregistered mnemonic reported encodable")
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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} {
|
||||
if cands, ok := LookupExtension(a, "ADD"); ok || cands != nil {
|
||||
t.Errorf("LookupExtension(%s, ADD) offered %d candidates", a, len(cands))
|
||||
}
|
||||
if cands, ok := LookupExtension(a, "MUL"); ok || cands != nil {
|
||||
t.Errorf("LookupExtension(%s, MUL) offered %d candidates", a, len(cands))
|
||||
}
|
||||
if got, err := EncodeExtension(a, "ADD", ops...); err == nil {
|
||||
t.Errorf("EncodeExtension(%s, ADD) encoded %x, want a refusal", a, got)
|
||||
} else if !strings.Contains(err.Error(), string(a)) {
|
||||
t.Errorf("EncodeExtension(%s) error %q does not name the architecture", a, err)
|
||||
}
|
||||
if ExtensionEncodable(a, "ADD", ops...) {
|
||||
t.Errorf("ExtensionEncodable(%s, ADD) reported true", a)
|
||||
}
|
||||
if names := ExtensionNames(a); len(names) != 0 {
|
||||
t.Errorf("ExtensionNames(%s) = %v, want none", a, names)
|
||||
}
|
||||
if got := arch.Extensions(a); len(got) != 0 {
|
||||
t.Errorf("arch.Extensions(%s) carries %d instructions", a, len(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtensionNamesARM64 checks the completion-facing name list: every
|
||||
// distinct mnemonic of the family, first-occurrence order, no duplicates.
|
||||
func TestExtensionNamesARM64(t *testing.T) {
|
||||
want := []string{"ADD", "SUB", "SQADD", "UQADD", "SQSUB", "UQSUB", "MUL", "SMULH", "UMULH", "SUBR"}
|
||||
got := ExtensionNames(arch.ARM64)
|
||||
if strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Errorf("ExtensionNames(ARM64) = %v, want %v", got, want)
|
||||
}
|
||||
if n := len(arch.Extensions(arch.ARM64)); n != 23 {
|
||||
t.Errorf("the family registers %d instructions, want 23", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user