diff --git a/go/analysis/unitchecker/sizes.go b/go/analysis/unitchecker/sizes.go new file mode 100644 index 00000000000..332e2e49a9b --- /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 00000000000..5d98a23338c --- /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 ff22d23ce5c..8334cf06301 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -218,7 +218,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re }) 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),