Source file src/simd/archsimd/_gen/simdgen/sve/instruction.go
1 // Copyright 2026 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package sve loads ARM64 SVE / SVE2 instruction definitions from the ARM A64 6 // ISA XML files and emits them as simdgen unify values. 7 // TODO: merge with the arm64 package, the approach taken here should take over 8 // the NEON loader. 9 // TODO: merge with x/arch/arm64/instgen? 10 // 11 // SVE registers are "scalable": their total bit width is the hardware 12 // implementation-defined vector length rather than a fixed 128/256/512 bits. So 13 // emitted vector operands carry only a base type and an element width, without a 14 // fixed bits/lanes count. 15 // 16 // Arrangement is per-operand. An SVE instruction template such as 17 // 18 // ADD <Zdn>.<T>, <Pg>/M, <Zdn>.<T>, <Zm>.<T> 19 // 20 // stands for a family of concrete instructions, one per value of the <T> 21 // arrangement symbol. simdgen enumerates them by resolving each operand's 22 // arrangement symbol from the section's explanations. Different symbols can be 23 // encoded in the same instruction field but interpreted differently, the 24 // loader also takes care of this. 25 // 26 // It emits register, mask, immediate, memory and special operands. 27 // Memory and special operands are opaque at this moment. 28 // Register-list operands are not modeled yet, except for single-register lists, 29 // so instructions carrying one are skipped (TODO); see classify. 30 // 31 // TODO: Peepholes might need the structure of memory operands, implement it? 32 // TODO: special operands are like registers with indexing, prefetch ops, etc. 33 // They seem too specialized that we might want to manually implment them instead 34 // of via simdgen, but we can revisit this. 35 package sve 36 37 import ( 38 "fmt" 39 "regexp" 40 "strings" 41 42 "golang.org/x/arch/arm64/instgen/xmlspec" 43 ) 44 45 // signedImmRe matches an [Instruction.brief] that describes a signed/unsigned *immediate* 46 // (e.g. DUP/CPY "Move signed integer immediate ..."). There the signedness is a 47 // property of the immediate encoding, not of the vector lane, so such ops are 48 // signedness-agnostic. 49 var signedImmRe = regexp.MustCompile(`(un)?signed(\s+\w+)?\s+immediate`) 50 51 // reZReg and rePReg detect a Z (scalable vector) or P (predicate) register in an 52 // assembly template, used to choose the Go opcode prefix (see goOpPrefix). The 53 // [^/] guard excludes the /<ZM> predication qualifier, which is not a Z register. 54 // Copied from x/arch/arm64/instgen/xmlspec. 55 var ( 56 reZReg = regexp.MustCompile(`(^|[^/])<Z[A-Za-z1-9]+>`) 57 rePReg = regexp.MustCompile(`<P[A-Za-z1-9]+>`) 58 ) 59 60 // Instruction is a *logical* SVE instruction, one per iclass. 61 type Instruction struct { 62 xmlspec.Instruction 63 // iclass is the specific class this logical instruction represents. 64 // A raw xmlspec.Instruction can hold several iclasses with distinct mnemonics. 65 // If nil, the first iclass is used. 66 iclass *xmlspec.Iclass 67 mnemonicCache string 68 } 69 70 // ic returns the iclass this logical instruction represents, defaulting to the 71 // first iclass of the section. 72 func (inst *Instruction) ic() *xmlspec.Iclass { 73 if inst.iclass != nil { 74 return inst.iclass 75 } 76 if len(inst.Classes.Iclass) > 0 { 77 return &inst.Classes.Iclass[0] 78 } 79 return nil 80 } 81 82 // extractDocVar returns the value of the named docvar, searching from most to 83 // least specific: this iclass, its encodings, then the section top level. 84 func (inst *Instruction) extractDocVar(key string) string { 85 if ic := inst.ic(); ic != nil { 86 for _, dv := range ic.DocVars { 87 if dv.Key == key { 88 return dv.Value 89 } 90 } 91 for _, enc := range ic.Encodings { 92 for _, dv := range enc.DocVars { 93 if dv.Key == key { 94 return dv.Value 95 } 96 } 97 } 98 } 99 for _, dv := range inst.DocVars { 100 if dv.Key == key { 101 return dv.Value 102 } 103 } 104 return "" 105 } 106 107 // mnemonic returns the instruction mnemonic, e.g. "ADD", "FADD", "SQADD". 108 func (inst *Instruction) mnemonic() string { 109 if inst.mnemonicCache != "" { 110 return inst.mnemonicCache 111 } 112 m := inst.extractDocVar("mnemonic") 113 if inst.isAlias() { 114 m = inst.extractDocVar("alias_mnemonic") 115 } 116 inst.mnemonicCache = m 117 return m 118 } 119 120 // isAlias reports whether this XML entry describes an alias of another 121 // instruction. 122 func (inst *Instruction) isAlias() bool { 123 return inst.Type == "alias" 124 } 125 126 // instrClass returns the instruction class docvar, e.g. "sve" or "sve2". 127 func (inst *Instruction) instrClass() string { 128 return inst.extractDocVar("instr-class") 129 } 130 131 // isSVE reports whether this is an SVE or SVE2 instruction. 132 func (inst *Instruction) isSVE() bool { 133 switch inst.instrClass() { 134 case "sve", "sve2": 135 return true 136 } 137 return false 138 } 139 140 // cpuFeature returns the simdgen cpuFeature string for this instruction. 141 func (inst *Instruction) cpuFeature() string { 142 switch inst.instrClass() { 143 case "sve2": 144 return "SVE2" 145 default: 146 return "SVE" 147 } 148 } 149 150 // goOpPrefix returns the Go opcode prefix: "Z" if the instruction uses a 151 // scalable vector register, else "P" if it uses a predicate register, else "". 152 // So the Go opcode is goOpPrefix()+mnemonic, e.g. ZADD but PPTRUE. Matches 153 // x/arch/arm64/instgen/xmlspec.goOpcodePrefix. 154 func (inst *Instruction) goOpPrefix() string { 155 ic := inst.ic() 156 if ic == nil { 157 return "" 158 } 159 hasZ, hasP := false, false 160 for _, enc := range ic.Encodings { 161 s := asmTemplateToString(enc.AsmTemplate) 162 hasZ = hasZ || reZReg.MatchString(s) 163 hasP = hasP || rePReg.MatchString(s) 164 } 165 switch { 166 case hasZ: 167 return "Z" 168 case hasP: 169 return "P" 170 default: 171 return "" 172 } 173 } 174 175 // laneIsFloat reports whether the given operand's vector lane holds 176 // floating-point values. 177 // 178 // The int<->float conversions have different lane types on input and output, and the 179 // operand's role selects which side this is: 180 // 181 // - int->float (SCVTF/SCVTFLT, UCVTF/UCVTFLT): destination float, source int. 182 // - float->int (FCVTZS/FCVTZU and narrowing, FLOGB): destination int, source 183 // float. 184 // 185 // Every other instruction is uniform, i.e. all lanes the same type. 186 func (inst *Instruction) laneIsFloat(op *Operand) bool { 187 switch op.Class { 188 case "vreg", "greg": 189 // has a lane 190 default: 191 // mask lanes are always integer; mem/immediate/special have no lane. 192 return false 193 } 194 dst := op.role == "destination" 195 switch inst.mnemonic() { 196 case "SCVTF", "SCVTFLT", "UCVTF", "UCVTFLT": // integer -> floating point 197 return dst 198 case "FCVTZS", "FCVTZSN", "FCVTZU", "FCVTZUN", "FLOGB": // floating point -> integer 199 return !dst 200 } 201 return isFloatBrief(inst.brief()) 202 } 203 204 // isFloatBrief reports whether a brief description names a floating-point type. 205 // SVE spells these as "floating-point", "bfloat", or an "X-precision" (half / 206 // single / double / 8-bit) qualifier. 207 func isFloatBrief(brief string) bool { 208 b := strings.ToLower(brief) 209 return strings.Contains(b, "floating-point") || 210 strings.Contains(b, "bfloat") || 211 strings.Contains(b, "precision") 212 } 213 214 // signedness reports whether an integer instruction interprets its lanes as 215 // signed, unsigned, or agnostic, so the loader emits only the signedness the 216 // hardware actually implements, not spurious values. Many low-half/bitwise 217 // ops, e.g. ADD, SUB, MUL, EOR, etc., are genuinely agnostic. 218 // others are signedness-specific, e.g. SMAX vs UMAX, SDIV vs UDIV, 219 // the int<->float converts, etc. 220 // 221 // The signal is the instruction's brief description, which names the signedness 222 // for the specific ops ("Signed maximum", "Unsigned divide", "Signed integer 223 // convert ...") and omits it for the agnostic ones. 224 // 225 // Two adjustments: a brief describing a signed/unsigned *immediate* 226 // (DUP/CPY) is about the immediate, not the lane, so it stays agnostic; and the 227 // shift-right family and FLOGB name their signedness differently (arithmetic vs 228 // logical shift; "logarithm as integer") and are handled explicitly. 229 func (inst *Instruction) signedness() string { 230 switch inst.mnemonic() { 231 case "ASR", "ASRD", "ASRR", "FLOGB": // arithmetic (sign-propagating) / signed exponent 232 return "int" 233 case "LSR", "LSRR": // logical (zero-filling) shift right 234 return "uint" 235 } 236 b := strings.ToLower(inst.brief()) 237 if signedImmRe.MatchString(b) { 238 return "" 239 } 240 switch { 241 case strings.Contains(b, "unsigned"): 242 return "uint" 243 case strings.Contains(b, "signed"): // "unsigned" already handled, so this is the word "signed" 244 return "int" 245 } 246 return "" 247 } 248 249 // integerSignedness returns the signed/unsigned base variants to enumerate for 250 // the instruction's integer lanes: the single value fixed by signedness for a 251 // signedness-specific op, both {"int","uint"} for an agnostic op with an integer 252 // lane (simdgen narrows later via the Go op definitions), or a single no-op pass 253 // when there are no integer lanes. 254 func (inst *Instruction) integerSignedness(ops []Operand) []string { 255 switch inst.signedness() { 256 case "int": 257 return []string{"int"} 258 case "uint": 259 return []string{"uint"} 260 } 261 for i := range ops { 262 if c := ops[i].Class; (c == "vreg" || c == "greg") && !inst.laneIsFloat(&ops[i]) { 263 return []string{"int", "uint"} 264 } 265 } 266 return []string{""} 267 } 268 269 // brief returns the instruction's short human-readable description, e.g. "Signed 270 // maximum (predicated)". 271 func (inst *Instruction) brief() string { 272 if len(inst.Desc.Brief.Para) > 0 { 273 return strings.TrimSpace(inst.Desc.Brief.Para[0].Text) 274 } 275 return "" 276 } 277 278 // findExplanation returns the explanation whose symbol is encoded with the 279 // given link, or nil. 280 func (inst *Instruction) findExplanation(link string) *xmlspec.Explanation { 281 for i := range inst.Explanations.Explanations { 282 if inst.Explanations.Explanations[i].Symbol.Link == link { 283 return &inst.Explanations.Explanations[i] 284 } 285 } 286 return nil 287 } 288 289 // arngRow is one row of an arrangement size table: the encoding value of the 290 // size field and the resulting element width in bits. 291 type arngRow struct { 292 size string // the size bitfield value, e.g. "01"; the shared key across symbols 293 bits int // element width for this size (8/16/32/64) 294 } 295 296 // resolveArrangementTable returns the (size -> element width) rows for the 297 // arrangement symbol encoded with the given link, read from its definition 298 // table in encoding order. RESERVED and header rows (no valid element letter) 299 // are dropped. 300 // 301 // Crucially, the size key is the shared encoding field, so different symbols 302 // (<T> and <Tb>) that select on the same field line up by size. That is what 303 // lets non-uniform (widening/narrowing) instructions like SUNPKHI give each 304 // operand its own element width for the same encoded instruction. 305 func (inst *Instruction) resolveArrangementTable(link string) []arngRow { 306 exp := inst.findExplanation(link) 307 if exp == nil { 308 return nil 309 } 310 var rows []arngRow 311 for i, row := range exp.Definition.Table.TGroup.TBody.Row { 312 var size string 313 bits := 0 314 for _, entry := range row.Entries { 315 switch entry.Class { 316 case "bitfield": 317 size = strings.TrimSpace(entry.Value) 318 case "symbol": 319 bits = elemLetterBits(strings.TrimSpace(entry.Value)) 320 } 321 } 322 if bits == 0 { 323 continue // header or RESERVED row 324 } 325 if size == "" { 326 size = fmt.Sprintf("#%d", i) // single-column table: key by position 327 } 328 rows = append(rows, arngRow{size: size, bits: bits}) 329 } 330 return rows 331 } 332 333 // arngLinks returns the distinct arrangement-symbol links used by ops, with the 334 // destination's link first (it is the primary size driver), preserving order. 335 func arngLinks(ops []Operand) []string { 336 seen := map[string]bool{} 337 var links []string 338 add := func(l string) { 339 if l != "" && !seen[l] { 340 seen[l] = true 341 links = append(links, l) 342 } 343 } 344 for _, op := range ops { 345 if op.role == "destination" { 346 add(op.arngLink) 347 } 348 } 349 for _, op := range ops { 350 add(op.arngLink) 351 } 352 return links 353 } 354 355 // elemLetterBits maps an SVE element specifier letter to its bit width. 356 func elemLetterBits(letter string) int { 357 switch letter { 358 case "B": 359 return 8 360 case "H": 361 return 16 362 case "S": 363 return 32 364 case "D": 365 return 64 366 default: 367 return 0 368 } 369 } 370 371 // elemLetter is the inverse of elemLetterBits: it maps a bit width to its SVE 372 // element specifier letter (used as the arrangement in emitted defs). 373 func elemLetter(bits int) string { 374 switch bits { 375 case 8: 376 return "B" 377 case 16: 378 return "H" 379 case 32: 380 return "S" 381 case 64: 382 return "D" 383 default: 384 return "" 385 } 386 } 387 388 // allEncodingOperands returns the operand list of every distinct encoding of this iclass. 389 func (inst *Instruction) allEncodingOperands() [][]Operand { 390 ic := inst.ic() 391 if ic == nil { 392 return nil 393 } 394 seen := map[string]bool{} 395 var out [][]Operand 396 for _, enc := range ic.Encodings { 397 s := asmTemplateToString(enc.AsmTemplate) 398 if s == "" || seen[s] { 399 continue 400 } 401 seen[s] = true 402 if ops := operandsFromTextA(enc.AsmTemplate.TextA); len(ops) > 0 { 403 inst.fixMemoryDirection(ops) 404 out = append(out, ops) 405 } 406 } 407 return out 408 } 409 410 // fixMemoryDirection re-roles a load/store's data direction, which the operand 411 // order does not reveal on its own. A store's destination is its memory operand 412 // (unusually, at the end of the syntax, e.g. ST1B {<Zt>.<T>}, <Pg>, [<Xn|SP>]); 413 // a load's destination is the transferred vector register (the memory is then a 414 // source). Load/store is read from the brief description. 415 func (inst *Instruction) fixMemoryDirection(ops []Operand) { 416 b := strings.ToLower(inst.brief()) 417 store := strings.Contains(b, "store") 418 load := strings.Contains(b, "load") 419 if !store && !load { 420 return 421 } 422 for i := range ops { 423 switch { 424 case store && ops[i].Class == "mem": 425 ops[i].role = "destination" 426 case load && ops[i].Class == "vreg": 427 ops[i].role = "destination" 428 } 429 } 430 } 431 432 // operands parses the operands of this instruction's first encoding form. Most 433 // instructions have exactly one; use templates for the complete set. 434 func (inst *Instruction) operands() []Operand { 435 if ops := inst.allEncodingOperands(); len(ops) > 0 { 436 return ops[0] 437 } 438 return nil 439 } 440 441 // hasClass reports whether any operand has the given class. 442 func hasClass(ops []Operand, class string) bool { 443 for _, op := range ops { 444 if op.Class == class { 445 return true 446 } 447 } 448 return false 449 } 450 451 // predicationVariants returns the governing-predicate qualifiers to emit for a 452 // template: the predicate operand's own qualifier ("M" or "Z"), both when a 453 // single encoding written "<Pg>/<ZM>" (MOVPRFX) selects merging or zeroing via a 454 // bit, or a single no-op pass when the template has no governing predicate. 455 func predicationVariants(ops []Operand) []string { 456 for i := range ops { 457 if ops[i].Class == "mask" && ops[i].role == "mask" { 458 if ops[i].Predication == "MZ" { 459 return []string{"M", "Z"} 460 } 461 return []string{ops[i].Predication} 462 } 463 } 464 return []string{""} 465 } 466 467 // documentation returns a one-line description of the instruction. 468 func (inst *Instruction) documentation() string { 469 if len(inst.Desc.Authored.Paragraphs) > 0 { 470 return inst.Desc.Authored.Paragraphs[0].Text 471 } 472 return inst.Title 473 } 474 475 // asmTemplateToString flattens an AsmTemplate to its text. 476 func asmTemplateToString(t xmlspec.AsmTemplate) string { 477 var b strings.Builder 478 for _, ta := range t.TextA { 479 b.WriteString(ta.Value) 480 } 481 return b.String() 482 } 483