blob: cdd2a25f1c73e75775830288329fa3a128a4f572 [file]
// Copyright 2025 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.
// simdgen is an experiment in generating Go <-> asm SIMD mappings.
//
// Usage: simdgen [-arch=amd64|arm64|sve] [-xedPath=path] [-arm64Path=path] [-q=query] input.yaml...
//
// The external data inputs (XED data for amd64 or ARM64 ISA XML specs for arm64 and sve)
// are resolved automatically via search paths including standard environment variables
// ($XEDPATH or $ARM64_ISA_PATH), local directories populated by fetch-xed.sh /
// fetch-arm64.sh (e.g. ../extern/), or default $HOME locations. Explicit -xedPath
// or -arm64Path flags may be supplied to override search paths.
//
// If arch is amd64, one of the inputs is a sum of op-code definitions generated
// from the Intel XED data (which can be downloaded via fetch-xed.sh).
//
// If arch is arm64, one of the inputs is a set of NEON (advsimd) instruction
// definitions parsed from ARM64 ISA XML files (which can be downloaded via
// fetch-arm64.sh).
//
// Likewise, if arch is sve, one of the inputs is the set of SVE / SVE2
// instruction definitions parsed from the ARM64 ISA XML files.
//
// If input YAML files are provided, each file is read as an input value. See
// [unify.Closure.UnmarshalYAML] or "go doc unify.Closure.UnmarshalYAML" for the
// format of these files.
//
// TODO: Example definitions and values.
//
// The command unifies across all of the inputs and prints all possible results
// of this unification.
//
// If the -q flag is provided, its string value is parsed as a value and treated
// as another input to unification. This is intended as a way to "query" the
// result, typically by narrowing it down to a small subset of results.
//
// Typical usage:
//
// go run . *.yaml
//
// To see just the definitions generated from XED, run:
//
// go run . -arch=amd64
//
// (This works because if there's only one input, there's nothing to unify it
// with, so the result is simply itself.)
//
// To see just the definitions for VPADDQ on AMD64:
//
// go run . -arch=amd64 -q '{asm: VPADDQ}'
//
// For VADD.S4 on ARM64:
//
// go run . -arch arm64 -q '{asm: VADD, arrangement: "4S"}'
//
// simdgen can also generate Go definitions of SIMD mappings.
// To generate go files to the go root, run:
//
// go run . -arch amd64 -o godefs -goroot $PATH/TO/go go_amd64.yaml categories.yaml types.yaml
//
// For ARM64:
//
// go run . -arch arm64 -o godefs -goroot $PATH/TO/go go_arm64.yaml categories.yaml types.yaml
//
// types.yaml is already written, it specifies the shapes of vectors.
// categories.yaml and go_<arch>.yaml contain definitions that unify with types.yaml and
// XED/ARM64 ISA data, you can find an example in ops/AddSub/.
//
// When generating Go definitions, simdgen do 3 "magic"s:
// - It splits masked operations(with op's [Masked] field set) to const and non const:
// - One is a normal masked operation, the original
// - The other has its mask operand's [Const] fields set to "K0".
// - This way the user does not need to provide a separate "K0"-masked operation def.
//
// - It deduplicates intrinsic names that have duplicates:
// - If there are two operations that shares the same signature, one is AVX512 the other
// is before AVX512, the other will be selected.
// - This happens often when some operations are defined both before AVX512 and after.
// This way the user does not need to provide a separate "K0" operation for the
// AVX512 counterpart.
//
// - It copies the op's [ConstImm] field to its immediate operand's [Const] field.
// - This way the user does not need to provide verbose op definition while only
// the const immediate field is different. This is useful to reduce verbosity of
// compares with imm control predicates.
//
// These 3 magics could be disabled by enabling -nosplitmask, -nodedup or
// -noconstimmporting flags.
//
// simdgen supports amd64 and arm64 architectures.
package main
// Big TODOs:
//
// - This can produce duplicates, which can also lead to less efficient
// environment merging. Add hashing and use it for deduplication. Be careful
// about how this shows up in debug traces, since it could make things
// confusing if we don't show it happening.
//
// - Do I need Closure, Value, and Domain? It feels like I should only need two
// types.
import (
"cmp"
"flag"
"fmt"
"log"
"maps"
"os"
"path/filepath"
"runtime/pprof"
"slices"
"strings"
"simd/archsimd/_gen/gentools"
"simd/archsimd/_gen/sgutil"
"simd/archsimd/_gen/simdgen/arm64"
"simd/archsimd/_gen/simdgen/sve"
"simd/archsimd/_gen/unify"
"gopkg.in/yaml.v3"
)
var (
flagXedPath = sgutil.FlagXEDPath("..")
flagArm64Path = sgutil.FlagARM64Path("..")
flagQ = flag.String("q", "", "query: read `def` as another input (skips final validation)")
flagO = flag.String("o", "yaml", "output type: yaml, godefs (generate definitions into a Go source tree")
genFlags = gentools.RegisterFlags(nil)
FlagNoDedup = flag.Bool("nodedup", false, "disable deduplicating godefs of 2 qualifying operations from different extensions")
FlagNoConstImmPorting = flag.Bool("noconstimmporting", false, "disable const immediate porting from op to imm operand")
FlagArch = flag.String("arch", "", "unify with architecture definitions for `arch`\n\tif amd64, loads from -xedPath\n\tif arm64 or sve, loads from -arm64Path")
Verbose = flag.Bool("v", false, "verbose")
flagDebugXED = flag.Bool("debug-xed", false, "show XED instructions")
flagDebugUnify = flag.Bool("debug-unify", false, "print unification trace")
flagDebugHTML = flag.String("debug-html", "", "write unification trace to `file.html`")
FlagReportDup = flag.Bool("reportdup", false, "report the duplicate godefs")
flagCPUProfile = flag.String("cpuprofile", "", "write CPU profile to `file`")
flagMemProfile = flag.String("memprofile", "", "write memory profile to `file`")
)
const simdPackage = "simd/archsimd"
func main() {
flag.Parse()
if *flagCPUProfile != "" {
f, err := os.Create(*flagCPUProfile)
if err != nil {
log.Fatalf("-cpuprofile: %s", err)
}
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
if *flagMemProfile != "" {
f, err := os.Create(*flagMemProfile)
if err != nil {
log.Fatalf("-memprofile: %s", err)
}
defer func() {
pprof.WriteHeapProfile(f)
f.Close()
}()
}
// Load instructions into the architecture-specific defs set.
var defs []*unify.Value
switch *FlagArch {
case "":
// No input from architecture definitions. That's fine if we're just
// emitting generic yaml, but if we're emitting godefs we need to know
// the arch.
if *flagO == "godefs" {
log.Fatalf("-o godefs requires -arch")
}
case "amd64":
xedPath, err := sgutil.ResolveXEDPath(flagXedPath)
if err != nil {
log.Fatal(err)
}
defs = loadXED(xedPath)
case "arm64":
arm64Path, err := sgutil.ResolveARM64Path(flagArm64Path)
if err != nil {
log.Fatal(err)
}
defs, err = arm64.Load(arm64Path)
if err != nil {
log.Fatalf("loading ARM64 instructions: %s", err)
}
case "sve":
arm64Path, err := sgutil.ResolveARM64Path(flagArm64Path)
if err != nil {
log.Fatal(err)
}
defs, err = sve.Load(arm64Path)
if err != nil {
log.Fatalf("loading ARM64 SVE instructions: %s", err)
}
default:
log.Fatalf("-arch must be one of: amd64, arm64, sve")
}
var inputs []unify.Closure
if defs != nil {
inputs = append(inputs, unify.NewSum(defs...))
}
// Load query.
if *flagQ != "" {
r := strings.NewReader(*flagQ)
def, err := unify.Read(r, "<query>", unify.ReadOpts{})
if err != nil {
log.Fatalf("parsing -q: %s", err)
}
inputs = append(inputs, def)
}
// Load defs files.
must := make(map[*unify.Value]struct{})
for _, path := range flag.Args() {
defs, err := unify.ReadFile(path, unify.ReadOpts{})
if err != nil {
log.Fatal(err)
}
inputs = append(inputs, defs)
base := filepath.Base(path)
if base == "go_amd64.yaml" || base == "go_arm64.yaml" || base == "go_sve.yaml" {
// These must all be used in the final result
for def := range defs.Summands() {
must[def] = struct{}{}
}
}
}
// Prepare for unification
if *flagDebugUnify {
unify.Debug.UnifyLog = os.Stderr
}
if *flagDebugHTML != "" {
f, err := os.Create(*flagDebugHTML)
if err != nil {
log.Fatal(err)
}
unify.Debug.HTML = f
defer f.Close()
}
// Unify!
unified, err := unify.Unify(inputs...)
if err != nil {
log.Fatal(err)
}
ok := true
// Validate results.
//
// Don't validate if this is a command-line query because that tends to
// eliminate lots of required defs and is used in cases where maybe defs
// aren't enumerable anyway.
if *flagQ == "" && len(must) > 0 {
ok = validate(unified, must)
}
// Print results.
switch *flagO {
case "yaml":
// Produce a result that looks like encoding a slice, but stream it.
fmt.Println("!sum")
var val1 [1]*unify.Value
for val := range unified.All() {
val1[0] = val
// We have to make a new encoder each time or it'll print a document
// separator between each object.
enc := yaml.NewEncoder(os.Stdout)
if err := enc.Encode(val1); err != nil {
log.Fatal(err)
}
enc.Close()
}
case "godefs":
if err := writeGoDefs(unified); err != nil {
log.Fatalf("Failed writing godefs: %+v", err)
}
}
if !*Verbose && *FlagArch == "amd64" {
if operandRemarks == 0 {
fmt.Fprintf(os.Stderr, "XED decoding generated no errors, which is unusual.\n")
} else {
fmt.Fprintf(os.Stderr, "XED decoding generated %d \"errors\" which is not cause for alarm, use -v for details.\n", operandRemarks)
}
}
if !ok {
os.Exit(1)
}
}
func validate(cl unify.Closure, required map[*unify.Value]struct{}) bool {
ok := true
// Validate that:
// 1. All final defs are exact
// 2. All required defs are used
for def := range cl.All() {
if _, ok := def.Domain.(unify.Def); !ok {
fmt.Fprintf(os.Stderr, "%s: expected Def, got %T\n", def.PosString(), def.Domain)
continue
}
if !def.Exact() {
fmt.Fprintf(os.Stderr, "%s: def not reduced to an exact value, why is %s:\n", def.PosString(), def.WhyNotExact())
fmt.Fprintf(os.Stderr, "\t%s\n", strings.ReplaceAll(def.String(), "\n", "\n\t"))
}
for root := range def.Provenance() {
delete(required, root)
}
}
// Report unused defs
unused := slices.SortedFunc(maps.Keys(required),
func(a, b *unify.Value) int {
return cmp.Or(
cmp.Compare(a.Pos().Path, b.Pos().Path),
cmp.Compare(a.Pos().Line, b.Pos().Line),
)
})
for _, def := range unused {
// TODO: Can we say anything more actionable? This is always a problem
// with unification: if it fails, it's very hard to point a finger at
// any particular reason. We could go back and try unifying this again
// with each subset of the inputs (starting with individual inputs) to
// at least say "it doesn't unify with anything in x.yaml". That's a lot
// of work, but if we have trouble debugging unification failure it may
// be worth it.
fmt.Fprintf(os.Stderr, "%s: def required, but did not unify (%v)\n",
def.PosString(), def)
ok = false
}
return ok
}