Skip to content

Commit fe1daf2

Browse files
committed
cmd/compile: use new for loop semantics for Go 1.22+ compilations
This includes version-dependent support for GOEXPERIMENT and -d=loopvar, -d=loopvarhash, to allow testing/porting of old code. Includes tests of downgrade (1.22 -> 1.21) and upgrade (1.21 -> 1.22) based on //go:build lines (while running a 1.22 build/compiler). Change-Id: Idd3be61a2b46acec33c7e7edac0924158cc726b4 Reviewed-on: https://go-review.googlesource.com/c/go/+/508819 Run-TryBot: David Chase <[email protected]> Reviewed-by: Russ Cox <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 6d8d88a commit fe1daf2

File tree

7 files changed

+238
-11
lines changed

7 files changed

+238
-11
lines changed

src/cmd/compile/internal/loopvar/loopvar.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ func LogTransformations(transformed []VarAndLoop) {
605605
// Intended to help with performance debugging, we record whole loop ranges
606606
logopt.LogOptRange(pos, last, "loop-modified-"+loopKind, "loopvar", ir.FuncName(l.curfn))
607607
}
608-
if print && 3 <= base.Debug.LoopVar {
608+
if print && 4 <= base.Debug.LoopVar {
609609
// TODO decide if we want to keep this, or not. It was helpful for validating logopt, otherwise, eh.
610610
inner := base.Ctxt.InnermostPos(pos)
611611
outer := base.Ctxt.OutermostPos(pos)

src/cmd/compile/internal/loopvar/loopvar_test.go

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ var cases = []testcase{
5151
}
5252

5353
// TestLoopVar checks that the GOEXPERIMENT and debug flags behave as expected.
54-
func TestLoopVar(t *testing.T) {
54+
func TestLoopVarGo1_21(t *testing.T) {
5555
switch runtime.GOOS {
5656
case "linux", "darwin":
5757
default:
@@ -71,7 +71,7 @@ func TestLoopVar(t *testing.T) {
7171
for i, tc := range cases {
7272
for _, f := range tc.files {
7373
source := f
74-
cmd := testenv.Command(t, gocmd, "build", "-o", output, "-gcflags=-d=loopvar="+tc.lvFlag, source)
74+
cmd := testenv.Command(t, gocmd, "build", "-o", output, "-gcflags=-lang=go1.21 -d=loopvar="+tc.lvFlag, source)
7575
cmd.Env = append(cmd.Env, "GOEXPERIMENT=loopvar", "HOME="+tmpdir)
7676
cmd.Dir = "testdata"
7777
t.Logf("File %s loopvar=%s expect '%s' exit code %d", f, tc.lvFlag, tc.buildExpect, tc.expectRC)
@@ -103,7 +103,7 @@ func TestLoopVar(t *testing.T) {
103103
}
104104
}
105105

106-
func TestLoopVarInlines(t *testing.T) {
106+
func TestLoopVarInlinesGo1_21(t *testing.T) {
107107
switch runtime.GOOS {
108108
case "linux", "darwin":
109109
default:
@@ -125,7 +125,7 @@ func TestLoopVarInlines(t *testing.T) {
125125
// This disables the loopvar change, except for the specified package.
126126
// The effect should follow the package, even though everything (except "c")
127127
// is inlined.
128-
cmd := testenv.Command(t, gocmd, "run", "-gcflags="+pkg+"=-d=loopvar=1", root)
128+
cmd := testenv.Command(t, gocmd, "run", "-gcflags="+root+"/...=-lang=go1.21", "-gcflags="+pkg+"=-d=loopvar=1", root)
129129
cmd.Env = append(cmd.Env, "GOEXPERIMENT=noloopvar", "HOME="+tmpdir)
130130
cmd.Dir = filepath.Join("testdata", "inlines")
131131

@@ -166,6 +166,7 @@ func countMatches(s, re string) int {
166166
}
167167

168168
func TestLoopVarHashes(t *testing.T) {
169+
// This behavior does not depend on Go version (1.21 or greater)
169170
switch runtime.GOOS {
170171
case "linux", "darwin":
171172
default:
@@ -187,7 +188,7 @@ func TestLoopVarHashes(t *testing.T) {
187188
// This disables the loopvar change, except for the specified hash pattern.
188189
// -trimpath is necessary so we get the same answer no matter where the
189190
// Go repository is checked out. This is not normally a concern since people
190-
// do not rely on the meaning of specific hashes.
191+
// do not normally rely on the meaning of specific hashes.
191192
cmd := testenv.Command(t, gocmd, "run", "-trimpath", root)
192193
cmd.Env = append(cmd.Env, "GOCOMPILEDEBUG=loopvarhash="+hash, "HOME="+tmpdir)
193194
cmd.Dir = filepath.Join("testdata", "inlines")
@@ -225,7 +226,8 @@ func TestLoopVarHashes(t *testing.T) {
225226
}
226227
}
227228

228-
func TestLoopVarOpt(t *testing.T) {
229+
// TestLoopVarVersionEnableFlag checks for loopvar transformation enabled by command line flag (1.22).
230+
func TestLoopVarVersionEnableFlag(t *testing.T) {
229231
switch runtime.GOOS {
230232
case "linux", "darwin":
231233
default:
@@ -240,7 +242,8 @@ func TestLoopVarOpt(t *testing.T) {
240242
testenv.MustHaveGoBuild(t)
241243
gocmd := testenv.GoToolPath(t)
242244

243-
cmd := testenv.Command(t, gocmd, "run", "-gcflags=-d=loopvar=2", "opt.go")
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")
244247
cmd.Dir = filepath.Join("testdata")
245248

246249
b, err := cmd.CombinedOutput()
@@ -260,5 +263,121 @@ func TestLoopVarOpt(t *testing.T) {
260263
if err != nil {
261264
t.Errorf("err=%v != nil", err)
262265
}
266+
}
267+
268+
// TestLoopVarVersionEnableGoBuild checks for loopvar transformation enabled by go:build version (1.22).
269+
func 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:32)")
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).
308+
func 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+
}
263319

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:30)")
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).
347+
func 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:32)")
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+
}
264383
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
//go:build go1.21
6+
7+
package main
8+
9+
import (
10+
"fmt"
11+
"os"
12+
)
13+
14+
var is []func() int
15+
16+
func inline(j, k int) []*int {
17+
var a []*int
18+
for private := j; private < k; private++ {
19+
a = append(a, &private)
20+
}
21+
return a
22+
23+
}
24+
25+
//go:noinline
26+
func notinline(j, k int) ([]*int, *int) {
27+
for shared := j; shared < k; shared++ {
28+
if shared == k/2 {
29+
// want the call inlined, want "private" in that inline to be transformed,
30+
// (believe it ends up on init node of the return).
31+
// but do not want "shared" transformed,
32+
return inline(j, k), &shared
33+
}
34+
}
35+
return nil, &j
36+
}
37+
38+
func main() {
39+
a, p := notinline(2, 9)
40+
fmt.Printf("a[0]=%d,*p=%d\n", *a[0], *p)
41+
if *a[0] != 2 {
42+
os.Exit(1)
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
//go:build go1.22
6+
7+
package main
8+
9+
import (
10+
"fmt"
11+
"os"
12+
)
13+
14+
var is []func() int
15+
16+
func inline(j, k int) []*int {
17+
var a []*int
18+
for private := j; private < k; private++ {
19+
a = append(a, &private)
20+
}
21+
return a
22+
23+
}
24+
25+
//go:noinline
26+
func notinline(j, k int) ([]*int, *int) {
27+
for shared := j; shared < k; shared++ {
28+
if shared == k/2 {
29+
// want the call inlined, want "private" in that inline to be transformed,
30+
// (believe it ends up on init node of the return).
31+
// but do not want "shared" transformed,
32+
return inline(j, k), &shared
33+
}
34+
}
35+
return nil, &j
36+
}
37+
38+
func main() {
39+
a, p := notinline(2, 9)
40+
fmt.Printf("a[0]=%d,*p=%d\n", *a[0], *p)
41+
if *a[0] != 2 {
42+
os.Exit(1)
43+
}
44+
}

src/cmd/compile/internal/noder/irgen.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func checkFiles(m posMap, noders []*noder) (*types2.Package, *types2.Info) {
6464
Implicits: make(map[syntax.Node]types2.Object),
6565
Scopes: make(map[syntax.Node]*types2.Scope),
6666
Instances: make(map[*syntax.Name]types2.Instance),
67+
FileVersions: make(map[*syntax.PosBase]types2.Version),
6768
// expand as needed
6869
}
6970

src/cmd/compile/internal/noder/reader.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,10 +1897,10 @@ func (r *reader) forStmt(label *types.Sym) ir.Node {
18971897
cond := r.optExpr()
18981898
post := r.stmt()
18991899
body := r.blockStmt()
1900-
dv := r.Bool()
1900+
perLoopVars := r.Bool()
19011901
r.closeAnotherScope()
19021902

1903-
stmt := ir.NewForStmt(pos, init, cond, post, body, dv)
1903+
stmt := ir.NewForStmt(pos, init, cond, post, body, perLoopVars)
19041904
stmt.Label = label
19051905
return stmt
19061906
}

src/cmd/compile/internal/noder/writer.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1456,10 +1456,29 @@ func (w *writer) forStmt(stmt *syntax.ForStmt) {
14561456
}
14571457

14581458
w.blockStmt(stmt.Body)
1459-
w.Bool(base.Debug.LoopVar > 0)
1459+
w.Bool(w.distinctVars(stmt))
14601460
w.closeAnotherScope()
14611461
}
14621462

1463+
func (w *writer) distinctVars(stmt *syntax.ForStmt) bool {
1464+
lv := base.Debug.LoopVar
1465+
v := w.p.info.FileVersions[stmt.Pos().Base()]
1466+
is122 := v.Major == 0 && v.Minor == 0 || v.Major == 1 && v.Minor >= 22
1467+
1468+
// Turning off loopvar for 1.22 is only possible with loopvarhash=qn
1469+
//
1470+
// Debug.LoopVar values to be preserved for 1.21 compatibility are 1 and 2,
1471+
// which are also set (=1) by GOEXPERIMENT=loopvar. The knobs for turning on
1472+
// the new, unshared, loopvar behavior apply to versions less than 1.21 because
1473+
// (1) 1.21 also did that and (2) this is believed to be the likely use case;
1474+
// anyone checking to see if it affects their code will just run the GOEXPERIMENT
1475+
// but will not also update all their go.mod files to 1.21.
1476+
//
1477+
// -gcflags=-d=loopvar=3 enables logging for 1.22 but does not turn loopvar on for <= 1.21.
1478+
1479+
return is122 || lv > 0 && lv != 3
1480+
}
1481+
14631482
// rangeTypes returns the types of values produced by ranging over
14641483
// expr.
14651484
func (pw *pkgWriter) rangeTypes(expr syntax.Expr) (key, value types2.Type) {

0 commit comments

Comments
 (0)