Skip to content

Inspect enum discriminant *after* calling its destructor #24765

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

Merged
merged 6 commits into from
Apr 27, 2015

Conversation

pnkfelix
Copy link
Member

Inspect enum discriminant after calling its destructor

Includes some drive-by cleanup (e.g. changed some field and method names to reflect fill-on-drop; added comments about zero-variant enums being classified as _match::Single).

Probably the most invasive change was the expansion of the maps available_drop_glues and drop_glues to now hold two different kinds of drop glues; there is the (old) normal drop glue, and there is (new) drop-contents glue that jumps straight to dropping the contents of a struct or enum, skipping its destructor.

  • For all types that do not have user-defined Drop implementations, the normal glue is generated as usual (i.e. recursively dropping the fields of the data structure).

    (And this actually is exactly what the newly-added drop-contents glue does as well.)

  • For types that have user-defined Drop implementations, the "normal" drop glue now schedules a cleanup before invoking the Drop::drop method that will call the drop-contents glue after that invocation returns.

Fix #23611.


Is this a breaking change? The prior behavior was totally unsound, and it seems unreasonable that anyone was actually relying on it.

Nonetheless, since there is a user-visible change to the language semantics, I guess I will conservatively mark this as a:

[breaking-change]

(To see an example of what sort of user-visible change this causes, see the comments in the regression test.)

…iant enum.

(This may not be the *best* fix, compared to e.g. returning
`_match::NoBranch` from `trans_switch` on a zero-variant enum. But it
is one of the *simplest* fixes available.)
without invoking the Drop::drop implementation.

This is necessary for dealing with an enum that switches own `self` to
a different variant while running its destructor.

Fix rust-lang#23611.
This addresses to-do in my code, and simplifies this method a lot to boot.

(The necessary enum dispatch has now effectively been shifted entirely
into the scheduled cleanup code for the enum contents.)
@pnkfelix
Copy link
Member Author

r? @nikomatsakis

@rust-highfive rust-highfive assigned nikomatsakis and unassigned Aatch Apr 24, 2015
@rust-highfive
Copy link
Contributor

r? @Aatch

(rust_highfive has picked a reviewer for you, use r? to override)

@ghost
Copy link

ghost commented Apr 24, 2015

I just want to say I'm always amazed by the thoroughness and clarity of your test cases. 👍

@nikomatsakis
Copy link
Contributor

This looks great. I second @jakub-'s commendation for the test case. :)

@nikomatsakis
Copy link
Contributor

@bors r+ b0a4808

@bors
Copy link
Collaborator

bors commented Apr 25, 2015

⌛ Testing commit b0a4808 with merge 7fd975e...

@bors
Copy link
Collaborator

bors commented Apr 25, 2015

💔 Test failed - auto-mac-64-opt

@pnkfelix
Copy link
Member Author

weird! i ran that test a bunch locally...


failures:

---- [run-pass] run-pass/issue-23611-enum-swap-in-drop.rs stdout ----

error: test run failed!
status: exit code: 101
command: x86_64-apple-darwin/test/run-pass/issue-23611-enum-swap-in-drop.stage2-x86_64-apple-darwin 
stdout:
------------------------------------------
                                        created empty log
+-- Make D(test_1, 10000000)
| +-- Make D(g_b_5, 50000001)
| |                                     in g_B(b2b0) from E::drop, b=b4b0
| +-- Drop D(g_b_5, 50000001)
|                                       
| +-- Make D(drop_6, 60000002)
| | +-- Make D(GaspA::drop_2, 20000003)
| | | +-- Make D(f_a_4, 40000004)
| | | |                                 in f_A(a3a0) from GaspA::drop
| | | +-- Drop D(f_a_4, 40000004)
| | |                                   
| | +-- Drop D(GaspA::drop_2, 20000003)
| |                                     
| +-- Drop D(drop_6, 60000002)
|                                       

------------------------------------------
stderr:
------------------------------------------
thread '<main>' panicked at 'assertion failed: `(left == right)` (left: `[50000001, 40000004, 20000003, 60000002]`, right: `[50000001, 50000003, 50000005, 30000004, 10000000, 40000007, 20000006, 60000002]`)', /Users/rustbuild/src/rust-buildbot/slave/auto-mac-64-opt/build/src/test/run-pass/issue-23611-enum-swap-in-drop.rs:27

------------------------------------------

thread '[run-pass] run-pass/issue-23611-enum-swap-in-drop.rs' panicked at 'explicit panic', /Users/rustbuild/src/rust-buildbot/slave/auto-mac-64-opt/build/src/compiletest/runtest.rs:1512



failures:
    [run-pass] run-pass/issue-23611-enum-swap-in-drop.rs

@pnkfelix
Copy link
Member Author

(though I admit I always only ran it in debug builds ... maybe something about the optimized non-debug build is making it think it can skip a bunch of the stuff that I expect it to do? will check locally about non-debug build...)

@pnkfelix
Copy link
Member Author

