feat(arch): the SVE predicate family in the extended layer

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 02:26:19 +02:00
1 parent 83052ab466
commit 8231302bca
5 files changed
+1451 -42

No files matched your search

+108 -18
View File
@@ -11,7 +11,11 @@
//
// The spellings are the layer's own Plan 9 forms, the ones its metadata
// documents: Zn, Zm, Zd for the unpredicated three-vector class, Zm, Pg/M,
// Zdn for the predicated class, imm{, LSL #8}, Zdn for the immediate classes.
// Zdn for the predicated class, imm{, LSL #8}, Zdn for the immediate
// classes, and for the predicate family Pm.B, Pn.B, Pg/Z (or Pg.Z), Pd.B
// for the logical operations, Pn.B, Pg.Z, Pd.B for the breaks, Pm.T, Pn.T,
// Pd.T for the permutations, Rm, Rn, Pd.T for the while compares, PN8-PN15
// for the counter destinations, and the bare SETFFR.
package asm
@@ -27,14 +31,16 @@ import (
// arm64ExtStatement converts one instruction's operands into the extended
// layer's operand form. pinned reports that the statement belongs to the
// layer: the mnemonic is registered in the registry and the operand list
// carries at least one scalable vector or predicate register. A pinned
// statement can only encode through the layer, so every operand is read
// here and its diagnostic replaces whatever the scalar paths would have
// said about operands they cannot read; err is non-nil for a pinned
// statement whose operands the layer refuses, and extops is complete only
// when err is nil. Unpinned means the statement is nobody's: the caller
// falls through to the ordinary arm64 encoders, which keep their exact
// behaviour for every scalar, NEON and FP operand list.
// carries at least one scalable vector, predicate or predicate-as-counter
// register, or no operands at all (the zero-operand forms such as SETFFR,
// which no scalar path could mean instead). A pinned statement can only
// encode through the layer, so every operand is read here and its
// diagnostic replaces whatever the scalar paths would have said about
// operands they cannot read; err is non-nil for a pinned statement whose
// operands the layer refuses, and extops is complete only when err is nil.
// Unpinned means the statement is nobody's: the caller falls through to the
// ordinary arm64 encoders, which keep their exact behaviour for every
// scalar, NEON and FP operand list.
func arm64ExtStatement(mnem string, ops []*ast.Operand) (extops []arch.ExtOperand, pinned bool, err error) {
if _, ok := LookupExtension(arch.ARM64, mnem); !ok {
return nil, false, nil
@@ -77,18 +83,31 @@ func arm64ExtStatement(mnem string, ops []*ast.Operand) (extops []arch.ExtOperan
out = append(out, ext)
continue
}
return nil, true, fmt.Errorf("%s: operand %d (%s) is not an extended-layer operand: want a scalable vector register, a predicate register or an immediate", mnem, i+1, op.Raw)
if ext, ok := arm64ExtCounter(text); ok {
out = append(out, ext)
continue
}
if ext, ok := arm64ExtGeneral(text); ok {
out = append(out, ext)
continue
}
return nil, true, fmt.Errorf("%s: operand %d (%s) is not an extended-layer operand: want a scalable vector, predicate, general or counter register, or an immediate", mnem, i+1, op.Raw)
}
return out, true, nil
}
// arm64ExtPinned reports whether any operand is a scalable vector or
// predicate register, the shapes only the extension layer reads. The test
// arm64ExtPinned reports whether the statement belongs to the layer: any
// operand is a scalable vector, predicate or predicate-as-counter register,
// the shapes only the extension layer reads, or the statement carries no
// operands at all and the mnemonic's zero-operand forms claim it. The test
// is deliberately loose about the suffixes: P0/B is not a spelling the
// layer takes, but the P of it makes the statement the layer's, and the
// conversion then diagnoses the operand precisely instead of leaving it to
// a scalar path that would report an unrelated register error.
func arm64ExtPinned(ops []*ast.Operand) bool {
if len(ops) == 0 {
return true
}
for _, op := range ops {
if op.Kind == ast.OpImmediate {
continue
@@ -104,16 +123,20 @@ func arm64ExtPinned(ops []*ast.Operand) bool {
return false
}
// arm64ExtPredicateShape reports whether text spells a predicate register at
// all: P, digits, an optional arrangement suffix and an optional qualifier
// after a slash, whatever the qualifier says. The strict parse in
// arm64ExtPredicate judges the suffix; this shape only decides who the
// operand belongs to.
// arm64ExtPredicateShape reports whether text spells a predicate or
// predicate-as-counter register at all: PN or P, digits, an optional
// arrangement suffix and an optional qualifier after a slash, whatever the
// qualifier says. The strict parses in arm64ExtPredicate and
// arm64ExtCounter judge the suffix; this shape only decides who the operand
// belongs to.
func arm64ExtPredicateShape(text string) bool {
if text == "" || text[0] != 'P' {
return false
}
text = text[1:]
if rest, found := strings.CutPrefix(text, "N"); found {
text = rest
}
if i := strings.IndexByte(text, '/'); i >= 0 {
text = text[:i]
}
@@ -160,7 +183,8 @@ func arm64ExtVector(text string) (arch.ExtOperand, bool) {
}
// arm64ExtPredicate parses a predicate register operand: P0..P15 with an
// optional element-size suffix and an optional qualifier, P0/M, P0.Z, P0.B/M.
// optional element-size suffix (P0.B) and an optional qualifier in either
// spelling the corpus and the wired forms use, P0/M and P0.Z.
func arm64ExtPredicate(text string) (arch.ExtOperand, bool) {
qual := arch.ExtQualNone
if base, suffix, found := strings.Cut(text, "/"); found {
@@ -173,6 +197,15 @@ func arm64ExtPredicate(text string) (arch.ExtOperand, bool) {
return arch.ExtOperand{}, false
}
text = base
} else if base, suffix, found := strings.Cut(text, "."); found &&
(suffix == "Z" || suffix == "M") {
// The dot qualifier stands in place of an arrangement, the spelling
// the toolchain's corpus writes (P1.Z, P14.M).
qual = arch.ExtQualMerging
if suffix == "Z" {
qual = arch.ExtQualZeroing
}
text = base
}
reg, arr, ok := arm64ExtReg(text, 'P')
if !ok {
@@ -181,6 +214,63 @@ func arm64ExtPredicate(text string) (arch.ExtOperand, bool) {
return arch.ExtOperand{Kind: arch.ExtPReg, Reg: reg, Arr: arr, Qual: qual}, true
}
// arm64ExtCounter parses a predicate-as-counter register operand: PN8..PN15
// with an optional element-size suffix, PN14.S. The register range is the
// counter range the layer's convention carries; the encoding validates it.
func arm64ExtCounter(text string) (arch.ExtOperand, bool) {
rest, ok := strings.CutPrefix(text, "PN")
if !ok {
return arch.ExtOperand{}, false
}
reg, arr, ok := arm64ExtRegDigits(rest)
if !ok {
return arch.ExtOperand{}, false
}
return arch.ExtOperand{Kind: arch.ExtPNReg, Reg: reg, Arr: arr}, true
}
// arm64ExtGeneral parses a general register operand: R0..R30, the plain
// spelling the while-compare forms take. The register range is left to the
// encoding, whose diagnostics name it.
func arm64ExtGeneral(text string) (arch.ExtOperand, bool) {
rest, ok := strings.CutPrefix(text, "R")
if !ok {
return arch.ExtOperand{}, false
}
reg, arr, ok := arm64ExtRegDigits(rest)
if !ok || arr != arch.ExtArrNone {
return arch.ExtOperand{}, false
}
return arch.ExtOperand{Kind: arch.ExtGReg, Reg: reg}, true
}
// arm64ExtRegDigits parses the digits and optional arrangement suffix of a
// register spelling once the letter prefix is gone.
func arm64ExtRegDigits(text string) (reg int, arr arch.ExtArrangement, ok bool) {
if base, suffix, found := strings.Cut(text, "."); found {
switch suffix {
case "B":
arr = arch.ExtArrB
case "H":
arr = arch.ExtArrH
case "S":
arr = arch.ExtArrS
case "D":
arr = arch.ExtArrD
case "Q":
arr = arch.ExtArrQ
default:
return 0, 0, false
}
text = base
}
n, err := strconv.Atoi(text)
if err != nil || n < 0 {
return 0, 0, false
}
return n, arr, true
}
// arm64ExtReg parses Pn or Zn with an optional arrangement suffix off a
// normalised operand text. The register range is left to the encoding: the
// layer's own diagnostics name the range a form carries.
+143 -1
View File
@@ -119,7 +119,7 @@ func TestArm64AssembleExtensionRefusals(t *testing.T) {
{"ADD Z1.S, P9/M, Z0.S", "outside P0-P7"},
{"ADD $300, Z0.S", "immediate 300"},
{"ADD $255<<8, Z0.S", "not an immediate the layer can read"},
{"ADD Z1.S, P0/M, R0", "not an extended-layer operand"},
{"ADD Z1.S, P0/M, R0", "wants a scalable vector register"},
{"ADD Z1.S, P0/B, Z0.S", "not an extended-layer operand"},
{"MUL $200, Z0.B", "outside the signed 8-bit range"},
{"ADD Z1.S, LSL #8, Z0.S", "LSL belongs straight after an immediate"},
@@ -132,6 +132,148 @@ func TestArm64AssembleExtensionRefusals(t *testing.T) {
}
}
// TestArm64AssembleExtensionPredicateGolden drives the predicate family
// through the full assembler: corpus spellings in, corpus words out. The
// want words are the same arm64sveenc.s lines the arch-level golden test
// pins; these prove the text-to-bytes path parses the dot and slash
// qualifiers, the general registers and the counter spelling the statements
// write.
func TestArm64AssembleExtensionPredicateGolden(t *testing.T) {
tests := []struct {
stmt string
want uint32
}{
// The logical operations, .B alone, the governing predicate with the
// zeroing qualifier in its dot spelling.
{"PAND P4.B, P2.B, P1.Z, P14.B", 0x2504444e},
{"PANDS P4.B, P2.B, P1.Z, P14.B", 0x2544444e},
{"PBIC P4.B, P2.B, P1.Z, P14.B", 0x2504445e},
{"PEOR P4.B, P2.B, P1.Z, P14.B", 0x2504464e},
{"PNAND P4.B, P2.B, P1.Z, P14.B", 0x2584465e},
{"PORR P4.B, P2.B, P1.Z, P14.B", 0x2584444e},
// The slash qualifier spells the same operand the dot spelling does.
{"PAND P4.B, P2.B, P1/Z, P14.B", 0x2504444e},
// The select and the breaks.
{"PSEL P4.B, P2.B, P1, P14.B", 0x2504465e},
{"PBRKA P5.B, P9.Z, P2.B", 0x251064a2},
{"PBRKAS P5.B, P9.Z, P4.B", 0x255064a4},
{"PBRKN P4.B, P2.B, P1.Z, P4.B", 0x25184444},
{"PBRKPA P4.B, P2.B, P1.Z, P14.B", 0x2504c44e},
// The permutations carry the arrangement into the size field.
{"PTRN1 P5.D, P4.D, P2.D", 0x05e55082},
{"PUZP2 P5.D, P4.D, P2.D", 0x05e54c82},
{"PZIP1 P5.H, P4.H, P2.H", 0x05654082},
// The singles and the first-fault group.
{"PPFALSE P13.B", 0x2518e40d},
{"PPFIRST P5.B, P9, P5.B", 0x2558c125},
{"PPNEXT P5.D, P4, P5.D", 0x25d9c485},
{"PPTEST P14.B, P0", 0x2550c1c0},
{"PPUNPKHI P14.B, P0.H", 0x053141c0},
{"PRDFFR P13.B", 0x2519f00d},
{"PRDFFR P14.Z, P0.B", 0x2518f1c0},
{"PRDFFRS P14.Z, P0.B", 0x2558f1c0},
{"PWRFFR P13.B", 0x252891a0},
{"PREV P14.S, P13.S", 0x05b441cd},
{"SETFFR", 0x252c9000},
// The while compares: general registers in, a sized predicate out.
{"PWHILEGE R2, R10, P10.H", 0x2562114a},
{"PWHILELT R2, R10, P10.H", 0x2562154a},
{"PWHILELS R2, R10, P10.B", 0x25221d5a},
{"PWHILERW R2, R10, P10.H", 0x2562315a},
}
for _, tt := range tests {
words := assembleArm64Words(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if len(words) != 2 {
t.Fatalf("%s: got %d words, want the statement and RET", tt.stmt, len(words))
}
if words[0] != tt.want {
t.Errorf("%s:\n got %08x\n want %08x", tt.stmt, words[0], tt.want)
}
if words[1] != 0xd65f03c0 {
t.Errorf("%s: RET encoded %08x", tt.stmt, words[1])
}
}
}
// TestArm64AssembleExtensionPredicateRefusals pins the diagnostics the
// predicate statements get from the layer.
func TestArm64AssembleExtensionPredicateRefusals(t *testing.T) {
tests := []struct {
stmt string
want string
}{
{"PAND P4.B, P2.B, P1.M, P14.B", "zeroing qualifier"},
{"PAND P4.B, P2.B, P9.Z, P14.B", "outside P0-P7"},
{"PAND P8.B, P2.B, P1.Z, P14.B", "outside P0-P7"},
{"PAND P4.S, P2.B, P1.Z, P14.B", "want .B"},
{"PBRKN P4.B, P2.B, P1.Z, P9.B", "same register Pdm"},
{"PBRKAS P5.B, P9.M, P4.B", "zeroing qualifier"},
{"PSEL P4.B, P2.B, P1.Z, P14.B", "takes no qualifier"},
{"PTRN1 P5.D, P4.S, P2.D", "want .D"},
{"PPFALSE P13.S", "want .B"},
{"PPTRUE P6.S", "predicate-as-counter register"},
{"PPTRUE PN6.S", "outside PN8-PN15"},
{"PWHILELT R2, R31, P10.H", "outside R0-R30"},
{"PWHILELT R2, R10, P10.Q", "no size encoding"},
{"SETFFR P0.B", "takes 0 operands"},
}
for _, tt := range tests {
got := assembleArm64SrcError(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if !strings.Contains(got, tt.want) {
t.Errorf("%s: error %q does not name %q", tt.stmt, got, tt.want)
}
}
}
// TestArm64AssembleExtensionPredicateLeavesScalarsAlone pins the non-
// invasion promise across the new operand kinds: general-register
// statements whose mnemonics the layer also carries for predicates keep
// their scalar behaviour whenever no vector, predicate or counter operand
// appears.
func TestArm64AssembleExtensionPredicateLeavesScalarsAlone(t *testing.T) {
tests := []struct {
stmt string
want uint32
}{
{"ADD R0, R1, R2", 0x8b000022},
{"SUB R0, R1, R2", 0xcb000022},
}
for _, tt := range tests {
words := assembleArm64Words(t, arm64ExtProbeHead+"\t"+tt.stmt+"\n\tRET\n")
if len(words) != 2 {
t.Fatalf("%s: got %d words, want the statement and RET", tt.stmt, len(words))
}
if words[0] != tt.want {
t.Errorf("%s:\n got %08x\n want %08x", tt.stmt, words[0], tt.want)
}
}
}
// TestArm64AssembleExtensionPredicateLabelOffsets proves pass 1 and pass 2
// agree on a function mixing the predicate family with the vector family:
// PWHILELT (4 bytes) and SETFFR (4 bytes) shift the label by exactly the
// words pass 2 lays down.
func TestArm64AssembleExtensionPredicateLabelOffsets(t *testing.T) {
src := arm64ExtProbeHead + `
PWHILELT R2, R10, P10.H
loop:
SETFFR
B loop
PPFALSE P13.B
RET
`
words := assembleArm64Words(t, src)
want := []uint32{0x2562154a, 0x252c9000, 0x17ffffff, 0x2518e40d, 0xd65f03c0}
if len(words) != len(want) {
t.Fatalf("got %d words, want %d", len(words), len(want))
}
for i := range want {
if words[i] != want[i] {
t.Errorf("word %d: got %08x, want %08x", i, words[i], want[i])
}
}
}
// TestArm64AssembleExtensionLabelOffsets proves pass 1 and pass 2 agree on a
// function that mixes the layer with ordinary statements: the label after an
// SVE instruction lands on the 4 bytes the encoder laid down, and the branch
+19 -3
View File
@@ -205,12 +205,28 @@ func TestExtensionArchIsolation(t *testing.T) {
// 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"}
want := []string{
// The SVE integer add/subtract/multiply family.
"ADD", "SUB", "SQADD", "UQADD", "SQSUB", "UQSUB", "MUL", "SMULH", "UMULH", "SUBR",
// The SVE and SVE2.1 predicate family: the logical operations, the
// breaks, the permutations, the singles, the first-fault group and
// the while compares.
"PAND", "PANDS", "PBIC", "PBICS", "PEOR", "PEORS",
"PNAND", "PNANDS", "PNOR", "PNORS", "PORN", "PORNS", "PORR", "PORRS",
"PSEL",
"PBRKA", "PBRKAS", "PBRKB", "PBRKBS", "PBRKN", "PBRKNS",
"PBRKPA", "PBRKPAS", "PBRKPB", "PBRKPBS",
"PTRN1", "PTRN2", "PUZP1", "PUZP2", "PZIP1", "PZIP2",
"PPFALSE", "PPFIRST", "PPNEXT", "PPTEST", "PPTRUE", "PPUNPKHI", "PPUNPKLO",
"PRDFFR", "PRDFFRS", "PWRFFR", "PREV", "SETFFR",
"PWHILEGE", "PWHILEGT", "PWHILEHI", "PWHILEHS",
"PWHILELE", "PWHILELO", "PWHILELS", "PWHILELT", "PWHILERW", "PWHILEWR",
}
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)
if n := len(arch.Extensions(arch.ARM64)); n != 77 {
t.Errorf("the family registers %d instructions, want 77", n)
}
}