feat(arch): the SVE predicate family in the extended layer
Assisted-by: GLM 5.3 Flash
This commit is contained in:
1 parent
83052ab466
commit
8231302bca
5 files changed
+1451
-42
No files matched your search
+108
-18
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user