Okay compiling the test with -O allows me to replicate the behavior we see on the test.

That is sort of scary. (I assume LLVM is finding some way to justify this transformation of the new code I am generating for destructors... maybe I am reading an undefined value from somewhere ...)

@pnkfelix
Copy link
Member Author

Here is a test case that presents problems for both the rust master nightly build and the version from this branch: http://is.gd/2Hxj9P

Here is a transcript comparing with/without -O and nightly versus this-branch. (Thus, there are four runs.) All of the compiles are done with -C debug-assertions, to catch issues where optimizations are leading to corruption of the drop flags that are eventually read from. (At least, that is my current best hypothesis as to what is happening; I have not yet quite dissected the LLVM IR being generated here.)

% rustc --version
rustc 1.1.0-nightly (da623844a 2015-04-25) (built 2015-04-25)

% rustc -C debug-assertions       small.rs  && ./small
enter main
calling foo return-new
E::drop hi1, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
back from foo return-new
calling foo pass-thru
E::drop hi foo, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
back from foo pass-thru
back in main
E::drop hi2, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
E::drop hi foo, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done

% rustc -C debug-assertions -O       small.rs  && ./small
enter main
calling foo return-new
E::drop hi1, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
back from foo return-new
calling foo pass-thru
E::drop hi foo, do_swap: true
Trace/BPT trap: 5

% ./x86_64-apple-darwin/stage2/bin/rustc -C debug-assertions       small.rs  && ./small
enter main
calling foo return-new
E::drop hi1, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
back from foo return-new
calling foo pass-thru
E::drop hi foo, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
back from foo pass-thru
back in main
E::drop hi2, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
E::drop hi foo, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done

% ./x86_64-apple-darwin/stage2/bin/rustc -C debug-assertions -O       small.rs  && ./small
enter main
calling foo return-new
E::drop hi1, do_swap: true
E::drop replaced, do_swap: false
E::drop done
E::drop done
back from foo return-new
calling foo pass-thru
E::drop hi foo, do_swap: true
Trace/BPT trap: 5

% ./x86_64-apple-darwin/stage2/bin/rustc --version
rustc 1.0.0-dev (b0a480875 2015-04-24) (built 2015-04-24)

% 

(If you take out the -C debug-assertions, then you can still see a behavioral difference between the runs; in particular, the drop flag corruption of -O leads to one of the drop calls being skipped.)

@dotdash
Copy link
Contributor

dotdash commented Apr 27, 2015

Seems to be related to lifetime intrinsics. Compiling with them disabled does not expose the problem.

@pnkfelix
Copy link
Member Author

Here is a smaller test for investigation purposes (not suitable for test suite):

#[allow(dead_code)] enum E { A(u64, u32), B(u64, u32), }

impl Drop for E {
    #[inline(never)]
    fn drop(&mut self) {
        let p = self as *const E as *const u64;
        for i in 0.. (((std::mem::size_of::<E>() + 7)/8)) {
            println!("i: {} p[i]: {:x}", i, unsafe { *p.offset(i as isize) });
        }

        unsafe { read(self as *mut E); }
    }
}

fn main() {
    let _e1 = E::B(0xFE113_410C4, 0xAAAA_AAAA);
}

#[inline(never)] unsafe fn read(p: *mut E) { std::ptr::read(p); }

This, when compiled with -C debug-assertions -O, should stack-overflow or infinite loop (due to infinite regress during the drop due to the read call of the &mut self). But instead I currently observe it hitting the int3 injected by the drop-flag consistency checking. This happens during the first invocation of read.

According to what I see so far, we never properly initialize the drop flag for this test when compiling with -O, and the output is:

i: 0 p[i]: 1
i: 1 p[i]: fe113410c4
i: 2 p[i]: 7fffaaaaaaaa
Trace/BPT trap: 5

without the -O flag, the output is:

i: 0 p[i]: 1
i: 1 p[i]: fe113410c4
i: 2 p[i]: 7fd4aaaaaaaa
i: 0 p[i]: 1
i: 1 p[i]: fe113410c4
i: 2 p[i]: 7fd4aaaaaaaa
...

Note that the lines with i: 2 p[i]: 7fd4aaaaaaaa have that d4 embedded within them; that is the correctly initialized drop flag.

@dotdash
Copy link
Contributor

dotdash commented Apr 27, 2015

I think this is due to the way that trans_drop_flag_ptr copies to value of the drop flag. That uses an Rvalue datum, which emits a lifetime end call in its post_store operation, thus marking the drop flag in the struct dead.

@dotdash
Copy link
Contributor

dotdash commented Apr 27, 2015

Yep, that's it, stage1 libstd just finished with the Rvalue changed to a Lvalue (not sure if that's correct, but worked for a test), and both, the playpen and the most small testcase work with that.

That is, scheduled drops are executed in reverse order, so for
correctness, we *schedule* the lifetime end before we schedule the
drop, so that when they are executed, the drop will be executed
*before* the lifetime end.
… which is wrong.

