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),