|
| 1 | +//! Deduces supplementary parameter attributes from MIR. |
| 2 | +//! |
| 3 | +//! Deduced parameter attributes are those that can only be soundly determined by examining the |
| 4 | +//! body of the function instead of just the signature. These can be useful for optimization |
| 5 | +//! purposes on a best-effort basis. We compute them here and store them into the crate metadata so |
| 6 | +//! dependent crates can use them. |
| 7 | +
|
| 8 | +use rustc_hir::def_id::DefId; |
| 9 | +use rustc_index::bit_set::BitSet; |
| 10 | +use rustc_middle::mir::visit::{NonMutatingUseContext, PlaceContext, Visitor}; |
| 11 | +use rustc_middle::mir::{Body, Local, Location, Operand, Terminator, TerminatorKind, RETURN_PLACE}; |
| 12 | +use rustc_middle::ty::{self, DeducedParamAttrs, ParamEnv, Ty, TyCtxt}; |
| 13 | +use rustc_session::config::OptLevel; |
| 14 | +use rustc_span::DUMMY_SP; |
| 15 | + |
| 16 | +/// A visitor that determines which arguments have been mutated. We can't use the mutability field |
| 17 | +/// on LocalDecl for this because it has no meaning post-optimization. |
| 18 | +struct DeduceReadOnly { |
| 19 | + /// Each bit is indexed by argument number, starting at zero (so 0 corresponds to local decl |
| 20 | + /// 1). The bit is true if the argument may have been mutated or false if we know it hasn't |
| 21 | + /// been up to the point we're at. |
| 22 | + mutable_args: BitSet<usize>, |
| 23 | +} |
| 24 | + |
| 25 | +impl DeduceReadOnly { |
| 26 | + /// Returns a new DeduceReadOnly instance. |
| 27 | + fn new(arg_count: usize) -> Self { |
| 28 | + Self { mutable_args: BitSet::new_empty(arg_count) } |
| 29 | + } |
| 30 | +} |
| 31 | + |
| 32 | +impl<'tcx> Visitor<'tcx> for DeduceReadOnly { |
| 33 | + fn visit_local(&mut self, local: Local, mut context: PlaceContext, _: Location) { |
| 34 | + // We're only interested in arguments. |
| 35 | + if local == RETURN_PLACE || local.index() > self.mutable_args.domain_size() { |
| 36 | + return; |
| 37 | + } |
| 38 | + |
| 39 | + // Replace place contexts that are moves with copies. This is safe in all cases except |
| 40 | + // function argument position, which we already handled in `visit_terminator()` by using the |
| 41 | + // ArgumentChecker. See the comment in that method for more details. |
| 42 | + // |
| 43 | + // In the future, we might want to move this out into a separate pass, but for now let's |
| 44 | + // just do it on the fly because that's faster. |
| 45 | + if matches!(context, PlaceContext::NonMutatingUse(NonMutatingUseContext::Move)) { |
| 46 | + context = PlaceContext::NonMutatingUse(NonMutatingUseContext::Copy); |
| 47 | + } |
| 48 | + |
| 49 | + match context { |
| 50 | + PlaceContext::MutatingUse(..) |
| 51 | + | PlaceContext::NonMutatingUse(NonMutatingUseContext::Move) => { |
| 52 | + // This is a mutation, so mark it as such. |
| 53 | + self.mutable_args.insert(local.index() - 1); |
| 54 | + } |
| 55 | + PlaceContext::NonMutatingUse(..) | PlaceContext::NonUse(..) => { |
| 56 | + // Not mutating, so it's fine. |
| 57 | + } |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + fn visit_terminator(&mut self, terminator: &Terminator<'tcx>, location: Location) { |
| 62 | + // OK, this is subtle. Suppose that we're trying to deduce whether `x` in `f` is read-only |
| 63 | + // and we have the following: |
| 64 | + // |
| 65 | + // fn f(x: BigStruct) { g(x) } |
| 66 | + // fn g(mut y: BigStruct) { y.foo = 1 } |
| 67 | + // |
| 68 | + // If, at the generated MIR level, `f` turned into something like: |
| 69 | + // |
| 70 | + // fn f(_1: BigStruct) -> () { |
| 71 | + // let mut _0: (); |
| 72 | + // bb0: { |
| 73 | + // _0 = g(move _1) -> bb1; |
| 74 | + // } |
| 75 | + // ... |
| 76 | + // } |
| 77 | + // |
| 78 | + // then it would be incorrect to mark `x` (i.e. `_1`) as `readonly`, because `g`'s write to |
| 79 | + // its copy of the indirect parameter would actually be a write directly to the pointer that |
| 80 | + // `f` passes. Note that function arguments are the only situation in which this problem can |
| 81 | + // arise: every other use of `move` in MIR doesn't actually write to the value it moves |
| 82 | + // from. |
| 83 | + // |
| 84 | + // Anyway, right now this situation doesn't actually arise in practice. Instead, the MIR for |
| 85 | + // that function looks like this: |
| 86 | + // |
| 87 | + // fn f(_1: BigStruct) -> () { |
| 88 | + // let mut _0: (); |
| 89 | + // let mut _2: BigStruct; |
| 90 | + // bb0: { |
| 91 | + // _2 = move _1; |
| 92 | + // _0 = g(move _2) -> bb1; |
| 93 | + // } |
| 94 | + // ... |
| 95 | + // } |
| 96 | + // |
| 97 | + // Because of that extra move that MIR construction inserts, `x` (i.e. `_1`) can *in |
| 98 | + // practice* safely be marked `readonly`. |
| 99 | + // |
| 100 | + // To handle the possibility that other optimizations (for example, destination propagation) |
| 101 | + // might someday generate MIR like the first example above, we panic upon seeing an argument |
| 102 | + // to *our* function that is directly moved into *another* function as an argument. Having |
| 103 | + // eliminated that problematic case, we can safely treat moves as copies in this analysis. |
| 104 | + // |
| 105 | + // In the future, if MIR optimizations cause arguments of a caller to be directly moved into |
| 106 | + // the argument of a callee, we can just add that argument to `mutated_args` instead of |
| 107 | + // panicking. |
| 108 | + // |
| 109 | + // Note that, because the problematic MIR is never actually generated, we can't add a test |
| 110 | + // case for this. |
| 111 | + |
| 112 | + if let TerminatorKind::Call { ref args, .. } = terminator.kind { |
| 113 | + for arg in args { |
| 114 | + if let Operand::Move(_) = *arg { |
| 115 | + // ArgumentChecker panics if a direct move of an argument from a caller to a |
| 116 | + // callee was detected. |
| 117 | + // |
| 118 | + // If, in the future, MIR optimizations cause arguments to be moved directly |
| 119 | + // from callers to callees, change the panic to instead add the argument in |
| 120 | + // question to `mutating_uses`. |
| 121 | + ArgumentChecker::new(self.mutable_args.domain_size()) |
| 122 | + .visit_operand(arg, location) |
| 123 | + } |
| 124 | + } |
| 125 | + }; |
| 126 | + |
| 127 | + self.super_terminator(terminator, location); |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +/// A visitor that simply panics if a direct move of an argument from a caller to a callee was |
| 132 | +/// detected. |
| 133 | +struct ArgumentChecker { |
| 134 | + /// The number of arguments to the calling function. |
| 135 | + arg_count: usize, |
| 136 | +} |
| 137 | + |
| 138 | +impl ArgumentChecker { |
| 139 | + /// Creates a new ArgumentChecker. |
| 140 | + fn new(arg_count: usize) -> Self { |
| 141 | + Self { arg_count } |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +impl<'tcx> Visitor<'tcx> for ArgumentChecker { |
| 146 | + fn visit_local(&mut self, local: Local, context: PlaceContext, _: Location) { |
| 147 | + // Check to make sure that, if this local is an argument, we didn't move directly from it. |
| 148 | + if matches!(context, PlaceContext::NonMutatingUse(NonMutatingUseContext::Move)) |
| 149 | + && local != RETURN_PLACE |
| 150 | + && local.index() <= self.arg_count |
| 151 | + { |
| 152 | + // If, in the future, MIR optimizations cause arguments to be moved directly from |
| 153 | + // callers to callees, change this panic to instead add the argument in question to |
| 154 | + // `mutating_uses`. |
| 155 | + panic!("Detected a direct move from a caller's argument to a callee's argument!") |
| 156 | + } |
| 157 | + } |
| 158 | +} |
| 159 | + |
| 160 | +/// Returns true if values of a given type will never be passed indirectly, regardless of ABI. |
| 161 | +fn type_will_always_be_passed_directly<'tcx>(ty: Ty<'tcx>) -> bool { |
| 162 | + matches!( |
| 163 | + ty.kind(), |
| 164 | + ty::Bool |
| 165 | + | ty::Char |
| 166 | + | ty::Float(..) |
| 167 | + | ty::Int(..) |
| 168 | + | ty::RawPtr(..) |
| 169 | + | ty::Ref(..) |
| 170 | + | ty::Slice(..) |
| 171 | + | ty::Uint(..) |
| 172 | + ) |
| 173 | +} |
| 174 | + |
| 175 | +/// Returns the deduced parameter attributes for a function. |
| 176 | +/// |
| 177 | +/// Deduced parameter attributes are those that can only be soundly determined by examining the |
| 178 | +/// body of the function instead of just the signature. These can be useful for optimization |
| 179 | +/// purposes on a best-effort basis. We compute them here and store them into the crate metadata so |
| 180 | +/// dependent crates can use them. |
| 181 | +pub fn deduced_param_attrs<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> &'tcx [DeducedParamAttrs] { |
| 182 | + // This computation is unfortunately rather expensive, so don't do it unless we're optimizing. |
| 183 | + // Also skip it in incremental mode. |
| 184 | + if tcx.sess.opts.optimize == OptLevel::No || tcx.sess.opts.incremental.is_some() { |
| 185 | + return &[]; |
| 186 | + } |
| 187 | + |
| 188 | + // If the Freeze language item isn't present, then don't bother. |
| 189 | + if tcx.lang_items().freeze_trait().is_none() { |
| 190 | + return &[]; |
| 191 | + } |
| 192 | + |
| 193 | + // Codegen won't use this information for anything if all the function parameters are passed |
| 194 | + // directly. Detect that and bail, for compilation speed. |
| 195 | + let fn_ty = tcx.type_of(def_id); |
| 196 | + if matches!(fn_ty.kind(), ty::FnDef(..)) { |
| 197 | + if fn_ty |
| 198 | + .fn_sig(tcx) |
| 199 | + .inputs() |
| 200 | + .skip_binder() |
| 201 | + .iter() |
| 202 | + .cloned() |
| 203 | + .all(type_will_always_be_passed_directly) |
| 204 | + { |
| 205 | + return &[]; |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + // Don't deduce any attributes for functions that have no MIR. |
| 210 | + if !tcx.is_mir_available(def_id) { |
| 211 | + return &[]; |
| 212 | + } |
| 213 | + |
| 214 | + // Deduced attributes for other crates should be read from the metadata instead of via this |
| 215 | + // function. |
| 216 | + debug_assert!(def_id.is_local()); |
| 217 | + |
| 218 | + // Grab the optimized MIR. Analyze it to determine which arguments have been mutated. |
| 219 | + let body: &Body<'tcx> = tcx.optimized_mir(def_id); |
| 220 | + let mut deduce_read_only = DeduceReadOnly::new(body.arg_count); |
| 221 | + deduce_read_only.visit_body(body); |
| 222 | + |
| 223 | + // Set the `readonly` attribute for every argument that we concluded is immutable and that |
| 224 | + // contains no UnsafeCells. |
| 225 | + // |
| 226 | + // FIXME: This is overly conservative around generic parameters: `is_freeze()` will always |
| 227 | + // return false for them. For a description of alternatives that could do a better job here, |
| 228 | + // see [1]. |
| 229 | + // |
| 230 | + // [1]: https://github.com/rust-lang/rust/pull/103172#discussion_r999139997 |
| 231 | + let mut deduced_param_attrs = tcx.arena.alloc_from_iter( |
| 232 | + body.local_decls.iter().skip(1).take(body.arg_count).enumerate().map( |
| 233 | + |(arg_index, local_decl)| DeducedParamAttrs { |
| 234 | + read_only: !deduce_read_only.mutable_args.contains(arg_index) |
| 235 | + && local_decl.ty.is_freeze(tcx.at(DUMMY_SP), ParamEnv::reveal_all()), |
| 236 | + }, |
| 237 | + ), |
| 238 | + ); |
| 239 | + |
| 240 | + // Trailing parameters past the size of the `deduced_param_attrs` array are assumed to have the |
| 241 | + // default set of attributes, so we don't have to store them explicitly. Pop them off to save a |
| 242 | + // few bytes in metadata. |
| 243 | + while deduced_param_attrs.last() == Some(&DeducedParamAttrs::default()) { |
| 244 | + let last_index = deduced_param_attrs.len() - 1; |
| 245 | + deduced_param_attrs = &mut deduced_param_attrs[0..last_index]; |
| 246 | + } |
| 247 | + |
| 248 | + deduced_param_attrs |
| 249 | +} |
0 commit comments