rustc_mir_transform/dest_prop.rs
1//! Propagates assignment destinations backwards in the CFG to eliminate redundant assignments.
2//!
3//! # Motivation
4//!
5//! MIR building can insert a lot of redundant copies, and Rust code in general often tends to move
6//! values around a lot. The result is a lot of assignments of the form `dest = {move} src;` in MIR.
7//! MIR building for constants in particular tends to create additional locals that are only used
8//! inside a single block to shuffle a value around unnecessarily.
9//!
10//! LLVM by itself is not good enough at eliminating these redundant copies (eg. see
11//! <https://github.com/rust-lang/rust/issues/32966>), so this leaves some performance on the table
12//! that we can regain by implementing an optimization for removing these assign statements in rustc
13//! itself. When this optimization runs fast enough, it can also speed up the constant evaluation
14//! and code generation phases of rustc due to the reduced number of statements and locals.
15//!
16//! # The Optimization
17//!
18//! Conceptually, this optimization is "destination propagation". It is similar to the Named Return
19//! Value Optimization, or NRVO, known from the C++ world, except that it isn't limited to return
20//! values or the return place `_0`. On a very high level, independent of the actual implementation
21//! details, it does the following:
22//!
23//! 1) Identify `dest = src;` statements with values for `dest` and `src` whose storage can soundly
24//! be merged.
25//! 2) Replace all mentions of `src` with `dest` ("unifying" them and propagating the destination
26//! backwards).
27//! 3) Delete the `dest = src;` statement (by making it a `nop`).
28//!
29//! Step 1) is by far the hardest, so it is explained in more detail below.
30//!
31//! ## Soundness
32//!
33//! We have a pair of places `p` and `q`, whose memory we would like to merge. In order for this to
34//! be sound, we need to check a number of conditions:
35//!
36//! * `p` and `q` must both be *constant* - it does not make much sense to talk about merging them
37//! if they do not consistently refer to the same place in memory. This is satisfied if they do
38//! not contain any indirection through a pointer or any indexing projections.
39//!
40//! * `p` and `q` must have the **same type**. If we replace a local with a subtype or supertype,
41//! we may end up with a different vtable for that local. See the `subtyping-impacts-selection`
42//! tests for an example where that causes issues.
43//!
44//! * We need to make sure that the goal of "merging the memory" is actually structurally possible
45//! in MIR. For example, even if all the other conditions are satisfied, there is no way to
46//! "merge" `_5.foo` and `_6.bar`. For now, we ensure this by requiring that both `p` and `q` are
47//! locals with no further projections. Future iterations of this pass should improve on this.
48//!
49//! * Finally, we want `p` and `q` to use the same memory - however, we still need to make sure that
50//! each of them has enough "ownership" of that memory to continue "doing its job." More
51//! precisely, what we will check is that whenever the program performs a write to `p`, then it
52//! does not currently care about what the value in `q` is (and vice versa). We formalize the
53//! notion of "does not care what the value in `q` is" by checking the *liveness* of `q`.
54//!
55//! Because of the difficulty of computing liveness of places that have their address taken, we do
56//! not even attempt to do it. Any places that are in a local that has its address taken is
57//! excluded from the optimization.
58//!
59//! The first two conditions are simple structural requirements on the `Assign` statements that can
60//! be trivially checked. The third requirement however is more difficult and costly to check.
61//!
62//! ## Current implementation
63//!
64//! The current implementation relies on live range computation to check for conflicts. We only
65//! allow to merge locals that have disjoint live ranges. The live range are defined with
66//! half-statement granularity, so as to make all writes be live for at least a half statement.
67//!
68//! ## Future Improvements
69//!
70//! There are a number of ways in which this pass could be improved in the future:
71//!
72//! * Merging storage liveness ranges instead of removing storage statements completely. This may
73//! improve stack usage.
74//!
75//! * Allow merging locals into places with projections, eg `_5` into `_6.foo`.
76//!
77//! * Liveness analysis with more precision than whole locals at a time. The smaller benefit of this
78//! is that it would allow us to dest prop at "sub-local" levels in some cases. The bigger benefit
79//! of this is that such liveness analysis can report more accurate results about whole locals at
80//! a time. For example, consider:
81//!
82//! ```ignore (syntax-highlighting-only)
83//! _1 = u;
84//! // unrelated code
85//! _1.f1 = v;
86//! _2 = _1.f1;
87//! ```
88//!
89//! Because the current analysis only thinks in terms of locals, it does not have enough
90//! information to report that `_1` is dead in the "unrelated code" section.
91//!
92//! * Liveness analysis enabled by alias analysis. This would allow us to not just bail on locals
93//! that ever have their address taken. Of course that requires actually having alias analysis
94//! (and a model to build it on), so this might be a bit of a ways off.
95//!
96//! * Various perf improvements. There are a bunch of comments in here marked `PERF` with ideas for
97//! how to do things more efficiently. However, the complexity of the pass as a whole should be
98//! kept in mind.
99//!
100//! ## Previous Work
101//!
102//! A [previous attempt][attempt 1] at implementing an optimization like this turned out to be a
103//! significant regression in compiler performance. Fixing the regressions introduced a lot of
104//! undesirable complexity to the implementation.
105//!
106//! A [subsequent approach][attempt 2] tried to avoid the costly computation by limiting itself to
107//! acyclic CFGs, but still turned out to be far too costly to run due to suboptimal performance
108//! within individual basic blocks, requiring a walk across the entire block for every assignment
109//! found within the block. For the `tuple-stress` benchmark, which has 458745 statements in a
110//! single block, this proved to be far too costly.
111//!
112//! [Another approach after that][attempt 3] was much closer to correct, but had some soundness
113//! issues - it was failing to consider stores outside live ranges, and failed to uphold some of the
114//! requirements that MIR has for non-overlapping places within statements. However, it also had
115//! performance issues caused by `O(l² * s)` runtime, where `l` is the number of locals and `s` is
116//! the number of statements and terminators.
117//!
118//! Since the first attempt at this, the compiler has improved dramatically, and new analysis
119//! frameworks have been added that should make this approach viable without requiring a limited
120//! approach that only works for some classes of CFGs:
121//! - rustc now has a powerful dataflow analysis framework that can handle forwards and backwards
122//! analyses efficiently.
123//! - Layout optimizations for coroutines have been added to improve code generation for
124//! async/await, which are very similar in spirit to what this optimization does.
125//!
126//! [The next approach][attempt 4] computes a conflict matrix between locals by forbidding merging
127//! locals with competing writes or with one write while the other is live.
128//!
129//! ## Pre/Post Optimization
130//!
131//! It is recommended to run `SimplifyCfg` and then `SimplifyLocals` some time after this pass, as
132//! it replaces the eliminated assign statements with `nop`s and leaves unused locals behind.
133//!
134//! [liveness]: https://en.wikipedia.org/wiki/Live_variable_analysis
135//! [attempt 1]: https://github.com/rust-lang/rust/pull/47954
136//! [attempt 2]: https://github.com/rust-lang/rust/pull/71003
137//! [attempt 3]: https://github.com/rust-lang/rust/pull/72632
138//! [attempt 4]: https://github.com/rust-lang/rust/pull/96451
139
140use rustc_data_structures::union_find::UnionFind;
141use rustc_index::bit_set::DenseBitSet;
142use rustc_index::interval::SparseIntervalMatrix;
143use rustc_index::{IndexVec, newtype_index};
144use rustc_middle::mir::visit::{MutVisitor, PlaceContext, Visitor};
145use rustc_middle::mir::*;
146use rustc_middle::ty::TyCtxt;
147use rustc_mir_dataflow::impls::{DefUse, MaybeLiveLocals};
148use rustc_mir_dataflow::points::DenseLocationMap;
149use rustc_mir_dataflow::{Analysis, Results};
150use tracing::{debug, trace};
151
152pub(super) struct DestinationPropagation;
153
154impl<'tcx> crate::MirPass<'tcx> for DestinationPropagation {
155 fn is_enabled(&self, sess: &rustc_session::Session) -> bool {
156 // For now, only run at MIR opt level 3. Two things need to be changed before this can be
157 // turned on by default:
158 // 1. Because of the overeager removal of storage statements, this can cause stack space
159 // regressions. This opt is not the place to fix this though, it's a more general
160 // problem in MIR.
161 // 2. Despite being an overall perf improvement, this still causes a 30% regression in
162 // keccak. We can temporarily fix this by bounding function size, but in the long term
163 // we should fix this by being smarter about invalidating analysis results.
164 sess.mir_opt_level() >= 3
165 }
166
167 #[tracing::instrument(level = "trace", skip(self, tcx, body))]
168 fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
169 let def_id = body.source.def_id();
170 trace!(?def_id);
171
172 let borrowed = rustc_mir_dataflow::impls::borrowed_locals(body);
173
174 let candidates = Candidates::find(body, &borrowed);
175 trace!(?candidates);
176 if candidates.c.is_empty() {
177 return;
178 }
179
180 let live = MaybeLiveLocals.iterate_to_fixpoint(tcx, body, Some("MaybeLiveLocals-DestProp"));
181
182 let points = DenseLocationMap::new(body);
183 let mut relevant = RelevantLocals::compute(&candidates, body.local_decls.len());
184 let mut live = save_as_intervals(&points, body, &relevant, live.results);
185
186 dest_prop_mir_dump(tcx, body, &points, &live, &relevant);
187
188 let mut merged_locals = DenseBitSet::new_empty(body.local_decls.len());
189
190 for (src, dst) in candidates.c.into_iter() {
191 trace!(?src, ?dst);
192
193 let Some(mut src) = relevant.find(src) else { continue };
194 let Some(mut dst) = relevant.find(dst) else { continue };
195 if src == dst {
196 continue;
197 }
198
199 let Some(src_live_ranges) = live.row(src) else { continue };
200 let Some(dst_live_ranges) = live.row(dst) else { continue };
201 trace!(?src, ?src_live_ranges);
202 trace!(?dst, ?dst_live_ranges);
203
204 if src_live_ranges.disjoint(dst_live_ranges) {
205 // We want to replace `src` by `dst`.
206 let mut orig_src = relevant.original[src];
207 let mut orig_dst = relevant.original[dst];
208
209 // The return place and function arguments are required and cannot be renamed.
210 // This check cannot be made during candidate collection, as we may want to
211 // unify the same non-required local with several required locals.
212 match (is_local_required(orig_src, body), is_local_required(orig_dst, body)) {
213 // Renaming `src` is ok.
214 (false, _) => {}
215 // Renaming `src` is wrong, but renaming `dst` is ok.
216 (true, false) => {
217 std::mem::swap(&mut src, &mut dst);
218 std::mem::swap(&mut orig_src, &mut orig_dst);
219 }
220 // Neither local can be renamed, so skip this case.
221 (true, true) => continue,
222 }
223
224 trace!(?src, ?dst, "merge");
225 merged_locals.insert(orig_src);
226 merged_locals.insert(orig_dst);
227
228 // Replace `src` by `dst`.
229 let head = relevant.union(src, dst);
230 live.union_rows(/* read */ src, /* write */ head);
231 live.union_rows(/* read */ dst, /* write */ head);
232 }
233 }
234 trace!(?merged_locals);
235 trace!(?relevant.renames);
236
237 if merged_locals.is_empty() {
238 return;
239 }
240
241 apply_merges(body, tcx, relevant, merged_locals);
242 }
243
244 fn is_required(&self) -> bool {
245 false
246 }
247}
248
249//////////////////////////////////////////////////////////
250// Merging
251//
252// Applies the actual optimization
253
254fn apply_merges<'tcx>(
255 body: &mut Body<'tcx>,
256 tcx: TyCtxt<'tcx>,
257 relevant: RelevantLocals,
258 merged_locals: DenseBitSet<Local>,
259) {
260 let mut merger = Merger { tcx, relevant, merged_locals };
261 merger.visit_body_preserves_cfg(body);
262}
263
264struct Merger<'tcx> {
265 tcx: TyCtxt<'tcx>,
266 relevant: RelevantLocals,
267 merged_locals: DenseBitSet<Local>,
268}
269
270impl<'tcx> MutVisitor<'tcx> for Merger<'tcx> {
271 fn tcx(&self) -> TyCtxt<'tcx> {
272 self.tcx
273 }
274
275 fn visit_local(&mut self, local: &mut Local, _: PlaceContext, _location: Location) {
276 if let Some(relevant) = self.relevant.find(*local) {
277 *local = self.relevant.original[relevant];
278 }
279 }
280
281 fn visit_statement(&mut self, statement: &mut Statement<'tcx>, location: Location) {
282 match &statement.kind {
283 // FIXME: Don't delete storage statements, but "merge" the storage ranges instead.
284 StatementKind::StorageDead(local) | StatementKind::StorageLive(local)
285 if self.merged_locals.contains(*local) =>
286 {
287 statement.make_nop();
288 return;
289 }
290 _ => (),
291 };
292 self.super_statement(statement, location);
293 match &statement.kind {
294 StatementKind::Assign(box (dest, rvalue)) => {
295 match rvalue {
296 Rvalue::CopyForDeref(place)
297 | Rvalue::Use(Operand::Copy(place) | Operand::Move(place)) => {
298 // These might've been turned into self-assignments by the replacement
299 // (this includes the original statement we wanted to eliminate).
300 if dest == place {
301 debug!("{:?} turned into self-assignment, deleting", location);
302 statement.make_nop();
303 }
304 }
305 _ => {}
306 }
307 }
308
309 _ => {}
310 }
311 }
312}
313
314//////////////////////////////////////////////////////////
315// Relevant locals
316//
317// Small utility to reduce size of the conflict matrix by only considering locals that appear in
318// the candidates
319
320newtype_index! {
321 /// Represent a subset of locals which appear in candidates.
322 struct RelevantLocal {}
323}
324
325#[derive(Debug)]
326struct RelevantLocals {
327 original: IndexVec<RelevantLocal, Local>,
328 shrink: IndexVec<Local, Option<RelevantLocal>>,
329 renames: UnionFind<RelevantLocal>,
330}
331
332impl RelevantLocals {
333 #[tracing::instrument(level = "trace", skip(candidates, num_locals), ret)]
334 fn compute(candidates: &Candidates, num_locals: usize) -> RelevantLocals {
335 let mut original = IndexVec::with_capacity(candidates.c.len());
336 let mut shrink = IndexVec::from_elem_n(None, num_locals);
337
338 // Mark a local as relevant and record it into the maps.
339 let mut declare = |local| {
340 shrink.get_or_insert_with(local, || original.push(local));
341 };
342
343 for &(src, dest) in candidates.c.iter() {
344 declare(src);
345 declare(dest)
346 }
347
348 let renames = UnionFind::new(original.len());
349 RelevantLocals { original, shrink, renames }
350 }
351
352 fn find(&mut self, src: Local) -> Option<RelevantLocal> {
353 let src = self.shrink[src]?;
354 let src = self.renames.find(src);
355 Some(src)
356 }
357
358 fn union(&mut self, lhs: RelevantLocal, rhs: RelevantLocal) -> RelevantLocal {
359 let head = self.renames.unify(lhs, rhs);
360 // We need to ensure we keep the original local of the RHS, as it may be a required local.
361 self.original[head] = self.original[rhs];
362 head
363 }
364}
365
366/////////////////////////////////////////////////////
367// Candidate accumulation
368
369#[derive(Debug, Default)]
370struct Candidates {
371 /// The set of candidates we are considering in this optimization.
372 ///
373 /// Whether a place ends up in the key or the value does not correspond to whether it appears as
374 /// the lhs or rhs of any assignment. As a matter of fact, the places in here might never appear
375 /// in an assignment at all. This happens because if we see an assignment like this:
376 ///
377 /// ```ignore (syntax-highlighting-only)
378 /// _1.0 = _2.0
379 /// ```
380 ///
381 /// We will still report that we would like to merge `_1` and `_2` in an attempt to allow us to
382 /// remove that assignment.
383 c: Vec<(Local, Local)>,
384}
385
386// We first implement some utility functions which we will expose removing candidates according to
387// different needs. Throughout the liveness filtering, the `candidates` are only ever accessed
388// through these methods, and not directly.
389impl Candidates {
390 /// Collects the candidates for merging.
391 ///
392 /// This is responsible for enforcing the first and third bullet point.
393 fn find(body: &Body<'_>, borrowed: &DenseBitSet<Local>) -> Candidates {
394 let mut visitor = FindAssignments { body, candidates: Default::default(), borrowed };
395 visitor.visit_body(body);
396
397 Candidates { c: visitor.candidates }
398 }
399}
400
401struct FindAssignments<'a, 'tcx> {
402 body: &'a Body<'tcx>,
403 candidates: Vec<(Local, Local)>,
404 borrowed: &'a DenseBitSet<Local>,
405}
406
407impl<'tcx> Visitor<'tcx> for FindAssignments<'_, 'tcx> {
408 fn visit_statement(&mut self, statement: &Statement<'tcx>, _: Location) {
409 if let StatementKind::Assign(box (
410 lhs,
411 Rvalue::CopyForDeref(rhs) | Rvalue::Use(Operand::Copy(rhs) | Operand::Move(rhs)),
412 )) = &statement.kind
413 && let Some(src) = lhs.as_local()
414 && let Some(dest) = rhs.as_local()
415 {
416 // As described at the top of the file, we do not go near things that have
417 // their address taken.
418 if self.borrowed.contains(src) || self.borrowed.contains(dest) {
419 return;
420 }
421
422 // As described at the top of this file, we do not touch locals which have
423 // different types.
424 let src_ty = self.body.local_decls()[src].ty;
425 let dest_ty = self.body.local_decls()[dest].ty;
426 if src_ty != dest_ty {
427 // FIXME(#112651): This can be removed afterwards. Also update the module description.
428 trace!("skipped `{src:?} = {dest:?}` due to subtyping: {src_ty} != {dest_ty}");
429 return;
430 }
431
432 // We may insert duplicates here, but that's fine
433 self.candidates.push((src, dest));
434 }
435 }
436}
437
438/// Some locals are part of the function's interface and can not be removed.
439///
440/// Note that these locals *can* still be merged with non-required locals by removing that other
441/// local.
442fn is_local_required(local: Local, body: &Body<'_>) -> bool {
443 match body.local_kind(local) {
444 LocalKind::Arg | LocalKind::ReturnPointer => true,
445 LocalKind::Temp => false,
446 }
447}
448
449/////////////////////////////////////////////////////////
450// MIR Dump
451
452fn dest_prop_mir_dump<'tcx>(
453 tcx: TyCtxt<'tcx>,
454 body: &Body<'tcx>,
455 points: &DenseLocationMap,
456 live: &SparseIntervalMatrix<RelevantLocal, TwoStepIndex>,
457 relevant: &RelevantLocals,
458) {
459 let locals_live_at = |location| {
460 live.rows()
461 .filter(|&r| live.contains(r, location))
462 .map(|rl| relevant.original[rl])
463 .collect::<Vec<_>>()
464 };
465
466 if let Some(dumper) = MirDumper::new(tcx, "DestinationPropagation-dataflow", body) {
467 let extra_data = &|pass_where, w: &mut dyn std::io::Write| {
468 if let PassWhere::BeforeLocation(loc) = pass_where {
469 let location = TwoStepIndex::new(points, loc, Effect::Before);
470 let live = locals_live_at(location);
471 writeln!(w, " // before: {:?} => {:?}", location, live)?;
472 }
473 if let PassWhere::AfterLocation(loc) = pass_where {
474 let location = TwoStepIndex::new(points, loc, Effect::After);
475 let live = locals_live_at(location);
476 writeln!(w, " // after: {:?} => {:?}", location, live)?;
477 }
478 Ok(())
479 };
480
481 dumper.set_extra_data(extra_data).dump_mir(body)
482 }
483}
484
485#[derive(Copy, Clone, Debug)]
486enum Effect {
487 Before,
488 After,
489}
490
491rustc_index::newtype_index! {
492 /// A reversed `PointIndex` but with the lower bit encoding early/late inside the statement.
493 /// The reversed order allows to use the more efficient `IntervalSet::append` method while we
494 /// iterate on the statements in reverse order.
495 #[orderable]
496 #[debug_format = "TwoStepIndex({})"]
497 struct TwoStepIndex {}
498}
499
500impl TwoStepIndex {
501 fn new(elements: &DenseLocationMap, location: Location, effect: Effect) -> TwoStepIndex {
502 let point = elements.point_from_location(location);
503 let effect = match effect {
504 Effect::Before => 0,
505 Effect::After => 1,
506 };
507 let max_index = 2 * elements.num_points() as u32 - 1;
508 let index = 2 * point.as_u32() + (effect as u32);
509 // Reverse the indexing to use more efficient `IntervalSet::append`.
510 TwoStepIndex::from_u32(max_index - index)
511 }
512}
513
514struct VisitPlacesWith<F>(F);
515
516impl<'tcx, F> Visitor<'tcx> for VisitPlacesWith<F>
517where
518 F: FnMut(Place<'tcx>, PlaceContext),
519{
520 fn visit_local(&mut self, local: Local, ctxt: PlaceContext, _: Location) {
521 (self.0)(local.into(), ctxt);
522 }
523
524 fn visit_place(&mut self, place: &Place<'tcx>, ctxt: PlaceContext, location: Location) {
525 (self.0)(*place, ctxt);
526 self.visit_projection(place.as_ref(), ctxt, location);
527 }
528}
529
530/// Add points depending on the result of the given dataflow analysis.
531fn save_as_intervals<'tcx>(
532 elements: &DenseLocationMap,
533 body: &Body<'tcx>,
534 relevant: &RelevantLocals,
535 results: Results<DenseBitSet<Local>>,
536) -> SparseIntervalMatrix<RelevantLocal, TwoStepIndex> {
537 let mut values = SparseIntervalMatrix::new(2 * elements.num_points());
538 let mut state = MaybeLiveLocals.bottom_value(body);
539 let reachable_blocks = traversal::reachable_as_bitset(body);
540
541 let two_step_loc = |location, effect| TwoStepIndex::new(elements, location, effect);
542 let append_at =
543 |values: &mut SparseIntervalMatrix<_, _>, state: &DenseBitSet<Local>, twostep| {
544 for (relevant, &original) in relevant.original.iter_enumerated() {
545 if state.contains(original) {
546 values.append(relevant, twostep);
547 }
548 }
549 };
550
551 // Iterate blocks in decreasing order, to visit locations in decreasing order. This
552 // allows to use the more efficient `append` method to interval sets.
553 for block in body.basic_blocks.indices().rev() {
554 if !reachable_blocks.contains(block) {
555 continue;
556 }
557
558 state.clone_from(&results[block]);
559
560 let block_data = &body.basic_blocks[block];
561 let loc = Location { block, statement_index: block_data.statements.len() };
562
563 let term = block_data.terminator();
564 let mut twostep = two_step_loc(loc, Effect::After);
565 append_at(&mut values, &state, twostep);
566 // Ensure we have a non-zero live range even for dead stores. This is done by marking all
567 // the written-to locals as live in the second half of the statement.
568 // We also ensure that operands read by terminators conflict with writes by that terminator.
569 // For instance a function call may read args after having written to the destination.
570 VisitPlacesWith(|place, ctxt| match DefUse::for_place(place, ctxt) {
571 DefUse::Def | DefUse::Use | DefUse::PartialWrite => {
572 if let Some(relevant) = relevant.shrink[place.local] {
573 values.insert(relevant, twostep);
574 }
575 }
576 DefUse::NonUse => {}
577 })
578 .visit_terminator(term, loc);
579
580 twostep = TwoStepIndex::from_u32(twostep.as_u32() + 1);
581 debug_assert_eq!(twostep, two_step_loc(loc, Effect::Before));
582 MaybeLiveLocals.apply_early_terminator_effect(&mut state, term, loc);
583 MaybeLiveLocals.apply_primary_terminator_effect(&mut state, term, loc);
584 append_at(&mut values, &state, twostep);
585
586 for (statement_index, stmt) in block_data.statements.iter().enumerate().rev() {
587 let loc = Location { block, statement_index };
588 twostep = TwoStepIndex::from_u32(twostep.as_u32() + 1);
589 debug_assert_eq!(twostep, two_step_loc(loc, Effect::After));
590 append_at(&mut values, &state, twostep);
591 // Ensure we have a non-zero live range even for dead stores. This is done by marking
592 // all the written-to locals as live in the second half of the statement.
593 VisitPlacesWith(|place, ctxt| match DefUse::for_place(place, ctxt) {
594 DefUse::Def | DefUse::PartialWrite => {
595 if let Some(relevant) = relevant.shrink[place.local] {
596 values.insert(relevant, twostep);
597 }
598 }
599 DefUse::Use | DefUse::NonUse => {}
600 })
601 .visit_statement(stmt, loc);
602
603 twostep = TwoStepIndex::from_u32(twostep.as_u32() + 1);
604 debug_assert_eq!(twostep, two_step_loc(loc, Effect::Before));
605 MaybeLiveLocals.apply_early_statement_effect(&mut state, stmt, loc);
606 MaybeLiveLocals.apply_primary_statement_effect(&mut state, stmt, loc);
607 // ... but reads from operands are marked as live here so they do not conflict with
608 // the all the writes we manually marked as live in the second half of the statement.
609 append_at(&mut values, &state, twostep);
610 }
611 }
612
613 values
614}