Go: language: go and gccgo has different behavior when returning multiple values

Created on 7 Jan 2020  路  11Comments  路  Source: golang/go

What version of Go are you using (go version)?

$ go version
go version go1.13.5 linux/amd64

And for gccgo:

$ go version
go version go1.12.2 gccgo (GCC) 9.2.0 linux/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

Arch Linux on 64-bit x86

go env Output

$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/afr/.cache/go-build"
GOENV="/home/afr/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/afr/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build475953563=/tmp/go-build -gno-record-gcc-switches"

What did you do?

In connection with simplebolt, I discovered that the test passed when running with go, but failed when running with gccgo. I shrunk it down to this small test case:

package main

import "fmt"

func wrap(f func() error) error {
    return f()
}

func check() (result string, err error) {
    result = "A"
    return result, wrap(func() error {
        result = "B"
        return nil
    })
}

func main() {
    if result, _ := check(); result == "A" {
        fmt.Println("Compiled with gccgo")
    } else {
        fmt.Println("Compiled with go")
    }
}

When running this, it outputs "Compiled with gccgo" when compiled with gccgo and "Compiled with go" when compiled with go.

Also available at play.golang.org

What did you expect to see?

I would expect to see the same output for both go and gccgo. I don't know which compilation is "correct".

What did you see instead?

Different output for go and gccgo.

NeedsInvestigation

All 11 comments

The Go language does not fully specify the order of evaluation. With regard to this test case, in the return statement, it does not specify whether the value of the local variable result is read before or after the function is executed. So I would say that this is a legitimate difference between the two compilers, and is not a bug.

CC @griesemer @mdempsky for a second opinion.

Although it is not a bug, it is interesting that the behaviors between standard and short variable declarations are different (for gc):

package main

import "fmt"

func f(p *int) int {
    *p = 123
    return *p
}

func bar() {
    var x int
    y, z := x, f(&x)
    fmt.Println(y, z) // 123 123
}

func baz() {
    var x int
    var y, z = x, f(&x)
    fmt.Println(y, z) // 0 123
}

func main() {
    bar()
    baz()
}

Given:

var result string
var err error
result, err = "A", func() error { result = "B"; return nil }()

I think the result is unambiguously "A": we evaluate the RHS fully before assigning to any values to the LHS. So the "A" assignment happens last and wins.

I think arguably the same logic should apply to return statements, and in the original test case gccgo is correct, while cmd/compile is wrong.

Edit: Oh, but since the original example uses result instead of "A", the evaluation of that variable can happen after the function call, like @ianlancetaylor pointed out already.

Better assignment example is:

var result string = "A"
var err error
result, err = result, func() error { result = "B"; return nil }()

Or:

var x, y int
x, y = x + 100, func() int { x = 1; return 0 }()
// Unspecified whether x is now 100 or 101.

Agreed this is a legitimate difference in implementation specific behavior.

@go101 Your examples showing discrepancy between normal and short variable initialization are interesting and probably worth fixing, but I think would be best as a separate issue.

I'm going to go ahead and close as Working As Intended, since @ianlancetaylor and I are in agreement on that.

Thanks for looking into this!

@go101 Your examples showing discrepancy between normal and short variable initialization are interesting and probably worth fixing, but I think would be best as a separate issue.

The behavior of var y, z = x, f(&x) seems wrong to me. It's evaluated as:

var x int
var y int
y = x
var z int
z = f(&x)

The spec requires the rhs is evaluated first.

@go101 want to fire an issue 馃槃

@cuonglm The RHS has to be fully evaluated before any assignments happen to the LHS, but it's not specified whether x is evaluated before or after f(&x).

For example,

y, z = x, f(&x)

can be validly compiled as:

// Evaluate RHS
tmp1 := f(&x)
tmp0 := x

// Assign to LHS
y = tmp0
z = tmp1

@cuonglm The RHS has to be fully evaluated before any assignments happen to the LHS, but it's not specified whether x is evaluated before or after f(&x).

For example,

y, z = x, f(&x)

can be validly compiled as:

// Evaluate RHS
tmp1 := f(&x)
tmp0 := x

// Assign to LHS
y = tmp0
z = tmp1

Yes right, but I mean the current var form seems not wait f(&x) evaluated before it does assignment, compile with -m -m:

main.go:3:6: can inline f as: func(*int) int { *p = 123; return *p }
main.go:8:6: can inline bar as: func() { var x int; x = <N>; y, z := x, f(&x); println(y, z) }
main.go:10:14: inlining call to f func(*int) int { *p = 123; return *p }
main.go:15:6: can inline baz as: func() { var x int; x = <N>; var y int; y = x; var z int; z = f(&x); println(y, z) }
main.go:17:17: inlining call to f func(*int) int { *p = 123; return *p }
main.go:22:6: can inline main as: func() { bar(); baz() }
main.go:23:5: inlining call to bar func() { var x int; x = <N>; y, z := x, f(&x); println(y, z) }
main.go:23:5: inlining call to f func(*int) int { *p = 123; return *p }
main.go:24:5: inlining call to baz func() { var x int; x = <N>; var y int; y = x; var z int; z = f(&x); println(y, z) }
main.go:24:5: inlining call to f func(*int) int { *p = 123; return *p }
main.go:3:8: p does not escape

Yes right, but I mean the current var form seems not wait f(&x) evaluated before it does assignment, compile with -m -m:

Fair point, and I think that hints to me why the behavior is different between @go101 's test cases.

But it's also just an implementation detail. Compilers are allowed to freely reorder/combine statements and operations, as long as the program visible behavior is correct. Eg, assigning to y before the RHS is done evaluating is fine because y doesn't come into scope until after the RHS anyway.

Was this page helpful?
0 / 5 - 0 ratings