mirror of
https://github.com/golang/go
synced 2024-11-14 15:00:27 -07:00
2b8e143dc3
If we cannot infer the asm arch from the filename or the build tags, assume that it is the current build arch. Assembly files with no restrictions ought to be usable on all arches. Updates #11041 Change-Id: I0ae807dbbd5fb67ca21d0157fe180237a074113a Reviewed-on: https://go-review.googlesource.com/27151 Run-TryBot: Josh Bleecher Snyder <josharian@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rob Pike <r@golang.org>
693 lines
16 KiB
Go
693 lines
16 KiB
Go
// Copyright 2013 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Identify mismatches between assembly files and Go func declarations.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/token"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// 'kind' is a kind of assembly variable.
|
|
// The kinds 1, 2, 4, 8 stand for values of that size.
|
|
type asmKind int
|
|
|
|
// These special kinds are not valid sizes.
|
|
const (
|
|
asmString asmKind = 100 + iota
|
|
asmSlice
|
|
asmInterface
|
|
asmEmptyInterface
|
|
)
|
|
|
|
// An asmArch describes assembly parameters for an architecture
|
|
type asmArch struct {
|
|
name string
|
|
ptrSize int
|
|
intSize int
|
|
maxAlign int
|
|
bigEndian bool
|
|
stack string
|
|
lr bool
|
|
}
|
|
|
|
// An asmFunc describes the expected variables for a function on a given architecture.
|
|
type asmFunc struct {
|
|
arch *asmArch
|
|
size int // size of all arguments
|
|
vars map[string]*asmVar
|
|
varByOffset map[int]*asmVar
|
|
}
|
|
|
|
// An asmVar describes a single assembly variable.
|
|
type asmVar struct {
|
|
name string
|
|
kind asmKind
|
|
typ string
|
|
off int
|
|
size int
|
|
inner []*asmVar
|
|
}
|
|
|
|
var (
|
|
asmArch386 = asmArch{"386", 4, 4, 4, false, "SP", false}
|
|
asmArchArm = asmArch{"arm", 4, 4, 4, false, "R13", true}
|
|
asmArchArm64 = asmArch{"arm64", 8, 8, 8, false, "RSP", true}
|
|
asmArchAmd64 = asmArch{"amd64", 8, 8, 8, false, "SP", false}
|
|
asmArchAmd64p32 = asmArch{"amd64p32", 4, 4, 8, false, "SP", false}
|
|
asmArchMips64 = asmArch{"mips64", 8, 8, 8, true, "R29", true}
|
|
asmArchMips64LE = asmArch{"mips64", 8, 8, 8, false, "R29", true}
|
|
asmArchPpc64 = asmArch{"ppc64", 8, 8, 8, true, "R1", true}
|
|
asmArchPpc64LE = asmArch{"ppc64le", 8, 8, 8, false, "R1", true}
|
|
|
|
arches = []*asmArch{
|
|
&asmArch386,
|
|
&asmArchArm,
|
|
&asmArchArm64,
|
|
&asmArchAmd64,
|
|
&asmArchAmd64p32,
|
|
&asmArchMips64,
|
|
&asmArchMips64LE,
|
|
&asmArchPpc64,
|
|
&asmArchPpc64LE,
|
|
}
|
|
)
|
|
|
|
var (
|
|
re = regexp.MustCompile
|
|
asmPlusBuild = re(`//\s+\+build\s+([^\n]+)`)
|
|
asmTEXT = re(`\bTEXT\b.*·([^\(]+)\(SB\)(?:\s*,\s*([0-9A-Z|+]+))?(?:\s*,\s*\$(-?[0-9]+)(?:-([0-9]+))?)?`)
|
|
asmDATA = re(`\b(DATA|GLOBL)\b`)
|
|
asmNamedFP = re(`([a-zA-Z0-9_\xFF-\x{10FFFF}]+)(?:\+([0-9]+))\(FP\)`)
|
|
asmUnnamedFP = re(`[^+\-0-9](([0-9]+)\(FP\))`)
|
|
asmSP = re(`[^+\-0-9](([0-9]+)\(([A-Z0-9]+)\))`)
|
|
asmOpcode = re(`^\s*(?:[A-Z0-9a-z_]+:)?\s*([A-Z]+)\s*([^,]*)(?:,\s*(.*))?`)
|
|
ppc64Suff = re(`([BHWD])(ZU|Z|U|BR)?$`)
|
|
)
|
|
|
|
func asmCheck(pkg *Package) {
|
|
if !vet("asmdecl") {
|
|
return
|
|
}
|
|
|
|
// No work if no assembly files.
|
|
if !pkg.hasFileWithSuffix(".s") {
|
|
return
|
|
}
|
|
|
|
// Gather declarations. knownFunc[name][arch] is func description.
|
|
knownFunc := make(map[string]map[string]*asmFunc)
|
|
|
|
for _, f := range pkg.files {
|
|
if f.file != nil {
|
|
for _, decl := range f.file.Decls {
|
|
if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil {
|
|
knownFunc[decl.Name.Name] = f.asmParseDecl(decl)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Files:
|
|
for _, f := range pkg.files {
|
|
if !strings.HasSuffix(f.name, ".s") {
|
|
continue
|
|
}
|
|
Println("Checking file", f.name)
|
|
|
|
// Determine architecture from file name if possible.
|
|
var arch string
|
|
var archDef *asmArch
|
|
for _, a := range arches {
|
|
if strings.HasSuffix(f.name, "_"+a.name+".s") {
|
|
arch = a.name
|
|
archDef = a
|
|
break
|
|
}
|
|
}
|
|
|
|
lines := strings.SplitAfter(string(f.content), "\n")
|
|
var (
|
|
fn *asmFunc
|
|
fnName string
|
|
localSize, argSize int
|
|
wroteSP bool
|
|
haveRetArg bool
|
|
retLine []int
|
|
)
|
|
|
|
flushRet := func() {
|
|
if fn != nil && fn.vars["ret"] != nil && !haveRetArg && len(retLine) > 0 {
|
|
v := fn.vars["ret"]
|
|
for _, line := range retLine {
|
|
f.Badf(token.NoPos, "%s:%d: [%s] %s: RET without writing to %d-byte ret+%d(FP)", f.name, line, arch, fnName, v.size, v.off)
|
|
}
|
|
}
|
|
retLine = nil
|
|
}
|
|
for lineno, line := range lines {
|
|
lineno++
|
|
|
|
badf := func(format string, args ...interface{}) {
|
|
f.Badf(token.NoPos, "%s:%d: [%s] %s: %s", f.name, lineno, arch, fnName, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
if arch == "" {
|
|
// Determine architecture from +build line if possible.
|
|
if m := asmPlusBuild.FindStringSubmatch(line); m != nil {
|
|
Fields:
|
|
for _, fld := range strings.Fields(m[1]) {
|
|
for _, a := range arches {
|
|
if a.name == fld {
|
|
arch = a.name
|
|
archDef = a
|
|
break Fields
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if m := asmTEXT.FindStringSubmatch(line); m != nil {
|
|
flushRet()
|
|
if arch == "" {
|
|
for _, a := range arches {
|
|
if a.name == build.Default.GOARCH {
|
|
arch = a.name
|
|
archDef = a
|
|
break
|
|
}
|
|
}
|
|
if arch == "" {
|
|
f.Warnf(token.NoPos, "%s: cannot determine architecture for assembly file", f.name)
|
|
continue Files
|
|
}
|
|
}
|
|
fnName = m[1]
|
|
fn = knownFunc[m[1]][arch]
|
|
if fn != nil {
|
|
size, _ := strconv.Atoi(m[4])
|
|
if size != fn.size && (m[2] != "7" && !strings.Contains(m[2], "NOSPLIT") || size != 0) {
|
|
badf("wrong argument size %d; expected $...-%d", size, fn.size)
|
|
}
|
|
}
|
|
localSize, _ = strconv.Atoi(m[3])
|
|
localSize += archDef.intSize
|
|
if archDef.lr {
|
|
// Account for caller's saved LR
|
|
localSize += archDef.intSize
|
|
}
|
|
argSize, _ = strconv.Atoi(m[4])
|
|
if fn == nil && !strings.Contains(fnName, "<>") {
|
|
badf("function %s missing Go declaration", fnName)
|
|
}
|
|
wroteSP = false
|
|
haveRetArg = false
|
|
continue
|
|
} else if strings.Contains(line, "TEXT") && strings.Contains(line, "SB") {
|
|
// function, but not visible from Go (didn't match asmTEXT), so stop checking
|
|
flushRet()
|
|
fn = nil
|
|
fnName = ""
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(line, "RET") {
|
|
retLine = append(retLine, lineno)
|
|
}
|
|
|
|
if fnName == "" {
|
|
continue
|
|
}
|
|
|
|
if asmDATA.FindStringSubmatch(line) != nil {
|
|
fn = nil
|
|
}
|
|
|
|
if archDef == nil {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(line, ", "+archDef.stack) || strings.Contains(line, ",\t"+archDef.stack) {
|
|
wroteSP = true
|
|
continue
|
|
}
|
|
|
|
for _, m := range asmSP.FindAllStringSubmatch(line, -1) {
|
|
if m[3] != archDef.stack || wroteSP {
|
|
continue
|
|
}
|
|
off := 0
|
|
if m[1] != "" {
|
|
off, _ = strconv.Atoi(m[2])
|
|
}
|
|
if off >= localSize {
|
|
if fn != nil {
|
|
v := fn.varByOffset[off-localSize]
|
|
if v != nil {
|
|
badf("%s should be %s+%d(FP)", m[1], v.name, off-localSize)
|
|
continue
|
|
}
|
|
}
|
|
if off >= localSize+argSize {
|
|
badf("use of %s points beyond argument frame", m[1])
|
|
continue
|
|
}
|
|
badf("use of %s to access argument frame", m[1])
|
|
}
|
|
}
|
|
|
|
if fn == nil {
|
|
continue
|
|
}
|
|
|
|
for _, m := range asmUnnamedFP.FindAllStringSubmatch(line, -1) {
|
|
off, _ := strconv.Atoi(m[2])
|
|
v := fn.varByOffset[off]
|
|
if v != nil {
|
|
badf("use of unnamed argument %s; offset %d is %s+%d(FP)", m[1], off, v.name, v.off)
|
|
} else {
|
|
badf("use of unnamed argument %s", m[1])
|
|
}
|
|
}
|
|
|
|
for _, m := range asmNamedFP.FindAllStringSubmatch(line, -1) {
|
|
name := m[1]
|
|
off := 0
|
|
if m[2] != "" {
|
|
off, _ = strconv.Atoi(m[2])
|
|
}
|
|
if name == "ret" || strings.HasPrefix(name, "ret_") {
|
|
haveRetArg = true
|
|
}
|
|
v := fn.vars[name]
|
|
if v == nil {
|
|
// Allow argframe+0(FP).
|
|
if name == "argframe" && off == 0 {
|
|
continue
|
|
}
|
|
v = fn.varByOffset[off]
|
|
if v != nil {
|
|
badf("unknown variable %s; offset %d is %s+%d(FP)", name, off, v.name, v.off)
|
|
} else {
|
|
badf("unknown variable %s", name)
|
|
}
|
|
continue
|
|
}
|
|
asmCheckVar(badf, fn, line, m[0], off, v)
|
|
}
|
|
}
|
|
flushRet()
|
|
}
|
|
}
|
|
|
|
// asmParseDecl parses a function decl for expected assembly variables.
|
|
func (f *File) asmParseDecl(decl *ast.FuncDecl) map[string]*asmFunc {
|
|
var (
|
|
arch *asmArch
|
|
fn *asmFunc
|
|
offset int
|
|
failed bool
|
|
)
|
|
|
|
addVar := func(outer string, v asmVar) {
|
|
if vo := fn.vars[outer]; vo != nil {
|
|
vo.inner = append(vo.inner, &v)
|
|
}
|
|
fn.vars[v.name] = &v
|
|
for i := 0; i < v.size; i++ {
|
|
fn.varByOffset[v.off+i] = &v
|
|
}
|
|
}
|
|
|
|
addParams := func(list []*ast.Field) {
|
|
for i, fld := range list {
|
|
// Determine alignment, size, and kind of type in declaration.
|
|
var align, size int
|
|
var kind asmKind
|
|
names := fld.Names
|
|
typ := f.gofmt(fld.Type)
|
|
switch t := fld.Type.(type) {
|
|
default:
|
|
switch typ {
|
|
default:
|
|
f.Warnf(fld.Type.Pos(), "unknown assembly argument type %s", typ)
|
|
failed = true
|
|
return
|
|
case "int8", "uint8", "byte", "bool":
|
|
size = 1
|
|
case "int16", "uint16":
|
|
size = 2
|
|
case "int32", "uint32", "float32":
|
|
size = 4
|
|
case "int64", "uint64", "float64":
|
|
align = arch.maxAlign
|
|
size = 8
|
|
case "int", "uint":
|
|
size = arch.intSize
|
|
case "uintptr", "iword", "Word", "Errno", "unsafe.Pointer":
|
|
size = arch.ptrSize
|
|
case "string", "ErrorString":
|
|
size = arch.ptrSize * 2
|
|
align = arch.ptrSize
|
|
kind = asmString
|
|
}
|
|
case *ast.ChanType, *ast.FuncType, *ast.MapType, *ast.StarExpr:
|
|
size = arch.ptrSize
|
|
case *ast.InterfaceType:
|
|
align = arch.ptrSize
|
|
size = 2 * arch.ptrSize
|
|
if len(t.Methods.List) > 0 {
|
|
kind = asmInterface
|
|
} else {
|
|
kind = asmEmptyInterface
|
|
}
|
|
case *ast.ArrayType:
|
|
if t.Len == nil {
|
|
size = arch.ptrSize + 2*arch.intSize
|
|
align = arch.ptrSize
|
|
kind = asmSlice
|
|
break
|
|
}
|
|
f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ)
|
|
failed = true
|
|
case *ast.StructType:
|
|
f.Warnf(fld.Type.Pos(), "unsupported assembly argument type %s", typ)
|
|
failed = true
|
|
}
|
|
if align == 0 {
|
|
align = size
|
|
}
|
|
if kind == 0 {
|
|
kind = asmKind(size)
|
|
}
|
|
offset += -offset & (align - 1)
|
|
|
|
// Create variable for each name being declared with this type.
|
|
if len(names) == 0 {
|
|
name := "unnamed"
|
|
if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 && &list[0] == &decl.Type.Results.List[0] && i == 0 {
|
|
// Assume assembly will refer to single unnamed result as r.
|
|
name = "ret"
|
|
}
|
|
names = []*ast.Ident{{Name: name}}
|
|
}
|
|
for _, id := range names {
|
|
name := id.Name
|
|
addVar("", asmVar{
|
|
name: name,
|
|
kind: kind,
|
|
typ: typ,
|
|
off: offset,
|
|
size: size,
|
|
})
|
|
switch kind {
|
|
case 8:
|
|
if arch.ptrSize == 4 {
|
|
w1, w2 := "lo", "hi"
|
|
if arch.bigEndian {
|
|
w1, w2 = w2, w1
|
|
}
|
|
addVar(name, asmVar{
|
|
name: name + "_" + w1,
|
|
kind: 4,
|
|
typ: "half " + typ,
|
|
off: offset,
|
|
size: 4,
|
|
})
|
|
addVar(name, asmVar{
|
|
name: name + "_" + w2,
|
|
kind: 4,
|
|
typ: "half " + typ,
|
|
off: offset + 4,
|
|
size: 4,
|
|
})
|
|
}
|
|
|
|
case asmEmptyInterface:
|
|
addVar(name, asmVar{
|
|
name: name + "_type",
|
|
kind: asmKind(arch.ptrSize),
|
|
typ: "interface type",
|
|
off: offset,
|
|
size: arch.ptrSize,
|
|
})
|
|
addVar(name, asmVar{
|
|
name: name + "_data",
|
|
kind: asmKind(arch.ptrSize),
|
|
typ: "interface data",
|
|
off: offset + arch.ptrSize,
|
|
size: arch.ptrSize,
|
|
})
|
|
|
|
case asmInterface:
|
|
addVar(name, asmVar{
|
|
name: name + "_itable",
|
|
kind: asmKind(arch.ptrSize),
|
|
typ: "interface itable",
|
|
off: offset,
|
|
size: arch.ptrSize,
|
|
})
|
|
addVar(name, asmVar{
|
|
name: name + "_data",
|
|
kind: asmKind(arch.ptrSize),
|
|
typ: "interface data",
|
|
off: offset + arch.ptrSize,
|
|
size: arch.ptrSize,
|
|
})
|
|
|
|
case asmSlice:
|
|
addVar(name, asmVar{
|
|
name: name + "_base",
|
|
kind: asmKind(arch.ptrSize),
|
|
typ: "slice base",
|
|
off: offset,
|
|
size: arch.ptrSize,
|
|
})
|
|
addVar(name, asmVar{
|
|
name: name + "_len",
|
|
kind: asmKind(arch.intSize),
|
|
typ: "slice len",
|
|
off: offset + arch.ptrSize,
|
|
size: arch.intSize,
|
|
})
|
|
addVar(name, asmVar{
|
|
name: name + "_cap",
|
|
kind: asmKind(arch.intSize),
|
|
typ: "slice cap",
|
|
off: offset + arch.ptrSize + arch.intSize,
|
|
size: arch.intSize,
|
|
})
|
|
|
|
case asmString:
|
|
addVar(name, asmVar{
|
|
name: name + "_base",
|
|
kind: asmKind(arch.ptrSize),
|
|
typ: "string base",
|
|
off: offset,
|
|
size: arch.ptrSize,
|
|
})
|
|
addVar(name, asmVar{
|
|
name: name + "_len",
|
|
kind: asmKind(arch.intSize),
|
|
typ: "string len",
|
|
off: offset + arch.ptrSize,
|
|
size: arch.intSize,
|
|
})
|
|
}
|
|
offset += size
|
|
}
|
|
}
|
|
}
|
|
|
|
m := make(map[string]*asmFunc)
|
|
for _, arch = range arches {
|
|
fn = &asmFunc{
|
|
arch: arch,
|
|
vars: make(map[string]*asmVar),
|
|
varByOffset: make(map[int]*asmVar),
|
|
}
|
|
offset = 0
|
|
addParams(decl.Type.Params.List)
|
|
if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 {
|
|
offset += -offset & (arch.maxAlign - 1)
|
|
addParams(decl.Type.Results.List)
|
|
}
|
|
fn.size = offset
|
|
m[arch.name] = fn
|
|
}
|
|
|
|
if failed {
|
|
return nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
// asmCheckVar checks a single variable reference.
|
|
func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar) {
|
|
m := asmOpcode.FindStringSubmatch(line)
|
|
if m == nil {
|
|
if !strings.HasPrefix(strings.TrimSpace(line), "//") {
|
|
badf("cannot find assembly opcode")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Determine operand sizes from instruction.
|
|
// Typically the suffix suffices, but there are exceptions.
|
|
var src, dst, kind asmKind
|
|
op := m[1]
|
|
switch fn.arch.name + "." + op {
|
|
case "386.FMOVLP":
|
|
src, dst = 8, 4
|
|
case "arm.MOVD":
|
|
src = 8
|
|
case "arm.MOVW":
|
|
src = 4
|
|
case "arm.MOVH", "arm.MOVHU":
|
|
src = 2
|
|
case "arm.MOVB", "arm.MOVBU":
|
|
src = 1
|
|
// LEA* opcodes don't really read the second arg.
|
|
// They just take the address of it.
|
|
case "386.LEAL":
|
|
dst = 4
|
|
case "amd64.LEAQ":
|
|
dst = 8
|
|
case "amd64p32.LEAL":
|
|
dst = 4
|
|
default:
|
|
switch fn.arch.name {
|
|
case "386", "amd64":
|
|
if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "D") || strings.HasSuffix(op, "DP")) {
|
|
// FMOVDP, FXCHD, etc
|
|
src = 8
|
|
break
|
|
}
|
|
if strings.HasPrefix(op, "P") && strings.HasSuffix(op, "RD") {
|
|
// PINSRD, PEXTRD, etc
|
|
src = 4
|
|
break
|
|
}
|
|
if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "F") || strings.HasSuffix(op, "FP")) {
|
|
// FMOVFP, FXCHF, etc
|
|
src = 4
|
|
break
|
|
}
|
|
if strings.HasSuffix(op, "SD") {
|
|
// MOVSD, SQRTSD, etc
|
|
src = 8
|
|
break
|
|
}
|
|
if strings.HasSuffix(op, "SS") {
|
|
// MOVSS, SQRTSS, etc
|
|
src = 4
|
|
break
|
|
}
|
|
if strings.HasPrefix(op, "SET") {
|
|
// SETEQ, etc
|
|
src = 1
|
|
break
|
|
}
|
|
switch op[len(op)-1] {
|
|
case 'B':
|
|
src = 1
|
|
case 'W':
|
|
src = 2
|
|
case 'L':
|
|
src = 4
|
|
case 'D', 'Q':
|
|
src = 8
|
|
}
|
|
case "ppc64", "ppc64le":
|
|
// Strip standard suffixes to reveal size letter.
|
|
m := ppc64Suff.FindStringSubmatch(op)
|
|
if m != nil {
|
|
switch m[1][0] {
|
|
case 'B':
|
|
src = 1
|
|
case 'H':
|
|
src = 2
|
|
case 'W':
|
|
src = 4
|
|
case 'D':
|
|
src = 8
|
|
}
|
|
}
|
|
case "mips64", "mips64le":
|
|
switch op {
|
|
case "MOVB", "MOVBU":
|
|
src = 1
|
|
case "MOVH", "MOVHU":
|
|
src = 2
|
|
case "MOVW", "MOVWU", "MOVF":
|
|
src = 4
|
|
case "MOVV", "MOVD":
|
|
src = 8
|
|
}
|
|
}
|
|
}
|
|
if dst == 0 {
|
|
dst = src
|
|
}
|
|
|
|
// Determine whether the match we're holding
|
|
// is the first or second argument.
|
|
if strings.Index(line, expr) > strings.Index(line, ",") {
|
|
kind = dst
|
|
} else {
|
|
kind = src
|
|
}
|
|
|
|
vk := v.kind
|
|
vt := v.typ
|
|
switch vk {
|
|
case asmInterface, asmEmptyInterface, asmString, asmSlice:
|
|
// allow reference to first word (pointer)
|
|
vk = v.inner[0].kind
|
|
vt = v.inner[0].typ
|
|
}
|
|
|
|
if off != v.off {
|
|
var inner bytes.Buffer
|
|
for i, vi := range v.inner {
|
|
if len(v.inner) > 1 {
|
|
fmt.Fprintf(&inner, ",")
|
|
}
|
|
fmt.Fprintf(&inner, " ")
|
|
if i == len(v.inner)-1 {
|
|
fmt.Fprintf(&inner, "or ")
|
|
}
|
|
fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
|
|
}
|
|
badf("invalid offset %s; expected %s+%d(FP)%s", expr, v.name, v.off, inner.String())
|
|
return
|
|
}
|
|
if kind != 0 && kind != vk {
|
|
var inner bytes.Buffer
|
|
if len(v.inner) > 0 {
|
|
fmt.Fprintf(&inner, " containing")
|
|
for i, vi := range v.inner {
|
|
if i > 0 && len(v.inner) > 2 {
|
|
fmt.Fprintf(&inner, ",")
|
|
}
|
|
fmt.Fprintf(&inner, " ")
|
|
if i > 0 && i == len(v.inner)-1 {
|
|
fmt.Fprintf(&inner, "and ")
|
|
}
|
|
fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
|
|
}
|
|
}
|
|
badf("invalid %s of %s; %s is %d-byte value%s", op, expr, vt, vk, inner.String())
|
|
}
|
|
}
|