go/src/cmd/compile/internal/logopt/logopt_test.go
Matthew Dempsky 063a91c0ab cmd/compile: fix recognition of unnamed return variables
In golang.org/cl/266199, I reused the existing code in inlining that
recognizes anonymous variables. However, it turns out that code
mistakenly recognizes anonymous return parameters as named when
inlining a function from the same package.

The issue is funcargs (which is only used for functions parsed from
source) synthesizes ~r names for anonymous return parameters, but
funcargs2 (which is only used for functions imported from export data)
does not.

This CL fixes the behavior so that anonymous return parameters are
handled identically whether a function is inlined within the same
package or across packages. It also adds a proper cross-package test
case demonstrating #33160 is fixed in both cases.

Change-Id: Iaa39a23f5666979a1f5ca6d09fc8c398e55b784c
Reviewed-on: https://go-review.googlesource.com/c/go/+/266719
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
Reviewed-by: David Chase <drchase@google.com>
Trust: Matthew Dempsky <mdempsky@google.com>
2020-11-01 01:06:56 +00:00

258 lines
10 KiB
Go

// Copyright 2019 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 logopt
import (
"internal/testenv"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
const srcCode = `package x
type pair struct {a,b int}
func bar(y *pair) *int {
return &y.b
}
var a []int
func foo(w, z *pair) *int {
if *bar(w) > 0 {
return bar(z)
}
if a[1] > 0 {
a = a[:2]
}
return &a[0]
}
// address taking prevents closure inlining
func n() int {
foo := func() int { return 1 }
bar := &foo
x := (*bar)() + foo()
return x
}
`
func want(t *testing.T, out string, desired string) {
// On Windows, Unicode escapes in the JSON output end up "normalized" elsewhere to /u....,
// so "normalize" what we're looking for to match that.
s := strings.ReplaceAll(desired, string(os.PathSeparator), "/")
if !strings.Contains(out, s) {
t.Errorf("did not see phrase %s in \n%s", s, out)
}
}
func wantN(t *testing.T, out string, desired string, n int) {
if strings.Count(out, desired) != n {
t.Errorf("expected exactly %d occurences of %s in \n%s", n, desired, out)
}
}
func TestPathStuff(t *testing.T) {
sep := string(filepath.Separator)
if path, whine := parseLogPath("file:///c:foo"); path != "c:foo" || whine != "" { // good path
t.Errorf("path='%s', whine='%s'", path, whine)
}
if path, whine := parseLogPath("file:///foo"); path != sep+"foo" || whine != "" { // good path
t.Errorf("path='%s', whine='%s'", path, whine)
}
if path, whine := parseLogPath("foo"); path != "" || whine == "" { // BAD path
t.Errorf("path='%s', whine='%s'", path, whine)
}
if sep == "\\" { // On WINDOWS ONLY
if path, whine := parseLogPath("C:/foo"); path != "C:\\foo" || whine != "" { // good path
t.Errorf("path='%s', whine='%s'", path, whine)
}
if path, whine := parseLogPath("c:foo"); path != "" || whine == "" { // BAD path
t.Errorf("path='%s', whine='%s'", path, whine)
}
if path, whine := parseLogPath("/foo"); path != "" || whine == "" { // BAD path
t.Errorf("path='%s', whine='%s'", path, whine)
}
} else { // ON UNIX ONLY
if path, whine := parseLogPath("/foo"); path != sep+"foo" || whine != "" { // good path
t.Errorf("path='%s', whine='%s'", path, whine)
}
}
}
func TestLogOpt(t *testing.T) {
t.Parallel()
testenv.MustHaveGoBuild(t)
dir, err := ioutil.TempDir("", "TestLogOpt")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dir = fixSlash(dir) // Normalize the directory name as much as possible, for Windows testing
src := filepath.Join(dir, "file.go")
if err := ioutil.WriteFile(src, []byte(srcCode), 0644); err != nil {
t.Fatal(err)
}
outfile := filepath.Join(dir, "file.o")
t.Run("JSON_fails", func(t *testing.T) {
// Test malformed flag
out, err := testLogOpt(t, "-json=foo", src, outfile)
if err == nil {
t.Error("-json=foo succeeded unexpectedly")
}
want(t, out, "option should be")
want(t, out, "number")
// Test a version number that is currently unsupported (and should remain unsupported for a while)
out, err = testLogOpt(t, "-json=9,foo", src, outfile)
if err == nil {
t.Error("-json=0,foo succeeded unexpectedly")
}
want(t, out, "version must be")
})
// replace d (dir) with t ("tmpdir") and convert path separators to '/'
normalize := func(out []byte, d, t string) string {
s := string(out)
s = strings.ReplaceAll(s, d, t)
s = strings.ReplaceAll(s, string(os.PathSeparator), "/")
return s
}
// Ensure that <128 byte copies are not reported and that 128-byte copies are.
// Check at both 1 and 8-byte alignments.
t.Run("Copy", func(t *testing.T) {
const copyCode = `package x
func s128a1(x *[128]int8) [128]int8 {
return *x
}
func s127a1(x *[127]int8) [127]int8 {
return *x
}
func s16a8(x *[16]int64) [16]int64 {
return *x
}
func s15a8(x *[15]int64) [15]int64 {
return *x
}
`
copy := filepath.Join(dir, "copy.go")
if err := ioutil.WriteFile(copy, []byte(copyCode), 0644); err != nil {
t.Fatal(err)
}
outcopy := filepath.Join(dir, "copy.o")
// On not-amd64, test the host architecture and os
arches := []string{runtime.GOARCH}
goos0 := runtime.GOOS
if runtime.GOARCH == "amd64" { // Test many things with "linux" (wasm will get "js")
arches = []string{"arm", "arm64", "386", "amd64", "mips", "mips64", "ppc64le", "riscv64", "s390x", "wasm"}
goos0 = "linux"
}
for _, arch := range arches {
t.Run(arch, func(t *testing.T) {
goos := goos0
if arch == "wasm" {
goos = "js"
}
_, err := testCopy(t, dir, arch, goos, copy, outcopy)
if err != nil {
t.Error("-json=0,file://log/opt should have succeeded")
}
logged, err := ioutil.ReadFile(filepath.Join(dir, "log", "opt", "x", "copy.json"))
if err != nil {
t.Error("-json=0,file://log/opt missing expected log file")
}
slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
t.Logf("%s", slogged)
want(t, slogged, `{"range":{"start":{"line":3,"character":2},"end":{"line":3,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
want(t, slogged, `{"range":{"start":{"line":9,"character":2},"end":{"line":9,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
wantN(t, slogged, `"code":"copy"`, 2)
})
}
})
// Some architectures don't fault on nil dereference, so nilchecks are eliminated differently.
// The N-way copy test also doesn't need to run N-ways N times.
if runtime.GOARCH != "amd64" {
return
}
t.Run("Success", func(t *testing.T) {
// This test is supposed to succeed
// Note 'file://' is the I-Know-What-I-Am-Doing way of specifying a file, also to deal with corner cases for Windows.
_, err := testLogOptDir(t, dir, "-json=0,file://log/opt", src, outfile)
if err != nil {
t.Error("-json=0,file://log/opt should have succeeded")
}
logged, err := ioutil.ReadFile(filepath.Join(dir, "log", "opt", "x", "file.json"))
if err != nil {
t.Error("-json=0,file://log/opt missing expected log file")
}
// All this delicacy with uriIfy and filepath.Join is to get this test to work right on Windows.
slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
t.Logf("%s", slogged)
// below shows proper nilcheck
want(t, slogged, `{"range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}},"severity":3,"code":"nilcheck","source":"go compiler","message":"",`+
`"relatedInformation":[{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"}]}`)
want(t, slogged, `{"range":{"start":{"line":11,"character":6},"end":{"line":11,"character":6}},"severity":3,"code":"isInBounds","source":"go compiler","message":""}`)
want(t, slogged, `{"range":{"start":{"line":7,"character":6},"end":{"line":7,"character":6}},"severity":3,"code":"canInlineFunction","source":"go compiler","message":"cost: 35"}`)
// escape analysis explanation
want(t, slogged, `{"range":{"start":{"line":7,"character":13},"end":{"line":7,"character":13}},"severity":3,"code":"leak","source":"go compiler","message":"parameter z leaks to ~r2 with derefs=0",`+
`"relatedInformation":[`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: y = z:"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y := z (assign-pair)"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: ~R0 = y:"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y.b (dot of pointer)"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from \u0026y.b (address-of)"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":9},"end":{"line":4,"character":9}}},"message":"inlineLoc"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from ~R0 = \u003cN\u003e (assign-pair)"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: flow: ~r2 = ~R0:"},`+
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: from return (*int)(~R0) (return)"}]}`)
})
}
func testLogOpt(t *testing.T, flag, src, outfile string) (string, error) {
run := []string{testenv.GoToolPath(t), "tool", "compile", flag, "-o", outfile, src}
t.Log(run)
cmd := exec.Command(run[0], run[1:]...)
out, err := cmd.CombinedOutput()
t.Logf("%s", out)
return string(out), err
}
func testLogOptDir(t *testing.T, dir, flag, src, outfile string) (string, error) {
// Notice the specified import path "x"
run := []string{testenv.GoToolPath(t), "tool", "compile", "-p", "x", flag, "-o", outfile, src}
t.Log(run)
cmd := exec.Command(run[0], run[1:]...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
t.Logf("%s", out)
return string(out), err
}
func testCopy(t *testing.T, dir, goarch, goos, src, outfile string) (string, error) {
// Notice the specified import path "x"
run := []string{testenv.GoToolPath(t), "tool", "compile", "-p", "x", "-json=0,file://log/opt", "-o", outfile, src}
t.Log(run)
cmd := exec.Command(run[0], run[1:]...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS="+goos)
out, err := cmd.CombinedOutput()
t.Logf("%s", out)
return string(out), err
}