github.com/FenixAra/go@v0.0.0-20170127160404-96ea0918e670/src/cmd/vet/asmdecl.go (about) 1 // Copyright 2013 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Identify mismatches between assembly files and Go func declarations. 6 7 package main 8 9 import ( 10 "bytes" 11 "fmt" 12 "go/ast" 13 "go/build" 14 "go/token" 15 "go/types" 16 "regexp" 17 "strconv" 18 "strings" 19 ) 20 21 // 'kind' is a kind of assembly variable. 22 // The kinds 1, 2, 4, 8 stand for values of that size. 23 type asmKind int 24 25 // These special kinds are not valid sizes. 26 const ( 27 asmString asmKind = 100 + iota 28 asmSlice 29 asmArray 30 asmInterface 31 asmEmptyInterface 32 asmStruct 33 asmComplex 34 ) 35 36 // An asmArch describes assembly parameters for an architecture 37 type asmArch struct { 38 name string 39 sizes *types.StdSizes 40 bigEndian bool 41 stack string 42 lr bool 43 } 44 45 // An asmFunc describes the expected variables for a function on a given architecture. 46 type asmFunc struct { 47 arch *asmArch 48 size int // size of all arguments 49 vars map[string]*asmVar 50 varByOffset map[int]*asmVar 51 } 52 53 // An asmVar describes a single assembly variable. 54 type asmVar struct { 55 name string 56 kind asmKind 57 typ string 58 off int 59 size int 60 inner []*asmVar 61 } 62 63 // Common architecture word sizes and alignments. 64 var ( 65 size44 = &types.StdSizes{WordSize: 4, MaxAlign: 4} 66 size48 = &types.StdSizes{WordSize: 4, MaxAlign: 8} 67 size88 = &types.StdSizes{WordSize: 8, MaxAlign: 8} 68 ) 69 70 var ( 71 asmArch386 = asmArch{"386", size44, false, "SP", false} 72 asmArchArm = asmArch{"arm", size44, false, "R13", true} 73 asmArchArm64 = asmArch{"arm64", size88, false, "RSP", true} 74 asmArchAmd64 = asmArch{"amd64", size88, false, "SP", false} 75 asmArchAmd64p32 = asmArch{"amd64p32", size48, false, "SP", false} 76 asmArchMips = asmArch{"mips", size44, true, "R29", true} 77 asmArchMipsLE = asmArch{"mipsle", size44, false, "R29", true} 78 asmArchMips64 = asmArch{"mips64", size88, true, "R29", true} 79 asmArchMips64LE = asmArch{"mips64le", size88, false, "R29", true} 80 asmArchPpc64 = asmArch{"ppc64", size88, true, "R1", true} 81 asmArchPpc64LE = asmArch{"ppc64le", size88, false, "R1", true} 82 asmArchS390X = asmArch{"s390x", size88, true, "R15", true} 83 84 arches = []*asmArch{ 85 &asmArch386, 86 &asmArchArm, 87 &asmArchArm64, 88 &asmArchAmd64, 89 &asmArchAmd64p32, 90 &asmArchMips, 91 &asmArchMipsLE, 92 &asmArchMips64, 93 &asmArchMips64LE, 94 &asmArchPpc64, 95 &asmArchPpc64LE, 96 &asmArchS390X, 97 } 98 ) 99 100 func (a *asmArch) intSize() int { return int(a.sizes.WordSize) } 101 func (a *asmArch) ptrSize() int { return int(a.sizes.WordSize) } 102 func (a *asmArch) maxAlign() int { return int(a.sizes.MaxAlign) } 103 104 var ( 105 re = regexp.MustCompile 106 asmPlusBuild = re(`//\s+\+build\s+([^\n]+)`) 107 asmTEXT = re(`\bTEXT\b(.*)·([^\(]+)\(SB\)(?:\s*,\s*([0-9A-Z|+]+))?(?:\s*,\s*\$(-?[0-9]+)(?:-([0-9]+))?)?`) 108 asmDATA = re(`\b(DATA|GLOBL)\b`) 109 asmNamedFP = re(`([a-zA-Z0-9_\xFF-\x{10FFFF}]+)(?:\+([0-9]+))\(FP\)`) 110 asmUnnamedFP = re(`[^+\-0-9](([0-9]+)\(FP\))`) 111 asmSP = re(`[^+\-0-9](([0-9]+)\(([A-Z0-9]+)\))`) 112 asmOpcode = re(`^\s*(?:[A-Z0-9a-z_]+:)?\s*([A-Z]+)\s*([^,]*)(?:,\s*(.*))?`) 113 ppc64Suff = re(`([BHWD])(ZU|Z|U|BR)?$`) 114 ) 115 116 func asmCheck(pkg *Package) { 117 if !vet("asmdecl") { 118 return 119 } 120 121 // No work if no assembly files. 122 if !pkg.hasFileWithSuffix(".s") { 123 return 124 } 125 126 // Gather declarations. knownFunc[name][arch] is func description. 127 knownFunc := make(map[string]map[string]*asmFunc) 128 129 for _, f := range pkg.files { 130 if f.file != nil { 131 for _, decl := range f.file.Decls { 132 if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil { 133 knownFunc[decl.Name.Name] = f.asmParseDecl(decl) 134 } 135 } 136 } 137 } 138 139 Files: 140 for _, f := range pkg.files { 141 if !strings.HasSuffix(f.name, ".s") { 142 continue 143 } 144 Println("Checking file", f.name) 145 146 // Determine architecture from file name if possible. 147 var arch string 148 var archDef *asmArch 149 for _, a := range arches { 150 if strings.HasSuffix(f.name, "_"+a.name+".s") { 151 arch = a.name 152 archDef = a 153 break 154 } 155 } 156 157 lines := strings.SplitAfter(string(f.content), "\n") 158 var ( 159 fn *asmFunc 160 fnName string 161 localSize, argSize int 162 wroteSP bool 163 haveRetArg bool 164 retLine []int 165 ) 166 167 flushRet := func() { 168 if fn != nil && fn.vars["ret"] != nil && !haveRetArg && len(retLine) > 0 { 169 v := fn.vars["ret"] 170 for _, line := range retLine { 171 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) 172 } 173 } 174 retLine = nil 175 } 176 for lineno, line := range lines { 177 lineno++ 178 179 badf := func(format string, args ...interface{}) { 180 f.Badf(token.NoPos, "%s:%d: [%s] %s: %s", f.name, lineno, arch, fnName, fmt.Sprintf(format, args...)) 181 } 182 183 if arch == "" { 184 // Determine architecture from +build line if possible. 185 if m := asmPlusBuild.FindStringSubmatch(line); m != nil { 186 // There can be multiple architectures in a single +build line, 187 // so accumulate them all and then prefer the one that 188 // matches build.Default.GOARCH. 189 var archCandidates []*asmArch 190 for _, fld := range strings.Fields(m[1]) { 191 for _, a := range arches { 192 if a.name == fld { 193 archCandidates = append(archCandidates, a) 194 } 195 } 196 } 197 for _, a := range archCandidates { 198 if a.name == build.Default.GOARCH { 199 archCandidates = []*asmArch{a} 200 break 201 } 202 } 203 if len(archCandidates) > 0 { 204 arch = archCandidates[0].name 205 archDef = archCandidates[0] 206 } 207 } 208 } 209 210 if m := asmTEXT.FindStringSubmatch(line); m != nil { 211 flushRet() 212 if arch == "" { 213 // Arch not specified by filename or build tags. 214 // Fall back to build.Default.GOARCH. 215 for _, a := range arches { 216 if a.name == build.Default.GOARCH { 217 arch = a.name 218 archDef = a 219 break 220 } 221 } 222 if arch == "" { 223 f.Warnf(token.NoPos, "%s: cannot determine architecture for assembly file", f.name) 224 continue Files 225 } 226 } 227 fnName = m[2] 228 if pkgName := strings.TrimSpace(m[1]); pkgName != "" { 229 pathParts := strings.Split(pkgName, "∕") 230 pkgName = pathParts[len(pathParts)-1] 231 if pkgName != f.pkg.path { 232 f.Warnf(token.NoPos, "%s:%d: [%s] cannot check cross-package assembly function: %s is in package %s", f.name, lineno, arch, fnName, pkgName) 233 fn = nil 234 fnName = "" 235 continue 236 } 237 } 238 fn = knownFunc[fnName][arch] 239 if fn != nil { 240 size, _ := strconv.Atoi(m[5]) 241 flag := m[3] 242 if size != fn.size && (flag != "7" && !strings.Contains(flag, "NOSPLIT") || size != 0) { 243 badf("wrong argument size %d; expected $...-%d", size, fn.size) 244 } 245 } 246 localSize, _ = strconv.Atoi(m[4]) 247 localSize += archDef.intSize() 248 if archDef.lr { 249 // Account for caller's saved LR 250 localSize += archDef.intSize() 251 } 252 argSize, _ = strconv.Atoi(m[5]) 253 if fn == nil && !strings.Contains(fnName, "<>") { 254 badf("function %s missing Go declaration", fnName) 255 } 256 wroteSP = false 257 haveRetArg = false 258 continue 259 } else if strings.Contains(line, "TEXT") && strings.Contains(line, "SB") { 260 // function, but not visible from Go (didn't match asmTEXT), so stop checking 261 flushRet() 262 fn = nil 263 fnName = "" 264 continue 265 } 266 267 if strings.Contains(line, "RET") { 268 retLine = append(retLine, lineno) 269 } 270 271 if fnName == "" { 272 continue 273 } 274 275 if asmDATA.FindStringSubmatch(line) != nil { 276 fn = nil 277 } 278 279 if archDef == nil { 280 continue 281 } 282 283 if strings.Contains(line, ", "+archDef.stack) || strings.Contains(line, ",\t"+archDef.stack) { 284 wroteSP = true 285 continue 286 } 287 288 for _, m := range asmSP.FindAllStringSubmatch(line, -1) { 289 if m[3] != archDef.stack || wroteSP { 290 continue 291 } 292 off := 0 293 if m[1] != "" { 294 off, _ = strconv.Atoi(m[2]) 295 } 296 if off >= localSize { 297 if fn != nil { 298 v := fn.varByOffset[off-localSize] 299 if v != nil { 300 badf("%s should be %s+%d(FP)", m[1], v.name, off-localSize) 301 continue 302 } 303 } 304 if off >= localSize+argSize { 305 badf("use of %s points beyond argument frame", m[1]) 306 continue 307 } 308 badf("use of %s to access argument frame", m[1]) 309 } 310 } 311 312 if fn == nil { 313 continue 314 } 315 316 for _, m := range asmUnnamedFP.FindAllStringSubmatch(line, -1) { 317 off, _ := strconv.Atoi(m[2]) 318 v := fn.varByOffset[off] 319 if v != nil { 320 badf("use of unnamed argument %s; offset %d is %s+%d(FP)", m[1], off, v.name, v.off) 321 } else { 322 badf("use of unnamed argument %s", m[1]) 323 } 324 } 325 326 for _, m := range asmNamedFP.FindAllStringSubmatch(line, -1) { 327 name := m[1] 328 off := 0 329 if m[2] != "" { 330 off, _ = strconv.Atoi(m[2]) 331 } 332 if name == "ret" || strings.HasPrefix(name, "ret_") { 333 haveRetArg = true 334 } 335 v := fn.vars[name] 336 if v == nil { 337 // Allow argframe+0(FP). 338 if name == "argframe" && off == 0 { 339 continue 340 } 341 v = fn.varByOffset[off] 342 if v != nil { 343 badf("unknown variable %s; offset %d is %s+%d(FP)", name, off, v.name, v.off) 344 } else { 345 badf("unknown variable %s", name) 346 } 347 continue 348 } 349 asmCheckVar(badf, fn, line, m[0], off, v) 350 } 351 } 352 flushRet() 353 } 354 } 355 356 func asmKindForType(t types.Type, size int) asmKind { 357 switch t := t.Underlying().(type) { 358 case *types.Basic: 359 switch t.Kind() { 360 case types.String: 361 return asmString 362 case types.Complex64, types.Complex128: 363 return asmComplex 364 } 365 return asmKind(size) 366 case *types.Pointer, *types.Chan, *types.Map, *types.Signature: 367 return asmKind(size) 368 case *types.Struct: 369 return asmStruct 370 case *types.Interface: 371 if t.Empty() { 372 return asmEmptyInterface 373 } 374 return asmInterface 375 case *types.Array: 376 return asmArray 377 case *types.Slice: 378 return asmSlice 379 } 380 panic("unreachable") 381 } 382 383 // A component is an assembly-addressable component of a composite type, 384 // or a composite type itself. 385 type component struct { 386 size int 387 offset int 388 kind asmKind 389 typ string 390 suffix string // Such as _base for string base, _0_lo for lo half of first element of [1]uint64 on 32 bit machine. 391 outer string // The suffix for immediately containing composite type. 392 } 393 394 func newComponent(suffix string, kind asmKind, typ string, offset, size int, outer string) component { 395 return component{suffix: suffix, kind: kind, typ: typ, offset: offset, size: size, outer: outer} 396 } 397 398 // componentsOfType generates a list of components of type t. 399 // For example, given string, the components are the string itself, the base, and the length. 400 func componentsOfType(arch *asmArch, t types.Type) []component { 401 return appendComponentsRecursive(arch, t, nil, "", 0) 402 } 403 404 // appendComponentsRecursive implements componentsOfType. 405 // Recursion is required to correct handle structs and arrays, 406 // which can contain arbitrary other types. 407 func appendComponentsRecursive(arch *asmArch, t types.Type, cc []component, suffix string, off int) []component { 408 s := t.String() 409 size := int(arch.sizes.Sizeof(t)) 410 kind := asmKindForType(t, size) 411 cc = append(cc, newComponent(suffix, kind, s, off, size, suffix)) 412 413 switch kind { 414 case 8: 415 if arch.ptrSize() == 4 { 416 w1, w2 := "lo", "hi" 417 if arch.bigEndian { 418 w1, w2 = w2, w1 419 } 420 cc = append(cc, newComponent(suffix+"_"+w1, 4, "half "+s, off, 4, suffix)) 421 cc = append(cc, newComponent(suffix+"_"+w2, 4, "half "+s, off+4, 4, suffix)) 422 } 423 424 case asmEmptyInterface: 425 cc = append(cc, newComponent(suffix+"_type", asmKind(arch.ptrSize()), "interface type", off, arch.ptrSize(), suffix)) 426 cc = append(cc, newComponent(suffix+"_data", asmKind(arch.ptrSize()), "interface data", off+arch.ptrSize(), arch.ptrSize(), suffix)) 427 428 case asmInterface: 429 cc = append(cc, newComponent(suffix+"_itable", asmKind(arch.ptrSize()), "interface itable", off, arch.ptrSize(), suffix)) 430 cc = append(cc, newComponent(suffix+"_data", asmKind(arch.ptrSize()), "interface data", off+arch.ptrSize(), arch.ptrSize(), suffix)) 431 432 case asmSlice: 433 cc = append(cc, newComponent(suffix+"_base", asmKind(arch.ptrSize()), "slice base", off, arch.ptrSize(), suffix)) 434 cc = append(cc, newComponent(suffix+"_len", asmKind(arch.intSize()), "slice len", off+arch.ptrSize(), arch.intSize(), suffix)) 435 cc = append(cc, newComponent(suffix+"_cap", asmKind(arch.intSize()), "slice cap", off+arch.ptrSize()+arch.intSize(), arch.intSize(), suffix)) 436 437 case asmString: 438 cc = append(cc, newComponent(suffix+"_base", asmKind(arch.ptrSize()), "string base", off, arch.ptrSize(), suffix)) 439 cc = append(cc, newComponent(suffix+"_len", asmKind(arch.intSize()), "string len", off+arch.ptrSize(), arch.intSize(), suffix)) 440 441 case asmComplex: 442 fsize := size / 2 443 cc = append(cc, newComponent(suffix+"_real", asmKind(fsize), fmt.Sprintf("real(complex%d)", size*8), off, fsize, suffix)) 444 cc = append(cc, newComponent(suffix+"_imag", asmKind(fsize), fmt.Sprintf("imag(complex%d)", size*8), off+fsize, fsize, suffix)) 445 446 case asmStruct: 447 tu := t.Underlying().(*types.Struct) 448 fields := make([]*types.Var, tu.NumFields()) 449 for i := 0; i < tu.NumFields(); i++ { 450 fields[i] = tu.Field(i) 451 } 452 offsets := arch.sizes.Offsetsof(fields) 453 for i, f := range fields { 454 cc = appendComponentsRecursive(arch, f.Type(), cc, suffix+"_"+f.Name(), off+int(offsets[i])) 455 } 456 457 case asmArray: 458 tu := t.Underlying().(*types.Array) 459 elem := tu.Elem() 460 // Calculate offset of each element array. 461 fields := []*types.Var{ 462 types.NewVar(token.NoPos, nil, "fake0", elem), 463 types.NewVar(token.NoPos, nil, "fake1", elem), 464 } 465 offsets := arch.sizes.Offsetsof(fields) 466 elemoff := int(offsets[1]) 467 for i := 0; i < int(tu.Len()); i++ { 468 cc = appendComponentsRecursive(arch, elem, cc, suffix+"_"+strconv.Itoa(i), i*elemoff) 469 } 470 } 471 472 return cc 473 } 474 475 // asmParseDecl parses a function decl for expected assembly variables. 476 func (f *File) asmParseDecl(decl *ast.FuncDecl) map[string]*asmFunc { 477 var ( 478 arch *asmArch 479 fn *asmFunc 480 offset int 481 ) 482 483 // addParams adds asmVars for each of the parameters in list. 484 // isret indicates whether the list are the arguments or the return values. 485 addParams := func(list []*ast.Field, isret bool) { 486 argnum := 0 487 for _, fld := range list { 488 t := f.pkg.types[fld.Type].Type 489 align := int(arch.sizes.Alignof(t)) 490 size := int(arch.sizes.Sizeof(t)) 491 offset += -offset & (align - 1) 492 cc := componentsOfType(arch, t) 493 494 // names is the list of names with this type. 495 names := fld.Names 496 if len(names) == 0 { 497 // Anonymous args will be called arg, arg1, arg2, ... 498 // Similarly so for return values: ret, ret1, ret2, ... 499 name := "arg" 500 if isret { 501 name = "ret" 502 } 503 if argnum > 0 { 504 name += strconv.Itoa(argnum) 505 } 506 names = []*ast.Ident{ast.NewIdent(name)} 507 } 508 argnum += len(names) 509 510 // Create variable for each name. 511 for _, id := range names { 512 name := id.Name 513 for _, c := range cc { 514 outer := name + c.outer 515 v := asmVar{ 516 name: name + c.suffix, 517 kind: c.kind, 518 typ: c.typ, 519 off: offset + c.offset, 520 size: c.size, 521 } 522 if vo := fn.vars[outer]; vo != nil { 523 vo.inner = append(vo.inner, &v) 524 } 525 fn.vars[v.name] = &v 526 for i := 0; i < v.size; i++ { 527 fn.varByOffset[v.off+i] = &v 528 } 529 } 530 offset += size 531 } 532 } 533 } 534 535 m := make(map[string]*asmFunc) 536 for _, arch = range arches { 537 fn = &asmFunc{ 538 arch: arch, 539 vars: make(map[string]*asmVar), 540 varByOffset: make(map[int]*asmVar), 541 } 542 offset = 0 543 addParams(decl.Type.Params.List, false) 544 if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 { 545 offset += -offset & (arch.maxAlign() - 1) 546 addParams(decl.Type.Results.List, true) 547 } 548 fn.size = offset 549 m[arch.name] = fn 550 } 551 552 return m 553 } 554 555 // asmCheckVar checks a single variable reference. 556 func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar) { 557 m := asmOpcode.FindStringSubmatch(line) 558 if m == nil { 559 if !strings.HasPrefix(strings.TrimSpace(line), "//") { 560 badf("cannot find assembly opcode") 561 } 562 return 563 } 564 565 // Determine operand sizes from instruction. 566 // Typically the suffix suffices, but there are exceptions. 567 var src, dst, kind asmKind 568 op := m[1] 569 switch fn.arch.name + "." + op { 570 case "386.FMOVLP": 571 src, dst = 8, 4 572 case "arm.MOVD": 573 src = 8 574 case "arm.MOVW": 575 src = 4 576 case "arm.MOVH", "arm.MOVHU": 577 src = 2 578 case "arm.MOVB", "arm.MOVBU": 579 src = 1 580 // LEA* opcodes don't really read the second arg. 581 // They just take the address of it. 582 case "386.LEAL": 583 dst = 4 584 case "amd64.LEAQ": 585 dst = 8 586 case "amd64p32.LEAL": 587 dst = 4 588 default: 589 switch fn.arch.name { 590 case "386", "amd64": 591 if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "D") || strings.HasSuffix(op, "DP")) { 592 // FMOVDP, FXCHD, etc 593 src = 8 594 break 595 } 596 if strings.HasPrefix(op, "P") && strings.HasSuffix(op, "RD") { 597 // PINSRD, PEXTRD, etc 598 src = 4 599 break 600 } 601 if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "F") || strings.HasSuffix(op, "FP")) { 602 // FMOVFP, FXCHF, etc 603 src = 4 604 break 605 } 606 if strings.HasSuffix(op, "SD") { 607 // MOVSD, SQRTSD, etc 608 src = 8 609 break 610 } 611 if strings.HasSuffix(op, "SS") { 612 // MOVSS, SQRTSS, etc 613 src = 4 614 break 615 } 616 if strings.HasPrefix(op, "SET") { 617 // SETEQ, etc 618 src = 1 619 break 620 } 621 switch op[len(op)-1] { 622 case 'B': 623 src = 1 624 case 'W': 625 src = 2 626 case 'L': 627 src = 4 628 case 'D', 'Q': 629 src = 8 630 } 631 case "ppc64", "ppc64le": 632 // Strip standard suffixes to reveal size letter. 633 m := ppc64Suff.FindStringSubmatch(op) 634 if m != nil { 635 switch m[1][0] { 636 case 'B': 637 src = 1 638 case 'H': 639 src = 2 640 case 'W': 641 src = 4 642 case 'D': 643 src = 8 644 } 645 } 646 case "mips", "mipsle", "mips64", "mips64le": 647 switch op { 648 case "MOVB", "MOVBU": 649 src = 1 650 case "MOVH", "MOVHU": 651 src = 2 652 case "MOVW", "MOVWU", "MOVF": 653 src = 4 654 case "MOVV", "MOVD": 655 src = 8 656 } 657 case "s390x": 658 switch op { 659 case "MOVB", "MOVBZ": 660 src = 1 661 case "MOVH", "MOVHZ": 662 src = 2 663 case "MOVW", "MOVWZ", "FMOVS": 664 src = 4 665 case "MOVD", "FMOVD": 666 src = 8 667 } 668 } 669 } 670 if dst == 0 { 671 dst = src 672 } 673 674 // Determine whether the match we're holding 675 // is the first or second argument. 676 if strings.Index(line, expr) > strings.Index(line, ",") { 677 kind = dst 678 } else { 679 kind = src 680 } 681 682 vk := v.kind 683 vs := v.size 684 vt := v.typ 685 switch vk { 686 case asmInterface, asmEmptyInterface, asmString, asmSlice: 687 // allow reference to first word (pointer) 688 vk = v.inner[0].kind 689 vs = v.inner[0].size 690 vt = v.inner[0].typ 691 } 692 693 if off != v.off { 694 var inner bytes.Buffer 695 for i, vi := range v.inner { 696 if len(v.inner) > 1 { 697 fmt.Fprintf(&inner, ",") 698 } 699 fmt.Fprintf(&inner, " ") 700 if i == len(v.inner)-1 { 701 fmt.Fprintf(&inner, "or ") 702 } 703 fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off) 704 } 705 badf("invalid offset %s; expected %s+%d(FP)%s", expr, v.name, v.off, inner.String()) 706 return 707 } 708 if kind != 0 && kind != vk { 709 var inner bytes.Buffer 710 if len(v.inner) > 0 { 711 fmt.Fprintf(&inner, " containing") 712 for i, vi := range v.inner { 713 if i > 0 && len(v.inner) > 2 { 714 fmt.Fprintf(&inner, ",") 715 } 716 fmt.Fprintf(&inner, " ") 717 if i > 0 && i == len(v.inner)-1 { 718 fmt.Fprintf(&inner, "and ") 719 } 720 fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off) 721 } 722 } 723 badf("invalid %s of %s; %s is %d-byte value%s", op, expr, vt, vs, inner.String()) 724 } 725 }