Skip to main content

rustc_mir_transform/
sroa.rs

1use rustc_abi::FieldIdx;
2use rustc_data_structures::flat_map_in_place::FlatMapInPlace;
3use rustc_index::IndexVec;
4use rustc_index::bit_set::{DenseBitSet, GrowableBitSet};
5use rustc_middle::mir::visit::*;
6use rustc_middle::mir::*;
7use rustc_middle::ty::{self, Ty, TyCtxt};
8use rustc_mir_dataflow::value_analysis::{excluded_locals, iter_fields};
9use rustc_span::bug;
10use tracing::{debug, instrument};
11
12use crate::PassPolicy;
13use crate::patch::MirPatch;
14
15pub(super) struct ScalarReplacementOfAggregates;
16
17impl<'tcx> crate::MirPass<'tcx> for ScalarReplacementOfAggregates {
18    fn policy(&self, ctx: &crate::PassCtx<'_>) -> PassPolicy {
19        PassPolicy::optional(ctx.mir_opt_level() >= 2)
20    }
21
22    #[instrument(level = "debug", skip(self, tcx, body))]
23    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
24        debug!(def_id = ?body.source.def_id());
25
26        // Avoid query cycles (coroutines require optimized MIR for layout).
27        if tcx.type_of(body.source.def_id()).instantiate_identity().skip_norm_wip().is_coroutine() {
28            return;
29        }
30
31        let mut excluded = excluded_locals(body);
32        let typing_env = body.typing_env(tcx);
33        loop {
34            debug!(?excluded);
35            let escaping = escaping_locals(tcx, &excluded, body);
36            debug!(?escaping);
37            let replacements = compute_flattening(tcx, typing_env, body, escaping);
38            debug!(?replacements);
39            let all_dead_locals = replace_flattened_locals(tcx, body, replacements);
40            if !all_dead_locals.is_empty() {
41                excluded.union(&all_dead_locals);
42            } else {
43                break;
44            }
45        }
46    }
47}
48
49/// Identify all locals that are not eligible for SROA.
50///
51/// There are 3 cases:
52/// - the aggregated local is used or passed to other code (function parameters and arguments);
53/// - the locals is a union or an enum;
54/// - the local's address is taken, and thus the relative addresses of the fields are observable to
55///   client code.
56fn escaping_locals<'tcx>(
57    tcx: TyCtxt<'tcx>,
58    excluded: &GrowableBitSet<Local>,
59    body: &Body<'tcx>,
60) -> DenseBitSet<Local> {
61    let is_excluded_ty = |ty: Ty<'tcx>| {
62        if ty.is_union() || ty.is_enum() {
63            return true;
64        }
65        // Types like MaybeDangling relax validity requirements of inner references.
66        if ty.is_like_maybe_dangling() {
67            return true;
68        }
69        if let ty::Adt(def, _args) = ty.kind()
70            && !def.can_project_fields(tcx)
71        {
72            return true;
73        }
74        // Default for non-ADTs
75        false
76    };
77
78    let mut set = DenseBitSet::new_empty(body.local_decls.len());
79    set.insert_range(RETURN_PLACE..Local::arg(body.arg_count));
80    for (local, decl) in body.local_decls().iter_enumerated() {
81        if excluded.contains(local) || is_excluded_ty(decl.ty) {
82            set.insert(local);
83        }
84    }
85    let mut visitor = EscapeVisitor { set };
86    visitor.visit_body(body);
87    return visitor.set;
88
89    struct EscapeVisitor {
90        set: DenseBitSet<Local>,
91    }
92
93    impl<'tcx> Visitor<'tcx> for EscapeVisitor {
94        fn visit_local(&mut self, local: Local, _: PlaceContext, _: Location) {
95            self.set.insert(local);
96        }
97
98        fn visit_place(&mut self, place: &Place<'tcx>, context: PlaceContext, location: Location) {
99            // Mirror the implementation in PreFlattenVisitor.
100            if let &[PlaceElem::Field(..), ..] = &place.projection[..] {
101                return;
102            }
103            self.super_place(place, context, location);
104        }
105
106        fn visit_assign(
107            &mut self,
108            lvalue: &Place<'tcx>,
109            rvalue: &Rvalue<'tcx>,
110            location: Location,
111        ) {
112            if lvalue.as_local().is_some() {
113                match rvalue {
114                    // Aggregate assignments are expanded in run_pass.
115                    Rvalue::Aggregate(..) | Rvalue::Use(..) => {
116                        self.visit_rvalue(rvalue, location);
117                        return;
118                    }
119                    _ => {}
120                }
121            }
122            self.super_assign(lvalue, rvalue, location)
123        }
124
125        fn visit_statement(&mut self, statement: &Statement<'tcx>, location: Location) {
126            match statement.kind {
127                // Storage statements are expanded in run_pass.
128                StatementKind::StorageLive(..) | StatementKind::StorageDead(..) => return,
129                _ => self.super_statement(statement, location),
130            }
131        }
132
133        // We ignore anything that happens in debuginfo, since we expand it using
134        // `VarDebugInfoFragment`.
135        fn visit_var_debug_info(&mut self, _: &VarDebugInfo<'tcx>) {}
136    }
137}
138
139#[derive(Default, Debug)]
140struct ReplacementMap<'tcx> {
141    /// Pre-computed list of all "new" locals for each "old" local. This is used to expand storage
142    /// and deinit statement and debuginfo.
143    fragments: IndexVec<Local, Option<IndexVec<FieldIdx, Option<(Ty<'tcx>, Local)>>>>,
144}
145
146impl<'tcx> ReplacementMap<'tcx> {
147    fn replace_place(&self, tcx: TyCtxt<'tcx>, place: PlaceRef<'tcx>) -> Option<Place<'tcx>> {
148        let &[PlaceElem::Field(f, _), ref rest @ ..] = place.projection else {
149            return None;
150        };
151        let fields = self.fragments[place.local].as_ref()?;
152        let (_, new_local) = fields[f]?;
153        Some(Place { local: new_local, projection: tcx.mk_place_elems(rest) })
154    }
155
156    fn place_fragments(
157        &self,
158        place: Place<'tcx>,
159    ) -> Option<impl Iterator<Item = (FieldIdx, Ty<'tcx>, Local)>> {
160        let local = place.as_local()?;
161        let fields = self.fragments[local].as_ref()?;
162        Some(fields.iter_enumerated().filter_map(|(field, &opt_ty_local)| {
163            let (ty, local) = opt_ty_local?;
164            Some((field, ty, local))
165        }))
166    }
167}
168
169/// Compute the replacement of flattened places into locals.
170///
171/// For each eligible place, we assign a new local to each accessed field.
172/// The replacement will be done later in `ReplacementVisitor`.
173fn compute_flattening<'tcx>(
174    tcx: TyCtxt<'tcx>,
175    typing_env: ty::TypingEnv<'tcx>,
176    body: &mut Body<'tcx>,
177    escaping: DenseBitSet<Local>,
178) -> ReplacementMap<'tcx> {
179    let mut fragments = IndexVec::from_elem(None, &body.local_decls);
180
181    for local in body.local_decls.indices() {
182        if escaping.contains(local) {
183            continue;
184        }
185        let decl = body.local_decls[local].clone();
186        let ty = decl.ty;
187        iter_fields(ty, tcx, typing_env, |variant, field, field_ty| {
188            if variant.is_some() {
189                // Downcasts are currently not supported.
190                return;
191            };
192            let new_local =
193                body.local_decls.push(LocalDecl { ty: field_ty, user_ty: None, ..decl.clone() });
194            fragments.get_or_insert_with(local, IndexVec::new).insert(field, (field_ty, new_local));
195        });
196    }
197    ReplacementMap { fragments }
198}
199
200/// Perform the replacement computed by `compute_flattening`.
201fn replace_flattened_locals<'tcx>(
202    tcx: TyCtxt<'tcx>,
203    body: &mut Body<'tcx>,
204    replacements: ReplacementMap<'tcx>,
205) -> GrowableBitSet<Local> {
206    // Start with an empty GrowableBitSet, to avoid allocation if nothing is dead.
207    // Then fill the set in descending order so that it allocates at most once.
208    let mut all_dead_locals = GrowableBitSet::new_empty();
209    for (local, replacements) in replacements.fragments.iter_enumerated().rev() {
210        if replacements.is_some() {
211            all_dead_locals.insert(local);
212        }
213    }
214    debug!(?all_dead_locals);
215    if all_dead_locals.is_empty() {
216        return all_dead_locals;
217    }
218
219    let mut visitor = ReplacementVisitor {
220        tcx,
221        local_decls: &body.local_decls,
222        replacements: &replacements,
223        all_dead_locals,
224        patch: MirPatch::new(body),
225    };
226    for (bb, data) in body.basic_blocks.as_mut_preserves_cfg().iter_enumerated_mut() {
227        visitor.visit_basic_block_data(bb, data);
228    }
229    for scope in &mut body.source_scopes {
230        visitor.visit_source_scope_data(scope);
231    }
232    for (index, annotation) in body.user_type_annotations.iter_enumerated_mut() {
233        visitor.visit_user_type_annotation(index, annotation);
234    }
235    visitor.expand_var_debug_info(&mut body.var_debug_info);
236    let ReplacementVisitor { patch, all_dead_locals, .. } = visitor;
237    patch.apply(body);
238    all_dead_locals
239}
240
241struct ReplacementVisitor<'tcx, 'll> {
242    tcx: TyCtxt<'tcx>,
243    /// This is only used to compute the type for `VarDebugInfoFragment`.
244    local_decls: &'ll LocalDecls<'tcx>,
245    /// Work to do.
246    replacements: &'ll ReplacementMap<'tcx>,
247    /// This is used to check that we are not leaving references to replaced locals behind.
248    all_dead_locals: GrowableBitSet<Local>,
249    patch: MirPatch<'tcx>,
250}
251
252impl<'tcx> ReplacementVisitor<'tcx, '_> {
253    #[instrument(level = "trace", skip(self))]
254    fn expand_var_debug_info(&mut self, var_debug_info: &mut Vec<VarDebugInfo<'tcx>>) {
255        var_debug_info.flat_map_in_place(|mut var_debug_info| {
256            let place = match var_debug_info.value {
257                VarDebugInfoContents::Const(_) => return vec![var_debug_info],
258                VarDebugInfoContents::Place(ref mut place) => place,
259            };
260
261            if let Some(repl) = self.replacements.replace_place(self.tcx, place.as_ref()) {
262                *place = repl;
263                return vec![var_debug_info];
264            }
265
266            let Some(parts) = self.replacements.place_fragments(*place) else {
267                return vec![var_debug_info];
268            };
269
270            let ty = place.ty(self.local_decls, self.tcx).ty;
271
272            parts
273                .map(|(field, field_ty, replacement_local)| {
274                    let mut var_debug_info = var_debug_info.clone();
275                    let composite = var_debug_info.composite.get_or_insert_with(|| {
276                        Box::new(VarDebugInfoFragment { ty, projection: Vec::new() })
277                    });
278                    composite.projection.push(PlaceElem::Field(field, field_ty));
279
280                    var_debug_info.value = VarDebugInfoContents::Place(replacement_local.into());
281                    var_debug_info
282                })
283                .collect()
284        });
285    }
286}
287
288impl<'tcx, 'll> MutVisitor<'tcx> for ReplacementVisitor<'tcx, 'll> {
289    fn tcx(&self) -> TyCtxt<'tcx> {
290        self.tcx
291    }
292
293    fn visit_place(&mut self, place: &mut Place<'tcx>, context: PlaceContext, location: Location) {
294        if let Some(repl) = self.replacements.replace_place(self.tcx, place.as_ref()) {
295            *place = repl
296        } else {
297            self.super_place(place, context, location)
298        }
299    }
300
301    #[instrument(level = "trace", skip(self))]
302    fn visit_statement(&mut self, statement: &mut Statement<'tcx>, location: Location) {
303        match statement.kind {
304            // Duplicate storage and deinit statements, as they pretty much apply to all fields.
305            StatementKind::StorageLive(l) => {
306                if let Some(final_locals) = self.replacements.place_fragments(l.into()) {
307                    for (_, _, fl) in final_locals {
308                        self.patch.add_statement(location, StatementKind::StorageLive(fl));
309                    }
310                    statement.make_nop(true);
311                }
312                return;
313            }
314            StatementKind::StorageDead(l) => {
315                if let Some(final_locals) = self.replacements.place_fragments(l.into()) {
316                    for (_, _, fl) in final_locals {
317                        self.patch.add_statement(location, StatementKind::StorageDead(fl));
318                    }
319                    statement.make_nop(true);
320                }
321                return;
322            }
323
324            // We have `a = Struct { 0: x, 1: y, .. }`.
325            // We replace it by
326            // ```
327            // a_0 = x
328            // a_1 = y
329            // ...
330            // ```
331            StatementKind::Assign((place, Rvalue::Aggregate(_, ref mut operands))) => {
332                if let Some(local) = place.as_local()
333                    && let Some(final_locals) = &self.replacements.fragments[local]
334                {
335                    // This is ok as we delete the statement later.
336                    let operands = std::mem::take(operands);
337                    for (&opt_ty_local, mut operand) in final_locals.iter().zip(operands) {
338                        if let Some((_, new_local)) = opt_ty_local {
339                            // Replace mentions of SROA'd locals that appear in the operand.
340                            self.visit_operand(&mut operand, location);
341
342                            let rvalue = Rvalue::Use(operand, WithRetag::Yes);
343                            self.patch.add_statement(
344                                location,
345                                StatementKind::Assign(Box::new((new_local.into(), rvalue))),
346                            );
347                        }
348                    }
349                    statement.make_nop(true);
350                    return;
351                }
352            }
353
354            // We have `a = some constant`
355            // We add the projections.
356            // ```
357            // a_0 = a.0
358            // a_1 = a.1
359            // ...
360            // ```
361            // ConstProp will pick up the pieces and replace them by actual constants.
362            StatementKind::Assign((place, Rvalue::Use(Operand::Constant(_), retag))) => {
363                if let Some(final_locals) = self.replacements.place_fragments(place) {
364                    // Put the deaggregated statements *after* the original one.
365                    let location = location.successor_within_block();
366                    for (field, ty, new_local) in final_locals {
367                        let rplace = self.tcx.mk_place_field(place, field, ty);
368                        let rvalue = Rvalue::Use(Operand::Move(rplace), retag);
369                        self.patch.add_statement(
370                            location,
371                            StatementKind::Assign(Box::new((new_local.into(), rvalue))),
372                        );
373                    }
374                    // We still need `place.local` to exist, so don't make it nop.
375                    return;
376                }
377            }
378
379            // We have `a = move? place`
380            // We replace it by
381            // ```
382            // a_0 = move? place.0
383            // a_1 = move? place.1
384            // ...
385            // ```
386            StatementKind::Assign((
387                lhs,
388                Rvalue::Use(ref op @ (Operand::Copy(rplace) | Operand::Move(rplace)), retag),
389            )) => {
390                let copy = match *op {
391                    Operand::Copy(_) => true,
392                    Operand::Move(_) => false,
393                    Operand::Constant(_) | Operand::RuntimeChecks(_) => bug!(),
394                };
395                if let Some(final_locals) = self.replacements.place_fragments(lhs) {
396                    for (field, ty, new_local) in final_locals {
397                        let rplace = self.tcx.mk_place_field(rplace, field, ty);
398                        debug!(?rplace);
399                        let rplace = self
400                            .replacements
401                            .replace_place(self.tcx, rplace.as_ref())
402                            .unwrap_or(rplace);
403                        debug!(?rplace);
404                        let rvalue = if copy {
405                            Rvalue::Use(Operand::Copy(rplace), retag)
406                        } else {
407                            Rvalue::Use(Operand::Move(rplace), retag)
408                        };
409                        self.patch.add_statement(
410                            location,
411                            StatementKind::Assign(Box::new((new_local.into(), rvalue))),
412                        );
413                    }
414                    statement.make_nop(true);
415                    return;
416                }
417            }
418
419            _ => {}
420        }
421        self.super_statement(statement, location)
422    }
423
424    fn visit_local(&mut self, local: &mut Local, _: PlaceContext, _: Location) {
425        assert!(!self.all_dead_locals.contains(*local));
426    }
427}