Source file src/simd/archsimd/_gen/simdgen/arm64/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 arm64
     6  
     7  import (
     8  	"fmt"
     9  	"strings"
    10  )
    11  
    12  // OperandType defines the type of an operand for ARM64 instruction generation.
    13  type OperandType int
    14  
    15  const (
    16  	OperandVReg  OperandType = iota // Vector register
    17  	OperandGReg                     // General register
    18  	OperandImm                      // Immediate
    19  	OperandVElem                    // Vector element (e.g., <Vm>.H[<index>]): early-lowered into immediate + vreg with same AsmPos
    20  	OperandList                     // List operand (e.g., { <Vn>.16B, <Vn+1>.16B }): early-lowered into vreg with ListNumber
    21  )
    22  
    23  func (t OperandType) String() string {
    24  	switch t {
    25  	case OperandVReg:
    26  		return "VReg"
    27  	case OperandGReg:
    28  		return "GReg"
    29  	case OperandImm:
    30  		return "Imm"
    31  	case OperandVElem:
    32  		return "VElem"
    33  	case OperandList:
    34  		return "List"
    35  	default:
    36  		return "Unknown"
    37  	}
    38  }
    39  
    40  // Operand represents an arm64 instruction operand instantiated for concrete arrangement.
    41  type Operand struct {
    42  	Type     OperandType
    43  	Class    string // "vreg", "greg", "immediate"
    44  	BaseType string // Base type ("int", "uint", "float")
    45  	ElemBits int    // Element bits (for vectors)
    46  	Bits     int    // Total bits
    47  	Lanes    int    // Number of lanes (for vectors)
    48  	ImmMax   int    // Immediate max value (for immediates)
    49  	// The operand's role. Possible values:
    50  	//   - "destination":      the output register
    51  	//   - "original":         the original SSA value of "destination" (for resultInArg0 instructions)
    52  	//   - ends with "_i":     vector element index: should get ImmMax = lanes-1
    53  	//   - "op0", "op1", ...:  input registers
    54  	//   - other strings:      immediate names (e.g. "immshift", "amount", "immzero")
    55  	Role       string
    56  	ListNumber int // List number for register list (e.g., useful for TBL/TBX instructions)
    57  	AsmPos     int // Assembly position (usually 0 for the destination register, 1+ for inputs).
    58  	// Input with AsmPos == 0 represents original value of the destination register for ssa form.
    59  	// Immediate with AsmPos == subsequent register operand's AsmPos represents a vector element (the immediate specifies the lane number).
    60  }
    61  
    62  // token represents a raw operand string from the assembly template.
    63  type token struct {
    64  	text   string // Raw operand text (e.g., "<Vd>.16B", "{ <Vn>.16B, <Vn+1>.16B }")
    65  	asmPos int    // Position in assembly syntax (0 for destination, 1+ for inputs)
    66  }
    67  
    68  // parsedOperand represents a classified but not lowered operand.
    69  type parsedOperand struct {
    70  	token                     // Embedded token with text and position
    71  	operandType   OperandType // Operand type (VReg, GReg, Imm, VElem, List)
    72  	isDestination bool        // True if this is a destination operand
    73  	immName       string      // Immediate role (for imm operands)
    74  	immMax        int         // Maximum immediate value (for imm operands)
    75  }
    76  
    77  // instantiate updates the operand's type information based on the given arrangement and instruction mnemonic.
    78  // This is used when generating instruction definitions for specific vector arrangements.
    79  func (op *Operand) instantiate(arrangement Arrangement, ashape ArngShape, vregPos int, mnemonic string) {
    80  	switch op.Type {
    81  	case OperandVReg:
    82  		switch {
    83  		case ashape == NarrowArngs && vregPos == 0:
    84  			op.ElemBits = arrangement.elemBits / 2
    85  			op.Bits = arrangement.bits
    86  			op.Lanes = arrangement.bits / (arrangement.elemBits / 2)
    87  		case ashape == LongArngs && vregPos == 0:
    88  			op.ElemBits = arrangement.elemBits * 2
    89  			op.Bits = arrangement.bits * 2
    90  			if op.Bits > 128 {
    91  				op.Bits = 128
    92  			}
    93  			op.Lanes = arrangement.bits / op.ElemBits
    94  		case ashape == WideArngs && vregPos == 2:
    95  			op.ElemBits = arrangement.elemBits / 2
    96  			op.Bits = arrangement.bits
    97  			op.Lanes = arrangement.bits / op.ElemBits
    98  		default:
    99  			op.ElemBits = arrangement.elemBits
   100  			op.Bits = arrangement.bits
   101  			op.Lanes = arrangement.lanes
   102  		}
   103  		op.BaseType = arrangement.baseType
   104  	case OperandImm:
   105  		// Update immediate operands based on arrangement
   106  		// For shift operations, set immediate max to element_bits - 1 (sometimes need element_bits?)
   107  		// For vector element indices, immediate max should be lanes - 1
   108  		if op.ImmMax == -1 {
   109  			// Check if this is a vector element immediate (role ends with "_i")
   110  			if strings.HasSuffix(op.Role, "_i") {
   111  				// Vector element index: max = lanes - 1
   112  				op.ImmMax = arrangement.lanes - 1
   113  			} else if mnemonic == "EXT" {
   114  				// EXT byte index: max = register size in bytes - 1 = 15 for 128-bit
   115  				op.ImmMax = arrangement.bits/8 - 1
   116  			} else if ashape == NarrowArngs {
   117  				// Narrow shift: max = destination element bits - 1 (half of source)
   118  				op.ImmMax = arrangement.elemBits/2 - 1
   119  			} else {
   120  				// Shift operation: max = element_bits - 1
   121  				op.ImmMax = arrangement.elemBits - 1
   122  			}
   123  		}
   124  	case OperandGReg:
   125  		op.BaseType = arrangement.baseType
   126  		if mnemonic == "UMOV" {
   127  			// Update general register width based on arrangement
   128  			// - 2D arrangement needs 64-bit (X register)
   129  			// - All other arrangements need 32-bit (W register)
   130  			if arrangement.arrangement == "2D" {
   131  				op.Bits = 64
   132  			} else {
   133  				op.Bits = 32
   134  			}
   135  		}
   136  		if mnemonic == "INS" {
   137  			op.Bits = arrangement.elemBits
   138  		}
   139  	case OperandVElem, OperandList:
   140  		panic("expected this operand type to be early-lowered")
   141  	}
   142  }
   143  
   144  // operands extracts operand information from the assembly template.
   145  func (instruction *Instruction) operands(asmTemplate string) []Operand {
   146  	tokens := tokenizeTemplate(asmTemplate)
   147  	parsed := classifyTokens(tokens)
   148  	return buildOperandList(parsed, instruction.ResultInArg0())
   149  }
   150  
   151  // tokenizeTemplate parses an assembly template string into a list of operand tokens.
   152  // It handles:
   153  // - Stripping the mnemonic from the first operand
   154  // - Register list operands like "{ <Vn>.16B, <Vn+1>.16B }" (preserves internal commas)
   155  // - Optional modifiers like "{, LSL #<amount>}" (merged with previous operand)
   156  func tokenizeTemplate(template string) []token {
   157  	template = stripMnemonic(template)
   158  	parts := strings.Split(template, ",")
   159  
   160  	var tokens []token
   161  	var listBuf strings.Builder
   162  	var inList bool
   163  
   164  	for _, part := range parts {
   165  		part = strings.TrimSpace(part)
   166  		if part == "" {
   167  			continue
   168  		}
   169  
   170  		// Register list: { <Vn>.16B, <Vn+1>.16B }
   171  		if isListStart(part) {
   172  			inList = true
   173  			listBuf.WriteString(part)
   174  			if strings.Contains(part, "}") {
   175  				tokens = append(tokens, token{text: listBuf.String(), asmPos: len(tokens)})
   176  				listBuf.Reset()
   177  				inList = false
   178  			}
   179  			continue
   180  		}
   181  		if inList {
   182  			listBuf.WriteString(", ")
   183  			listBuf.WriteString(part)
   184  			if strings.HasSuffix(part, "}") {
   185  				tokens = append(tokens, token{text: listBuf.String(), asmPos: len(tokens)})
   186  				listBuf.Reset()
   187  				inList = false
   188  			}
   189  			continue
   190  		}
   191  
   192  		// Optional modifier: previous ends with "{" or current starts with "{"
   193  		if shouldMergeWithPrevious(part, tokens) {
   194  			tokens[len(tokens)-1].text += ", " + part
   195  			continue
   196  		}
   197  
   198  		tokens = append(tokens, token{text: part, asmPos: len(tokens)})
   199  	}
   200  	return tokens
   201  }
   202  
   203  // stripMnemonic removes the instruction mnemonic from the template string.
   204  // For example, "ADD  <Vd>.<T>, <Vn>.<T>, <Vm>.<T>" becomes "<Vd>.<T>, <Vn>.<T>, <Vm>.<T>".
   205  func stripMnemonic(template string) string {
   206  	if idx := strings.Index(template, " "); idx >= 0 {
   207  		return strings.TrimSpace(template[idx+1:])
   208  	}
   209  	return template
   210  }
   211  
   212  // isListStart checks if a part is the start of a register list operand.
   213  // Register lists start with "{" and contain a vector register like "<V".
   214  func isListStart(part string) bool {
   215  	return strings.HasPrefix(part, "{") && strings.Contains(part, "<V")
   216  }
   217  
   218  // shouldMergeWithPrevious determines if the current part should be merged with the previous token.
   219  // This happens for optional modifiers like "{, LSL #<amount>}" that follow an operand.
   220  func shouldMergeWithPrevious(part string, tokens []token) bool {
   221  	if len(tokens) == 0 {
   222  		return false
   223  	}
   224  	return strings.HasPrefix(part, "{") || strings.HasSuffix(tokens[len(tokens)-1].text, "{")
   225  }
   226  
   227  // classifyTokens analyzes each token and determines its operand type and role.
   228  // It returns a slice of parsedOperand with type information and metadata.
   229  func classifyTokens(tokens []token) []parsedOperand {
   230  	parsed := make([]parsedOperand, len(tokens))
   231  	for i, tok := range tokens {
   232  		opType, isDest, immName, immMax := analyzeOperand(tok.text)
   233  		parsed[i] = parsedOperand{
   234  			token:         tok,
   235  			operandType:   opType,
   236  			isDestination: isDest,
   237  			immName:       immName,
   238  			immMax:        immMax,
   239  		}
   240  	}
   241  	return parsed
   242  }
   243  
   244  // buildOperandList constructs the final ordered list of operands from parsed operands.
   245  // It lowers compound operands (VElem, List) and orders them as: outputs + immediates + [original] + inputs.
   246  func buildOperandList(parsed []parsedOperand, resultInArg0 bool) []Operand {
   247  	var outs, ins, imms []Operand
   248  	inputCount := 0
   249  
   250  	for _, p := range parsed {
   251  		switch p.operandType {
   252  		case OperandVElem:
   253  			idx, reg := lowerVElem(p, &inputCount)
   254  			imms = append(imms, idx)
   255  			if p.isDestination {
   256  				outs = append(outs, reg)
   257  				resultInArg0 = true // element dest implies read-modify-write
   258  			} else {
   259  				ins = append(ins, reg)
   260  			}
   261  
   262  		case OperandList:
   263  			ins = append(ins, lowerList(p, inputCount))
   264  			inputCount++
   265  
   266  		case OperandImm:
   267  			imms = append(imms, makeImm(p, inputCount))
   268  			inputCount++
   269  
   270  		case OperandVReg, OperandGReg:
   271  			op := makeReg(p, p.operandType, inputCount)
   272  			if p.isDestination {
   273  				outs = append(outs, op)
   274  			} else {
   275  				ins = append(ins, op)
   276  				inputCount++
   277  			}
   278  		}
   279  	}
   280  
   281  	// Assemble final order: outs + imms + [original] + ins
   282  	result := append(outs, imms...)
   283  	if resultInArg0 && len(outs) > 0 {
   284  		original := outs[0]
   285  		original.Role = "original"
   286  		result = append(result, original)
   287  	}
   288  	return append(result, ins...)
   289  }
   290  
   291  // lowerVElem expands a vector element operand into an index immediate and a vector register.
   292  // For destination elements, both get AsmPos=0. For source elements, they share the original AsmPos.
   293  func lowerVElem(p parsedOperand, inputCount *int) (idx Operand, reg Operand) {
   294  	if p.isDestination {
   295  		idx = Operand{
   296  			Type: OperandImm, Class: "immediate",
   297  			Role: "destination_i", AsmPos: 0, ListNumber: -1, ImmMax: -1,
   298  		}
   299  		reg = Operand{
   300  			Type: OperandVReg, Class: "vreg",
   301  			Role: "destination", AsmPos: 0, ListNumber: -1,
   302  		}
   303  	} else {
   304  		role := inputRole(*inputCount)
   305  		idx = Operand{
   306  			Type: OperandImm, Class: "immediate",
   307  			Role: role + "_i", AsmPos: p.token.asmPos, ListNumber: -1, ImmMax: -1,
   308  		}
   309  		reg = Operand{
   310  			Type: OperandVReg, Class: "vreg",
   311  			Role: role, AsmPos: p.token.asmPos, ListNumber: -1,
   312  		}
   313  		*inputCount++
   314  	}
   315  	return
   316  }
   317  
   318  // lowerList expands a list operand into a vector register with ListNumber=0.
   319  func lowerList(p parsedOperand, inputCount int) Operand {
   320  	return Operand{
   321  		Type: OperandVReg, Class: "vreg",
   322  		Role: inputRole(inputCount), AsmPos: p.token.asmPos, ListNumber: 0,
   323  	}
   324  }
   325  
   326  // makeImm creates an immediate operand from a parsed operand.
   327  func makeImm(p parsedOperand, inputCount int) Operand {
   328  	return Operand{
   329  		Type: OperandImm, Class: "immediate",
   330  		Role: p.immName, AsmPos: p.token.asmPos, ListNumber: -1, ImmMax: p.immMax,
   331  	}
   332  }
   333  
   334  // makeReg creates a register operand (vector or general) from a parsed operand.
   335  func makeReg(p parsedOperand, opType OperandType, inputCount int) Operand {
   336  	class := "vreg"
   337  	if opType == OperandGReg {
   338  		class = "greg"
   339  	}
   340  	role := "destination"
   341  	if !p.isDestination {
   342  		role = inputRole(inputCount)
   343  	}
   344  	return Operand{
   345  		Type: opType, Class: class,
   346  		Role: role, AsmPos: p.token.asmPos, ListNumber: -1,
   347  	}
   348  }
   349  
   350  // inputRole generates a role name for an input operand at the given index: "op0", "op1", "op2", etc.
   351  // TODO: consider extracting these names from ARM64 register suffixes in templates.
   352  func inputRole(index int) string {
   353  	return fmt.Sprintf("op%d", index)
   354  }
   355  
   356  // extractImmediateInfo extracts immediate name and immMax value from immediate operand strings.
   357  func extractImmediateInfo(operandStr string) (string, int) {
   358  	if strings.Contains(operandStr, "#<") {
   359  		// Immediate operand: #<immediate_name>
   360  		// Extract the immediate name from between #< and >
   361  		start := strings.Index(operandStr, "#<") + 2
   362  		end := strings.Index(operandStr[start:], ">")
   363  		if end >= 0 {
   364  			immediateName := operandStr[start : start+end]
   365  			return immediateName, -1
   366  		}
   367  		return "immediate", -1
   368  	}
   369  	if strings.Contains(operandStr, "#0") {
   370  		return "immzero", 0
   371  	}
   372  	return "", 0
   373  }
   374  
   375  // detectOperandType determines the basic operand type (VReg, GReg, Imm) based on its string pattern.
   376  func detectOperandType(operandStr string) OperandType {
   377  	switch {
   378  	case strings.HasPrefix(operandStr, "<V"):
   379  		return OperandVReg
   380  	case strings.HasPrefix(operandStr, "<W") || strings.HasPrefix(operandStr, "<X") || strings.HasPrefix(operandStr, "<R"):
   381  		return OperandGReg
   382  	case strings.Contains(operandStr, "#<") || strings.Contains(operandStr, "#0"):
   383  		return OperandImm
   384  	default:
   385  		return OperandVReg
   386  	}
   387  }
   388  
   389  // analyzeOperand analyzes an operand string and returns the operand type, whether it's a destination, and the immediate name and immMax (if any).
   390  func analyzeOperand(operandStr string) (OperandType, bool, string, int) {
   391  	opType := detectOperandType(operandStr)
   392  	isDestination := strings.Contains(operandStr, "d>")
   393  	switch opType {
   394  	case OperandVReg:
   395  		if strings.HasPrefix(operandStr, "{") && strings.HasSuffix(operandStr, "}") {
   396  			return OperandList, false, "", 0
   397  		}
   398  		if strings.Contains(operandStr, "[<index") {
   399  			return OperandVElem, isDestination, "", 0
   400  		}
   401  	case OperandImm:
   402  		immediateName, immMax := extractImmediateInfo(operandStr)
   403  		return opType, false, immediateName, immMax
   404  	}
   405  	return opType, isDestination, "", 0
   406  }
   407  

View as plain text