Skip to content

Don't store locals in generators that are immediately overwritten with the resume argument #69716

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 4 commits into from
Mar 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 29 additions & 21 deletions src/librustc_mir/dataflow/generic/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::borrow::Borrow;

use rustc::mir::{self, BasicBlock, Location};
use rustc::mir::{self, BasicBlock, Location, TerminatorKind};
use rustc_index::bit_set::BitSet;

use super::{Analysis, Results};
Expand All @@ -29,14 +29,14 @@ where

pos: CursorPosition,

/// When this flag is set, the cursor is pointing at a `Call` terminator whose call return
/// effect has been applied to `state`.
/// When this flag is set, the cursor is pointing at a `Call` or `Yield` terminator whose call
/// return or resume effect has been applied to `state`.
///
/// This flag helps to ensure that multiple calls to `seek_after_assume_call_returns` with the
/// This flag helps to ensure that multiple calls to `seek_after_assume_success` with the
/// same target will result in exactly one invocation of `apply_call_return_effect`. It is
/// sufficient to clear this only in `seek_to_block_start`, since seeking away from a
/// terminator will always require a cursor reset.
call_return_effect_applied: bool,
success_effect_applied: bool,
}

impl<'mir, 'tcx, A, R> ResultsCursor<'mir, 'tcx, A, R>
Expand All @@ -50,7 +50,7 @@ where
body,
pos: CursorPosition::BlockStart(mir::START_BLOCK),
state: results.borrow().entry_sets[mir::START_BLOCK].clone(),
call_return_effect_applied: false,
success_effect_applied: false,
results,
}
}
Expand All @@ -76,14 +76,14 @@ where
pub fn seek_to_block_start(&mut self, block: BasicBlock) {
self.state.overwrite(&self.results.borrow().entry_sets[block]);
self.pos = CursorPosition::BlockStart(block);
self.call_return_effect_applied = false;
self.success_effect_applied = false;
}

