Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/tools/go/analysis/passes/nilness: spurious "impossible/tautological condition" with cond := a != nil; if cond {...}; if cond { ... } #70381

Open
hut8 opened this issue Nov 15, 2024 · 10 comments
Assignees
Labels
Tools This label describes issues relating to any tools in the x/tools repository.
Milestone

Comments

@hut8
Copy link

hut8 commented Nov 15, 2024

Go version

go version go1.23.0 linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/liam/.cache/go-build'
GOENV='/home/liam/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/liam/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/liam/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.23.0'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/liam/.config/go/telemetry'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/liam/nilness-bug/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1057087488=/tmp/go-build -gno-record-gcc-switches'

What did you do?

I believe there is a bug in the nilness checker that incorrectly reports an impossible "nil != nil" and also omits an error in a different location about a tautological comparison. This code demonstrates it:

package main

import "fmt"

func TriggerBug(a *struct{}) {
	hasA := a != nil // "impossible condition: nil != nil"
	if hasA {
		fmt.Println("A is not nil")
		return
	}
	if !hasA {
		fmt.Println("A is nil")
	}
}

I checked here: https://github.com/golang/go/issues?page=1&q=is%3Aissue+is%3Aopen+nilness and I can't find anything that matches.

What did you see happen?

	hasA := a != nil // "impossible condition: nil != nil"

Here is a screenshot for clarity:

image

What did you expect to see?

The above error implies that a is always nil and therefore a != nil will never happen. That is not true. However, there still is a problem with the code. Because of the return, then the if !hasA is tautological -- although, that's a different error, and it's not reported, and the error that is reported is not only erroneous but also in the wrong place.

This, for example, correctly reports no errors:

func TriggerBug(a *struct{}) {
	hasA := a != nil
	if hasA {
		fmt.Println("A is not nil")
		return
	}

	fmt.Println("A is nil")
}

So I would expect to see something like this:

func TriggerBug(a *struct{}) {
	hasA := a != nil
	if hasA {
		fmt.Println("A is not nil")
		return
	}
	// at this point, hasA is definitely false
	if !hasA { //  "tautological condition: nil == nil"
		fmt.Println("A is nil")
	}
}

Interestingly, if I just get rid of hasA and put the expression directly in the if statement, I get:

func TriggerBug(a *struct{}) {
	if a != nil {
		fmt.Println("A is not nil")
		return
	}
	if a == nil { // tautological condition: nil == nil
		fmt.Println("A is nil")
	}
}

image

That's what I would expect to see whether or not I have the boolean value broken out into a different variable.

@gopherbot gopherbot added the Tools This label describes issues relating to any tools in the x/tools repository. label Nov 15, 2024
@gopherbot gopherbot added this to the Unreleased milestone Nov 15, 2024
@IlyasYOY
Copy link

IlyasYOY commented Nov 16, 2024

Hello! I found your issue interesting.

I tried to debug the cases you provided and found this. Code assumes that left operand is nil in case of nil right operand.

Here is the commit: IlyasYOY/tools@5c4cf21 (you might want to experiment with it)

TriggerTautological stills fails, but no false positive.


I'd be glad to dive deeper and find the solution. If there are any hints I'd really appreciate it .

IlyasYOY added a commit to IlyasYOY/tools that referenced this issue Nov 16, 2024
@adonovan adonovan self-assigned this Nov 18, 2024
@adonovan
Copy link
Member

Thanks for the very thorough bug report! The problem is that the nilness analyzer is not taking care to distinguish whether a condition appears directly within an if condition, or via an intervening variable. In the original test case:

func TriggerBug(a *struct{}) {
	hasA := a != nil
	if hasA {
		fmt.Println("A is not nil")
		return
	}
	if !hasA {
		fmt.Println("A is nil")
	}
}

the analyzer detects that the first if hasA, which is really of the form if a != nil in SSA, is fine; but the second if hasA, which of course has the same form if a != nil in SSA, is dominated by code that establishes that a is nil, so the analyzer reports a problem about this condition. But there is syntactically only one condition.

