1
2
3
4
5
6
7 package asmdecl
8
9 import (
10 "bytes"
11 "fmt"
12 "go/ast"
13 "go/build"
14 "go/token"
15 "go/types"
16 "log"
17 "regexp"
18 "strconv"
19 "strings"
20
21 "golang.org/x/tools/go/analysis"
22 "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
23 )
24
25 const Doc = "report mismatches between assembly files and Go declarations"
26
27 var Analyzer = &analysis.Analyzer{
28 Name: "asmdecl",
29 Doc: Doc,
30 Run: run,
31 }
32
33
34
35 type asmKind int
36
37
38 const (
39 asmString asmKind = 100 + iota
40 asmSlice
41 asmArray
42 asmInterface
43 asmEmptyInterface
44 asmStruct
45 asmComplex
46 )
47
48
49 type asmArch struct {
50 name string
51 bigEndian bool
52 stack string
53 lr bool
54
55
56
57
58 retRegs []string
59
60 sizes types.Sizes
61 intSize int
62 ptrSize int
63 maxAlign int
64 }
65
66
67 type asmFunc struct {
68 arch *asmArch
69 size int
70 vars map[string]*asmVar
71 varByOffset map[int]*asmVar
72 }
73
74
75 type asmVar struct {
76 name string
77 kind asmKind
78 typ string
79 off int
80 size int
81 inner []*asmVar
82 }
83
84 var (
85 asmArch386 = asmArch{name: "386", bigEndian: false, stack: "SP", lr: false}
86 asmArchArm = asmArch{name: "arm", bigEndian: false, stack: "R13", lr: true}
87 asmArchArm64 = asmArch{name: "arm64", bigEndian: false, stack: "RSP", lr: true, retRegs: []string{"R0", "F0"}}
88 asmArchAmd64 = asmArch{name: "amd64", bigEndian: false, stack: "SP", lr: false, retRegs: []string{"AX", "X0"}}
89 asmArchMips = asmArch{name: "mips", bigEndian: true, stack: "R29", lr: true}
90 asmArchMipsLE = asmArch{name: "mipsle", bigEndian: false, stack: "R29", lr: true}
91 asmArchMips64 = asmArch{name: "mips64", bigEndian: true, stack: "R29", lr: true}
92 asmArchMips64LE = asmArch{name: "mips64le", bigEndian: false, stack: "R29", lr: true}
93 asmArchPpc64 = asmArch{name: "ppc64", bigEndian: true, stack: "R1", lr: true}
94 asmArchPpc64LE = asmArch{name: "ppc64le", bigEndian: false, stack: "R1", lr: true}
95 asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true}
96 asmArchS390X = asmArch{name: "s390x", bigEndian: true, stack: "R15", lr: true}
97 asmArchWasm = asmArch{name: "wasm", bigEndian: false, stack: "SP", lr: false}
98
99 arches = []*asmArch{
100 &asmArch386,
101 &asmArchArm,
102 &asmArchArm64,
103 &asmArchAmd64,
104 &asmArchMips,
105 &asmArchMipsLE,
106 &asmArchMips64,
107 &asmArchMips64LE,
108 &asmArchPpc64,
109 &asmArchPpc64LE,
110 &asmArchRISCV64,
111 &asmArchS390X,
112 &asmArchWasm,
113 }
114 )
115
116 func init() {
117 for _, arch := range arches {
118 arch.sizes = types.SizesFor("gc", arch.name)
119 if arch.sizes == nil {
120
121
122
123
124
125
126 arch.sizes = types.SizesFor("gc", "amd64")
127 log.Printf("unknown architecture %s", arch.name)
128 }
129 arch.intSize = int(arch.sizes.Sizeof(types.Typ[types.Int]))
130 arch.ptrSize = int(arch.sizes.Sizeof(types.Typ[types.UnsafePointer]))
131 arch.maxAlign = int(arch.sizes.Alignof(types.Typ[types.Int64]))
132 }
133 }
134
135 var (
136 re = regexp.MustCompile
137 asmPlusBuild = re(`//\s+\+build\s+([^\n]+)`)
138 asmTEXT = re(`\bTEXT\b(.*)·([^\(]+)\(SB\)(?:\s*,\s*([0-9A-Z|+()]+))?(?:\s*,\s*\$(-?[0-9]+)(?:-([0-9]+))?)?`)
139 asmDATA = re(`\b(DATA|GLOBL)\b`)
140 asmNamedFP = re(`\$?([a-zA-Z0-9_\xFF-\x{10FFFF}]+)(?:\+([0-9]+))\(FP\)`)
141 asmUnnamedFP = re(`[^+\-0-9](([0-9]+)\(FP\))`)
142 asmSP = re(`[^+\-0-9](([0-9]+)\(([A-Z0-9]+)\))`)
143 asmOpcode = re(`^\s*(?:[A-Z0-9a-z_]+:)?\s*([A-Z]+)\s*([^,]*)(?:,\s*(.*))?`)
144 ppc64Suff = re(`([BHWD])(ZU|Z|U|BR)?$`)
145 abiSuff = re(`^(.+)<(ABI.+)>$`)
146 )
147
148 func run(pass *analysis.Pass) (interface{}, error) {
149
150 var sfiles []string
151 for _, fname := range pass.OtherFiles {
152 if strings.HasSuffix(fname, ".s") {
153 sfiles = append(sfiles, fname)
154 }
155 }
156 if sfiles == nil {
157 return nil, nil
158 }
159
160
161 knownFunc := make(map[string]map[string]*asmFunc)
162
163 for _, f := range pass.Files {
164 for _, decl := range f.Decls {
165 if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body == nil {
166 knownFunc[decl.Name.Name] = asmParseDecl(pass, decl)
167 }
168 }
169 }
170
171 Files:
172 for _, fname := range sfiles {
173 content, tf, err := analysisutil.ReadFile(pass.Fset, fname)
174 if err != nil {
175 return nil, err
176 }
177
178
179 var arch string
180 var archDef *asmArch
181 for _, a := range arches {
182 if strings.HasSuffix(fname, "_"+a.name+".s") {
183 arch = a.name
184 archDef = a
185 break
186 }
187 }
188
189 lines := strings.SplitAfter(string(content), "\n")
190 var (
191 fn *asmFunc
192 fnName string
193 abi string
194 localSize, argSize int
195 wroteSP bool
196 noframe bool
197 haveRetArg bool
198 retLine []int
199 )
200
201 flushRet := func() {
202 if fn != nil && fn.vars["ret"] != nil && !haveRetArg && len(retLine) > 0 {
203 v := fn.vars["ret"]
204 resultStr := fmt.Sprintf("%d-byte ret+%d(FP)", v.size, v.off)
205 if abi == "ABIInternal" {
206 resultStr = "result register"
207 }
208 for _, line := range retLine {
209 pass.Reportf(analysisutil.LineStart(tf, line), "[%s] %s: RET without writing to %s", arch, fnName, resultStr)
210 }
211 }
212 retLine = nil
213 }
214 trimABI := func(fnName string) (string, string) {
215 m := abiSuff.FindStringSubmatch(fnName)
216 if m != nil {
217 return m[1], m[2]
218 }
219 return fnName, ""
220 }
221 for lineno, line := range lines {
222 lineno++
223
224 badf := func(format string, args ...interface{}) {
225 pass.Reportf(analysisutil.LineStart(tf, lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...))
226 }
227
228 if arch == "" {
229
230 if m := asmPlusBuild.FindStringSubmatch(line); m != nil {
231
232
233
234 var archCandidates []*asmArch
235 for _, fld := range strings.Fields(m[1]) {
236 for _, a := range arches {
237 if a.name == fld {
238 archCandidates = append(archCandidates, a)
239 }
240 }
241 }
242 for _, a := range archCandidates {
243 if a.name == build.Default.GOARCH {
244 archCandidates = []*asmArch{a}
245 break
246 }
247 }
248 if len(archCandidates) > 0 {
249 arch = archCandidates[0].name
250 archDef = archCandidates[0]
251 }
252 }
253 }
254
255
256 if i := strings.Index(line, "//"); i >= 0 {
257 line = line[:i]
258 }
259
260 if m := asmTEXT.FindStringSubmatch(line); m != nil {
261 flushRet()
262 if arch == "" {
263
264
265 for _, a := range arches {
266 if a.name == build.Default.GOARCH {
267 arch = a.name
268 archDef = a
269 break
270 }
271 }
272 if arch == "" {
273 log.Printf("%s: cannot determine architecture for assembly file", fname)
274 continue Files
275 }
276 }
277 fnName = m[2]
278 if pkgPath := strings.TrimSpace(m[1]); pkgPath != "" {
279
280
281 pkgPath = strings.Replace(pkgPath, "∕", "/", -1)
282 if pkgPath != pass.Pkg.Path() {
283
284 fn = nil
285 fnName = ""
286 abi = ""
287 continue
288 }
289 }
290
291 fnName, abi = trimABI(fnName)
292 flag := m[3]
293 fn = knownFunc[fnName][arch]
294 if fn != nil {
295 size, _ := strconv.Atoi(m[5])
296 if size != fn.size && (flag != "7" && !strings.Contains(flag, "NOSPLIT") || size != 0) {
297 badf("wrong argument size %d; expected $...-%d", size, fn.size)
298 }
299 }
300 localSize, _ = strconv.Atoi(m[4])
301 localSize += archDef.intSize
302 if archDef.lr && !strings.Contains(flag, "NOFRAME") {
303
304 localSize += archDef.intSize
305 }
306 argSize, _ = strconv.Atoi(m[5])
307 noframe = strings.Contains(flag, "NOFRAME")
308 if fn == nil && !strings.Contains(fnName, "<>") && !noframe {
309 badf("function %s missing Go declaration", fnName)
310 }
311 wroteSP = false
312 haveRetArg = false
313 continue
314 } else if strings.Contains(line, "TEXT") && strings.Contains(line, "SB") {
315
316 flushRet()
317 fn = nil
318 fnName = ""
319 abi = ""
320 continue
321 }
322
323 if strings.Contains(line, "RET") && !strings.Contains(line, "(SB)") {
324
325 retLine = append(retLine, lineno)
326 }
327
328 if fnName == "" {
329 continue
330 }
331
332 if asmDATA.FindStringSubmatch(line) != nil {
333 fn = nil
334 }
335
336 if archDef == nil {
337 continue
338 }
339
340 if strings.Contains(line, ", "+archDef.stack) || strings.Contains(line, ",\t"+archDef.stack) || strings.Contains(line, "NOP "+archDef.stack) || strings.Contains(line, "NOP\t"+archDef.stack) {
341 wroteSP = true
342 continue
343 }
344
345 if arch == "wasm" && strings.Contains(line, "CallImport") {
346
347 haveRetArg = true
348 }
349
350 if abi == "ABIInternal" && !haveRetArg {
351 for _, reg := range archDef.retRegs {
352 if strings.Contains(line, reg) {
353 haveRetArg = true
354 break
355 }
356 }
357 }
358
359 for _, m := range asmSP.FindAllStringSubmatch(line, -1) {
360 if m[3] != archDef.stack || wroteSP || noframe {
361 continue
362 }
363 off := 0
364 if m[1] != "" {
365 off, _ = strconv.Atoi(m[2])
366 }
367 if off >= localSize {
368 if fn != nil {
369 v := fn.varByOffset[off-localSize]
370 if v != nil {
371 badf("%s should be %s+%d(FP)", m[1], v.name, off-localSize)
372 continue
373 }
374 }
375 if off >= localSize+argSize {
376 badf("use of %s points beyond argument frame", m[1])
377 continue
378 }
379 badf("use of %s to access argument frame", m[1])
380 }
381 }
382
383 if fn == nil {
384 continue
385 }
386
387 for _, m := range asmUnnamedFP.FindAllStringSubmatch(line, -1) {
388 off, _ := strconv.Atoi(m[2])
389 v := fn.varByOffset[off]
390 if v != nil {
391 badf("use of unnamed argument %s; offset %d is %s+%d(FP)", m[1], off, v.name, v.off)
392 } else {
393 badf("use of unnamed argument %s", m[1])
394 }
395 }
396
397 for _, m := range asmNamedFP.FindAllStringSubmatch(line, -1) {
398 name := m[1]
399 off := 0
400 if m[2] != "" {
401 off, _ = strconv.Atoi(m[2])
402 }
403 if name == "ret" || strings.HasPrefix(name, "ret_") {
404 haveRetArg = true
405 }
406 v := fn.vars[name]
407 if v == nil {
408
409 if name == "argframe" && off == 0 {
410 continue
411 }
412 v = fn.varByOffset[off]
413 if v != nil {
414 badf("unknown variable %s; offset %d is %s+%d(FP)", name, off, v.name, v.off)
415 } else {
416 badf("unknown variable %s", name)
417 }
418 continue
419 }
420 asmCheckVar(badf, fn, line, m[0], off, v, archDef)
421 }
422 }
423 flushRet()
424 }
425 return nil, nil
426 }
427
428 func asmKindForType(t types.Type, size int) asmKind {
429 switch t := t.Underlying().(type) {
430 case *types.Basic:
431 switch t.Kind() {
432 case types.String:
433 return asmString
434 case types.Complex64, types.Complex128:
435 return asmComplex
436 }
437 return asmKind(size)
438 case *types.Pointer, *types.Chan, *types.Map, *types.Signature:
439 return asmKind(size)
440 case *types.Struct:
441 return asmStruct
442 case *types.Interface:
443 if t.Empty() {
444 return asmEmptyInterface
445 }
446 return asmInterface
447 case *types.Array:
448 return asmArray
449 case *types.Slice:
450 return asmSlice
451 }
452 panic("unreachable")
453 }
454
455
456
457 type component struct {
458 size int
459 offset int
460 kind asmKind
461 typ string
462 suffix string
463 outer string
464 }
465
466 func newComponent(suffix string, kind asmKind, typ string, offset, size int, outer string) component {
467 return component{suffix: suffix, kind: kind, typ: typ, offset: offset, size: size, outer: outer}
468 }
469
470
471
472 func componentsOfType(arch *asmArch, t types.Type) []component {
473 return appendComponentsRecursive(arch, t, nil, "", 0)
474 }
475
476
477
478
479 func appendComponentsRecursive(arch *asmArch, t types.Type, cc []component, suffix string, off int) []component {
480 s := t.String()
481 size := int(arch.sizes.Sizeof(t))
482 kind := asmKindForType(t, size)
483 cc = append(cc, newComponent(suffix, kind, s, off, size, suffix))
484
485 switch kind {
486 case 8:
487 if arch.ptrSize == 4 {
488 w1, w2 := "lo", "hi"
489 if arch.bigEndian {
490 w1, w2 = w2, w1
491 }
492 cc = append(cc, newComponent(suffix+"_"+w1, 4, "half "+s, off, 4, suffix))
493 cc = append(cc, newComponent(suffix+"_"+w2, 4, "half "+s, off+4, 4, suffix))
494 }
495
496 case asmEmptyInterface:
497 cc = append(cc, newComponent(suffix+"_type", asmKind(arch.ptrSize), "interface type", off, arch.ptrSize, suffix))
498 cc = append(cc, newComponent(suffix+"_data", asmKind(arch.ptrSize), "interface data", off+arch.ptrSize, arch.ptrSize, suffix))
499
500 case asmInterface:
501 cc = append(cc, newComponent(suffix+"_itable", asmKind(arch.ptrSize), "interface itable", off, arch.ptrSize, suffix))
502 cc = append(cc, newComponent(suffix+"_data", asmKind(arch.ptrSize), "interface data", off+arch.ptrSize, arch.ptrSize, suffix))
503
504 case asmSlice:
505 cc = append(cc, newComponent(suffix+"_base", asmKind(arch.ptrSize), "slice base", off, arch.ptrSize, suffix))
506 cc = append(cc, newComponent(suffix+"_len", asmKind(arch.intSize), "slice len", off+arch.ptrSize, arch.intSize, suffix))
507 cc = append(cc, newComponent(suffix+"_cap", asmKind(arch.intSize), "slice cap", off+arch.ptrSize+arch.intSize, arch.intSize, suffix))
508
509 case asmString:
510 cc = append(cc, newComponent(suffix+"_base", asmKind(arch.ptrSize), "string base", off, arch.ptrSize, suffix))
511 cc = append(cc, newComponent(suffix+"_len", asmKind(arch.intSize), "string len", off+arch.ptrSize, arch.intSize, suffix))
512
513 case asmComplex:
514 fsize := size / 2
515 cc = append(cc, newComponent(suffix+"_real", asmKind(fsize), fmt.Sprintf("real(complex%d)", size*8), off, fsize, suffix))
516 cc = append(cc, newComponent(suffix+"_imag", asmKind(fsize), fmt.Sprintf("imag(complex%d)", size*8), off+fsize, fsize, suffix))
517
518 case asmStruct:
519 tu := t.Underlying().(*types.Struct)
520 fields := make([]*types.Var, tu.NumFields())
521 for i := 0; i < tu.NumFields(); i++ {
522 fields[i] = tu.Field(i)
523 }
524 offsets := arch.sizes.Offsetsof(fields)
525 for i, f := range fields {
526 cc = appendComponentsRecursive(arch, f.Type(), cc, suffix+"_"+f.Name(), off+int(offsets[i]))
527 }
528
529 case asmArray:
530 tu := t.Underlying().(*types.Array)
531 elem := tu.Elem()
532
533 fields := []*types.Var{
534 types.NewVar(token.NoPos, nil, "fake0", elem),
535 types.NewVar(token.NoPos, nil, "fake1", elem),
536 }
537 offsets := arch.sizes.Offsetsof(fields)
538 elemoff := int(offsets[1])
539 for i := 0; i < int(tu.Len()); i++ {
540 cc = appendComponentsRecursive(arch, elem, cc, suffix+"_"+strconv.Itoa(i), off+i*elemoff)
541 }
542 }
543
544 return cc
545 }
546
547
548 func asmParseDecl(pass *analysis.Pass, decl *ast.FuncDecl) map[string]*asmFunc {
549 var (
550 arch *asmArch
551 fn *asmFunc
552 offset int
553 )
554
555
556
557
558
559 addParams := func(list []*ast.Field, isret bool) {
560 argnum := 0
561 for _, fld := range list {
562 t := pass.TypesInfo.Types[fld.Type].Type
563
564
565 if t == nil {
566 if ell, ok := fld.Type.(*ast.Ellipsis); ok {
567 t = types.NewSlice(pass.TypesInfo.Types[ell.Elt].Type)
568 }
569 }
570
571 align := int(arch.sizes.Alignof(t))
572 size := int(arch.sizes.Sizeof(t))
573 offset += -offset & (align - 1)
574 cc := componentsOfType(arch, t)
575
576
577 names := fld.Names
578 if len(names) == 0 {
579
580
581 name := "arg"
582 if isret {
583 name = "ret"
584 }
585 if argnum > 0 {
586 name += strconv.Itoa(argnum)
587 }
588 names = []*ast.Ident{ast.NewIdent(name)}
589 }
590 argnum += len(names)
591
592
593 for _, id := range names {
594 name := id.Name
595 for _, c := range cc {
596 outer := name + c.outer
597 v := asmVar{
598 name: name + c.suffix,
599 kind: c.kind,
600 typ: c.typ,
601 off: offset + c.offset,
602 size: c.size,
603 }
604 if vo := fn.vars[outer]; vo != nil {
605 vo.inner = append(vo.inner, &v)
606 }
607 fn.vars[v.name] = &v
608 for i := 0; i < v.size; i++ {
609 fn.varByOffset[v.off+i] = &v
610 }
611 }
612 offset += size
613 }
614 }
615 }
616
617 m := make(map[string]*asmFunc)
618 for _, arch = range arches {
619 fn = &asmFunc{
620 arch: arch,
621 vars: make(map[string]*asmVar),
622 varByOffset: make(map[int]*asmVar),
623 }
624 offset = 0
625 addParams(decl.Type.Params.List, false)
626 if decl.Type.Results != nil && len(decl.Type.Results.List) > 0 {
627 offset += -offset & (arch.maxAlign - 1)
628 addParams(decl.Type.Results.List, true)
629 }
630 fn.size = offset
631 m[arch.name] = fn
632 }
633
634 return m
635 }
636
637
638 func asmCheckVar(badf func(string, ...interface{}), fn *asmFunc, line, expr string, off int, v *asmVar, archDef *asmArch) {
639 m := asmOpcode.FindStringSubmatch(line)
640 if m == nil {
641 if !strings.HasPrefix(strings.TrimSpace(line), "//") {
642 badf("cannot find assembly opcode")
643 }
644 return
645 }
646
647 addr := strings.HasPrefix(expr, "$")
648
649
650
651 var src, dst, kind asmKind
652 op := m[1]
653 switch fn.arch.name + "." + op {
654 case "386.FMOVLP":
655 src, dst = 8, 4
656 case "arm.MOVD":
657 src = 8
658 case "arm.MOVW":
659 src = 4
660 case "arm.MOVH", "arm.MOVHU":
661 src = 2
662 case "arm.MOVB", "arm.MOVBU":
663 src = 1
664
665
666 case "386.LEAL":
667 dst = 4
668 addr = true
669 case "amd64.LEAQ":
670 dst = 8
671 addr = true
672 default:
673 switch fn.arch.name {
674 case "386", "amd64":
675 if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "D") || strings.HasSuffix(op, "DP")) {
676
677 src = 8
678 break
679 }
680 if strings.HasPrefix(op, "P") && strings.HasSuffix(op, "RD") {
681
682 src = 4
683 break
684 }
685 if strings.HasPrefix(op, "F") && (strings.HasSuffix(op, "F") || strings.HasSuffix(op, "FP")) {
686
687 src = 4
688 break
689 }
690 if strings.HasSuffix(op, "SD") {
691
692 src = 8
693 break
694 }
695 if strings.HasSuffix(op, "SS") {
696
697 src = 4
698 break
699 }
700 if op == "MOVO" || op == "MOVOU" {
701 src = 16
702 break
703 }
704 if strings.HasPrefix(op, "SET") {
705
706 src = 1
707 break
708 }
709 switch op[len(op)-1] {
710 case 'B':
711 src = 1
712 case 'W':
713 src = 2
714 case 'L':
715 src = 4
716 case 'D', 'Q':
717 src = 8
718 }
719 case "ppc64", "ppc64le":
720
721 m := ppc64Suff.FindStringSubmatch(op)
722 if m != nil {
723 switch m[1][0] {
724 case 'B':
725 src = 1
726 case 'H':
727 src = 2
728 case 'W':
729 src = 4
730 case 'D':
731 src = 8
732 }
733 }
734 case "mips", "mipsle", "mips64", "mips64le":
735 switch op {
736 case "MOVB", "MOVBU":
737 src = 1
738 case "MOVH", "MOVHU":
739 src = 2
740 case "MOVW", "MOVWU", "MOVF":
741 src = 4
742 case "MOVV", "MOVD":
743 src = 8
744 }
745 case "s390x":
746 switch op {
747 case "MOVB", "MOVBZ":
748 src = 1
749 case "MOVH", "MOVHZ":
750 src = 2
751 case "MOVW", "MOVWZ", "FMOVS":
752 src = 4
753 case "MOVD", "FMOVD":
754 src = 8
755 }
756 }
757 }
758 if dst == 0 {
759 dst = src
760 }
761
762
763
764 if strings.Index(line, expr) > strings.Index(line, ",") {
765 kind = dst
766 } else {
767 kind = src
768 }
769
770 vk := v.kind
771 vs := v.size
772 vt := v.typ
773 switch vk {
774 case asmInterface, asmEmptyInterface, asmString, asmSlice:
775
776 vk = v.inner[0].kind
777 vs = v.inner[0].size
778 vt = v.inner[0].typ
779 case asmComplex:
780
781 if int(kind) == vs {
782 kind = asmComplex
783 }
784 }
785 if addr {
786 vk = asmKind(archDef.ptrSize)
787 vs = archDef.ptrSize
788 vt = "address"
789 }
790
791 if off != v.off {
792 var inner bytes.Buffer
793 for i, vi := range v.inner {
794 if len(v.inner) > 1 {
795 fmt.Fprintf(&inner, ",")
796 }
797 fmt.Fprintf(&inner, " ")
798 if i == len(v.inner)-1 {
799 fmt.Fprintf(&inner, "or ")
800 }
801 fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
802 }
803 badf("invalid offset %s; expected %s+%d(FP)%s", expr, v.name, v.off, inner.String())
804 return
805 }
806 if kind != 0 && kind != vk {
807 var inner bytes.Buffer
808 if len(v.inner) > 0 {
809 fmt.Fprintf(&inner, " containing")
810 for i, vi := range v.inner {
811 if i > 0 && len(v.inner) > 2 {
812 fmt.Fprintf(&inner, ",")
813 }
814 fmt.Fprintf(&inner, " ")
815 if i > 0 && i == len(v.inner)-1 {
816 fmt.Fprintf(&inner, "and ")
817 }
818 fmt.Fprintf(&inner, "%s+%d(FP)", vi.name, vi.off)
819 }
820 }
821 badf("invalid %s of %s; %s is %d-byte value%s", op, expr, vt, vs, inner.String())
822 }
823 }
824
View as plain text