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