/// Advances the cursor to hold all effects up to and including to the "before" effect of the
/// statement (or terminator) at the given location.
///
/// If you wish to observe the full effect of a statement or terminator, not just the "before"
/// effect, use `seek_after` or `seek_after_assume_call_returns`.
/// effect, use `seek_after` or `seek_after_assume_success`.
pub fn seek_before(&mut self, target: Location) {
assert!(target <= self.body.terminator_loc(target.block));
self.seek_(target, false);
Expand All @@ -93,15 +93,15 @@ where
/// terminators) up to and including the `target`.
///
/// If the `target` is a `Call` terminator, any call return effect for that terminator will
/// **not** be observed. Use `seek_after_assume_call_returns` if you wish to observe the call
/// **not** be observed. Use `seek_after_assume_success` if you wish to observe the call
/// return effect.
pub fn seek_after(&mut self, target: Location) {
assert!(target <= self.body.terminator_loc(target.block));

// If we have already applied the call return effect, we are currently pointing at a `Call`
// terminator. Unconditionally reset the dataflow cursor, since there is no way to "undo"
// the call return effect.
if self.call_return_effect_applied {
if self.success_effect_applied {
self.seek_to_block_start(target.block);
}

Expand All @@ -111,25 +111,25 @@ where
/// Advances the cursor to hold all effects up to and including of the statement (or
/// terminator) at the given location.
///
/// If the `target` is a `Call` terminator, any call return effect for that terminator will
/// be observed. Use `seek_after` if you do **not** wish to observe the call return effect.
pub fn seek_after_assume_call_returns(&mut self, target: Location) {
/// If the `target` is a `Call` or `Yield` terminator, any call return or resume effect for that
/// terminator will be observed. Use `seek_after` if you do **not** wish to observe the
/// "success" effect.
pub fn seek_after_assume_success(&mut self, target: Location) {
let terminator_loc = self.body.terminator_loc(target.block);
assert!(target.statement_index <= terminator_loc.statement_index);

self.seek_(target, true);

if target != terminator_loc {
if target != terminator_loc || self.success_effect_applied {
return;
}

// Apply the effect of the "success" path of the terminator.

self.success_effect_applied = true;
let terminator = self.body.basic_blocks()[target.block].terminator();
if let mir::TerminatorKind::Call {
destination: Some((return_place, _)), func, args, ..
} = &terminator.kind
{
if !self.call_return_effect_applied {
self.call_return_effect_applied = true;
match &terminator.kind {
TerminatorKind::Call { destination: Some((return_place, _)), func, args, .. } => {
self.results.borrow().analysis.apply_call_return_effect(
&mut self.state,
target.block,
Expand All @@ -138,6 +138,14 @@ where
return_place,
);
}
TerminatorKind::Yield { resume, resume_arg, .. } => {
self.results.borrow().analysis.apply_yield_resume_effect(
&mut self.state,
*resume,
resume_arg,
);
}
_ => {}
}
}

Expand Down Expand Up @@ -172,7 +180,7 @@ where
self.seek_to_block_start(target.block)
}

// N.B., `call_return_effect_applied` is checked in `seek_after`, not here.
// N.B., `success_effect_applied` is checked in `seek_after`, not here.
_ => (),
}

Expand Down
9 changes: 6 additions & 3 deletions src/librustc_mir/dataflow/generic/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,18 @@ where

Goto { target }
| Assert { target, cleanup: None, .. }
| Yield { resume: target, drop: None, .. }
| Drop { target, location: _, unwind: None }
| DropAndReplace { target, value: _, location: _, unwind: None } => {
self.propagate_bits_into_entry_set_for(in_out, target, dirty_list)
}

Yield { resume: target, drop: Some(drop), .. } => {
Yield { resume: target, drop, resume_arg, .. } => {
if let Some(drop) = drop {
self.propagate_bits_into_entry_set_for(in_out, drop, dirty_list);
}

self.analysis.apply_yield_resume_effect(in_out, target, &resume_arg);
self.propagate_bits_into_entry_set_for(in_out, target, dirty_list);
self.propagate_bits_into_entry_set_for(in_out, drop, dirty_list);
}

Assert { target, cleanup: Some(unwind), .. }
Expand Down
2 changes: 1 addition & 1 deletion src/librustc_mir/dataflow/generic/graphviz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ where
)?;

let state_on_unwind = this.results.get().clone();
this.results.seek_after_assume_call_returns(terminator_loc);
this.results.seek_after_assume_success(terminator_loc);
write_diff(w, this.results.analysis(), &state_on_unwind, this.results.get())?;

write!(w, "</td>")
Expand Down
32 changes: 32 additions & 0 deletions src/librustc_mir/dataflow/generic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,20 @@ pub trait Analysis<'tcx>: AnalysisDomain<'tcx> {
return_place: &mir::Place<'tcx>,
);

/// Updates the current dataflow state with the effect of resuming from a `Yield` terminator.
///
/// This is similar to `apply_call_return_effect` in that it only takes place after the
/// generator is resumed, not when it is dropped.
///
/// By default, no effects happen.
fn apply_yield_resume_effect(
&self,
_state: &mut BitSet<Self::Idx>,
_resume_block: BasicBlock,
_resume_place: &mir::Place<'tcx>,
) {
}

/// Updates the current dataflow state with the effect of taking a particular branch in a
/// `SwitchInt` terminator.
///
Expand Down Expand Up @@ -284,6 +298,15 @@ pub trait GenKillAnalysis<'tcx>: Analysis<'tcx> {
return_place: &mir::Place<'tcx>,
);

/// See `Analysis::apply_yield_resume_effect`.
fn yield_resume_effect(
&self,
_trans: &mut BitSet<Self::Idx>,
_resume_block: BasicBlock,
_resume_place: &mir::Place<'tcx>,
) {
}

/// See `Analysis::apply_discriminant_switch_effect`.
fn discriminant_switch_effect(
&self,
Expand Down Expand Up @@ -347,6 +370,15 @@ where
self.call_return_effect(state, block, func, args, return_place);
}

fn apply_yield_resume_effect(
&self,
state: &mut BitSet<Self::Idx>,
resume_block: BasicBlock,
resume_place: &mir::Place<'tcx>,
) {
self.yield_resume_effect(state, resume_block, resume_place);
}

fn apply_discriminant_switch_effect(
&self,
state: &mut BitSet<Self::Idx>,
Expand Down
4 changes: 2 additions & 2 deletions src/librustc_mir/dataflow/generic/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ fn cursor_seek() {

cursor.seek_after(call_terminator_loc);
assert!(!cursor.get().contains(call_return_effect));
cursor.seek_after_assume_call_returns(call_terminator_loc);
cursor.seek_after_assume_success(call_terminator_loc);
assert!(cursor.get().contains(call_return_effect));

let every_target = || {
Expand All @@ -310,7 +310,7 @@ fn cursor_seek() {
BlockStart(block) => cursor.seek_to_block_start(block),
Before(loc) => cursor.seek_before(loc),
After(loc) => cursor.seek_after(loc),
AfterAssumeCallReturns(loc) => cursor.seek_after_assume_call_returns(loc),
AfterAssumeCallReturns(loc) => cursor.seek_after_assume_success(loc),
}

assert_eq!(cursor.get(), &cursor.analysis().expected_state_at_target(targ));
Expand Down
18 changes: 16 additions & 2 deletions src/librustc_mir/dataflow/impls/storage_liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,16 @@ impl<'mir, 'tcx> dataflow::GenKillAnalysis<'tcx> for MaybeRequiresStorage<'mir,
self.borrowed_locals.borrow().analysis().terminator_effect(trans, terminator, loc);

match &terminator.kind {
TerminatorKind::Call { destination: Some((place, _)), .. }
| TerminatorKind::Yield { resume_arg: place, .. } => {
TerminatorKind::Call { destination: Some((place, _)), .. } => {
trans.gen(place.local);
}

// Note that we do *not* gen the `resume_arg` of `Yield` terminators. The reason for
// that is that a `yield` will return from the function, and `resume_arg` is written
// only when the generator is later resumed. Unlike `Call`, this doesn't require the
// place to have storage *before* the yield, only after.
TerminatorKind::Yield { .. } => {}

// Nothing to do for these. Match exhaustively so this fails to compile when new
// variants are added.
TerminatorKind::Call { destination: None, .. }
Expand Down Expand Up @@ -230,6 +235,15 @@ impl<'mir, 'tcx> dataflow::GenKillAnalysis<'tcx> for MaybeRequiresStorage<'mir,
) {
trans.gen(return_place.local);
}

fn yield_resume_effect(
&self,
trans: &mut BitSet<Self::Idx>,
_resume_block: BasicBlock,
resume_place: &mir::Place<'tcx>,
) {
trans.gen(resume_place.local);
}
}

impl<'mir, 'tcx> MaybeRequiresStorage<'mir, 'tcx> {
Expand Down
4 changes: 2 additions & 2 deletions src/librustc_mir/transform/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ fn locals_live_across_suspend_points(

for (block, data) in body.basic_blocks().iter_enumerated() {
if let TerminatorKind::Yield { .. } = data.terminator().kind {
let loc = Location { block: block, statement_index: data.statements.len() };
let loc = Location { block, statement_index: data.statements.len() };

if !movable {
// The `liveness` variable contains the liveness of MIR locals ignoring borrows.
Expand Down Expand Up @@ -539,7 +539,7 @@ fn locals_live_across_suspend_points(
let mut live_locals_here = storage_required;
live_locals_here.intersect(&liveness.outs[block]);

// The generator argument is ignored
// The generator argument is ignored.
live_locals_here.remove(self_arg());

debug!("loc = {:?}, live_locals_here = {:?}", loc, live_locals_here);
Expand Down
6 changes: 5 additions & 1 deletion src/test/ui/generator/issue-69039.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

use std::ops::{Generator, GeneratorState};

fn mkstr(my_name: String, my_mood: String) -> String {
format!("{} is {}", my_name.trim(), my_mood.trim())
}

fn my_scenario() -> impl Generator<String, Yield = &'static str, Return = String> {
|_arg: String| {
let my_name = yield "What is your name?";
let my_mood = yield "How are you feeling?";
format!("{} is {}", my_name.trim(), my_mood.trim())
mkstr(my_name, my_mood)
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/test/ui/generator/resume-arg-size.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#![feature(generators)]

// run-pass

use std::mem::size_of_val;

fn main() {
// Generator taking a `Copy`able resume arg.
let gen_copy = |mut x: usize| {
loop {
drop(x);
x = yield;
}
};

// Generator taking a non-`Copy` resume arg.
let gen_move = |mut x: Box<usize>| {
loop {
drop(x);
x = yield;
}
};

// Neither of these generators have the resume arg live across the `yield`, so they should be
// 4 Bytes in size (only storing the discriminant)
assert_eq!(size_of_val(&gen_copy), 4);
assert_eq!(size_of_val(&gen_move), 4);
}