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