Skip to main content

rapx/verify/call_summary/
interprocedural.rs

1//! Interprocedural call summaries derived from MIR for local wrapper functions.
2//!
3//! When no hand-crafted summary exists, this module inspects a callee's own MIR
4//! to approximate its effects: pointer-arithmetic wrappers, `from_raw_parts`
5//! wrappers, argument-to-return dataflow, and index-disjointness validators.
6
7use std::collections::{HashMap, HashSet, VecDeque};
8
9use rustc_hir::def_id::DefId;
10use rustc_middle::{
11    mir::{
12        BasicBlock, BinOp, Local, Operand, Place, ProjectionElem, Rvalue, StatementKind,
13        TerminatorKind,
14    },
15    ty::{Ty, TyCtxt, TyKind},
16};
17
18use crate::analysis::dataflow::{DataflowAnalysis, default::DataflowAnalyzer};
19use crate::analysis::path::graph::{PathEnumerator, PathGraph};
20use crate::compat::Spanned;
21use crate::helpers::mir_utils as helpers;
22use crate::limit::{
23    FIELD_LOAD_EFFECT_BLOCK_LIMIT, FROM_RAW_PARTS_WRAPPER_BLOCK_LIMIT,
24    POINTER_ARITH_WRAPPER_BLOCK_LIMIT, SLICE_BOUNDED_RETURN_BLOCK_LIMIT,
25};
26
27use super::{CallContext, CallEffect};
28
29/// Trace backward from an operand (inner call arg) through Copy/Move/Cast/
30/// Ref/RawPtr assignments to the outer callee's argument local, returning its
31/// index. `Ref`/`RawPtr` are treated as data-flow too, which is an
32/// approximation (taking a reference is not a pure copy) but is adequate for
33/// wrapper recognition.
34fn trace_to_callee_arg<'tcx>(
35    body: &rustc_middle::mir::Body<'tcx>,
36    operand: &Operand<'_>,
37) -> Option<usize> {
38    let local = match operand {
39        Operand::Copy(place) | Operand::Move(place) => place.local,
40        _ => return None,
41    };
42    let idx = local.as_usize();
43    if idx >= 1 && idx <= body.arg_count {
44        return Some(idx - 1);
45    }
46    let mut queue = VecDeque::from([local]);
47    let mut seen = HashSet::from([local]);
48    while let Some(current) = queue.pop_front() {
49        let cidx = current.as_usize();
50        if cidx >= 1 && cidx <= body.arg_count {
51            return Some(cidx - 1);
52        }
53        for bb in body.basic_blocks.iter() {
54            for stmt in &bb.statements {
55                let StatementKind::Assign(assign) = &stmt.kind else {
56                    continue;
57                };
58                let dest = assign.0.local;
59                if dest != current {
60                    continue;
61                }
62                let source = match &assign.1 {
63                    Rvalue::Use(Operand::Copy(place), ..)
64                    | Rvalue::Use(Operand::Move(place), ..)
65                    | Rvalue::Cast(_, Operand::Copy(place), _)
66                    | Rvalue::Cast(_, Operand::Move(place), _)
67                    | Rvalue::Ref(_, _, place)
68                    | Rvalue::RawPtr(_, place)
69                    | Rvalue::CopyForDeref(place) => place.local,
70                    _ => continue,
71                };
72                if !seen.contains(&source) {
73                    seen.insert(source);
74                    queue.push_back(source);
75                }
76            }
77            let Some(terminator) = &bb.terminator else {
78                continue;
79            };
80            let TerminatorKind::Call {
81                func,
82                args,
83                destination,
84                ..
85            } = &terminator.kind
86            else {
87                continue;
88            };
89            if destination.local != current {
90                continue;
91            }
92            // Trace through pointer-preserving calls: `as_ptr`/`as_mut_ptr`
93            // (and friends) return the pointee address, while `add`/`sub`/
94            // `offset` return the base pointer shifted by an offset — the
95            // provenance (and thus the written-through arg) is carried by
96            // their first (base/receiver) argument.
97            let callee = helpers::dep_callee_def_id(func);
98            let traces_base = crate::verify::api_classify::is_as_ptr(callee)
99                || crate::verify::api_classify::is_pointer_add(callee)
100                || crate::verify::api_classify::is_pointer_sub(callee);
101            if !traces_base {
102                continue;
103            }
104            let Some(source) = args.first().and_then(|arg| match &arg.node {
105                Operand::Copy(place) | Operand::Move(place) => Some(place.local),
106                Operand::Constant(_) => None,
107                #[cfg(rapx_ge_95)]
108                Operand::RuntimeChecks(_) => None,
109            }) else {
110                continue;
111            };
112            if !seen.contains(&source) {
113                seen.insert(source);
114                queue.push_back(source);
115            }
116        }
117    }
118    None
119}
120
121/// Memo state for the transitive wrapper-effect walk: `Computing` marks a
122/// callee currently being resolved (a re-entry is a self/mutual-recursive
123/// cycle), `Done` caches the finished result.
124enum WrapperEffectMemo {
125    Computing,
126    Done(Option<CallEffect>),
127}
128
129/// Resolve `callee`'s wrapper effect by walking nested wrapper calls, with
130/// cycle detection and memoization. `probe` inspects `callee`'s body and, for
131/// a nested call it follows, invokes `recurse`, which routes back through this
132/// resolver so the memo applies uniformly. A callee re-entered while still
133/// being resolved is a cycle and resolves to `None` (no finite wrapper chain).
134fn resolve_wrapper_effect<'tcx>(
135    tcx: TyCtxt<'tcx>,
136    callee: DefId,
137    memo: &mut HashMap<DefId, WrapperEffectMemo>,
138    probe: &dyn Fn(
139        TyCtxt<'tcx>,
140        DefId,
141        &mut (dyn FnMut(DefId) -> Option<CallEffect> + '_),
142    ) -> Option<CallEffect>,
143) -> Option<CallEffect> {
144    if let Some(state) = memo.get(&callee) {
145        return match state {
146            WrapperEffectMemo::Computing => None,
147            WrapperEffectMemo::Done(effect) => effect.clone(),
148        };
149    }
150    memo.insert(callee, WrapperEffectMemo::Computing);
151    let result = {
152        let recurse = &mut |inner: DefId| resolve_wrapper_effect(tcx, inner, memo, probe);
153        probe(tcx, callee, recurse)
154    };
155    memo.insert(callee, WrapperEffectMemo::Done(result.clone()));
156    result
157}
158
159/// Probe whether `callee` is a pointer-arithmetic (add/sub) wrapper, following
160/// nested wrapper calls transitively. `effect_summary` runs this on every local
161/// callee; it returns `None` for anything that is not — transitively — a
162/// pointer add/sub wrapper.
163pub(super) fn try_pointer_arith_wrapper_effect<'tcx>(
164    tcx: TyCtxt<'tcx>,
165    callee: DefId,
166) -> Option<CallEffect> {
167    let mut memo: HashMap<DefId, WrapperEffectMemo> = HashMap::new();
168    resolve_wrapper_effect(tcx, callee, &mut memo, &pointer_arith_wrapper_probe)
169}
170
171/// Single-effect recognizer for [`resolve_wrapper_effect`]: does `callee`
172/// directly wrap a pointer add/sub, or delegate to a nested callee that itself
173/// resolves to one?
174fn pointer_arith_wrapper_probe<'tcx>(
175    tcx: TyCtxt<'tcx>,
176    callee: DefId,
177    recurse: &mut (dyn FnMut(DefId) -> Option<CallEffect> + '_),
178) -> Option<CallEffect> {
179    if !tcx.is_mir_available(callee) {
180        return None;
181    }
182    let body = tcx.optimized_mir(callee);
183    if body.basic_blocks.len() > POINTER_ARITH_WRAPPER_BLOCK_LIMIT {
184        return None;
185    }
186
187    for bb in body.basic_blocks.iter() {
188        let Some(terminator) = &bb.terminator else {
189            continue;
190        };
191        let TerminatorKind::Call {
192            func,
193            args,
194            destination: call_dest,
195            ..
196        } = &terminator.kind
197        else {
198            continue;
199        };
200
201        let callee_id = helpers::dep_callee_def_id(func);
202        let is_add = crate::verify::api_classify::is_pointer_add(callee_id);
203        let is_sub = crate::verify::api_classify::is_pointer_sub(callee_id);
204
205        let inner_effect = if !is_add && !is_sub {
206            helpers::dep_callee_def_id(func).and_then(|inner_callee| {
207                if tcx.intrinsic(inner_callee).is_some() || helpers::is_drop_in_place(inner_callee)
208                {
209                    return None;
210                }
211                recurse(inner_callee)
212            })
213        } else {
214            None
215        };
216
217        if !is_add && !is_sub && inner_effect.is_none() {
218            continue;
219        }
220
221        if !call_result_reaches_return(body, call_dest.local) {
222            continue;
223        }
224
225        if let Some(effect) = inner_effect {
226            let (inner_base, inner_offset, stride, dereferenceable) = match effect {
227                CallEffect::ReturnPointerAdd {
228                    base_arg,
229                    offset_arg,
230                    stride,
231                    dereferenceable,
232                } => (base_arg, offset_arg, stride, dereferenceable),
233                CallEffect::ReturnPointerSub {
234                    base_arg,
235                    offset_arg,
236                    stride,
237                } => (base_arg, offset_arg, stride, false),
238                _ => {
239                    continue;
240                }
241            };
242            let base_arg = trace_to_callee_arg(body, &args.get(inner_base)?.node)?;
243            let offset_arg = trace_to_callee_arg(body, &args.get(inner_offset)?.node)?;
244            let is_sub = matches!(effect, CallEffect::ReturnPointerSub { .. });
245            return Some(if is_sub {
246                CallEffect::ReturnPointerSub {
247                    base_arg,
248                    offset_arg,
249                    stride,
250                }
251            } else {
252                CallEffect::ReturnPointerAdd {
253                    base_arg,
254                    offset_arg,
255                    stride,
256                    dereferenceable,
257                }
258            });
259        }
260
261        let base_arg = trace_to_callee_arg(body, &args.get(0)?.node)?;
262        let offset_arg = trace_to_callee_arg(body, &args.get(1)?.node)?;
263        let stride = if crate::verify::api_classify::is_byte_ptr_arith(callee_id) {
264            Some(1)
265        } else {
266            helpers::destination_stride(tcx, callee, Some(call_dest.local))
267        };
268
269        return if is_sub {
270            Some(CallEffect::ReturnPointerSub {
271                base_arg,
272                offset_arg,
273                stride,
274            })
275        } else {
276            Some(CallEffect::ReturnPointerAdd {
277                base_arg,
278                offset_arg,
279                stride,
280                dereferenceable: false,
281            })
282        };
283    }
284
285    None
286}
287
288/// Check whether a callee body contains pointer arithmetic calls.
289pub(super) fn callee_contains_pointer_arithmetic(tcx: TyCtxt<'_>, callee: DefId) -> bool {
290    let Some(_) = callee.as_local() else {
291        return false;
292    };
293    if !tcx.is_mir_available(callee) {
294        return false;
295    }
296    let body = tcx.optimized_mir(callee);
297    for bb in body.basic_blocks.iter() {
298        let Some(terminator) = &bb.terminator else {
299            continue;
300        };
301        let TerminatorKind::Call { func, .. } = &terminator.kind else {
302            continue;
303        };
304        if crate::verify::api_classify::is_pointer_add(helpers::dep_callee_def_id(func))
305            || crate::verify::api_classify::is_pointer_sub(helpers::dep_callee_def_id(func))
306        {
307            return true;
308        }
309    }
310    false
311}
312
313/// Use the existing dataflow graph to approximate callee return deps.
314/// Works for any callee with available MIR (local or cross-crate `#[inline]`).
315pub(super) fn local_return_dependencies(tcx: TyCtxt<'_>, callee: DefId) -> Option<Vec<usize>> {
316    if !tcx.is_mir_available(callee) {
317        return None;
318    }
319    helpers::catch_panic(|| {
320        let mut analyzer = DataflowAnalyzer::new(tcx, false);
321        analyzer.build_graph(callee);
322        let deps = analyzer.get_fn_arg2ret(callee);
323        deps.iter_enumerated()
324            .filter_map(|(local, depends)| {
325                if *depends && local.as_usize() > 0 {
326                    Some(local.as_usize() - 1)
327                } else {
328                    None
329                }
330            })
331            .collect()
332    })
333    .ok()
334}
335
336/// Detect when a local callee wraps `from_raw_parts(ptr, len)` and produce
337/// a `ReturnFreshAllocation` effect with the correct element size.
338pub(super) fn try_from_raw_parts_wrapper_effect<'tcx>(
339    tcx: TyCtxt<'tcx>,
340    callee: DefId,
341) -> Option<CallEffect> {
342    if !tcx.is_mir_available(callee) {
343        return None;
344    }
345    let body = tcx.optimized_mir(callee);
346    if body.basic_blocks.len() > FROM_RAW_PARTS_WRAPPER_BLOCK_LIMIT {
347        return None;
348    }
349    let ret = Local::from_usize(0);
350
351    for bb in body.basic_blocks.iter() {
352        let Some(terminator) = &bb.terminator else {
353            continue;
354        };
355        let TerminatorKind::Call {
356            func,
357            args,
358            destination: call_dest,
359            ..
360        } = &terminator.kind
361        else {
362            continue;
363        };
364
365        let inner_callee = helpers::dep_callee_def_id(func);
366        if !crate::verify::api_classify::is_from_raw_parts(inner_callee) {
367            continue;
368        }
369
370        // Verify the call result reaches return
371        if !call_result_reaches_return(body, call_dest.local) {
372            continue;
373        }
374
375        // Trace from_raw_parts args to callee args
376        let pointer_arg = trace_to_callee_arg(body, &args.get(0)?.node)?;
377        let size_arg = trace_to_callee_arg(body, &args.get(1)?.node)?;
378
379        // Determine element size from return type (slice or Vec).
380        let elem_size =
381            crate::verify::call_summary::from_raw_parts_elem_size(tcx, callee, Some(ret));
382
383        return Some(CallEffect::ReturnFreshAllocation {
384            pointer_arg,
385            size_arg,
386            elem_size,
387        });
388    }
389    None
390}
391
392/// Detect a field-getter callee from its MIR: a function whose body is
393/// (essentially) `(*self).field` — a single `Deref` + `Field` load returned as
394/// the function's result. Produces a `ReturnFieldOfArg` effect so the
395/// materialized field is returned, without any name- or length-specific
396/// knowledge.
397///
398/// The match is conservative: the body must contain *only* the field load
399/// (plus a unit-return and storage markers).
400pub(crate) fn try_field_load_effect(tcx: TyCtxt<'_>, callee: DefId) -> Option<CallEffect> {
401    if !tcx.is_mir_available(callee) {
402        return None;
403    }
404    let body = tcx.optimized_mir(callee);
405    if body.basic_blocks.len() > FIELD_LOAD_EFFECT_BLOCK_LIMIT || body.arg_count < 1 {
406        return None;
407    }
408
409    for bb in body.basic_blocks.iter() {
410        for stmt in &bb.statements {
411            match &stmt.kind {
412                StatementKind::Assign(assign) => {
413                    let (place, rvalue) = &**assign;
414                    // `_0 = (*_1).<field>` (return value is a field read).
415                    if place.local.as_usize() == 0 && place.projection.is_empty() {
416                        let src_place = match rvalue {
417                            Rvalue::Use(Operand::Copy(p), ..)
418                            | Rvalue::Use(Operand::Move(p), ..) => p,
419                            Rvalue::CopyForDeref(p) => p,
420                            _ => return None,
421                        };
422                        if src_place.local.as_usize() == 1 {
423                            let mut proj = src_place.projection.iter();
424                            if !matches!(proj.next().map(|p| p.kind()), Some(ProjectionElem::Deref))
425                            {
426                                return None;
427                            }
428                            let Some(ProjectionElem::Field(idx, _)) = proj.next().map(|p| p.kind())
429                            else {
430                                return None;
431                            };
432                            if proj.next().is_some() {
433                                return None;
434                            }
435                            return Some(CallEffect::ReturnFieldOfArg {
436                                arg: 0,
437                                field: idx.as_usize(),
438                            });
439                        }
440                        return None;
441                    }
442                    // Any other real statement disqualifies the shape.
443                    return None;
444                }
445                StatementKind::StorageLive(_) | StatementKind::StorageDead(_) => {}
446                _ => return None,
447            }
448        }
449    }
450    None
451}
452
453/// Detect a function that returns a raw-pointer field of its receiver
454/// (`(*self).end_or_len`-shaped), even when the body also contains a
455/// ZST/non-ZST branch and a preceding mutation call (e.g. an iterator's
456/// `next_back_unchecked`). Produces a `ReturnFieldOfArg` effect so the returned
457/// pointer keeps the field's provenance across the interprocedural boundary.
458///
459/// Unlike [`try_field_load_effect`], this does not require the body to be a
460/// single field-load shape — it only requires that *some* return path loads a
461/// raw-pointer field of the receiver directly into the return local. This is a
462/// conservative over-approximation: for a ZST receiver the returned pointer is
463/// never dereferenced (ZST accesses are vacuous), so preferring the raw field
464/// is sound.
465pub(crate) fn try_ptr_field_return_effect(tcx: TyCtxt<'_>, callee: DefId) -> Option<CallEffect> {
466    if !tcx.is_mir_available(callee) {
467        return None;
468    }
469    let body = tcx.optimized_mir(callee);
470    if body.arg_count < 1 {
471        return None;
472    }
473    let ret_ty = body.local_decls[Local::from_usize(0)].ty;
474    if !matches!(ret_ty.kind(), TyKind::RawPtr(..)) {
475        return None;
476    }
477    // A preceding `pre_dec_end(offset)` on the receiver (arg 0) mutates its
478    // `end_or_len` field *before* it is returned (e.g. `next_back_unchecked`).
479    // In that case the returned pointer is `field - offset` elements past the
480    // stored field value; returning the un-adjusted field would point one past
481    // the last element. Record the offset so the effect can be adjusted.
482    let pre_dec_offset = detect_pre_dec_end_offset(tcx, body);
483    // Trace backward from the return local through Copy/Move/Cast/CopyForDeref
484    // assignments until a `(*arg).field` load of the receiver is reached. This
485    // handles the optimized-MIR form where the field is first copied into a
486    // temporary and then cast into the return slot (`_0 = _tmp as *const T`).
487    let mut queue = VecDeque::from([Local::from_usize(0)]);
488    let mut seen = HashSet::from([Local::from_usize(0)]);
489    while let Some(cur) = queue.pop_front() {
490        for bb in body.basic_blocks.iter() {
491            for stmt in &bb.statements {
492                let StatementKind::Assign(assign) = &stmt.kind else {
493                    continue;
494                };
495                let (dest, rvalue) = &**assign;
496                if dest.local != cur || !dest.projection.is_empty() {
497                    continue;
498                }
499                let src: Option<&Place<'_>> = match rvalue {
500                    Rvalue::Use(Operand::Copy(p) | Operand::Move(p), ..) => Some(p),
501                    Rvalue::CopyForDeref(p) => Some(p),
502                    Rvalue::Cast(_, Operand::Copy(p) | Operand::Move(p), _) => Some(p),
503                    _ => None,
504                };
505                let Some(src) = src else {
506                    continue;
507                };
508                // `(*arg).field` — a raw-pointer field of the receiver.
509                if src.local.as_usize() >= 1 && src.local.as_usize() <= body.arg_count {
510                    let mut proj = src.projection.iter();
511                    if matches!(proj.next().map(|p| p.kind()), Some(ProjectionElem::Deref)) {
512                        if let Some(ProjectionElem::Field(idx, _)) = proj.next().map(|p| p.kind()) {
513                            if proj.next().is_none() {
514                                let arg = src.local.as_usize() - 1;
515                                let field = idx.as_usize();
516                                return match pre_dec_offset {
517                                    Some(offset) if offset > 0 => {
518                                        Some(CallEffect::ReturnFieldOfArgSub { arg, field, offset })
519                                    }
520                                    _ => Some(CallEffect::ReturnFieldOfArg { arg, field }),
521                                };
522                            }
523                        }
524                    }
525                }
526                // Otherwise keep tracing through the source local.
527                if src.projection.is_empty() && seen.insert(src.local) {
528                    queue.push_back(src.local);
529                }
530            }
531        }
532    }
533    None
534}
535
536/// Trace a local back through `x = copy y` / `x = move y` assignments to its
537/// copy root (the original loop variable before MIR temporaries).
538fn copy_root(body: &rustc_middle::mir::Body<'_>, mut local: Local) -> Local {
539    let mut seen = HashSet::new();
540    loop {
541        if !seen.insert(local) {
542            break;
543        }
544        let mut next = None;
545        for bb in body.basic_blocks.iter() {
546            for stmt in &bb.statements {
547                let StatementKind::Assign(assign) = &stmt.kind else {
548                    continue;
549                };
550                let (dest, rvalue) = &**assign;
551                if dest.local != local || !dest.projection.is_empty() {
552                    continue;
553                }
554                let Rvalue::Use(op, ..) = rvalue else {
555                    continue;
556                };
557                let (Operand::Copy(p) | Operand::Move(p)) = op else {
558                    continue;
559                };
560                if p.projection.is_empty() {
561                    next = Some(p.local);
562                }
563            }
564        }
565        match next {
566            Some(n) => local = n,
567            None => break,
568        }
569    }
570    local
571}
572
573/// Detect a `memchr`-style search function: it returns `Option<usize>` whose
574/// `Some(i)` payload is an index guarded by a loop condition `i < arg.len()`
575/// (where `arg` is a slice argument).  The summary lets a caller re-prove a
576/// numeric invariant like `finger <= finger_back` after `finger += i + 1`.
577pub(crate) fn try_slice_bounded_return_effect(
578    tcx: TyCtxt<'_>,
579    callee: DefId,
580) -> Option<CallEffect> {
581    if !tcx.is_mir_available(callee) {
582        return None;
583    }
584    let body = tcx.optimized_mir(callee);
585    if body.basic_blocks.len() > SLICE_BOUNDED_RETURN_BLOCK_LIMIT || body.arg_count < 1 {
586        return None;
587    }
588
589    // (1) Find `_0 = Some(payload)` and record the payload's copy root.
590    let mut payload_root: Option<Local> = None;
591    for bb in body.basic_blocks.iter() {
592        for stmt in &bb.statements {
593            let StatementKind::Assign(assign) = &stmt.kind else {
594                continue;
595            };
596            let (place, rvalue) = &**assign;
597            if place.local.as_usize() != 0 || !place.projection.is_empty() {
598                continue;
599            }
600            let Rvalue::Aggregate(kind, operands) = rvalue else {
601                continue;
602            };
603            let rustc_middle::mir::AggregateKind::Adt(adt, variant_idx, ..) = &**kind else {
604                continue;
605            };
606            if !tcx.is_diagnostic_item(rustc_span::sym::Option, *adt) {
607                continue;
608            }
609            if variant_idx.as_usize() != 1 {
610                continue; // not `Some`
611            }
612            let Some(payload) = operands.iter().next() else {
613                continue;
614            };
615            let (Operand::Copy(p) | Operand::Move(p)) = payload else {
616                continue;
617            };
618            if p.projection.is_empty() {
619                payload_root = Some(copy_root(body, p.local));
620            }
621        }
622    }
623    let payload_root = payload_root?;
624
625    // (2) Find `tmp = PtrMetadata(arg)` — the slice argument's length.
626    let mut len_defs: Vec<(Local, usize)> = Vec::new();
627    for bb in body.basic_blocks.iter() {
628        for stmt in &bb.statements {
629            let StatementKind::Assign(assign) = &stmt.kind else {
630                continue;
631            };
632            let (place, rvalue) = &**assign;
633            if !place.projection.is_empty() {
634                continue;
635            }
636            let Rvalue::UnaryOp(op, operand) = rvalue else {
637                continue;
638            };
639            if !matches!(op, rustc_middle::mir::UnOp::PtrMetadata) {
640                continue;
641            }
642            let (Operand::Copy(p) | Operand::Move(p)) = operand else {
643                continue;
644            };
645            if p.projection.is_empty()
646                && p.local.as_usize() >= 1
647                && p.local.as_usize() <= body.arg_count
648            {
649                len_defs.push((place.local, p.local.as_usize() - 1));
650            }
651        }
652    }
653
654    // (3) Find `x = Lt(payload, tmp)` (or `Le`) where `tmp` is a length temp.
655    for bb in body.basic_blocks.iter() {
656        for stmt in &bb.statements {
657            let StatementKind::Assign(assign) = &stmt.kind else {
658                continue;
659            };
660            let (_, rvalue) = &**assign;
661            let Rvalue::BinaryOp(op, pair) = rvalue else {
662                continue;
663            };
664            if !matches!(op, BinOp::Lt | BinOp::Le) {
665                continue;
666            }
667            let (a, b) = &**pair;
668            let a_root = match a {
669                Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => {
670                    copy_root(body, p.local)
671                }
672                _ => continue,
673            };
674            if a_root != payload_root {
675                continue;
676            }
677            let b_local = match b {
678                Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => Some(p.local),
679                _ => None,
680            };
681            let Some(&(_, arg)) = len_defs.iter().find(|(tmp, _)| Some(*tmp) == b_local) else {
682                continue;
683            };
684            return match op {
685                BinOp::Lt => Some(CallEffect::ReturnOptionSomeIndexLtArgLen { arg }),
686                _ => None,
687            };
688        }
689    }
690    None
691}
692
693/// Detect `<Option<T> as Try>::branch`: `Option<T>` -> `ControlFlow<Option<!>, T>`.
694/// The `Continue` payload (field 0) equals the `Some` payload (field 0), so a
695/// `?`-operator `if let Some(..) = expr?` unwrap keeps the payload's provenance.
696pub(crate) fn try_branch_effect(tcx: TyCtxt<'_>, callee: DefId) -> Option<CallEffect> {
697    let name = tcx.def_path_str(callee);
698    if !name.ends_with("::branch") {
699        return None;
700    }
701    if !tcx.is_mir_available(callee) {
702        return None;
703    }
704    let body = tcx.optimized_mir(callee);
705    if body.arg_count != 1 {
706        return None;
707    }
708    // Input is `Option<T>` (the `self` argument).
709    let arg_ty = body.local_decls[Local::from_usize(1)].ty;
710    let TyKind::Adt(arg_adt, _) = arg_ty.kind() else {
711        return None;
712    };
713    if !tcx.is_diagnostic_item(rustc_span::sym::Option, arg_adt.did()) {
714        return None;
715    }
716    // Output is `ControlFlow<..>`.
717    let ret_ty = body.local_decls[Local::from_usize(0)].ty;
718    let TyKind::Adt(ret_adt, _) = ret_ty.kind() else {
719        return None;
720    };
721    if !tcx.def_path_str(ret_adt.did()).contains("ControlFlow") {
722        return None;
723    }
724    Some(CallEffect::ReturnBranchPayload { arg: 0 })
725}
726
727/// Whether block `a` dominates block `b` (every path from the entry to `b`
728/// passes through `a`).  Simple BFS: `a` dominates `b` iff `b` is *not*
729/// reachable from the entry when `a` is skipped.
730fn block_dominates(body: &rustc_middle::mir::Body<'_>, a: BasicBlock, b: BasicBlock) -> bool {
731    let entry = 0usize;
732    let mut queue = VecDeque::from([entry]);
733    let mut seen = HashSet::from([entry]);
734    while let Some(cur) = queue.pop_front() {
735        if cur == b.as_usize() {
736            return false; // reached b without passing a
737        }
738        if cur == a.as_usize() {
739            continue; // skip a's successors
740        }
741        for succ in body.basic_blocks[BasicBlock::from_usize(cur)]
742            .terminator()
743            .successors()
744        {
745            if seen.insert(succ.as_usize()) {
746                queue.push_back(succ.as_usize());
747            }
748        }
749    }
750    true
751}
752
753/// Detect a UTF-8-decoder shape: the function returns `Option<(.., usize, ..)>`
754/// whose length field is a *constant* on each `Some` return, and each
755/// `Some((.., len))` return is guarded by a `slice.get(len - 1)?` (so
756/// `len <= slice.len()`).  Summarizes the tuple's length field as
757/// `field <= arg.len()` so a caller can re-prove `finger <= finger_back` after
758/// `finger += len`.
759pub(crate) fn try_decode_length_return_effect(
760    tcx: TyCtxt<'_>,
761    callee: DefId,
762) -> Option<CallEffect> {
763    if !tcx.is_mir_available(callee) {
764        return None;
765    }
766    let body = tcx.optimized_mir(callee);
767    if body.arg_count < 1 {
768        return None;
769    }
770    // Return type must be `Option<(.., usize, ..)>`.
771    let ret_ty = body.local_decls[Local::from_usize(0)].ty;
772    let TyKind::Adt(adt, substs) = ret_ty.kind() else {
773        return None;
774    };
775    if !tcx.is_diagnostic_item(rustc_span::sym::Option, adt.did()) {
776        return None;
777    }
778    let inner = substs.type_at(0);
779    let TyKind::Tuple(tys) = inner.kind() else {
780        return None;
781    };
782    let Some(field) = tys
783        .iter()
784        .position(|t| matches!(t.kind(), TyKind::Uint(rustc_middle::ty::UintTy::Usize)))
785    else {
786        return None;
787    };
788
789    // Collect `Some((.., L))` returns with constant or computed length `L`.
790    let mut returns: Vec<(BasicBlock, u64)> = Vec::new();
791    let mut computed_returns: Vec<BasicBlock> = Vec::new();
792    for (bb, data) in body.basic_blocks.iter_enumerated() {
793        for stmt in &data.statements {
794            let StatementKind::Assign(assign) = &stmt.kind else {
795                continue;
796            };
797            let (place, rvalue) = &**assign;
798            if place.local.as_usize() != 0 || !place.projection.is_empty() {
799                continue;
800            }
801            let Rvalue::Aggregate(kind, operands) = rvalue else {
802                continue;
803            };
804            let rustc_middle::mir::AggregateKind::Adt(adt, variant_idx, ..) = &**kind else {
805                continue;
806            };
807            if !tcx.is_diagnostic_item(rustc_span::sym::Option, *adt) {
808                continue;
809            }
810            if variant_idx.as_usize() != 1 {
811                continue; // not `Some`
812            }
813            let Some(payload) = operands.iter().next() else {
814                continue;
815            };
816            // Payload is `(code, len)` (or a temp holding it).
817            match tuple_field_len_kind(body, payload, field) {
818                Some(TupleFieldLen::Const(len)) => returns.push((bb, len)),
819                Some(TupleFieldLen::LenSub) => computed_returns.push(bb),
820                None => {}
821            }
822        }
823    }
824    if returns.is_empty() && computed_returns.is_empty() {
825        return None;
826    }
827
828    // Collect `slice.get(k)` calls with a constant `k`.
829    let mut gets: Vec<(BasicBlock, u64)> = Vec::new();
830    for (bb, data) in body.basic_blocks.iter_enumerated() {
831        let TerminatorKind::Call { func, args, .. } = &data.terminator().kind else {
832            continue;
833        };
834        let Some(get_callee) = helpers::dep_callee_def_id(func) else {
835            continue;
836        };
837        if !tcx.opt_associated_item(get_callee).is_some_and(|a| {
838            let name = a.name();
839            matches!(name.as_str(), "get" | "index" | "index_mut")
840        }) {
841            continue;
842        }
843        let Some(k) = args
844            .get(1)
845            .and_then(|a| helpers::operand_const_u64(&a.node))
846        else {
847            continue;
848        };
849        gets.push((bb, k));
850    }
851    if gets.is_empty() && computed_returns.is_empty() {
852        return None;
853    }
854
855    // For each return with length `L`, `get(L - 1)` must dominate it.  The
856    // `L == 1` (ASCII) case only needs the slice to be non-empty, which a
857    // well-formed decoder establishes with its first byte access (often
858    // optimized away from `get(0)` into a direct deref), so it is accepted.
859    for (bb, len) in &returns {
860        if *len <= 1 {
861            continue;
862        }
863        let k = len.checked_sub(1)?;
864        let Some(&(get_bb, _)) = gets.iter().find(|(_, kk)| *kk == k) else {
865            return None;
866        };
867        if !block_dominates(body, get_bb, *bb) {
868            return None;
869        }
870    }
871
872    Some(CallEffect::ReturnOptionSomeTupleFieldLeArgLen { field, arg: 0 })
873}
874
875/// Length kind of a decoder's returned tuple length field.
876enum TupleFieldLen {
877    /// A constant byte length (e.g. `Some((code, 2))`).
878    Const(u64),
879    /// `slice.len() - x` for a non-negative `x` (e.g. `decode_last_char`'s
880    /// `n - lead_idx`), so `len <= slice.len()`.
881    LenSub,
882}
883
884/// Classify the length field of a `(.., len, ..)` tuple, tracing through
885/// copy/move temps and a `_tmp = (..)` tuple aggregate assignment.
886fn tuple_field_len_kind<'tcx>(
887    body: &rustc_middle::mir::Body<'tcx>,
888    operand: &Operand<'tcx>,
889    field: usize,
890) -> Option<TupleFieldLen> {
891    let tuple_local = match operand {
892        Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => p.local,
893        _ => return None,
894    };
895    let mut field_operand: Option<Operand<'tcx>> = None;
896    for bb in body.basic_blocks.iter() {
897        for stmt in &bb.statements {
898            let StatementKind::Assign(assign) = &stmt.kind else {
899                continue;
900            };
901            let (place, rvalue) = &**assign;
902            if place.local != tuple_local || !place.projection.is_empty() {
903                continue;
904            }
905            let Rvalue::Aggregate(kind, operands) = rvalue else {
906                continue;
907            };
908            if !matches!(&**kind, rustc_middle::mir::AggregateKind::Tuple) {
909                continue;
910            }
911            field_operand = operands
912                .get(rustc_abi::FieldIdx::from_usize(field))
913                .cloned();
914        }
915    }
916    let mut cur = field_operand?;
917
918    loop {
919        if let Some(c) = helpers::operand_const_u64(&cur) {
920            return Some(TupleFieldLen::Const(c));
921        }
922        let local = match &cur {
923            Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => p.local,
924            // `SubWithOverflow`/`AddWithOverflow` yield a tuple; the value is
925            // the `.0` field, so strip a single Field(0) projection and keep
926            // tracing the base local.
927            Operand::Copy(p) | Operand::Move(p)
928                if p.projection.len() == 1
929                    && matches!(
930                        p.projection[0].kind(),
931                        rustc_middle::mir::ProjectionElem::Field(rustc_abi::FieldIdx::ZERO, _)
932                    ) =>
933            {
934                p.local
935            }
936            _ => return None,
937        };
938        let mut defining: Option<&Rvalue<'tcx>> = None;
939        for bb in body.basic_blocks.iter() {
940            for stmt in &bb.statements {
941                let StatementKind::Assign(assign) = &stmt.kind else {
942                    continue;
943                };
944                let (place, rvalue) = &**assign;
945                if place.local != local || !place.projection.is_empty() {
946                    continue;
947                }
948                defining = Some(rvalue);
949            }
950        }
951        match defining? {
952            Rvalue::Use(op, ..) => cur = op.clone(),
953            Rvalue::BinaryOp(BinOp::Sub | BinOp::SubWithOverflow, pair) => {
954                // `len = lhs - rhs`; sound iff `lhs == slice.len()` and
955                // `rhs >= 0` (the latter holds because `rhs` is a `usize`).
956                let (lhs, _) = &**pair;
957                return operand_is_ptr_metadata(body, lhs).then_some(TupleFieldLen::LenSub);
958            }
959            _ => return None,
960        }
961    }
962}
963
964/// Whether `operand` (through copy/move temps) is `PtrMetadata(slice)`,
965/// including a `slice.len()` call (which is semantically `PtrMetadata`).
966fn operand_is_ptr_metadata<'tcx>(
967    body: &rustc_middle::mir::Body<'tcx>,
968    operand: &Operand<'tcx>,
969) -> bool {
970    let mut cur = operand.clone();
971    loop {
972        let local = match &cur {
973            Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => p.local,
974            _ => return false,
975        };
976        // A `slice.len()` call result is the slice's length.
977        for bb in body.basic_blocks.iter() {
978            let TerminatorKind::Call {
979                func, destination, ..
980            } = &bb.terminator().kind
981            else {
982                continue;
983            };
984            if destination.local == local
985                && crate::verify::api_classify::is_len(helpers::dep_callee_def_id(func))
986            {
987                return true;
988            }
989        }
990        let mut defining: Option<&Rvalue<'tcx>> = None;
991        for bb in body.basic_blocks.iter() {
992            for stmt in &bb.statements {
993                let StatementKind::Assign(assign) = &stmt.kind else {
994                    continue;
995                };
996                let (place, rvalue) = &**assign;
997                if place.local != local || !place.projection.is_empty() {
998                    continue;
999                }
1000                defining = Some(rvalue);
1001            }
1002        }
1003        match defining {
1004            Some(Rvalue::UnaryOp(op, _)) => {
1005                return matches!(op, rustc_middle::mir::UnOp::PtrMetadata);
1006            }
1007            Some(Rvalue::Use(op, ..)) => cur = op.clone(),
1008            _ => return false,
1009        }
1010    }
1011}
1012
1013/// Detect a `pre_dec_end(offset)` call on the receiver (arg 0) and return its
1014/// constant offset. `next_back_unchecked` calls `self.pre_dec_end(1)` before
1015/// returning the `end_or_len` field, so the returned pointer must be adjusted
1016/// by `offset` elements.
1017fn detect_pre_dec_end_offset<'tcx>(
1018    tcx: TyCtxt<'tcx>,
1019    body: &rustc_middle::mir::Body<'tcx>,
1020) -> Option<u64> {
1021    for bb in body.basic_blocks.iter() {
1022        let Some(term) = &bb.terminator else {
1023            continue;
1024        };
1025        let TerminatorKind::Call { func, args, .. } = &term.kind else {
1026            continue;
1027        };
1028        let Some(callee) = helpers::dep_callee_def_id(func) else {
1029            continue;
1030        };
1031        if !crate::helpers::mir_utils::is_pre_dec_end(tcx, callee) {
1032            continue;
1033        }
1034        // Receiver is arg 0 (the iterator), offset is arg 1.
1035        return args
1036            .get(1)
1037            .and_then(|a| helpers::operand_const_u64(&a.node));
1038    }
1039    None
1040}
1041
1042/// Detect a slice-iterator constructor structurally: a callee whose argument
1043/// is a `&[T]`/`&mut [T]` and whose return type is a struct whose first two
1044/// fields are pointers into `T` (field 0 = start `NonNull<T>`, field 1 = end
1045/// `*const T`/`*mut T`). This matches `slice::Iter`/`IterMut` *and* same-shaped
1046/// local re-implementations by structure rather than by the type's name.
1047pub(super) fn try_iter_constructor_effect<'tcx>(
1048    tcx: TyCtxt<'tcx>,
1049    callee: DefId,
1050) -> Option<CallEffect> {
1051    let fn_sig = tcx.fn_sig(callee).skip_binder();
1052    let output = fn_sig.output().skip_binder();
1053    let TyKind::Adt(adt, substs) = output.kind() else {
1054        return None;
1055    };
1056    let inputs = fn_sig.inputs().skip_binder();
1057    let Some(arg0) = inputs.first() else {
1058        return None;
1059    };
1060    let TyKind::Ref(_, inner, _) = arg0.kind() else {
1061        return None;
1062    };
1063    let TyKind::Slice(elem_ty) = inner.kind() else {
1064        return None;
1065    };
1066    let elem_ty = *elem_ty;
1067    if adt.is_enum() {
1068        return None;
1069    }
1070    let variant = adt.non_enum_variant();
1071    if variant.fields.len() < 2 {
1072        return None;
1073    }
1074    // A pointer field is one of `*const T`/`*mut T` or `NonNull<T>` (a
1075    // `NonNull` pointer wrapper), whose pointee is the slice element type.
1076    let is_elem_ptr = |ty: Ty<'tcx>| -> bool {
1077        match ty.kind() {
1078            TyKind::RawPtr(pointee, _) => *pointee == elem_ty,
1079            TyKind::Adt(a, args)
1080                if crate::verify::api_classify::is_std_nonnull(a.did())
1081                    && args.type_at(0) == elem_ty =>
1082            {
1083                true
1084            }
1085            _ => false,
1086        }
1087    };
1088    let mut fields = variant.fields.iter();
1089    let (Some(f0), Some(f1)) = (fields.next(), fields.next()) else {
1090        return None;
1091    };
1092    if !is_elem_ptr(helpers::field_ty(tcx, f0, substs))
1093        || !is_elem_ptr(helpers::field_ty(tcx, f1, substs))
1094    {
1095        return None;
1096    }
1097    // The shape alone is not sufficient — two pointers into `T` could be an
1098    // unrelated pair. Verify (following at most one thin `_0 = ctor(&*_1)`
1099    // wrapper) that the constructor actually reads the slice's length, which is
1100    // necessary for an iterator that covers the whole slice.
1101    if !iter_ctor_reads_slice_len(tcx, callee, 1) {
1102        return None;
1103    }
1104    Some(CallEffect::ReturnIter { receiver_arg: 0 })
1105}
1106
1107/// Whether the callee (following at most `depth` single-call wrappers) reads
1108/// the length of its slice argument — a necessary condition for a slice
1109/// iterator, whose `end` field is `start + len`.
1110fn iter_ctor_reads_slice_len<'tcx>(tcx: TyCtxt<'tcx>, callee: DefId, depth: usize) -> bool {
1111    if !tcx.is_mir_available(callee) {
1112        return false;
1113    }
1114    let body = tcx.optimized_mir(callee);
1115    if body_reads_slice_len(body) {
1116        return true;
1117    }
1118    if depth > 0 {
1119        if let Some(target) = single_call_wrapper_target(tcx, callee) {
1120            return iter_ctor_reads_slice_len(tcx, target, depth - 1);
1121        }
1122    }
1123    false
1124}
1125
1126/// Whether `body` contains a `len` call whose receiver traces back to argument 0.
1127fn body_reads_slice_len<'tcx>(body: &rustc_middle::mir::Body<'tcx>) -> bool {
1128    for bb in body.basic_blocks.iter() {
1129        let Some(term) = &bb.terminator else { continue };
1130        let TerminatorKind::Call { func, args, .. } = &term.kind else {
1131            continue;
1132        };
1133        if !crate::verify::api_classify::is_len(helpers::dep_callee_def_id(func)) {
1134            continue;
1135        }
1136        if let Some(arg0) = args.first()
1137            && trace_to_callee_arg(body, &arg0.node) == Some(0)
1138        {
1139            return true;
1140        }
1141    }
1142    false
1143}
1144
1145/// The callee delegated to by a thin wrapper whose body is a single call
1146/// returning directly into `_0` (e.g. `slice::iter` → `Iter::new`).
1147fn single_call_wrapper_target<'tcx>(tcx: TyCtxt<'tcx>, callee: DefId) -> Option<DefId> {
1148    let body = tcx.optimized_mir(callee);
1149    let mut found: Option<DefId> = None;
1150    for bb in body.basic_blocks.iter() {
1151        let Some(term) = &bb.terminator else { continue };
1152        let TerminatorKind::Call {
1153            func, destination, ..
1154        } = &term.kind
1155        else {
1156            continue;
1157        };
1158        if destination.local.as_usize() != 0 {
1159            continue;
1160        }
1161        let Some(c) = helpers::dep_callee_def_id(func) else {
1162            return None;
1163        };
1164        match found {
1165            Some(f) if f != c => return None,
1166            _ => found = Some(c),
1167        }
1168    }
1169    found
1170}
1171
1172/// Cached must-write summaries, keyed by `(callee, depth, context)`. Depth is
1173/// part of the key because the `depth > 4` cutoff makes a summary computed
1174/// deeper in the wrapper chain less complete than one computed higher up, and
1175/// the DFS reaches the deep ones first. The context is part of the key because
1176/// one query can reach the same callee with different concrete arguments, which
1177/// prune different paths.
1178type MustWriteMemo = HashMap<(DefId, usize, Vec<(usize, i128)>), Option<HashSet<usize>>>;
1179
1180/// Canonical, sortable representation of a [`CallContext`]'s concrete
1181/// arguments, used as part of the memo key (`FxHashMap` is not `Hash`).
1182fn context_key(context: &CallContext) -> Vec<(usize, i128)> {
1183    let mut entries: Vec<(usize, i128)> = context.concrete.iter().map(|(k, v)| (*k, *v)).collect();
1184    entries.sort_unstable();
1185    entries
1186}
1187
1188/// Return callee argument indices that are definitely written on every
1189/// reachable return path, pruning paths infeasible under `context`. Works for
1190/// any callee with available MIR, and follows wrapper calls
1191/// (`Vec::push` → `push_mut`) with bounded depth.
1192pub(super) fn local_must_write_args(
1193    tcx: TyCtxt<'_>,
1194    callee: DefId,
1195    context: &CallContext,
1196) -> Option<Vec<usize>> {
1197    must_write_args_rec(tcx, callee, 0, context, &mut HashMap::new())
1198        .map(|set| set.into_iter().collect())
1199}
1200
1201fn must_write_args_rec(
1202    tcx: TyCtxt<'_>,
1203    callee: DefId,
1204    depth: usize,
1205    context: &CallContext,
1206    memo: &mut MustWriteMemo,
1207) -> Option<HashSet<usize>> {
1208    if depth > 4 {
1209        return None;
1210    }
1211    if !tcx.is_mir_available(callee) {
1212        return None;
1213    }
1214    if tcx.intrinsic(callee).is_some() || helpers::is_drop_in_place(callee) {
1215        return None;
1216    }
1217    let key = (callee, depth, context_key(context));
1218    if let Some(summary) = memo.get(&key) {
1219        return summary.clone();
1220    }
1221
1222    let summary = helpers::catch_panic(|| {
1223        let body = tcx.optimized_mir(callee);
1224        let mut graph = PathGraph::new(tcx, callee);
1225        graph.find_scc();
1226        let mut enumerator = PathEnumerator::new(&graph);
1227        let paths = enumerator.enumerate_paths_repeat(0);
1228        // An intersection over only some of the paths can claim a write that a
1229        // missing path skips.
1230        if paths.is_truncated() {
1231            return None;
1232        }
1233
1234        let mut must_write: Option<HashSet<usize>> = None;
1235        for path in paths.iter() {
1236            if !path_ends_in_return(body, &path) {
1237                continue;
1238            }
1239            if path_infeasible_under_context(body, &path, context) {
1240                continue;
1241            }
1242            let writes = write_args_on_path(tcx, body, &path, depth, context, memo);
1243            must_write = Some(match must_write {
1244                Some(current) => current.intersection(&writes).copied().collect(),
1245                None => writes,
1246            });
1247        }
1248
1249        Some(must_write.unwrap_or_default())
1250    })
1251    .ok()
1252    .flatten();
1253    memo.insert(key, summary.clone());
1254    summary
1255}
1256
1257/// Return `true` if `path` is provably infeasible under `context`, by folding a
1258/// `SwitchInt` whose discriminant is a direct copy of a concrete argument. Only
1259/// prunes when the taken target is uniquely determined, so a feasible path is
1260/// never removed.
1261fn path_infeasible_under_context(
1262    body: &rustc_middle::mir::Body<'_>,
1263    path: &[usize],
1264    context: &CallContext,
1265) -> bool {
1266    if context.concrete.is_empty() {
1267        return false;
1268    }
1269    for window in path.windows(2) {
1270        let (block, next) = (window[0], window[1]);
1271        let Some(data) = body.basic_blocks.get(BasicBlock::from_usize(block)) else {
1272            continue;
1273        };
1274        let Some(terminator) = &data.terminator else {
1275            continue;
1276        };
1277        let TerminatorKind::SwitchInt { discr, targets } = &terminator.kind else {
1278            continue;
1279        };
1280        let Some(value) = switch_discriminant_concrete(body, discr, context) else {
1281            continue;
1282        };
1283        let expected = targets
1284            .iter()
1285            .find(|(val, _)| *val == value as u128)
1286            .map(|(_, t)| t)
1287            .unwrap_or_else(|| targets.otherwise());
1288        if expected.as_usize() != next {
1289            return true;
1290        }
1291    }
1292    false
1293}
1294
1295/// Trace a `SwitchInt` discriminant back to a concrete argument value, following
1296/// only direct `Copy`/`Move` assignments (no casts or pointer arithmetic) so the
1297/// recovered value is identical to the argument's.
1298fn switch_discriminant_concrete(
1299    body: &rustc_middle::mir::Body<'_>,
1300    discr: &Operand<'_>,
1301    context: &CallContext,
1302) -> Option<i128> {
1303    let local = match discr {
1304        Operand::Copy(place) | Operand::Move(place) => place.local,
1305        _ => return None,
1306    };
1307    let mut queue = VecDeque::from([local]);
1308    let mut seen = HashSet::from([local]);
1309    while let Some(current) = queue.pop_front() {
1310        let cidx = current.as_usize();
1311        if cidx >= 1 && cidx <= body.arg_count {
1312            return context.concrete.get(&(cidx - 1)).copied();
1313        }
1314        for bb in body.basic_blocks.iter() {
1315            for stmt in &bb.statements {
1316                let StatementKind::Assign(assign) = &stmt.kind else {
1317                    continue;
1318                };
1319                if assign.0.local != current {
1320                    continue;
1321                }
1322                let source = match &assign.1 {
1323                    Rvalue::Use(Operand::Copy(place), ..)
1324                    | Rvalue::Use(Operand::Move(place), ..) => place.local,
1325                    _ => continue,
1326                };
1327                if !seen.contains(&source) {
1328                    seen.insert(source);
1329                    queue.push_back(source);
1330                }
1331            }
1332        }
1333    }
1334    None
1335}
1336
1337/// Recognize the standard-library `get_disjoint_check_valid` helper as a
1338/// trusted index-disjoint validator by name.
1339pub(super) fn named_index_disjoint_validator(name: &str) -> Option<(usize, usize)> {
1340    let base = name
1341        .split('<')
1342        .next()
1343        .unwrap_or(name)
1344        .trim_end_matches("::");
1345    if base.ends_with("get_disjoint_check_valid") || base.ends_with("get_disjoint_check_valid_ext")
1346    {
1347        Some((0, 1))
1348    } else {
1349        None
1350    }
1351}
1352
1353/// Detect an "index disjoint validator": a function whose body loads elements
1354/// from an array argument, and returns early (`Err`) both when an element is
1355/// out of range against a scalar argument (`>= len`) and when two elements are
1356/// equal (a duplicate).  Returns `(indices_arg, len_arg)`.
1357pub(super) fn detect_index_disjoint_validator(
1358    tcx: TyCtxt<'_>,
1359    callee: DefId,
1360) -> Option<(usize, usize)> {
1361    callee.as_local()?;
1362    if !tcx.is_mir_available(callee) {
1363        return None;
1364    }
1365    helpers::catch_panic(|| {
1366        let body = tcx.optimized_mir(callee);
1367        let arg_count = body.arg_count;
1368        let mut elem_load_arg: HashSet<(Local, usize)> = HashSet::new();
1369        let mut copy_of_arg: HashSet<(Local, usize)> = HashSet::new();
1370
1371        for bb in body.basic_blocks.iter() {
1372            for stmt in &bb.statements {
1373                let StatementKind::Assign(assign) = &stmt.kind else {
1374                    continue;
1375                };
1376                let (dest, rvalue) = &**assign;
1377                if !dest.projection.is_empty() {
1378                    continue;
1379                }
1380                let Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..) = rvalue else {
1381                    continue;
1382                };
1383                let Some(arg) = helpers::arg_of_local(place.local, arg_count) else {
1384                    continue;
1385                };
1386                if place
1387                    .projection
1388                    .iter()
1389                    .any(|p| matches!(p, ProjectionElem::Index(_)))
1390                {
1391                    elem_load_arg.insert((dest.local, arg));
1392                } else if place.projection.is_empty() {
1393                    copy_of_arg.insert((dest.local, arg));
1394                }
1395            }
1396        }
1397
1398        let elem_arg = |op: &Operand<'_>| -> Option<usize> {
1399            let (Operand::Copy(p) | Operand::Move(p)) = op else {
1400                return None;
1401            };
1402            if !p.projection.is_empty() {
1403                return None;
1404            }
1405            elem_load_arg
1406                .iter()
1407                .find(|(l, _)| *l == p.local)
1408                .map(|(_, a)| *a)
1409        };
1410        let scalar_arg = |op: &Operand<'_>| -> Option<usize> {
1411            let (Operand::Copy(p) | Operand::Move(p)) = op else {
1412                return None;
1413            };
1414            if !p.projection.is_empty() {
1415                return None;
1416            }
1417            helpers::arg_of_local(p.local, arg_count).or_else(|| {
1418                copy_of_arg
1419                    .iter()
1420                    .find(|(l, _)| *l == p.local)
1421                    .map(|(_, a)| *a)
1422            })
1423        };
1424
1425        let mut bounds: Option<(usize, usize)> = None;
1426        let mut disjoint_arg: Option<usize> = None;
1427        for bb in body.basic_blocks.iter() {
1428            for stmt in &bb.statements {
1429                let StatementKind::Assign(assign) = &stmt.kind else {
1430                    continue;
1431                };
1432                let (_, Rvalue::BinaryOp(op, pair)) = &**assign else {
1433                    continue;
1434                };
1435                let (a, b) = &**pair;
1436                match op {
1437                    BinOp::Ge | BinOp::Gt | BinOp::Le | BinOp::Lt => {
1438                        if let (Some(idx), Some(len)) = (elem_arg(a), scalar_arg(b)) {
1439                            bounds = Some((idx, len));
1440                        } else if let (Some(idx), Some(len)) = (elem_arg(b), scalar_arg(a)) {
1441                            bounds = Some((idx, len));
1442                        }
1443                    }
1444                    BinOp::Eq | BinOp::Ne => {
1445                        if let (Some(x), Some(y)) = (elem_arg(a), elem_arg(b))
1446                            && x == y
1447                        {
1448                            disjoint_arg = Some(x);
1449                        }
1450                    }
1451                    _ => {}
1452                }
1453            }
1454        }
1455
1456        match (bounds, disjoint_arg) {
1457            (Some((idx, len)), Some(dj)) if dj == idx && idx != len => Some((idx, len)),
1458            _ => None,
1459        }
1460    })
1461    .ok()
1462    .flatten()
1463}
1464fn path_ends_in_return(body: &rustc_middle::mir::Body<'_>, path: &[usize]) -> bool {
1465    path.last().is_some_and(|block| {
1466        body.basic_blocks
1467            .get(BasicBlock::from_usize(*block))
1468            .and_then(|data| data.terminator.as_ref())
1469            .is_some_and(|terminator| matches!(terminator.kind, TerminatorKind::Return))
1470    })
1471}
1472
1473fn write_args_on_path<'tcx>(
1474    tcx: TyCtxt<'tcx>,
1475    body: &rustc_middle::mir::Body<'tcx>,
1476    path: &[usize],
1477    depth: usize,
1478    context: &CallContext,
1479    memo: &mut MustWriteMemo,
1480) -> HashSet<usize> {
1481    let mut writes = HashSet::new();
1482    for block in path {
1483        let Some(data) = body.basic_blocks.get(BasicBlock::from_usize(*block)) else {
1484            continue;
1485        };
1486
1487        // Direct writes through `&mut` args: `*self = ...`, `(*self).0 = ...`.
1488        for stmt in &data.statements {
1489            let StatementKind::Assign(assign) = &stmt.kind else {
1490                continue;
1491            };
1492            let dest = &assign.0;
1493            if dest.projection.first() == Some(&ProjectionElem::Deref) {
1494                if let Some(arg) = helpers::arg_of_local(dest.local, body.arg_count) {
1495                    writes.insert(arg);
1496                }
1497            }
1498        }
1499
1500        let Some(terminator) = data.terminator.as_ref() else {
1501            continue;
1502        };
1503        let TerminatorKind::Call { func, args, .. } = &terminator.kind else {
1504            continue;
1505        };
1506
1507        // `ptr::write`-style writes: trace the pointer arg to a callee arg.
1508        if crate::verify::api_classify::is_ptr_write(helpers::dep_callee_def_id(func)) {
1509            if let Some(pointer_arg) = args
1510                .first()
1511                .and_then(|arg| trace_to_callee_arg(body, &arg.node))
1512            {
1513                writes.insert(pointer_arg);
1514            }
1515            continue;
1516        }
1517
1518        // Wrapper calls: a nested callee that writes its own args maps those
1519        // writes back onto this callee's args. The nested callee sees its own
1520        // argument positions, so rebuild its context from this call's arguments
1521        // (its own literals plus the outer concrete values passed through).
1522        if let Some(nested) = helpers::dep_callee_def_id(func) {
1523            let nested_context = nested_call_context(body, args, context);
1524            if let Some(nested_writes) =
1525                must_write_args_rec(tcx, nested, depth + 1, &nested_context, memo)
1526            {
1527                for (i, arg) in args.iter().enumerate() {
1528                    if nested_writes.contains(&i) {
1529                        if let Some(outer) = trace_to_callee_arg(body, &arg.node) {
1530                            writes.insert(outer);
1531                        }
1532                    }
1533                }
1534            }
1535        }
1536    }
1537    writes
1538}
1539
1540/// Build the `CallContext` a nested call sees, keyed by the *nested* callee's
1541/// own argument indices. Each nested argument is concrete either because it is a
1542/// literal at this call site, or because it passes an outer concrete value
1543/// straight through (`Copy`/`Move` of an argument). This keeps a caller's
1544/// literal at position `i` from being read as the nested callee's position-`i`
1545/// argument.
1546fn nested_call_context<'tcx>(
1547    body: &rustc_middle::mir::Body<'tcx>,
1548    args: &[Spanned<Operand<'tcx>>],
1549    context: &CallContext,
1550) -> CallContext {
1551    let mut nested_context = CallContext::default();
1552    for (i, arg) in args.iter().enumerate() {
1553        if let Some(v) = helpers::operand_const_u64(&arg.node) {
1554            nested_context.concrete.insert(i, v as i128);
1555        } else if let Some(outer) = trace_to_callee_arg(body, &arg.node) {
1556            if let Some(v) = context.concrete.get(&outer) {
1557                nested_context.concrete.insert(i, *v);
1558            }
1559        }
1560    }
1561    nested_context
1562}
1563
1564/// Return true when `call_dest`'s value flows (via Copy/Move/Cast) to the
1565/// function's return place `_0`.
1566fn call_result_reaches_return<'tcx>(
1567    body: &rustc_middle::mir::Body<'tcx>,
1568    call_dest: Local,
1569) -> bool {
1570    let ret = Local::from_usize(0);
1571    let mut queue = VecDeque::from([call_dest]);
1572    let mut seen = HashSet::from([call_dest]);
1573    while let Some(current) = queue.pop_front() {
1574        if current == ret {
1575            return true;
1576        }
1577        for bb in body.basic_blocks.iter() {
1578            for stmt in &bb.statements {
1579                let StatementKind::Assign(assign) = &stmt.kind else {
1580                    continue;
1581                };
1582                let dest = assign.0.local;
1583                if seen.contains(&dest) {
1584                    continue;
1585                }
1586                match &assign.1 {
1587                    Rvalue::Use(Operand::Copy(place), ..)
1588                    | Rvalue::Use(Operand::Move(place), ..)
1589                    | Rvalue::Cast(_, Operand::Copy(place), _)
1590                    | Rvalue::Cast(_, Operand::Move(place), _) => {
1591                        if place.local == current {
1592                            queue.push_back(dest);
1593                            seen.insert(dest);
1594                        }
1595                    }
1596                    _ => {}
1597                }
1598            }
1599        }
1600    }
1601    false
1602}
1603
1604/// Return true if the callee body contains any Call terminator, meaning the
1605/// callee is not self-contained (a nested call may have side effects that a
1606/// shallow summary cannot capture).
1607pub(super) fn callee_calls_other_local(tcx: TyCtxt<'_>, callee: DefId) -> bool {
1608    let body = tcx.optimized_mir(callee);
1609    for bb in body.basic_blocks.iter() {
1610        if matches!(
1611            bb.terminator().kind,
1612            rustc_middle::mir::TerminatorKind::Call { .. }
1613        ) {
1614            return true;
1615        }
1616    }
1617    false
1618}