Kudos to dotdash for tracking down this fix.

Presumably the use of `ByRef` was because this value is a reference to
the drop-flag; but an Lvalue will serve just as well for that. dotdash
argues:

> since the drop_flag is in its "final home", Lvalue seems to be the
> correct choice.
@pnkfelix
Copy link
Member Author

@bors r=nikomatsakis 805349a p=1

@bors
Copy link
Collaborator

bors commented Apr 27, 2015

⌛ Testing commit 805349a with merge 48cbb8b...

@bors
Copy link
Collaborator

bors commented Apr 27, 2015

💔 Test failed - auto-linux-64-nopt-t

@pnkfelix
Copy link
Member Author

i don't understand why the above test failed. Here is the relevant (?) portion of the log file:


failures:

---- [run-pass] run-pass/issue-868.rs stdout ----

error: compilation failed!
status: signal: 4
command: x86_64-unknown-linux-gnu/stage2/bin/rustc /home/ubuntu/src/rust-buildbot/slave/auto-linux-64-nopt-t/build/src/test/run-pass/issue-868.rs -L x86_64-unknown-linux-gnu/test/run-pass/ --target=x86_64-unknown-linux-gnu -L x86_64-unknown-linux-gnu/test/run-pass/issue-868.stage2-x86_64-unknown-linux-gnu.run-pass.libaux -C prefer-dynamic -o x86_64-unknown-linux-gnu/test/run-pass/issue-868.stage2-x86_64-unknown-linux-gnu --cfg rtopt --cfg ndebug -L x86_64-unknown-linux-gnu/rt
stdout:
------------------------------------------

------------------------------------------
stderr:
------------------------------------------
/home/ubuntu/src/rust-buildbot/slave/auto-linux-64-nopt-t/build/src/test/run-pass/issue-868.rs:25:13: 25:19 warning: unnecessary parentheses around assigned value, #[warn(unused_parens)] on by default
/home/ubuntu/src/rust-buildbot/slave/auto-linux-64-nopt-t/build/src/test/run-pass/issue-868.rs:25     let _ = (||{});
                                                                                                              ^~~~~~

------------------------------------------

thread '[run-pass] run-pass/issue-868.rs' panicked at 'explicit panic', /home/ubuntu/src/rust-buildbot/slave/auto-linux-64-nopt-t/build/src/compiletest/runtest.rs:1512



failures:
    [run-pass] run-pass/issue-868.rs

test result: FAILED. 2039 passed; 1 failed; 25 ignored; 0 measured

@pnkfelix
Copy link
Member Author

@bors retry

@bors
Copy link
Collaborator

bors commented Apr 27, 2015

⌛ Testing commit 805349a with merge 9c88f3b...

bors added a commit that referenced this pull request Apr 27, 2015
Inspect enum discriminant *after* calling its destructor

Includes some drive-by cleanup (e.g. changed some field and method names to reflect fill-on-drop; added comments about zero-variant enums being classified as `_match::Single`).

Probably the most invasive change was the expansion of the maps `available_drop_glues` and `drop_glues` to now hold two different kinds of drop glues; there is the (old) normal drop glue, and there is (new) drop-contents glue that jumps straight to dropping the contents of a struct or enum, skipping its destructor.

 * For all types that do not have user-defined Drop implementations, the normal glue is generated as usual (i.e. recursively dropping the fields of the data structure).

  (And this actually is exactly what the newly-added drop-contents glue does as well.)

 * For types that have user-defined Drop implementations, the "normal" drop glue now schedules a cleanup before invoking the `Drop::drop` method that will call the drop-contents glue after that invocation returns.

Fix #23611.

----

Is this a breaking change?  The prior behavior was totally unsound, and it seems unreasonable that anyone was actually relying on it.

Nonetheless, since there is a user-visible change to the language semantics, I guess I will conservatively mark this as a:

[breaking-change]

(To see an example of what sort of user-visible change this causes, see the comments in the regression test.)
@bors bors merged commit 805349a into rust-lang:master Apr 27, 2015
@pnkfelix
Copy link
Member Author

triage: beta-nominated

Just ensuring discussion; I am not a strong advocate for cherry-pick )

@rust-highfive rust-highfive added the beta-nominated Nominated for backporting to the compiler in the beta channel. label Apr 28, 2015
@pnkfelix
Copy link
Member Author

oh man, i had so many opportunities to land that name-change clean up that I had promised. Let me do that now.

alexcrichton added a commit to alexcrichton/rust that referenced this pull request Apr 29, 2015
rename `schedule_drop_{enum,adt}_contents`.

addresses review nit from rust-lang#24765 (it was my mistake for not doing this earlier before it landed).
@pnkfelix
Copy link
Member Author

not accepted for beta backport.

@pnkfelix pnkfelix removed the beta-nominated Nominated for backporting to the compiler in the beta channel. label Apr 30, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Member destructor is always executed even if self is replaced in Drop
6 participants