mirror of
https://github.com/golang/go.git
synced 2026-01-29 07:02:05 +03:00
cmd/compile: fix loopvar version detection with line directives
The Go loop variable semantics changed in Go 1.22: loop variables are now
created per-iteration instead of per-loop. The compiler decides which
semantics to use based on the Go version in go.mod.
When go.mod specifies go 1.21 and the code is built with a Go 1.22+
compiler, the per-loop(compatible behavior) semantics should be used.
However, when a line directive is present in the source file,
go.mod 1.21 and go1.22+ compiler outputs a per-iteration semantics.
For example, the file below wants output 333 but got 012.
-- go.mod --
module test
go 1.21
-- main.go --
//line main.go:1
func main() {
var fns []func()
for i := 0; i < 3; i++ {
fns = append(fns, func() { fmt.Print(i) })
}
for _, fn := range fns {
fn()
}
}
The distinctVars function uses stmt.Pos().Base() to look up the file
version in FileVersions. Base() returns the file name after line
directives are applied (e.g., "main.go" for "//line main.go:1"), not
the actual source file path. This causes the version lookup to fail
for files with line directives.
This CL fixes the bug by using stmt.Pos().FileBase() instead. FileBase()
returns the actual file path before line directives are applied, ensuring
the correct version information is retrieved from the original source file.
Fixes: #77248
Change-Id: Idacc0816d112ee393089262468a02acfe40e4b72
Reviewed-on: https://go-review.googlesource.com/c/go/+/737820
Reviewed-by: Keith Randall <khr@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
This commit is contained in:
committed by
Keith Randall
parent
27fcec4d8f
commit
b408256be7
@@ -6,6 +6,7 @@ package loopvar_test
|
||||
|
||||
import (
|
||||
"internal/testenv"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -381,3 +382,62 @@ func TestLoopVarVersionDisableGoBuild(t *testing.T) {
|
||||
t.Errorf("err=%v == nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoopVarLineDirective tests that loopvar version detection works correctly
|
||||
// with line directives. This is a regression test for a bug where FileBase() was
|
||||
// used instead of Base(), causing incorrect version lookup when line directives
|
||||
// were present.
|
||||
func TestLoopVarLineDirective(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
default:
|
||||
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
|
||||
}
|
||||
switch runtime.GOARCH {
|
||||
case "amd64", "arm64":
|
||||
default:
|
||||
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
|
||||
}
|
||||
|
||||
testenv.MustHaveGoBuild(t)
|
||||
gocmd := testenv.GoToolPath(t)
|
||||
tmpdir := t.TempDir()
|
||||
output := filepath.Join(tmpdir, "foo.exe")
|
||||
|
||||
// Create a go.mod file with Go 1.21 to test compatibility behavior.
|
||||
// When building with a higher Go compiler, the loopvar should be created per-loop.
|
||||
gomodPath := filepath.Join(tmpdir, "go.mod")
|
||||
if err := os.WriteFile(gomodPath, []byte("module test\n\ngo 1.21\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Copy the test file (with line directive) to the temporary module
|
||||
testFile := "range_esc_closure_linedir.go"
|
||||
srcPath := filepath.Join("testdata", testFile)
|
||||
dstPath := filepath.Join(tmpdir, testFile)
|
||||
src, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(dstPath, src, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Build the module (not as a single file, so go.mod is respected)
|
||||
cmd := testenv.Command(t, gocmd, "build", "-o", output, ".")
|
||||
cmd.Dir = tmpdir
|
||||
b, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Logf("build output: %s", b)
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("build output: %s", b)
|
||||
|
||||
cmd = testenv.Command(t, output)
|
||||
b, err = cmd.CombinedOutput()
|
||||
t.Logf("run output: %s", b)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expected success (exit code 0), got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
24
src/cmd/compile/internal/loopvar/testdata/range_esc_closure_linedir.go
vendored
Normal file
24
src/cmd/compile/internal/loopvar/testdata/range_esc_closure_linedir.go
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2026 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.
|
||||
|
||||
//line range_esc_closure_linedir.go:5
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
var is []func() int
|
||||
|
||||
func main() {
|
||||
var ints = []int{0, 0, 0}
|
||||
for i := range ints {
|
||||
is = append(is, func() int { return i })
|
||||
}
|
||||
|
||||
for _, f := range is {
|
||||
fmt.Println(f())
|
||||
if f() != 2 {
|
||||
panic("loop variable i: expected shared per-loop, but got distinct per-iteration")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1557,7 +1557,7 @@ func (w *writer) forStmt(stmt *syntax.ForStmt) {
|
||||
|
||||
func (w *writer) distinctVars(stmt *syntax.ForStmt) bool {
|
||||
lv := base.Debug.LoopVar
|
||||
fileVersion := w.p.info.FileVersions[stmt.Pos().Base()]
|
||||
fileVersion := w.p.info.FileVersions[stmt.Pos().FileBase()]
|
||||
is122 := fileVersion == "" || version.Compare(fileVersion, "go1.22") >= 0
|
||||
|
||||
// Turning off loopvar for 1.22 is only possible with loopvarhash=qn
|
||||
|
||||
Reference in New Issue
Block a user