| // Copyright 2026 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package sgutil provides shared utilities for SIMD file |
| // generation across architectures. This includes |
| // |
| // - "natural" comparison for better ordering |
| // - formatted-Go file saving |
| // - file merging for simdgenericOps.go |
| // - naming conventions and templates for the |
| // bitwise vector reinterpretation no-op methods. |
| |
| package sgutil |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "os" |
| "regexp" |
| "slices" |
| "sort" |
| "strconv" |
| "strings" |
| "text/template" |
| ) |
| |
| const simdGenericOpsTmpl = ` |
| package main |
| |
| func simdGenericOps() []opData { |
| return []opData{ |
| {{- range .Ops }} |
| {name: "{{.OpName}}", argLength: {{.OpInLen}}{{if .Comm}}, commutative: true{{end}}}, // ARCH:{{.ArchTag}} |
| {{- end }} |
| {{- range .OpsImm }} |
| {name: "{{.OpName}}", argLength: {{.OpInLen}}{{if .Comm}}, commutative: true{{end}}, aux: "UInt8"}, // ARCH:{{.ArchTag}} |
| {{- end }} |
| } |
| } |
| ` |
| |
| // TemplateNamedreturns a parsed template from temp, named name. |
| func TemplateNamed(name, temp string) *template.Template { |
| t, err := template.New(name).Parse(temp) |
| if err != nil { |
| panic(fmt.Errorf("failed to parse template %s: %w", name, err)) |
| } |
| return t |
| } |
| |
| // simdGenericOpsHeader is the arch-agnostic header for simdgenericOps.go. |
| const simdGenericOpsHeader = "// Code generated by 'simdgen' (merged automatically); DO NOT EDIT.\n" |
| |
| // GenericOpsData holds one generic op entry for template rendering. |
| type GenericOpsData struct { |
| OpName string |
| OpInLen int |
| Comm bool |
| HasAux bool |
| Archs []string // e.g. ["amd64","arm64"] |
| } |
| |
| // ArchTag returns the comma-separated arch list for the template comment. |
| func (d GenericOpsData) ArchTag() string { |
| return strings.Join(d.Archs, ",") |
| } |
| |
| // Match lines like: |
| // |
| // {name: "GetElemFloat32x4", argLength: 2, aux: "UInt8"}, // ARCH:amd64,arm64 |
| // {name: "MulInt8x16", argLength: 2, commutative: true}, // ARCH:arm64 |
| var reEntry = regexp.MustCompile(`^\s*\{name:\s*"([^"]+)",\s*argLength:\s*(\d+)(?:,\s*commutative:\s*(true|false))?(?:,\s*aux:\s*"[^"]+")?\s*\}\s*,\s*(?://\s*ARCH:(\S+))?`) |
| |
| // parseOps reads an existing simdgenericOps.go and extracts op entries along with |
| // their ARCH: tags. It strips currentArch from all parsed entries and drops those |
| // with no remaining tags (ops that belonged only to the current arch). |
| func parseOps(oldFile, currentArch string) ([]GenericOpsData, error) { |
| f, ferr := os.Open(oldFile) |
| if ferr != nil { |
| if os.IsNotExist(ferr) { |
| return nil, nil |
| } |
| return nil, ferr |
| } |
| defer f.Close() |
| |
| untagged := 0 |
| unmatched := 0 |
| noarch := 0 |
| |
| var result []GenericOpsData |
| scanner := bufio.NewScanner(f) |
| for scanner.Scan() { |
| line := scanner.Text() |
| matches := reEntry.FindStringSubmatch(line) |
| if matches == nil { |
| unmatched++ |
| continue |
| } |
| name := matches[1] |
| argLen, _ := strconv.Atoi(matches[2]) |
| comm := matches[3] == "true" // matches[3] may be empty if commutative is omitted |
| hasAux := strings.Contains(line, `aux: "UInt8"`) |
| archTag := matches[4] |
| |
| if archTag == "" { |
| untagged++ |
| // Skip untagged entries (shouldn't occur after initial run). |
| continue |
| } |
| |
| archs := slices.DeleteFunc(strings.Split(archTag, ","), func(a string) bool { return a == currentArch }) |
| if len(archs) == 0 { |
| noarch++ |
| continue |
| } |
| |
| result = append(result, GenericOpsData{ |
| OpName: name, |
| OpInLen: argLen, |
| Comm: comm, |
| HasAux: hasAux, |
| Archs: archs, |
| }) |
| } |
| if err := scanner.Err(); err != nil { |
| return nil, err |
| } |
| |
| return result, nil |
| } |
| |
| // mergeOps merges existing and new op slices using a merge-sort walk. |
| // Both inputs must be sorted by OpName (natural sort); the output is verified |
| // to be sorted. |
| // |
| // existing must contain only tags from other architectures. |
| // new entries must be tagged with currentArch. |
| // |
| // Overlapping names have their arch tags merged and properties verified for |
| // consistency (argLength, commutativity, HasAux). |
| func mergeOps(currentArch string, existing, new []GenericOpsData) ([]GenericOpsData, error) { |
| var ( |
| result []GenericOpsData |
| i, j int |
| ) |
| for i < len(existing) || j < len(new) { |
| var cmp int |
| if i < len(existing) && j < len(new) { |
| cmp = CompareNatural(existing[i].OpName, new[j].OpName) |
| } else if i < len(existing) { |
| cmp = -1 |
| } else { |
| cmp = 1 |
| } |
| |
| var entry GenericOpsData |
| switch cmp { |
| case -1: |
| entry = existing[i] |
| i++ |
| |
| case 1: |
| entry = new[j] |
| j++ |
| |
| case 0: |
| e, n := existing[i], new[j] |
| if e.OpInLen != n.OpInLen { |
| return nil, fmt.Errorf("simdgen: op %q has inconsistent argLength: existing=%d, new=%d", e.OpName, e.OpInLen, n.OpInLen) |
| } |
| if e.Comm != n.Comm { |
| return nil, fmt.Errorf("simdgen: op %q has inconsistent commutativity: existing=%v, new=%v", e.OpName, e.Comm, n.Comm) |
| } |
| if e.HasAux != n.HasAux { |
| return nil, fmt.Errorf("simdgen: op %q has inconsistent HasAux: existing=%v, new=%v", e.OpName, e.HasAux, n.HasAux) |
| } |
| entry = e |
| entry.Archs = append(entry.Archs, currentArch) |
| sort.Strings(entry.Archs) |
| i++ |
| j++ |
| } |
| if len(result) > 0 && CompareNatural(result[len(result)-1].OpName, entry.OpName) >= 0 { |
| return nil, fmt.Errorf("simdgen: mergeOps: sort invariant violated between %q and %q", |
| result[len(result)-1].OpName, entry.OpName) |
| } |
| result = append(result, entry) |
| } |
| |
| return result, nil |
| } |
| |
| // MergeSIMDGenericOps merges a new set of generic ops with the existing ones read |
| // from goroot "/" file. |
| func MergeSIMDGenericOps(newOps []GenericOpsData, oldFile, currentArch string) *bytes.Buffer { |
| thisArch := []string{currentArch} |
| for i := range newOps { |
| newOps[i].Archs = thisArch |
| } |
| |
| sortNatural := func(s []GenericOpsData) []GenericOpsData { |
| sort.Slice(s, func(i, j int) bool { |
| return CompareNatural(s[i].OpName, s[j].OpName) < 0 |
| }) |
| return s |
| } |
| |
| existing, err := parseOps(oldFile, currentArch) |
| if err != nil { |
| panic(fmt.Errorf("failed to parse existing %s: %w", oldFile, err)) |
| } |
| |
| merged, err := mergeOps(currentArch, sortNatural(existing), sortNatural(newOps)) |
| if err != nil { |
| panic(err) |
| } |
| |
| // Split into ops/opsImm. |
| type opData struct { |
| Ops []GenericOpsData |
| OpsImm []GenericOpsData |
| } |
| var opsData opData |
| for _, gOp := range merged { |
| if gOp.HasAux { |
| opsData.OpsImm = append(opsData.OpsImm, gOp) |
| } else { |
| opsData.Ops = append(opsData.Ops, gOp) |
| } |
| } |
| |
| t := TemplateNamed("simdgenericOps", simdGenericOpsTmpl) |
| buffer := new(bytes.Buffer) |
| buffer.WriteString(simdGenericOpsHeader) |
| |
| err = t.Execute(buffer, opsData) |
| if err != nil { |
| panic(fmt.Errorf("failed to execute template: %w", err)) |
| } |
| |
| return buffer |
| } |