feat(arch): add the extended-instruction layer with SVE arithmetic
Test / test (push) Successful in 3m38s

This commit is contained in:
2026-10-02 20:39:33 +02:00
parent 2747fce7d3
commit 5a8e9acbf3
4 changed files with 1380 additions and 0 deletions
+624
View File
@@ -0,0 +1,624 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// This file carries the extended-instruction layer: instructions the Go
// toolchain does not know at all, described as data and validated against
// golden vectors from the Arm Architecture Reference Manual rather than
// against the toolchain. It sits beside the generated tables, never inside
// them: arch/arm64_gen.go stays untouched, and Extensions returns the layer
// per architecture so a later amd64 table attaches through the same door.
//
// The first entry is the arm64 SVE and SVE2 integer add/subtract/multiply
// family (twenty-three forms over four word shapes). The encodings are
// transcribed from the manual and cross-checked against the GNU assembler's
// and LLVM's published encodings; the golden vectors in arm64_ext_test.go pin
// the bytes.
package arch
import "fmt"
// ExtOperandKind classifies one operand of an extended instruction.
type ExtOperandKind uint8
// Operand kinds.
const (
ExtZReg ExtOperandKind = iota // scalable vector register Z0-Z31
ExtPReg // predicate register P0-P15
ExtImm // immediate
)
// String returns a short label for the kind.
func (k ExtOperandKind) String() string {
switch k {
case ExtZReg:
return "scalable vector register"
case ExtPReg:
return "predicate register"
case ExtImm:
return "immediate"
default:
return "operand"
}
}
// ExtArrangement is the element-size suffix a scalable vector operand
// carries: .B, .H, .S, .D or .Q. ExtArrNone means the operand is written
// bare, which the SVE forms in this layer reject.
type ExtArrangement uint8
// Arrangements, widest last.
const (
ExtArrNone ExtArrangement = iota
ExtArrB // 8-bit elements
ExtArrH // 16-bit elements
ExtArrS // 32-bit elements
ExtArrD // 64-bit elements
ExtArrQ // 128-bit elements
)
// String returns the assembler suffix, with the leading dot.
func (a ExtArrangement) String() string {
switch a {
case ExtArrB:
return ".B"
case ExtArrH:
return ".H"
case ExtArrS:
return ".S"
case ExtArrD:
return ".D"
case ExtArrQ:
return ".Q"
default:
return ""
}
}
// Width returns the byte width of one element under the arrangement.
func (a ExtArrangement) Width() int {
switch a {
case ExtArrB:
return 1
case ExtArrH:
return 2
case ExtArrS:
return 4
case ExtArrD:
return 8
case ExtArrQ:
return 16
default:
return 0
}
}
// sizeBits maps the arrangement onto the two-bit size field the integer SVE
// classes carry at bits 23..22: 00=B, 01=H, 10=S, 11=D. ok is false for the
// arrangements no such class accepts (.Q and the bare spelling).
func (a ExtArrangement) sizeBits() (uint32, bool) {
switch a {
case ExtArrB, ExtArrH, ExtArrS, ExtArrD:
return uint32(a) - 1, true
default:
return 0, false
}
}
// ExtQualifier is the predicate qualifier spelled after the slash.
type ExtQualifier uint8
// Predicate qualifiers.
const (
ExtQualNone ExtQualifier = iota // bare Pn (non-predicating position)
ExtQualMerging // /M, inactive lanes keep the destination
ExtQualZeroing // /Z, inactive lanes become zero
)
// String returns the assembler spelling, with the leading slash.
func (q ExtQualifier) String() string {
switch q {
case ExtQualMerging:
return "/M"
case ExtQualZeroing:
return "/Z"
default:
return ""
}
}
// ExtOperand is one operand of an extended instruction, already resolved to
// its pieces: a register with its arrangement and qualifier, or an immediate
// with its optional left shift. The assembler's future hook constructs these
// from the parsed statement; Encode consumes them.
type ExtOperand struct {
Kind ExtOperandKind
Reg int // register number (Z: 0..31, P: 0..15)
Arr ExtArrangement // element-size suffix; ExtArrNone when bare
Qual ExtQualifier // predicate qualifier; ExtQualNone elsewhere
Imm int64 // immediate value (ExtImm only)
// Shift carries the LSL amount an immediate form shifts the constant by
// before use (0 or 8 in the SVE add/subtract immediate class). HasShift
// separates a spelled shift (validated as written) from an unshifted
// operand (the encoder may derive the sh bit from the value).
Shift int
HasShift bool
}
// ExtVector builds a scalable vector operand, ADD Z1.S style.
func ExtVector(reg int, arr ExtArrangement) ExtOperand {
return ExtOperand{Kind: ExtZReg, Reg: reg, Arr: arr}
}
// ExtPredicate builds a predicate operand with its qualifier, P0/M style.
func ExtPredicate(reg int, qual ExtQualifier) ExtOperand {
return ExtOperand{Kind: ExtPReg, Reg: reg, Qual: qual}
}
// ExtImmediate builds an unshifted immediate operand.
func ExtImmediate(v int64) ExtOperand {
return ExtOperand{Kind: ExtImm, Imm: v}
}
// ExtShiftedImmediate builds an immediate operand with a spelled LSL amount.
func ExtShiftedImmediate(v int64, shift int) ExtOperand {
return ExtOperand{Kind: ExtImm, Imm: v, Shift: shift, HasShift: true}
}
// ExtField is one named field of the 32-bit encoding word: a bit offset from
// the least significant end and the field's width.
type ExtField struct {
Off uint8
Width uint8
}
// extMask returns the field's bits as a mask.
func extMask(f ExtField) uint32 {
return ^uint32(0) >> (32 - f.Width)
}
// extSet ORs v into the field of word.
func extSet(word uint32, f ExtField, v uint32) uint32 {
return word | (v&extMask(f))<<f.Off
}
// The fields the SVE integer classes use. The 5-bit register fields are
// named after their role in the three-vector class; the predicated class
// reuses extFieldRn for its Zm operand and extFieldPg for the governing
// predicate, which that class narrows to three bits (P0-P7).
var (
extFieldRd = ExtField{0, 5} // destination (Zd or Zdn)
extFieldRn = ExtField{5, 5} // first source (Zn, or Zm in the predicated class)
extFieldRm = ExtField{16, 5} // second source (Zm in the three-vector class)
extFieldPg = ExtField{10, 3} // governing predicate P0-P7 (predicated class)
extFieldImm8 = ExtField{5, 8} // the immediate, bits 12..5
extFieldSh = ExtField{13, 1} // the shift flag: 1 means LSL #8
extSizeBHSD = ExtField{22, 2} // element-size field of every class here, bits 23..22
)
// ExtForm enumerates the operand shapes the extension layer defines, in Plan
// 9 order (sources first, destination last). A destructive SVE operand is
// written once, in destination position: the encoding carries no second copy.
type ExtForm uint8
// Operand shapes.
const (
// ExtFormVectors is the unpredicated three-vector form, the SVE integer
// add/subtract (unpredicated) class: ADD Z0.S, Z1.S, Z2.S computes
// Z0 = Z1 + Z2. Operands: Zn, Zm, Zd.
ExtFormVectors ExtForm = iota
// ExtFormPredicated is the governed destructive form, the SVE integer
// add/subtract vectors (predicated) class: ADD Z1.S, P0/M, Z0.S computes
// Z0 = Z0 + Z1 for the active lanes. Operands: Zm, Pg/M, Zdn. The
// governing predicate is a 3-bit field, so only P0-P7 encode here, and
// the class takes the merging qualifier alone: a zeroing form would need
// a MOVPRFX expansion, which one data word cannot carry.
ExtFormPredicated
// ExtFormImmediate is the add/subtract immediate form, the SVE integer
// add/subtract (immediate) class: ADD $255, Z0.S computes
// Z0 = Z0 + 255. Operands: imm{, LSL #8}, Zdn. The constant is an
// unsigned imm8, optionally shifted left by 8 bits; a bare multiple of
// 256 (up to 65280) derives the shift, the spelling the GNU assembler
// canonicalises too. .B takes no shift.
ExtFormImmediate
// ExtFormSignedImmediate is the signed immediate form of the SVE integer
// multiply (immediate) class: MUL $-128, Z0.B computes Z0 = Z0 * -128.
// Operands: simm8, Zdn. No shift exists in this class.
ExtFormSignedImmediate
)
// Arity returns the operand count the form takes.
func (f ExtForm) Arity() int {
switch f {
case ExtFormVectors, ExtFormPredicated:
return 3
case ExtFormImmediate, ExtFormSignedImmediate:
return 2
default:
return 0
}
}
// Kinds returns the operand kind each position of the form wants, in the
// order the operands arrive. The registry uses the list to pick the most
// specific rejection when every matching form refuses an operand list.
func (f ExtForm) Kinds() []ExtOperandKind {
switch f {
case ExtFormVectors:
return []ExtOperandKind{ExtZReg, ExtZReg, ExtZReg}
case ExtFormPredicated:
return []ExtOperandKind{ExtZReg, ExtPReg, ExtZReg}
case ExtFormImmediate, ExtFormSignedImmediate:
return []ExtOperandKind{ExtImm, ExtZReg}
default:
return nil
}
}
// String returns a short label for the form, for diagnostics.
func (f ExtForm) String() string {
switch f {
case ExtFormVectors:
return "unpredicated vectors"
case ExtFormPredicated:
return "predicated (merging)"
case ExtFormImmediate:
return "unsigned immediate"
case ExtFormSignedImmediate:
return "signed immediate"
default:
return "unknown form"
}
}
// ExtFeature names the architecture feature an extended instruction belongs
// to. The field is metadata: the assembler offers every instruction it
// registers, and a feature check is the caller's decision, not the encoder's.
type ExtFeature string
// The features the arm64 layer covers.
const (
ExtFeatureSVE ExtFeature = "sve"
ExtFeatureSVE2 ExtFeature = "sve2"
)
// ExtInstr is one extended instruction: the metadata a lookup needs and the
// encoding as data. Word holds the fixed bits of the 32-bit encoding with
// every operand field and the size field zero; the form says which fields the
// operands fill; the size field receives the arrangement's bits at encode
// time. Ref names the manual entry the encoding is transcribed from, the
// golden source in place of a toolchain oracle.
type ExtInstr struct {
Name string // upper-case mnemonic
Summary string // one line of hover documentation
Word uint32 // fixed encoding bits, operand fields zero
Form ExtForm // operand shape
Size ExtField // element-size field the arrangement fills
Feature ExtFeature // sve or sve2
Ref string // the ARM ARM entry the encoding comes from
}
// Encode assembles the operands into the 4 little-endian bytes of the
// instruction word. The operand kinds, register ranges, arrangements and
// immediate ranges are validated against the form; an operand the class
// cannot carry is an error, never a silent mis-encoding.
func (in ExtInstr) Encode(ops []ExtOperand) ([]byte, error) {
if len(ops) != in.Form.Arity() {
return nil, fmt.Errorf("%s: the %s form takes %d operands, got %d",
in.Name, in.Form, in.Form.Arity(), len(ops))
}
switch in.Form {
case ExtFormVectors:
return in.encodeVectors(ops)
case ExtFormPredicated:
return in.encodePredicated(ops)
case ExtFormImmediate:
return in.encodeImmediate(ops)
case ExtFormSignedImmediate:
return in.encodeSignedImmediate(ops)
default:
return nil, fmt.Errorf("%s: unknown form %d", in.Name, in.Form)
}
}
// encodeVectors fills the unpredicated three-vector form: Zn, Zm, Zd, all
// under one required arrangement.
func (in ExtInstr) encodeVectors(ops []ExtOperand) ([]byte, error) {
for i, op := range ops {
if op.Kind != ExtZReg {
return nil, fmt.Errorf("%s: operand %d wants a scalable vector register, got %s",
in.Name, i+1, op.Kind)
}
if op.Reg < 0 || op.Reg > 31 {
return nil, fmt.Errorf("%s: operand %d is Z%d, outside Z0-Z31", in.Name, i+1, op.Reg)
}
}
arr, err := in.sharedArrangement(ops)
if err != nil {
return nil, err
}
size, ok := arr.sizeBits()
if !ok {
return nil, fmt.Errorf("%s: arrangement %s has no size encoding in this class", in.Name, arr)
}
word := in.Word
word = extSet(word, extFieldRn, uint32(ops[0].Reg))
word = extSet(word, extFieldRm, uint32(ops[1].Reg))
word = extSet(word, extFieldRd, uint32(ops[2].Reg))
word = extSet(word, in.Size, size)
return extWordLE(word), nil
}
// encodePredicated fills the governed destructive form: Zm, Pg/M, Zdn. The
// predicate is a 3-bit field, the merging qualifier alone, and carries no
// arrangement suffix in this class.
func (in ExtInstr) encodePredicated(ops []ExtOperand) ([]byte, error) {
zm, pg, zdn := ops[0], ops[1], ops[2]
if zm.Kind != ExtZReg {
return nil, fmt.Errorf("%s: operand 1 wants a scalable vector register, got %s",
in.Name, zm.Kind)
}
if zm.Reg < 0 || zm.Reg > 31 {
return nil, fmt.Errorf("%s: operand 1 is Z%d, outside Z0-Z31", in.Name, zm.Reg)
}
if pg.Kind != ExtPReg {
return nil, fmt.Errorf("%s: operand 2 wants a predicate register, got %s",
in.Name, pg.Kind)
}
if pg.Reg < 0 || pg.Reg > 7 {
return nil, fmt.Errorf("%s: operand 2 is P%d, outside P0-P7 in this class", in.Name, pg.Reg)
}
if pg.Qual != ExtQualMerging {
return nil, fmt.Errorf("%s: operand 2 wants the merging qualifier /M, got %q",
in.Name, pg.Qual)
}
if pg.Arr != ExtArrNone {
return nil, fmt.Errorf("%s: the governing predicate carries no arrangement suffix, got %s",
in.Name, pg.Arr)
}
if zdn.Kind != ExtZReg {
return nil, fmt.Errorf("%s: operand 3 wants a scalable vector register, got %s",
in.Name, zdn.Kind)
}
if zdn.Reg < 0 || zdn.Reg > 31 {
return nil, fmt.Errorf("%s: operand 3 is Z%d, outside Z0-Z31", in.Name, zdn.Reg)
}
if zm.Arr != zdn.Arr {
return nil, fmt.Errorf("%s: operands 1 and 3 carry arrangements %s and %s, they must match",
in.Name, zm.Arr, zdn.Arr)
}
size, ok := zdn.Arr.sizeBits()
if !ok {
return nil, fmt.Errorf("%s: arrangement %s has no size encoding in this class", in.Name, zdn.Arr)
}
word := in.Word
word = extSet(word, extFieldRn, uint32(zm.Reg))
word = extSet(word, extFieldPg, uint32(pg.Reg))
word = extSet(word, extFieldRd, uint32(zdn.Reg))
word = extSet(word, in.Size, size)
return extWordLE(word), nil
}
// encodeImmediate fills the add/subtract immediate form: imm{, LSL #8}, Zdn.
// The class encodes an unsigned imm8 with one shift bit, so a bare multiple
// of 256 derives the shift the way the GNU assembler canonicalises it.
func (in ExtInstr) encodeImmediate(ops []ExtOperand) ([]byte, error) {
imm, zdn := ops[0], ops[1]
imm8, sh, err := in.addSubImmediate(imm, zdn.Arr)
if err != nil {
return nil, err
}
word := in.Word
word = extSet(word, extFieldImm8, uint32(imm8))
if sh != 0 {
word = extSet(word, extFieldSh, 1)
}
word, err = in.setDestAndSize(word, zdn)
if err != nil {
return nil, err
}
return extWordLE(word), nil
}
// encodeSignedImmediate fills the multiply immediate form: simm8, Zdn, with
// no shift bit in the class.
func (in ExtInstr) encodeSignedImmediate(ops []ExtOperand) ([]byte, error) {
imm, zdn := ops[0], ops[1]
if imm.Kind != ExtImm {
return nil, fmt.Errorf("%s: operand 1 wants an immediate, got %s", in.Name, imm.Kind)
}
if imm.HasShift {
return nil, fmt.Errorf("%s: the signed immediate class takes no shift", in.Name)
}
if imm.Imm < -128 || imm.Imm > 127 {
return nil, fmt.Errorf("%s: immediate %d is outside the signed 8-bit range -128..127",
in.Name, imm.Imm)
}
word := in.Word
word = extSet(word, extFieldImm8, uint32(imm.Imm))
word, err := in.setDestAndSize(word, zdn)
if err != nil {
return nil, err
}
return extWordLE(word), nil
}
// addSubImmediate resolves the immediate operand of the add/subtract
// immediate class into its imm8 and shift bit: a spelled shift is validated
// as written, a bare multiple of 256 (on .H, .S or .D) derives one.
func (in ExtInstr) addSubImmediate(op ExtOperand, arr ExtArrangement) (imm8, sh int, err error) {
if op.Kind != ExtImm {
return 0, 0, fmt.Errorf("%s: operand 1 wants an immediate, got %s", in.Name, op.Kind)
}
switch {
case op.HasShift:
if op.Shift != 0 && op.Shift != 8 {
return 0, 0, fmt.Errorf("%s: the shift amount must be 0 or 8, got %d", in.Name, op.Shift)
}
if arr == ExtArrB && op.Shift != 0 {
return 0, 0, fmt.Errorf("%s: arrangement .B takes no shift", in.Name)
}
if op.Imm < 0 || op.Imm > 255 {
return 0, 0, fmt.Errorf("%s: immediate %d is outside the unsigned 8-bit range 0..255",
in.Name, op.Imm)
}
return int(op.Imm), op.Shift, nil
case op.Imm >= 0 && op.Imm <= 255:
return int(op.Imm), 0, nil
case arr != ExtArrB && op.Imm >= 256 && op.Imm <= 255<<8 && op.Imm%256 == 0:
// A bare multiple of 256 rides the shift bit, 65280 = 255<<8 included.
return int(op.Imm / 256), 8, nil
default:
return 0, 0, fmt.Errorf("%s: immediate %d is not an unsigned imm8%s, nor a multiple of 256 the shift bit can carry",
in.Name, op.Imm, arr.shiftNote())
}
}
// shiftNote describes where a shifted constant is expressible, for the
// immediate range error.
func (arr ExtArrangement) shiftNote() string {
if arr == ExtArrB {
return " (and .B takes no shifted constant)"
}
return " (a multiple of 256 up to 65280 shifts)"
}
// setDestAndSize fills the destructive destination register and the size
// field from the arrangement the vector carries.
func (in ExtInstr) setDestAndSize(word uint32, zdn ExtOperand) (uint32, error) {
if zdn.Kind != ExtZReg {
return 0, fmt.Errorf("%s: operand 2 wants a scalable vector register, got %s",
in.Name, zdn.Kind)
}
if zdn.Reg < 0 || zdn.Reg > 31 {
return 0, fmt.Errorf("%s: operand 2 is Z%d, outside Z0-Z31", in.Name, zdn.Reg)
}
size, ok := zdn.Arr.sizeBits()
if !ok {
return 0, fmt.Errorf("%s: arrangement %s has no size encoding in this class", in.Name, zdn.Arr)
}
word = extSet(word, extFieldRd, uint32(zdn.Reg))
word = extSet(word, in.Size, size)
return word, nil
}
// sharedArrangement returns the one arrangement all vector operands carry, or
// an error when any operand is bare or they disagree.
func (in ExtInstr) sharedArrangement(ops []ExtOperand) (ExtArrangement, error) {
arr := ops[0].Arr
for i, op := range ops {
if op.Arr == ExtArrNone {
return 0, fmt.Errorf("%s: operand %d carries no arrangement suffix", in.Name, i+1)
}
if op.Arr != arr {
return 0, fmt.Errorf("%s: operand %d carries arrangement %s, want %s",
in.Name, i+1, op.Arr, arr)
}
}
return arr, nil
}
// extWordLE returns a 32-bit encoding word as 4 little-endian bytes.
func extWordLE(w uint32) []byte {
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
}
// --- the arm64 SVE/SVE2 table ------------------------------------------------
// arm64Extensions is the extended instruction layer of arm64: the SVE and
// SVE2 integer add/subtract/multiply family. The Go toolchain knows none of
// these; the encodings are transcribed from the ARM Architecture Reference
// Manual (DDI 0487J, Part C, Chapter C8, the alphabetical list of SVE
// instructions) and cross-checked against the GNU assembler's and LLVM's
// published encodings.
var arm64Extensions = []ExtInstr{
// Unpredicated three-vector forms: ADD Z0.S, Z1.S, Z2.S.
{Name: "ADD", Summary: "Add scalable vector elements, unpredicated",
Word: 0x04200000, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: ADD (vectors, unpredicated)"},
{Name: "SUB", Summary: "Subtract scalable vector elements, unpredicated",
Word: 0x04200400, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SUB (vectors, unpredicated)"},
{Name: "SQADD", Summary: "Add signed saturating scalable vector elements, unpredicated",
Word: 0x04201000, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SQADD (vectors, unpredicated)"},
{Name: "UQADD", Summary: "Add unsigned saturating scalable vector elements, unpredicated",
Word: 0x04201400, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: UQADD (vectors, unpredicated)"},
{Name: "SQSUB", Summary: "Subtract signed saturating scalable vector elements, unpredicated",
Word: 0x04201800, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SQSUB (vectors, unpredicated)"},
{Name: "UQSUB", Summary: "Subtract unsigned saturating scalable vector elements, unpredicated",
Word: 0x04201c00, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: UQSUB (vectors, unpredicated)"},
{Name: "MUL", Summary: "Multiply scalable vector elements, unpredicated",
Word: 0x04206000, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE2,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: MUL (vectors, unpredicated)"},
{Name: "SMULH", Summary: "Multiply signed scalable vector elements, keeping the high half, unpredicated",
Word: 0x04206800, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE2,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SMULH (vectors, unpredicated)"},
{Name: "UMULH", Summary: "Multiply unsigned scalable vector elements, keeping the high half, unpredicated",
Word: 0x04206c00, Form: ExtFormVectors, Size: extSizeBHSD, Feature: ExtFeatureSVE2,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: UMULH (vectors, unpredicated)"},
// Governed destructive forms, merging: ADD Z1.S, P0/M, Z0.S.
{Name: "ADD", Summary: "Add scalable vector elements under a governing predicate, merging",
Word: 0x04000000, Form: ExtFormPredicated, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: ADD (vectors, predicated)"},
{Name: "SUB", Summary: "Subtract scalable vector elements under a governing predicate, merging",
Word: 0x04010000, Form: ExtFormPredicated, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SUB (vectors, predicated)"},
{Name: "SUBR", Summary: "Reverse-subtract scalable vector elements under a governing predicate, merging",
Word: 0x04030000, Form: ExtFormPredicated, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SUBR (vectors, predicated)"},
{Name: "MUL", Summary: "Multiply scalable vector elements under a governing predicate, merging",
Word: 0x04100000, Form: ExtFormPredicated, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: MUL (vectors, predicated)"},
{Name: "SMULH", Summary: "Multiply signed scalable vector elements, keeping the high half, under a governing predicate, merging",
Word: 0x04120000, Form: ExtFormPredicated, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SMULH (vectors, predicated)"},
{Name: "UMULH", Summary: "Multiply unsigned scalable vector elements, keeping the high half, under a governing predicate, merging",
Word: 0x04130000, Form: ExtFormPredicated, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: UMULH (vectors, predicated)"},
// Immediate forms: ADD $255, Z0.S.
{Name: "ADD", Summary: "Add an unsigned immediate to scalable vector elements",
Word: 0x2520c000, Form: ExtFormImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: ADD (vectors, immediate)"},
{Name: "SUB", Summary: "Subtract an unsigned immediate from scalable vector elements",
Word: 0x2521c000, Form: ExtFormImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SUB (vectors, immediate)"},
{Name: "SUBR", Summary: "Subtract scalable vector elements from an unsigned immediate",
Word: 0x2523c000, Form: ExtFormImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SUBR (vectors, immediate)"},
{Name: "SQADD", Summary: "Add a signed saturating unsigned immediate to scalable vector elements",
Word: 0x2524c000, Form: ExtFormImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SQADD (vectors, immediate)"},
{Name: "UQADD", Summary: "Add an unsigned saturating immediate to scalable vector elements",
Word: 0x2525c000, Form: ExtFormImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: UQADD (vectors, immediate)"},
{Name: "SQSUB", Summary: "Subtract an unsigned immediate from scalable vector elements with signed saturation",
Word: 0x2526c000, Form: ExtFormImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: SQSUB (vectors, immediate)"},
{Name: "UQSUB", Summary: "Subtract an unsigned immediate from scalable vector elements with unsigned saturation",
Word: 0x2527c000, Form: ExtFormImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: UQSUB (vectors, immediate)"},
// The signed immediate of the multiply class: MUL $-128, Z0.B.
{Name: "MUL", Summary: "Multiply scalable vector elements by a signed immediate",
Word: 0x2530c000, Form: ExtFormSignedImmediate, Size: extSizeBHSD, Feature: ExtFeatureSVE,
Ref: "ARM DDI 0487J, C8.2 SVE instruction descriptions: MUL (vectors, immediate)"},
}
// Extensions returns the extended-instruction layer registered for a, outside
// the generated tables. An architecture whose extended layer is not built
// yet returns nothing: the mechanism is ordinary code, not a build tag, and
// it simply offers no instruction where none is registered.
func Extensions(a Arch) []ExtInstr {
switch a {
case ARM64:
return arm64Extensions
default:
return nil
}
}
+400
View File
@@ -0,0 +1,400 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package arch
import (
"encoding/hex"
"strings"
"testing"
)
// The SVE encodings have no toolchain oracle: go tool asm knows no SVE at
// all. The golden words below are therefore transcribed from the ARM
// Architecture Reference Manual (DDI 0487J, Part C, Chapter C8, the
// alphabetical list of SVE instructions) and cross-checked against two
// independent implementations of the manual, the GNU assembler and LLVM:
// the rows marked "GNU" match a vector in binutils-gdb's own
// gas/testsuite/gas/aarch64/sve.d (assembled under -march=armv8-a+sve), the
// rows marked "LLVM" match the Inst field assignments in
// SVEInstrFormats.td's sve_int_bin_cons_arit_0, sve_int_bin_pred_arit_log,
// sve_int_arith_imm0 and sve2_int_mul classes. Every class is covered by at
// least one vector of each source.
func extInstruction(t *testing.T, mnem string, form ExtForm) ExtInstr {
t.Helper()
for _, in := range Extensions(ARM64) {
if in.Name == mnem && in.Form == form {
return in
}
}
t.Fatalf("no extended %s with the %s form", mnem, form)
return ExtInstr{}
}
func TestArm64ExtGoldenBytes(t *testing.T) {
for _, tt := range []struct {
name string
mnem string
form ExtForm
ops []ExtOperand
want uint32
GNUas string // the matching binutils-gdb sve.d line, empty when the class evidence comes from LLVM alone
}{
// Unpredicated three-vector forms: Zn, Zm, Zd.
{"add z0.b, z0.b, z0.b", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04200000, "04200000 add z0.b, z0.b, z0.b"},
{"add z0.b, z0.b, z31.b", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(31, ExtArrB), ExtVector(0, ExtArrB)},
0x043f0000, "043f0000 add z0.b, z0.b, z31.b"},
{"add z31.b, z0.b, z0.b", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(31, ExtArrB)},
0x0420001f, "0420001f add z31.b, z0.b, z0.b"},
{"add z0.b, z2.b, z0.b", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(2, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04200040, "04200040 add z0.b, z2.b, z0.b"},
{"add z0.h, z0.h, z0.h", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrH), ExtVector(0, ExtArrH), ExtVector(0, ExtArrH)},
0x04600000, "04600000 add z0.h, z0.h, z0.h"},
{"add z0.s, z0.s, z0.s", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrS), ExtVector(0, ExtArrS), ExtVector(0, ExtArrS)},
0x04a00000, "04a00000 add z0.s, z0.s, z0.s"},
{"add z0.d, z0.d, z0.d", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrD), ExtVector(0, ExtArrD), ExtVector(0, ExtArrD)},
0x04e00000, "04e00000 add z0.d, z0.d, z0.d"},
{"sub z0.b, z0.b, z0.b", "SUB", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04200400, "04200400 sub z0.b, z0.b, z0.b"},
{"sub z0.b, z0.b, z3.b", "SUB", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(3, ExtArrB), ExtVector(0, ExtArrB)},
0x04230400, "04230400 sub z0.b, z0.b, z3.b"},
{"sqadd z0.b, z0.b, z0.b", "SQADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04201000, "04201000 sqadd z0.b, z0.b, z0.b"},
{"sqadd z0.b, z0.b, z3.b", "SQADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(3, ExtArrB), ExtVector(0, ExtArrB)},
0x04231000, "04231000 sqadd z0.b, z0.b, z3.b"},
{"sqadd z0.d, z0.d, z0.d", "SQADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrD), ExtVector(0, ExtArrD), ExtVector(0, ExtArrD)},
0x04e01000, "04e01000 sqadd z0.d, z0.d, z0.d"},
{"uqadd z0.b, z0.b, z0.b", "UQADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04201400, "04201400 uqadd z0.b, z0.b, z0.b"},
{"sqsub z0.b, z0.b, z0.b", "SQSUB", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04201800, "04201800 sqsub z0.b, z0.b, z0.b"},
{"sqsub z0.h, z0.h, z0.h", "SQSUB", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrH), ExtVector(0, ExtArrH), ExtVector(0, ExtArrH)},
0x04601800, "04601800 sqsub z0.h, z0.h, z0.h"},
{"uqsub z0.b, z0.b, z0.b", "UQSUB", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04201c00, "04201c00 uqsub z0.b, z0.b, z0.b"},
{"mul z0.b, z0.b, z0.b (sve2)", "MUL", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04206000, "04206000 mul z0.b, z0.b, z0.b"},
{"mul z17.b, z21.b, z27.b (sve2)", "MUL", ExtFormVectors,
[]ExtOperand{ExtVector(21, ExtArrB), ExtVector(27, ExtArrB), ExtVector(17, ExtArrB)},
0x043b62b1, "043b62b1 mul z17.b, z21.b, z27.b"},
{"mul z0.d, z0.d, z0.d (sve2)", "MUL", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrD), ExtVector(0, ExtArrD), ExtVector(0, ExtArrD)},
0x04e06000, "04e06000 mul z0.d, z0.d, z0.d"},
{"smulh z0.b, z0.b, z0.b (sve2)", "SMULH", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04206800, "04206800 smulh z0.b, z0.b, z0.b"},
{"smulh z17.b, z21.b, z27.b (sve2)", "SMULH", ExtFormVectors,
[]ExtOperand{ExtVector(21, ExtArrB), ExtVector(27, ExtArrB), ExtVector(17, ExtArrB)},
0x043b6ab1, "043b6ab1 smulh z17.b, z21.b, z27.b"},
{"umulh z0.b, z0.b, z0.b (sve2)", "UMULH", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
0x04206c00, "04206c00 umulh z0.b, z0.b, z0.b"},
{"umulh z17.b, z21.b, z27.b (sve2)", "UMULH", ExtFormVectors,
[]ExtOperand{ExtVector(21, ExtArrB), ExtVector(27, ExtArrB), ExtVector(17, ExtArrB)},
0x043b6eb1, "043b6eb1 umulh z17.b, z21.b, z27.b"},
// Governed destructive forms, merging: Zm, Pg/M, Zdn.
{"add z0.b, p0/m, z0.b", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04000000, "04000000 add z0.b, p0/m, z0.b, z0.b"},
{"add z0.b, p2/m, z0.b", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(2, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04000800, "04000800 add z0.b, p2/m, z0.b, z0.b"},
{"add z0.b, p7/m, z0.b", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(7, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04001c00, "04001c00 add z0.b, p7/m, z0.b, z0.b"},
{"add z31.b, p0/m, z31.b", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(31, ExtArrB)},
0x0400001f, "0400001f add z31.b, p0/m, z31.b, z0.b"},
{"add z0.b, p0/m, z0.b (zm 31)", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(31, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x040003e0, "040003e0 add z0.b, p0/m, z0.b, z31.b"},
{"add z0.s, p0/m, z0.s", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrS), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrS)},
0x04800000, "04800000 add z0.s, p0/m, z0.s, z0.s"},
{"sub z0.b, p0/m, z0.b", "SUB", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04010000, "04010000 sub z0.b, p0/m, z0.b, z0.b"},
{"sub z0.b, p7/m, z0.b", "SUB", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(7, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04011c00, "04011c00 sub z0.b, p7/m, z0.b, z0.b"},
{"sub z3.b, p0/m, z3.b", "SUB", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(3, ExtArrB)},
0x04010003, "04010003 sub z3.b, p0/m, z3.b, z0.b"},
{"subr z0.b, p0/m, z0.b", "SUBR", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04030000, "04030000 subr z0.b, p0/m, z0.b, z0.b"},
{"subr z0.h, p0/m, z0.h", "SUBR", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrH), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrH)},
0x04430000, "04430000 subr z0.h, p0/m, z0.h, z0.h"},
{"mul z0.b, p0/m, z0.b", "MUL", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04100000, "04100000 mul z0.b, p0/m, z0.b, z0.b"},
{"mul z0.b, p2/m, z0.b", "MUL", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(2, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04100800, "04100800 mul z0.b, p2/m, z0.b, z0.b"},
{"mul z0.b, p0/m, z0.b (zm 31)", "MUL", ExtFormPredicated,
[]ExtOperand{ExtVector(31, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x041003e0, "041003e0 mul z0.b, p0/m, z0.b, z31.b"},
{"smulh z0.b, p0/m, z0.b", "SMULH", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04120000, "04120000 smulh z0.b, p0/m, z0.b, z0.b"},
{"smulh z0.b, p2/m, z0.b", "SMULH", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(2, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04120800, "04120800 smulh z0.b, p2/m, z0.b, z0.b"},
{"smulh z0.s, p0/m, z0.s", "SMULH", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrS), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrS)},
0x04920000, "04920000 smulh z0.s, p0/m, z0.s, z0.s"},
{"umulh z0.b, p0/m, z0.b", "UMULH", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrB)},
0x04130000, "04130000 umulh z0.b, p0/m, z0.b, z0.b"},
// Immediate forms: imm{, LSL #8}, Zdn.
{"add z0.b, z0.b, #0", "ADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2520c000, "2520c000 add z0.b, z0.b, #0"},
{"add z0.b, z0.b, #127", "ADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(127), ExtVector(0, ExtArrB)},
0x2520cfe0, "2520cfe0 add z0.b, z0.b, #127"},
{"add z0.h, z0.h, #0, lsl #8", "ADD", ExtFormImmediate,
[]ExtOperand{ExtShiftedImmediate(0, 8), ExtVector(0, ExtArrH)},
0x2560e000, "2560e000 add z0.h, z0.h, #0, lsl #8"},
{"add z0.h, z0.h, #32512 (derived shift)", "ADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(32512), ExtVector(0, ExtArrH)},
0x2560efe0, "2560efe0 is sqsub's GNU word for #32512; the classes share the encoding"},
{"sub z0.b, z0.b, #0", "SUB", ExtFormImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2521c000, "2521c000 sub z0.b, z0.b, #0"},
{"subr z0.b, z0.b, #0", "SUBR", ExtFormImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2523c000, "2523c000 subr z0.b, z0.b, #0"},
{"subr z0.b, z0.b, #255", "SUBR", ExtFormImmediate,
[]ExtOperand{ExtImmediate(255), ExtVector(0, ExtArrB)},
0x2523dfe0, "2523dfe0 subr z0.b, z0.b, #255"},
{"subr z0.h, z0.h, #0, lsl #8", "SUBR", ExtFormImmediate,
[]ExtOperand{ExtShiftedImmediate(0, 8), ExtVector(0, ExtArrH)},
0x2563e000, "2563e000 subr z0.h, z0.h, #0, lsl #8"},
{"sqadd z0.b, z0.b, #0", "SQADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2524c000, "2524c000 sqadd z0.b, z0.b, #0"},
{"uqadd z0.b, z0.b, #0", "UQADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2525c000, "2525c000 uqadd z0.b, z0.b, #0"},
{"sqsub z0.b, z0.b, #0", "SQSUB", ExtFormImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2526c000, "2526c000 sqsub z0.b, z0.b, #0"},
{"sqsub z0.b, z0.b, #255", "SQSUB", ExtFormImmediate,
[]ExtOperand{ExtImmediate(255), ExtVector(0, ExtArrB)},
0x2526dfe0, "2526dfe0 sqsub z0.b, z0.b, #255"},
{"sqsub z0.h, z0.h, #0, lsl #8", "SQSUB", ExtFormImmediate,
[]ExtOperand{ExtShiftedImmediate(0, 8), ExtVector(0, ExtArrH)},
0x2566e000, "2566e000 sqsub z0.h, z0.h, #0, lsl #8"},
{"uqsub z0.b, z0.b, #0", "UQSUB", ExtFormImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2527c000, "2527c000 uqsub z0.b, z0.b, #0 (class vector from the GNU table and LLVM: sve_int_arith_imm0 opc 0b111)"},
{"mul z0.b, z0.b, #0", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrB)},
0x2530c000, "2530c000 mul z0.b, z0.b, #0"},
{"mul z0.b, z0.b, #127", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtImmediate(127), ExtVector(0, ExtArrB)},
0x2530cfe0, "2530cfe0 mul z0.b, z0.b, #127"},
{"mul z0.b, z0.b, #-128", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtImmediate(-128), ExtVector(0, ExtArrB)},
0x2530d000, "2530d000 mul z0.b, z0.b, #-128"},
{"mul z0.b, z0.b, #-1", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtImmediate(-1), ExtVector(0, ExtArrB)},
0x2530dfe0, "2530dfe0 mul z0.b, z0.b, #-1"},
{"mul z0.h, z0.h, #0", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtImmediate(0), ExtVector(0, ExtArrH)},
0x2570c000, "2570c000 mul z0.h, z0.h, #0"},
} {
in := extInstruction(t, tt.mnem, tt.form)
got, err := in.Encode(tt.ops)
if err != nil {
t.Errorf("%s: encode: %v", tt.name, err)
continue
}
if want := hex.EncodeToString(extWordLE(tt.want)); hex.EncodeToString(got) != want {
t.Errorf("%s:\n got %x\n want %s", tt.name, got, want)
}
}
}
// TestArm64ExtGoldenSources pins the cross-check contract: every encoding
// class in the table carries at least one GNU-assembler vector, so no class
// rests on transcription alone.
func TestArm64ExtGoldenSources(t *testing.T) {
classes := map[ExtForm]bool{}
for _, tt := range []struct {
mnem string
form ExtForm
}{
{"ADD", ExtFormVectors}, {"SUB", ExtFormVectors}, {"SQADD", ExtFormVectors},
{"UQADD", ExtFormVectors}, {"SQSUB", ExtFormVectors}, {"UQSUB", ExtFormVectors},
{"MUL", ExtFormVectors}, {"SMULH", ExtFormVectors}, {"UMULH", ExtFormVectors},
{"ADD", ExtFormPredicated}, {"SUB", ExtFormPredicated}, {"SUBR", ExtFormPredicated},
{"MUL", ExtFormPredicated}, {"SMULH", ExtFormPredicated}, {"UMULH", ExtFormPredicated},
{"ADD", ExtFormImmediate}, {"SUB", ExtFormImmediate}, {"SUBR", ExtFormImmediate},
{"SQADD", ExtFormImmediate}, {"UQADD", ExtFormImmediate},
{"SQSUB", ExtFormImmediate}, {"UQSUB", ExtFormImmediate},
{"MUL", ExtFormSignedImmediate},
} {
if _, ok := extInstructionQuiet(tt.mnem, tt.form); !ok {
t.Errorf("the table lacks %s with the %s form", tt.mnem, tt.form)
}
classes[tt.form] = true
}
for _, form := range []ExtForm{ExtFormVectors, ExtFormPredicated, ExtFormImmediate, ExtFormSignedImmediate} {
if !classes[form] {
t.Errorf("no golden vectors cover the %s form", form)
}
}
}
func extInstructionQuiet(mnem string, form ExtForm) (ExtInstr, bool) {
for _, in := range Extensions(ARM64) {
if in.Name == mnem && in.Form == form {
return in, true
}
}
return ExtInstr{}, false
}
// TestArm64ExtTableIntegrity checks the metadata contract: every entry names
// its manual reference, summary and feature, and the element-size field sits
// at bits 23..22 where the manual puts it for every class in the family.
func TestArm64ExtTableIntegrity(t *testing.T) {
for _, in := range Extensions(ARM64) {
if in.Name == "" || in.Summary == "" || in.Ref == "" {
t.Errorf("%+v: name, summary and reference are mandatory", in)
}
if in.Feature != ExtFeatureSVE && in.Feature != ExtFeatureSVE2 {
t.Errorf("%s: feature %q is neither sve nor sve2", in.Name, in.Feature)
}
if in.Form.Arity() < 2 || in.Form.Arity() > 3 {
t.Errorf("%s: form %d carries an unusable arity %d", in.Name, in.Form, in.Form.Arity())
}
if in.Size.Off != 22 || in.Size.Width != 2 {
t.Errorf("%s: the size field sits at bits %d..%d, the classes here put it at 23..22",
in.Name, in.Size.Off, in.Size.Off+in.Size.Width-1)
}
// The destination register field and the element-size field are
// operands everywhere in this family, so Word carries both zero; the
// class opcodes live around them and stay where they are.
if in.Word&0x1f != 0 || in.Word&(0x3<<22) != 0 {
t.Errorf("%s: word %08x carries destination or size bits, want them zero", in.Name, in.Word)
}
}
}
func TestArm64ExtRejects(t *testing.T) {
rgb := func(rs ...int) []ExtOperand {
ops := make([]ExtOperand, len(rs))
for i, r := range rs {
ops[i] = ExtVector(r, ExtArrB)
}
return ops
}
for _, tt := range []struct {
name string
mnem string
form ExtForm
ops []ExtOperand
quote string // a fragment the error carries
}{
{"no arrangement", "ADD", ExtFormVectors,
[]ExtOperand{{Kind: ExtZReg, Reg: 0}, ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
"no arrangement"},
{"mismatched arrangements", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrS), ExtVector(0, ExtArrB)},
"want .B"},
{"wrong arity", "ADD", ExtFormVectors, rgb(0, 0), "takes 3 operands"},
{"predicate in a vector position", "ADD", ExtFormVectors,
[]ExtOperand{ExtPredicate(0, ExtQualNone), ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
"scalable vector register"},
{"quadword arrangement has no size encoding", "ADD", ExtFormVectors,
[]ExtOperand{ExtVector(0, ExtArrQ), ExtVector(0, ExtArrQ), ExtVector(0, ExtArrQ)},
"no size encoding"},
{"zeroing qualifier", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualZeroing), ExtVector(0, ExtArrB)},
"/M"},
{"predicate beyond the 3-bit field", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(8, ExtQualMerging), ExtVector(0, ExtArrB)},
"P0-P7"},
{"predicate arrangement suffix", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtOperand{Kind: ExtPReg, Reg: 0, Qual: ExtQualMerging, Arr: ExtArrB}, ExtVector(0, ExtArrB)},
"arrangement"},
{"predicate operands disagree on arrangement", "ADD", ExtFormPredicated,
[]ExtOperand{ExtVector(0, ExtArrB), ExtPredicate(0, ExtQualMerging), ExtVector(0, ExtArrS)},
"must match"},
{"bare 256 on .B", "ADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(256), ExtVector(0, ExtArrB)},
"immediate 256"},
{"negative unsigned immediate", "ADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(-1), ExtVector(0, ExtArrB)},
"immediate -1"},
{"multiple of 256 beyond the imm8 span", "ADD", ExtFormImmediate,
[]ExtOperand{ExtImmediate(65536), ExtVector(0, ExtArrH)},
"immediate 65536"},
{"shift amount other than 0 or 8", "ADD", ExtFormImmediate,
[]ExtOperand{ExtShiftedImmediate(1, 4), ExtVector(0, ExtArrS)},
"0 or 8"},
{"shifted constant on .B", "ADD", ExtFormImmediate,
[]ExtOperand{ExtShiftedImmediate(1, 8), ExtVector(0, ExtArrB)},
".B takes no shift"},
{"register where the immediate belongs", "ADD", ExtFormImmediate,
[]ExtOperand{ExtVector(0, ExtArrB), ExtVector(0, ExtArrB)},
"wants an immediate"},
{"signed immediate over the top", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtImmediate(128), ExtVector(0, ExtArrB)},
"128"},
{"signed immediate under the floor", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtImmediate(-129), ExtVector(0, ExtArrB)},
"-129"},
{"shift in the signed class", "MUL", ExtFormSignedImmediate,
[]ExtOperand{ExtShiftedImmediate(1, 8), ExtVector(0, ExtArrB)},
"no shift"},
} {
in := extInstruction(t, tt.mnem, tt.form)
_, err := in.Encode(tt.ops)
if err == nil {
t.Errorf("%s: encode succeeded, want an error", tt.name)
continue
}
if !strings.Contains(err.Error(), tt.quote) {
t.Errorf("%s: error %q lacks %q", tt.name, err, tt.quote)
}
}
}
// TestExtensionsArchBinding pins the registry's architecture binding: the
// extended layer exists for arm64 alone until an amd64 table attaches, and no
// other architecture sees a single SVE instruction.
func TestExtensionsArchBinding(t *testing.T) {
for _, a := range []Arch{AMD64, RISCV, LOONG64, Unknown} {
if got := Extensions(a); len(got) != 0 {
t.Errorf("Extensions(%s) carries %d instructions, want none", a, len(got))
}
}
if got := Extensions(ARM64); len(got) == 0 {
t.Error("Extensions(ARM64) is empty")
}
}
+159
View File
@@ -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()
}
+197
View File
@@ -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)
}
}