The analyzer will need to detect whether the condition has multiple referrers; if so, it will need to report that the condition is trivially true/false only for some of them, or, report the error at the if statement itself, if this information is available.

@adonovan adonovan changed the title x/tools/go/analysis/passes/nilness: incorrect "impossible condition: nil != nil"; missing tautological error x/tools/go/analysis/passes/nilness: spurious "impossible/tautological condition" with cond := a != nil; if cond {...}; if cond { ... } Nov 18, 2024
@adonovan
Copy link
Member

Unfortunately ssa.If does not record the position of the ast.IfStmt. (In fact its Pos method promises to return NoPos, a youthful error: never promise to return empty information since you can't change your mind later.)

Some options:

  1. Change buildssa to use Debug ssa.Mode, allowing us to use variables to infer the structure. But this is expensive.
  2. Detect that the condition has multiple Referrers and suppress the diagnostic in that case. But that's sad, because it is real diagnostic reported in the wrong place.
  3. Same, but instead of suppressing the error, qualify it with something like "(may apply to only some uses of the variable assigned by condition)". But it still lacks position information.
  4. Change the stack of dominating facts to record not just the fact but the SSA value that it came from. Then if we notice that an SSA value is its own witness (as in the case of hasA above), we report a different error (something like "the variable assigned to this condition is checked after it has already been established/falsified"). But again it still lacks the crucial position of the second reference.
  5. Find some weaselly way to make ssa.If.Pos report the position of the IfStmt. But conditional branches created by the SSA builder are pretty far abstracted from the source, so there isn't always an obvious way to express the position.

Option 5 is really the only approach that gives us the position of the second If statement, so it's my favorite. But it's a project.

@timothy-king
Copy link
Contributor

Long term I think recording the ast.Node unconditionally is likely better for ssa. That extra word per Instruction buys a lot.

But I agree option 5 seems like the right short term solution for this. Changing that comment is a waste of the proposal committee's time IMO, but I'll leave that up to you.

@adonovan
Copy link
Member

Long term I think recording the ast.Node unconditionally is likely better for ssa. That extra word per Instruction buys a lot.

Two words! And it's far from clear what the correct Node is for each Instr once the syntax is all boiled down.

@timothy-king
Copy link
Contributor

You get to reclaim the token.Pos so the delta should be +1 word per Instruction (pre-padding/alignment considerations).

And it's far from clear what the correct Node is for each Instr once the syntax is all boiled down.

Good point. This is much easier if you only need to be consistent within a handful of repos, but ssa does aspire to more consistency than this.

@adonovan
Copy link
Member

adonovan commented Nov 18, 2024

You get to reclaim the token.Pos so the delta should be +1 word per Instruction (pre-padding/alignment considerations).

The Pos is not a field, it's code:

func (s *If) Pos() token.Pos        { return token.NoPos }

And for the instructions that do have a Pos field, they would still need to keep it because the hypothetical ast.Node field would be potentially nil (or too diverse a range of things to reliably derive pos from it).

@lublak
Copy link

lublak commented Dec 30, 2024

I dont know if my issue matches, but i have the ?same? issue with a nil pointer:

type Infos struct {
	m     map[string]string
}

// ....

var infos *Infos

for k, v := range data {
  if v.valid {
    if infos == nil { // tautological condition: nil == nil
      infos = &Infos{}
    }
  }
}

@adonovan
Copy link
Member

type Infos struct {
	m     map[string]string
}

// ....

var infos *Infos

for k, v := range data {
  if v.valid {
    if data == nil { // tautological condition: nil == nil
      data = &Infos{}
    }
  }
}

This code isn't valid: data cannot be both the operand of a range statement and a pointer to a struct. What is its type?

Can you provide a working example that reproduces the problem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Tools This label describes issues relating to any tools in the x/tools repository.
Projects
None yet
Development

No branches or pull requests

7 participants