blob: 6a3feb362d615a84f5e9c30a7248e774a48b4214 [file] [log] [blame]
// 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.
package main
import (
"fmt"
"log"
"regexp"
"strconv"
"strings"
"golang.org/x/arch/internal/unify"
"golang.org/x/arch/x86/xeddata"
"gopkg.in/yaml.v3"
)
const (
NOT_REG_CLASS = 0 // not a register
VREG_CLASS = 1 // classify as a vector register; see
GREG_CLASS = 2 // classify as a general register
)
// instVariant is a bitmap indicating a variant of an instruction that has
// optional parameters.
type instVariant uint8
const (
instVariantNone instVariant = 0
// instVariantMasked indicates that this is the masked variant of an
// optionally-masked instruction.
instVariantMasked instVariant = 1 << iota
)
var operandRemarks int
// TODO: Doc. Returns Values with Def domains.
func loadXED(xedPath string) []*unify.Value {
// TODO: Obviously a bunch more to do here.
db, err := xeddata.NewDatabase(xedPath)
if err != nil {
log.Fatalf("open database: %v", err)
}
var defs []*unify.Value
err = xeddata.WalkInsts(xedPath, func(inst *xeddata.Inst) {
inst.Pattern = xeddata.ExpandStates(db, inst.Pattern)
switch {
case inst.RealOpcode == "N":
return // Skip unstable instructions
case !(strings.HasPrefix(inst.Extension, "SSE") || strings.HasPrefix(inst.Extension, "AVX")):
// We're only intested in SSE and AVX instuctions.
return // Skip non-AVX or SSE instructions
}
if *flagDebugXED {
fmt.Printf("%s:\n%+v\n", inst.Pos, inst)
}
ops, err := decodeOperands(db, strings.Fields(inst.Operands))
if err != nil {
operandRemarks++
if *Verbose {
log.Printf("%s: [%s] %s", inst.Pos, inst.Opcode(), err)
}
return
}
applyQuirks(inst, ops)
defsPos := len(defs)
defs = append(defs, instToUVal(inst, ops)...)
if *flagDebugXED {
for i := defsPos; i < len(defs); i++ {
y, _ := yaml.Marshal(defs[i])
fmt.Printf("==>\n%s\n", y)
}
}
})
if err != nil {
log.Fatalf("walk insts: %v", err)
}
return defs
}
var (
maskRequiredRe = regexp.MustCompile(`VPCOMPRESS[BWDQ]|VCOMPRESSP[SD]`)
maskOptionalRe = regexp.MustCompile(`VPCMP(EQ|GT|U)?[BWDQ]|VCMPP[SD]`)
)
func applyQuirks(inst *xeddata.Inst, ops []operand) {
opc := inst.Opcode()
switch {
case maskRequiredRe.MatchString(opc):
// The mask on these instructions is marked optional, but the
// instruction is pointless without the mask.
for i, op := range ops {
if op, ok := op.(operandMask); ok {
op.optional = false
ops[i] = op
}
}
case maskOptionalRe.MatchString(opc):
// Conversely, these masks should be marked optional and aren't.
for i, op := range ops {
if op, ok := op.(operandMask); ok && op.action.r {
op.optional = true
ops[i] = op
}
}
}
}
type operandCommon struct {
action operandAction
}
// operandAction defines whether this operand is read and/or written.
//
// TODO: Should this live in [xeddata.Operand]?
type operandAction struct {
r bool // Read
w bool // Written
cr bool // Read is conditional (implies r==true)
cw bool // Write is conditional (implies w==true)
}
type operandMem struct {
operandCommon
// TODO
}
type vecShape struct {
elemBits int // Element size in bits
bits int // Register width in bits (total vector bits)
}
type operandVReg struct { // Vector register
operandCommon
vecShape
elemBaseType scalarBaseType
}
type operandGReg struct { // Vector register
operandCommon
vecShape
elemBaseType scalarBaseType
}
// operandMask is a vector mask.
//
// Regardless of the actual mask representation, the [vecShape] of this operand
// corresponds to the "bit for bit" type of mask. That is, elemBits gives the
// element width covered by each mask element, and bits/elemBits gives the total
// number of mask elements. (bits gives the total number of bits as if this were
// a bit-for-bit mask, which may be meaningless on its own.)
type operandMask struct {
operandCommon
vecShape
// Bits in the mask is w/bits.
allMasks bool // If set, size cannot be inferred because all operands are masks.
// Mask can be omitted, in which case it defaults to K0/"no mask"
optional bool
}
type operandImm struct {
operandCommon
bits int // Immediate size in bits
}
type operand interface {
common() operandCommon
addToDef(b *unify.DefBuilder)
}
func strVal(s any) *unify.Value {
return unify.NewValue(unify.NewStringExact(fmt.Sprint(s)))
}
func (o operandCommon) common() operandCommon {
return o
}
func (o operandMem) addToDef(b *unify.DefBuilder) {
// TODO: w, base
b.Add("class", strVal("memory"))
}
func (o operandVReg) addToDef(b *unify.DefBuilder) {
baseDomain, err := unify.NewStringRegex(o.elemBaseType.regex())
if err != nil {
panic("parsing baseRe: " + err.Error())
}
b.Add("class", strVal("vreg"))
b.Add("bits", strVal(o.bits))
b.Add("base", unify.NewValue(baseDomain))
// If elemBits == bits, then the vector can be ANY shape. This happens with,
// for example, logical ops.
if o.elemBits != o.bits {
b.Add("elemBits", strVal(o.elemBits))
}
}
func (o operandGReg) addToDef(b *unify.DefBuilder) {
baseDomain, err := unify.NewStringRegex(o.elemBaseType.regex())
if err != nil {
panic("parsing baseRe: " + err.Error())
}
b.Add("class", strVal("greg"))
b.Add("bits", strVal(o.bits))
b.Add("base", unify.NewValue(baseDomain))
if o.elemBits != o.bits {
b.Add("elemBits", strVal(o.elemBits))
}
}
func (o operandMask) addToDef(b *unify.DefBuilder) {
b.Add("class", strVal("mask"))
if o.allMasks {
// If all operands are masks, omit sizes and let unification determine mask sizes.
return
}
b.Add("elemBits", strVal(o.elemBits))
b.Add("bits", strVal(o.bits))
}
func (o operandImm) addToDef(b *unify.DefBuilder) {
b.Add("class", strVal("immediate"))
b.Add("bits", strVal(o.bits))
}
var actionEncoding = map[string]operandAction{
"r": {r: true},
"cr": {r: true, cr: true},
"w": {w: true},
"cw": {w: true, cw: true},
"rw": {r: true, w: true},
"crw": {r: true, w: true, cr: true},
"rcw": {r: true, w: true, cw: true},
}
func decodeOperand(db *xeddata.Database, operand string) (operand, error) {
op, err := xeddata.NewOperand(db, operand)
if err != nil {
log.Fatalf("parsing operand %q: %v", operand, err)
}
if *flagDebugXED {
fmt.Printf(" %+v\n", op)
}
if strings.HasPrefix(op.Name, "EMX_BROADCAST") {
// This refers to a set of macros defined in all-state.txt that set a
// BCAST operand to various fixed values. But the BCAST operand is
// itself suppressed and "internal", so I think we can just ignore this
// operand.
return nil, nil
}
// TODO: See xed_decoded_inst_operand_action. This might need to be more
// complicated.
action, ok := actionEncoding[op.Action]
if !ok {
return nil, fmt.Errorf("unknown action %q", op.Action)
}
common := operandCommon{action: action}
lhs := op.NameLHS()
if strings.HasPrefix(lhs, "MEM") {
// TODO: Width, base type
return operandMem{
operandCommon: common,
}, nil
} else if strings.HasPrefix(lhs, "REG") {
if op.Width == "mskw" {
// The mask operand doesn't specify a width. We have to infer it.
//
// XED uses the marker ZEROSTR to indicate that a mask operand is
// optional and, if omitted, implies K0, aka "no mask".
return operandMask{
operandCommon: common,
optional: op.Attributes["TXT=ZEROSTR"],
}, nil
} else {
class, regBits := decodeReg(op)
if class == NOT_REG_CLASS {
return nil, fmt.Errorf("failed to decode register %q", operand)
}
baseType, elemBits, ok := decodeType(op)
if !ok {
return nil, fmt.Errorf("failed to decode register width %q", operand)
}
shape := vecShape{elemBits: elemBits, bits: regBits}
if class == VREG_CLASS {
return operandVReg{
operandCommon: common,
vecShape: shape,
elemBaseType: baseType,
}, nil
}
// general register
m := min(shape.bits, shape.elemBits)
shape.bits, shape.elemBits = m, m
return operandGReg{
operandCommon: common,
vecShape: shape,
elemBaseType: baseType,
}, nil
}
} else if strings.HasPrefix(lhs, "IMM") {
_, bits, ok := decodeType(op)
if !ok {
return nil, fmt.Errorf("failed to decode register width %q", operand)
}
return operandImm{
operandCommon: common,
bits: bits,
}, nil
}
// TODO: BASE and SEG
return nil, fmt.Errorf("unknown operand LHS %q in %q", lhs, operand)
}
func decodeOperands(db *xeddata.Database, operands []string) (ops []operand, err error) {
// Decode the XED operand descriptions.
for _, o := range operands {
op, err := decodeOperand(db, o)
if err != nil {
return nil, err
}
if op != nil {
ops = append(ops, op)
}
}
// XED doesn't encode the size of mask operands. If there are mask operands,
// try to infer their sizes from other operands.
if err := inferMaskSizes(ops); err != nil {
return nil, fmt.Errorf("%w in operands %+v", err, operands)
}
return ops, nil
}
func inferMaskSizes(ops []operand) error {
// This is a heuristic and it falls apart in some cases:
//
// - Mask operations like KAND[BWDQ] have *nothing* in the XED to indicate
// mask size.
//
// - VINSERT*, VPSLL*, VPSRA*, and VPSRL* and some others naturally have
// mixed input sizes and the XED doesn't indicate which operands the mask
// applies to.
//
// - VPDP* and VP4DP* have really complex mixed operand patterns.
//
// I think for these we may just have to hand-write a table of which
// operands each mask applies to.
inferMask := func(r, w bool) error {
var masks []int
var rSizes, wSizes, sizes []vecShape
allMasks := true
hasWMask := false
for i, op := range ops {
action := op.common().action
if _, ok := op.(operandMask); ok {
if action.r && action.w {
return fmt.Errorf("unexpected rw mask")
}
if action.r == r || action.w == w {
masks = append(masks, i)
}
if action.w {
hasWMask = true
}
} else {
allMasks = false
if reg, ok := op.(operandVReg); ok {
if action.r {
rSizes = append(rSizes, reg.vecShape)
}
if action.w {
wSizes = append(wSizes, reg.vecShape)
}
}
}
}
if len(masks) == 0 {
return nil
}
if r {
sizes = rSizes
if len(sizes) == 0 {
sizes = wSizes
}
}
if w {
sizes = wSizes
if len(sizes) == 0 {
sizes = rSizes
}
}
if len(sizes) == 0 {
// If all operands are masks, leave the mask inferrence to the users.
if allMasks {
for _, i := range masks {
m := ops[i].(operandMask)
m.allMasks = true
ops[i] = m
}
return nil
}
return fmt.Errorf("cannot infer mask size: no register operands")
}
shape, ok := singular(sizes)
if !ok {
if !hasWMask && len(wSizes) == 1 && len(masks) == 1 {
// This pattern looks like predicate mask, so its shape should align with the
// output. TODO: verify this is a safe assumption.
shape = wSizes[0]
} else {
return fmt.Errorf("cannot infer mask size: multiple register sizes %v", sizes)
}
}
for _, i := range masks {
m := ops[i].(operandMask)
m.vecShape = shape
ops[i] = m
}
return nil
}
if err := inferMask(true, false); err != nil {
return err
}
if err := inferMask(false, true); err != nil {
return err
}
return nil
}
// addOperandstoDef adds "in", "inVariant", and "out" to an instruction Def.
//
// Optional mask input operands are added to the inVariant field if
// variant&instVariantMasked, and omitted otherwise.
func addOperandsToDef(ops []operand, instDB *unify.DefBuilder, variant instVariant) {
var inVals, inVar, outVals []*unify.Value
asmPos := 0
for _, op := range ops {
var db unify.DefBuilder
op.addToDef(&db)
db.Add("asmPos", unify.NewValue(unify.NewStringExact(fmt.Sprint(asmPos))))
action := op.common().action
asmCount := 1 // # of assembly operands; 0 or 1
if action.r {
inVal := unify.NewValue(db.Build())
// If this is an optional mask, put it in the input variant tuple.
if mask, ok := op.(operandMask); ok && mask.optional {
if variant&instVariantMasked != 0 {
inVar = append(inVar, inVal)
} else {
// This operand doesn't appear in the assembly at all.
asmCount = 0
}
} else {
// Just a regular input operand.
inVals = append(inVals, inVal)
}
}
if action.w {
outVal := unify.NewValue(db.Build())
outVals = append(outVals, outVal)
}
asmPos += asmCount
}
instDB.Add("in", unify.NewValue(unify.NewTuple(inVals...)))
instDB.Add("inVariant", unify.NewValue(unify.NewTuple(inVar...)))
instDB.Add("out", unify.NewValue(unify.NewTuple(outVals...)))
}
func instToUVal(inst *xeddata.Inst, ops []operand) []*unify.Value {
var vals []*unify.Value
vals = append(vals, instToUVal1(inst, ops, instVariantNone))
if hasOptionalMask(ops) {
vals = append(vals, instToUVal1(inst, ops, instVariantMasked))
}
return vals
}
func instToUVal1(inst *xeddata.Inst, ops []operand, variant instVariant) *unify.Value {
// TODO: "feature"
var db unify.DefBuilder
db.Add("goarch", unify.NewValue(unify.NewStringExact("amd64")))
db.Add("asm", unify.NewValue(unify.NewStringExact(inst.Opcode())))
addOperandsToDef(ops, &db, variant)
db.Add("extension", unify.NewValue(unify.NewStringExact(inst.Extension)))
db.Add("isaset", unify.NewValue(unify.NewStringExact(inst.ISASet)))
if strings.Contains(inst.Pattern, "ZEROING=0") {
// This is an EVEX instruction, but the ".Z" (zero-merging)
// instruction flag is NOT valid. EVEX.z must be zero.
//
// This can mean a few things:
//
// - The output of an instruction is a mask, so merging modes don't
// make any sense. E.g., VCMPPS.
//
// - There are no masks involved anywhere. (Maybe MASK=0 is also set
// in this case?) E.g., VINSERTPS.
//
// - The operation inherently performs merging. E.g., VCOMPRESSPS
// with a mem operand.
//
// There may be other reasons.
db.Add("zeroing", unify.NewValue(unify.NewStringExact("false")))
}
pos := unify.Pos{Path: inst.Pos.Path, Line: inst.Pos.Line}
return unify.NewValuePos(db.Build(), pos)
}
// hasOptionalMask returns whether there is an optional mask operand in ops.
func hasOptionalMask(ops []operand) bool {
for _, op := range ops {
if op, ok := op.(operandMask); ok && op.optional {
return true
}
}
return false
}
func singular[T comparable](xs []T) (T, bool) {
if len(xs) == 0 {
return *new(T), false
}
for _, x := range xs[1:] {
if x != xs[0] {
return *new(T), false
}
}
return xs[0], true
}
// decodeReg returns class (NOT_REG_CLASS, VREG_CLASS, GREG_CLASS),
// and width in bits. If the operand cannot be decided as a register,
// then the clas is NOT_REG_CLASS.
func decodeReg(op *xeddata.Operand) (class, width int) {
// op.Width tells us the total width, e.g.,:
//
// dq => 128 bits (XMM)
// qq => 256 bits (YMM)
// mskw => K
// z[iuf?](8|16|32|...) => 512 bits (ZMM)
//
// But the encoding is really weird and it's not clear if these *always*
// mean XMM/YMM/ZMM or if other irregular things can use these large widths.
// Hence, we dig into the register sets themselves.
if !strings.HasPrefix(op.NameLHS(), "REG") {
return NOT_REG_CLASS, 0
}
// TODO: We shouldn't be relying on the macro naming conventions. We should
// use all-dec-patterns.txt, but xeddata doesn't support that table right now.
rhs := op.NameRHS()
if !strings.HasSuffix(rhs, "()") {
return NOT_REG_CLASS, 0
}
switch {
case strings.HasPrefix(rhs, "XMM_"):
return VREG_CLASS, 128
case strings.HasPrefix(rhs, "YMM_"):
return VREG_CLASS, 256
case strings.HasPrefix(rhs, "ZMM_"):
return VREG_CLASS, 512
case strings.HasPrefix(rhs, "GPR64_"), strings.HasPrefix(rhs, "VGPR64_"):
return GREG_CLASS, 64
case strings.HasPrefix(rhs, "GPR32_"), strings.HasPrefix(rhs, "VGPR32_"):
return GREG_CLASS, 32
}
return NOT_REG_CLASS, 0
}
var xtypeRe = regexp.MustCompile(`^([iuf])([0-9]+)$`)
// scalarBaseType describes the base type of a scalar element. This is a Go
// type, but without the bit width suffix (with the exception of
// scalarBaseIntOrUint).
type scalarBaseType int
const (
scalarBaseInt scalarBaseType = iota
scalarBaseUint
scalarBaseIntOrUint // Signed or unsigned is unspecified
scalarBaseFloat
scalarBaseComplex
scalarBaseBFloat
scalarBaseHFloat
)
func (s scalarBaseType) regex() string {
switch s {
case scalarBaseInt:
return "int"
case scalarBaseUint:
return "uint"
case scalarBaseIntOrUint:
return "int|uint"
case scalarBaseFloat:
return "float"
case scalarBaseComplex:
return "complex"
case scalarBaseBFloat:
return "BFloat"
case scalarBaseHFloat:
return "HFloat"
}
panic(fmt.Sprintf("unknown scalar base type %d", s))
}
func decodeType(op *xeddata.Operand) (base scalarBaseType, bits int, ok bool) {
// The xtype tells you the element type. i8, i16, i32, i64, f32, etc.
//
// TODO: Things like AVX2 VPAND have an xtype of u256 because they're
// element-width agnostic. Do I map that to all widths, or just omit the
// element width and let unification flesh it out? There's no u512
// (presumably those are all masked, so elem width matters). These are all
// Category: LOGICAL, so maybe we could use that info?
// Handle some weird ones.
switch op.Xtype {
// 8-bit float formats as defined by Open Compute Project "OCP 8-bit
// Floating Point Specification (OFP8)".
case "bf8": // E5M2 float
return scalarBaseBFloat, 8, true
case "hf8": // E4M3 float
return scalarBaseHFloat, 8, true
case "bf16": // bfloat16 float
return scalarBaseBFloat, 16, true
case "2f16":
// Complex consisting of 2 float16s. Doesn't exist in Go, but we can say
// what it would be.
return scalarBaseComplex, 32, true
case "2i8", "2I8":
// These just use the lower INT8 in each 16 bit field.
// As far as I can tell, "2I8" is a typo.
return scalarBaseInt, 8, true
case "2u16", "2U16":
// some VPDP* has it
// TODO: does "z" means it has zeroing?
return scalarBaseUint, 16, true
case "2i16", "2I16":
// some VPDP* has it
return scalarBaseInt, 16, true
case "4u8", "4U8":
// some VPDP* has it
return scalarBaseUint, 8, true
case "4i8", "4I8":
// some VPDP* has it
return scalarBaseInt, 8, true
}
// The rest follow a simple pattern.
m := xtypeRe.FindStringSubmatch(op.Xtype)
if m == nil {
// TODO: Report unrecognized xtype
return 0, 0, false
}
bits, _ = strconv.Atoi(m[2])
switch m[1] {
case "i", "u":
// XED is rather inconsistent about what's signed, unsigned, or doesn't
// matter, so merge them together and let the Go definitions narrow as
// appropriate. Maybe there's a better way to do this.
return scalarBaseIntOrUint, bits, true
case "f":
return scalarBaseFloat, bits, true
default:
panic("unreachable")
}
}