github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/loopvar/loopvar_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package loopvar_test
     6  
     7  import (
     8  	"os/exec"
     9  	"path/filepath"
    10  	"regexp"
    11  	"runtime"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/go-asm/go/testenv"
    16  )
    17  
    18  type testcase struct {
    19  	lvFlag      string // ==-2, -1, 0, 1, 2
    20  	buildExpect string // message, if any
    21  	expectRC    int
    22  	files       []string
    23  }
    24  
    25  var for_files = []string{
    26  	"for_esc_address.go",             // address of variable
    27  	"for_esc_closure.go",             // closure of variable
    28  	"for_esc_minimal_closure.go",     // simple closure of variable
    29  	"for_esc_method.go",              // method value of variable
    30  	"for_complicated_esc_address.go", // modifies loop index in body
    31  }
    32  
    33  var range_files = []string{
    34  	"range_esc_address.go",         // address of variable
    35  	"range_esc_closure.go",         // closure of variable
    36  	"range_esc_minimal_closure.go", // simple closure of variable
    37  	"range_esc_method.go",          // method value of variable
    38  }
    39  
    40  var cases = []testcase{
    41  	{"-1", "", 11, for_files[:1]},
    42  	{"0", "", 0, for_files[:1]},
    43  	{"1", "", 0, for_files[:1]},
    44  	{"2", "loop variable i now per-iteration,", 0, for_files},
    45  
    46  	{"-1", "", 11, range_files[:1]},
    47  	{"0", "", 0, range_files[:1]},
    48  	{"1", "", 0, range_files[:1]},
    49  	{"2", "loop variable i now per-iteration,", 0, range_files},
    50  
    51  	{"1", "", 0, []string{"for_nested.go"}},
    52  }
    53  
    54  // TestLoopVar checks that the GOEXPERIMENT and debug flags behave as expected.
    55  func TestLoopVarGo1_21(t *testing.T) {
    56  	switch runtime.GOOS {
    57  	case "linux", "darwin":
    58  	default:
    59  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
    60  	}
    61  	switch runtime.GOARCH {
    62  	case "amd64", "arm64":
    63  	default:
    64  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
    65  	}
    66  
    67  	testenv.MustHaveGoBuild(t)
    68  	gocmd := testenv.GoToolPath(t)
    69  	tmpdir := t.TempDir()
    70  	output := filepath.Join(tmpdir, "foo.exe")
    71  
    72  	for i, tc := range cases {
    73  		for _, f := range tc.files {
    74  			source := f
    75  			cmd := testenv.Command(t, gocmd, "build", "-o", output, "-gcflags=-lang=go1.21 -d=loopvar="+tc.lvFlag, source)
    76  			cmd.Env = append(cmd.Env, "GOEXPERIMENT=loopvar", "HOME="+tmpdir)
    77  			cmd.Dir = "testdata"
    78  			t.Logf("File %s loopvar=%s expect '%s' exit code %d", f, tc.lvFlag, tc.buildExpect, tc.expectRC)
    79  			b, e := cmd.CombinedOutput()
    80  			if e != nil {
    81  				t.Error(e)
    82  			}
    83  			if tc.buildExpect != "" {
    84  				s := string(b)
    85  				if !strings.Contains(s, tc.buildExpect) {
    86  					t.Errorf("File %s test %d expected to match '%s' with \n-----\n%s\n-----", f, i, tc.buildExpect, s)
    87  				}
    88  			}
    89  			// run what we just built.
    90  			cmd = testenv.Command(t, output)
    91  			b, e = cmd.CombinedOutput()
    92  			if tc.expectRC != 0 {
    93  				if e == nil {
    94  					t.Errorf("Missing expected error, file %s, case %d", f, i)
    95  				} else if ee, ok := (e).(*exec.ExitError); !ok || ee.ExitCode() != tc.expectRC {
    96  					t.Error(e)
    97  				} else {
    98  					// okay
    99  				}
   100  			} else if e != nil {
   101  				t.Error(e)
   102  			}
   103  		}
   104  	}
   105  }
   106  
   107  func TestLoopVarInlinesGo1_21(t *testing.T) {
   108  	switch runtime.GOOS {
   109  	case "linux", "darwin":
   110  	default:
   111  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   112  	}
   113  	switch runtime.GOARCH {
   114  	case "amd64", "arm64":
   115  	default:
   116  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   117  	}
   118  
   119  	testenv.MustHaveGoBuild(t)
   120  	gocmd := testenv.GoToolPath(t)
   121  	tmpdir := t.TempDir()
   122  
   123  	root := "github.com/go-asm/go/cmd/compile/loopvar/testdata/inlines"
   124  
   125  	f := func(pkg string) string {
   126  		// This disables the loopvar change, except for the specified package.
   127  		// The effect should follow the package, even though everything (except "c")
   128  		// is inlined.
   129  		cmd := testenv.Command(t, gocmd, "run", "-gcflags="+root+"/...=-lang=go1.21", "-gcflags="+pkg+"=-d=loopvar=1", root)
   130  		cmd.Env = append(cmd.Env, "GOEXPERIMENT=noloopvar", "HOME="+tmpdir)
   131  		cmd.Dir = filepath.Join("testdata", "inlines")
   132  
   133  		b, e := cmd.CombinedOutput()
   134  		if e != nil {
   135  			t.Error(e)
   136  		}
   137  		return string(b)
   138  	}
   139  
   140  	a := f(root + "/a")
   141  	b := f(root + "/b")
   142  	c := f(root + "/c")
   143  	m := f(root)
   144  
   145  	t.Logf(a)
   146  	t.Logf(b)
   147  	t.Logf(c)
   148  	t.Logf(m)
   149  
   150  	if !strings.Contains(a, "f, af, bf, abf, cf sums = 100, 45, 100, 100, 100") {
   151  		t.Errorf("Did not see expected value of a")
   152  	}
   153  	if !strings.Contains(b, "f, af, bf, abf, cf sums = 100, 100, 45, 45, 100") {
   154  		t.Errorf("Did not see expected value of b")
   155  	}
   156  	if !strings.Contains(c, "f, af, bf, abf, cf sums = 100, 100, 100, 100, 45") {
   157  		t.Errorf("Did not see expected value of c")
   158  	}
   159  	if !strings.Contains(m, "f, af, bf, abf, cf sums = 45, 100, 100, 100, 100") {
   160  		t.Errorf("Did not see expected value of m")
   161  	}
   162  }
   163  
   164  func countMatches(s, re string) int {
   165  	slice := regexp.MustCompile(re).FindAllString(s, -1)
   166  	return len(slice)
   167  }
   168  
   169  func TestLoopVarHashes(t *testing.T) {
   170  	// This behavior does not depend on Go version (1.21 or greater)
   171  	switch runtime.GOOS {
   172  	case "linux", "darwin":
   173  	default:
   174  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   175  	}
   176  	switch runtime.GOARCH {
   177  	case "amd64", "arm64":
   178  	default:
   179  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   180  	}
   181  
   182  	testenv.MustHaveGoBuild(t)
   183  	gocmd := testenv.GoToolPath(t)
   184  	tmpdir := t.TempDir()
   185  
   186  	root := "github.com/go-asm/go/cmd/compile/loopvar/testdata/inlines"
   187  
   188  	f := func(hash string) string {
   189  		// This disables the loopvar change, except for the specified hash pattern.
   190  		// -trimpath is necessary so we get the same answer no matter where the
   191  		// Go repository is checked out. This is not normally a concern since people
   192  		// do not normally rely on the meaning of specific hashes.
   193  		cmd := testenv.Command(t, gocmd, "run", "-trimpath", root)
   194  		cmd.Env = append(cmd.Env, "GOCOMPILEDEBUG=loopvarhash="+hash, "HOME="+tmpdir)
   195  		cmd.Dir = filepath.Join("testdata", "inlines")
   196  
   197  		b, _ := cmd.CombinedOutput()
   198  		// Ignore the error, sometimes it's supposed to fail, the output test will catch it.
   199  		return string(b)
   200  	}
   201  
   202  	for _, arg := range []string{"v001100110110110010100100", "vx336ca4"} {
   203  		m := f(arg)
   204  		t.Logf(m)
   205  
   206  		mCount := countMatches(m, "loopvarhash triggered github.com/go-asm/go/cmd/compile/loopvar/testdata/inlines/main.go:27:6: .* 001100110110110010100100")
   207  		otherCount := strings.Count(m, "loopvarhash")
   208  		if mCount < 1 {
   209  			t.Errorf("%s: did not see triggered main.go:27:6", arg)
   210  		}
   211  		if mCount != otherCount {
   212  			t.Errorf("%s: too many matches", arg)
   213  		}
   214  		mCount = countMatches(m, "github.com/go-asm/go/cmd/compile/loopvar/testdata/inlines/main.go:27:6: .* \\[bisect-match 0x7802e115b9336ca4\\]")
   215  		otherCount = strings.Count(m, "[bisect-match ")
   216  		if mCount < 1 {
   217  			t.Errorf("%s: did not see bisect-match for main.go:27:6", arg)
   218  		}
   219  		if mCount != otherCount {
   220  			t.Errorf("%s: too many matches", arg)
   221  		}
   222  
   223  		// This next test carefully dodges a bug-to-be-fixed with inlined locations for ir.Names.
   224  		if !strings.Contains(m, ", 100, 100, 100, 100") {
   225  			t.Errorf("%s: did not see expected value of m run", arg)
   226  		}
   227  	}
   228  }
   229  
   230  // TestLoopVarVersionEnableFlag checks for loopvar transformation enabled by command line flag (1.22).
   231  func TestLoopVarVersionEnableFlag(t *testing.T) {
   232  	switch runtime.GOOS {
   233  	case "linux", "darwin":
   234  	default:
   235  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   236  	}
   237  	switch runtime.GOARCH {
   238  	case "amd64", "arm64":
   239  	default:
   240  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   241  	}
   242  
   243  	testenv.MustHaveGoBuild(t)
   244  	gocmd := testenv.GoToolPath(t)
   245  
   246  	// loopvar=3 logs info but does not change loopvarness
   247  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.22 -d=loopvar=3", "opt.go")
   248  	cmd.Dir = filepath.Join("testdata")
   249  
   250  	b, err := cmd.CombinedOutput()
   251  	m := string(b)
   252  
   253  	t.Logf(m)
   254  
   255  	yCount := strings.Count(m, "opt.go:16:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt.go:29)")
   256  	nCount := strings.Count(m, "shared")
   257  
   258  	if yCount != 1 {
   259  		t.Errorf("yCount=%d != 1", yCount)
   260  	}
   261  	if nCount > 0 {
   262  		t.Errorf("nCount=%d > 0", nCount)
   263  	}
   264  	if err != nil {
   265  		t.Errorf("err=%v != nil", err)
   266  	}
   267  }
   268  
   269  // TestLoopVarVersionEnableGoBuild checks for loopvar transformation enabled by go:build version (1.22).
   270  func TestLoopVarVersionEnableGoBuild(t *testing.T) {
   271  	switch runtime.GOOS {
   272  	case "linux", "darwin":
   273  	default:
   274  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   275  	}
   276  	switch runtime.GOARCH {
   277  	case "amd64", "arm64":
   278  	default:
   279  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   280  	}
   281  
   282  	testenv.MustHaveGoBuild(t)
   283  	gocmd := testenv.GoToolPath(t)
   284  
   285  	// loopvar=3 logs info but does not change loopvarness
   286  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.21 -d=loopvar=3", "opt-122.go")
   287  	cmd.Dir = filepath.Join("testdata")
   288  
   289  	b, err := cmd.CombinedOutput()
   290  	m := string(b)
   291  
   292  	t.Logf(m)
   293  
   294  	yCount := strings.Count(m, "opt-122.go:18:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt-122.go:31)")
   295  	nCount := strings.Count(m, "shared")
   296  
   297  	if yCount != 1 {
   298  		t.Errorf("yCount=%d != 1", yCount)
   299  	}
   300  	if nCount > 0 {
   301  		t.Errorf("nCount=%d > 0", nCount)
   302  	}
   303  	if err != nil {
   304  		t.Errorf("err=%v != nil", err)
   305  	}
   306  }
   307  
   308  // TestLoopVarVersionDisableFlag checks for loopvar transformation DISABLED by command line version (1.21).
   309  func TestLoopVarVersionDisableFlag(t *testing.T) {
   310  	switch runtime.GOOS {
   311  	case "linux", "darwin":
   312  	default:
   313  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   314  	}
   315  	switch runtime.GOARCH {
   316  	case "amd64", "arm64":
   317  	default:
   318  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   319  	}
   320  
   321  	testenv.MustHaveGoBuild(t)
   322  	gocmd := testenv.GoToolPath(t)
   323  
   324  	// loopvar=3 logs info but does not change loopvarness
   325  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.21 -d=loopvar=3", "opt.go")
   326  	cmd.Dir = filepath.Join("testdata")
   327  
   328  	b, err := cmd.CombinedOutput()
   329  	m := string(b)
   330  
   331  	t.Logf(m) // expect error
   332  
   333  	yCount := strings.Count(m, "opt.go:16:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt.go:29)")
   334  	nCount := strings.Count(m, "shared")
   335  
   336  	if yCount != 0 {
   337  		t.Errorf("yCount=%d != 0", yCount)
   338  	}
   339  	if nCount > 0 {
   340  		t.Errorf("nCount=%d > 0", nCount)
   341  	}
   342  	if err == nil { // expect error
   343  		t.Errorf("err=%v == nil", err)
   344  	}
   345  }
   346  
   347  // TestLoopVarVersionDisableGoBuild checks for loopvar transformation DISABLED by go:build version (1.21).
   348  func TestLoopVarVersionDisableGoBuild(t *testing.T) {
   349  	switch runtime.GOOS {
   350  	case "linux", "darwin":
   351  	default:
   352  		t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
   353  	}
   354  	switch runtime.GOARCH {
   355  	case "amd64", "arm64":
   356  	default:
   357  		t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
   358  	}
   359  
   360  	testenv.MustHaveGoBuild(t)
   361  	gocmd := testenv.GoToolPath(t)
   362  
   363  	// loopvar=3 logs info but does not change loopvarness
   364  	cmd := testenv.Command(t, gocmd, "run", "-gcflags=-lang=go1.22 -d=loopvar=3", "opt-121.go")
   365  	cmd.Dir = filepath.Join("testdata")
   366  
   367  	b, err := cmd.CombinedOutput()
   368  	m := string(b)
   369  
   370  	t.Logf(m) // expect error
   371  
   372  	yCount := strings.Count(m, "opt-121.go:18:6: loop variable private now per-iteration, heap-allocated (loop inlined into ./opt-121.go:31)")
   373  	nCount := strings.Count(m, "shared")
   374  
   375  	if yCount != 0 {
   376  		t.Errorf("yCount=%d != 0", yCount)
   377  	}
   378  	if nCount > 0 {
   379  		t.Errorf("nCount=%d > 0", nCount)
   380  	}
   381  	if err == nil { // expect error
   382  		t.Errorf("err=%v == nil", err)
   383  	}
   384  }