Skip to main content

rapx/verify/property_checker/
bounds.rs

1//! Checkers for `InBound` and `NonOverlap`.
2//!
3//! Bounds are discharged from `has_checked_bounds` facts, layout field-offset
4//! invariants, or an SMT coverage check over allocation base/size.
5//! `NonOverlap` uses provenance-distinctness and range-overlap reasoning.
6
7use crate::helpers::mir_scan::Checkpoint;
8use crate::verify::api_classify;
9use crate::verify::contract::{
10    ContractExpr, NumericBinOp, PlaceBase, Property, PropertyArg, RelOp,
11};
12use crate::verify::report::{CheckResult, UnknownReason};
13use crate::verify::vm::state::{OffsetKind, VmState, VmValue};
14use rustc_middle::mir::{Local, Operand, Rvalue, StatementKind};
15use rustc_middle::ty::{Ty, TyKind};
16use z3::{
17    SatResult, Solver,
18    ast::{Ast, Bool, Int},
19};
20
21use super::PropertyChecker;
22
23impl PropertyChecker {
24    pub(super) fn check_in_bound<'z3, 'tcx>(
25        &self,
26        vm_state: &VmState<'z3, 'tcx>,
27        solver: &Solver<'z3>,
28        checkpoint: &Checkpoint<'tcx>,
29        property: &Property<'tcx>,
30    ) -> CheckResult {
31        // Fast-path: if a prior ChecksIndexBoundsDisjoint call already
32        // validated bounds for this function, the InBound holds.
33        if vm_state.path_facts.has_checked_bounds {
34            return CheckResult::ProvedByRule;
35        }
36        // Fast-path: contract with for_each guarantees all elements
37        // of the index array are in bounds.
38        if property.for_each().is_some() {
39            return CheckResult::ProvedByRule;
40        }
41
42        if let Some(PropertyArg::Expr(ContractExpr::IndexAccess { index: _, .. })) =
43            property.args().first()
44        {
45            return self.check_in_bound_slice(vm_state, solver, checkpoint, property);
46        }
47
48        let required_ty = Self::ty_arg(property, 1);
49        if self.zst_guard(vm_state, checkpoint, property) {
50            return CheckResult::ProvedByRule;
51        }
52
53        let Some(value) = self.target_value(vm_state, checkpoint, property) else {
54            return CheckResult::Unknown(UnknownReason::Unimplemented);
55        };
56        // `in_bounds` records "points at a valid element" (established by a
57        // single-element deref/contract), so it only discharges a single-element
58        // InBound.  A `count > 1` check still needs the byte-range proof below.
59        if value.facts.in_bounds {
60            let count_one = property
61                .args()
62                .get(2)
63                .and_then(|a| self.resolve_arg_term(vm_state, checkpoint, a))
64                .is_none_or(|c| c.simplify().as_u64() == Some(1));
65            if count_one {
66                return CheckResult::ProvedByRule;
67            }
68        }
69        if matches!(value.ty.kind(), TyKind::Ref(..)) {
70            return CheckResult::ProvedByRule;
71        }
72        if value.is_pointer() {
73            if let TyKind::Adt(adt_def, _) = value.ty.kind() {
74                if api_classify::is_std_nonnull(adt_def.did()) {
75                    return CheckResult::ProvedByRule;
76                }
77            }
78        }
79        // `byte_add(offset_of!(Container, field))` always keeps the pointer
80        // within the container allocation, because the byte offset of a field
81        // never exceeds `size_of::<Container>()`.  This covers patterns such
82        // as `Option::as_slice`.
83        if self.count_is_offset_of(vm_state, checkpoint, property, &value) {
84            return CheckResult::ProvedByRule;
85        }
86        // When the contract expression for the element count evaluates to
87        // zero (e.g. div-by-sizeof for ZST generic params), the byte-level
88        // access is zero and limits checking is trivial.
89        if self.count_is_zero(vm_state, checkpoint, property, 2) {
90            return CheckResult::ProvedByRule;
91        }
92        let access = self.access_bytes(vm_state, property, 1, 2, checkpoint, &value);
93        let Some(alloc_id) = value.provenance_alloc_id() else {
94            return CheckResult::Unknown(UnknownReason::Unimplemented);
95        };
96        let base = vm_state.allocation_base(alloc_id).clone();
97        let size = vm_state.allocation_size(alloc_id).clone();
98
99        let alloc = vm_state.alloc(alloc_id);
100        if let (Some(alloc_elem_ty), Some(req_ty)) = (alloc.element_ty.as_ty(), required_ty) {
101            if self.alloc_elem_is_array_of(alloc_elem_ty, req_ty) {
102                return CheckResult::ProvedByRule;
103            }
104        }
105
106        // An external allocation whose size is the `i64::MAX` "unbounded"
107        // sentinel (a Vec/slice buffer, or a materialized `Allocated` fact)
108        // auto-passes any `InBound` access.  A *raw-pointer target* carries a
109        // symbolic "unknown" size instead, so its access falls through and must
110        // be proved (and otherwise fails).
111        if alloc.is_external() && size.simplify().as_u64() == Some(i64::MAX as u64) {
112            return CheckResult::ProvedByRule;
113        }
114
115        // Element-level bounds fast-path: when the allocation materializes a
116        // slice length and the pointer was derived by element-strided arithmetic
117        // (its `element_offset` is tracked), check `k + count <= len` linearly.
118        // This avoids the non-linear `(k + count)·S <= len·S` byte form for a
119        // generic element size `S`, which Z3's NIA solver cannot decide.
120        // `sub` walks *backwards* (`[value - access, value)`), so this forward
121        // form would mis-classify `end.sub(n)` as out of bounds; the `sub`-aware
122        // byte-range proof below handles that direction.
123        if !api_classify::is_pointer_sub(checkpoint.callee) {
124            if let (Some(len), Some(k)) = (
125                alloc.slice_len().cloned(),
126                value
127                    .provenance
128                    .as_ref()
129                    .and_then(|p| match &p.offset_kind {
130                        Some(OffsetKind::Element(e)) => Some(e.clone()),
131                        _ => None,
132                    }),
133            ) {
134                let count_term = property
135                    .args()
136                    .get(2)
137                    .and_then(|a| self.resolve_arg_term(vm_state, checkpoint, a))
138                    .unwrap_or_else(|| Int::from_u64(vm_state.z3_ctx, 1));
139                let zero = Int::from_u64(vm_state.z3_ctx, 0);
140                let covered = Int::add(vm_state.z3_ctx, &[&k, &count_term]);
141                solver.push();
142                solver.assert(&z3::ast::Bool::or(
143                    vm_state.z3_ctx,
144                    &[&covered.gt(&len), &k.lt(&zero)],
145                ));
146                let r = match solver.check() {
147                    SatResult::Unsat => CheckResult::ProvedBySmt,
148                    SatResult::Sat => CheckResult::Failed,
149                    _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
150                };
151                solver.pop(1);
152                return r;
153            }
154        }
155
156        let alloc_elem_is_generic = vm_state
157            .alloc(alloc_id)
158            .element_ty
159            .as_ty()
160            .is_some_and(|ty| matches!(ty.kind(), TyKind::Param(_)));
161        let fallback_for_generic =
162            alloc_elem_is_generic && !size.as_u64().is_some() && !access.as_u64().is_some();
163
164        solver.push();
165        // A field-offset pointer (`byte_add(offset_of!())`) is valid within the
166        // *field* it addresses: the accessed range must fit in the field's own
167        // size.  "The field lies inside its container" is a layout invariant
168        // that needs no proof here, so skip the container-coverage check below
169        // (whose container layout may be unknown, e.g. `Option<T>`).
170        if value
171            .provenance
172            .as_ref()
173            .is_some_and(|prov| matches!(prov.offset_kind, Some(OffsetKind::Field)))
174        {
175            let field_size = crate::helpers::mir_utils::pointee_ty(value.ty)
176                .map(|ty| vm_state.size_sym_read(ty))
177                .unwrap_or_else(|| Int::from_u64(vm_state.z3_ctx, 1));
178            solver.assert(&access.le(&field_size).not());
179            let r = match solver.check() {
180                SatResult::Unsat => CheckResult::ProvedBySmt,
181                SatResult::Sat => CheckResult::Failed,
182                _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
183            };
184            solver.pop(1);
185            return r;
186        }
187
188        let bound = Int::add(vm_state.z3_ctx, &[&base, &size]);
189        // `sub` walks *backwards*: the accessed range is `[value - access, value)`,
190        // so the lower bound is `value - access >= base` and the upper bound is
191        // `value <= base + size`.  `add` (and everything else) walks forwards.
192        let (above_negated, below_negated) = if api_classify::is_pointer_sub(checkpoint.callee) {
193            let walked = Int::sub(vm_state.z3_ctx, &[&value.z3_term, &access]);
194            (value.z3_term.gt(&bound), walked.lt(&base))
195        } else {
196            let covered = Int::add(vm_state.z3_ctx, &[&value.z3_term, &access]);
197            (covered.gt(&bound), value.z3_term.lt(&base))
198        };
199        let negated = z3::ast::Bool::or(vm_state.z3_ctx, &[&above_negated, &below_negated]);
200
201        // Generic element size: discharge by a case split on `S = 0` (ZST) vs
202        // `S ≥ 1` (non-ZST) rather than a single nonlinear query.
203        if let Some(s) = vm_state.generic_elem_size(alloc_id) {
204            solver.pop(1);
205            let on_sat = if fallback_for_generic {
206                CheckResult::Unknown(UnknownReason::Unimplemented)
207            } else {
208                CheckResult::Failed
209            };
210            return Self::smt_check_size_split(vm_state, &s, &negated, on_sat);
211        }
212
213        solver.assert(&negated);
214        let sat_result = solver.check();
215        let r = match sat_result {
216            SatResult::Unsat => CheckResult::ProvedBySmt,
217            SatResult::Sat if fallback_for_generic => {
218                CheckResult::Unknown(UnknownReason::Unimplemented)
219            }
220            SatResult::Sat => CheckResult::Failed,
221            _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
222        };
223        solver.pop(1);
224        r
225    }
226
227    pub(super) fn count_is_offset_of<'z3, 'tcx>(
228        &self,
229        vm_state: &VmState<'z3, 'tcx>,
230        checkpoint: &Checkpoint<'tcx>,
231        property: &Property<'tcx>,
232        value: &VmValue<'z3, 'tcx>,
233    ) -> bool {
234        let Some(count_arg) = property.args().get(2) else {
235            return false;
236        };
237        let PropertyArg::Expr(ContractExpr::Place(cp)) = count_arg else {
238            return false;
239        };
240        let PlaceBase::Arg(n) = cp.base else {
241            return false;
242        };
243        let Some(operand) = checkpoint.args.get(n) else {
244            return false;
245        };
246        let Operand::Constant(c) = operand else {
247            return false;
248        };
249        let Some(container) =
250            crate::helpers::mir_utils::offset_of_container(vm_state.tcx, &c.const_)
251        else {
252            return false;
253        };
254        // The pointer must be the base of its allocation (offset 0), otherwise
255        // adding the field offset could overflow the container end.
256        let at_base = value
257            .provenance
258            .as_ref()
259            .is_some_and(|p| p.offset.as_u64() == Some(0));
260        if !at_base {
261            return false;
262        }
263        // The allocation must be the same container the offset was computed on.
264        crate::helpers::mir_utils::pointee_ty(value.ty).is_some_and(|pointee| pointee == container)
265    }
266
267    pub(super) fn resolve_index_access_args(
268        property: &Property<'_>,
269    ) -> (Option<usize>, Option<usize>) {
270        if let Some(PropertyArg::Expr(ContractExpr::IndexAccess { slice, index })) =
271            property.args().first()
272        {
273            let slice_idx = Self::extract_place_arg_index(slice);
274            let index_idx = Self::extract_place_arg_index(index);
275            (slice_idx, index_idx)
276        } else {
277            (Some(0), Some(1))
278        }
279    }
280
281    pub(super) fn extract_place_arg_index(expr: &ContractExpr<'_>) -> Option<usize> {
282        match expr {
283            ContractExpr::Place(cp) => match cp.base {
284                PlaceBase::Arg(n) => Some(n),
285                _ => None,
286            },
287            _ => None,
288        }
289    }
290
291    pub(super) fn check_in_bound_slice<'z3, 'tcx>(
292        &self,
293        vm_state: &VmState<'z3, 'tcx>,
294        solver: &Solver<'z3>,
295        checkpoint: &Checkpoint<'tcx>,
296        property: &Property<'tcx>,
297    ) -> CheckResult {
298        let (slice_arg_idx, index_arg_idx) = Self::resolve_index_access_args(property);
299
300        let slice_val = match slice_arg_idx.and_then(|idx| checkpoint.args.get(idx)) {
301            Some(op) => vm_state.value_of_operand(op),
302            None => return CheckResult::Unknown(UnknownReason::Unimplemented),
303        };
304        let (index_val, is_range) = match index_arg_idx.and_then(|idx| checkpoint.args.get(idx)) {
305            Some(op) => {
306                if let Some(end_val) = self.extract_range_end(vm_state, op) {
307                    (end_val, true)
308                } else {
309                    (vm_state.value_of_operand(op), false)
310                }
311            }
312            None => return CheckResult::Unknown(UnknownReason::Unimplemented),
313        };
314
315        let data_alloc_id = slice_val.provenance_alloc_id();
316        let Some(data_alloc_id) = data_alloc_id else {
317            return CheckResult::Unknown(UnknownReason::Unimplemented);
318        };
319
320        // Prefer the materialized slice length; fall back to `size / elem_size`
321        // for allocations that never got a materialized `slice_len`.
322        let len = vm_state
323            .slice_len_from_value(&slice_val)
324            .unwrap_or_else(|| {
325                let size = vm_state.allocation_size(data_alloc_id).clone();
326                let elem_sz = vm_state
327                    .alloc(data_alloc_id)
328                    .element_ty
329                    .as_ty()
330                    .map(|ty| vm_state.size_sym_read(ty))
331                    .unwrap_or_else(|| Int::from_u64(vm_state.z3_ctx, 1));
332                size.div(&elem_sz)
333            });
334
335        solver.push();
336        let negated = if is_range {
337            // For range-based InBound (start..end), check end <= len
338            index_val.z3_term.le(&len).not()
339        } else {
340            // For single-element InBound (index), check index < len — the same
341            // strict bound recorded by `assert_in_bound_single`.
342            index_val.z3_term.lt(&len).not()
343        };
344        solver.assert(&negated);
345        let r = match solver.check() {
346            SatResult::Unsat => CheckResult::ProvedBySmt,
347            SatResult::Sat => CheckResult::Failed,
348            _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
349        };
350        solver.pop(1);
351        r
352    }
353
354    pub(super) fn extract_range_end<'z3, 'tcx>(
355        &self,
356        vm_state: &VmState<'z3, 'tcx>,
357        op: &Operand<'tcx>,
358    ) -> Option<VmValue<'z3, 'tcx>> {
359        let place = match op {
360            Operand::Copy(p) | Operand::Move(p) => p,
361            _ => return None,
362        };
363        if !place.projection.is_empty() {
364            return None;
365        }
366        let range_local = place.local;
367        let ty = vm_state.body().local_decls[range_local].ty;
368        let adt_def = match ty.kind() {
369            TyKind::Adt(adt_def, _) => *adt_def,
370            _ => return None,
371        };
372        // The `end` field index depends on the range kind: `Range`/`RangeInclusive`
373        // store `(start, end)` (end at field 1), while `RangeTo` stores just `end`
374        // (field 0). `RangeFrom`/`RangeFull`/`RangeToInclusive` have no usable end
375        // field here and fall back to the single-index path.
376        let end_idx = match crate::helpers::mir_utils::range_kind(vm_state.tcx, adt_def.did()) {
377            crate::helpers::mir_utils::RangeKind::RangeTo => {
378                Some(rustc_abi::FieldIdx::from_usize(0))
379            }
380            crate::helpers::mir_utils::RangeKind::Range
381            | crate::helpers::mir_utils::RangeKind::RangeInclusive => {
382                Some(rustc_abi::FieldIdx::from_usize(1))
383            }
384            crate::helpers::mir_utils::RangeKind::Other => {
385                // `core::ops::IndexRange` is a private `{ start, end }` struct
386                // (no lang item); its `end` lives at field 1 like `Range`.
387                let name = vm_state.tcx.def_path_str(adt_def.did());
388                if name.ends_with("::IndexRange") || name == "IndexRange" {
389                    Some(rustc_abi::FieldIdx::from_usize(1))
390                } else {
391                    None
392                }
393            }
394            _ => None,
395        };
396        let Some(end_idx) = end_idx else { return None };
397        for block in vm_state.body().basic_blocks.iter() {
398            for stmt in &block.statements {
399                if let StatementKind::Assign(assign) = &stmt.kind {
400                    let (dest, rvalue) = &**assign;
401                    if dest.local == range_local && dest.projection.is_empty() {
402                        if let Rvalue::Aggregate(_kind, operands) = rvalue {
403                            if let Some(end_op) = operands.get(end_idx) {
404                                return Some(self.trace_value(vm_state, end_op));
405                            }
406                        }
407                    }
408                }
409            }
410        }
411        None
412    }
413
414    pub(super) fn check_non_overlap<'z3, 'tcx>(
415        &self,
416        vm_state: &VmState<'z3, 'tcx>,
417        solver: &Solver<'z3>,
418        checkpoint: &Checkpoint<'tcx>,
419        property: &Property<'tcx>,
420    ) -> CheckResult {
421        let Some(v1) = self.target_value(vm_state, checkpoint, property) else {
422            return CheckResult::Unknown(UnknownReason::Unimplemented);
423        };
424        // Get the second pointer from the property args (not from checkpoint directly).
425        // The property may reference the two pointers in any order (e.g. dst at args[0]).
426        let v2 = property
427            .args()
428            .get(1)
429            .and_then(|a| {
430                let cp = match a {
431                    PropertyArg::Expr(ContractExpr::Place(cp)) => cp.clone(),
432                    _ => return None,
433                };
434                match cp.base {
435                    PlaceBase::Arg(n) => checkpoint
436                        .args
437                        .get(n)
438                        .map(|op| vm_state.value_of_operand(op)),
439                    PlaceBase::Local(n) => vm_state.local_value(Local::from_usize(n)).cloned(),
440                    _ => None,
441                }
442            })
443            .or_else(|| {
444                checkpoint
445                    .args
446                    .get(1)
447                    .map(|op| vm_state.value_of_operand(op))
448            });
449        let Some(v2) = v2 else {
450            // Without a second pointer we cannot prove non-overlap.
451            return CheckResult::Unknown(UnknownReason::Unimplemented);
452        };
453        // Only discharge non-overlap when both pointers carry provenance into
454        // distinct allocations. `Option` comparison here is unsound: a `None`
455        // (unknown) provenance would compare unequal to any concrete `AllocId`
456        // and spuriously report the pointers as non-overlapping.
457        if let (Some(a), Some(b)) = (v1.provenance_alloc_id(), v2.provenance_alloc_id()) {
458            if a != b {
459                return CheckResult::ProvedByRule;
460            }
461        }
462
463        // Try range-based overlap detection when count and element size are available.
464        if let Some(count_term) = checkpoint
465            .args
466            .get(2)
467            .map(|op| vm_state.value_of_operand(op).z3_term)
468        {
469            // Use the pointee element size from either pointer type.
470            let elem_size = vm_state
471                .pointee_elem_size(v1.ty)
472                .max(vm_state.pointee_elem_size(v2.ty))
473                .max(1);
474            if let Some(count) = count_term.simplify().as_u64() {
475                let range = Int::from_u64(vm_state.z3_ctx, elem_size * count.max(1));
476                let src_end = Int::add(vm_state.z3_ctx, &[&v1.z3_term, &range]);
477                let dst_end = Int::add(vm_state.z3_ctx, &[&v2.z3_term, &range]);
478                solver.push();
479                let overlap = Bool::and(
480                    vm_state.z3_ctx,
481                    &[&v1.z3_term.lt(&dst_end), &v2.z3_term.lt(&src_end)],
482                );
483                solver.assert(&overlap);
484                let r = match solver.check() {
485                    SatResult::Unsat => CheckResult::ProvedBySmt,
486                    SatResult::Sat => CheckResult::Failed,
487                    _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
488                };
489                solver.pop(1);
490                return r;
491            }
492        }
493
494        // Fallback: check pointer-distinctness.
495        solver.push();
496        let ne = v1.z3_term._eq(&v2.z3_term).not();
497        solver.assert(&ne);
498        let r = match solver.check() {
499            SatResult::Unsat => CheckResult::ProvedBySmt,
500            SatResult::Sat => CheckResult::Failed,
501            _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
502        };
503        solver.pop(1);
504        r
505    }
506
507    pub(super) fn all_predicates_are_slice_size_invariant<'z3, 'tcx>(
508        &self,
509        vm_state: &VmState<'z3, 'tcx>,
510        checkpoint: &Checkpoint<'tcx>,
511        predicates: &[crate::verify::contract::NumericPredicate<'tcx>],
512    ) -> bool {
513        !predicates.is_empty()
514            && predicates
515                .iter()
516                .all(|p| self.predicate_is_slice_size_invariant(vm_state, checkpoint, p))
517    }
518
519    pub(super) fn predicate_is_slice_size_invariant<'z3, 'tcx>(
520        &self,
521        vm_state: &VmState<'z3, 'tcx>,
522        checkpoint: &Checkpoint<'tcx>,
523        pred: &crate::verify::contract::NumericPredicate<'tcx>,
524    ) -> bool {
525        if !matches!(pred.op, RelOp::Le | RelOp::Lt) {
526            return false;
527        }
528        // rhs must be >= isize::MAX (the language invaraint bound)
529        let ContractExpr::Const(bound) = &pred.rhs else {
530            return false;
531        };
532        if *bound < i64::MAX as u128 {
533            return false;
534        }
535        // lhs must be size_of(T) * count
536        let ContractExpr::Binary {
537            op: NumericBinOp::Mul,
538            lhs,
539            rhs,
540        } = &pred.lhs
541        else {
542            return false;
543        };
544        let (size_ty, count_expr) = match (lhs.as_ref(), rhs.as_ref()) {
545            (ContractExpr::SizeOf(ty), count) => (*ty, count),
546            (count, ContractExpr::SizeOf(ty)) => (*ty, count),
547            _ => return false,
548        };
549        // Resolve SizeOf type via callsite substitutions
550        let resolved_ty = self.instantiate_callsite_ty(vm_state, checkpoint, size_ty);
551        // count must be a Place referencing a callsite arg
552        self.count_derives_from_slice_param(vm_state, checkpoint, count_expr, resolved_ty)
553    }
554
555    pub(super) fn count_derives_from_slice_param<'z3, 'tcx>(
556        &self,
557        vm_state: &VmState<'z3, 'tcx>,
558        checkpoint: &Checkpoint<'tcx>,
559        count_expr: &ContractExpr<'tcx>,
560        elem_ty: Ty<'tcx>,
561    ) -> bool {
562        // Must be a Place, not a constant literal
563        let ContractExpr::Place(cp) = count_expr else {
564            return false;
565        };
566        if !cp.projections.is_empty() {
567            return false;
568        }
569        let Some(local) = cp.local_base() else {
570            return false;
571        };
572        if local == 0 {
573            return false;
574        }
575        let Some(callee) = checkpoint.callee else {
576            return false;
577        };
578        let Some(arg_idx) =
579            crate::helpers::mir_utils::callee_param_index_for_local(vm_state.tcx, callee, local)
580        else {
581            return false;
582        };
583        // Reject constant literal arguments (like usize::MAX)
584        if matches!(checkpoint.args.get(arg_idx), Some(Operand::Constant(_))) {
585            return false;
586        }
587        // Check caller has a matching slice reference parameter.
588        let body = vm_state.body();
589        let has_slice_param = (1..=body.arg_count).any(|i| {
590            let param_ty = body.local_decls[Local::from_usize(i)].ty;
591            self.is_slice_ref_with_elem(param_ty, elem_ty, vm_state, checkpoint)
592        });
593        if has_slice_param {
594            return true;
595        }
596        // No direct slice param — check if the pointer has provenance from
597        // an external allocation (raw pointer params get this in init_parameters).
598        if let Some(op) = checkpoint.args.first() {
599            let target_val = vm_state.value_of_operand(op);
600            if target_val.is_pointer() {
601                return true;
602            }
603        }
604        false
605    }
606
607    pub(super) fn is_slice_ref_with_elem<'z3, 'tcx>(
608        &self,
609        ty: Ty<'tcx>,
610        elem_ty: Ty<'tcx>,
611        vm_state: &VmState<'z3, 'tcx>,
612        checkpoint: &Checkpoint<'tcx>,
613    ) -> bool {
614        let rustc_middle::ty::TyKind::Ref(_, inner, _) = ty.kind() else {
615            return false;
616        };
617        match inner.kind() {
618            rustc_middle::ty::TyKind::Slice(slice_elem) => {
619                let resolved = self.instantiate_callsite_ty(vm_state, checkpoint, *slice_elem);
620                self.same_erased_ty(vm_state, resolved, elem_ty)
621            }
622            _ => false,
623        }
624    }
625
626    pub(super) fn same_erased_ty<'z3, 'tcx>(
627        &self,
628        vm_state: &VmState<'z3, 'tcx>,
629        a: Ty<'tcx>,
630        b: Ty<'tcx>,
631    ) -> bool {
632        vm_state.size_of_ty(a) > 0
633            && vm_state.size_of_ty(b) > 0
634            && vm_state.size_of_ty(a) == vm_state.size_of_ty(b)
635    }
636
637    pub(super) fn is_caller_type_param<'z3, 'tcx>(
638        &self,
639        vm_state: &VmState<'z3, 'tcx>,
640        ty: Ty<'tcx>,
641    ) -> bool {
642        let rustc_middle::ty::TyKind::Param(param_ty) = ty.kind() else {
643            return false;
644        };
645        let generics = vm_state.tcx.generics_of(vm_state.current_frame.current_def_id);
646        generics.own_params.iter().any(|p| {
647            matches!(p.kind, rustc_middle::ty::GenericParamDefKind::Type { .. })
648                && p.name == param_ty.name
649        })
650    }
651}