Source file src/simd/archsimd/_gen/simdgen/sve/operands.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 6 7 import ( 8 "fmt" 9 "regexp" 10 "strings" 11 12 "golang.org/x/arch/arm64/instgen/xmlspec" 13 ) 14 15 // arngValueRe matches an arrangement symbol's displayed value: the vector forms 16 // <T>, <Ta>, <Tb>, and the <V> size specifier of a SIMD&FP scalar (<V><d>). Its 17 // <a> link identifies the size table that gives this operand's element widths 18 // (see Instruction.resolveArrangementTable). 19 var arngValueRe = regexp.MustCompile(`^<(T[a-z]*|V)>$`) 20 21 // fixedArngRe matches a hardcoded element specifier, e.g. the ".D" in <Zm>.D. 22 var fixedArngRe = regexp.MustCompile(`\.([BHSD])\b`) 23 24 // simdFPRe matches a SIMD&FP scalar register: a fixed-width form (<Dd>, <Sn>, 25 // <Hd>, <Bd>, <Qd>) or an element-sized form (<V><d>, <V><n>). These hold a 26 // single value (a reduction result, or a DUP source), not a scalable vector. 27 var simdFPRe = regexp.MustCompile(`^(<[BHSDQ][a-z]>|<V><[a-z]>)$`) 28 29 // OperandType classifies an SVE instruction operand. 30 type OperandType int 31 32 const ( 33 // OperandZReg is a scalable vector register (Z), e.g. <Zd>.<T>, <Zn>.<T>. 34 // It has no fixed total bit width: the width is the implementation-defined 35 // vector length. Only its element type and element width are known. 36 OperandZReg OperandType = iota 37 // OperandPReg is a scalable predicate register (P), e.g. <Pg>/M, <Pd>.<T>. 38 // A predicate is modeled as a Go mask value. 39 OperandPReg 40 // OperandGReg is a general-purpose scalar register (W/X/R). 41 OperandGReg 42 // OperandVFP is a SIMD&FP scalar register (<Dd>, <V><d>, ...): a single 43 // fixed-width value, such as a horizontal reduction's result (SADDV <Dd>) or 44 // a DUP scalar source. Unlike a Z register it is not scalable. 45 OperandVFP 46 // OperandImm is an immediate. 47 OperandImm 48 // OperandMem is a memory operand, e.g. [<Xn|SP>{, #<imm>, MUL VL}] or a 49 // gather/scatter address like [<Xn|SP>, <Zm>.D, SXTW]. simdgen does not yet 50 // distinguish the memory addressing modes; they are all one "mem" class. 51 OperandMem 52 // OperandList is a register list, e.g. { <Zt>.B } or { <Zt1>.D-<Zt2>.D }. 53 // TODO: register lists are not modeled yet; instructions carrying one are 54 // skipped (see classify). 55 OperandList 56 // OperandSpecial is a recognized but not-yet-detailed operand: an indexed 57 // register (<Zm>.<T>[<index>]), a register with an optional modifier 58 // ({, <pattern>}), or a special token (<prfop>, <vl>, <pattern>, <const>, 59 // <mod>, and NEON-style <Vd>/<Dd> reduction results). 60 OperandSpecial 61 // OperandUnknown is a token the classifier could not place at all; an anomaly. 62 OperandUnknown 63 ) 64 65 func (t OperandType) String() string { 66 switch t { 67 case OperandZReg: 68 return "ZReg" 69 case OperandPReg: 70 return "PReg" 71 case OperandGReg: 72 return "GReg" 73 case OperandVFP: 74 return "VFP" 75 case OperandImm: 76 return "Imm" 77 case OperandMem: 78 return "Mem" 79 case OperandList: 80 return "List" 81 case OperandSpecial: 82 return "Special" 83 default: 84 return "Unknown" 85 } 86 } 87 88 // Operand is an SVE instruction operand instantiated for a concrete element size. 89 type Operand struct { 90 Type OperandType 91 Class string // "vreg", "mask", "greg", "immediate", "mem", "reglist", "special" 92 BaseType string // "int", "uint", "float" (for vreg/mask/greg) 93 ElemBits int // element width in bits (8/16/32/64); 0 if unsized 94 // Bits and Lanes are set for a fixed-width scalar register — a general-purpose 95 // greg (<Xd>) or a SIMD&FP vreg (<Dd>): the total register width and lane 96 // count (always 1). A scalable Z-vector leaves them 0 and is marked 97 // "scalable" in the emitted def instead. 98 Bits int 99 Lanes int 100 101 // Predication is "M" (merging) or "Z" (zeroing) for governing predicates, 102 // otherwise "". 103 Predication string 104 // AsmPos is the position in the assembly syntax (0 for the destination 105 // register, 1+ for inputs). It mirrors the source template order and is the 106 // field simdgen uses to order operands. 107 AsmPos int 108 // Raw is the source operand token, retained for deferred (mem/list/special) 109 // and unknown operands so diagnostics can name what was skipped. 110 Raw string 111 112 // role is the operand's internal role: "destination", "op0"/"op1"/..., or 113 // "mask" (a governing predicate). It drives out/in/inVariant partitioning at 114 // emit time but is NOT emitted (simdgen orders operands by AsmPos, so a role 115 // field in the YAML would be redundant). 116 role string 117 // arngLink is the <a> link of this operand's arrangement symbol (<T>/<Ta>/ 118 // <Tb>), used to resolve its per-operand element widths. Empty if the 119 // operand has a fixed or no arrangement. 120 arngLink string 121 // fixedElem is a hardcoded element width (from e.g. ".D"), or 0. 122 fixedElem int 123 // fixedBits is the fixed total width of a SIMD&FP scalar named by a size 124 // letter (<Dd> -> 64, <Sd> -> 32, ...), or 0 for an element-sized <V><d>. 125 fixedBits int 126 // isList reports that this register came from a single-register list 127 // ("{ <Zt>.<T> }"). It is a distinct assembler encoding from a bare register, 128 // so it is preserved (emitted as listNumber) even though the register is 129 // otherwise handled like any vreg. 130 isList bool 131 // regName is the inner register symbol, e.g. "Zdn", "Zm", "Pg". 132 regName string 133 } 134 135 // resultInArg0 reports whether this destination register is also read, i.e. it 136 // is written in place (an ARM <Zdn>/<Zda>-style operand). 137 func (op *Operand) resultInArg0() bool { 138 return op.role == "destination" && isInPlaceReg(op.regName) 139 } 140 141 // aElem is a single <a> symbol from an assembly template: its displayed value 142 // and its link. The link, not the value, is the stable key used to resolve a 143 // symbol's definition (see Instruction.findExplanation). 144 // 145 // For example, in the template "ADD <Zdn>.<T>, ..." the operand "<Zdn>.<T>" 146 // contributes two <a> elements: 147 // 148 // {value: "<Zdn>", link: "Zdn"} // the register symbol 149 // {value: "<T>", link: "T__3"} // the arrangement symbol 150 type aElem struct { 151 value string 152 link string 153 } 154 155 // rawTok is one operand's raw text plus the <a> symbols it contains, before 156 // classification. The <a> links let us resolve each operand's arrangement. 157 // 158 // For "ADD <Zdn>.<T>, <Pg>/M, <Zdn>.<T>, <Zm>.<T>", the third operand tokenizes 159 // to: 160 // 161 // rawTok{ 162 // text: "<Zdn>.<T>", 163 // asmPos: 2, // 0 = destination, 1+ = following operands 164 // aElems: [{"<Zdn>","Zdn"}, {"<T>","T__3"}], 165 // } 166 type rawTok struct { 167 text string 168 asmPos int 169 aElems []aElem 170 } 171 172 // tok is a rawTok after classification, before it is instantiated for 173 // a concrete element size. Examples of the interesting fields: 174 // 175 // "<Zdn>.<T>" -> {operandType: OperandZReg, isDestination: true, 176 // regName: "Zdn", arngLink: "T__3"} 177 // "<Zm>.<T>" -> {operandType: OperandZReg, isDestination: false, 178 // regName: "Zm", arngLink: "T__3"} 179 // "<Pg>/M" -> {operandType: OperandPReg, predication: "M", 180 // regName: "Pg"} // governing predicate ("Z"/"MZ" too) 181 // "<Zt>.D" -> {operandType: OperandZReg, fixedElem: 64} 182 // // hardcoded arrangement, so no arngLink 183 // "#<imm>" -> {operandType: OperandImm} 184 // "[<Xn|SP>{, #<imm>}]"-> {operandType: OperandMem} 185 // "<Zm>.<T>[<index>]" -> {operandType: OperandSpecial} // indexed, not modeled 186 type tok struct { 187 // text is the raw operand token, e.g. "<Zdn>.<T>". 188 text string 189 // asmPos is the position in the assembly syntax (0 = destination, 1+ = the 190 // following operands), mirroring the template order. 191 asmPos int 192 // operandType is the classification (OperandZReg, OperandPReg, OperandMem, 193 // OperandSpecial, ...). 194 operandType OperandType 195 // isDestination is true when the register is written (an ARM 'd'-role symbol 196 // such as <Zd>, <Zdn>, <Pd>). 197 isDestination bool 198 // predication is "M" (merging), "Z" (zeroing), or "MZ" (a <Pg>/<ZM> encoding 199 // selecting either) for a governing predicate; "" otherwise. 200 predication string 201 // regName is the inner register symbol, e.g. "Zdn", "Zm", "Pg". 202 regName string 203 // arngLink is the <a> link of this operand's variable arrangement symbol 204 // (<T>/<Ta>/<Tb>, or <V> for a SIMD&FP scalar), used to resolve its element 205 // widths; "" if the arrangement is fixed or absent. 206 arngLink string 207 // fixedElem is a hardcoded element width in bits from a literal ".B"/".H"/ 208 // ".S"/".D" (8/16/32/64), or 0. 209 fixedElem int 210 // fixedBits is the fixed total width of a SIMD&FP scalar named by a size 211 // letter (<Dd> -> 64, <Sd> -> 32, ...), or 0 for an element-sized <V><d>. 212 fixedBits int 213 // isList reports that this register came from a single-register list 214 isList bool 215 } 216 217 // operandsFromTextA parses operands from an assembly template's <text>/<a> 218 // sequence, preserving each operand's arrangement-symbol link. 219 func operandsFromTextA(textA []xmlspec.TextA) []Operand { 220 return buildOperandList(classifyToks(tokenizeTextA(textA))) 221 } 222 223 // operands parses operands from a flattened template string. It cannot recover 224 // <a> links, so arrangement symbols resolve to empty links; it is used for 225 // classification-only paths and tests. The real loader path uses 226 // operandsFromTextA. 227 func operands(asmTemplate string) []Operand { 228 return buildOperandList(classifyToks(tokenizeString(asmTemplate))) 229 } 230 231 // tokenizeTextA splits a <text>/<a> sequence into operand tokens on top-level 232 // commas, stripping the leading mnemonic and recording each <a> symbol. 233 func tokenizeTextA(textA []xmlspec.TextA) []rawTok { 234 var toks []rawTok 235 cur := rawTok{} 236 depth := 0 237 started := false // have we passed the mnemonic word? 238 flush := func() { 239 cur.text = strings.TrimSpace(cur.text) 240 if cur.text != "" || len(cur.aElems) > 0 { 241 cur.asmPos = len(toks) 242 toks = append(toks, cur) 243 } 244 cur = rawTok{} 245 } 246 for _, ta := range textA { 247 if ta.Link != "" { 248 cur.text += ta.Value 249 cur.aElems = append(cur.aElems, aElem{strings.TrimSpace(ta.Value), ta.Link}) 250 started = true 251 continue 252 } 253 s := ta.Value 254 if !started { 255 // Strip the mnemonic: keep everything after the first space. 256 if i := strings.IndexByte(s, ' '); i >= 0 { 257 s = s[i:] 258 } else { 259 s = "" 260 } 261 started = true 262 } 263 for _, r := range s { 264 switch r { 265 case '[', '{': 266 depth++ 267 case ']', '}': 268 depth-- 269 case ',': 270 if depth == 0 { 271 flush() 272 continue 273 } 274 } 275 cur.text += string(r) 276 } 277 } 278 flush() 279 return toks 280 } 281 282 // tokenizeString splits a flattened template string into operand tokens. It has 283 // no <a> link information. 284 func tokenizeString(template string) []rawTok { 285 template = stripMnemonic(template) 286 var toks []rawTok 287 depth := 0 288 var cur strings.Builder 289 flush := func() { 290 if s := strings.TrimSpace(cur.String()); s != "" { 291 toks = append(toks, rawTok{text: s, asmPos: len(toks)}) 292 } 293 cur.Reset() 294 } 295 for _, r := range template { 296 switch r { 297 case '[', '{': 298 depth++ 299 case ']', '}': 300 depth-- 301 case ',': 302 if depth == 0 { 303 flush() 304 continue 305 } 306 } 307 cur.WriteRune(r) 308 } 309 flush() 310 return toks 311 } 312 313 // stripMnemonic removes the leading mnemonic from an assembly template. A 314 // template with no space is a mnemonic-only (nullary) instruction. 315 func stripMnemonic(template string) string { 316 if _, after, ok := strings.Cut(strings.TrimSpace(template), " "); ok { 317 return strings.TrimSpace(after) 318 } 319 return "" 320 } 321 322 // classifyToks classifies each raw token and attaches its arrangement source. 323 func classifyToks(toks []rawTok) []tok { 324 parsed := make([]tok, 0, len(toks)) 325 for _, t := range toks { 326 p := classifyText(t.text, t.asmPos) 327 // Per-operand arrangement: could be a variable arrangement symbol (<T>/<Ta>/<Tb>) 328 // or a fixed element, or none, e.g. for a greg. 329 for _, a := range t.aElems { 330 if arngValueRe.MatchString(a.value) { 331 p.arngLink = a.link 332 } 333 } 334 if p.arngLink == "" { 335 if m := fixedArngRe.FindStringSubmatch(t.text); m != nil { 336 p.fixedElem = elemLetterBits(m[1]) 337 } 338 } 339 parsed = append(parsed, p) 340 } 341 return parsed 342 } 343 344 // classifyText determines an operand's type, destination-ness, predication and 345 // register symbol from its text. 346 // 347 // A register token counts as "clean" only if it has no index or optional 348 // modifier ('[' or '{'). Indexed/modified registers and other angle-bracket 349 // tokens (<prfop>, <vl>, <mod>, <Vd>, ...) are OperandSpecial; anything else is 350 // OperandUnknown. 351 func classifyText(text string, asmPos int) tok { 352 p := tok{text: text, asmPos: asmPos} 353 // A single-register list ("{ <Zt>.<T> }") is treated as its inner register 354 // (but flagged, as it is a distinct assembler encoding); multi-register lists 355 // remain OperandList (deferred). 356 reg := text 357 if inner, ok := singleRegList(text); ok { 358 reg = inner 359 p.isList = true 360 } 361 clean := !strings.ContainsAny(reg, "[{") 362 switch { 363 case strings.HasPrefix(reg, "["): 364 p.operandType = OperandMem 365 case strings.HasPrefix(reg, "{"): 366 p.operandType = OperandList 367 case strings.HasPrefix(reg, "#"), strings.HasPrefix(reg, "<const>"): 368 p.operandType = OperandImm 369 case simdFPRe.MatchString(reg): 370 // A SIMD&FP scalar register: a reduction result <Dd>/<V><d> or a DUP 371 // source <V><n>. Its width is fixed by the size letter, or element-sized 372 // for the <V> form (resolved via its <a> link like <T>). 373 p.operandType = OperandVFP 374 p.regName = regSymbol(reg) 375 p.isDestination = isDestinationReg(p.regName) || strings.Contains(reg, "<d>") 376 p.fixedBits = simdFPLetterBits(reg) 377 case clean && strings.HasPrefix(reg, "<Z"): 378 p.operandType = OperandZReg 379 p.regName = regSymbol(reg) 380 p.isDestination = isDestinationReg(p.regName) 381 case clean && strings.HasPrefix(reg, "<P"): 382 p.operandType = OperandPReg 383 p.regName = regSymbol(reg) 384 p.isDestination = isDestinationReg(p.regName) 385 switch { 386 case strings.Contains(reg, "/<ZM>"): 387 // A single encoding (MOVPRFX) whose bit selects merging or zeroing. 388 p.predication = "MZ" 389 case strings.HasSuffix(reg, "/M"): 390 p.predication = "M" 391 case strings.HasSuffix(reg, "/Z"): 392 p.predication = "Z" 393 } 394 case clean && (strings.HasPrefix(reg, "<W") || strings.HasPrefix(reg, "<X") || strings.HasPrefix(reg, "<R")): 395 p.operandType = OperandGReg 396 p.regName = regSymbol(reg) 397 p.isDestination = isDestinationReg(p.regName) 398 p.fixedBits = gregLetterBits(reg) 399 case strings.HasPrefix(reg, "<"): 400 p.operandType = OperandSpecial 401 // A special operand can still be a destination, e.g. an indexed 402 // destination <Zd>.<T>[<index>]. 403 p.regName = regSymbol(reg) 404 p.isDestination = isDestinationReg(p.regName) || strings.Contains(reg, "<d>") 405 default: 406 p.operandType = OperandUnknown 407 } 408 return p 409 } 410 411 // singleRegList reports whether text is a single-register list like 412 // "{ <Zt>.<T> }" and, if so, returns its inner register token. Multi-register 413 // lists (a comma-separated set or a "-" range) return false and stay opaque. 414 func singleRegList(text string) (string, bool) { 415 if !strings.HasPrefix(text, "{") || !strings.HasSuffix(text, "}") { 416 return "", false 417 } 418 inner := strings.TrimSpace(text[1 : len(text)-1]) 419 if strings.ContainsAny(inner, ",-") { // multiple registers or a range 420 return "", false 421 } 422 return inner, true 423 } 424 425 // simdFPLetterBits returns the fixed width of a size-lettered SIMD&FP scalar 426 // register (<Bd>=8, <Hd>=16, <Sd>=32, <Dd>=64, <Qd>=128), or 0 for the 427 // element-sized <V><d> form (whose width comes from its <V> arrangement link). 428 func simdFPLetterBits(text string) int { 429 if len(text) < 2 { 430 return 0 431 } 432 switch text[1] { 433 case 'B': 434 return 8 435 case 'H': 436 return 16 437 case 'S': 438 return 32 439 case 'D': 440 return 64 441 case 'Q': 442 return 128 443 } 444 return 0 445 } 446 447 // gregLetterBits returns the width of a general-purpose scalar register from its 448 // size letter (<Wd>=32, <Xd>=64), or 0 when the width is not fixed by the name 449 // (e.g. the width-variable <R> form). 450 func gregLetterBits(text string) int { 451 if len(text) < 2 { 452 return 0 453 } 454 switch text[1] { 455 case 'W': 456 return 32 457 case 'X': 458 return 64 459 } 460 return 0 461 } 462 463 // regSymbol extracts the inner register symbol from a token, e.g. "<Zdn>.<T>" -> 464 // "Zdn", "<Pg>/M" -> "Pg". 465 func regSymbol(text string) string { 466 if i := strings.IndexByte(text, '<'); i >= 0 { 467 text = text[i+1:] 468 } 469 if i := strings.IndexByte(text, '>'); i >= 0 { 470 text = text[:i] 471 } 472 return text 473 } 474 475 // isDestinationReg reports whether a register symbol names a destination 476 // register. The destination role letter 'd' appears either right after the 477 // class letter (Zd, Zda, Zdn), or as the trailing role letter (Pd, Wd, Xd, PNd). 478 func isDestinationReg(name string) bool { 479 if len(name) < 2 { 480 return false 481 } 482 return name[1] == 'd' || name[len(name)-1] == 'd' 483 } 484 485 // isInPlaceReg reports whether a destination register symbol is also a source 486 // (read-modify-write), such as <Zdn> or <Zda>. A bare <Zd> is a pure output. 487 func isInPlaceReg(name string) bool { 488 return len(name) >= 3 && name[1] == 'd' 489 } 490 491 // buildOperandList lowers tokens into Operands ordered as outputs then 492 // inputs, assigning roles and handling read-modify-write destinations. 493 // 494 // Unlike an AMD64 AVX-512 K-mask, an SVE governing predicate is NOT optional: 495 // there is no K0-style "no predicate" encoding, so it is a mandatory literal 496 // input (class "mask", role "mask"), not an inVariant. See the discussion in 497 // emitOne. 498 func buildOperandList(parsed []tok) []Operand { 499 var outs, ins []Operand 500 inputCount := 0 501 destAssigned := false 502 503 // place assigns op's role — the (single) destination if isDestination, 504 // otherwise the next numbered input "opN" (a repeated destination symbol is 505 // the in-place source) — and files it under outs or ins. 506 place := func(op Operand, isDestination bool) { 507 if isDestination && !destAssigned { 508 op.role = "destination" 509 destAssigned = true 510 outs = append(outs, op) 511 return 512 } 513 op.role = inputRole(inputCount) 514 inputCount++ 515 ins = append(ins, op) 516 } 517 518 deferredClass := map[OperandType]string{ 519 OperandMem: "mem", 520 OperandList: "reglist", 521 OperandSpecial: "special", 522 OperandUnknown: "unknown", 523 } 524 525 for _, p := range parsed { 526 // We don't model the details of these types yet, so just naively record them and continue. 527 // TODO: we might need at least the details of OperandMem soon. 528 if class, ok := deferredClass[p.operandType]; ok { 529 place(Operand{ 530 Type: p.operandType, Class: class, Raw: p.text, 531 AsmPos: p.asmPos, regName: p.regName, 532 }, p.isDestination) 533 continue 534 } 535 switch p.operandType { 536 case OperandPReg: 537 if p.regName == "Pg" || p.predication != "" { 538 // Governing predicate: the operand named <Pg> ("g" for governing), a 539 // mandatory mask input (role "mask", not a numbered opN). Most carry a 540 // /Z or /M qualifier (predicated data-processing ops), but some do not 541 // — e.g. the store ST1B {<Zt>.B}, <Pg>, [...] governs with a plain 542 // <Pg> — so key on the register name, not the qualifier. Source 543 // predicates <Pn>/<Pm> and the destination <Pd> are ordinary operands, 544 // filed by place() below. 545 ins = append(ins, Operand{ 546 Type: OperandPReg, Class: "mask", role: "mask", 547 Predication: p.predication, AsmPos: p.asmPos, 548 arngLink: p.arngLink, fixedElem: p.fixedElem, regName: p.regName, 549 }) 550 continue 551 } 552 place(Operand{ 553 Type: OperandPReg, Class: "mask", AsmPos: p.asmPos, 554 arngLink: p.arngLink, fixedElem: p.fixedElem, isList: p.isList, regName: p.regName, 555 }, p.isDestination) 556 case OperandImm: 557 place(Operand{Type: OperandImm, Class: "immediate", AsmPos: p.asmPos}, false) 558 default: // OperandZReg, OperandGReg, OperandVFP 559 class := "vreg" 560 if p.operandType == OperandGReg { 561 // A general-purpose scalar register. 562 class = "greg" 563 } 564 // A SIMD&FP scalar (OperandVFP) stays "vreg": it lives in the FP/SIMD 565 // register bank, not the GP bank — just with a fixed width and lanes:1 566 // rather than a scalable length. 567 place(Operand{ 568 Type: p.operandType, Class: class, AsmPos: p.asmPos, 569 arngLink: p.arngLink, fixedElem: p.fixedElem, fixedBits: p.fixedBits, 570 isList: p.isList, regName: p.regName, 571 }, p.isDestination) 572 } 573 } 574 return append(outs, ins...) 575 } 576 577 // inputRole names an input operand: "op0", "op1", ... 578 func inputRole(index int) string { 579 return fmt.Sprintf("op%d", index) 580 } 581 582 // instantiate stamps a base type and element width into a typed operand. mem, 583 // immediate, reglist and special operands are opaque and left unchanged. 584 func (op *Operand) instantiate(baseType string, elemBits int) { 585 switch op.Type { 586 case OperandZReg: 587 // A scalable Z vector: only base type and element width; the total width 588 // is the (unknown) vector length. 589 op.BaseType = baseType 590 op.ElemBits = elemBits 591 case OperandGReg, OperandVFP: 592 // A scalar register — general-purpose (<Xd>) or SIMD&FP (<Dd>) — holds a 593 // single fixed-width value, so it has a concrete total width and lanes=1. 594 op.BaseType = baseType 595 op.ElemBits = elemBits 596 op.Bits = elemBits 597 op.Lanes = 1 598 case OperandPReg: 599 // Predicates are integer masks; their element width tracks the governed 600 // vector's element width. 601 op.BaseType = "int" 602 op.ElemBits = elemBits 603 } 604 } 605