Skip to content

Commit

Permalink
go/analysis: add Sizes that matches gc size computations
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
cuonglm authored and gopherbot committed Jun 27, 2023
1 parent 5a89a3b commit 5aa6acb
Show file tree
Hide file tree
Showing 3 changed files with 323 additions and 1 deletion.
237 changes: 237 additions & 0 deletions go/analysis/unitchecker/sizes.go
Original file line number Diff line number Diff line change
@@ -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
}
85 changes: 85 additions & 0 deletions go/analysis/unitchecker/sizes_test.go
Original file line number Diff line number Diff line change
@@ -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
}
})
}
}
2 changes: 1 addition & 1 deletion go/analysis/unitchecker/unitchecker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 5aa6acb

Please sign in to comment.