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

Confusion with double negation and booleans #36856

Closed
PieterPenninckx opened this Issue Sep 30, 2016 · 16 comments

Comments

Projects
None yet
7 participants
@PieterPenninckx
Copy link
Contributor

PieterPenninckx commented Sep 30, 2016

In certain circumstances, true != !false is considered true.

Steps to reproduce

  1. Create a new project with cargo new negation --bin
  2. Change the file src/main.rs so that the contents is the following:
fn g() -> bool {
    false
}

fn main() {
    let a = !g();
    if a != !g() {
        println!("wrong");
    } else {
        println!("correct");
    }
}

3.Run with cargo run

Expected and observed behavior

When run with cargo run, the compiled program prints wrong.
I expect correct to be printed because after the assignment,
a should still equal the value it was assigned to (note that g() always
returns the same value).

Note that cargo run --release results in printing correct, so even if it's me who is being confused, the output should still be the same in debug mode and in release mode.

Meta

When I do a normal rustc main.rs and witch cargo run --release the resulting
program behaves as I expect.
The unexpected behaviour only occurs when using cargo run (or running target/debug/negation).

rustc --version --verbose:

rustc 1.12.0 (3191fbae9 2016-09-23)
binary: rustc
commit-hash: 3191fbae9da539442351f883bdabcad0d72efcb6
commit-date: 2016-09-23
host: x86_64-unknown-linux-gnu
release: 1.12.0

cargo --version --verbose

cargo 0.13.0-nightly (109cb7c 2016-08-19)

Edit: added short description.

@TimNN

This comment has been minimized.

Copy link
Contributor

TimNN commented Sep 30, 2016

This is... interesting

I tested on OS X with various rust versions with various flags:

no flags -g -O -g -O
1.2.0 correct correct correct correct
1.3.0 wrong wrong correct correct
1.10 wrong wrong correct correct
1.11 wrong wrong correct correct
1.12 correct wrong correct correct
beta correct wrong correct correct
nightly correct wrong correct correct
@TimNN

This comment has been minimized.

Copy link
Contributor

TimNN commented Sep 30, 2016

The first column seems to have been "fixed" by the switch to mir.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Sep 30, 2016

The 1.12 change might mean MIR fixed an old trans bug. Unoptimized -g being wrong leads me to believe this has to do with whether we put variables in allocas.

Before MIR trans, we didn't have the infrastructure to handle turning let y = 5; x + y into add %x, 5.
We always used an alloca (a local stack slot in LLVM) for variables. With MIR trans, we can do better.

What hasn't changed, though, is the behavior with -g (debuginfo) turned on: while LLVM supports annotating values with debug information (i.e. saying that a variable y has value 5 without that 5 needing to be kept in memory), we haven't fully transitioned to that model so we still need allocas.

-g -O working means LLVM optimizations unbreak it, thus it's one of:

  • LLVM codegen for the variable case and/or with no optimizations is wrong
  • we generate bad IR for the variable case that somehow optimizes to the correct result
@TimNN

This comment has been minimized.

Copy link
Contributor

TimNN commented Sep 30, 2016

(I updated the table above): 1.2.0 was correct in all cases, so marking as a stable-to-stable regression.

Also, I checked with -Z orbit=on/off which toggles the behaviour of the first column, leading me to suspect that mir was the fix.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Sep 30, 2016

Minimal version for inspecting LLVM IR or assembly.

Cleaned up assembly:

call g
xor al, -1
and al, 1
mov byte ptr [rbp - 10], al
call g
xor al, -1
mov cl, byte ptr [rbp - 10]
cmp cl, al
je correct

With g returning 0 in al and byte ptr [rbp - 10] being the location of a, that's equivalent to:

mov al, 255
mov cl, 1
cmp cl, al
je correct

So LLVM turns xor i1 %0, true into xor al, -1, but that can't be correct when other instructions operating on i1 values end up leaving the rest of the bits 0 (i.e. and al, 1).

@eddyb eddyb added the A-LLVM label Sep 30, 2016

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Sep 30, 2016

clang doesn't hit this (assembly) because each zext from i1 appears to be an and 1, and it does the compare on i32 (i8 would also work AFAICT).

So we could just make compares use i8 instead, but I don't know what else is broken in LLVM.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Sep 30, 2016

To further explain what MIR "fixed": both following forms also result in "correct" with -g:

fn main() {
    if !g() != !g() {
        println!("wrong");
    } else {
        println!("correct");
    }
}
fn main() {
    let a = !g();
    let b = !g();
    if a != b {
        println!("wrong");
    } else {
        println!("correct");
    }
}

Variables end up holding 1 for true while !g() is 255.

@pnkfelix

This comment has been minimized.

Copy link
Member

pnkfelix commented Sep 30, 2016

we could just make compares use i8 instead

we may want to consider doing this. It seems like llvm bugs for comparing i8 would simply be more likely to be encountered and subsequently fixed in the wild by clang or other projects

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Sep 30, 2016

In that case, these two arguments need to be wrapped in from_immediate calls
(i.e. from_immediate(bcx, lhs) and from_immediate(bcx, rhs), respectively).

@nagisa

This comment has been minimized.

Copy link
Contributor

nagisa commented Sep 30, 2016

@nagisa

This comment has been minimized.

Copy link
Contributor

nagisa commented Sep 30, 2016

Something to note: MIPS, ARM and PowerPC should not be affected on quick visual inspection of the assembly.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Oct 4, 2016

I am in favor of working around this by using i8 or some other means. I think blocking on LLVM for perf fixes is one thing, but we should try to resolve correctness errors locally if we can.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Oct 4, 2016

(Preparing a patch.)

@nikomatsakis nikomatsakis self-assigned this Oct 4, 2016

nikomatsakis added a commit to nikomatsakis/rust that referenced this issue Oct 4, 2016

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

nikomatsakis commented Oct 4, 2016

See PR #36958

@nagisa

This comment has been minimized.

Copy link
Contributor

nagisa commented Oct 4, 2016

For all its worth, it seems like LLVM Trunk does not reproduce, though I didn’t check myself, yet. Backporting the LLVM patch would still make rustc compiled against 3.9/3.8 fail so working around the issue in rustc seems good to me.

bors added a commit that referenced this issue Oct 5, 2016

Auto merge of #36958 - nikomatsakis:issue-36856, r=eddyb
force `i1` booleans to `i8` when comparing

Work around LLVM bug.

cc #36856

r? @eddyb
@brson

This comment has been minimized.

Copy link
Contributor

brson commented Oct 6, 2016

Just waiting for backport

alexcrichton added a commit to alexcrichton/rust that referenced this issue Oct 11, 2016

brson added a commit to brson/rust that referenced this issue Oct 14, 2016

@brson brson closed this Oct 20, 2016

nox added a commit to nox/rust that referenced this issue Apr 21, 2018

Remove hack around comparisons of i1 values (fixes rust-lang#36856)
The regression test still passes without that 2 years old hack. The underlying
LLVM bug has probably been fixed upstream since then.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.