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