go/analysis: add Sizes that matches gc size computations
For golang/go#60431
For golang/go#60734
Change-Id: I6a15a24e3e121635b33d77fde9170a41514c0db1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/501495
Run-TryBot: Cuong Manh Le <cuong.manhle.vn@gmail.com>
Reviewed-by: Damien Neil <dneil@google.com>
Auto-Submit: Cuong Manh Le <cuong.manhle.vn@gmail.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Robert Griesemer <gri@google.com>
Auto-Submit: Robert Griesemer <gri@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Griesemer <gri@google.com>
diff --git a/go/analysis/unitchecker/sizes.go b/go/analysis/unitchecker/sizes.go
new file mode 100644
index 0000000..332e2e4
--- /dev/null
+++ b/go/analysis/unitchecker/sizes.go
@@ -0,0 +1,237 @@
+// Copyright 2023 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.
+
+package unitchecker
+
+import (
+ "fmt"
+ "go/types"
+)
+
+type gcSizes struct {
+ WordSize int64 // word size in bytes - must be >= 4 (32bits)
+ MaxAlign int64 // maximum alignment in bytes - must be >= 1
+}
+
+func (s *gcSizes) Alignof(T types.Type) int64 {
+ // For arrays and structs, alignment is defined in terms
+ // of alignment of the elements and fields, respectively.
+ switch t := T.Underlying().(type) {
+ case *types.Array:
+ // spec: "For a variable x of array type: unsafe.Alignof(x)
+ // is the same as unsafe.Alignof(x[0]), but at least 1."
+ return s.Alignof(t.Elem())
+ case *types.Struct:
+ if t.NumFields() == 0 && isSyncAtomicAlign64(T) {
+ // Special case: sync/atomic.align64 is an
+ // empty struct we recognize as a signal that
+ // the struct it contains must be
+ // 64-bit-aligned.
+ //
+ // This logic is equivalent to the logic in
+ // cmd/compile/internal/types/size.go:calcStructOffset
+ return 8
+ }
+
+ // spec: "For a variable x of struct type: unsafe.Alignof(x)
+ // is the largest of the values unsafe.Alignof(x.f) for each
+ // field f of x, but at least 1."
+ max := int64(1)
+ for i, nf := 0, t.NumFields(); i < nf; i++ {
+ if a := s.Alignof(t.Field(i).Type()); a > max {
+ max = a
+ }
+ }
+ return max
+ case *types.Slice, *types.Interface:
+ // Multiword data structures are effectively structs
+ // in which each element has size PtrSize.
+ return s.WordSize
+ case *types.Basic:
+ // Strings are like slices and interfaces.
+ if t.Info()&types.IsString != 0 {
+ return s.WordSize
+ }
+ }
+ a := s.Sizeof(T) // may be 0
+ // spec: "For a variable x of any type: unsafe.Alignof(x) is at least 1."
+ if a < 1 {
+ return 1
+ }
+ // complex{64,128} are aligned like [2]float{32,64}.
+ if isComplex(T) {
+ a /= 2
+ }
+ if a > s.MaxAlign {
+ return s.MaxAlign
+ }
+ return a
+}
+
+func isComplex(T types.Type) bool {
+ basic, ok := T.Underlying().(*types.Basic)
+ return ok && basic.Info()&types.IsComplex != 0
+}
+
+func (s *gcSizes) Offsetsof(fields []*types.Var) []int64 {
+ offsets := make([]int64, len(fields))
+ var offs int64
+ for i, f := range fields {
+ if offs < 0 {
+ // all remaining offsets are too large
+ offsets[i] = -1
+ continue
+ }
+ // offs >= 0
+ typ := f.Type()
+ a := s.Alignof(typ)
+ offs = roundUp(offs, a) // possibly < 0 if align overflows
+ offsets[i] = offs
+ if d := s.Sizeof(typ); d >= 0 && offs >= 0 {
+ offs += d // ok to overflow to < 0
+ } else {
+ offs = -1
+ }
+ }
+ return offsets
+}
+
+func (s *gcSizes) Sizeof(T types.Type) int64 {
+ switch t := T.Underlying().(type) {
+ case *types.Basic:
+ k := t.Kind()
+ if int(k) < len(basicSizes) {
+ if s := basicSizes[k]; s > 0 {
+ return int64(s)
+ }
+ }
+ switch k {
+ case types.String:
+ return s.WordSize * 2
+ case types.Int, types.Uint, types.Uintptr, types.UnsafePointer:
+ return s.WordSize
+ }
+ panic(fmt.Sprintf("unimplemented basic: %v (kind %v)", T, k))
+ case *types.Array:
+ n := t.Len()
+ if n <= 0 {
+ return 0
+ }
+ // n > 0
+ // gc: Size includes alignment padding.
+ esize := s.Sizeof(t.Elem())
+ if esize < 0 {
+ return -1 // array element too large
+ }
+ if esize == 0 {
+ return 0 // 0-size element
+ }
+ // esize > 0
+ // Final size is esize * n; and size must be <= maxInt64.
+ const maxInt64 = 1<<63 - 1
+ if esize > maxInt64/n {
+ return -1 // esize * n overflows
+ }
+ return esize * n
+ case *types.Slice:
+ return s.WordSize * 3
+ case *types.Struct:
+ n := t.NumFields()
+ if n == 0 {
+ return 0
+ }
+ // n > 0
+ fields := make([]*types.Var, n)
+ for i := range fields {
+ fields[i] = t.Field(i)
+ }
+ offsets := s.Offsetsof(fields)
+ // gc: The last field of a non-zero-sized struct is not allowed to
+ // have size 0.
+ last := s.Sizeof(fields[n-1].Type())
+ if last == 0 && offsets[n-1] > 0 {
+ last = 1
+ }
+ // gc: Size includes alignment padding.
+ return roundUp(offsets[n-1]+last, s.Alignof(t)) // may overflow to < 0 which is ok
+ case *types.Interface:
+ return s.WordSize * 2
+ case *types.Chan, *types.Map, *types.Pointer, *types.Signature:
+ return s.WordSize
+ default:
+ panic(fmt.Sprintf("Sizeof(%T) unimplemented", t))
+ }
+}
+
+func isSyncAtomicAlign64(T types.Type) bool {
+ named, ok := T.(*types.Named)
+ if !ok {
+ return false
+ }
+ obj := named.Obj()
+ return obj.Name() == "align64" &&
+ obj.Pkg() != nil &&
+ (obj.Pkg().Path() == "sync/atomic" ||
+ obj.Pkg().Path() == "runtime/internal/atomic")
+}
+
+// roundUp rounds o to a multiple of r, r is a power of 2.
+func roundUp(o int64, r int64) int64 {
+ if r < 1 || r > 8 || r&(r-1) != 0 {
+ panic(fmt.Sprintf("Round %d", r))
+ }
+ return (o + r - 1) &^ (r - 1)
+}
+
+var basicSizes = [...]byte{
+ types.Invalid: 1,
+ types.Bool: 1,
+ types.Int8: 1,
+ types.Int16: 2,
+ types.Int32: 4,
+ types.Int64: 8,
+ types.Uint8: 1,
+ types.Uint16: 2,
+ types.Uint32: 4,
+ types.Uint64: 8,
+ types.Float32: 4,
+ types.Float64: 8,
+ types.Complex64: 8,
+ types.Complex128: 16,
+}
+
+// common architecture word sizes and alignments
+var gcArchSizes = map[string]*gcSizes{
+ "386": {4, 4},
+ "amd64": {8, 8},
+ "amd64p32": {4, 8},
+ "arm": {4, 4},
+ "arm64": {8, 8},
+ "loong64": {8, 8},
+ "mips": {4, 4},
+ "mipsle": {4, 4},
+ "mips64": {8, 8},
+ "mips64le": {8, 8},
+ "ppc64": {8, 8},
+ "ppc64le": {8, 8},
+ "riscv64": {8, 8},
+ "s390x": {8, 8},
+ "sparc64": {8, 8},
+ "wasm": {8, 8},
+ // When adding more architectures here,
+ // update the doc string of sizesFor below.
+}
+
+// sizesFor returns the go/types.Sizes used by a compiler for an architecture.
+// The result is nil if a compiler/architecture pair is not known.
+func sizesFor(compiler, arch string) types.Sizes {
+ if compiler != "gc" {
+ return nil
+ }
+ s, ok := gcArchSizes[arch]
+ if !ok {
+ return nil
+ }
+ return s
+}
diff --git a/go/analysis/unitchecker/sizes_test.go b/go/analysis/unitchecker/sizes_test.go
new file mode 100644
index 0000000..5d98a23
--- /dev/null
+++ b/go/analysis/unitchecker/sizes_test.go
@@ -0,0 +1,85 @@
+// Copyright 2023 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.
+
+package unitchecker
+
+import (
+ "go/ast"
+ "go/importer"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "testing"
+)
+
+type gcSizeTest struct {
+ name string
+ src string
+}
+
+var gcSizesTests = []gcSizeTest{
+ {
+ "issue60431",
+ `package main
+
+ import "unsafe"
+
+ // The foo struct size is expected to be rounded up to 16 bytes.
+ type foo struct {
+ a int64
+ b bool
+ }
+
+ func main() {
+ var _ [unsafe.Sizeof(foo{}) - 16]byte
+ println(unsafe.Sizeof(foo{}))
+ }`,
+ },
+ {
+ "issue60734",
+ `package main
+
+ import (
+ "unsafe"
+ )
+
+ // The Data struct size is expected to be rounded up to 16 bytes.
+ type Data struct {
+ Value uint32 // 4 bytes
+ Label [10]byte // 10 bytes
+ Active bool // 1 byte
+ // padded with 1 byte to make it align
+ }
+
+ const (
+ dataSize = unsafe.Sizeof(Data{})
+ dataSizeLiteral = 16
+ )
+
+ func main() {
+ _ = [16]byte{0, 132, 95, 237, 80, 104, 111, 110, 101, 0, 0, 0, 0, 0, 1, 0}
+ _ = [dataSize]byte{0, 132, 95, 237, 80, 104, 111, 110, 101, 0, 0, 0, 0, 0, 1, 0}
+ _ = [dataSizeLiteral]byte{0, 132, 95, 237, 80, 104, 111, 110, 101, 0, 0, 0, 0, 0, 1, 0}
+ }`,
+ },
+}
+
+func TestGCSizes(t *testing.T) {
+ for _, tc := range gcSizesTests {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, "x.go", tc.src, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ conf := types.Config{Importer: importer.Default(), Sizes: sizesFor("gc", "amd64")}
+ if _, err := conf.Check("main.go", fset, []*ast.File{f}, nil); err != nil {
+ t.Fatal(err) // type error
+ }
+ })
+ }
+}
diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go
index ff22d23..8334cf0 100644
--- a/go/analysis/unitchecker/unitchecker.go
+++ b/go/analysis/unitchecker/unitchecker.go
@@ -218,7 +218,7 @@
})
tc := &types.Config{
Importer: importer,
- Sizes: types.SizesFor("gc", build.Default.GOARCH), // assume gccgo ≡ gc?
+ Sizes: sizesFor("gc", build.Default.GOARCH), // assume gccgo ≡ gc?
}
info := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),