all: AVX-512 (#217)
Extends avo to support most AVX-512 instruction sets.
The instruction type is extended to support suffixes. The K family of opmask
registers is added to the register package, and the operand package is updated
to support the new operand types. Move instruction deduction in `Load` and
`Store` is extended to support KMOV* and VMOV* forms.
Internal code generation packages were overhauled. Instruction database loading
required various messy changes to account for the additional complexities of the
AVX-512 instruction sets. The internal/api package was added to introduce a
separation between instruction forms in the database, and the functions avo
provides to create them. This was required since with instruction suffixes there
is no longer a one-to-one mapping between instruction constructors and opcodes.
AVX-512 bloated generated source code size substantially, initially increasing
compilation and CI test times to an unacceptable level. Two changes were made to
address this:
1. Instruction constructors in the `x86` package moved to an optab-based
approach. This compiles substantially faster than the verbose code
generation we had before.
2. The most verbose code-generated tests are moved under build tags and
limited to a stress test mode. Stress test builds are run on
schedule but not in regular CI.
An example of AVX-512 accelerated 16-lane MD5 is provided to demonstrate and
test the new functionality.
Updates #20 #163 #229
Co-authored-by: Vaughn Iverson <vsivsi@yahoo.com>
This commit is contained in:
2
internal/api/doc.go
Normal file
2
internal/api/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package api determines the API of avo instruction constructors.
|
||||
package api
|
||||
177
internal/api/function.go
Normal file
177
internal/api/function.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
)
|
||||
|
||||
// Function represents a function that constructs some collection of
|
||||
// instruction forms.
|
||||
type Function struct {
|
||||
Instruction inst.Instruction
|
||||
Suffixes inst.Suffixes
|
||||
inst.Forms
|
||||
}
|
||||
|
||||
// Name returns the function name.
|
||||
func (f *Function) Name() string {
|
||||
return f.opcodesuffix("_")
|
||||
}
|
||||
|
||||
// Opcode returns the full Go opcode of the instruction built by this function. Includes any suffixes.
|
||||
func (f *Function) Opcode() string {
|
||||
return f.opcodesuffix(".")
|
||||
}
|
||||
|
||||
func (f *Function) opcodesuffix(sep string) string {
|
||||
n := f.Instruction.Opcode
|
||||
for _, suffix := range f.Suffixes {
|
||||
n += sep
|
||||
n += suffix.String()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// HasSuffix reports whether the function has the provided suffix.
|
||||
func (f *Function) HasSuffix(suffix inst.Suffix) bool {
|
||||
for _, s := range f.Suffixes {
|
||||
if s == suffix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Summary returns a summary of the instruction this function constructs.
|
||||
func (f *Function) Summary() string {
|
||||
summary := f.Instruction.Summary
|
||||
if len(f.Suffixes) > 0 {
|
||||
summary += " (" + strings.Join(f.Suffixes.Summaries(), ", ") + ")"
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// Doc returns the function document comment as a list of lines.
|
||||
func (f *Function) Doc() []string {
|
||||
lines := []string{
|
||||
fmt.Sprintf("%s: %s.", f.Name(), f.Summary()),
|
||||
"",
|
||||
"Forms:",
|
||||
"",
|
||||
}
|
||||
|
||||
// Write a table of instruction forms.
|
||||
buf := bytes.NewBuffer(nil)
|
||||
w := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
|
||||
for _, form := range f.Forms {
|
||||
row := f.Opcode() + "\t" + strings.Join(form.Signature(), "\t") + "\n"
|
||||
fmt.Fprint(w, row)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
tbl := strings.TrimSpace(buf.String())
|
||||
for _, line := range strings.Split(tbl, "\n") {
|
||||
lines = append(lines, "\t"+line)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// Signature of the function. Derived from the instruction forms generated by this function.
|
||||
func (f *Function) Signature() Signature {
|
||||
// Handle the case of forms with multiple arities.
|
||||
switch {
|
||||
case f.IsVariadic():
|
||||
return variadic{name: "ops"}
|
||||
case f.IsNiladic():
|
||||
return niladic{}
|
||||
}
|
||||
|
||||
// Generate nice-looking variable names.
|
||||
n := f.Arity()
|
||||
ops := make([]string, n)
|
||||
count := map[string]int{}
|
||||
for j := 0; j < n; j++ {
|
||||
// Collect unique lowercase bytes from first characters of operand types.
|
||||
s := map[byte]bool{}
|
||||
for _, form := range f.Forms {
|
||||
c := form.Operands[j].Type[0]
|
||||
if 'a' <= c && c <= 'z' {
|
||||
s[c] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Operand name is the sorted bytes.
|
||||
var b []byte
|
||||
for c := range s {
|
||||
b = append(b, c)
|
||||
}
|
||||
sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
|
||||
name := string(b)
|
||||
|
||||
// Append a counter if we've seen it already.
|
||||
m := count[name]
|
||||
count[name]++
|
||||
if m > 0 {
|
||||
name += strconv.Itoa(m)
|
||||
}
|
||||
ops[j] = name
|
||||
}
|
||||
|
||||
return argslist(ops)
|
||||
}
|
||||
|
||||
// InstructionFunctions builds the list of all functions for a given
|
||||
// instruction.
|
||||
func InstructionFunctions(i inst.Instruction) []*Function {
|
||||
// One function for each possible suffix combination.
|
||||
bysuffix := map[string]*Function{}
|
||||
for _, f := range i.Forms {
|
||||
for _, suffixes := range f.SupportedSuffixes() {
|
||||
k := suffixes.String()
|
||||
if _, ok := bysuffix[k]; !ok {
|
||||
bysuffix[k] = &Function{
|
||||
Instruction: i,
|
||||
Suffixes: suffixes,
|
||||
}
|
||||
}
|
||||
bysuffix[k].Forms = append(bysuffix[k].Forms, f)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to a sorted slice.
|
||||
var fns []*Function
|
||||
for _, fn := range bysuffix {
|
||||
fns = append(fns, fn)
|
||||
}
|
||||
|
||||
SortFunctions(fns)
|
||||
|
||||
return fns
|
||||
}
|
||||
|
||||
// InstructionsFunctions builds all functions for a list of instructions.
|
||||
func InstructionsFunctions(is []inst.Instruction) []*Function {
|
||||
var all []*Function
|
||||
for _, i := range is {
|
||||
fns := InstructionFunctions(i)
|
||||
all = append(all, fns...)
|
||||
}
|
||||
|
||||
SortFunctions(all)
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// SortFunctions sorts a list of functions by name.
|
||||
func SortFunctions(fns []*Function) {
|
||||
sort.Slice(fns, func(i, j int) bool {
|
||||
return fns[i].Name() < fns[j].Name()
|
||||
})
|
||||
}
|
||||
50
internal/api/function_test.go
Normal file
50
internal/api/function_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
)
|
||||
|
||||
func TestFunctionsDuplicateFormSignatures(t *testing.T) {
|
||||
// Test for duplicate form signatures within a given function. This could
|
||||
// manifest as duplicate case statements in generated code.
|
||||
fns := InstructionsFunctions(inst.Instructions)
|
||||
for _, fn := range fns {
|
||||
fn := fn // scopelint
|
||||
t.Run(fn.Name(), func(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for _, f := range fn.Forms {
|
||||
sig := strings.Join(f.Signature(), "_")
|
||||
t.Log(sig)
|
||||
if seen[sig] {
|
||||
t.Fatalf("duplicate: %s", sig)
|
||||
}
|
||||
seen[sig] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionsUniqueArgNames(t *testing.T) {
|
||||
fns := InstructionsFunctions(inst.Instructions)
|
||||
for _, fn := range fns {
|
||||
s := fn.Signature()
|
||||
for _, n := range fn.Arities() {
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for j := 0; j < n; j++ {
|
||||
names[s.ParameterName(j)] = true
|
||||
}
|
||||
if len(names) != n {
|
||||
t.Errorf("repeated argument for instruction %s", fn.Name())
|
||||
}
|
||||
if _, found := names[""]; found {
|
||||
t.Errorf("empty argument name for instruction %s", fn.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
internal/api/signature.go
Normal file
59
internal/api/signature.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Signature provides access to details about the signature of an instruction function.
|
||||
type Signature interface {
|
||||
ParameterList() string
|
||||
Arguments() string
|
||||
ParameterName(int) string
|
||||
ParameterSlice() string
|
||||
Length() string
|
||||
}
|
||||
|
||||
// ArgsList builds a signature for a function with the named parameters.
|
||||
func ArgsList(args []string) Signature {
|
||||
return argslist(args)
|
||||
}
|
||||
|
||||
type argslist []string
|
||||
|
||||
func (a argslist) ParameterList() string { return strings.Join(a, ", ") + " " + OperandType }
|
||||
func (a argslist) Arguments() string { return strings.Join(a, ", ") }
|
||||
func (a argslist) ParameterName(i int) string { return a[i] }
|
||||
func (a argslist) ParameterSlice() string {
|
||||
return fmt.Sprintf("[]%s{%s}", OperandType, strings.Join(a, ", "))
|
||||
}
|
||||
func (a argslist) Length() string { return strconv.Itoa(len(a)) }
|
||||
|
||||
// Variadic is the signature for a variadic function with the named argument slice.
|
||||
func Variadic(name string) Signature {
|
||||
return variadic{name: name}
|
||||
}
|
||||
|
||||
type variadic struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (v variadic) ParameterList() string { return v.name + " ..." + OperandType }
|
||||
func (v variadic) Arguments() string { return v.name + "..." }
|
||||
func (v variadic) ParameterName(i int) string { return fmt.Sprintf("%s[%d]", v.name, i) }
|
||||
func (v variadic) ParameterSlice() string { return v.name }
|
||||
func (v variadic) Length() string { return fmt.Sprintf("len(%s)", v.name) }
|
||||
|
||||
// Niladic is the signature for a function with no arguments.
|
||||
func Niladic() Signature {
|
||||
return niladic{}
|
||||
}
|
||||
|
||||
type niladic struct{}
|
||||
|
||||
func (n niladic) ParameterList() string { return "" }
|
||||
func (n niladic) Arguments() string { return "" }
|
||||
func (n niladic) ParameterName(i int) string { panic("niladic function has no parameters") }
|
||||
func (n niladic) ParameterSlice() string { return fmt.Sprintf("[]%s{}", OperandType) }
|
||||
func (n niladic) Length() string { return "0" }
|
||||
75
internal/api/types.go
Normal file
75
internal/api/types.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// Package is the avo package import path.
|
||||
Package = "github.com/mmcloughlin/avo"
|
||||
|
||||
// IRPackage is the package that defines intermediate representation types.
|
||||
IRPackage = "ir"
|
||||
|
||||
// OperandPackage is the package for operand types.
|
||||
OperandPackage = "operand"
|
||||
|
||||
// OperandType is the type used for operands.
|
||||
OperandType = OperandPackage + ".Op"
|
||||
|
||||
// RegisterPackage is the name of the package containing register types.
|
||||
RegisterPackage = "reg"
|
||||
|
||||
// RegisterType is the type used for registers.
|
||||
RegisterType = RegisterPackage + ".Register"
|
||||
)
|
||||
|
||||
// ImportPath returns the full import path for an avo subpackage.
|
||||
func ImportPath(pkg string) string {
|
||||
return path.Join(Package, pkg)
|
||||
}
|
||||
|
||||
// ImplicitRegisterIdentifier maps an implicit register name to a string
|
||||
// suitable for a related Go identifier.
|
||||
func ImplicitRegisterIdentifier(r string) string {
|
||||
r = strings.Replace(r, "mm", "", 1) // handles the "xmm0" type
|
||||
return strings.ToUpper(r)
|
||||
}
|
||||
|
||||
// ImplicitRegister returns avo syntax for the given implicit register type (from Opcodes XML).
|
||||
func ImplicitRegister(r string) string {
|
||||
return RegisterPackage + "." + ImplicitRegisterIdentifier(r)
|
||||
}
|
||||
|
||||
// OperandTypeIdentifier maps an operand type to a string suitable for a related
|
||||
// Go identifier.
|
||||
func OperandTypeIdentifier(t string) string {
|
||||
return strings.ToUpper(strings.ReplaceAll(t, "/", ""))
|
||||
}
|
||||
|
||||
// CheckerName returns the name of the function that checks an operand of type t.
|
||||
func CheckerName(t string) string {
|
||||
return "operand.Is" + OperandTypeIdentifier(t)
|
||||
}
|
||||
|
||||
// ISAsIdentifier returns a string representation of an ISA list that suitable
|
||||
// for use in a Go identifier.
|
||||
func ISAsIdentifier(isas []string) string {
|
||||
if len(isas) == 0 {
|
||||
return "Base"
|
||||
}
|
||||
sorted := append([]string(nil), isas...)
|
||||
sort.Strings(sorted)
|
||||
ident := strings.Join(sorted, "_")
|
||||
ident = strings.ReplaceAll(ident, ".", "") // SSE4.1
|
||||
ident = strings.ReplaceAll(ident, "+", "") // MMX+
|
||||
return ident
|
||||
}
|
||||
|
||||
// SuffixesClassIdentifier returns a string representation of a suffix class
|
||||
// that's suitable for use in a Go identifier.
|
||||
func SuffixesClassIdentifier(c string) string {
|
||||
return strings.ToUpper(c)
|
||||
}
|
||||
26
internal/api/types_test.go
Normal file
26
internal/api/types_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
"testing"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
)
|
||||
|
||||
func TestISAsIdentifier(t *testing.T) {
|
||||
for _, isas := range inst.ISACombinations(inst.Instructions) {
|
||||
ident := ISAsIdentifier(isas)
|
||||
if !token.IsIdentifier(ident) {
|
||||
t.Errorf("expected %q to be an identifier", ident)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixesClassIdentifier(t *testing.T) {
|
||||
for key := range inst.SuffixesClasses(inst.Instructions) {
|
||||
ident := SuffixesClassIdentifier(key)
|
||||
if !token.IsIdentifier(ident) {
|
||||
t.Errorf("expected %q to be an identifier", ident)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,17 @@ import (
|
||||
)
|
||||
|
||||
var generators = map[string]gen.Builder{
|
||||
"asmtest": gen.NewAsmTest,
|
||||
"godata": gen.NewGoData,
|
||||
"godatatest": gen.NewGoDataTest,
|
||||
"ctors": gen.NewCtors,
|
||||
"ctorstest": gen.NewCtorsTest,
|
||||
"build": gen.NewBuild,
|
||||
"mov": gen.NewMOV,
|
||||
"asmtest": gen.NewAsmTest,
|
||||
"godata": gen.NewGoData,
|
||||
"godatatest": gen.NewGoDataTest,
|
||||
"optab": gen.NewOptab,
|
||||
"ctors": gen.NewCtors,
|
||||
"ctorstest": gen.NewCtorsTest,
|
||||
"ctorsstress": gen.NewCtorsStress,
|
||||
"ctorsbench": gen.NewCtorsBench,
|
||||
"build": gen.NewBuild,
|
||||
"buildtest": gen.NewBuildTest,
|
||||
"mov": gen.NewMOV,
|
||||
}
|
||||
|
||||
// Command-line flags.
|
||||
|
||||
257
internal/gen/api.go
Normal file
257
internal/gen/api.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
)
|
||||
|
||||
// Enum is a generated enumeration type. This assists with mapping between the
|
||||
// conceptual values of the enum, and it's materialization as Go code.
|
||||
type Enum struct {
|
||||
name string
|
||||
doc []string
|
||||
values []string
|
||||
}
|
||||
|
||||
// NewEnum initializes an empty enum type with the given name.
|
||||
func NewEnum(name string) *Enum {
|
||||
return &Enum{name: name}
|
||||
}
|
||||
|
||||
// Name returns the type name.
|
||||
func (e *Enum) Name() string { return e.name }
|
||||
|
||||
// Receiver returns the receiver variable name.
|
||||
func (e *Enum) Receiver() string {
|
||||
return strings.ToLower(e.name[:1])
|
||||
}
|
||||
|
||||
// SetDoc sets type documentation, as a list of lines.
|
||||
func (e *Enum) SetDoc(doc ...string) {
|
||||
e.doc = doc
|
||||
}
|
||||
|
||||
// Doc returns the type documentation.
|
||||
func (e *Enum) Doc() []string { return e.doc }
|
||||
|
||||
// AddValue adds a named enumerator.
|
||||
func (e *Enum) AddValue(value string) {
|
||||
e.values = append(e.values, value)
|
||||
}
|
||||
|
||||
// Values returns all enumerators.
|
||||
func (e *Enum) Values() []string { return e.values }
|
||||
|
||||
// None returns the name of the "unset" constant of this enumeration.
|
||||
func (e *Enum) None() string {
|
||||
return e.ConstName("None")
|
||||
}
|
||||
|
||||
// ConstName returns the constant name that refers to the given enumerator
|
||||
// value.
|
||||
func (e *Enum) ConstName(value string) string {
|
||||
return e.name + value
|
||||
}
|
||||
|
||||
// ConstNames returns the constant names for all enumerator values.
|
||||
func (e *Enum) ConstNames() []string {
|
||||
var consts []string
|
||||
for _, v := range e.values {
|
||||
consts = append(consts, e.ConstName(v))
|
||||
}
|
||||
return consts
|
||||
}
|
||||
|
||||
// MaxName returns the name of the constant that represents the maximum
|
||||
// enumerator. This value is placed at the very end of the enum, so all values
|
||||
// will be between the None and Max enumerators.
|
||||
func (e *Enum) MaxName() string {
|
||||
return strings.ToLower(e.ConstName("max"))
|
||||
}
|
||||
|
||||
// Max returns the value of the maximum enumerator.
|
||||
func (e *Enum) Max() int {
|
||||
return len(e.values)
|
||||
}
|
||||
|
||||
// UnderlyingType returns the underlying unsigned integer type used for the
|
||||
// enumeration. This will be the smallest type that can represent all the
|
||||
// values.
|
||||
func (e *Enum) UnderlyingType() string {
|
||||
b := uint(8)
|
||||
for ; b < 64 && e.Max() > ((1<<b)-1); b <<= 1 {
|
||||
}
|
||||
return fmt.Sprintf("uint%d", b)
|
||||
}
|
||||
|
||||
// Table represents all the types required to represent the instruction
|
||||
// operation table (optab).
|
||||
type Table struct {
|
||||
instructions []inst.Instruction
|
||||
|
||||
operandType *Enum
|
||||
implicitRegister *Enum
|
||||
suffix *Enum
|
||||
suffixesClass *Enum
|
||||
isas *Enum
|
||||
opcode *Enum
|
||||
}
|
||||
|
||||
// NewTable builds optab types to represent the given instructions.
|
||||
func NewTable(is []inst.Instruction) *Table {
|
||||
t := &Table{instructions: is}
|
||||
t.init()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Table) init() {
|
||||
// Operand type.
|
||||
t.operandType = NewEnum("oprndtype")
|
||||
types := inst.OperandTypes(t.instructions)
|
||||
for _, typ := range types {
|
||||
t.operandType.AddValue(api.OperandTypeIdentifier(typ))
|
||||
}
|
||||
|
||||
// Implicit register.
|
||||
registers := inst.ImplicitRegisters(t.instructions)
|
||||
t.implicitRegister = NewEnum("implreg")
|
||||
for _, r := range registers {
|
||||
t.implicitRegister.AddValue(api.ImplicitRegisterIdentifier(r))
|
||||
}
|
||||
|
||||
// Suffix.
|
||||
t.suffix = NewEnum("sffx")
|
||||
for _, s := range inst.UniqueSuffixes(t.instructions) {
|
||||
t.suffix.AddValue(s.String())
|
||||
}
|
||||
|
||||
// Suffixes class.
|
||||
t.suffixesClass = NewEnum("sffxscls")
|
||||
|
||||
classes := inst.SuffixesClasses(t.instructions)
|
||||
keys := make([]string, 0, len(classes))
|
||||
for key := range classes {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
t.suffixesClass.AddValue(api.SuffixesClassIdentifier(key))
|
||||
}
|
||||
|
||||
// ISAs.
|
||||
t.isas = NewEnum("isas")
|
||||
for _, isas := range inst.ISACombinations(t.instructions) {
|
||||
t.isas.AddValue(api.ISAsIdentifier(isas))
|
||||
}
|
||||
|
||||
// Opcodes.
|
||||
t.opcode = NewEnum("opc")
|
||||
for _, i := range t.instructions {
|
||||
t.opcode.AddValue(i.Opcode)
|
||||
}
|
||||
}
|
||||
|
||||
// OperandType returns the enumeration representing all possible operand types.
|
||||
func (t *Table) OperandType() *Enum { return t.operandType }
|
||||
|
||||
// OperandTypeConst returns the constant name for the given operand type.
|
||||
func (t *Table) OperandTypeConst(typ string) string {
|
||||
return t.operandType.ConstName(api.OperandTypeIdentifier(typ))
|
||||
}
|
||||
|
||||
// ImplicitRegister returns the enumeration representing all possible operand
|
||||
// types.
|
||||
func (t *Table) ImplicitRegister() *Enum { return t.implicitRegister }
|
||||
|
||||
// ImplicitRegisterConst returns the constant name for the given register.
|
||||
func (t *Table) ImplicitRegisterConst(r string) string {
|
||||
return t.implicitRegister.ConstName(api.ImplicitRegisterIdentifier(r))
|
||||
}
|
||||
|
||||
// Suffix returns the enumeration representing instruction suffixes.
|
||||
func (t *Table) Suffix() *Enum { return t.suffix }
|
||||
|
||||
// SuffixConst returns the constant name for the given instruction suffix.
|
||||
func (t *Table) SuffixConst(s inst.Suffix) string { return t.suffix.ConstName(s.String()) }
|
||||
|
||||
// SuffixesTypeName returns the name of the array type for a list of suffixes.
|
||||
func (t *Table) SuffixesTypeName() string {
|
||||
return t.Suffix().Name() + "s"
|
||||
}
|
||||
|
||||
// SuffixesConst returns the constant for a list of suffixes. Suffixes is a
|
||||
// generated array type, so the list is a value not slice type.
|
||||
func (t *Table) SuffixesConst(suffixes inst.Suffixes) string {
|
||||
var parts []string
|
||||
for _, suffix := range suffixes {
|
||||
parts = append(parts, t.SuffixConst(suffix))
|
||||
}
|
||||
return t.SuffixesTypeName() + "{" + strings.Join(parts, ", ") + "}"
|
||||
}
|
||||
|
||||
// SuffixesClass returns the enumeration representing all suffixes classes.
|
||||
func (t *Table) SuffixesClass() *Enum { return t.suffixesClass }
|
||||
|
||||
// SuffixesClassConst returns the constant name for a given suffixes class. The
|
||||
// key is as returned from inst.SuffixesClasses() function.
|
||||
func (t *Table) SuffixesClassConst(key string) string {
|
||||
ident := api.SuffixesClassIdentifier(key)
|
||||
if ident == "" {
|
||||
return t.suffixesClass.None()
|
||||
}
|
||||
return t.suffixesClass.ConstName(ident)
|
||||
}
|
||||
|
||||
// ISAs returns the enumeration for all possible ISA combinations.
|
||||
func (t *Table) ISAs() *Enum { return t.isas }
|
||||
|
||||
// ISAsConst returns the constant name for the given ISA combination.
|
||||
func (t *Table) ISAsConst(isas []string) string {
|
||||
return t.isas.ConstName(api.ISAsIdentifier(isas))
|
||||
}
|
||||
|
||||
// Opcode returns the opcode enumeration.
|
||||
func (t *Table) Opcode() *Enum { return t.opcode }
|
||||
|
||||
// OpcodeConst returns the constant name for the given opcode.
|
||||
func (t *Table) OpcodeConst(opcode string) string {
|
||||
return t.opcode.ConstName(opcode)
|
||||
}
|
||||
|
||||
// Features returns code for the features constant describing the features of
|
||||
// the given instruction form.
|
||||
func Features(i inst.Instruction, f inst.Form) string {
|
||||
var enabled []string
|
||||
for _, feature := range []struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
}{
|
||||
{"Terminal", i.IsTerminal()},
|
||||
{"Branch", i.IsBranch()},
|
||||
{"ConditionalBranch", i.IsConditionalBranch()},
|
||||
{"CancellingInputs", f.CancellingInputs},
|
||||
} {
|
||||
if feature.Enabled {
|
||||
enabled = append(enabled, "feature"+feature.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(enabled) == 0 {
|
||||
return "0"
|
||||
}
|
||||
return strings.Join(enabled, "|")
|
||||
}
|
||||
|
||||
// Action returns code representing the instruction action.
|
||||
func Action(a inst.Action) string {
|
||||
c := strings.ToUpper(a.String())
|
||||
if c == "" {
|
||||
c = "N"
|
||||
}
|
||||
return "action" + c
|
||||
}
|
||||
@@ -37,13 +37,10 @@ func (a *asmtest) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
a.Printf("rel32:\n")
|
||||
a.rel32 = "rel32"
|
||||
|
||||
counts := map[string]int{}
|
||||
|
||||
for _, i := range is {
|
||||
a.Printf("\t// %s %s\n", i.Opcode, i.Summary)
|
||||
if skip, msg := a.skip(i.Opcode); skip {
|
||||
a.Printf("\t// SKIP: %s\n", msg)
|
||||
counts["skip"]++
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -54,24 +51,23 @@ func (a *asmtest) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
}
|
||||
|
||||
for _, f := range i.Forms {
|
||||
as := a.args(i.Opcode, f.Operands)
|
||||
if as == nil {
|
||||
a.Printf("\t// TODO: %s %#v\n", i.Opcode, f.Operands)
|
||||
counts["todo"]++
|
||||
continue
|
||||
as, err := a.args(i.Opcode, f.Operands)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tests for %s: %w", i.Opcode, err)
|
||||
}
|
||||
for _, suffixes := range f.SupportedSuffixes() {
|
||||
opcode := i.Opcode
|
||||
if len(suffixes) > 0 {
|
||||
opcode += "." + suffixes.String()
|
||||
}
|
||||
a.Printf("\t%s\t%s\n", opcode, strings.Join(as, ", "))
|
||||
}
|
||||
a.Printf("\t%s\t%s\n", i.Opcode, strings.Join(as, ", "))
|
||||
counts["total"]++
|
||||
}
|
||||
a.Printf("\n")
|
||||
}
|
||||
|
||||
a.Printf("\tRET\n")
|
||||
|
||||
for m, c := range counts {
|
||||
a.Printf("// %s: %d\n", m, c)
|
||||
}
|
||||
|
||||
return a.Result()
|
||||
}
|
||||
|
||||
@@ -88,21 +84,21 @@ func (a asmtest) skip(opcode string) (bool, string) {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (a asmtest) args(opcode string, ops []inst.Operand) []string {
|
||||
func (a asmtest) args(opcode string, ops []inst.Operand) ([]string, error) {
|
||||
// Special case for CALL, since it needs a different type of rel32 argument than others.
|
||||
if opcode == "CALL" {
|
||||
return []string{a.sym}
|
||||
return []string{a.sym}, nil
|
||||
}
|
||||
|
||||
as := make([]string, len(ops))
|
||||
for i, op := range ops {
|
||||
a := a.arg(op.Type, i)
|
||||
if a == "" {
|
||||
return nil
|
||||
return nil, fmt.Errorf("unsupported operand type %q", op.Type)
|
||||
}
|
||||
as[i] = a
|
||||
}
|
||||
return as
|
||||
return as, nil
|
||||
}
|
||||
|
||||
// arg generates an argument for an operand of the given type.
|
||||
@@ -134,10 +130,10 @@ func (a asmtest) arg(t string, i int) string {
|
||||
"ymm": "Y" + strconv.Itoa(3+i), // <xs:enumeration value="ymm" />
|
||||
// <xs:enumeration value="ymm{k}" />
|
||||
// <xs:enumeration value="ymm{k}{z}" />
|
||||
// <xs:enumeration value="zmm" />
|
||||
"zmm": "Z" + strconv.Itoa(16+i), // <xs:enumeration value="zmm" />
|
||||
// <xs:enumeration value="zmm{k}" />
|
||||
// <xs:enumeration value="zmm{k}{z}" />
|
||||
// <xs:enumeration value="k" />
|
||||
"k": "K" + strconv.Itoa(1+i), // <xs:enumeration value="k" />
|
||||
// <xs:enumeration value="k{k}" />
|
||||
// <xs:enumeration value="moffs32" />
|
||||
// <xs:enumeration value="moffs64" />
|
||||
@@ -155,15 +151,8 @@ func (a asmtest) arg(t string, i int) string {
|
||||
// <xs:enumeration value="m128{k}{z}" />
|
||||
"m256": "256(AX)(CX*2)", // <xs:enumeration value="m256" />
|
||||
// <xs:enumeration value="m256{k}{z}" />
|
||||
// <xs:enumeration value="m512" />
|
||||
"m512": "512(AX)(CX*2)", // <xs:enumeration value="m512" />
|
||||
// <xs:enumeration value="m512{k}{z}" />
|
||||
// <xs:enumeration value="m64/m32bcst" />
|
||||
// <xs:enumeration value="m128/m32bcst" />
|
||||
// <xs:enumeration value="m256/m32bcst" />
|
||||
// <xs:enumeration value="m512/m32bcst" />
|
||||
// <xs:enumeration value="m128/m64bcst" />
|
||||
// <xs:enumeration value="m256/m64bcst" />
|
||||
// <xs:enumeration value="m512/m64bcst" />
|
||||
"vm32x": "32(X14*8)", // <xs:enumeration value="vm32x" />
|
||||
// <xs:enumeration value="vm32x{k}" />
|
||||
"vm64x": "64(X14*8)", // <xs:enumeration value="vm64x" />
|
||||
@@ -172,9 +161,9 @@ func (a asmtest) arg(t string, i int) string {
|
||||
// <xs:enumeration value="vm32y{k}" />
|
||||
"vm64y": "64(Y13*8)", // <xs:enumeration value="vm64y" />
|
||||
// <xs:enumeration value="vm64y{k}" />
|
||||
// <xs:enumeration value="vm32z" />
|
||||
"vm32z": "32(Z13*8)", // <xs:enumeration value="vm32z" />
|
||||
// <xs:enumeration value="vm32z{k}" />
|
||||
// <xs:enumeration value="vm64z" />
|
||||
"vm64z": "64(Z13*8)", // <xs:enumeration value="vm64z" />
|
||||
// <xs:enumeration value="vm64z{k}" />
|
||||
"rel8": a.rel8, // <xs:enumeration value="rel8" />
|
||||
"rel32": a.rel32, // <xs:enumeration value="rel32" />
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
pkg = "github.com/mmcloughlin/avo"
|
||||
operandType = "operand.Op"
|
||||
)
|
||||
|
||||
// implicitRegister returns avo syntax for the given implicit register type (from Opcodes XML).
|
||||
func implicitRegister(t string) string {
|
||||
r := strings.Replace(t, "mm", "", 1) // handles the "xmm0" type
|
||||
return fmt.Sprintf("reg.%s", strings.ToUpper(r))
|
||||
}
|
||||
|
||||
// checkername returns the name of the function that checks an operand of type t.
|
||||
func checkername(t string) string {
|
||||
return "operand.Is" + strings.ToUpper(t)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package gen
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
"github.com/mmcloughlin/avo/printer"
|
||||
@@ -26,34 +27,41 @@ func (b *build) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
b.Printf("package build\n\n")
|
||||
|
||||
b.Printf("import (\n")
|
||||
b.Printf("\t\"%s/operand\"\n", pkg)
|
||||
b.Printf("\t\"%s/x86\"\n", pkg)
|
||||
b.Printf("\t%q\n", api.ImportPath(api.IRPackage))
|
||||
b.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
|
||||
b.Printf("\t%q\n", api.ImportPath("x86"))
|
||||
b.Printf(")\n\n")
|
||||
|
||||
for _, i := range is {
|
||||
b.instruction(i)
|
||||
// Helper to reduce source code size a little.
|
||||
b.Printf("func (c *Context) addinstruction(i *ir.Instruction, err error) {\n")
|
||||
b.Printf("if err == nil { c.Instruction(i) }")
|
||||
b.Printf(" else { c.adderror(err) }\n")
|
||||
b.Printf("}\n\n")
|
||||
|
||||
// Generate build functions.
|
||||
fns := api.InstructionsFunctions(is)
|
||||
for _, fn := range fns {
|
||||
b.function(fn)
|
||||
}
|
||||
|
||||
return b.Result()
|
||||
}
|
||||
|
||||
func (b *build) instruction(i inst.Instruction) {
|
||||
s := params(i)
|
||||
d := doc(i)
|
||||
func (b *build) function(fn *api.Function) {
|
||||
s := fn.Signature()
|
||||
d := fn.Doc()
|
||||
|
||||
// Context method.
|
||||
methoddoc := append([]string{}, d...)
|
||||
methoddoc = append(methoddoc, fmt.Sprintf("Construct and append a %s instruction to the active function.", i.Opcode))
|
||||
methoddoc = append(methoddoc, fmt.Sprintf("Construct and append a %s instruction to the active function.", fn.Opcode()))
|
||||
b.Comment(methoddoc...)
|
||||
b.Printf("func (c *Context) %s(%s) {\n", i.Opcode, s.ParameterList())
|
||||
b.Printf("if inst, err := x86.%s(%s); err == nil", i.Opcode, s.Arguments())
|
||||
b.Printf(" { c.Instruction(inst) }")
|
||||
b.Printf(" else { c.adderror(err) }\n")
|
||||
b.Printf("func (c *Context) %s(%s) {\n", fn.Name(), s.ParameterList())
|
||||
b.Printf("c.addinstruction(x86.%s(%s))", fn.Name(), s.Arguments())
|
||||
b.Printf("}\n\n")
|
||||
|
||||
// Global version.
|
||||
globaldoc := append([]string{}, methoddoc...)
|
||||
globaldoc = append(globaldoc, "Operates on the global context.")
|
||||
b.Comment(globaldoc...)
|
||||
b.Printf("func %s(%s) { ctx.%s(%s) }\n\n", i.Opcode, s.ParameterList(), i.Opcode, s.Arguments())
|
||||
b.Printf("func %s(%s) { ctx.%s(%s) }\n\n", fn.Name(), s.ParameterList(), fn.Name(), s.Arguments())
|
||||
}
|
||||
|
||||
51
internal/gen/buildtest.go
Normal file
51
internal/gen/buildtest.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
"github.com/mmcloughlin/avo/printer"
|
||||
)
|
||||
|
||||
type buildtest struct {
|
||||
cfg printer.Config
|
||||
prnt.Generator
|
||||
}
|
||||
|
||||
// NewBuildTest autogenerates tests for instruction methods on the build
|
||||
// context.
|
||||
func NewBuildTest(cfg printer.Config) Interface {
|
||||
return GoFmt(&buildtest{cfg: cfg})
|
||||
}
|
||||
|
||||
func (b *buildtest) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
b.Printf("// %s\n\n", b.cfg.GeneratedWarning())
|
||||
b.BuildTag("!integration")
|
||||
b.NL()
|
||||
b.Printf("package build\n\n")
|
||||
b.Printf("import (\n")
|
||||
b.Printf("\t\"math\"\n")
|
||||
b.Printf("\t\"testing\"\n")
|
||||
b.NL()
|
||||
b.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
|
||||
b.Printf("\t%q\n", api.ImportPath(api.RegisterPackage))
|
||||
b.Printf(")\n\n")
|
||||
|
||||
DeclareTestArguments(&b.Generator)
|
||||
|
||||
b.Printf("func TestContextInstructions(t *testing.T) {")
|
||||
b.Printf("ctx := NewContext()\n")
|
||||
b.Printf("ctx.Function(\"Instructions\")\n")
|
||||
|
||||
fns := api.InstructionsFunctions(is)
|
||||
for _, fn := range fns {
|
||||
f := fn.Forms[0]
|
||||
s := TestSignature(f)
|
||||
b.Printf("ctx.%s(%s)\n", fn.Name(), s.Arguments())
|
||||
}
|
||||
|
||||
b.Printf("if _, err := ctx.Result(); err != nil { t.Fatal(err) }\n")
|
||||
b.Printf("}\n\n")
|
||||
|
||||
return b.Result()
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
"github.com/mmcloughlin/avo/printer"
|
||||
@@ -15,9 +12,9 @@ type ctors struct {
|
||||
prnt.Generator
|
||||
}
|
||||
|
||||
// NewCtors will build instruction constructors. Each constructor will check
|
||||
// that the provided operands match one of the allowed instruction forms. If so
|
||||
// it will return an Instruction object that can be added to an avo Function.
|
||||
// NewCtors will build instruction constructors. Each constructor delegates to
|
||||
// the optab-based instruction builder, providing it with a candidate list of
|
||||
// forms to match against.
|
||||
func NewCtors(cfg printer.Config) Interface {
|
||||
return GoFmt(&ctors{cfg: cfg})
|
||||
}
|
||||
@@ -26,107 +23,30 @@ func (c *ctors) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
c.Printf("// %s\n\n", c.cfg.GeneratedWarning())
|
||||
c.Printf("package x86\n\n")
|
||||
c.Printf("import (\n")
|
||||
c.Printf("\t\"errors\"\n")
|
||||
c.NL()
|
||||
c.Printf("\tintrep \"%s/ir\"\n", pkg)
|
||||
c.Printf("\t\"%s/reg\"\n", pkg)
|
||||
c.Printf("\t\"%s/operand\"\n", pkg)
|
||||
c.Printf("\tintrep %q\n", api.ImportPath(api.IRPackage))
|
||||
c.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
|
||||
c.Printf(")\n\n")
|
||||
|
||||
for _, i := range is {
|
||||
c.instruction(i)
|
||||
fns := api.InstructionsFunctions(is)
|
||||
table := NewTable(is)
|
||||
for _, fn := range fns {
|
||||
c.function(fn, table)
|
||||
}
|
||||
|
||||
return c.Result()
|
||||
}
|
||||
|
||||
func (c *ctors) instruction(i inst.Instruction) {
|
||||
c.Comment(doc(i)...)
|
||||
func (c *ctors) function(fn *api.Function, table *Table) {
|
||||
c.Comment(fn.Doc()...)
|
||||
|
||||
s := params(i)
|
||||
s := fn.Signature()
|
||||
|
||||
c.Printf("func %s(%s) (*intrep.Instruction, error) {\n", i.Opcode, s.ParameterList())
|
||||
c.forms(i, s)
|
||||
c.Printf("func %s(%s) (*intrep.Instruction, error) {\n", fn.Name(), s.ParameterList())
|
||||
c.Printf(
|
||||
"return build(%s.Forms(), %s, %s)\n",
|
||||
table.OpcodeConst(fn.Instruction.Opcode),
|
||||
table.SuffixesConst(fn.Suffixes),
|
||||
s.ParameterSlice(),
|
||||
)
|
||||
c.Printf("}\n\n")
|
||||
}
|
||||
|
||||
func (c *ctors) forms(i inst.Instruction, s signature) {
|
||||
if i.IsNiladic() {
|
||||
if len(i.Forms) != 1 {
|
||||
c.AddError(fmt.Errorf("%s breaks assumption that niladic instructions have one form", i.Opcode))
|
||||
}
|
||||
c.Printf("return &%s, nil\n", construct(i, i.Forms[0], s))
|
||||
return
|
||||
}
|
||||
|
||||
c.Printf("switch {\n")
|
||||
|
||||
for _, f := range i.Forms {
|
||||
var conds []string
|
||||
|
||||
if i.IsVariadic() {
|
||||
checklen := fmt.Sprintf("%s == %d", s.Length(), len(f.Operands))
|
||||
conds = append(conds, checklen)
|
||||
}
|
||||
|
||||
for j, op := range f.Operands {
|
||||
checktype := fmt.Sprintf("%s(%s)", checkername(op.Type), s.ParameterName(j))
|
||||
conds = append(conds, checktype)
|
||||
}
|
||||
|
||||
c.Printf("case %s:\n", strings.Join(conds, " && "))
|
||||
c.Printf("return &%s, nil\n", construct(i, f, s))
|
||||
}
|
||||
|
||||
c.Printf("}\n")
|
||||
c.Printf("return nil, errors.New(\"%s: bad operands\")\n", i.Opcode)
|
||||
}
|
||||
|
||||
func construct(i inst.Instruction, f inst.Form, s signature) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
fmt.Fprintf(buf, "intrep.Instruction{\n")
|
||||
fmt.Fprintf(buf, "\tOpcode: %#v,\n", i.Opcode)
|
||||
fmt.Fprintf(buf, "\tOperands: %s,\n", s.ParameterSlice())
|
||||
|
||||
// Input output.
|
||||
fmt.Fprintf(buf, "\tInputs: %s,\n", operandsWithAction(f, inst.R, s))
|
||||
fmt.Fprintf(buf, "\tOutputs: %s,\n", operandsWithAction(f, inst.W, s))
|
||||
|
||||
// ISAs.
|
||||
if len(f.ISA) > 0 {
|
||||
fmt.Fprintf(buf, "\tISA: %#v,\n", f.ISA)
|
||||
}
|
||||
|
||||
// Branch variables.
|
||||
if i.IsTerminal() {
|
||||
fmt.Fprintf(buf, "\tIsTerminal: true,\n")
|
||||
}
|
||||
|
||||
if i.IsBranch() {
|
||||
fmt.Fprintf(buf, "\tIsBranch: true,\n")
|
||||
fmt.Fprintf(buf, "\tIsConditional: %#v,\n", i.IsConditionalBranch())
|
||||
}
|
||||
|
||||
// Cancelling inputs.
|
||||
if f.CancellingInputs {
|
||||
fmt.Fprintf(buf, "\tCancellingInputs: true,\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "}")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func operandsWithAction(f inst.Form, a inst.Action, s signature) string {
|
||||
opexprs := []string{}
|
||||
for i, op := range f.Operands {
|
||||
if op.Action.Contains(a) {
|
||||
opexprs = append(opexprs, s.ParameterName(i))
|
||||
}
|
||||
}
|
||||
for _, op := range f.ImplicitOperands {
|
||||
if op.Action.Contains(a) {
|
||||
opexprs = append(opexprs, implicitRegister(op.Register))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("[]%s{%s}", operandType, strings.Join(opexprs, ", "))
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
)
|
||||
|
||||
func TestParamsUniqueArgNames(t *testing.T) {
|
||||
for _, i := range inst.Instructions {
|
||||
s := params(i)
|
||||
for _, n := range i.Arities() {
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
names := map[string]bool{}
|
||||
for j := 0; j < n; j++ {
|
||||
names[s.ParameterName(j)] = true
|
||||
}
|
||||
if len(names) != n {
|
||||
t.Errorf("repeated argument for instruction %s", i.Opcode)
|
||||
}
|
||||
if _, found := names[""]; found {
|
||||
t.Errorf("empty argument name for instruction %s", i.Opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
"github.com/mmcloughlin/avo/printer"
|
||||
|
||||
@@ -21,93 +24,173 @@ func NewCtorsTest(cfg printer.Config) Interface {
|
||||
|
||||
func (c *ctorstest) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
c.Printf("// %s\n\n", c.cfg.GeneratedWarning())
|
||||
c.BuildTag("!integration")
|
||||
c.NL()
|
||||
c.Printf("package x86\n\n")
|
||||
c.Printf("import (\n")
|
||||
c.Printf("\t\"testing\"\n")
|
||||
c.Printf("\t\"math\"\n")
|
||||
c.Printf("\t\"testing\"\n")
|
||||
c.NL()
|
||||
c.Printf("\t\"%s/reg\"\n", pkg)
|
||||
c.Printf("\t\"%s/operand\"\n", pkg)
|
||||
c.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
|
||||
c.Printf("\t%q\n", api.ImportPath(api.RegisterPackage))
|
||||
c.Printf(")\n\n")
|
||||
|
||||
for _, i := range is {
|
||||
c.instruction(i)
|
||||
DeclareTestArguments(&c.Generator)
|
||||
|
||||
fns := api.InstructionsFunctions(is)
|
||||
for _, fn := range fns {
|
||||
c.function(fn)
|
||||
}
|
||||
|
||||
return c.Result()
|
||||
}
|
||||
|
||||
func (c *ctorstest) instruction(i inst.Instruction) {
|
||||
c.Printf("func Test%sValidForms(t *testing.T) {", i.Opcode)
|
||||
|
||||
for _, f := range i.Forms {
|
||||
name := strings.Join(f.Signature(), "_")
|
||||
c.Printf("t.Run(\"form=%s\", func(t *testing.T) {\n", name)
|
||||
|
||||
for _, args := range validFormArgs(f) {
|
||||
c.Printf("if _, err := %s(%s)", i.Opcode, strings.Join(args, ", "))
|
||||
c.Printf("; err != nil { t.Fatal(err) }\n")
|
||||
}
|
||||
|
||||
c.Printf("})\n")
|
||||
func (c *ctorstest) function(fn *api.Function) {
|
||||
c.Printf("func Test%sValidFormsNoError(t *testing.T) {", fn.Name())
|
||||
for _, f := range fn.Forms {
|
||||
s := TestSignature(f)
|
||||
c.Printf("if _, err := %s(%s); err != nil { t.Fatal(err) }\n", fn.Name(), s.Arguments())
|
||||
}
|
||||
|
||||
c.Printf("}\n\n")
|
||||
}
|
||||
|
||||
func validFormArgs(f inst.Form) [][]string {
|
||||
n := len(f.Operands)
|
||||
args := make([][]string, n)
|
||||
for i, op := range f.Operands {
|
||||
valid, ok := validArgs[op.Type]
|
||||
if !ok {
|
||||
panic("missing operands for type " + op.Type)
|
||||
}
|
||||
args[i] = valid
|
||||
type ctorsstress struct {
|
||||
cfg printer.Config
|
||||
prnt.Generator
|
||||
}
|
||||
|
||||
// NewCtorsStress autogenerates stress tests for instruction constructors.
|
||||
func NewCtorsStress(cfg printer.Config) Interface {
|
||||
return GoFmt(&ctorsstress{cfg: cfg})
|
||||
}
|
||||
|
||||
func (c *ctorsstress) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
c.Printf("// %s\n\n", c.cfg.GeneratedWarning())
|
||||
c.BuildTag("stress")
|
||||
c.NL()
|
||||
c.Printf("package x86\n\n")
|
||||
c.Printf("import (\n")
|
||||
c.Printf("\t\"reflect\"\n")
|
||||
c.Printf("\t\"testing\"\n")
|
||||
c.NL()
|
||||
c.Printf("\t%q\n", api.ImportPath(api.IRPackage))
|
||||
c.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
|
||||
c.Printf("\t%q\n", api.ImportPath(api.RegisterPackage))
|
||||
c.Printf(")\n\n")
|
||||
|
||||
fns := api.InstructionsFunctions(is)
|
||||
for _, fn := range fns {
|
||||
c.function(fn)
|
||||
}
|
||||
return cross(args)
|
||||
|
||||
return c.Result()
|
||||
}
|
||||
|
||||
var validArgs = map[string][]string{
|
||||
// Immediates
|
||||
"1": {"operand.Imm(1)"},
|
||||
"3": {"operand.Imm(3)"},
|
||||
"imm2u": {"operand.Imm(1)", "operand.Imm(3)"},
|
||||
"imm8": {"operand.Imm(math.MaxInt8)"},
|
||||
"imm16": {"operand.Imm(math.MaxInt16)"},
|
||||
"imm32": {"operand.Imm(math.MaxInt32)"},
|
||||
"imm64": {"operand.Imm(math.MaxInt64)"},
|
||||
|
||||
// Registers
|
||||
"al": {"reg.AL"},
|
||||
"cl": {"reg.CL"},
|
||||
"ax": {"reg.AX"},
|
||||
"eax": {"reg.EAX"},
|
||||
"rax": {"reg.RAX"},
|
||||
"r8": {"reg.CH", "reg.BL", "reg.R13B"},
|
||||
"r16": {"reg.CX", "reg.R9W"},
|
||||
"r32": {"reg.R10L"},
|
||||
"r64": {"reg.R11"},
|
||||
"xmm0": {"reg.X0"},
|
||||
"xmm": {"reg.X7"},
|
||||
"ymm": {"reg.Y15"},
|
||||
|
||||
// Memory
|
||||
"m": {"operand.Mem{Base: reg.BX, Index: reg.CX, Scale: 2}"},
|
||||
"m8": {"operand.Mem{Base: reg.BL, Index: reg.CH, Scale: 1}"},
|
||||
"m16": {"operand.Mem{Base: reg.BX, Index: reg.CX, Scale: 2}"},
|
||||
"m32": {"operand.Mem{Base: reg.EBX, Index: reg.ECX, Scale: 4}"},
|
||||
"m64": {"operand.Mem{Base: reg.RBX, Index: reg.RCX, Scale: 8}"},
|
||||
"m128": {"operand.Mem{Base: reg.RBX, Index: reg.RCX, Scale: 8}"},
|
||||
"m256": {"operand.Mem{Base: reg.RBX, Index: reg.RCX, Scale: 8}"},
|
||||
|
||||
// Vector memory
|
||||
"vm32x": {"operand.Mem{Base: reg.R13, Index: reg.X4, Scale: 1}"},
|
||||
"vm64x": {"operand.Mem{Base: reg.R13, Index: reg.X8, Scale: 1}"},
|
||||
"vm32y": {"operand.Mem{Base: reg.R13, Index: reg.Y4, Scale: 1}"},
|
||||
"vm64y": {"operand.Mem{Base: reg.R13, Index: reg.Y8, Scale: 1}"},
|
||||
|
||||
// Relative
|
||||
"rel8": {"operand.Rel(math.MaxInt8)"},
|
||||
"rel32": {"operand.Rel(math.MaxInt32)", "operand.LabelRef(\"lbl\")"},
|
||||
func (c *ctorsstress) function(fn *api.Function) {
|
||||
c.Printf("func Test%sValidFormsCorrectInstruction(t *testing.T) {", fn.Name())
|
||||
for _, f := range fn.Forms {
|
||||
name := strings.Join(f.Signature(), "_")
|
||||
c.Printf("t.Run(\"form=%s\", func(t *testing.T) {\n", name)
|
||||
s := TestSignature(f)
|
||||
c.Printf("expect := &%s\n", construct(fn, f, s))
|
||||
c.Printf("got, err := %s(%s);\n", fn.Name(), s.Arguments())
|
||||
c.Printf("if err != nil { t.Fatal(err) }\n")
|
||||
c.Printf("if !reflect.DeepEqual(got, expect) { t.Fatal(\"mismatch\") }\n")
|
||||
c.Printf("})\n")
|
||||
}
|
||||
c.Printf("}\n\n")
|
||||
}
|
||||
|
||||
type ctorsbench struct {
|
||||
cfg printer.Config
|
||||
prnt.Generator
|
||||
}
|
||||
|
||||
// NewCtorsBench autogenerates a benchmark for the instruction constructors.
|
||||
func NewCtorsBench(cfg printer.Config) Interface {
|
||||
return GoFmt(&ctorsbench{cfg: cfg})
|
||||
}
|
||||
|
||||
func (c *ctorsbench) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
c.Printf("// %s\n\n", c.cfg.GeneratedWarning())
|
||||
c.BuildTag("stress")
|
||||
c.NL()
|
||||
c.Printf("package x86\n\n")
|
||||
c.Printf("import (\n")
|
||||
c.Printf("\t\"time\"\n")
|
||||
c.Printf("\t\"testing\"\n")
|
||||
c.Printf(")\n\n")
|
||||
|
||||
c.Printf("func BenchmarkConstructors(b *testing.B) {\n")
|
||||
c.Printf("start := time.Now()\n")
|
||||
c.Printf("for i := 0; i < b.N; i++ {\n")
|
||||
n := 0
|
||||
for _, fn := range api.InstructionsFunctions(is) {
|
||||
for _, f := range fn.Forms {
|
||||
n++
|
||||
c.Printf("%s(%s)\n", fn.Name(), TestSignature(f).Arguments())
|
||||
}
|
||||
}
|
||||
c.Printf("}\n")
|
||||
c.Printf("elapsed := time.Since(start)\n")
|
||||
c.Printf("\tb.ReportMetric(%d * float64(b.N) / elapsed.Seconds(), \"inst/s\")\n", n)
|
||||
c.Printf("}\n\n")
|
||||
return c.Result()
|
||||
}
|
||||
|
||||
func construct(fn *api.Function, f inst.Form, s api.Signature) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
fmt.Fprintf(buf, "ir.Instruction{\n")
|
||||
fmt.Fprintf(buf, "\tOpcode: %#v,\n", fn.Instruction.Opcode)
|
||||
if len(fn.Suffixes) > 0 {
|
||||
fmt.Fprintf(buf, "\tSuffixes: %#v,\n", fn.Suffixes.Strings())
|
||||
}
|
||||
fmt.Fprintf(buf, "\tOperands: %s,\n", s.ParameterSlice())
|
||||
|
||||
// Inputs.
|
||||
fmt.Fprintf(buf, "\tInputs: %s,\n", operandsWithAction(f, inst.R, s))
|
||||
|
||||
// Outputs.
|
||||
fmt.Fprintf(buf, "\tOutputs: %s,\n", operandsWithAction(f, inst.W, s))
|
||||
|
||||
// ISAs.
|
||||
if len(f.ISA) > 0 {
|
||||
fmt.Fprintf(buf, "\tISA: %#v,\n", f.ISA)
|
||||
}
|
||||
|
||||
// Branch variables.
|
||||
if fn.Instruction.IsTerminal() {
|
||||
fmt.Fprintf(buf, "\tIsTerminal: true,\n")
|
||||
}
|
||||
|
||||
if fn.Instruction.IsBranch() {
|
||||
fmt.Fprintf(buf, "\tIsBranch: true,\n")
|
||||
fmt.Fprintf(buf, "\tIsConditional: %#v,\n", fn.Instruction.IsConditionalBranch())
|
||||
}
|
||||
|
||||
// Cancelling inputs.
|
||||
if f.CancellingInputs {
|
||||
fmt.Fprintf(buf, "\tCancellingInputs: true,\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "}")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func operandsWithAction(f inst.Form, a inst.Action, s api.Signature) string {
|
||||
var opexprs []string
|
||||
for i, op := range f.Operands {
|
||||
if op.Action.ContainsAny(a) {
|
||||
opexprs = append(opexprs, s.ParameterName(i))
|
||||
}
|
||||
}
|
||||
for _, op := range f.ImplicitOperands {
|
||||
if op.Action.ContainsAny(a) {
|
||||
opexprs = append(opexprs, api.ImplicitRegister(op.Register))
|
||||
}
|
||||
}
|
||||
if len(opexprs) == 0 {
|
||||
return "nil"
|
||||
}
|
||||
return fmt.Sprintf("[]%s{%s}", api.OperandType, strings.Join(opexprs, ", "))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
"github.com/mmcloughlin/avo/printer"
|
||||
@@ -57,8 +58,21 @@ func (g *godata) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
g.Printf("},\n")
|
||||
}
|
||||
|
||||
if f.CancellingInputs {
|
||||
g.Printf("CancellingInputs: true,\n")
|
||||
g.Printf("EncodingType: %#v,\n", f.EncodingType)
|
||||
|
||||
for _, flag := range []struct {
|
||||
Field string
|
||||
Enabled bool
|
||||
}{
|
||||
{"CancellingInputs", f.CancellingInputs},
|
||||
{"Zeroing", f.Zeroing},
|
||||
{"EmbeddedRounding", f.EmbeddedRounding},
|
||||
{"SuppressAllExceptions", f.SuppressAllExceptions},
|
||||
{"Broadcast", f.Broadcast},
|
||||
} {
|
||||
if flag.Enabled {
|
||||
g.Printf("%s: true,\n", flag.Field)
|
||||
}
|
||||
}
|
||||
|
||||
g.Printf("},\n")
|
||||
@@ -90,6 +104,7 @@ func NewGoDataTest(cfg printer.Config) Interface {
|
||||
|
||||
func (g *godatatest) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
g.Printf("// %s\n\n", g.cfg.GeneratedWarning())
|
||||
g.NL()
|
||||
g.Printf("package inst_test\n\n")
|
||||
|
||||
g.Printf(`import (
|
||||
@@ -98,7 +113,7 @@ func (g *godatatest) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
|
||||
"%s/internal/inst"
|
||||
)
|
||||
`, pkg)
|
||||
`, api.Package)
|
||||
|
||||
g.Printf("var raw = %#v\n\n", is)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
"github.com/mmcloughlin/avo/printer"
|
||||
@@ -29,7 +30,7 @@ func (m *mov) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
m.Printf("import (\n")
|
||||
m.Printf("\t\"go/types\"\n")
|
||||
m.NL()
|
||||
m.Printf("\t\"%s/operand\"\n", pkg)
|
||||
m.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
|
||||
m.Printf(")\n\n")
|
||||
|
||||
m.Printf("func (c *Context) mov(a, b operand.Op, an, bn int, t *types.Basic) {\n")
|
||||
@@ -48,15 +49,17 @@ func (m *mov) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
|
||||
func (m *mov) instruction(i inst.Instruction) {
|
||||
f := flags(i)
|
||||
sizes, err := formsizes(i)
|
||||
mfs, err := movforms(i)
|
||||
if err != nil {
|
||||
m.AddError(err)
|
||||
return
|
||||
}
|
||||
for _, size := range sizes {
|
||||
for _, mf := range mfs {
|
||||
conds := []string{
|
||||
fmt.Sprintf("an == %d", size.A),
|
||||
fmt.Sprintf("bn == %d", size.B),
|
||||
fmt.Sprintf("an == %d", opsize[mf.A]),
|
||||
fmt.Sprintf("%s(a)", api.CheckerName(mf.A)),
|
||||
fmt.Sprintf("bn == %d", opsize[mf.B]),
|
||||
fmt.Sprintf("%s(b)", api.CheckerName(mf.B)),
|
||||
}
|
||||
for c, on := range f {
|
||||
cmp := map[bool]string{true: "!=", false: "=="}
|
||||
@@ -71,15 +74,29 @@ func (m *mov) instruction(i inst.Instruction) {
|
||||
|
||||
// ismov decides whether the given instruction is a plain move instruction.
|
||||
func ismov(i inst.Instruction) bool {
|
||||
if i.AliasOf != "" || !strings.HasPrefix(i.Opcode, "MOV") {
|
||||
// Ignore aliases.
|
||||
if i.AliasOf != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Accept specific move instruction prefixes.
|
||||
prefixes := []string{"MOV", "KMOV", "VMOV"}
|
||||
accept := false
|
||||
for _, prefix := range prefixes {
|
||||
accept = strings.HasPrefix(i.Opcode, prefix) || accept
|
||||
}
|
||||
if !accept {
|
||||
return false
|
||||
}
|
||||
|
||||
// Exclude some cases based on instruction descriptions.
|
||||
exclude := []string{"Packed", "Duplicate", "Aligned", "Hint", "Swapping"}
|
||||
for _, substring := range exclude {
|
||||
if strings.Contains(i.Summary, substring) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -100,34 +117,27 @@ func flags(i inst.Instruction) map[string]bool {
|
||||
return f
|
||||
}
|
||||
|
||||
type movsize struct{ A, B int8 }
|
||||
type movform struct{ A, B string }
|
||||
|
||||
func (s movsize) sortkey() uint16 { return (uint16(s.A) << 8) | uint16(s.B) }
|
||||
|
||||
func formsizes(i inst.Instruction) ([]movsize, error) {
|
||||
set := map[movsize]bool{}
|
||||
func movforms(i inst.Instruction) ([]movform, error) {
|
||||
var mfs []movform
|
||||
for _, f := range i.Forms {
|
||||
if f.Arity() != 2 {
|
||||
continue
|
||||
}
|
||||
s := movsize{
|
||||
A: opsize[f.Operands[0].Type],
|
||||
B: opsize[f.Operands[1].Type],
|
||||
mf := movform{
|
||||
A: f.Operands[0].Type,
|
||||
B: f.Operands[1].Type,
|
||||
}
|
||||
if s.A < 0 || s.B < 0 {
|
||||
if opsize[mf.A] < 0 || opsize[mf.B] < 0 {
|
||||
continue
|
||||
}
|
||||
if s.A == 0 || s.B == 0 {
|
||||
if opsize[mf.A] == 0 || opsize[mf.B] == 0 {
|
||||
return nil, errors.New("unknown operand type")
|
||||
}
|
||||
set[s] = true
|
||||
mfs = append(mfs, mf)
|
||||
}
|
||||
var ss []movsize
|
||||
for s := range set {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
sort.Slice(ss, func(i, j int) bool { return ss[i].sortkey() < ss[j].sortkey() })
|
||||
return ss, nil
|
||||
return mfs, nil
|
||||
}
|
||||
|
||||
var opsize = map[string]int8{
|
||||
@@ -140,9 +150,14 @@ var opsize = map[string]int8{
|
||||
"r32": 4,
|
||||
"r64": 8,
|
||||
"xmm": 16,
|
||||
"ymm": 32,
|
||||
"zmm": 64,
|
||||
"m8": 1,
|
||||
"m16": 2,
|
||||
"m32": 4,
|
||||
"m64": 8,
|
||||
"m128": 16,
|
||||
"m256": 32,
|
||||
"m512": 64,
|
||||
"k": 8,
|
||||
}
|
||||
|
||||
288
internal/gen/optab.go
Normal file
288
internal/gen/optab.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
"github.com/mmcloughlin/avo/printer"
|
||||
)
|
||||
|
||||
type optab struct {
|
||||
prnt.Generator
|
||||
|
||||
cfg printer.Config
|
||||
|
||||
table *Table
|
||||
}
|
||||
|
||||
// NewOptab builds the operator table. This contains a more compact
|
||||
// representation of the instruction database, containing the data needed for
|
||||
// instruction builders to match against provided operands, and build the
|
||||
// selected instruction.
|
||||
func NewOptab(cfg printer.Config) Interface {
|
||||
return GoFmt(&optab{cfg: cfg})
|
||||
}
|
||||
|
||||
func (t *optab) Generate(is []inst.Instruction) ([]byte, error) {
|
||||
t.Printf("// %s\n\n", t.cfg.GeneratedWarning())
|
||||
t.Printf("package x86\n\n")
|
||||
t.Printf("import (\n")
|
||||
t.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
|
||||
t.Printf("\t%q\n", api.ImportPath(api.RegisterPackage))
|
||||
t.Printf(")\n\n")
|
||||
|
||||
// Generate instruction data table.
|
||||
t.table = NewTable(is)
|
||||
|
||||
// Size constants.
|
||||
t.maxOperands(is)
|
||||
|
||||
// Types.
|
||||
t.operandTypeEnum(is)
|
||||
t.implicitRegisterEnum(is)
|
||||
t.enum(t.table.Suffix())
|
||||
t.suffixesType(is)
|
||||
t.suffixesClassEnum(is)
|
||||
t.isasEnum(is)
|
||||
t.opcodeEnum(is)
|
||||
|
||||
// Forms table.
|
||||
t.forms(is)
|
||||
|
||||
return t.Result()
|
||||
}
|
||||
|
||||
func (t *optab) maxOperands(is []inst.Instruction) {
|
||||
max := 0
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
a := len(f.Operands) + len(f.ImplicitOperands)
|
||||
if a > max {
|
||||
max = a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Comment("maxoperands is the maximum number of operands in an instruction form, including implicit operands.")
|
||||
t.Printf("const maxoperands = %d\n\n", max)
|
||||
}
|
||||
|
||||
func (t *optab) operandTypeEnum(is []inst.Instruction) {
|
||||
// Operand type enum.
|
||||
e := t.table.OperandType()
|
||||
t.enum(e)
|
||||
|
||||
// Operand match function.
|
||||
types := inst.OperandTypes(is)
|
||||
t.Printf("func (%s %s) Match(op %s) bool {\n", e.Receiver(), e.Name(), api.OperandType)
|
||||
t.Printf("\tswitch %s {\n", e.Receiver())
|
||||
t.Printf("\t\tdefault: return false\n")
|
||||
for _, typ := range types {
|
||||
t.Printf("\t\tcase %s: return %s(op)\n", t.table.OperandTypeConst(typ), api.CheckerName(typ))
|
||||
}
|
||||
t.Printf("\t}\n")
|
||||
t.Printf("}\n\n")
|
||||
}
|
||||
|
||||
func (t *optab) implicitRegisterEnum(is []inst.Instruction) {
|
||||
// Implicit register enum.
|
||||
e := t.table.ImplicitRegister()
|
||||
t.enum(e)
|
||||
|
||||
// Register conversion function.
|
||||
registers := inst.ImplicitRegisters(is)
|
||||
t.Printf("func (%s %s) Register() %s {\n", e.Receiver(), e.Name(), api.RegisterType)
|
||||
t.Printf("\tswitch %s {\n", e.Receiver())
|
||||
t.Printf("\t\tdefault: panic(\"unexpected implicit register type\")\n")
|
||||
for _, r := range registers {
|
||||
t.Printf("\t\tcase %s: return %s\n", t.table.ImplicitRegisterConst(r), api.ImplicitRegister(r))
|
||||
}
|
||||
t.Printf("\t}\n")
|
||||
t.Printf("}\n\n")
|
||||
}
|
||||
|
||||
func (t *optab) suffixesType(is []inst.Instruction) {
|
||||
// Declare the type as an array. This requires us to know the maximum number
|
||||
// of suffixes an instruction can have.
|
||||
max := 0
|
||||
for _, class := range inst.SuffixesClasses(is) {
|
||||
for _, suffixes := range class {
|
||||
if len(suffixes) > max {
|
||||
max = len(suffixes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Comment("maxsuffixes is the maximum number of suffixes an instruction can have.")
|
||||
t.Printf("const maxsuffixes = %d\n\n", max)
|
||||
|
||||
name := t.table.SuffixesTypeName()
|
||||
t.Printf("type %s [maxsuffixes]%s\n", name, t.table.Suffix().Name())
|
||||
|
||||
// Conversion function to list of strings.
|
||||
mapname := name + "stringsmap"
|
||||
|
||||
t.Printf("func (s %s) Strings() []string {\n", name)
|
||||
t.Printf("return %s[s]", mapname)
|
||||
t.Printf("}\n")
|
||||
|
||||
var entries []string
|
||||
for _, class := range inst.SuffixesClasses(is) {
|
||||
for _, suffixes := range class {
|
||||
entry := fmt.Sprintf("%s: %#v", t.table.SuffixesConst(suffixes), suffixes.Strings())
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
|
||||
t.Printf("var %s = map[%s][]string{\n", mapname, name)
|
||||
sort.Strings(entries)
|
||||
for _, entry := range entries {
|
||||
t.Printf("%s,\n", entry)
|
||||
}
|
||||
t.Printf("}\n")
|
||||
}
|
||||
|
||||
func (t *optab) suffixesClassEnum(is []inst.Instruction) {
|
||||
// Suffixes class enum.
|
||||
e := t.table.SuffixesClass()
|
||||
t.enum(e)
|
||||
|
||||
// Mapping method to the set of accepted suffixes.
|
||||
sets := map[string]string{}
|
||||
for key, class := range inst.SuffixesClasses(is) {
|
||||
var entries []string
|
||||
for _, suffixes := range class {
|
||||
entry := fmt.Sprintf("%s: true", t.table.SuffixesConst(suffixes))
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
sort.Strings(entries)
|
||||
sets[api.SuffixesClassIdentifier(key)] = "{" + strings.Join(entries, ", ") + "}"
|
||||
}
|
||||
|
||||
settype := fmt.Sprintf("map[%s]bool", t.table.SuffixesTypeName())
|
||||
t.mapping(e, "SuffixesSet", settype, "nil", sets)
|
||||
}
|
||||
|
||||
func (t *optab) isasEnum(is []inst.Instruction) {
|
||||
// ISAs enum.
|
||||
e := t.table.ISAs()
|
||||
t.enum(e)
|
||||
|
||||
// Mapping method to produce the list of ISAs.
|
||||
lists := map[string]string{}
|
||||
for _, isas := range inst.ISACombinations(is) {
|
||||
lists[api.ISAsIdentifier(isas)] = fmt.Sprintf("%#v", isas)
|
||||
}
|
||||
t.mapping(e, "List", "[]string", "nil", lists)
|
||||
}
|
||||
|
||||
func (t *optab) opcodeEnum(is []inst.Instruction) {
|
||||
e := t.table.Opcode()
|
||||
t.enum(e)
|
||||
t.stringmethod(e)
|
||||
}
|
||||
|
||||
func (t *optab) forms(is []inst.Instruction) {
|
||||
// We require all instructions for a given opcode to be in a contiguous
|
||||
// block. This is likely true already but we'll make a sorted copy to ensure
|
||||
// the optab is robust to changes elsewhere.
|
||||
is = append([]inst.Instruction(nil), is...)
|
||||
sort.Slice(is, func(i, j int) bool {
|
||||
return is[i].Opcode < is[j].Opcode
|
||||
})
|
||||
|
||||
// Output instruction forms table.
|
||||
table := "forms"
|
||||
t.Printf("var %s = []form{\n", table)
|
||||
for _, i := range is {
|
||||
for _, f := range i.Forms {
|
||||
t.Printf("{")
|
||||
|
||||
// Basic properties.
|
||||
t.Printf("%s, ", t.table.OpcodeConst(i.Opcode))
|
||||
t.Printf("%s, ", t.table.SuffixesClassConst(f.SuffixesClass()))
|
||||
t.Printf("%s, ", Features(i, f))
|
||||
t.Printf("%s, ", t.table.ISAsConst(f.ISA))
|
||||
|
||||
// Operands.
|
||||
t.Printf("%d, ", len(f.Operands))
|
||||
t.Printf("oprnds{")
|
||||
for _, op := range f.Operands {
|
||||
t.Printf(
|
||||
"{uint8(%s),false,%s},",
|
||||
t.table.OperandTypeConst(op.Type),
|
||||
Action(op.Action),
|
||||
)
|
||||
}
|
||||
for _, op := range f.ImplicitOperands {
|
||||
t.Printf(
|
||||
"{uint8(%s),true,%s},",
|
||||
t.table.ImplicitRegisterConst(op.Register),
|
||||
Action(op.Action),
|
||||
)
|
||||
}
|
||||
t.Printf("}")
|
||||
|
||||
t.Printf("},\n")
|
||||
}
|
||||
}
|
||||
t.Printf("}\n\n")
|
||||
|
||||
// Build mapping from opcode to corresponding forms.
|
||||
forms := map[string]string{}
|
||||
n := 0
|
||||
for _, i := range is {
|
||||
e := n + len(i.Forms)
|
||||
forms[i.Opcode] = fmt.Sprintf("%s[%d:%d]", table, n, e)
|
||||
n = e
|
||||
}
|
||||
|
||||
t.mapping(t.table.Opcode(), "Forms", "[]form", "nil", forms)
|
||||
}
|
||||
|
||||
func (t *optab) enum(e *Enum) {
|
||||
// Type declaration.
|
||||
t.Comment(e.Doc()...)
|
||||
t.Printf("type %s %s\n\n", e.Name(), e.UnderlyingType())
|
||||
|
||||
// Supported values.
|
||||
t.Printf("const (\n")
|
||||
t.Printf("\t%s %s = iota\n", e.None(), e.name)
|
||||
for _, name := range e.ConstNames() {
|
||||
t.Printf("\t%s\n", name)
|
||||
}
|
||||
t.Printf("\t%s\n", e.MaxName())
|
||||
t.Printf(")\n\n")
|
||||
}
|
||||
|
||||
func (t *optab) mapping(e *Enum, name, ret, zero string, to map[string]string) {
|
||||
table := strings.ToLower(e.Name() + name + "table")
|
||||
|
||||
r := e.Receiver()
|
||||
t.Printf("func (%s %s) %s() %s {\n", r, e.Name(), name, ret)
|
||||
t.Printf("if %s < %s && %s < %s {\n", e.None(), r, r, e.MaxName())
|
||||
t.Printf("return %s[%s-1]\n", table, r)
|
||||
t.Printf("}\n")
|
||||
t.Printf("return %s\n", zero)
|
||||
t.Printf("}\n\n")
|
||||
|
||||
t.Printf("var %s = []%s{\n", table, ret)
|
||||
for _, value := range e.Values() {
|
||||
t.Printf("\t%s,\n", to[value])
|
||||
}
|
||||
t.Printf("}\n\n")
|
||||
}
|
||||
|
||||
func (t *optab) stringmethod(e *Enum) {
|
||||
s := map[string]string{}
|
||||
for _, value := range e.Values() {
|
||||
s[value] = strconv.Quote(value)
|
||||
}
|
||||
t.mapping(e, "String", "string", `""`, s)
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
)
|
||||
|
||||
// signature provides access to details about the signature of an instruction function.
|
||||
type signature interface {
|
||||
ParameterList() string
|
||||
Arguments() string
|
||||
ParameterName(int) string
|
||||
ParameterSlice() string
|
||||
Length() string
|
||||
}
|
||||
|
||||
// argslist is the signature for a function with the given named parameters.
|
||||
type argslist []string
|
||||
|
||||
func (a argslist) ParameterList() string { return strings.Join(a, ", ") + " " + operandType }
|
||||
func (a argslist) Arguments() string { return strings.Join(a, ", ") }
|
||||
func (a argslist) ParameterName(i int) string { return a[i] }
|
||||
func (a argslist) ParameterSlice() string {
|
||||
return fmt.Sprintf("[]%s{%s}", operandType, strings.Join(a, ", "))
|
||||
}
|
||||
func (a argslist) Length() string { return strconv.Itoa(len(a)) }
|
||||
|
||||
// variadic is the signature for a variadic function.
|
||||
type variadic struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (v variadic) ParameterList() string { return v.name + " ..." + operandType }
|
||||
func (v variadic) Arguments() string { return v.name + "..." }
|
||||
func (v variadic) ParameterName(i int) string { return fmt.Sprintf("%s[%d]", v.name, i) }
|
||||
func (v variadic) ParameterSlice() string { return v.name }
|
||||
func (v variadic) Length() string { return fmt.Sprintf("len(%s)", v.name) }
|
||||
|
||||
// niladic is the signature for a function with no arguments.
|
||||
type niladic struct{}
|
||||
|
||||
func (n niladic) ParameterList() string { return "" }
|
||||
func (n niladic) Arguments() string { return "" }
|
||||
func (n niladic) ParameterName(i int) string { panic("niladic function has no parameters") }
|
||||
func (n niladic) ParameterSlice() string { return "nil" }
|
||||
func (n niladic) Length() string { return "0" }
|
||||
|
||||
// params generates the function parameters and a function.
|
||||
func params(i inst.Instruction) signature {
|
||||
// Handle the case of forms with multiple arities.
|
||||
switch {
|
||||
case i.IsVariadic():
|
||||
return variadic{name: "ops"}
|
||||
case i.IsNiladic():
|
||||
return niladic{}
|
||||
}
|
||||
|
||||
// Generate nice-looking variable names.
|
||||
n := i.Arity()
|
||||
ops := make([]string, n)
|
||||
count := map[string]int{}
|
||||
for j := 0; j < n; j++ {
|
||||
// Collect unique lowercase bytes from first characters of operand types.
|
||||
s := map[byte]bool{}
|
||||
for _, f := range i.Forms {
|
||||
c := f.Operands[j].Type[0]
|
||||
if 'a' <= c && c <= 'z' {
|
||||
s[c] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Operand name is the sorted bytes.
|
||||
var b []byte
|
||||
for c := range s {
|
||||
b = append(b, c)
|
||||
}
|
||||
sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
|
||||
name := string(b)
|
||||
|
||||
// Append a counter if we've seen it already.
|
||||
m := count[name]
|
||||
count[name]++
|
||||
if m > 0 {
|
||||
name += strconv.Itoa(m)
|
||||
}
|
||||
ops[j] = name
|
||||
}
|
||||
|
||||
return argslist(ops)
|
||||
}
|
||||
|
||||
// doc generates the lines of the function comment.
|
||||
func doc(i inst.Instruction) []string {
|
||||
lines := []string{
|
||||
fmt.Sprintf("%s: %s.", i.Opcode, i.Summary),
|
||||
"",
|
||||
"Forms:",
|
||||
"",
|
||||
}
|
||||
|
||||
// Write a table of instruction forms.
|
||||
buf := bytes.NewBuffer(nil)
|
||||
w := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
|
||||
for _, f := range i.Forms {
|
||||
row := i.Opcode + "\t" + strings.Join(f.Signature(), "\t") + "\n"
|
||||
fmt.Fprint(w, row)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
tbl := strings.TrimSpace(buf.String())
|
||||
for _, line := range strings.Split(tbl, "\n") {
|
||||
lines = append(lines, "\t"+line)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
85
internal/gen/testing.go
Normal file
85
internal/gen/testing.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"github.com/mmcloughlin/avo/internal/api"
|
||||
"github.com/mmcloughlin/avo/internal/inst"
|
||||
"github.com/mmcloughlin/avo/internal/prnt"
|
||||
)
|
||||
|
||||
// DeclareTestArguments prints a block of variables declaring a valid operand of
|
||||
// each operand type.
|
||||
func DeclareTestArguments(g *prnt.Generator) {
|
||||
g.Printf("var (\n")
|
||||
for _, arg := range validArgs {
|
||||
g.Printf("\t%s operand.Op = %s\n", TestArgumentName(arg.Type), arg.Code)
|
||||
}
|
||||
g.Printf(")\n")
|
||||
}
|
||||
|
||||
// TestSignature returns a function signature with arguments matching the given
|
||||
// instruction form. Requires variables declared by DeclareTestArguments().
|
||||
func TestSignature(f inst.Form) api.Signature {
|
||||
var names []string
|
||||
for _, op := range f.Operands {
|
||||
names = append(names, TestArgumentName(op.Type))
|
||||
}
|
||||
return api.ArgsList(names)
|
||||
}
|
||||
|
||||
// TestArgumentName returns the name of the variable of operand type t declared
|
||||
// by DeclareTestArguments().
|
||||
func TestArgumentName(t string) string {
|
||||
return "op" + t
|
||||
}
|
||||
|
||||
var validArgs = []struct {
|
||||
Type string
|
||||
Code string
|
||||
}{
|
||||
// Immediates
|
||||
{"1", "operand.Imm(1)"},
|
||||
{"3", "operand.Imm(3)"},
|
||||
{"imm2u", "operand.Imm(3)"},
|
||||
{"imm8", "operand.Imm(math.MaxInt8)"},
|
||||
{"imm16", "operand.Imm(math.MaxInt16)"},
|
||||
{"imm32", "operand.Imm(math.MaxInt32)"},
|
||||
{"imm64", "operand.Imm(math.MaxInt64)"},
|
||||
|
||||
// Registers
|
||||
{"al", "reg.AL"},
|
||||
{"cl", "reg.CL"},
|
||||
{"ax", "reg.AX"},
|
||||
{"eax", "reg.EAX"},
|
||||
{"rax", "reg.RAX"},
|
||||
{"r8", "reg.CH"},
|
||||
{"r16", "reg.R9W"},
|
||||
{"r32", "reg.R10L"},
|
||||
{"r64", "reg.R11"},
|
||||
{"xmm0", "reg.X0"},
|
||||
{"xmm", "reg.X7"},
|
||||
{"ymm", "reg.Y15"},
|
||||
{"zmm", "reg.Z31"},
|
||||
{"k", "reg.K7"},
|
||||
|
||||
// Memory
|
||||
{"m", "operand.Mem{Base: reg.BX, Index: reg.CX, Scale: 2}"},
|
||||
{"m8", "operand.Mem{Base: reg.BL, Index: reg.CH, Scale: 1}"},
|
||||
{"m16", "operand.Mem{Base: reg.BX, Index: reg.CX, Scale: 2}"},
|
||||
{"m32", "operand.Mem{Base: reg.EBX, Index: reg.ECX, Scale: 4}"},
|
||||
{"m64", "operand.Mem{Base: reg.RBX, Index: reg.RCX, Scale: 8}"},
|
||||
{"m128", "operand.Mem{Base: reg.RBX, Index: reg.RCX, Scale: 8}"},
|
||||
{"m256", "operand.Mem{Base: reg.RBX, Index: reg.RCX, Scale: 8}"},
|
||||
{"m512", "operand.Mem{Base: reg.RBX, Index: reg.RCX, Scale: 8}"},
|
||||
|
||||
// Vector memory
|
||||
{"vm32x", "operand.Mem{Base: reg.R13, Index: reg.X4, Scale: 1}"},
|
||||
{"vm64x", "operand.Mem{Base: reg.R13, Index: reg.X8, Scale: 1}"},
|
||||
{"vm32y", "operand.Mem{Base: reg.R13, Index: reg.Y4, Scale: 1}"},
|
||||
{"vm64y", "operand.Mem{Base: reg.R13, Index: reg.Y8, Scale: 1}"},
|
||||
{"vm32z", "operand.Mem{Base: reg.R13, Index: reg.Z4, Scale: 1}"},
|
||||
{"vm64z", "operand.Mem{Base: reg.R13, Index: reg.Z8, Scale: 1}"},
|
||||
|
||||
// Relative
|
||||
{"rel8", "operand.Rel(math.MaxInt8)"},
|
||||
{"rel32", "operand.LabelRef(\"lbl\")"},
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package gen
|
||||
|
||||
// cross returns the cross product of the lists in x.
|
||||
func cross(x [][]string) [][]string {
|
||||
r := [][]string{nil}
|
||||
for _, s := range x {
|
||||
var nxt [][]string
|
||||
for _, pre := range r {
|
||||
for _, a := range s {
|
||||
concat := make([]string, len(pre), len(pre)+1)
|
||||
copy(concat, pre)
|
||||
concat = append(concat, a)
|
||||
nxt = append(nxt, concat)
|
||||
}
|
||||
}
|
||||
r = nxt
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCross(t *testing.T) {
|
||||
x := [][]string{
|
||||
{"a", "b", "c"},
|
||||
{"1", "2"},
|
||||
{"!", "?"},
|
||||
}
|
||||
expect := [][]string{
|
||||
{"a", "1", "!"},
|
||||
{"a", "1", "?"},
|
||||
{"a", "2", "!"},
|
||||
{"a", "2", "?"},
|
||||
{"b", "1", "!"},
|
||||
{"b", "1", "?"},
|
||||
{"b", "2", "!"},
|
||||
{"b", "2", "?"},
|
||||
{"c", "1", "!"},
|
||||
{"c", "1", "?"},
|
||||
{"c", "2", "!"},
|
||||
{"c", "2", "?"},
|
||||
}
|
||||
got := cross(x)
|
||||
if !reflect.DeepEqual(got, expect) {
|
||||
t.Errorf("bad cross product")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossSimple(t *testing.T) {
|
||||
x := [][]string{
|
||||
{"a", "b"},
|
||||
}
|
||||
expect := [][]string{
|
||||
{"a"},
|
||||
{"b"},
|
||||
}
|
||||
got := cross(x)
|
||||
if !reflect.DeepEqual(got, expect) {
|
||||
t.Errorf("bad cross product")
|
||||
}
|
||||
}
|
||||
9
internal/inst/stress_test.go
Normal file
9
internal/inst/stress_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// The godata test relies on a huge generated file, so we limit it to a
|
||||
// stress-test only build.
|
||||
|
||||
//go:build stress
|
||||
// +build stress
|
||||
|
||||
package inst
|
||||
|
||||
//go:generate avogen -bootstrap -data ../data -output ztable_test.go godatatest
|
||||
@@ -1,7 +1,11 @@
|
||||
package inst
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate avogen -bootstrap -data ../data -output ztable.go godata
|
||||
//go:generate avogen -bootstrap -data ../data -output ztable_test.go godatatest
|
||||
|
||||
// Lookup returns the instruction with the given opcode. Boolean return value
|
||||
// indicates whether the instruction was found.
|
||||
@@ -13,3 +17,119 @@ func Lookup(opcode string) (Instruction, bool) {
|
||||
}
|
||||
return Instruction{}, false
|
||||
}
|
||||
|
||||
// OperandTypes returns all the operand types that appear in the provided
|
||||
// instructions.
|
||||
func OperandTypes(is []Instruction) []string {
|
||||
set := map[string]bool{}
|
||||
for _, i := range is {
|
||||
for _, f := range i.Forms {
|
||||
for _, op := range f.Operands {
|
||||
set[op.Type] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedslice(set)
|
||||
}
|
||||
|
||||
// ImplicitRegisters returns all the registers that appear as implicit operands
|
||||
// in the provided instructions.
|
||||
func ImplicitRegisters(is []Instruction) []string {
|
||||
set := map[string]bool{}
|
||||
for _, i := range is {
|
||||
for _, f := range i.Forms {
|
||||
for _, op := range f.ImplicitOperands {
|
||||
set[op.Register] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedslice(set)
|
||||
}
|
||||
|
||||
// UniqueSuffixes returns all the non-empty suffixes that appear in the provided
|
||||
// instructions.
|
||||
func UniqueSuffixes(is []Instruction) []Suffix {
|
||||
// Collect set.
|
||||
set := map[Suffix]bool{}
|
||||
for _, i := range is {
|
||||
for _, f := range i.Forms {
|
||||
for _, suffixes := range f.SupportedSuffixes() {
|
||||
for _, suffix := range suffixes {
|
||||
set[suffix] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to sorted slice.
|
||||
suffixes := make([]Suffix, 0, len(set))
|
||||
for suffix := range set {
|
||||
suffixes = append(suffixes, suffix)
|
||||
}
|
||||
|
||||
sort.Slice(suffixes, func(i, j int) bool {
|
||||
return suffixes[i] < suffixes[j]
|
||||
})
|
||||
|
||||
return suffixes
|
||||
}
|
||||
|
||||
// SuffixesClasses returns all possible classes of suffix combinations.
|
||||
func SuffixesClasses(is []Instruction) map[string][]Suffixes {
|
||||
classes := map[string][]Suffixes{}
|
||||
for _, i := range is {
|
||||
for _, f := range i.Forms {
|
||||
class := f.SuffixesClass()
|
||||
if _, ok := classes[class]; ok {
|
||||
continue
|
||||
}
|
||||
classes[class] = f.SupportedSuffixes()
|
||||
}
|
||||
}
|
||||
return classes
|
||||
}
|
||||
|
||||
// ISAs returns all the unique ISAs seen in the given instructions.
|
||||
func ISAs(is []Instruction) []string {
|
||||
set := map[string]bool{}
|
||||
for _, i := range is {
|
||||
for _, f := range i.Forms {
|
||||
for _, isa := range f.ISA {
|
||||
set[isa] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedslice(set)
|
||||
}
|
||||
|
||||
// ISACombinations returns all the unique combinations of ISAs seen in the given
|
||||
// instructions.
|
||||
func ISACombinations(is []Instruction) [][]string {
|
||||
var combinations [][]string
|
||||
seen := map[string]bool{}
|
||||
for _, i := range is {
|
||||
for _, f := range i.Forms {
|
||||
isas := append([]string(nil), f.ISA...)
|
||||
sort.Strings(isas)
|
||||
key := strings.Join(isas, ",")
|
||||
|
||||
if !seen[key] {
|
||||
combinations = append(combinations, isas)
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return combinations
|
||||
}
|
||||
|
||||
// sortedslice builds a sorted slice of strings from a set.
|
||||
func sortedslice(set map[string]bool) []string {
|
||||
ss := make([]string, 0, len(set))
|
||||
for s := range set {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
|
||||
sort.Strings(ss)
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package inst_test
|
||||
import (
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -70,6 +71,84 @@ func TestInstructionProperties(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHaveSuffixes(t *testing.T) {
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
if len(f.SupportedSuffixes()) == 0 {
|
||||
t.Errorf("%s: no supported suffixes", i.Opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptsSuffixes(t *testing.T) {
|
||||
// Verify consistency between the AcceptsSuffixes and SupportedSuffixes methods.
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
expect := false
|
||||
for _, suffixes := range f.SupportedSuffixes() {
|
||||
if len(suffixes) > 0 {
|
||||
expect = true
|
||||
}
|
||||
}
|
||||
|
||||
if got := f.AcceptsSuffixes(); got != expect {
|
||||
t.Errorf("%s: AcceptsSuffixes() = %v; expect %v", i.Opcode, got, expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixesClasses(t *testing.T) {
|
||||
// Verify that all instructions in a suffix class support the same suffixes.
|
||||
reps := map[string][]inst.Suffixes{}
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
class := f.SuffixesClass()
|
||||
expect, ok := reps[class]
|
||||
if !ok {
|
||||
t.Logf("new class %q: representative from instruction %s", class, i.Opcode)
|
||||
reps[class] = f.SupportedSuffixes()
|
||||
continue
|
||||
}
|
||||
|
||||
got := f.SupportedSuffixes()
|
||||
if !reflect.DeepEqual(expect, got) {
|
||||
t.Fatalf("suffixes mismatch for class %q", class)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixesHaveSummaries(t *testing.T) {
|
||||
set := map[inst.Suffix]bool{}
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
for _, suffixes := range f.SupportedSuffixes() {
|
||||
for _, suffix := range suffixes {
|
||||
set[suffix] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for suffix := range set {
|
||||
if suffix.Summary() == "" {
|
||||
t.Errorf("suffix %q missing summary", suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestISASorted(t *testing.T) {
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
if !sort.StringsAreSorted(f.ISA) {
|
||||
t.Fatalf("%s: isa not sorted", i.Opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssembles(t *testing.T) {
|
||||
g := gen.NewAsmTest(printer.NewArgvConfig())
|
||||
b, err := g.Generate(inst.Instructions)
|
||||
@@ -143,17 +222,86 @@ func TestCancellingInputs(t *testing.T) {
|
||||
n := 0
|
||||
for _, op := range f.Operands {
|
||||
if op.Action.Read() {
|
||||
n++
|
||||
switch op.Type {
|
||||
case "r8", "r16", "r32", "r64", "xmm", "ymm":
|
||||
// pass
|
||||
case "r8", "r16", "r32", "r64", "xmm", "ymm", "zmm":
|
||||
n++
|
||||
case "k":
|
||||
// skip mask registers
|
||||
default:
|
||||
t.Errorf("%s: unexpected operand type %q for self-cancelling input", i.Opcode, op.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
if n != 2 {
|
||||
t.Errorf("%s: expected two inputs for self-cancelling form", i.Opcode)
|
||||
if n < 2 {
|
||||
t.Log(f)
|
||||
t.Errorf("%s: expected at least two inputs for self-cancelling form", i.Opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagOperandTypes(t *testing.T) {
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
// Check for memory operands.
|
||||
mem := false
|
||||
for _, op := range f.Operands {
|
||||
if strings.HasPrefix(op.Type, "m") {
|
||||
mem = true
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast applies to memory instructions only.
|
||||
if f.Broadcast && !mem {
|
||||
t.Errorf("%s: expect broadcast form to have memory operand", i.Opcode)
|
||||
}
|
||||
|
||||
// Embedded rounding must be register-only.
|
||||
if f.EmbeddedRounding && mem {
|
||||
t.Errorf("%s: embedded-rounding only supported for register-only forms", i.Opcode)
|
||||
}
|
||||
|
||||
// Suppress all exceptions is register only.
|
||||
if f.SuppressAllExceptions && mem {
|
||||
t.Errorf("%s: embedded-rounding only supported for register-only forms", i.Opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagCombinations(t *testing.T) {
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
if f.EmbeddedRounding && f.SuppressAllExceptions {
|
||||
t.Errorf("%s: embedded-rounding cannot be combined with suppress-all-exceptions", i.Opcode)
|
||||
}
|
||||
if f.Broadcast && f.EmbeddedRounding {
|
||||
t.Errorf("%s: broadcast cannot be combined with embedded-rounding", i.Opcode)
|
||||
}
|
||||
if f.Broadcast && f.SuppressAllExceptions {
|
||||
t.Errorf("%s: broadcast cannot be combined with suppress-all-exceptions", i.Opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroingHasMask(t *testing.T) {
|
||||
for _, i := range inst.Instructions {
|
||||
for _, f := range i.Forms {
|
||||
if !f.Zeroing {
|
||||
continue
|
||||
}
|
||||
|
||||
// Expect mask operand.
|
||||
mask := false
|
||||
for _, op := range f.Operands {
|
||||
if op.Type == "k" {
|
||||
mask = true
|
||||
}
|
||||
}
|
||||
|
||||
if !mask {
|
||||
t.Errorf("%s: expect mask operand if zeroing is enabled", i.Opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ type Instruction struct {
|
||||
Opcode string // Golang assembly mnemonic
|
||||
AliasOf string // Opcode of instruction that this is an alias for
|
||||
Summary string // Description of the instruction
|
||||
Forms []Form // Accepted operand forms
|
||||
Forms // Accepted operand forms
|
||||
}
|
||||
|
||||
// IsTerminal reports whether the instruction exits a function.
|
||||
@@ -40,10 +40,13 @@ func (i Instruction) IsConditionalBranch() bool {
|
||||
return i.IsBranch() && i.Opcode != "JMP"
|
||||
}
|
||||
|
||||
// Forms is a collection of instruction forms.
|
||||
type Forms []Form
|
||||
|
||||
// Arities returns the unique arities among the instruction forms.
|
||||
func (i Instruction) Arities() []int {
|
||||
func (fs Forms) Arities() []int {
|
||||
s := map[int]bool{}
|
||||
for _, f := range i.Forms {
|
||||
for _, f := range fs {
|
||||
s[f.Arity()] = true
|
||||
}
|
||||
a := make([]int, 0, len(s))
|
||||
@@ -56,22 +59,22 @@ func (i Instruction) Arities() []int {
|
||||
|
||||
// Arity is a convenience for returning the unique instruction arity when you
|
||||
// know it is not variadic. Panics for a variadic instruction.
|
||||
func (i Instruction) Arity() int {
|
||||
if i.IsVariadic() {
|
||||
func (fs Forms) Arity() int {
|
||||
if fs.IsVariadic() {
|
||||
panic("variadic")
|
||||
}
|
||||
a := i.Arities()
|
||||
a := fs.Arities()
|
||||
return a[0]
|
||||
}
|
||||
|
||||
// IsVariadic reports whether the instruction has more than one arity.
|
||||
func (i Instruction) IsVariadic() bool {
|
||||
return len(i.Arities()) > 1
|
||||
func (fs Forms) IsVariadic() bool {
|
||||
return len(fs.Arities()) > 1
|
||||
}
|
||||
|
||||
// IsNiladic reports whether the instruction takes no operands.
|
||||
func (i Instruction) IsNiladic() bool {
|
||||
a := i.Arities()
|
||||
func (fs Forms) IsNiladic() bool {
|
||||
a := fs.Arities()
|
||||
return len(a) == 1 && a[0] == 0
|
||||
}
|
||||
|
||||
@@ -86,12 +89,35 @@ type Form struct {
|
||||
// Registers read or written but not explicitly passed to the instruction.
|
||||
ImplicitOperands []ImplicitOperand
|
||||
|
||||
// Encoding type required for this instruction form.
|
||||
EncodingType EncodingType
|
||||
|
||||
// CancellingInputs indicates this instruction form has no dependency on the
|
||||
// input operands when they refer to the same register. The classic example of
|
||||
// this is "XORQ RAX, RAX", in which case the output has no dependence on the
|
||||
// value of RAX. Instruction forms with cancelling inputs have only two input
|
||||
// operands, which have the same register type.
|
||||
CancellingInputs bool
|
||||
|
||||
// Zeroing indicates whether the instruction form uses AVX-512 zeroing. This
|
||||
// is the .Z suffix in Go, usually indicated with {z} operand suffix in
|
||||
// Intel manuals.
|
||||
Zeroing bool
|
||||
|
||||
// EmbeddedRounding indicates whether the instruction form uses AVX-512
|
||||
// embedded rounding. This is the RN_SAE, RZ_SAE, RD_SAE and RU_SAE suffixes
|
||||
// in Go, usually indicated with {er} in Intel manuals.
|
||||
EmbeddedRounding bool
|
||||
|
||||
// SuppressAllExceptions indicates whether the instruction form uses AVX-512
|
||||
// "suppress all exceptions". This is the SAE suffix in Go, usually
|
||||
// indicated with {sae} in Intel manuals.
|
||||
SuppressAllExceptions bool
|
||||
|
||||
// Broadcast indicates whether the instruction form uses AVX-512
|
||||
// broadcast. This is the BCST suffix in Go, usually indicated by operand
|
||||
// types like "m64bcst" in Intel manuals.
|
||||
Broadcast bool
|
||||
}
|
||||
|
||||
// Arity returns the number of operands this form expects.
|
||||
@@ -108,6 +134,143 @@ func (f Form) Signature() []string {
|
||||
return s
|
||||
}
|
||||
|
||||
// Clone the instruction form.
|
||||
func (f Form) Clone() Form {
|
||||
c := f
|
||||
c.ISA = append([]string(nil), f.ISA...)
|
||||
c.Operands = append([]Operand(nil), f.Operands...)
|
||||
c.ImplicitOperands = append([]ImplicitOperand(nil), f.ImplicitOperands...)
|
||||
return c
|
||||
}
|
||||
|
||||
// AcceptsSuffixes reports whether this form takes any opcode suffixes.
|
||||
func (f Form) AcceptsSuffixes() bool {
|
||||
return f.Broadcast || f.EmbeddedRounding || f.SuppressAllExceptions || f.Zeroing
|
||||
}
|
||||
|
||||
// SuffixesClass returns a key representing the class of instruction suffixes it
|
||||
// accepts. All instructions sharing a suffix class accept the same suffixes.
|
||||
func (f Form) SuffixesClass() string {
|
||||
if !f.AcceptsSuffixes() {
|
||||
return "nil"
|
||||
}
|
||||
var parts []string
|
||||
for _, flag := range []struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
}{
|
||||
{"er", f.EmbeddedRounding},
|
||||
{"sae", f.SuppressAllExceptions},
|
||||
{"bcst", f.Broadcast},
|
||||
{"z", f.Zeroing},
|
||||
} {
|
||||
if flag.Enabled {
|
||||
parts = append(parts, flag.Name)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "_")
|
||||
}
|
||||
|
||||
// SupportedSuffixes returns the list of all possible suffix combinations
|
||||
// supported by this instruction form.
|
||||
func (f Form) SupportedSuffixes() []Suffixes {
|
||||
suffixes := []Suffixes{
|
||||
{},
|
||||
}
|
||||
|
||||
add := func(ss ...Suffix) {
|
||||
var exts []Suffixes
|
||||
for _, s := range ss {
|
||||
for _, suffix := range suffixes {
|
||||
ext := append(Suffixes(nil), suffix...)
|
||||
ext = append(ext, s)
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
}
|
||||
suffixes = exts
|
||||
}
|
||||
|
||||
if f.Broadcast {
|
||||
add(BCST)
|
||||
}
|
||||
|
||||
if f.EmbeddedRounding {
|
||||
add(RN_SAE, RZ_SAE, RD_SAE, RU_SAE)
|
||||
}
|
||||
|
||||
if f.SuppressAllExceptions {
|
||||
add(SAE)
|
||||
}
|
||||
|
||||
if f.Zeroing {
|
||||
add(Z)
|
||||
}
|
||||
|
||||
return suffixes
|
||||
}
|
||||
|
||||
// Suffix is an opcode suffix.
|
||||
type Suffix string
|
||||
|
||||
// Supported opcode suffixes in x86 assembly.
|
||||
const (
|
||||
BCST Suffix = "BCST"
|
||||
RN_SAE Suffix = "RN_SAE"
|
||||
RZ_SAE Suffix = "RZ_SAE"
|
||||
RD_SAE Suffix = "RD_SAE"
|
||||
RU_SAE Suffix = "RU_SAE"
|
||||
SAE Suffix = "SAE"
|
||||
Z Suffix = "Z"
|
||||
)
|
||||
|
||||
func (s Suffix) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// Summary of the opcode suffix, for documentation purposes.
|
||||
func (s Suffix) Summary() string {
|
||||
return suffixsummary[s]
|
||||
}
|
||||
|
||||
var suffixsummary = map[Suffix]string{
|
||||
BCST: "Broadcast",
|
||||
RN_SAE: "Round Towards Nearest",
|
||||
RZ_SAE: "Round Towards Zero",
|
||||
RD_SAE: "Round Towards Negative Infinity",
|
||||
RU_SAE: "Round Towards Positive Infinity",
|
||||
SAE: "Suppress All Exceptions",
|
||||
Z: "Zeroing Masking",
|
||||
}
|
||||
|
||||
// Suffixes is a list of opcode suffixes.
|
||||
type Suffixes []Suffix
|
||||
|
||||
// String returns the dot-separated suffixes.
|
||||
func (s Suffixes) String() string { return s.Join(".") }
|
||||
|
||||
// Join suffixes with the given separator.
|
||||
func (s Suffixes) Join(sep string) string {
|
||||
return strings.Join(s.Strings(), sep)
|
||||
}
|
||||
|
||||
// Strings returns the suffixes as strings.
|
||||
func (s Suffixes) Strings() []string {
|
||||
var ss []string
|
||||
for _, suffix := range s {
|
||||
ss = append(ss, suffix.String())
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// Summaries returns all the suffix summaries.
|
||||
func (s Suffixes) Summaries() []string {
|
||||
var summaries []string
|
||||
for _, suffix := range s {
|
||||
summaries = append(summaries, suffix.Summary())
|
||||
}
|
||||
return summaries
|
||||
}
|
||||
|
||||
// Operand is an operand to an instruction, describing the expected type and read/write action.
|
||||
type Operand struct {
|
||||
Type string
|
||||
@@ -125,9 +288,10 @@ type Action uint8
|
||||
|
||||
// Possible Action types.
|
||||
const (
|
||||
R Action = 0x1
|
||||
W Action = 0x2
|
||||
RW Action = R | W
|
||||
R Action = 1 << iota // Read
|
||||
W // Write
|
||||
|
||||
RW Action = R | W // Read-Write
|
||||
)
|
||||
|
||||
// ActionFromReadWrite builds an Action from boolean flags.
|
||||
@@ -142,19 +306,24 @@ func ActionFromReadWrite(r, w bool) Action {
|
||||
return a
|
||||
}
|
||||
|
||||
// Contains reports whether a supports all actions in s.
|
||||
func (a Action) Contains(s Action) bool {
|
||||
// ContainsAll reports whether a supports all actions in s.
|
||||
func (a Action) ContainsAll(s Action) bool {
|
||||
return (a & s) == s
|
||||
}
|
||||
|
||||
// ContainsAny reports whether a supports any actions in s.
|
||||
func (a Action) ContainsAny(s Action) bool {
|
||||
return (a & s) != 0
|
||||
}
|
||||
|
||||
// Read reports whether a supports read.
|
||||
func (a Action) Read() bool {
|
||||
return a.Contains(R)
|
||||
return a.ContainsAll(R)
|
||||
}
|
||||
|
||||
// Write reports whether a supports write.
|
||||
func (a Action) Write() bool {
|
||||
return a.Contains(W)
|
||||
return a.ContainsAll(W)
|
||||
}
|
||||
|
||||
// String represents a as a human-readable string.
|
||||
@@ -168,3 +337,14 @@ func (a Action) String() string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// EncodingType specifies a category of encoding types.
|
||||
type EncodingType uint8
|
||||
|
||||
// Supported encoding types.
|
||||
const (
|
||||
EncodingTypeLegacy EncodingType = 1 + iota
|
||||
EncodingTypeREX
|
||||
EncodingTypeVEX
|
||||
EncodingTypeEVEX
|
||||
)
|
||||
|
||||
82
internal/inst/types_test.go
Normal file
82
internal/inst/types_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package inst
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormSupportedSuffixes(t *testing.T) {
|
||||
cases := []struct {
|
||||
Form Form
|
||||
Expect []Suffixes
|
||||
}{
|
||||
{
|
||||
Form: Form{},
|
||||
Expect: []Suffixes{
|
||||
{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Form: Form{
|
||||
Broadcast: true,
|
||||
},
|
||||
Expect: []Suffixes{
|
||||
{BCST},
|
||||
},
|
||||
},
|
||||
{
|
||||
Form: Form{
|
||||
EmbeddedRounding: true,
|
||||
},
|
||||
Expect: []Suffixes{
|
||||
{RN_SAE},
|
||||
{RZ_SAE},
|
||||
{RD_SAE},
|
||||
{RU_SAE},
|
||||
},
|
||||
},
|
||||
{
|
||||
Form: Form{
|
||||
SuppressAllExceptions: true,
|
||||
},
|
||||
Expect: []Suffixes{
|
||||
{SAE},
|
||||
},
|
||||
},
|
||||
{
|
||||
Form: Form{
|
||||
Zeroing: true,
|
||||
},
|
||||
Expect: []Suffixes{
|
||||
{Z},
|
||||
},
|
||||
},
|
||||
{
|
||||
Form: Form{
|
||||
EmbeddedRounding: true,
|
||||
Zeroing: true,
|
||||
},
|
||||
Expect: []Suffixes{
|
||||
{RN_SAE, Z},
|
||||
{RZ_SAE, Z},
|
||||
{RD_SAE, Z},
|
||||
{RU_SAE, Z},
|
||||
},
|
||||
},
|
||||
{
|
||||
Form: Form{
|
||||
Broadcast: true,
|
||||
Zeroing: true,
|
||||
},
|
||||
Expect: []Suffixes{
|
||||
{BCST, Z},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := c.Form.SupportedSuffixes()
|
||||
if !reflect.DeepEqual(c.Expect, got) {
|
||||
t.Errorf("%v.SupportedSuffixes() = %v; expect %v", c.Form, got, c.Expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
104531
internal/inst/ztable.go
104531
internal/inst/ztable.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,8 +1,11 @@
|
||||
package load
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -12,6 +15,18 @@ import (
|
||||
"github.com/mmcloughlin/avo/internal/opcodesxml"
|
||||
)
|
||||
|
||||
// This file is a mess. Some of this complexity is unavoidable, since the state
|
||||
// of x86 instruction databases is also a mess, especially when it comes to
|
||||
// idiosyncrasies of the Go assembler implementation. Some of the complexity is
|
||||
// probably avoidable by migrating to using Intel XED
|
||||
// (https://github.com/mmcloughlin/avo/issues/23), but for now this is an unholy
|
||||
// mix of PeachPy's Opcodes database and Go's x86 CSV file.
|
||||
//
|
||||
// The goal is simply to keep as much of the uglyness in this file as possible,
|
||||
// producing a clean instruction database for the rest of avo to use. Any nasty
|
||||
// logic here should be backed up with a test somewhere to ensure the result is
|
||||
// correct, even if the code that produced it is awful.
|
||||
|
||||
// Expected data source filenames.
|
||||
const (
|
||||
DefaultCSVName = "x86.v0.2.csv"
|
||||
@@ -64,7 +79,8 @@ func (l *Loader) Load() ([]inst.Instruction, error) {
|
||||
Summary: i.Summary,
|
||||
}
|
||||
}
|
||||
im[opcode].Forms = append(im[opcode].Forms, l.form(opcode, f))
|
||||
forms := l.forms(opcode, f)
|
||||
im[opcode].Forms = append(im[opcode].Forms, forms...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +110,15 @@ func (l *Loader) Load() ([]inst.Instruction, error) {
|
||||
i.Forms = dedupe(i.Forms)
|
||||
}
|
||||
|
||||
// Convert to a slice, sorted by opcode.
|
||||
// Resolve forms that have VEX and EVEX encoded forms.
|
||||
for _, i := range im {
|
||||
i.Forms, err = vexevex(i.Forms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to a slice. Sort instructions and forms for reproducibility.
|
||||
is := make([]inst.Instruction, 0, len(im))
|
||||
for _, i := range im {
|
||||
is = append(is, *i)
|
||||
@@ -104,6 +128,10 @@ func (l *Loader) Load() ([]inst.Instruction, error) {
|
||||
return is[i].Opcode < is[j].Opcode
|
||||
})
|
||||
|
||||
for _, i := range im {
|
||||
sortforms(i.Forms)
|
||||
}
|
||||
|
||||
return is, nil
|
||||
}
|
||||
|
||||
@@ -132,6 +160,9 @@ func (l Loader) include(f opcodesxml.Form) bool {
|
||||
// AMD-only.
|
||||
case "TBM", "CLZERO", "FMA4", "XOP", "SSE4A", "3dnow!", "3dnow!+":
|
||||
return false
|
||||
// AVX512PF doesn't work without some special case handling, and is only on Knights Landing/Knights Mill.
|
||||
case "AVX512PF":
|
||||
return false
|
||||
// Incomplete support for some prefetching instructions.
|
||||
case "PREFETCH", "PREFETCHW", "PREFETCHWT1", "CLWB":
|
||||
return false
|
||||
@@ -139,11 +170,6 @@ func (l Loader) include(f opcodesxml.Form) bool {
|
||||
case "MONITORX", "FEMMS":
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO(mbm): support AVX512
|
||||
if strings.HasPrefix(isa.ID, "AVX512") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Go appears to have skeleton support for MMX instructions. See the many TODO lines in the testcases:
|
||||
@@ -277,32 +303,12 @@ func (l Loader) gonames(f opcodesxml.Form) []string {
|
||||
n := strings.ToUpper(f.GASName)
|
||||
|
||||
// Some need data sizes added to them.
|
||||
// TODO(mbm): is there a better way of determining which ones these are?
|
||||
suffix := map[int]string{16: "W", 32: "L", 64: "Q", 128: "X", 256: "Y"}
|
||||
switch n {
|
||||
case "VCVTUSI2SS", "VCVTSD2USI", "VCVTSS2USI", "VCVTUSI2SD", "VCVTTSS2USI", "VCVTTSD2USI":
|
||||
fallthrough
|
||||
case "MOVBEW", "MOVBEL", "MOVBEQ":
|
||||
// MOVEBE* instructions seem to be inconsistent with x86 CSV.
|
||||
//
|
||||
// Reference: https://github.com/golang/arch/blob/b19384d3c130858bb31a343ea8fce26be71b5998/x86/x86spec/format.go#L282-L287
|
||||
//
|
||||
// "MOVBE r16, m16": "movbeww",
|
||||
// "MOVBE m16, r16": "movbeww",
|
||||
// "MOVBE m32, r32": "movbell",
|
||||
// "MOVBE r32, m32": "movbell",
|
||||
// "MOVBE m64, r64": "movbeqq",
|
||||
// "MOVBE r64, m64": "movbeqq",
|
||||
//
|
||||
fallthrough
|
||||
case "RDRAND", "RDSEED":
|
||||
n += suffix[s]
|
||||
}
|
||||
n += sizesuffix(n, f)
|
||||
|
||||
return []string{n}
|
||||
}
|
||||
|
||||
func (l Loader) form(opcode string, f opcodesxml.Form) inst.Form {
|
||||
func (l Loader) forms(opcode string, f opcodesxml.Form) []inst.Form {
|
||||
// Map operands to avo format and ensure correct order.
|
||||
ops := operands(f.Operands)
|
||||
|
||||
@@ -357,13 +363,36 @@ func (l Loader) form(opcode string, f opcodesxml.Form) inst.Form {
|
||||
for _, isa := range f.ISA {
|
||||
isas = append(isas, isa.ID)
|
||||
}
|
||||
sort.Strings(isas)
|
||||
|
||||
return inst.Form{
|
||||
// Initialize form.
|
||||
form := inst.Form{
|
||||
ISA: isas,
|
||||
Operands: ops,
|
||||
ImplicitOperands: implicits,
|
||||
EncodingType: enctype(f),
|
||||
CancellingInputs: f.CancellingInputs,
|
||||
}
|
||||
|
||||
// Apply modification stages to produce final list of forms.
|
||||
stages := []func(string, inst.Form) []inst.Form{
|
||||
avx512rounding,
|
||||
avx512sae,
|
||||
avx512bcst,
|
||||
avx512masking,
|
||||
avx512zeroing,
|
||||
}
|
||||
|
||||
forms := []inst.Form{form}
|
||||
for _, stage := range stages {
|
||||
var next []inst.Form
|
||||
for _, f := range forms {
|
||||
next = append(next, stage(opcode, f)...)
|
||||
}
|
||||
forms = next
|
||||
}
|
||||
|
||||
return forms
|
||||
}
|
||||
|
||||
// operands maps Opcodes XML operands to avo format. Returned in Intel order.
|
||||
@@ -384,6 +413,187 @@ func operand(op opcodesxml.Operand) inst.Operand {
|
||||
}
|
||||
}
|
||||
|
||||
// avx512rounding handles AVX-512 embedded rounding. Opcodes database represents
|
||||
// these as {er} operands, whereas Go uses instruction suffixes. Remove the
|
||||
// operand if present and set the corresponding flag.
|
||||
func avx512rounding(opcode string, f inst.Form) []inst.Form {
|
||||
i, found := findoperand(f.Operands, "{er}")
|
||||
if !found {
|
||||
return []inst.Form{f}
|
||||
}
|
||||
|
||||
// Delete the {er} operand.
|
||||
f.Operands = append(f.Operands[:i], f.Operands[i+1:]...)
|
||||
|
||||
// Create a second form with the rounding flag.
|
||||
er := f.Clone()
|
||||
er.EmbeddedRounding = true
|
||||
|
||||
return []inst.Form{f, er}
|
||||
}
|
||||
|
||||
// avx512sae handles AVX-512 "suppress all exceptions". Opcodes database
|
||||
// represents these as {sae} operands, whereas Go uses instruction suffixes.
|
||||
// Remove the operand if present and set the corresponding flag.
|
||||
func avx512sae(opcode string, f inst.Form) []inst.Form {
|
||||
i, found := findoperand(f.Operands, "{sae}")
|
||||
if !found {
|
||||
return []inst.Form{f}
|
||||
}
|
||||
|
||||
// Delete the {sae} operand.
|
||||
f.Operands = append(f.Operands[:i], f.Operands[i+1:]...)
|
||||
|
||||
// Create a second form with the rounding flag.
|
||||
sae := f.Clone()
|
||||
sae.SuppressAllExceptions = true
|
||||
|
||||
return []inst.Form{f, sae}
|
||||
}
|
||||
|
||||
// avx512bcst handles AVX-512 broadcast. Opcodes database uses operands like
|
||||
// "m512/m64bcst" to indicate broadcast. Go uses the BCST suffix to enable it.
|
||||
// Split the form into two, the regular and broadcast versions.
|
||||
func avx512bcst(opcode string, f inst.Form) []inst.Form {
|
||||
// Look for broadcast operand.
|
||||
idx := -1
|
||||
for i, op := range f.Operands {
|
||||
if bcstrx.MatchString(op.Type) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx < 0 {
|
||||
return []inst.Form{f}
|
||||
}
|
||||
|
||||
// Create two forms.
|
||||
match := bcstrx.FindStringSubmatch(f.Operands[idx].Type)
|
||||
|
||||
mem := f.Clone()
|
||||
mem.Operands[idx].Type = match[1]
|
||||
|
||||
bcst := f.Clone()
|
||||
bcst.Broadcast = true
|
||||
bcst.Operands[idx].Type = match[2]
|
||||
|
||||
return []inst.Form{mem, bcst}
|
||||
}
|
||||
|
||||
var bcstrx = regexp.MustCompile(`^(m\d+)/(m\d+)bcst$`)
|
||||
|
||||
// avx512masking handles AVX-512 masking forms.
|
||||
func avx512masking(opcode string, f inst.Form) []inst.Form {
|
||||
// In order to support implicit masking (with K0), Go has two instruction
|
||||
// forms, one with the mask and one without. The mask register precedes the
|
||||
// output register. The Opcodes database (similar to Intel manuals)
|
||||
// represents masking with the {k} operand suffix, possibly with {z} for
|
||||
// zeroing.
|
||||
|
||||
// Look for masking with possible zeroing. Zeroing is handled by a later
|
||||
// processing stage, but we need to be sure to notice and preserve it here.
|
||||
masking := false
|
||||
zeroing := false
|
||||
idx := -1
|
||||
for i := range f.Operands {
|
||||
op := &f.Operands[i]
|
||||
if strings.HasSuffix(op.Type, "{z}") {
|
||||
zeroing = true
|
||||
op.Type = strings.TrimSuffix(op.Type, "{z}")
|
||||
}
|
||||
if strings.HasSuffix(op.Type, "{k}") {
|
||||
masking = true
|
||||
idx = i
|
||||
op.Type = strings.TrimSuffix(op.Type, "{k}")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Bail if no masking.
|
||||
if !masking {
|
||||
return []inst.Form{f}
|
||||
}
|
||||
|
||||
// Unmasked variant.
|
||||
unmasked := f.Clone()
|
||||
|
||||
// Masked form has "k" operand inserted.
|
||||
masked := f.Clone()
|
||||
mask := inst.Operand{Type: "k", Action: inst.R}
|
||||
ops := append([]inst.Operand(nil), masked.Operands[:idx]...)
|
||||
ops = append(ops, mask)
|
||||
ops = append(ops, masked.Operands[idx:]...)
|
||||
masked.Operands = ops
|
||||
|
||||
// Restore zeroing suffix, so it can he handled later.
|
||||
if zeroing {
|
||||
masked.Operands[idx+1].Type += "{z}"
|
||||
}
|
||||
|
||||
// Almost all instructions take an optional mask, apart from a few
|
||||
// special cases.
|
||||
if maskrequired[opcode] {
|
||||
return []inst.Form{masked}
|
||||
}
|
||||
return []inst.Form{unmasked, masked}
|
||||
}
|
||||
|
||||
// avx512zeroing handles AVX-512 zeroing forms.
|
||||
func avx512zeroing(opcode string, f inst.Form) []inst.Form {
|
||||
// Zeroing in Go is handled with the Z opcode suffix. Note that zeroing has
|
||||
// an important effect on the instruction form, since the merge masking form
|
||||
// has an input dependency for the output register, and the zeroing form
|
||||
// does not.
|
||||
|
||||
// Look for zeroing operand.
|
||||
idx := -1
|
||||
for i := range f.Operands {
|
||||
op := &f.Operands[i]
|
||||
if strings.HasSuffix(op.Type, "{z}") {
|
||||
idx = i
|
||||
op.Type = strings.TrimSuffix(op.Type, "{z}")
|
||||
}
|
||||
}
|
||||
|
||||
if idx < 0 {
|
||||
return []inst.Form{f}
|
||||
}
|
||||
|
||||
// Duplicate into two forms for merging and zeroing.
|
||||
merging := f.Clone()
|
||||
merging.Operands[idx].Action |= inst.R
|
||||
|
||||
zeroing := f.Clone()
|
||||
zeroing.Zeroing = true
|
||||
|
||||
return []inst.Form{merging, zeroing}
|
||||
}
|
||||
|
||||
// findoperand looks for an operand type and returns its index, if found.
|
||||
func findoperand(ops []inst.Operand, t string) (int, bool) {
|
||||
for i, op := range ops {
|
||||
if op.Type == t {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// enctype selects the encoding type for the instruction form.
|
||||
func enctype(f opcodesxml.Form) inst.EncodingType {
|
||||
switch {
|
||||
case f.Encoding.EVEX != nil:
|
||||
return inst.EncodingTypeEVEX
|
||||
case f.Encoding.VEX != nil:
|
||||
return inst.EncodingTypeVEX
|
||||
case f.Encoding.REX != nil:
|
||||
return inst.EncodingTypeREX
|
||||
default:
|
||||
return inst.EncodingTypeLegacy
|
||||
}
|
||||
}
|
||||
|
||||
// datasize (intelligently) guesses the datasize of an instruction form.
|
||||
func datasize(f opcodesxml.Form) int {
|
||||
// Determine from encoding bits.
|
||||
@@ -413,6 +623,220 @@ func operandsize(op opcodesxml.Operand) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// sizesuffix returns an optional size suffix to be added to the opcode name.
|
||||
func sizesuffix(n string, f opcodesxml.Form) string {
|
||||
// Reference: https://github.com/golang/arch/blob/5de9028c2478e6cb4e1c1b1f4386f3f0a93e383a/x86/x86avxgen/main.go#L275-L322
|
||||
//
|
||||
// func addGoSuffixes(ctx *context) {
|
||||
// var opcodeSuffixMatchers map[string][]string
|
||||
// {
|
||||
// opXY := []string{"VL=0", "X", "VL=1", "Y"}
|
||||
// opXYZ := []string{"VL=0", "X", "VL=1", "Y", "VL=2", "Z"}
|
||||
// opQ := []string{"REXW=1", "Q"}
|
||||
// opLQ := []string{"REXW=0", "L", "REXW=1", "Q"}
|
||||
//
|
||||
// opcodeSuffixMatchers = map[string][]string{
|
||||
// "VCVTPD2DQ": opXY,
|
||||
// "VCVTPD2PS": opXY,
|
||||
// "VCVTTPD2DQ": opXY,
|
||||
// "VCVTQQ2PS": opXY,
|
||||
// "VCVTUQQ2PS": opXY,
|
||||
// "VCVTPD2UDQ": opXY,
|
||||
// "VCVTTPD2UDQ": opXY,
|
||||
//
|
||||
// "VFPCLASSPD": opXYZ,
|
||||
// "VFPCLASSPS": opXYZ,
|
||||
//
|
||||
// "VCVTSD2SI": opQ,
|
||||
// "VCVTTSD2SI": opQ,
|
||||
// "VCVTTSS2SI": opQ,
|
||||
// "VCVTSS2SI": opQ,
|
||||
//
|
||||
// "VCVTSD2USI": opLQ,
|
||||
// "VCVTSS2USI": opLQ,
|
||||
// "VCVTTSD2USI": opLQ,
|
||||
// "VCVTTSS2USI": opLQ,
|
||||
// "VCVTUSI2SD": opLQ,
|
||||
// "VCVTUSI2SS": opLQ,
|
||||
// "VCVTSI2SD": opLQ,
|
||||
// "VCVTSI2SS": opLQ,
|
||||
// "ANDN": opLQ,
|
||||
// "BEXTR": opLQ,
|
||||
// "BLSI": opLQ,
|
||||
// "BLSMSK": opLQ,
|
||||
// "BLSR": opLQ,
|
||||
// "BZHI": opLQ,
|
||||
// "MULX": opLQ,
|
||||
// "PDEP": opLQ,
|
||||
// "PEXT": opLQ,
|
||||
// "RORX": opLQ,
|
||||
// "SARX": opLQ,
|
||||
// "SHLX": opLQ,
|
||||
// "SHRX": opLQ,
|
||||
// }
|
||||
// }
|
||||
//
|
||||
|
||||
type rule struct {
|
||||
Size func(opcodesxml.Form) int
|
||||
Suffix map[int]string
|
||||
}
|
||||
|
||||
var (
|
||||
XY = rule{evexLLsize, map[int]string{128: "X", 256: "Y"}}
|
||||
XYZ = rule{evexLLsize, map[int]string{128: "X", 256: "Y", 512: "Z"}}
|
||||
Q = rule{rexWsize, map[int]string{64: "Q"}}
|
||||
LQ = rule{rexWsize, map[int]string{32: "L", 64: "Q"}}
|
||||
WLQ = rule{datasize, map[int]string{16: "W", 32: "L", 64: "Q"}}
|
||||
)
|
||||
|
||||
rules := map[string]rule{
|
||||
"VCVTPD2DQ": XY,
|
||||
"VCVTPD2PS": XY,
|
||||
"VCVTTPD2DQ": XY,
|
||||
"VCVTQQ2PS": XY,
|
||||
"VCVTUQQ2PS": XY,
|
||||
"VCVTPD2UDQ": XY,
|
||||
"VCVTTPD2UDQ": XY,
|
||||
|
||||
"VFPCLASSPD": XYZ,
|
||||
"VFPCLASSPS": XYZ,
|
||||
|
||||
"VCVTSD2SI": Q,
|
||||
"VCVTTSD2SI": Q,
|
||||
"VCVTTSS2SI": Q,
|
||||
"VCVTSS2SI": Q,
|
||||
|
||||
"VCVTSD2USI": LQ,
|
||||
"VCVTSS2USI": LQ,
|
||||
"VCVTTSD2USI": LQ,
|
||||
"VCVTTSS2USI": LQ,
|
||||
"VCVTUSI2SD": LQ,
|
||||
"VCVTUSI2SS": LQ,
|
||||
"VCVTSI2SD": LQ,
|
||||
"VCVTSI2SS": LQ,
|
||||
"ANDN": LQ,
|
||||
"BEXTR": LQ,
|
||||
"BLSI": LQ,
|
||||
"BLSMSK": LQ,
|
||||
"BLSR": LQ,
|
||||
"BZHI": LQ,
|
||||
"MULX": LQ,
|
||||
"PDEP": LQ,
|
||||
"PEXT": LQ,
|
||||
"RORX": LQ,
|
||||
"SARX": LQ,
|
||||
"SHLX": LQ,
|
||||
"SHRX": LQ,
|
||||
|
||||
"RDRAND": LQ,
|
||||
"RDSEED": LQ,
|
||||
|
||||
// MOVEBE* instructions seem to be inconsistent with x86 CSV.
|
||||
//
|
||||
// Reference: https://github.com/golang/arch/blob/b19384d3c130858bb31a343ea8fce26be71b5998/x86/x86spec/format.go#L282-L287
|
||||
//
|
||||
// "MOVBE r16, m16": "movbeww",
|
||||
// "MOVBE m16, r16": "movbeww",
|
||||
// "MOVBE m32, r32": "movbell",
|
||||
// "MOVBE r32, m32": "movbell",
|
||||
// "MOVBE m64, r64": "movbeqq",
|
||||
// "MOVBE r64, m64": "movbeqq",
|
||||
//
|
||||
"MOVBEW": WLQ,
|
||||
"MOVBEL": WLQ,
|
||||
"MOVBEQ": WLQ,
|
||||
}
|
||||
|
||||
r, ok := rules[n]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
s := r.Size(f)
|
||||
return r.Suffix[s]
|
||||
}
|
||||
|
||||
func rexWsize(f opcodesxml.Form) int {
|
||||
e := f.Encoding
|
||||
switch {
|
||||
case e.EVEX != nil && e.EVEX.W != nil:
|
||||
return 32 << *e.EVEX.W
|
||||
default:
|
||||
return 32
|
||||
}
|
||||
}
|
||||
|
||||
func evexLLsize(f opcodesxml.Form) int {
|
||||
e := f.Encoding
|
||||
if e.EVEX == nil {
|
||||
return 0
|
||||
}
|
||||
size := map[string]int{"00": 128, "01": 256, "10": 512}
|
||||
return size[e.EVEX.LL]
|
||||
}
|
||||
|
||||
// vexevex fixes instructions that have both VEX and EVEX encoded forms with the
|
||||
// same operand types. Go uses the VEX encoded form unless EVEX-only features
|
||||
// are used. This function will only keep the VEX encoded version in the case
|
||||
// where both exist.
|
||||
//
|
||||
// Note this is somewhat of a hack. There are real reasons to use the EVEX
|
||||
// encoded version even when both exist. The main reason to use the EVEX version
|
||||
// rather than VEX is to use the registers Z16, Z17, ... and up. However, avo
|
||||
// does not implement the logic to distinguish between the two halfs of the
|
||||
// vector registers. So in its current state the only reason to need the EVEX
|
||||
// version is to encode suffixes, and these are represented by other instruction
|
||||
// forms.
|
||||
//
|
||||
// TODO(mbm): restrict use of vector registers https://github.com/mmcloughlin/avo/issues/146
|
||||
func vexevex(fs []inst.Form) ([]inst.Form, error) {
|
||||
// Group forms by deduping ID.
|
||||
byid := map[string][]inst.Form{}
|
||||
for _, f := range fs {
|
||||
id := fmt.Sprintf(
|
||||
"%s {%t,%t,%t,%t}",
|
||||
strings.Join(f.Signature(), "_"),
|
||||
f.Zeroing,
|
||||
f.EmbeddedRounding,
|
||||
f.SuppressAllExceptions,
|
||||
f.Broadcast,
|
||||
)
|
||||
byid[id] = append(byid[id], f)
|
||||
}
|
||||
|
||||
// Resolve overlaps.
|
||||
var results []inst.Form
|
||||
for id, group := range byid {
|
||||
if len(group) < 2 {
|
||||
results = append(results, group...)
|
||||
continue
|
||||
}
|
||||
|
||||
// We expect these conflicts are caused by VEX/EVEX pairs. Bail if it's
|
||||
// something else.
|
||||
if len(group) > 2 {
|
||||
return nil, fmt.Errorf("more than two forms of type %q", id)
|
||||
}
|
||||
|
||||
if group[0].EncodingType == inst.EncodingTypeEVEX {
|
||||
group[0], group[1] = group[1], group[0]
|
||||
}
|
||||
|
||||
if group[0].EncodingType != inst.EncodingTypeVEX || group[1].EncodingType != inst.EncodingTypeEVEX {
|
||||
fmt.Println(group)
|
||||
return nil, errors.New("expected pair of VEX/EVEX encoded forms")
|
||||
}
|
||||
|
||||
vex := group[0]
|
||||
|
||||
// In this case we only keep the VEX encoded form.
|
||||
results = append(results, vex)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// dedupe a list of forms.
|
||||
func dedupe(fs []inst.Form) []inst.Form {
|
||||
uniq := make([]inst.Form, 0, len(fs))
|
||||
@@ -430,3 +854,14 @@ func dedupe(fs []inst.Form) []inst.Form {
|
||||
}
|
||||
return uniq
|
||||
}
|
||||
|
||||
// sortforms sorts a list of forms
|
||||
func sortforms(fs []inst.Form) {
|
||||
sort.Slice(fs, func(i, j int) bool {
|
||||
return sortkey(fs[i]) < sortkey(fs[j])
|
||||
})
|
||||
}
|
||||
|
||||
func sortkey(f inst.Form) string {
|
||||
return fmt.Sprintf("%d %v %v", f.EncodingType, f.ISA, f)
|
||||
}
|
||||
|
||||
@@ -95,6 +95,54 @@ func init() {
|
||||
aliases = append(aliases, annoyingaliases...)
|
||||
}
|
||||
|
||||
// maskrequired is a set of AVX-512 opcodes where the mask register is required.
|
||||
// Usually the mask register can be omitted, in which case K0 is implied.
|
||||
var maskrequired = map[string]bool{
|
||||
// Reference: https://github.com/golang/go/blob/4fd94558820100129b98f284e21b19fc27a99926/src/cmd/internal/obj/x86/asm6.go#L4219-L4240
|
||||
//
|
||||
// // Checks to warn about instruction/arguments combinations that
|
||||
// // will unconditionally trigger illegal instruction trap (#UD).
|
||||
// switch p.As {
|
||||
// case AVGATHERDPD,
|
||||
// AVGATHERQPD,
|
||||
// AVGATHERDPS,
|
||||
// AVGATHERQPS,
|
||||
// AVPGATHERDD,
|
||||
// AVPGATHERQD,
|
||||
// AVPGATHERDQ,
|
||||
// AVPGATHERQQ:
|
||||
// // AVX512 gather requires explicit K mask.
|
||||
// if p.GetFrom3().Reg >= REG_K0 && p.GetFrom3().Reg <= REG_K7 {
|
||||
// if !avx512gatherValid(ctxt, p) {
|
||||
// return
|
||||
// }
|
||||
// } else {
|
||||
// if !avx2gatherValid(ctxt, p) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
"VGATHERDPD": true,
|
||||
"VGATHERQPD": true,
|
||||
"VGATHERDPS": true,
|
||||
"VGATHERQPS": true,
|
||||
"VPGATHERDD": true,
|
||||
"VPGATHERQD": true,
|
||||
"VPGATHERDQ": true,
|
||||
"VPGATHERQQ": true,
|
||||
|
||||
// Restriction applies to SCATTER instructions too.
|
||||
"VPSCATTERDD": true,
|
||||
"VPSCATTERDQ": true,
|
||||
"VPSCATTERQD": true,
|
||||
"VPSCATTERQQ": true,
|
||||
"VSCATTERDPD": true,
|
||||
"VSCATTERDPS": true,
|
||||
"VSCATTERQPD": true,
|
||||
"VSCATTERQPS": true,
|
||||
}
|
||||
|
||||
// extras is simply a list of extra instructions to add to the database.
|
||||
var extras = []*inst.Instruction{
|
||||
// MOVLQZX does not appear in either x86 CSV or Opcodes, but does appear in stdlib assembly.
|
||||
|
||||
@@ -75,6 +75,11 @@ func (g *Generator) Comment(lines ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// BuildTag outputs a build tag.
|
||||
func (g *Generator) BuildTag(tag string) {
|
||||
g.Comment("+build " + tag)
|
||||
}
|
||||
|
||||
// AddError records an error in code generation. The first non-nil error will
|
||||
// prevent printing operations from writing anything else, and the error will be
|
||||
// returned from Result().
|
||||
|
||||
Reference in New Issue
Block a user