From 5a8e9acbf30e8dd0b00450de7acea1ee7098d1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Fri, 2 Oct 2026 17:58:55 +0200 Subject: [PATCH] feat(arch): add the extended-instruction layer with SVE arithmetic --- arch/arm64_ext.go | 624 +++++++++++++++++++++++++++++++++++++++++ arch/arm64_ext_test.go | 400 ++++++++++++++++++++++++++ asm/extension.go | 159 +++++++++++ asm/extension_test.go | 197 +++++++++++++ 4 files changed, 1380 insertions(+) create mode 100644 arch/arm64_ext.go create mode 100644 arch/arm64_ext_test.go create mode 100644 asm/extension.go create mode 100644 asm/extension_test.go diff --git a/arch/arm64_ext.go b/arch/arm64_ext.go new file mode 100644 index 0000000..fdeddc6 --- /dev/null +++ b/arch/arm64_ext.go @@ -0,0 +1,624 @@ +// Copyright (c) 2026 Petr Balvín (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))< 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 + } +} diff --git a/arch/arm64_ext_test.go b/arch/arm64_ext_test.go new file mode 100644 index 0000000..9cfc9d0 --- /dev/null +++ b/arch/arm64_ext_test.go @@ -0,0 +1,400 @@ +// Copyright (c) 2026 Petr Balvín (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") + } +} diff --git a/asm/extension.go b/asm/extension.go new file mode 100644 index 0000000..bc1f569 --- /dev/null +++ b/asm/extension.go @@ -0,0 +1,159 @@ +// Copyright (c) 2026 Petr Balvín (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() +} diff --git a/asm/extension_test.go b/asm/extension_test.go new file mode 100644 index 0000000..15dc2b7 --- /dev/null +++ b/asm/extension_test.go @@ -0,0 +1,197 @@ +// 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" +) + +// 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) + } +}