rapx/verify/smt_check/
common.rs

1//! Common SMT checking backend for the staged verifier.
2//!
3//! The SMT layer consumes the abstract facts produced by `verifying` and
4//! exposes one property-oriented entry point. Safety properties do not call Z3
5//! directly. Instead, each property-specific module lowers its requirement into
6//! one of the common SMT obligations below, and the common backend discharges
7//! that obligation against the path-local abstract facts.
8//!
9//! Current common obligations:
10//!
11//! - `SmtObligation::Aligned`: prove `addr(place) % align == 0`.
12//! - `SmtObligation::NonZero`: prove `addr(place) != 0`.
13//! - `SmtObligation::Range`: reserved for future bounds-style checks.
14//!
15//! Current property lowering:
16//!
17//! - `Align(p, T)` lowers to `Aligned { place: p, align: align_of(T) }`.
18//! - `NonNull(p)` lowers to `NonZero { place: p }`.
19//! - `ValidPtr(p, T, n)` is decomposed by `valid_ptr.rs` into primitive SMT
20//!   checks that are implemented today, while unsupported primitives remain
21//!   explicit `Unknown` notes.
22//!
23//! Future SPs should add small lowering modules next to `align.rs` and reuse
24//! `SmtModel`, `SmtQuery`, and `SmtCheckResult` instead of constructing solver
25//! queries ad hoc.
26
27use std::collections::{HashMap, HashSet};
28
29use rustc_middle::{
30    mir::{BinOp, Local, Operand, Rvalue, TerminatorKind, UnOp},
31    ty::{ConstKind, GenericArgKind, PseudoCanonicalInput, Ty, TyCtxt, TyKind, UintTy},
32};
33use z3::{
34    Config, Context, SatResult, Solver,
35    ast::{Ast, Bool, Int},
36};
37
38use super::{
39    alias, align, alive, allocated, deref, in_bound, init, non_null, non_overlap, typed, valid_num,
40    valid_ptr,
41};
42
43use crate::verify::{
44    contract::{
45        ContractExpr, ContractPlace, ContractProjection, NumericOp, NumericPredicate, PlaceBase,
46        Property, PropertyArg, PropertyKind, RelOp,
47    },
48    def_use::{PlaceBaseKey, PlaceKey},
49    generic::GenericTypeCandidates,
50    helpers::{Checkpoint, callee_param_index_for_local},
51    path_extractor::PathStep,
52    primitive::PrimitiveCall,
53    report::CheckResult,
54    verifier::{AbstractValue, CallSummary, ForwardVisitResult, StateFact},
55};
56
57type ValueCursor = usize;
58type TraceSeen = HashSet<(PlaceKey, ValueCursor)>;
59
60#[derive(Clone, Copy)]
61struct PathCursorCutoff {
62    block: rustc_middle::mir::BasicBlock,
63    statement_index: Option<usize>,
64}
65
66/// SMT backend for verifier properties.
67pub struct SmtChecker<'tcx> {
68    pub(crate) tcx: TyCtxt<'tcx>,
69}
70
71fn ty_has_param_const(ty: Ty<'_>) -> bool {
72    for arg in ty.walk() {
73        match arg.kind() {
74            GenericArgKind::Const(c) if matches!(c.kind(), ConstKind::Param(_)) => return true,
75            GenericArgKind::Type(inner_ty) if matches!(inner_ty.kind(), TyKind::Alias(..)) => {
76                return true;
77            }
78            _ => {}
79        }
80    }
81    false
82}
83
84fn safe_type_layout<'tcx>(
85    tcx: TyCtxt<'tcx>,
86    caller: rustc_hir::def_id::DefId,
87    ty: Ty<'tcx>,
88) -> Option<(u64, u64)> {
89    if let TyKind::Ref(_, _, _) | TyKind::RawPtr(_, _) = ty.kind() {
90        let ptr_size = tcx.data_layout.pointer_size().bytes();
91        let ptr_align = tcx.data_layout.pointer_align().abi.bytes();
92        return Some((ptr_align, ptr_size));
93    }
94    if ty_has_param_const(ty) {
95        return None;
96    }
97    let typing_env = rustc_middle::ty::TypingEnv::post_analysis(tcx, caller);
98    let input = PseudoCanonicalInput {
99        typing_env,
100        value: ty,
101    };
102    match tcx.layout_of(input) {
103        Ok(layout) => Some((layout.align.abi.bytes(), layout.size.bytes())),
104        Err(_) if matches!(ty.kind(), TyKind::Param(_)) => Some((0, 0)),
105        Err(_) => None,
106    }
107}
108
109impl<'tcx> SmtChecker<'tcx> {
110    /// Create an SMT checker for the current compiler type context.
111    pub fn new(tcx: TyCtxt<'tcx>) -> Self {
112        Self { tcx }
113    }
114
115    /// Try to prove one property using SMT.
116    pub fn check(
117        &self,
118        checkpoint: &Checkpoint<'tcx>,
119        property: &Property<'tcx>,
120        forward: &ForwardVisitResult<'tcx>,
121    ) -> SmtCheckResult {
122        match property.kind {
123            PropertyKind::Align => align::check(self, checkpoint, property, forward),
124            PropertyKind::Alias => alias::check(self, checkpoint, property, forward),
125            PropertyKind::Alive => alive::check(self, checkpoint, property, forward),
126            PropertyKind::Allocated => allocated::check(self, checkpoint, property, forward),
127            PropertyKind::Deref => deref::check(self, checkpoint, property, forward),
128            PropertyKind::NonNull => non_null::check(self, checkpoint, property, forward),
129            PropertyKind::InBound => in_bound::check(self, checkpoint, property, forward),
130            PropertyKind::Init => init::check(self, checkpoint, property, forward),
131            PropertyKind::NonOverlap => non_overlap::check(self, checkpoint, property, forward),
132            PropertyKind::Typed => typed::check(self, checkpoint, property, forward),
133            PropertyKind::ValidNum => valid_num::check(self, checkpoint, property, forward),
134            PropertyKind::ValidPtr => valid_ptr::check(self, checkpoint, property, forward),
135            PropertyKind::NonVolatile => super::non_volatile::check(self, checkpoint, property, forward),
136            _ => SmtCheckResult::unknown("no SMT lowering for this property yet"),
137        }
138    }
139
140    /// Try to prove one property at a return checkpoint (struct invariant).
141    ///
142    /// Unlike [`check`], this does not use callee-to-caller argument mapping
143    /// because struct invariant properties are already resolved in the caller's
144    /// local namespace.
145    pub fn check_for_checkpoint(
146        &self,
147        caller: rustc_hir::def_id::DefId,
148        property: &Property<'tcx>,
149        forward: &ForwardVisitResult<'tcx>,
150    ) -> SmtCheckResult {
151        match property.kind {
152            PropertyKind::Align => align::check_for_checkpoint(self, caller, property, forward),
153            PropertyKind::Allocated => {
154                SmtCheckResult::unknown("Allocated struct invariant not implemented yet")
155            }
156            PropertyKind::NonNull => {
157                SmtCheckResult::unknown("NonNull struct invariant not implemented yet")
158            }
159            PropertyKind::InBound => {
160                in_bound::check_for_checkpoint(self, caller, property, forward)
161            }
162            PropertyKind::Init => init::check_for_checkpoint(self, caller, property, forward),
163            PropertyKind::ValidPtr => {
164                SmtCheckResult::unknown("ValidPtr struct invariant not implemented yet")
165            }
166            _ => SmtCheckResult::unknown("no struct invariant SMT lowering for this property yet"),
167        }
168    }
169
170    /// Prove one already-lowered common SMT obligation.
171    pub(crate) fn prove_obligation(
172        &self,
173        checkpoint: &Checkpoint<'tcx>,
174        forward: &ForwardVisitResult<'tcx>,
175        obligation: SmtObligation,
176    ) -> SmtCheckResult {
177        let has_contracts = forward
178            .facts
179            .iter()
180            .any(|f| matches!(f, StateFact::Contract(_)));
181
182        // Build the SMT model once.  Contract facts (caller_requires)
183        // are always asserted, even when the path has forgets from
184        // unsupported intrinsics.
185        let cfg = Config::new();
186        let ctx = Context::new(&cfg);
187        let solver = Solver::new(&ctx);
188        let mut model = SmtModel::new(self.tcx, checkpoint, forward, &ctx);
189        model.assert_forward_facts(&solver);
190
191        if matches!(solver.check(), SatResult::Unsat) {
192            return SmtCheckResult::proved(
193                "path facts are infeasible; the obligation holds vacuously on this path",
194            )
195            .with_query(SmtQuery::new(
196                obligation,
197                model.assumptions().to_vec(),
198                SmtPredicate::Custom(String::from("path constraints are unsat")),
199            ));
200        }
201
202        if !forward.forgets.is_empty() && !has_contracts {
203            let reasons = forward
204                .forgets
205                .iter()
206                .map(|reason| format!("{reason:?}"))
207                .collect::<Vec<_>>()
208                .join(", ");
209            return SmtCheckResult::unknown(
210                "path has precision loss; SMT proof is not trusted without a summary",
211            )
212            .with_note(format!("precision loss: {reasons}"));
213        }
214
215        // When forgets exist but contracts provide path assumptions,
216        // allow the SMT check to proceed against the contract-fact
217        // model.  The contract assertions may be sufficient to
218        // discharge the obligation even without full path precision.
219        // (No early return — fall through to obligation match.)
220
221        match &obligation {
222            SmtObligation::Aligned {
223                place,
224                align,
225                ty_name,
226            } => {
227                if *align > 0 && *align <= 1 {
228                    return SmtCheckResult {
229                        result: CheckResult::Proved,
230                        query: Some(SmtQuery::new(
231                            obligation.clone(),
232                            model.assumptions().to_vec(),
233                            SmtPredicate::Custom(format!(
234                                "{} has trivial 1-byte alignment",
235                                place_label(place)
236                            )),
237                        )),
238                        notes: vec![String::from("alignment requirement is trivial")],
239                    };
240                }
241
242                let target_label = place_label(place);
243                let Some(target_term) = model.term_for_place(place) else {
244                    return SmtCheckResult::unknown(format!(
245                        "could not build an address term for {target_label}"
246                    ))
247                    .with_query(SmtQuery::new(
248                        obligation.clone(),
249                        model.assumptions().to_vec(),
250                        SmtPredicate::Not(Box::new(SmtPredicate::Divisible {
251                            term: SmtTerm::Place(place.clone()),
252                            modulus: *align,
253                        })),
254                    ));
255                };
256
257                let is_symbolic = *align == 0;
258                let align_term = if is_symbolic {
259                    model.symbolic_align_term(&ty_name)
260                } else {
261                    Int::from_u64(&ctx, *align)
262                };
263                let align_u64 = if is_symbolic { 0 } else { *align };
264                let zero = Int::from_u64(&ctx, 0);
265                let goal = target_term.modulo(&align_term)._eq(&zero);
266                let query = SmtQuery::new(
267                    obligation.clone(),
268                    model.assumptions().to_vec(),
269                    SmtPredicate::Not(Box::new(SmtPredicate::Divisible {
270                        term: SmtTerm::Place(place.clone()),
271                        modulus: align_u64,
272                    })),
273                );
274
275                solver.assert(&goal.not());
276                match solver.check() {
277                    SatResult::Unsat => SmtCheckResult::proved(
278                        "alignment proved; no counterexample satisfies the path facts",
279                    )
280                    .with_query(query),
281                    SatResult::Sat => {
282                        rap_debug!("  [SMT Align] {} sat: counterexample found", target_label);
283                        SmtCheckResult::unknown(
284                            "current path facts do not prove the required alignment",
285                        )
286                        .with_query(query)
287                        .with_note(
288                            "hint: add an offset-alignment guard or provide a pointer-add/layout summary",
289                        )
290                    }
291                    SatResult::Unknown => {
292                        rap_info!("  [SMT Align] {} unknown result", target_label);
293                        SmtCheckResult::unknown("solver returned unknown").with_query(query)
294                    }
295                }
296            }
297            SmtObligation::NonZero { place } => {
298                let target_label = place_label(place);
299                let Some(target_term) = model.term_for_place(place) else {
300                    return SmtCheckResult::unknown(format!(
301                        "could not build an address term for {target_label}"
302                    ))
303                    .with_query(SmtQuery::new(
304                        obligation.clone(),
305                        model.assumptions().to_vec(),
306                        SmtPredicate::Eq(SmtTerm::Place(place.clone()), SmtTerm::Const(0)),
307                    ));
308                };
309
310                let zero = Int::from_u64(&ctx, 0);
311                let query = SmtQuery::new(
312                    obligation.clone(),
313                    model.assumptions().to_vec(),
314                    SmtPredicate::Eq(SmtTerm::Place(place.clone()), SmtTerm::Const(0)),
315                );
316
317                solver.assert(&target_term._eq(&zero));
318                match solver.check() {
319                    SatResult::Unsat => SmtCheckResult::proved(
320                        "non-null proved; no zero-address model satisfies the path facts",
321                    )
322                    .with_query(query),
323                    SatResult::Sat => SmtCheckResult::unknown(
324                        "current path facts do not prove the target is non-null",
325                    )
326                    .with_query(query)
327                    .with_note("hint: add a non-null guard or provide a source/provenance summary"),
328                    SatResult::Unknown => {
329                        SmtCheckResult::unknown("solver returned unknown").with_query(query)
330                    }
331                }
332            }
333            SmtObligation::InBounds {
334                place,
335                ty_name,
336                access_count,
337                ..
338            } => {
339                let target_label = place_label(place);
340                if model.has_index_access_assumptions {
341                    return SmtCheckResult::proved(
342                        "IndexAccess in-bounds proved via caller contract",
343                    )
344                    .with_query(SmtQuery::new(
345                        obligation.clone(),
346                        model.assumptions().to_vec(),
347                        SmtPredicate::Custom(String::from("IndexAccess InBound by contract")),
348                    ));
349                }
350                let Some(bounds) = model.pointer_bounds_for_place(place) else {
351                    rap_debug!(
352                        "  [SMT InBound] could not recover pointer bounds for {target_label}"
353                    );
354                    if model.has_equivalent_contract_fact(place, PropertyKind::InBound) {
355                        return SmtCheckResult::proved(
356                            "in-bounds proved via caller contract on equivalent place",
357                        )
358                        .with_query(SmtQuery::new(
359                            obligation.clone(),
360                            model.assumptions().to_vec(),
361                            SmtPredicate::Not(Box::new(SmtPredicate::InBounds {
362                                index: SmtTerm::Value("index(?)".to_string()),
363                                access_count: access_count.clone(),
364                                len: SmtTerm::Value("len(?)".to_string()),
365                            })),
366                        ))
367                        .with_note("caller contract provides InBound for raw pointer parameter");
368                    }
369                    return SmtCheckResult::unknown(format!(
370                        "could not connect {target_label} to a slice length and pointer-add index"
371                    ))
372                    .with_query(SmtQuery::new(
373                        obligation.clone(),
374                        model.assumptions().to_vec(),
375                        SmtPredicate::Not(Box::new(SmtPredicate::InBounds {
376                            index: SmtTerm::Value("index(?)".to_string()),
377                            access_count: access_count.clone(),
378                            len: SmtTerm::Value("len(?)".to_string()),
379                        })),
380                    ))
381                    .with_note(
382                        "hint: this first InBound lowering needs slice.as_ptr(), ptr.add(index), and a matching index < slice.len() path fact",
383                    );
384                };
385
386                let zero = Int::from_u64(&ctx, 0);
387                let Some(access) = model.term_for_smt_term(access_count) else {
388                    rap_debug!(
389                        "  [SMT InBound] could not lower access-count term {}",
390                        access_count.describe()
391                    );
392                    return SmtCheckResult::unknown(format!(
393                        "could not build an access-count term for {}",
394                        access_count.describe()
395                    ))
396                    .with_query(SmtQuery::new(
397                        obligation.clone(),
398                        model.assumptions().to_vec(),
399                        SmtPredicate::Not(Box::new(SmtPredicate::InBounds {
400                            index: bounds.index_term,
401                            access_count: access_count.clone(),
402                            len: bounds.len_term,
403                        })),
404                    ));
405                };
406                let index_non_negative = bounds.index.ge(&zero);
407                let access_non_negative = access.ge(&zero);
408                let len_non_negative = bounds.len.ge(&zero);
409                let covered_end = Int::add(&ctx, &[bounds.index.clone(), access]);
410                let within_len = covered_end.le(&bounds.len);
411                solver.assert(&index_non_negative);
412                solver.assert(&access_non_negative);
413                solver.assert(&len_non_negative);
414                model.assumptions.push(SmtPredicate::Ge(
415                    bounds.index_term.clone(),
416                    SmtTerm::Const(0),
417                ));
418                model
419                    .assumptions
420                    .push(SmtPredicate::Ge(access_count.clone(), SmtTerm::Const(0)));
421                let goal = Bool::and(
422                    &ctx,
423                    &[&index_non_negative, &access_non_negative, &within_len],
424                );
425                let query = SmtQuery::new(
426                    obligation.clone(),
427                    model.assumptions().to_vec(),
428                    SmtPredicate::Not(Box::new(SmtPredicate::InBounds {
429                        index: bounds.index_term,
430                        access_count: access_count.clone(),
431                        len: bounds.len_term,
432                    })),
433                );
434
435                solver.assert(&goal.not());
436                match solver.check() {
437                    SatResult::Unsat => SmtCheckResult::proved(format!(
438                        "in-bounds proved for {target_label}; {} {ty_name} element(s) fit under the matched slice length",
439                        access_count.describe()
440                    ))
441                    .with_query(query),
442                    SatResult::Sat => {
443                        rap_debug!(
444                            "  [SMT InBound] sat for {target_label}; assumptions: {:?}; negated goal: {}",
445                            query.assumptions,
446                            query.negated_goal.describe()
447                        );
448                        SmtCheckResult::unknown(
449                            "current path facts do not prove the required bounds",
450                        )
451                        .with_query(query)
452                        .with_note(
453                            "hint: add an index < len guard or provide a richer object-size summary",
454                        )
455                    }
456                    SatResult::Unknown => {
457                        SmtCheckResult::unknown("solver returned unknown").with_query(query)
458                    }
459                }
460            }
461            SmtObligation::PointerRangeInBounds {
462                place,
463                ty_name,
464                lower_delta,
465                upper_delta,
466            } => {
467                let target_label = place_label(place);
468                let Some(bounds) = model.pointer_bounds_for_place(place) else {
469                    return SmtCheckResult::unknown(format!(
470                        "could not connect {target_label} to a slice length and pointer index"
471                    ))
472                    .with_query(SmtQuery::new(
473                        obligation.clone(),
474                        model.assumptions().to_vec(),
475                        pointer_range_negated_goal(
476                            SmtTerm::Value("index(?)".to_string()),
477                            lower_delta.clone(),
478                            upper_delta.clone(),
479                            SmtTerm::Value("len(?)".to_string()),
480                        ),
481                    ))
482                    .with_note(
483                        "hint: pointer arithmetic bounds need a recoverable base object and index/length facts",
484                    );
485                };
486
487                let Some(lower) = model.term_for_smt_term(lower_delta) else {
488                    return SmtCheckResult::unknown(format!(
489                        "could not build a lower pointer-range term for {}",
490                        lower_delta.describe()
491                    ));
492                };
493                let Some(upper) = model.term_for_smt_term(upper_delta) else {
494                    return SmtCheckResult::unknown(format!(
495                        "could not build an upper pointer-range term for {}",
496                        upper_delta.describe()
497                    ));
498                };
499
500                model.assert_unsigned_bounds_for_term(&solver, lower_delta, &mut HashSet::new());
501                model.assert_unsigned_bounds_for_term(&solver, upper_delta, &mut HashSet::new());
502
503                let zero = Int::from_u64(&ctx, 0);
504                // A recovered object length is a slice/allocation size and is
505                // therefore non-negative.  Without this fact the solver may pick
506                // a negative `len`, defeating goals whose offset is expressed as
507                // `len - x` (e.g. `ptr.add(len - half_len)`), where the bound
508                // `len - x <= len` only holds because `x >= 0` and `len >= 0`.
509                solver.assert(&bounds.len.ge(&zero));
510                model
511                    .assumptions
512                    .push(SmtPredicate::Ge(bounds.len_term.clone(), SmtTerm::Const(0)));
513
514                let lower_index = Int::add(&ctx, &[bounds.index.clone(), lower]);
515                let upper_index = Int::add(&ctx, &[bounds.index.clone(), upper]);
516                let base_non_negative = bounds.index.ge(&zero);
517                let base_within_len = bounds.index.le(&bounds.len);
518                let lower_in_object = lower_index.ge(&zero);
519                let upper_in_object = upper_index.le(&bounds.len);
520
521                let goal = Bool::and(
522                    &ctx,
523                    &[
524                        &base_non_negative,
525                        &base_within_len,
526                        &lower_in_object,
527                        &upper_in_object,
528                    ],
529                );
530                let query = SmtQuery::new(
531                    obligation.clone(),
532                    model.assumptions().to_vec(),
533                    pointer_range_negated_goal(
534                        bounds.index_term,
535                        lower_delta.clone(),
536                        upper_delta.clone(),
537                        bounds.len_term,
538                    ),
539                );
540
541                solver.assert(&goal.not());
542                match solver.check() {
543                    SatResult::Unsat => SmtCheckResult::proved(format!(
544                        "pointer arithmetic range proved for {target_label}; the {ty_name} range stays inside the matched object"
545                    ))
546                    .with_query(query),
547                    SatResult::Sat => SmtCheckResult::unknown(
548                        "current path facts do not prove the pointer arithmetic stays in bounds",
549                    )
550                    .with_query(query)
551                    .with_note("hint: add bounds guards for the pointer arithmetic count"),
552                    SatResult::Unknown => {
553                        SmtCheckResult::unknown("solver returned unknown").with_query(query)
554                    }
555                }
556            }
557            SmtObligation::Initialized {
558                place,
559                ty_name,
560                elements,
561                elem_size,
562                array_elem_size,
563                array_len_term,
564            } => {
565                if *elem_size == Some(0) {
566                    return SmtCheckResult::proved(format!(
567                        "initialization proved; zero-sized type {ty_name}"
568                    ));
569                }
570                if let (Some(ae), Some(alt)) = (array_elem_size, array_len_term) {
571                    if *ae > 0 {
572                        if let Some(len_term) = model.term_for_smt_term(alt) {
573                            let zero = Int::from_u64(&ctx, 0);
574                            let size_term = Int::from_u64(&ctx, *ae);
575                            let total_gt_zero =
576                                Bool::and(&ctx, &[&len_term.gt(&zero), &size_term.gt(&zero)]);
577                            solver.push();
578                            solver.assert(&total_gt_zero);
579                            let check = solver.check();
580                            solver.pop(1);
581                            if matches!(check, SatResult::Unsat) {
582                                return SmtCheckResult::proved(format!(
583                                    "initialization proved; array length is provably zero for {ty_name}"
584                                ));
585                            }
586                        }
587                    }
588                }
589                let target_label = place_label(place);
590                let target_terms = model.init_target_terms(place);
591                if target_terms.is_empty() {
592                    return SmtCheckResult::unknown(format!(
593                        "could not build an address term for {target_label}"
594                    ))
595                    .with_query(SmtQuery::new(
596                        obligation.clone(),
597                        model.assumptions().to_vec(),
598                        SmtPredicate::Custom(format!(
599                            "not Init({}, {ty_name}, {elements})",
600                            target_label,
601                            elements = elements.describe()
602                        )),
603                    ));
604                }
605
606                if model.has_equivalent_contract_fact(place, PropertyKind::Init) {
607                    return SmtCheckResult::proved(
608                        "initialized proved via caller contract on equivalent place",
609                    )
610                    .with_query(SmtQuery::new(
611                        obligation.clone(),
612                        model.assumptions().to_vec(),
613                        SmtPredicate::Custom(format!(
614                            "Init({}, {ty_name}, {elements})",
615                            target_label,
616                            elements = elements.describe()
617                        )),
618                    ))
619                    .with_note("caller contract provides Init for raw pointer parameter");
620                }
621
622                if let Some(bounds) = model.pointer_bounds_for_place(place)
623                    && model.origin_is_initialized_for_ty(&bounds.origin_key, ty_name)
624                {
625                    let Some(access) = model.term_for_smt_term(elements) else {
626                        return SmtCheckResult::unknown(format!(
627                            "could not build an Init element-count term for {}",
628                            elements.describe()
629                        ));
630                    };
631                    model.assert_unsigned_bounds_for_term(&solver, elements, &mut HashSet::new());
632                    let zero = Int::from_u64(&ctx, 0);
633                    let index_non_negative = bounds.index.ge(&zero);
634                    let access_non_negative = access.ge(&zero);
635                    let len_non_negative = bounds.len.ge(&zero);
636                    let covered_end = Int::add(&ctx, &[bounds.index.clone(), access]);
637                    let within_len = covered_end.le(&bounds.len);
638                    solver.assert(&len_non_negative);
639                    let goal = Bool::and(
640                        &ctx,
641                        &[&index_non_negative, &access_non_negative, &within_len],
642                    );
643                    let query = SmtQuery::new(
644                        obligation.clone(),
645                        model.assumptions().to_vec(),
646                        SmtPredicate::Custom(format!(
647                            "not initialized_object_range({}, {}, {})",
648                            target_label,
649                            bounds.index_term.describe(),
650                            elements.describe()
651                        )),
652                    );
653
654                    solver.assert(&goal.not());
655                    return match solver.check() {
656                        SatResult::Unsat => SmtCheckResult::proved(format!(
657                            "initialization proved; {target_label} covers {} initialized {ty_name} element(s) from its source object",
658                            elements.describe()
659                        ))
660                        .with_query(query),
661                        SatResult::Sat => SmtCheckResult::unknown(
662                            "current path facts do not prove the initialized object range covers the target",
663                        )
664                        .with_query(query)
665                        .with_note("hint: keep object length facts for the initialized source"),
666                        SatResult::Unknown => {
667                            SmtCheckResult::unknown("solver returned unknown").with_query(query)
668                        }
669                    };
670                }
671
672                let init_facts: Vec<_> = forward
673                    .facts
674                    .iter()
675                    .filter_map(|fact| match fact {
676                        StateFact::KnownInit {
677                            place,
678                            ty_name,
679                            elements,
680                            reason,
681                        } => Some((place.clone(), ty_name.clone(), *elements, reason.clone())),
682                        _ => None,
683                    })
684                    .collect();
685
686                let mut checked_any_init_fact = false;
687                for target_term in &target_terms {
688                    let mut matched_elements = 0_u64;
689                    let mut matched_places = HashSet::new();
690                    let mut matched_notes = Vec::new();
691                    let mut last_query = None;
692                    let Some(required_elements) = smt_term_const_u64(elements) else {
693                        continue;
694                    };
695
696                    for (init_place, init_ty_name, init_elements, init_reason) in &init_facts {
697                        if !init_type_compatible(init_ty_name, ty_name) {
698                            continue;
699                        }
700                        if !matched_places.insert(init_place.clone()) {
701                            continue;
702                        }
703                        let init_terms = model.init_source_terms(init_place);
704                        if init_terms.is_empty() {
705                            continue;
706                        }
707
708                        for init_term in &init_terms {
709                            let query = SmtQuery::new(
710                                obligation.clone(),
711                                model.assumptions().to_vec(),
712                                SmtPredicate::Custom(format!(
713                                    "not same_addr({}, {}) for Init({}, {ty_name}, {elements})",
714                                    target_label,
715                                    place_label(init_place),
716                                    target_label,
717                                    elements = elements.describe()
718                                )),
719                            );
720                            solver.push();
721                            solver.assert(&target_term._eq(init_term).not());
722                            let check = solver.check();
723                            solver.pop(1);
724                            if matches!(check, SatResult::Unsat) {
725                                checked_any_init_fact = true;
726                                matched_elements = matched_elements.saturating_add(*init_elements);
727                                matched_notes.push(format!(
728                                    "{} element(s) from {} ({init_reason})",
729                                    init_elements,
730                                    place_label(init_place)
731                                ));
732                                last_query = Some(query);
733                                break;
734                            }
735                        }
736
737                        if matched_elements >= required_elements {
738                            let query = last_query.unwrap_or_else(|| {
739                                SmtQuery::new(
740                                    obligation.clone(),
741                                    model.assumptions().to_vec(),
742                                    SmtPredicate::Custom(format!(
743                                        "not Init({}, {ty_name}, {elements})",
744                                        target_label,
745                                        elements = elements.describe()
746                                    )),
747                                )
748                            });
749                            return SmtCheckResult::proved(format!(
750                                "initialization proved; {target_label} aliases {matched_elements} initialized element(s)"
751                            ))
752                            .with_query(query)
753                            .with_note(format!(
754                                "matched initialized writes: {}",
755                                matched_notes.join("; ")
756                            ));
757                        }
758                    }
759                }
760
761                let mut result = SmtCheckResult::unknown(
762                    "current path facts do not prove the target memory is initialized",
763                )
764                .with_query(SmtQuery::new(
765                    obligation.clone(),
766                    model.assumptions().to_vec(),
767                    SmtPredicate::Custom(format!(
768                        "not Init({}, {ty_name}, {elements})",
769                        target_label,
770                        elements = elements.describe()
771                    )),
772                ));
773                if checked_any_init_fact {
774                    result = result.with_note(
775                        "hint: a write was found, but SMT could not prove it aliases the Init target",
776                    );
777                } else {
778                    result = result.with_note(
779                        "hint: add a preceding ptr.write summary or a verified init-range summary",
780                    );
781                }
782                result
783            }
784            SmtObligation::Allocated {
785                place,
786                ty_name,
787                elements,
788            } => {
789                let target_label = place_label(place);
790
791                if let Some(bounds) = model.pointer_bounds_for_place(place) {
792                    let zero = Int::from_u64(&ctx, 0);
793                    let Some(access) = model.term_for_smt_term(elements) else {
794                        return SmtCheckResult::unknown(format!(
795                            "could not build an allocation element-count term for {}",
796                            elements.describe()
797                        ))
798                        .with_query(SmtQuery::new(
799                            obligation.clone(),
800                            model.assumptions().to_vec(),
801                            SmtPredicate::Custom(format!(
802                                "not Allocated({}, {ty_name}, {})",
803                                target_label,
804                                elements.describe()
805                            )),
806                        ));
807                    };
808                    let index_non_negative = bounds.index.ge(&zero);
809                    let access_non_negative = access.ge(&zero);
810                    let len_non_negative = bounds.len.ge(&zero);
811                    let covered_end = Int::add(&ctx, &[bounds.index.clone(), access]);
812                    let within_len = covered_end.le(&bounds.len);
813                    solver.assert(&index_non_negative);
814                    solver.assert(&access_non_negative);
815                    solver.assert(&len_non_negative);
816                    model.assumptions.push(SmtPredicate::Ge(
817                        bounds.index_term.clone(),
818                        SmtTerm::Const(0),
819                    ));
820                    model
821                        .assumptions
822                        .push(SmtPredicate::Ge(elements.clone(), SmtTerm::Const(0)));
823                    model.assumptions.push(SmtPredicate::Ge(
824                        bounds.len_term.clone(),
825                        SmtTerm::Const(0),
826                    ));
827                    let goal = Bool::and(
828                        &ctx,
829                        &[&index_non_negative, &access_non_negative, &within_len],
830                    );
831                    let query = SmtQuery::new(
832                        obligation.clone(),
833                        model.assumptions().to_vec(),
834                        SmtPredicate::Custom(format!(
835                            "not same_object_bounds({}, {}, {})",
836                            target_label,
837                            bounds.index_term.describe(),
838                            elements.describe()
839                        )),
840                    );
841
842                    solver.assert(&goal.not());
843                    return match solver.check() {
844                        SatResult::Unsat => SmtCheckResult::proved(format!(
845                            "allocation proved for {target_label}; requested range stays inside the matched object"
846                        ))
847                        .with_query(query),
848                        SatResult::Sat => SmtCheckResult::unknown(
849                            "current path facts do not prove the requested range stays inside one allocation",
850                        )
851                        .with_query(query)
852                        .with_note(
853                            "hint: add an object-length guard or provide a richer allocation summary",
854                        ),
855                        SatResult::Unknown => {
856                            SmtCheckResult::unknown("solver returned unknown").with_query(query)
857                        }
858                    };
859                }
860
861                let Some(target_term) = model.term_for_place(place) else {
862                    return SmtCheckResult::unknown(format!(
863                        "could not build an address term for {target_label}"
864                    ))
865                    .with_query(SmtQuery::new(
866                        obligation.clone(),
867                        model.assumptions().to_vec(),
868                        SmtPredicate::Custom(format!(
869                            "not Allocated({}, {ty_name}, {})",
870                            target_label,
871                            elements.describe()
872                        )),
873                    ));
874                };
875                let Some(required_elements) = model.term_for_smt_term(elements) else {
876                    return SmtCheckResult::unknown(format!(
877                        "could not build an allocation element-count term for {}",
878                        elements.describe()
879                    ))
880                    .with_query(SmtQuery::new(
881                        obligation.clone(),
882                        model.assumptions().to_vec(),
883                        SmtPredicate::Custom(format!(
884                            "not Allocated({}, {ty_name}, {})",
885                            target_label,
886                            elements.describe()
887                        )),
888                    ));
889                };
890
891                let allocated_facts = forward
892                    .facts
893                    .iter()
894                    .filter_map(|fact| match fact {
895                        StateFact::KnownAllocated {
896                            place,
897                            object,
898                            ty_name,
899                            elements,
900                            reason,
901                        } => Some((
902                            place.clone(),
903                            object.clone(),
904                            ty_name.clone(),
905                            *elements,
906                            reason.clone(),
907                        )),
908                        _ => None,
909                    })
910                    .collect::<Vec<_>>();
911
912                for (alloc_place, object, alloc_ty_name, alloc_elements, reason) in allocated_facts
913                {
914                    if !allocated_type_compatible(&alloc_ty_name, ty_name) {
915                        continue;
916                    }
917                    if allocation_object_invalidated(forward, &object) {
918                        continue;
919                    }
920                    let Some(alloc_term) = model.term_for_place(&alloc_place) else {
921                        continue;
922                    };
923                    let query = SmtQuery::new(
924                        obligation.clone(),
925                        model.assumptions().to_vec(),
926                        SmtPredicate::Custom(format!(
927                            "not same_allocated_object({}, {}) or {} > {}",
928                            target_label,
929                            place_label(&alloc_place),
930                            elements.describe(),
931                            alloc_elements
932                        )),
933                    );
934
935                    solver.push();
936                    solver.assert(&target_term._eq(&alloc_term).not());
937                    let same_address = matches!(solver.check(), SatResult::Unsat);
938                    solver.pop(1);
939                    if !same_address {
940                        continue;
941                    }
942
943                    solver.push();
944                    solver.assert(&required_elements.gt(&Int::from_u64(&ctx, alloc_elements)));
945                    let enough_elements = matches!(solver.check(), SatResult::Unsat);
946                    solver.pop(1);
947                    if enough_elements {
948                        return SmtCheckResult::proved(format!(
949                            "allocation proved; {target_label} aliases {} element(s) of {} ({reason})",
950                            alloc_elements, alloc_ty_name
951                        ))
952                        .with_query(query);
953                    }
954                }
955
956                SmtCheckResult::unknown(
957                    "current path facts do not prove the target range is backed by one live allocation",
958                )
959                .with_query(SmtQuery::new(
960                    obligation.clone(),
961                    model.assumptions().to_vec(),
962                    SmtPredicate::Custom(format!(
963                        "not Allocated({}, {ty_name}, {})",
964                        target_label,
965                        elements.describe()
966                    )),
967                ))
968                .with_note(
969                    "hint: keep pointer provenance, object length, and lifetime facts for the target object",
970                )
971            }
972            SmtObligation::NonOverlapping {
973                left,
974                right,
975                left_count,
976                right_count,
977                elem_size,
978            } => {
979                if let (Some((left_object, left_offset)), Some((right_object, right_offset))) = (
980                    model.pointer_object_offset_for_place(left),
981                    model.pointer_object_offset_for_place(right),
982                ) && left_object == right_object
983                {
984                    let Some(left_offset_term) = model.term_for_smt_term(&left_offset) else {
985                        return SmtCheckResult::unknown(format!(
986                            "could not lower object offset {}",
987                            left_offset.describe()
988                        ));
989                    };
990                    let Some(right_offset_term) = model.term_for_smt_term(&right_offset) else {
991                        return SmtCheckResult::unknown(format!(
992                            "could not lower object offset {}",
993                            right_offset.describe()
994                        ));
995                    };
996                    let Some(left_count_term) = model.term_for_smt_term(left_count) else {
997                        return SmtCheckResult::unknown(format!(
998                            "could not build a range-count term for {}",
999                            left_count.describe()
1000                        ));
1001                    };
1002                    let Some(right_count_term) = model.term_for_smt_term(right_count) else {
1003                        return SmtCheckResult::unknown(format!(
1004                            "could not build a range-count term for {}",
1005                            right_count.describe()
1006                        ));
1007                    };
1008                    let left_end = Int::add(&ctx, &[left_offset_term.clone(), left_count_term]);
1009                    let right_end = Int::add(&ctx, &[right_offset_term.clone(), right_count_term]);
1010                    let disjoint = Bool::or(
1011                        &ctx,
1012                        &[
1013                            &left_end.le(&right_offset_term),
1014                            &right_end.le(&left_offset_term),
1015                        ],
1016                    );
1017                    let negated = SmtPredicate::Not(Box::new(SmtPredicate::NonOverlapping {
1018                        left: left_offset,
1019                        right: right_offset,
1020                        left_count: left_count.clone(),
1021                        right_count: right_count.clone(),
1022                        elem_size: 1,
1023                    }));
1024                    let query =
1025                        SmtQuery::new(obligation.clone(), model.assumptions().to_vec(), negated);
1026
1027                    model.assert_unsigned_bounds_for_term(&solver, left_count, &mut HashSet::new());
1028                    model.assert_unsigned_bounds_for_term(
1029                        &solver,
1030                        right_count,
1031                        &mut HashSet::new(),
1032                    );
1033                    solver.assert(&disjoint.not());
1034                    return match solver.check() {
1035                        SatResult::Unsat => SmtCheckResult::proved(format!(
1036                            "non-overlap proved inside allocation {}",
1037                            place_label(&left_object)
1038                        ))
1039                        .with_query(query),
1040                        SatResult::Sat => {
1041                            failed_smt("the two ranges overlap within the same allocation object")
1042                                .with_query(query)
1043                        }
1044                        SatResult::Unknown => {
1045                            SmtCheckResult::unknown("solver returned unknown").with_query(query)
1046                        }
1047                    };
1048                }
1049
1050                let Some(left_addr) = model.term_for_place(left) else {
1051                    return SmtCheckResult::unknown(format!(
1052                        "could not build an address term for {}",
1053                        place_label(left)
1054                    ));
1055                };
1056                let Some(right_addr) = model.term_for_place(right) else {
1057                    return SmtCheckResult::unknown(format!(
1058                        "could not build an address term for {}",
1059                        place_label(right)
1060                    ));
1061                };
1062                let Some(left_count_term) = model.term_for_smt_term(left_count) else {
1063                    return SmtCheckResult::unknown(format!(
1064                        "could not build a range-count term for {}",
1065                        left_count.describe()
1066                    ));
1067                };
1068                let Some(right_count_term) = model.term_for_smt_term(right_count) else {
1069                    return SmtCheckResult::unknown(format!(
1070                        "could not build a range-count term for {}",
1071                        right_count.describe()
1072                    ));
1073                };
1074
1075                let size = Int::from_u64(&ctx, *elem_size);
1076                let left_end = Int::add(
1077                    &ctx,
1078                    &[
1079                        left_addr.clone(),
1080                        Int::mul(&ctx, &[left_count_term, size.clone()]),
1081                    ],
1082                );
1083                let right_end = Int::add(
1084                    &ctx,
1085                    &[
1086                        right_addr.clone(),
1087                        Int::mul(&ctx, &[right_count_term, size]),
1088                    ],
1089                );
1090                let disjoint = Bool::or(
1091                    &ctx,
1092                    &[&left_end.le(&right_addr), &right_end.le(&left_addr)],
1093                );
1094                let negated = SmtPredicate::Not(Box::new(SmtPredicate::NonOverlapping {
1095                    left: SmtTerm::Place(left.clone()),
1096                    right: SmtTerm::Place(right.clone()),
1097                    left_count: left_count.clone(),
1098                    right_count: right_count.clone(),
1099                    elem_size: *elem_size,
1100                }));
1101                let query =
1102                    SmtQuery::new(obligation.clone(), model.assumptions().to_vec(), negated);
1103
1104                model.assert_unsigned_bounds_for_term(&solver, left_count, &mut HashSet::new());
1105                model.assert_unsigned_bounds_for_term(&solver, right_count, &mut HashSet::new());
1106                solver.assert(&disjoint.not());
1107                match solver.check() {
1108                    SatResult::Unsat => SmtCheckResult::proved(
1109                        "non-overlap proved; the two pointer ranges cannot intersect on this path",
1110                    )
1111                    .with_query(query),
1112                    SatResult::Sat => failed_smt(
1113                        "the two pointer ranges may overlap under the current path facts",
1114                    )
1115                    .with_query(query),
1116                    SatResult::Unknown => {
1117                        SmtCheckResult::unknown("solver returned unknown").with_query(query)
1118                    }
1119                }
1120            }
1121            SmtObligation::Predicate { predicates } => {
1122                // If caller contract already provides IndexAccess InBound assumptions,
1123                // the proof is trivial
1124                if model.has_index_access_assumptions {
1125                    return SmtCheckResult::proved(
1126                        "IndexAccess in-bounds proved via caller contract",
1127                    );
1128                }
1129                if predicates.is_empty() {
1130                    return SmtCheckResult::unknown("ValidNum predicate set is empty").with_query(
1131                        SmtQuery::new(
1132                            obligation.clone(),
1133                            model.assumptions().to_vec(),
1134                            SmtPredicate::Custom(String::from("empty ValidNum predicate")),
1135                        ),
1136                    );
1137                }
1138                model.assert_unsigned_bounds_for_predicates(&solver, predicates);
1139
1140                let Some(goal) = model.bool_for_predicates(predicates) else {
1141                    return SmtCheckResult::unknown(
1142                        "ValidNum predicate could not be lowered to SMT",
1143                    )
1144                    .with_query(SmtQuery::new(
1145                        obligation.clone(),
1146                        model.assumptions().to_vec(),
1147                        SmtPredicate::Not(Box::new(if predicates.len() == 1 {
1148                            predicates[0].clone()
1149                        } else {
1150                            SmtPredicate::And(predicates.clone())
1151                        })),
1152                    ));
1153                };
1154                let query = SmtQuery::new(
1155                    obligation.clone(),
1156                    model.assumptions().to_vec(),
1157                    SmtPredicate::Not(Box::new(if predicates.len() == 1 {
1158                        predicates[0].clone()
1159                    } else {
1160                        SmtPredicate::And(predicates.clone())
1161                    })),
1162                );
1163
1164                solver.assert(&goal.not());
1165                match solver.check() {
1166                    SatResult::Unsat => SmtCheckResult::proved(
1167                        "numeric precondition proved; no counterexample satisfies the path facts",
1168                    )
1169                    .with_query(query),
1170                    SatResult::Sat => SmtCheckResult::unknown(
1171                        "current path facts do not prove the numeric precondition",
1172                    )
1173                    .with_query(query)
1174                    .with_note("hint: add a matching numeric guard or expose a stronger summary"),
1175                    SatResult::Unknown => {
1176                        SmtCheckResult::unknown("solver returned unknown").with_query(query)
1177                    }
1178                }
1179            }
1180            SmtObligation::Range { .. } => SmtCheckResult::unknown(
1181                "range obligations are not implemented yet",
1182            )
1183            .with_query(SmtQuery::new(
1184                obligation.clone(),
1185                model.assumptions().to_vec(),
1186                SmtPredicate::Custom(String::from("range refutation not implemented")),
1187            )),
1188        }
1189    }
1190
1191    /// Prove one lowered obligation at a return checkpoint (struct invariant).
1192    ///
1193    /// Wraps `prove_obligation` with a minimal dummy checkpoint that only carries
1194    /// the caller DefId. No callee-to-caller argument mapping is needed.
1195    pub(crate) fn prove_obligation_for_checkpoint(
1196        &self,
1197        caller: rustc_hir::def_id::DefId,
1198        forward: &ForwardVisitResult<'tcx>,
1199        obligation: SmtObligation,
1200    ) -> SmtCheckResult {
1201        let dummy_checkpoint = Checkpoint {
1202            caller,
1203            callee: Some(caller),
1204            block: rustc_middle::mir::BasicBlock::from_usize(0),
1205            span: rustc_span::Span::default(),
1206            args: Vec::new(),
1207            kind: crate::helpers::mir_scan::CheckpointKind::UnsafeCall,
1208            is_ref: false,
1209        };
1210        self.prove_obligation(&dummy_checkpoint, forward, obligation)
1211    }
1212
1213    /// Resolve the target place of a property at a concrete checkpoint.
1214    pub(crate) fn property_target(
1215        &self,
1216        checkpoint: &Checkpoint<'tcx>,
1217        property: &Property<'tcx>,
1218    ) -> Option<PlaceKey> {
1219        let arg = property.args.first()?;
1220        match arg {
1221            PropertyArg::Place(place) => self.contract_place_to_callsite_place(checkpoint, place),
1222            PropertyArg::Expr(ContractExpr::Place(place)) => {
1223                self.contract_place_to_callsite_place(checkpoint, place)
1224            }
1225            PropertyArg::Expr(ContractExpr::IndexAccess { slice, .. }) => {
1226                match slice.as_ref() {
1227                    ContractExpr::Place(place) => {
1228                        self.contract_place_to_callsite_place(checkpoint, place)
1229                    }
1230                    _ => None,
1231                }
1232            }
1233            PropertyArg::Expr(ContractExpr::Const(index)) => {
1234                let index = usize::try_from(*index).ok()?;
1235                self.callsite_arg_place(checkpoint, index)
1236            }
1237            _ => None,
1238        }
1239    }
1240
1241    /// Resolve the `index`-th property argument as a concrete checkpoint place.
1242    pub(crate) fn property_place_arg(
1243        &self,
1244        checkpoint: &Checkpoint<'tcx>,
1245        property: &Property<'tcx>,
1246        index: usize,
1247    ) -> Option<PlaceKey> {
1248        let arg = property.args.get(index)?;
1249        match arg {
1250            PropertyArg::Place(place) => self.contract_place_to_callsite_place(checkpoint, place),
1251            PropertyArg::Expr(ContractExpr::Place(place)) => {
1252                self.contract_place_to_callsite_place(checkpoint, place)
1253            }
1254            PropertyArg::Expr(ContractExpr::Const(arg_index)) => {
1255                let arg_index = usize::try_from(*arg_index).ok()?;
1256                self.callsite_arg_place(checkpoint, arg_index)
1257            }
1258            _ => None,
1259        }
1260    }
1261
1262    /// Resolve the target place of a property directly from a contract place
1263    /// without going through callee argument mapping.
1264    pub(crate) fn property_target_direct(&self, property: &Property<'tcx>) -> Option<PlaceKey> {
1265        let arg = property.args.first()?;
1266        match arg {
1267            PropertyArg::Place(place) => Some(self.resolve_contract_place(place)),
1268            PropertyArg::Expr(ContractExpr::Place(place)) => {
1269                Some(self.resolve_contract_place(place))
1270            }
1271            _ => None,
1272        }
1273    }
1274
1275    /// Convert a contract place to a PlaceKey, resolving Arg(n) to Local(n+1).
1276    fn resolve_contract_place(&self, place: &ContractPlace<'tcx>) -> PlaceKey {
1277        let mut key = PlaceKey::from_contract_place(place);
1278        if let PlaceBaseKey::Arg(index) = key.base {
1279            key.base = PlaceBaseKey::Local(index + 1);
1280        }
1281        key
1282    }
1283
1284    /// Resolve the type argument used by an alignment property.
1285    pub(crate) fn property_required_ty(
1286        &self,
1287        checkpoint: &Checkpoint<'tcx>,
1288        property: &Property<'tcx>,
1289    ) -> Option<Ty<'tcx>> {
1290        property.args.iter().find_map(|arg| {
1291            let PropertyArg::Ty(ty) = arg else {
1292                return None;
1293            };
1294            Some(self.instantiate_callsite_ty(checkpoint, *ty))
1295        })
1296    }
1297
1298    /// Resolve the type argument of a property directly without callee generic instantiation.
1299    pub(crate) fn property_required_ty_direct(
1300        &self,
1301        property: &Property<'tcx>,
1302    ) -> Option<Ty<'tcx>> {
1303        property.args.iter().find_map(|arg| {
1304            let PropertyArg::Ty(ty) = arg else {
1305                return None;
1306            };
1307            Some(*ty)
1308        })
1309    }
1310
1311    /// Resolve the trailing length expression directly (without checkpoint binding).
1312    ///
1313    /// For checkpoint checks this extracts the last argument as an expression
1314    /// using the function's own parameter space.
1315    pub(crate) fn property_len_expr_direct(
1316        &self,
1317        property: &Property<'tcx>,
1318    ) -> Option<ContractExpr<'tcx>> {
1319        property.args.iter().rev().find_map(|arg| {
1320            let PropertyArg::Expr(expr) = arg else {
1321                return None;
1322            };
1323            Some(expr.clone())
1324        })
1325    }
1326
1327    /// Resolve the trailing length expression at a concrete checkpoint.
1328    ///
1329    /// This keeps constants unchanged and rewrites callee argument places, such
1330    /// as `Arg_2` from std-contract JSON, to the concrete MIR place passed by
1331    /// the caller at this checkpoint.  Composite numeric expressions are rebound
1332    /// recursively.
1333    pub(crate) fn property_len_expr(
1334        &self,
1335        checkpoint: &Checkpoint<'tcx>,
1336        property: &Property<'tcx>,
1337    ) -> Option<ContractExpr<'tcx>> {
1338        property.args.iter().rev().find_map(|arg| {
1339            let PropertyArg::Expr(expr) = arg else {
1340                return None;
1341            };
1342            self.bind_contract_expr_to_callsite(checkpoint, expr)
1343        })
1344    }
1345
1346    /// Lower a rebound contract arithmetic expression into the common SMT term model.
1347    pub(crate) fn contract_expr_to_smt_term(
1348        &self,
1349        caller: rustc_hir::def_id::DefId,
1350        expr: &ContractExpr<'tcx>,
1351    ) -> Option<SmtTerm> {
1352        match expr {
1353            ContractExpr::Place(place) => {
1354                Some(SmtTerm::Place(PlaceKey::from_contract_place(place)))
1355            }
1356            ContractExpr::Const(value) => u64::try_from(*value).ok().map(SmtTerm::Const),
1357            ContractExpr::ConstParam { name, .. } => {
1358                Some(SmtTerm::ConstParam(name.clone()))
1359            }
1360            ContractExpr::SizeOf(ty) => {
1361                let size = self.required_size(caller, *ty)?;
1362                Some(SmtTerm::Const(size))
1363            }
1364            ContractExpr::AlignOf(ty) => {
1365                let align = self.required_alignment(caller, *ty)?;
1366                Some(SmtTerm::Const(align))
1367            }
1368            ContractExpr::Len(expr) => Some(SmtTerm::Value(format!(
1369                "len({})",
1370                self.contract_expr_label(expr)?
1371            ))),
1372            ContractExpr::IndexAccess { .. } => None,
1373            ContractExpr::Binary { op, lhs, rhs } => {
1374                let lhs = Box::new(self.contract_expr_to_smt_term(caller, lhs)?);
1375                let rhs = Box::new(self.contract_expr_to_smt_term(caller, rhs)?);
1376                match op {
1377                    NumericOp::Add => Some(SmtTerm::Add(lhs, rhs)),
1378                    NumericOp::Sub => Some(SmtTerm::Sub(lhs, rhs)),
1379                    NumericOp::Mul => Some(SmtTerm::Mul(lhs, rhs)),
1380                    NumericOp::Div => Some(SmtTerm::Div(lhs, rhs)),
1381                    NumericOp::Rem => Some(SmtTerm::Rem(lhs, rhs)),
1382                    NumericOp::BitAnd | NumericOp::BitOr | NumericOp::BitXor => None,
1383                }
1384            }
1385            ContractExpr::Unary { .. } | ContractExpr::Unknown => None,
1386        }
1387    }
1388
1389    /// Resolve a `ValidNum` predicate list at a concrete checkpoint.
1390    pub(crate) fn property_numeric_predicates(
1391        &self,
1392        checkpoint: &Checkpoint<'tcx>,
1393        property: &Property<'tcx>,
1394    ) -> Option<Vec<NumericPredicate<'tcx>>> {
1395        property.args.iter().find_map(|arg| {
1396            let PropertyArg::Predicates(predicates) = arg else {
1397                return None;
1398            };
1399            predicates
1400                .iter()
1401                .map(|predicate| {
1402                    Some(NumericPredicate {
1403                        lhs: self.bind_contract_expr_to_callsite(checkpoint, &predicate.lhs)?,
1404                        op: predicate.op,
1405                        rhs: self.bind_contract_expr_to_callsite(checkpoint, &predicate.rhs)?,
1406                    })
1407                })
1408                .collect()
1409        })
1410    }
1411
1412    /// Convert a rebound contract predicate into the shared SMT predicate model.
1413    pub(crate) fn numeric_predicate_to_smt_predicate(
1414        &self,
1415        caller: rustc_hir::def_id::DefId,
1416        predicate: &NumericPredicate<'tcx>,
1417    ) -> Option<SmtPredicate> {
1418        let lhs = self.contract_expr_to_smt_term(caller, &predicate.lhs)?;
1419        let rhs = self.contract_expr_to_smt_term(caller, &predicate.rhs)?;
1420        Some(match predicate.op {
1421            RelOp::Eq => SmtPredicate::Eq(lhs, rhs),
1422            RelOp::Ne => SmtPredicate::Ne(lhs, rhs),
1423            RelOp::Lt => SmtPredicate::Lt(lhs, rhs),
1424            RelOp::Le => SmtPredicate::Le(lhs, rhs),
1425            RelOp::Gt => SmtPredicate::Gt(lhs, rhs),
1426            RelOp::Ge => SmtPredicate::Ge(lhs, rhs),
1427        })
1428    }
1429
1430    /// Resolve and lower `ValidNum` predicates, expanding non-scalar helpers.
1431    pub(crate) fn property_numeric_smt_predicates(
1432        &self,
1433        checkpoint: &Checkpoint<'tcx>,
1434        property: &Property<'tcx>,
1435    ) -> Option<Vec<SmtPredicate>> {
1436        let predicates = self.property_numeric_predicates(checkpoint, property)?;
1437        let mut lowered = Vec::new();
1438        for predicate in predicates {
1439            if let Some(expanded) = self.expand_index_access_predicate(checkpoint, &predicate)? {
1440                lowered.extend(expanded);
1441            } else {
1442                lowered
1443                    .push(self.numeric_predicate_to_smt_predicate(checkpoint.caller, &predicate)?);
1444            }
1445        }
1446        Some(lowered)
1447    }
1448
1449    /// Resolve an `InBound(index_access(slice, index))` property to range predicates.
1450    pub(crate) fn property_index_access_in_bound_predicates(
1451        &self,
1452        checkpoint: &Checkpoint<'tcx>,
1453        property: &Property<'tcx>,
1454    ) -> Option<Vec<SmtPredicate>> {
1455        property.args.iter().find_map(|arg| {
1456            let PropertyArg::Expr(expr) = arg else {
1457                return None;
1458            };
1459            if !matches!(expr, ContractExpr::IndexAccess { .. }) {
1460                return None;
1461            }
1462            let rebound = self.bind_contract_expr_to_callsite(checkpoint, expr)?;
1463            self.index_access_in_bound_predicates(checkpoint, &rebound)
1464        })
1465    }
1466
1467    fn expand_index_access_predicate(
1468        &self,
1469        checkpoint: &Checkpoint<'tcx>,
1470        predicate: &NumericPredicate<'tcx>,
1471    ) -> Option<Option<Vec<SmtPredicate>>> {
1472        let ContractExpr::IndexAccess { slice, index } = &predicate.lhs else {
1473            if matches!(predicate.rhs, ContractExpr::IndexAccess { .. }) {
1474                return None;
1475            }
1476            return Some(None);
1477        };
1478        if !matches!(predicate.op, RelOp::Ne) || !matches!(predicate.rhs, ContractExpr::Const(0)) {
1479            return None;
1480        }
1481
1482        self.index_access_in_bound_predicates(
1483            checkpoint,
1484            &ContractExpr::IndexAccess {
1485                slice: slice.clone(),
1486                index: index.clone(),
1487            },
1488        )
1489        .map(Some)
1490    }
1491
1492    fn index_access_in_bound_predicates(
1493        &self,
1494        checkpoint: &Checkpoint<'tcx>,
1495        expr: &ContractExpr<'tcx>,
1496    ) -> Option<Vec<SmtPredicate>> {
1497        let ContractExpr::IndexAccess { slice, index } = expr else {
1498            return None;
1499        };
1500        let len = SmtTerm::Value(format!("len({})", self.contract_expr_label(slice)?));
1501        let (lower, upper) = self.slice_index_bounds(checkpoint.caller, index, len.clone())?;
1502        Some(vec![
1503            SmtPredicate::Le(SmtTerm::Const(0), lower.clone()),
1504            SmtPredicate::Le(lower, upper.clone()),
1505            SmtPredicate::Le(upper, len),
1506        ])
1507    }
1508
1509    fn slice_index_bounds(
1510        &self,
1511        caller: rustc_hir::def_id::DefId,
1512        index: &ContractExpr<'tcx>,
1513        len: SmtTerm,
1514    ) -> Option<(SmtTerm, SmtTerm)> {
1515        let index_term = self.contract_expr_to_smt_term(caller, index)?;
1516        let Some(kind) = self.slice_index_kind(caller, index) else {
1517            return Some((
1518                index_term.clone(),
1519                SmtTerm::Add(Box::new(index_term), Box::new(SmtTerm::Const(1))),
1520            ));
1521        };
1522
1523        match kind {
1524            SliceIndexKind::Scalar => Some((
1525                index_term.clone(),
1526                SmtTerm::Add(Box::new(index_term), Box::new(SmtTerm::Const(1))),
1527            )),
1528            SliceIndexKind::Range => Some((
1529                self.contract_expr_field_term(caller, index, 0)?,
1530                self.contract_expr_field_term(caller, index, 1)?,
1531            )),
1532            SliceIndexKind::RangeFrom => Some((
1533                self.contract_expr_field_term(caller, index, 0)?,
1534                len,
1535            )),
1536            SliceIndexKind::RangeTo => Some((
1537                SmtTerm::Const(0),
1538                self.contract_expr_field_term(caller, index, 0)?,
1539            )),
1540            SliceIndexKind::RangeFull => Some((SmtTerm::Const(0), len)),
1541            SliceIndexKind::RangeInclusive => {
1542                let start = self.contract_expr_field_term(caller, index, 0)?;
1543                let end = self.contract_expr_field_term(caller, index, 1)?;
1544                Some((
1545                    start,
1546                    SmtTerm::Add(Box::new(end), Box::new(SmtTerm::Const(1))),
1547                ))
1548            }
1549            SliceIndexKind::RangeToInclusive => {
1550                let end = self.contract_expr_field_term(caller, index, 0)?;
1551                Some((
1552                    SmtTerm::Const(0),
1553                    SmtTerm::Add(Box::new(end), Box::new(SmtTerm::Const(1))),
1554                ))
1555            }
1556        }
1557    }
1558
1559    fn slice_index_kind(
1560        &self,
1561        caller: rustc_hir::def_id::DefId,
1562        index: &ContractExpr<'tcx>,
1563    ) -> Option<SliceIndexKind> {
1564        let place = self.contract_expr_place(index)?;
1565        let ty = self.place_ty_for_caller(caller, &place)?;
1566        if matches!(ty.kind(), TyKind::Uint(UintTy::Usize)) {
1567            return Some(SliceIndexKind::Scalar);
1568        }
1569
1570        let ty_name = format!("{ty:?}");
1571        if ty_name.contains("RangeToInclusive<usize>") {
1572            Some(SliceIndexKind::RangeToInclusive)
1573        } else if ty_name.contains("RangeInclusive<usize>") {
1574            Some(SliceIndexKind::RangeInclusive)
1575        } else if ty_name.contains("RangeFrom<usize>") {
1576            Some(SliceIndexKind::RangeFrom)
1577        } else if ty_name.contains("RangeTo<usize>") {
1578            Some(SliceIndexKind::RangeTo)
1579        } else if ty_name.contains("RangeFull") {
1580            Some(SliceIndexKind::RangeFull)
1581        } else if ty_name.contains("Range<usize>") || ty_name.contains("IndexRange") {
1582            Some(SliceIndexKind::Range)
1583        } else {
1584            None
1585        }
1586    }
1587
1588    fn contract_expr_field_term(
1589        &self,
1590        caller: rustc_hir::def_id::DefId,
1591        expr: &ContractExpr<'tcx>,
1592        field: usize,
1593    ) -> Option<SmtTerm> {
1594        let mut place = self.contract_expr_place(expr)?;
1595        place.fields.push(field);
1596        self.contract_expr_to_smt_term(caller, &contract_expr_from_place_key(place))
1597    }
1598
1599    fn contract_expr_place(&self, expr: &ContractExpr<'tcx>) -> Option<PlaceKey> {
1600        let ContractExpr::Place(place) = expr else {
1601            return None;
1602        };
1603        Some(PlaceKey::from_contract_place(place))
1604    }
1605
1606    fn contract_expr_label(&self, expr: &ContractExpr<'tcx>) -> Option<String> {
1607        match expr {
1608            ContractExpr::Place(place) => Some(place_label(&PlaceKey::from_contract_place(place))),
1609            ContractExpr::Const(value) => Some(value.to_string()),
1610            ContractExpr::ConstParam { name, .. } => Some(name.clone()),
1611            _ => None,
1612        }
1613    }
1614
1615    fn place_ty_for_caller(
1616        &self,
1617        caller: rustc_hir::def_id::DefId,
1618        place: &PlaceKey,
1619    ) -> Option<Ty<'tcx>> {
1620        if !place.fields.is_empty() {
1621            return None;
1622        }
1623        let local = match place.base {
1624            PlaceBaseKey::Return => Local::from_usize(0),
1625            PlaceBaseKey::Local(local) => Local::from_usize(local),
1626            PlaceBaseKey::Arg(_) => return None,
1627        };
1628        Some(self.tcx.optimized_mir(caller).local_decls[local].ty)
1629    }
1630
1631    pub(crate) fn infer_pointee_ty(
1632        &self,
1633        caller: rustc_hir::def_id::DefId,
1634        place: &PlaceKey,
1635    ) -> Option<Ty<'tcx>> {
1636        let ty = self.place_ty_for_caller(caller, place)?;
1637        infer_element_ty(ty)
1638    }
1639
1640    /// Return true if a place is a safe reference/slice/string carrying object length.
1641    pub(crate) fn is_len_carrying_place_for_caller(
1642        &self,
1643        caller: rustc_hir::def_id::DefId,
1644        place: &PlaceKey,
1645    ) -> bool {
1646        self.place_ty_for_caller(caller, place)
1647            .is_some_and(is_len_carrying_ty)
1648    }
1649
1650    fn bind_contract_expr_to_callsite(
1651        &self,
1652        checkpoint: &Checkpoint<'tcx>,
1653        expr: &ContractExpr<'tcx>,
1654    ) -> Option<ContractExpr<'tcx>> {
1655        match expr {
1656            ContractExpr::Place(place) => self.contract_place_to_callsite_expr(checkpoint, place),
1657            ContractExpr::Const(value) => Some(ContractExpr::Const(*value)),
1658            ContractExpr::ConstParam { index, name } => self
1659                .instantiate_callsite_const(checkpoint, *index)
1660                .map(ContractExpr::Const)
1661                .or_else(|| {
1662                    Some(ContractExpr::ConstParam {
1663                        index: *index,
1664                        name: name.clone(),
1665                    })
1666                }),
1667            ContractExpr::SizeOf(ty) => Some(ContractExpr::SizeOf(
1668                self.instantiate_callsite_ty(checkpoint, *ty),
1669            )),
1670            ContractExpr::AlignOf(ty) => Some(ContractExpr::AlignOf(
1671                self.instantiate_callsite_ty(checkpoint, *ty),
1672            )),
1673            ContractExpr::Len(expr) => Some(ContractExpr::Len(Box::new(
1674                self.bind_contract_expr_to_callsite(checkpoint, expr)?,
1675            ))),
1676            ContractExpr::IndexAccess { slice, index } => Some(ContractExpr::IndexAccess {
1677                slice: Box::new(self.bind_contract_expr_to_callsite(checkpoint, slice)?),
1678                index: Box::new(self.bind_contract_expr_to_callsite(checkpoint, index)?),
1679            }),
1680            ContractExpr::Binary { op, lhs, rhs } => Some(ContractExpr::Binary {
1681                op: *op,
1682                lhs: Box::new(self.bind_contract_expr_to_callsite(checkpoint, lhs)?),
1683                rhs: Box::new(self.bind_contract_expr_to_callsite(checkpoint, rhs)?),
1684            }),
1685            ContractExpr::Unary { op, expr } => Some(ContractExpr::Unary {
1686                op: *op,
1687                expr: Box::new(self.bind_contract_expr_to_callsite(checkpoint, expr)?),
1688            }),
1689            ContractExpr::Unknown => Some(ContractExpr::Unknown),
1690        }
1691    }
1692
1693    fn contract_place_to_callsite_expr(
1694        &self,
1695        checkpoint: &Checkpoint<'tcx>,
1696        place: &ContractPlace<'tcx>,
1697    ) -> Option<ContractExpr<'tcx>> {
1698        let key = PlaceKey::from_contract_place(place);
1699        match place.base {
1700            PlaceBase::Arg(index) => self.callsite_arg_expr(checkpoint, index, &key.fields),
1701            PlaceBase::Local(local) => {
1702                if let Some(index) = checkpoint
1703                    .callee
1704                    .and_then(|callee| callee_param_index_for_local(self.tcx, callee, local))
1705                {
1706                    self.callsite_arg_expr(checkpoint, index, &key.fields)
1707                } else {
1708                    Some(ContractExpr::Place(place.clone()))
1709                }
1710            }
1711            PlaceBase::Return => Some(ContractExpr::Place(place.clone())),
1712        }
1713    }
1714
1715    fn callsite_arg_expr(
1716        &self,
1717        checkpoint: &Checkpoint<'tcx>,
1718        index: usize,
1719        fields: &[usize],
1720    ) -> Option<ContractExpr<'tcx>> {
1721        // Try checkpoint args first
1722        let operand = checkpoint.args.get(index)
1723            // Fallback: read MIR body directly (needed for Rust 1.96+ where
1724            // checkpoint may miss some call arguments)
1725            .or_else(|| {
1726                let body = self.tcx.optimized_mir(checkpoint.caller);
1727                let terminator = body.basic_blocks[checkpoint.block].terminator();
1728                if let TerminatorKind::Call { args, .. } = &terminator.kind {
1729                    args.get(index).map(|a| &a.node)
1730                } else {
1731                    None
1732                }
1733            })?;
1734        if fields.is_empty()
1735            && let Operand::Constant(constant) = operand
1736        {
1737            let const_debug = format!("{:?}", constant.const_);
1738            if let Some(value) = const_int_from_debug(&const_debug) {
1739                return Some(ContractExpr::Const(value));
1740            }
1741            if let Some(name) = const_param_name_from_debug(&const_debug) {
1742                return Some(ContractExpr::ConstParam {
1743                    index: 0,
1744                    name,
1745                });
1746            }
1747            rap_warn!("callsite Const debug={}", const_debug);
1748        }
1749        self.callsite_arg_place_with_fields(checkpoint, index, fields)
1750            .map(contract_expr_from_place_key)
1751    }
1752
1753    /// Convert a contract place into a concrete MIR place when possible.
1754    pub(crate) fn contract_place_to_callsite_place(
1755        &self,
1756        checkpoint: &Checkpoint<'tcx>,
1757        place: &ContractPlace<'tcx>,
1758    ) -> Option<PlaceKey> {
1759        match place.base {
1760            PlaceBase::Arg(index) => self.callsite_arg_place_with_fields(
1761                checkpoint,
1762                index,
1763                &PlaceKey::from_contract_place(place).fields,
1764            ),
1765            PlaceBase::Local(local) => {
1766                if let Some(index) = checkpoint
1767                    .callee
1768                    .and_then(|callee| callee_param_index_for_local(self.tcx, callee, local))
1769                {
1770                    self.callsite_arg_place_with_fields(
1771                        checkpoint,
1772                        index,
1773                        &PlaceKey::from_contract_place(place).fields,
1774                    )
1775                } else {
1776                    Some(PlaceKey::from_contract_place(place))
1777                }
1778            }
1779            PlaceBase::Return => Some(PlaceKey::from_contract_place(place)),
1780        }
1781    }
1782
1783    /// Return the concrete MIR place used as the `index`-th call argument.
1784    pub(crate) fn callsite_arg_place(
1785        &self,
1786        checkpoint: &Checkpoint<'tcx>,
1787        index: usize,
1788    ) -> Option<PlaceKey> {
1789        let operand = checkpoint.args.get(index)?;
1790        operand_place(operand)
1791    }
1792
1793    /// Return the `index`-th call argument as a common SMT term.
1794    pub(crate) fn callsite_arg_smt_term(
1795        &self,
1796        checkpoint: &Checkpoint<'tcx>,
1797        index: usize,
1798    ) -> Option<SmtTerm> {
1799        let expr = self.callsite_arg_expr(checkpoint, index, &[])?;
1800        self.contract_expr_to_smt_term(checkpoint.caller, &expr)
1801    }
1802
1803    /// Return the pointee size for a concrete pointer place.
1804    pub(crate) fn place_pointee_size(
1805        &self,
1806        caller: rustc_hir::def_id::DefId,
1807        place: &PlaceKey,
1808    ) -> Option<u64> {
1809        if !place.fields.is_empty() {
1810            return None;
1811        }
1812        let local = match place.base {
1813            PlaceBaseKey::Return => Local::from_usize(0),
1814            PlaceBaseKey::Local(local) => Local::from_usize(local),
1815            PlaceBaseKey::Arg(_) => return None,
1816        };
1817        let body = self.tcx.optimized_mir(caller);
1818        let ty = body.local_decls[local].ty;
1819        let pointee = pointee_ty(ty)?;
1820        self.type_layout(caller, pointee).map(|(_, size)| size)
1821    }
1822
1823    /// Return the `index`-th call argument with contract projections appended.
1824    pub(crate) fn callsite_arg_place_with_fields(
1825        &self,
1826        checkpoint: &Checkpoint<'tcx>,
1827        index: usize,
1828        fields: &[usize],
1829    ) -> Option<PlaceKey> {
1830        let mut place = self.callsite_arg_place(checkpoint, index)?;
1831        place.fields.extend(fields.iter().copied());
1832        Some(place)
1833    }
1834
1835    /// Replace a callee generic parameter with its concrete checkpoint type.
1836    pub(crate) fn instantiate_callsite_ty(
1837        &self,
1838        checkpoint: &Checkpoint<'tcx>,
1839        ty: Ty<'tcx>,
1840    ) -> Ty<'tcx> {
1841        let TyKind::Param(param) = ty.kind() else {
1842            return ty;
1843        };
1844
1845        let body = self.tcx.optimized_mir(checkpoint.caller);
1846        let terminator = body.basic_blocks[checkpoint.block].terminator();
1847        let TerminatorKind::Call { func, .. } = &terminator.kind else {
1848            return ty;
1849        };
1850        let Operand::Constant(func_constant) = func else {
1851            return ty;
1852        };
1853        let TyKind::FnDef(_, args) = func_constant.const_.ty().kind() else {
1854            return ty;
1855        };
1856        let Some(arg) = args.get(param.index as usize) else {
1857            return ty;
1858        };
1859        match arg.kind() {
1860            GenericArgKind::Type(actual_ty) => actual_ty,
1861            _ => ty,
1862        }
1863    }
1864
1865    /// Replace a callee const generic parameter with its concrete checkpoint value.
1866    pub(crate) fn instantiate_callsite_const(
1867        &self,
1868        checkpoint: &Checkpoint<'tcx>,
1869        index: u32,
1870    ) -> Option<u128> {
1871        let body = self.tcx.optimized_mir(checkpoint.caller);
1872        let terminator = body.basic_blocks[checkpoint.block].terminator();
1873        let TerminatorKind::Call { func, .. } = &terminator.kind else {
1874            return None;
1875        };
1876        let Operand::Constant(func_constant) = func else {
1877            return None;
1878        };
1879        let TyKind::FnDef(_, args) = func_constant.const_.ty().kind() else {
1880            return None;
1881        };
1882        let arg = args.get(index as usize)?;
1883        match arg.kind() {
1884            GenericArgKind::Const(actual_const) => actual_const
1885                .try_to_target_usize(self.tcx)
1886                .map(|value| value as u128)
1887                .or_else(|| const_int_from_debug(&format!("{actual_const:?}"))),
1888            _ => None,
1889        }
1890    }
1891
1892    /// Return ABI alignment and size for a type.
1893    pub(crate) fn type_layout(
1894        &self,
1895        caller: rustc_hir::def_id::DefId,
1896        ty: Ty<'tcx>,
1897    ) -> Option<(u64, u64)> {
1898        safe_type_layout(self.tcx, caller, ty)
1899    }
1900
1901    /// Return the alignment required by a property type.
1902    ///
1903    /// For concrete types this is the ABI alignment. For generic parameters with
1904    /// finite representative candidates, the requirement must hold for every
1905    /// candidate, so we use the maximum candidate alignment.
1906    pub(crate) fn required_alignment(
1907        &self,
1908        caller: rustc_hir::def_id::DefId,
1909        ty: Ty<'tcx>,
1910    ) -> Option<u64> {
1911        if let Some((align, _)) = self.type_layout(caller, ty).filter(|(align, _)| *align > 0) {
1912            return Some(align);
1913        }
1914        if let Some(max_align) = self
1915            .generic_candidate_alignments(caller, ty)
1916            .and_then(|candidates| candidates.into_iter().max())
1917        {
1918            return Some(max_align);
1919        }
1920        if let TyKind::Array(elem, _) = ty.kind()
1921            && matches!(elem.kind(), TyKind::Param(_))
1922        {
1923            return Some(0);
1924        }
1925        if matches!(ty.kind(), TyKind::Param(_)) {
1926            return Some(0);
1927        }
1928        if matches!(ty.kind(), TyKind::Ref(..) | TyKind::RawPtr(..)) {
1929            return Some(0);
1930        }
1931        None
1932    }
1933
1934    /// Return a conservative byte size for a concrete or bounded generic type.
1935    ///
1936    /// For a generic parameter with representative candidates, every candidate
1937    /// must satisfy a numeric precondition.  We therefore use the maximum
1938    /// candidate size for upper-bound formulas such as
1939    /// `size_of(T) * len <= isize::MAX`.
1940    pub(crate) fn required_size(
1941        &self,
1942        caller: rustc_hir::def_id::DefId,
1943        ty: Ty<'tcx>,
1944    ) -> Option<u64> {
1945        if let TyKind::Array(elem, _) = ty.kind()
1946            && matches!(elem.kind(), TyKind::Param(_))
1947        {
1948            return Some(0);
1949        }
1950        if !matches!(ty.kind(), TyKind::Param(_)) {
1951            return self.type_layout(caller, ty).map(|(_, size)| size);
1952        }
1953        if let Some(max_size) = self
1954            .generic_candidate_sizes(caller, ty)
1955            .and_then(|candidates| candidates.into_iter().max())
1956        {
1957            return Some(max_size);
1958        }
1959        if matches!(ty.kind(), TyKind::Param(_)) {
1960            return Some(0);
1961        }
1962        None
1963    }
1964
1965    /// Classify whether a type is definitely zero-sized, definitely non-zero,
1966    /// or still layout-ambiguous under the current generic bounds.
1967    pub(crate) fn type_size_class(
1968        &self,
1969        caller: rustc_hir::def_id::DefId,
1970        ty: Ty<'tcx>,
1971    ) -> TypeSizeClass {
1972        if let TyKind::Array(elem, _) = ty.kind()
1973            && matches!(elem.kind(), TyKind::Param(_))
1974        {
1975            return TypeSizeClass::Unknown;
1976        }
1977        if !matches!(ty.kind(), TyKind::Param(_)) {
1978            return match self.type_layout(caller, ty).map(|(_, size)| size) {
1979                Some(0) => TypeSizeClass::Zero,
1980                Some(_) => TypeSizeClass::NonZero,
1981                None => TypeSizeClass::Unknown,
1982            };
1983        }
1984
1985        let Some(sizes) = self.generic_candidate_sizes(caller, ty) else {
1986            return TypeSizeClass::Unknown;
1987        };
1988        if sizes.iter().all(|size| *size == 0) {
1989            TypeSizeClass::Zero
1990        } else if sizes.iter().all(|size| *size > 0) {
1991            TypeSizeClass::NonZero
1992        } else {
1993            TypeSizeClass::Unknown
1994        }
1995    }
1996
1997    fn generic_candidate_alignments(
1998        &self,
1999        caller: rustc_hir::def_id::DefId,
2000        ty: Ty<'tcx>,
2001    ) -> Option<Vec<u64>> {
2002        let candidates = GenericTypeCandidates::for_def(self.tcx, caller);
2003        let alignments = candidates
2004            .candidates_for_ty(ty)?
2005            .iter()
2006            .filter_map(|candidate| self.type_layout(caller, *candidate).map(|(align, _)| align))
2007            .filter(|align| *align > 0)
2008            .collect::<Vec<_>>();
2009        if alignments.is_empty() {
2010            None
2011        } else {
2012            Some(alignments)
2013        }
2014    }
2015
2016    fn generic_candidate_sizes(
2017        &self,
2018        caller: rustc_hir::def_id::DefId,
2019        ty: Ty<'tcx>,
2020    ) -> Option<Vec<u64>> {
2021        let candidates = GenericTypeCandidates::for_def(self.tcx, caller);
2022        let sizes = candidates
2023            .candidates_for_ty(ty)?
2024            .iter()
2025            .filter_map(|candidate| self.type_layout(caller, *candidate).map(|(_, size)| size))
2026            .collect::<Vec<_>>();
2027        if sizes.is_empty() { None } else { Some(sizes) }
2028    }
2029}
2030
2031/// Trivalent size classification for type-dependent composite SPs.
2032#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2033pub(crate) enum TypeSizeClass {
2034    Zero,
2035    NonZero,
2036    Unknown,
2037}
2038
2039#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2040enum SliceIndexKind {
2041    Scalar,
2042    Range,
2043    RangeFrom,
2044    RangeTo,
2045    RangeFull,
2046    RangeInclusive,
2047    RangeToInclusive,
2048}
2049
2050/// General SMT obligation produced by an SP-specific lowering.
2051#[derive(Clone, Debug)]
2052pub enum SmtObligation {
2053    /// Prove that the address denoted by `place` is aligned to `align` bytes.
2054    Aligned {
2055        place: PlaceKey,
2056        align: u64,
2057        ty_name: String,
2058    },
2059    /// Future form for non-zero integer/address requirements.
2060    NonZero { place: PlaceKey },
2061    /// Future form for interval/bounds requirements.
2062    Range {
2063        value: PlaceKey,
2064        lower: i128,
2065        upper: Option<i128>,
2066    },
2067    /// Prove that `place` points to `access_count` elements inside its object.
2068    InBounds {
2069        place: PlaceKey,
2070        ty_name: String,
2071        elem_size: u64,
2072        access_count: SmtTerm,
2073    },
2074    /// Prove that pointer arithmetic starting at `place` stays inside the
2075    /// matched object for the whole delta range.
2076    PointerRangeInBounds {
2077        place: PlaceKey,
2078        ty_name: String,
2079        lower_delta: SmtTerm,
2080        upper_delta: SmtTerm,
2081    },
2082    /// Prove that `place` denotes initialized memory for `elements` elements.
2083    Initialized {
2084        place: PlaceKey,
2085        ty_name: String,
2086        elements: SmtTerm,
2087        /// Byte size of one element of the required type (None = unknown).
2088        elem_size: Option<u64>,
2089        /// Per-element size of the inner array element type.
2090        array_elem_size: Option<u64>,
2091        /// SMT term for the array length const-generic (when type is [T; N]).
2092        array_len_term: Option<SmtTerm>,
2093    },
2094    /// Prove that `place` points to `elements` elements in one live allocation.
2095    Allocated {
2096        place: PlaceKey,
2097        ty_name: String,
2098        elements: SmtTerm,
2099    },
2100    /// Prove that two pointer ranges do not overlap.
2101    NonOverlapping {
2102        left: PlaceKey,
2103        right: PlaceKey,
2104        left_count: SmtTerm,
2105        right_count: SmtTerm,
2106        elem_size: u64,
2107    },
2108    /// Prove one or more numeric predicates.
2109    Predicate { predicates: Vec<SmtPredicate> },
2110}
2111
2112impl SmtObligation {
2113    /// Render a compact obligation description for diagnostics.
2114    pub fn describe(&self) -> String {
2115        match self {
2116            SmtObligation::Aligned {
2117                place,
2118                align,
2119                ty_name,
2120            } => {
2121                format!(
2122                    "Align({}, {}, {}-byte boundary)",
2123                    place_label(place),
2124                    ty_name,
2125                    align
2126                )
2127            }
2128            SmtObligation::NonZero { place } => format!("NonZero({})", place_label(place)),
2129            SmtObligation::Range {
2130                value,
2131                lower,
2132                upper,
2133            } => match upper {
2134                Some(upper) => format!("Range({}, {lower}..{upper})", place_label(value)),
2135                None => format!("Range({}, {lower}..)", place_label(value)),
2136            },
2137            SmtObligation::InBounds {
2138                place,
2139                ty_name,
2140                elem_size,
2141                access_count,
2142            } => format!(
2143                "InBound({}, {}, {} element(s), {} byte(s) each)",
2144                place_label(place),
2145                ty_name,
2146                access_count.describe(),
2147                elem_size
2148            ),
2149            SmtObligation::PointerRangeInBounds {
2150                place,
2151                ty_name,
2152                lower_delta,
2153                upper_delta,
2154            } => format!(
2155                "PointerRangeInBound({}, {}, lower={}, upper={})",
2156                place_label(place),
2157                ty_name,
2158                lower_delta.describe(),
2159                upper_delta.describe()
2160            ),
2161            SmtObligation::Initialized {
2162                place,
2163                ty_name,
2164                elements,
2165                ..
2166            } => format!(
2167                "Init({}, {}, {} element(s))",
2168                place_label(place),
2169                ty_name,
2170                elements.describe()
2171            ),
2172            SmtObligation::Allocated {
2173                place,
2174                ty_name,
2175                elements,
2176            } => format!(
2177                "Allocated({}, {}, {} element(s))",
2178                place_label(place),
2179                ty_name,
2180                elements.describe()
2181            ),
2182            SmtObligation::NonOverlapping {
2183                left,
2184                right,
2185                left_count,
2186                right_count,
2187                elem_size,
2188            } => format!(
2189                "NonOverlap({}, {}, left={} element(s), right={} element(s), elem_size={})",
2190                place_label(left),
2191                place_label(right),
2192                left_count.describe(),
2193                right_count.describe(),
2194                elem_size
2195            ),
2196            SmtObligation::Predicate { predicates } => {
2197                let rendered = predicates
2198                    .iter()
2199                    .map(SmtPredicate::describe)
2200                    .collect::<Vec<_>>()
2201                    .join(" && ");
2202                format!("ValidNum({rendered})")
2203            }
2204        }
2205    }
2206}
2207
2208/// Common SMT term used by diagnostics and property-independent query building.
2209#[derive(Clone, Debug)]
2210pub enum SmtTerm {
2211    Place(PlaceKey),
2212    Value(String),
2213    Const(u64),
2214    /// Const-generic parameter value (produces the same SMT constant as
2215    /// `term_for_value` for `AbstractValue::Const`).
2216    ConstParam(String),
2217    Add(Box<SmtTerm>, Box<SmtTerm>),
2218    Sub(Box<SmtTerm>, Box<SmtTerm>),
2219    Mul(Box<SmtTerm>, Box<SmtTerm>),
2220    Div(Box<SmtTerm>, Box<SmtTerm>),
2221    Rem(Box<SmtTerm>, Box<SmtTerm>),
2222}
2223
2224impl SmtTerm {
2225    /// Render this term in compact source-facing form.
2226    pub fn describe(&self) -> String {
2227        match self {
2228            SmtTerm::Place(place) => place_label(place),
2229            SmtTerm::Value(value) => value.clone(),
2230            SmtTerm::ConstParam(value) => value.clone(),
2231            SmtTerm::Const(value) => value.to_string(),
2232            SmtTerm::Add(lhs, rhs) => format!("({} + {})", lhs.describe(), rhs.describe()),
2233            SmtTerm::Sub(lhs, rhs) => format!("({} - {})", lhs.describe(), rhs.describe()),
2234            SmtTerm::Mul(lhs, rhs) => format!("({} * {})", lhs.describe(), rhs.describe()),
2235            SmtTerm::Div(lhs, rhs) => format!("({} / {})", lhs.describe(), rhs.describe()),
2236            SmtTerm::Rem(lhs, rhs) => format!("({} % {})", lhs.describe(), rhs.describe()),
2237        }
2238    }
2239}
2240
2241/// Common boolean predicate asserted or refuted by SMT queries.
2242#[derive(Clone, Debug)]
2243pub enum SmtPredicate {
2244    Eq(SmtTerm, SmtTerm),
2245    Ne(SmtTerm, SmtTerm),
2246    Le(SmtTerm, SmtTerm),
2247    Lt(SmtTerm, SmtTerm),
2248    Ge(SmtTerm, SmtTerm),
2249    Gt(SmtTerm, SmtTerm),
2250    And(Vec<SmtPredicate>),
2251    Divisible {
2252        term: SmtTerm,
2253        modulus: u64,
2254    },
2255    InBounds {
2256        index: SmtTerm,
2257        access_count: SmtTerm,
2258        len: SmtTerm,
2259    },
2260    NonOverlapping {
2261        left: SmtTerm,
2262        right: SmtTerm,
2263        left_count: SmtTerm,
2264        right_count: SmtTerm,
2265        elem_size: u64,
2266    },
2267    Not(Box<SmtPredicate>),
2268    Custom(String),
2269}
2270
2271impl SmtPredicate {
2272    /// Render this predicate for diagnostics.
2273    pub fn describe(&self) -> String {
2274        match self {
2275            SmtPredicate::Eq(lhs, rhs) => format!("{} == {}", lhs.describe(), rhs.describe()),
2276            SmtPredicate::Ne(lhs, rhs) => format!("{} != {}", lhs.describe(), rhs.describe()),
2277            SmtPredicate::Le(lhs, rhs) => format!("{} <= {}", lhs.describe(), rhs.describe()),
2278            SmtPredicate::Lt(lhs, rhs) => format!("{} < {}", lhs.describe(), rhs.describe()),
2279            SmtPredicate::Ge(lhs, rhs) => format!("{} >= {}", lhs.describe(), rhs.describe()),
2280            SmtPredicate::Gt(lhs, rhs) => format!("{} > {}", lhs.describe(), rhs.describe()),
2281            SmtPredicate::And(predicates) => predicates
2282                .iter()
2283                .map(SmtPredicate::describe)
2284                .collect::<Vec<_>>()
2285                .join(" && "),
2286            SmtPredicate::Divisible { term, modulus } => {
2287                format!("{} % {modulus} == 0", term.describe())
2288            }
2289            SmtPredicate::InBounds {
2290                index,
2291                access_count,
2292                len,
2293            } => format!(
2294                "0 <= {} && {} + {} <= {}",
2295                index.describe(),
2296                index.describe(),
2297                access_count.describe(),
2298                len.describe()
2299            ),
2300            SmtPredicate::NonOverlapping {
2301                left,
2302                right,
2303                left_count,
2304                right_count,
2305                elem_size,
2306            } => format!(
2307                "{} + {} * {} <= {} || {} + {} * {} <= {}",
2308                left.describe(),
2309                left_count.describe(),
2310                elem_size,
2311                right.describe(),
2312                right.describe(),
2313                right_count.describe(),
2314                elem_size,
2315                left.describe()
2316            ),
2317            SmtPredicate::Not(predicate) => format!("not({})", predicate.describe()),
2318            SmtPredicate::Custom(text) => text.clone(),
2319        }
2320    }
2321}
2322
2323/// Solver query built from path facts plus one negated obligation.
2324#[derive(Clone, Debug)]
2325pub struct SmtQuery {
2326    /// Property-specific obligation being proved.
2327    pub obligation: SmtObligation,
2328    /// Assumptions asserted from forward facts.
2329    pub assumptions: Vec<SmtPredicate>,
2330    /// Negated goal sent to the solver.
2331    pub negated_goal: SmtPredicate,
2332}
2333
2334impl SmtQuery {
2335    /// Create a query description.
2336    pub fn new(
2337        obligation: SmtObligation,
2338        assumptions: Vec<SmtPredicate>,
2339        negated_goal: SmtPredicate,
2340    ) -> Self {
2341        Self {
2342            obligation,
2343            assumptions,
2344            negated_goal,
2345        }
2346    }
2347}
2348
2349/// Result of one SMT check.
2350#[derive(Clone, Debug)]
2351pub struct SmtCheckResult {
2352    /// Final status produced by the SMT backend.
2353    pub result: CheckResult,
2354    /// Optional structured query description.
2355    pub query: Option<SmtQuery>,
2356    /// Human-readable explanation.
2357    pub notes: Vec<String>,
2358}
2359
2360impl SmtCheckResult {
2361    /// Build a proved SMT result.
2362    pub fn proved(note: impl Into<String>) -> Self {
2363        Self {
2364            result: CheckResult::Proved,
2365            query: None,
2366            notes: vec![note.into()],
2367        }
2368    }
2369
2370    /// Build an unknown SMT result.
2371    pub fn unknown(note: impl Into<String>) -> Self {
2372        Self {
2373            result: CheckResult::Unknown,
2374            query: None,
2375            notes: vec![note.into()],
2376        }
2377    }
2378
2379    /// Attach the query that produced this result.
2380    pub fn with_query(mut self, query: SmtQuery) -> Self {
2381        self.query = Some(query);
2382        self
2383    }
2384
2385    /// Add a diagnostic note to this result.
2386    pub fn with_note(mut self, note: impl Into<String>) -> Self {
2387        self.notes.push(note.into());
2388        self
2389    }
2390
2391    /// Render this SMT result as a diagnostic block.
2392    pub fn describe(&self) -> String {
2393        let mut lines = vec![format!("      smt check: {:?}", self.result)];
2394        if let Some(query) = &self.query {
2395            lines.push(format!("        |_ goal: {}", query.obligation.describe()));
2396            if !query.assumptions.is_empty() {
2397                lines.push("        |_ known facts:".to_string());
2398                for assumption in &query.assumptions {
2399                    lines.push(format!("        |  |_ {}", assumption.describe()));
2400                }
2401            }
2402            lines.push(format!(
2403                "        |_ checked: {}",
2404                query.negated_goal.describe()
2405            ));
2406        }
2407        if let Some((first, rest)) = self.notes.split_first() {
2408            lines.push(format!("        |_ verdict: {first}"));
2409            for note in rest {
2410                if let Some(hint) = note.strip_prefix("hint: ") {
2411                    lines.push(format!("        |_ hint: {hint}"));
2412                } else {
2413                    lines.push(format!("        |_ detail: {note}"));
2414                }
2415            }
2416        }
2417        lines.join("\n")
2418    }
2419}
2420
2421fn failed_smt(note: impl Into<String>) -> SmtCheckResult {
2422    SmtCheckResult {
2423        result: CheckResult::Failed,
2424        query: None,
2425        notes: vec![note.into()],
2426    }
2427}
2428
2429fn smt_term_const_u64(term: &SmtTerm) -> Option<u64> {
2430    match term {
2431        SmtTerm::Const(value) => Some(*value),
2432        _ => None,
2433    }
2434}
2435
2436fn pointer_range_negated_goal(
2437    index: SmtTerm,
2438    lower_delta: SmtTerm,
2439    upper_delta: SmtTerm,
2440    len: SmtTerm,
2441) -> SmtPredicate {
2442    let lower_index = SmtTerm::Add(Box::new(index.clone()), Box::new(lower_delta));
2443    let upper_index = SmtTerm::Add(Box::new(index.clone()), Box::new(upper_delta));
2444    SmtPredicate::Not(Box::new(SmtPredicate::And(vec![
2445        SmtPredicate::Ge(index.clone(), SmtTerm::Const(0)),
2446        SmtPredicate::Le(index, len.clone()),
2447        SmtPredicate::Ge(lower_index, SmtTerm::Const(0)),
2448        SmtPredicate::Le(upper_index, len),
2449    ])))
2450}
2451
2452/// Per-query SMT term builder over a forward visit result.
2453pub(crate) struct SmtModel<'a, 'ctx, 'tcx> {
2454    tcx: TyCtxt<'tcx>,
2455    checkpoint: &'a Checkpoint<'tcx>,
2456    forward: &'a ForwardVisitResult<'tcx>,
2457    ctx: &'ctx Context,
2458    place_terms: HashMap<PlaceKey, Int<'ctx>>,
2459    /// Shared Z3 constants per MIR local — ensures every reference to the
2460    /// same local produces the exact same SMT term (e.g. `mid` used in both
2461    /// `ptr.add` and `unchecked_sub`).
2462    local_terms: HashMap<usize, Int<'ctx>>,
2463    symbolic_align_terms: HashMap<String, Int<'ctx>>,
2464    symbolic_len_terms: HashMap<String, Int<'ctx>>,
2465    const_terms: HashMap<String, Int<'ctx>>,
2466    assumptions: Vec<SmtPredicate>,
2467    /// Set to true when IndexAccess InBound assumptions were added from caller contract
2468    has_index_access_assumptions: bool,
2469}
2470
2471impl<'a, 'ctx, 'tcx> SmtModel<'a, 'ctx, 'tcx> {
2472    /// Create a fresh SMT model builder.
2473    pub(crate) fn new(
2474        tcx: TyCtxt<'tcx>,
2475        checkpoint: &'a Checkpoint<'tcx>,
2476        forward: &'a ForwardVisitResult<'tcx>,
2477        ctx: &'ctx Context,
2478    ) -> Self {
2479        Self {
2480            tcx,
2481            checkpoint,
2482            forward,
2483            ctx,
2484            place_terms: HashMap::new(),
2485            local_terms: HashMap::new(),
2486            symbolic_align_terms: HashMap::new(),
2487            symbolic_len_terms: HashMap::new(),
2488            const_terms: HashMap::new(),
2489            assumptions: Vec::new(),
2490            has_index_access_assumptions: false,
2491        }
2492    }
2493
2494    /// Create or return a cached symbolic alignment constant for a type name.
2495    pub(crate) fn symbolic_align_term(&mut self, ty_name: &str) -> Int<'ctx> {
2496        let ty_name = normalize_init_ty_name(ty_name);
2497        if let Some(term) = self.symbolic_align_terms.get(&ty_name) {
2498            return term.clone();
2499        }
2500        let term = Int::new_const(self.ctx, format!("align_{ty_name}"));
2501        self.symbolic_align_terms
2502            .insert(ty_name.to_string(), term.clone());
2503        term
2504    }
2505
2506    fn symbolic_len_term(&mut self, len_key: &str) -> Int<'ctx> {
2507        let name = sanitize_smt_name(len_key);
2508        if let Some(term) = self.symbolic_len_terms.get(&name) {
2509            return term.clone();
2510        }
2511        let term = Int::new_const(self.ctx, name.as_str());
2512        self.symbolic_len_terms.insert(name, term.clone());
2513        term
2514    }
2515
2516    /// Check whether a ContractFact in the forward facts targets the same
2517    /// (or Cast-equivalent) place with the given property kind.
2518    ///
2519    /// Used by struct-invariant checkpoint checks to short-circuit when a
2520    /// caller `#[rapx::requires]` already establishes the property.
2521    fn has_equivalent_contract_fact(&mut self, place: &PlaceKey, _kind: PropertyKind) -> bool {
2522        let Some(target_term) = self.term_for_place(place) else {
2523            return false;
2524        };
2525        for fact in &self.forward.facts {
2526            let StateFact::Contract(property) = fact else {
2527                continue;
2528            };
2529            let is_target_kind =
2530                matches!(property.kind, PropertyKind::InBound | PropertyKind::Init);
2531            if !is_target_kind {
2532                continue;
2533            }
2534            let Some(contract_target) = property.args.first().and_then(|arg| {
2535                if let PropertyArg::Place(contract_place) = arg {
2536                    let mut key = PlaceKey::from_contract_place(contract_place);
2537                    if let PlaceBaseKey::Arg(index) = key.base {
2538                        key.base = PlaceBaseKey::Local(index + 1);
2539                    }
2540                    Some(key)
2541                } else {
2542                    None
2543                }
2544            }) else {
2545                continue;
2546            };
2547            let Some(contract_term) = self.term_for_place(&contract_target) else {
2548                continue;
2549            };
2550            if target_term.eq(&contract_term) {
2551                return true;
2552            }
2553        }
2554        false
2555    }
2556
2557    fn contract_predicate_to_smt(
2558        &mut self,
2559        predicate: &NumericPredicate<'tcx>,
2560    ) -> Option<SmtPredicate> {
2561        let lhs = self.smt_term_from_contract_expr(&predicate.lhs)?;
2562        let rhs = self.smt_term_from_contract_expr(&predicate.rhs)?;
2563        Some(match predicate.op {
2564            RelOp::Eq => SmtPredicate::Eq(lhs, rhs),
2565            RelOp::Ne => SmtPredicate::Ne(lhs, rhs),
2566            RelOp::Lt => SmtPredicate::Lt(lhs, rhs),
2567            RelOp::Le => SmtPredicate::Le(lhs, rhs),
2568            RelOp::Gt => SmtPredicate::Gt(lhs, rhs),
2569            RelOp::Ge => SmtPredicate::Ge(lhs, rhs),
2570        })
2571    }
2572
2573    fn smt_term_from_contract_expr(
2574        &mut self,
2575        expr: &ContractExpr<'tcx>,
2576    ) -> Option<SmtTerm> {
2577        match expr {
2578            ContractExpr::Place(place) => {
2579                let mut key = PlaceKey::from_contract_place(place);
2580                if let PlaceBaseKey::Arg(index) = key.base {
2581                    key.base = PlaceBaseKey::Local(index + 1);
2582                }
2583                Some(SmtTerm::Place(key))
2584            }
2585            ContractExpr::Const(value) => {
2586                u64::try_from(*value).ok().map(SmtTerm::Const)
2587            }
2588            ContractExpr::ConstParam { name, .. } => {
2589                Some(SmtTerm::ConstParam(name.clone()))
2590            }
2591            ContractExpr::Len(inner) => {
2592                let origin = match inner.as_ref() {
2593                    ContractExpr::Place(cp) => {
2594                        let mut key = PlaceKey::from_contract_place(cp);
2595                        if let PlaceBaseKey::Arg(index) = key.base {
2596                            key.base = PlaceBaseKey::Local(index + 1);
2597                        }
2598                        let val = AbstractValue::Place(key.clone());
2599                        self.origin_key_for_value(&val, &mut TraceSeen::new())
2600                    }
2601                    _ => None,
2602                };
2603
2604                // Try to link the contract's `len(self)` to the actual MIR
2605                // `slice::len()` call result on this path.
2606                let origin_str = origin.clone().unwrap_or_default();
2607                let mut first_len_dest: Option<PlaceKey> = None;
2608                for fact in &self.forward.facts {
2609                    let StateFact::Call(call) = fact else { continue };
2610                    let is_len_call = call.func.ends_with("::len")
2611                        || call.func.contains("::len(");
2612                    if !is_len_call {
2613                        continue;
2614                    }
2615                    for effect in &call.effects {
2616                        let crate::verify::call_summary::CallEffect::ReturnLengthOfArg {
2617                            arg,
2618                        } = effect
2619                        else {
2620                            continue;
2621                        };
2622                        let dest = PlaceKey {
2623                            base: PlaceBaseKey::Local(call.destination.as_usize()),
2624                            fields: Vec::new(),
2625                        };
2626                        // Remember first len call for fallback.
2627                        if first_len_dest.is_none() {
2628                            first_len_dest = Some(dest.clone());
2629                        }
2630                        let Some(arg_value) = call.args.get(*arg) else {
2631                            continue;
2632                        };
2633                        let arg_origin = self.origin_key_for_value(
2634                            arg_value,
2635                            &mut TraceSeen::new(),
2636                        );
2637                        let matches = arg_origin.as_deref() == Some(&origin_str)
2638                            || arg_origin
2639                                .as_deref()
2640                                .is_some_and(|ao| ao.starts_with(&origin_str));
2641                        if matches {
2642                            return Some(SmtTerm::Place(dest));
2643                        }
2644                    }
2645                }
2646                // Fallback: use any len() call result on this path.
2647                if let Some(dest) = first_len_dest {
2648                    return Some(SmtTerm::Place(dest));
2649                }
2650
2651                Some(SmtTerm::Value(format!("len({})", origin_str)))
2652            }
2653            ContractExpr::Binary { op, lhs, rhs } => {
2654                let lhs = Box::new(self.smt_term_from_contract_expr(lhs)?);
2655                let rhs = Box::new(self.smt_term_from_contract_expr(rhs)?);
2656                Some(match op {
2657                    NumericOp::Add => SmtTerm::Add(lhs, rhs),
2658                    NumericOp::Sub => SmtTerm::Sub(lhs, rhs),
2659                    NumericOp::Mul => SmtTerm::Mul(lhs, rhs),
2660                    NumericOp::Div => SmtTerm::Div(lhs, rhs),
2661                    NumericOp::Rem => SmtTerm::Rem(lhs, rhs),
2662                    _ => return None,
2663                })
2664            }
2665            _ => None,
2666        }
2667    }
2668
2669    fn value_to_int(&mut self, value: &AbstractValue<'tcx>) -> Option<Int<'ctx>> {
2670        match value {
2671            AbstractValue::ConstInt(v) => u64::try_from(*v).ok().map(|v| Int::from_u64(self.ctx, v)),
2672            AbstractValue::Place(place) => self.term_for_place(place),
2673            AbstractValue::ConstParam(name) => Some(Int::new_const(self.ctx, format!("const_{name}").as_str())),
2674            AbstractValue::Const(name) => Some(Int::new_const(self.ctx, sanitize_smt_name(name).as_str())),
2675            _ => None,
2676        }
2677    }
2678
2679    /// Assert facts collected by the forward visitor.
2680    pub(crate) fn assert_forward_facts(&mut self, solver: &Solver<'ctx>) {
2681        for fact in &self.forward.facts {
2682            match fact {
2683                StateFact::PointsTo { pointer, source } => {
2684                    self.assert_place_non_zero(
2685                        solver,
2686                        pointer,
2687                        "created from a reference/raw pointer",
2688                    );
2689                    // Only assert alignment on the pointer when the pointee types
2690                    // match — pointer-arithmetic wrappers produce pointers of a
2691                    // different pointee type whose alignment must be proved from
2692                    // guard facts, not from the raw type alone.
2693                    // Normalize types (strip MaybeUninit, array/slice brackets)
2694                    // so that e.g. [T] and T share the same alignment key.
2695                    let ptr_pointee_str = self.place_ty(pointer)
2696                        .and_then(|ty| pointee_ty_str(ty))
2697                        .map(|s| normalize_init_ty_name(&s));
2698                    let src_pointee_str = self.place_ty(source)
2699                        .and_then(|ty| pointee_ty_str(ty))
2700                        .map(|s| normalize_init_ty_name(&s));
2701                    if ptr_pointee_str == src_pointee_str {
2702                        self.assert_place_alignment(solver, pointer);
2703                    }
2704                    self.assert_place_alignment(solver, source);
2705                    self.assert_length_alias(solver, pointer, source);
2706                }
2707                StateFact::Call(call) => {
2708                    if is_as_ptr_call(&call.func) {
2709                        let place = PlaceKey {
2710                            base: PlaceBaseKey::Local(call.destination.as_usize()),
2711                            fields: Vec::new(),
2712                        };
2713                        self.assert_place_non_zero(solver, &place, "returned by as_ptr");
2714                        self.assert_place_alignment(solver, &place);
2715                    }
2716                    self.record_call_effect_assumptions(call);
2717                    // exact_div(x, y) returns x / y.  Assert result <= x
2718                    // so that downstream bounds checks (e.g.
2719                    // from_raw_parts(ptr, exact_div(len, N))) can infer
2720                    // chunk_count <= len.
2721                    if call.func.contains("exact_div") {
2722                        let dest = PlaceKey {
2723                            base: PlaceBaseKey::Local(call.destination.as_usize()),
2724                            fields: Vec::new(),
2725                        };
2726                        if let Some(dest_term) = self.term_for_place(&dest)
2727                            && let Some(arg0) = call.args.get(0)
2728                        {
2729                            let cursor = self.call_definition_cursor(call);
2730                            if let Some(arg0_term) = self.term_for_value_at(
2731                                arg0,
2732                                cursor,
2733                                &mut TraceSeen::new(),
2734                            ) {
2735                                solver.assert(&dest_term.le(&arg0_term));
2736                                // Cache the term so subsequent uses of this
2737                                // local (e.g. Allocated check) get the same
2738                                // Z3 variable.
2739                                self.place_terms.insert(dest.clone(), dest_term);
2740                                self.assumptions.push(SmtPredicate::Le(
2741                                    SmtTerm::Place(dest),
2742                                    SmtTerm::Value(value_label(arg0)),
2743                                ));
2744                            }
2745                        }
2746                    }
2747                }
2748                StateFact::KnownNonZero { place, reason } => {
2749                    self.assert_place_non_zero(solver, place, reason);
2750                }
2751                StateFact::KnownAligned {
2752                    place,
2753                    align,
2754                    ty_name,
2755                    reason,
2756                } => {
2757                    self.assert_known_alignment(solver, place, *align, ty_name, reason);
2758                }
2759                StateFact::KnownInit {
2760                    place,
2761                    ty_name,
2762                    elements,
2763                    reason,
2764                } => {
2765                    self.assumptions.push(SmtPredicate::Custom(format!(
2766                        "{} initialized for {ty_name}, {elements} element(s) ({reason})",
2767                        place_label(place)
2768                    )));
2769                }
2770                StateFact::KnownAllocated {
2771                    place,
2772                    object,
2773                    ty_name,
2774                    elements,
2775                    reason,
2776                } => {
2777                    self.assumptions.push(SmtPredicate::Custom(format!(
2778                        "{} allocated in {} for {ty_name}, {elements} element(s) ({reason})",
2779                        place_label(place),
2780                        place_label(object)
2781                    )));
2782                }
2783                StateFact::KnownConst {
2784                    place,
2785                    value,
2786                    reason,
2787                } => {
2788                    self.assert_known_const(solver, place, *value, reason);
2789                }
2790                StateFact::BranchEq { value, equals, cmp_op, cmp_lhs, cmp_rhs } => {
2791                    if let Some(term) = self.term_for_value(value, &mut HashSet::new()) {
2792                        let expected = Int::from_u64(self.ctx, *equals as u64);
2793                        solver.assert(&term._eq(&expected));
2794                        self.assumptions.push(SmtPredicate::Eq(
2795                            SmtTerm::Value(value_label(value)),
2796                            SmtTerm::Const(*equals as u64),
2797                        ));
2798                    }
2799                    if let (Some(op), Some(lhs), Some(rhs)) = (cmp_op, cmp_lhs, cmp_rhs) {
2800                        if let (Some(lhs_t), Some(rhs_t)) = (
2801                            self.value_to_int(lhs), self.value_to_int(rhs),
2802                        ) {
2803                            let holds = *equals == 1;
2804                            match (op, holds) {
2805                                (BinOp::Eq, true) | (BinOp::Ne, false) => solver.assert(&lhs_t._eq(&rhs_t)),
2806                                (BinOp::Ne, true) | (BinOp::Eq, false) => solver.assert(&lhs_t._eq(&rhs_t).not()),
2807                                (BinOp::Lt, true) | (BinOp::Ge, false) => solver.assert(&lhs_t.lt(&rhs_t)),
2808                                (BinOp::Le, true) | (BinOp::Gt, false) => solver.assert(&lhs_t.le(&rhs_t)),
2809                                (BinOp::Gt, true) | (BinOp::Le, false) => solver.assert(&lhs_t.gt(&rhs_t)),
2810                                (BinOp::Ge, true) | (BinOp::Lt, false) => solver.assert(&lhs_t.ge(&rhs_t)),
2811                                _ => {}
2812                            }
2813                        }
2814                    }
2815                }
2816                StateFact::Cast { target, source, .. } => {
2817                    self.assumptions.push(SmtPredicate::Eq(
2818                        SmtTerm::Place(target.clone()),
2819                        SmtTerm::Value(value_label(source)),
2820                    ));
2821                    if let AbstractValue::Place(source_place) = source {
2822                        if self
2823                            .place_ty(source_place)
2824                            .is_some_and(|ty| pointee_ty(ty).is_some())
2825                        {
2826                            self.assert_place_alignment(solver, source_place);
2827                        }
2828                    }
2829                    if let Some(term) = self.term_for_value(source, &mut HashSet::new()) {
2830                        self.place_terms.insert(target.clone(), term);
2831                    }
2832                }
2833                StateFact::Binary {
2834                    target,
2835                    op,
2836                    lhs,
2837                    rhs,
2838                } => {
2839                    self.assumptions.push(SmtPredicate::Eq(
2840                        SmtTerm::Place(target.clone()),
2841                        SmtTerm::Value(format!(
2842                            "({} {} {})",
2843                            value_label(lhs),
2844                            binop_label(*op),
2845                            value_label(rhs)
2846                        )),
2847                    ));
2848                }
2849                StateFact::Contract(property) => match property.kind {
2850                    PropertyKind::Align => {
2851                        let Some(target) = (|| {
2852                            let arg = property.args.first()?;
2853                            let PropertyArg::Place(place) = arg else {
2854                                return None;
2855                            };
2856                            let mut key = PlaceKey::from_contract_place(place);
2857                            if let PlaceBaseKey::Arg(index) = key.base {
2858                                key.base = PlaceBaseKey::Local(index + 1);
2859                            }
2860                            Some(key)
2861                        })() else {
2862                            continue;
2863                        };
2864                        let Some(required_ty) = property.args.iter().find_map(|arg| {
2865                            if let PropertyArg::Ty(ty) = arg {
2866                                Some(*ty)
2867                            } else {
2868                                None
2869                            }
2870                        }) else {
2871                            continue;
2872                        };
2873                        let Some((align, _)) = self.type_layout(required_ty) else {
2874                            continue;
2875                        };
2876                        if align == 0 {
2877                            let ty_name = format!("{required_ty:?}");
2878                            if let Some(term) = self.term_for_place(&target) {
2879                                let align_term = self.symbolic_align_term(&ty_name);
2880                                let zero = Int::from_u64(self.ctx, 0);
2881                                solver.assert(&term.modulo(&align_term)._eq(&zero));
2882                                self.assumptions.push(SmtPredicate::Custom(format!(
2883                                    "{} aligned for {ty_name} (symbolic, struct-invariant)",
2884                                    place_label(&target)
2885                                )));
2886                            }
2887                        } else {
2888                            self.assert_known_alignment(
2889                                solver,
2890                                &target,
2891                                align,
2892                                &format!("{required_ty:?}"),
2893                                "struct-invariant",
2894                            );
2895                        }
2896                    }
2897                    PropertyKind::NonNull => {
2898                        let Some(target) = (|| {
2899                            let arg = property.args.first()?;
2900                            let PropertyArg::Place(place) = arg else {
2901                                return None;
2902                            };
2903                            let mut key = PlaceKey::from_contract_place(place);
2904                            if let PlaceBaseKey::Arg(index) = key.base {
2905                                key.base = PlaceBaseKey::Local(index + 1);
2906                            }
2907                            Some(key)
2908                        })() else {
2909                            continue;
2910                        };
2911                        self.assert_place_non_zero(solver, &target, "caller-contract");
2912                    }
2913                    PropertyKind::InBound => {
2914                        if let Some(PropertyArg::Expr(ContractExpr::IndexAccess {
2915                            slice,
2916                            index,
2917                        })) = property.args.first()
2918                        {
2919                            let slice_key = match slice.as_ref() {
2920                                ContractExpr::Place(place) => {
2921                                    let mut key = PlaceKey::from_contract_place(place);
2922                                    if let PlaceBaseKey::Arg(ix) = key.base {
2923                                        key.base = PlaceBaseKey::Local(ix + 1);
2924                                    }
2925                                    key
2926                                }
2927                                _ => {
2928                                    continue;
2929                                }
2930                            };
2931                            let slice_label = place_label(&slice_key);
2932                            let len =
2933                                SmtTerm::Value(format!("len({slice_label})"));
2934
2935                            if let ContractExpr::Place(place) = index.as_ref() {
2936                                let mut index_key =
2937                                    PlaceKey::from_contract_place(place);
2938                                if let PlaceBaseKey::Arg(ix) = index_key.base {
2939                                    index_key.base = PlaceBaseKey::Local(ix + 1);
2940                                }
2941                                if let Some(ty) = self.place_ty(&index_key)
2942                                    && let TyKind::Array(_, len_const) = ty.kind()
2943                                {
2944                                    if let Some(array_len) =
2945                                        len_const.try_to_target_usize(self.tcx)
2946                                    {
2947                                        for j in 0..array_len {
2948                                            let elem_key = PlaceKey {
2949                                                base: index_key.base.clone(),
2950                                                fields: vec![j as usize],
2951                                            };
2952                                            let index_term =
2953                                                SmtTerm::Place(elem_key);
2954                                            let lower = index_term.clone();
2955                                            let upper = SmtTerm::Add(
2956                                                Box::new(index_term),
2957                                                Box::new(SmtTerm::Const(1)),
2958                                            );
2959                                            let preds = vec![
2960                                                SmtPredicate::Le(
2961                                                    SmtTerm::Const(0),
2962                                                    lower.clone(),
2963                                                ),
2964                                                SmtPredicate::Le(
2965                                                    lower.clone(),
2966                                                    upper.clone(),
2967                                                ),
2968                                                SmtPredicate::Le(
2969                                                    upper,
2970                                                    len.clone(),
2971                                                ),
2972                                            ];
2973                                            for pred in &preds {
2974                                                if let Some(bool_term) =
2975                                                    self.bool_for_predicate(pred)
2976                                                {
2977                                                    solver.assert(&bool_term);
2978                                                }
2979                                                self.assumptions
2980                                                    .push(pred.clone());
2981                                            }
2982                                        }
2983                                        self.has_index_access_assumptions =
2984                                            true;
2985                                        continue;
2986            }
2987        }
2988        // Const-generic parameters (e.g. const N: usize) are
2989        // always non-negative.  Assert this so that constraints like
2990        // N != 0 or N >= 1 are provable.
2991        for term in self.const_terms.values() {
2992            solver.assert(&term.ge(&Int::from_u64(self.ctx, 0)));
2993        }
2994    }
2995
2996                            let index_term = match index.as_ref() {
2997                                ContractExpr::Place(place) => {
2998                                    let mut key =
2999                                        PlaceKey::from_contract_place(place);
3000                                    if let PlaceBaseKey::Arg(ix) = key.base {
3001                                        key.base = PlaceBaseKey::Local(ix + 1);
3002                                    }
3003                                    Some(SmtTerm::Place(key))
3004                                }
3005                                ContractExpr::Const(value) => {
3006                                    u64::try_from(*value).ok().map(SmtTerm::Const)
3007                                }
3008                                _ => None,
3009                            };
3010                            let Some(index_term) = index_term else {
3011                                continue;
3012                            };
3013                            let lower = index_term.clone();
3014                            let upper = SmtTerm::Add(
3015                                Box::new(index_term),
3016                                Box::new(SmtTerm::Const(1)),
3017                            );
3018                            let preds = vec![
3019                                SmtPredicate::Le(
3020                                    SmtTerm::Const(0),
3021                                    lower.clone(),
3022                                ),
3023                                SmtPredicate::Le(
3024                                    lower.clone(),
3025                                    upper.clone(),
3026                                ),
3027                                SmtPredicate::Le(upper, len),
3028                            ];
3029                            for pred in &preds {
3030                                if let Some(bool_term) =
3031                                    self.bool_for_predicate(pred)
3032                                {
3033                                    solver.assert(&bool_term);
3034                                }
3035                                self.assumptions.push(pred.clone());
3036                            }
3037                            self.has_index_access_assumptions = true;
3038                            continue;
3039                        }
3040                        // Handle 3-arg InBound(ptr, Ty, index) form
3041                        // args = [Place(slice_ptr), Ty(T), Expr(index)]
3042                        if property.args.len() == 3 {
3043                            let slice_place = match property.args.get(0) {
3044                                Some(PropertyArg::Place(place)) => place,
3045                                _ => {
3046                                    continue;
3047                                }
3048                            };
3049                            let index_expr = match property.args.get(2) {
3050                                Some(PropertyArg::Expr(expr)) => expr,
3051                                _ => {
3052                                    continue;
3053                                }
3054                            };
3055                            let slice_label = {
3056                                let mut key = PlaceKey::from_contract_place(slice_place);
3057                                if let PlaceBaseKey::Arg(ix) = key.base {
3058                                    key.base = PlaceBaseKey::Local(ix + 1);
3059                                }
3060                                place_label(&key)
3061                            };
3062                            let len = SmtTerm::Value(format!("len({slice_label})"));
3063                            let index_term = match index_expr {
3064                                ContractExpr::Place(place) => {
3065                                    let mut key = PlaceKey::from_contract_place(place);
3066                                    if let PlaceBaseKey::Arg(ix) = key.base {
3067                                        key.base = PlaceBaseKey::Local(ix + 1);
3068                                    }
3069                                    Some(SmtTerm::Place(key))
3070                                }
3071                                ContractExpr::Const(value) => {
3072                                    u64::try_from(*value).ok().map(SmtTerm::Const)
3073                                }
3074                                _ => None,
3075                            };
3076                            let Some(index_term) = index_term else {
3077                                continue;
3078                            };
3079                            let lower = index_term.clone();
3080                            let upper = SmtTerm::Add(
3081                                Box::new(index_term),
3082                                Box::new(SmtTerm::Const(1)),
3083                            );
3084                            let preds = vec![
3085                                SmtPredicate::Le(SmtTerm::Const(0), lower.clone()),
3086                                SmtPredicate::Le(lower.clone(), upper.clone()),
3087                                SmtPredicate::Le(upper, len),
3088                            ];
3089                            for pred in &preds {
3090                                if let Some(bool_term) = self.bool_for_predicate(pred) {
3091                                    solver.assert(&bool_term);
3092                                }
3093                                self.assumptions.push(pred.clone());
3094                            }
3095                            self.has_index_access_assumptions = true;
3096                            continue;
3097                        }
3098                        let Some(target) = (|| {
3099                            let arg = property.args.first()?;
3100                            let PropertyArg::Place(place) = arg else {
3101                                return None;
3102                            };
3103                            let mut key = PlaceKey::from_contract_place(place);
3104                            if let PlaceBaseKey::Arg(index) = key.base {
3105                                key.base = PlaceBaseKey::Local(index + 1);
3106                            }
3107                            Some(key)
3108                        })() else {
3109                            continue;
3110                        };
3111                        let Some(required_ty) = property.args.iter().find_map(|arg| {
3112                            if let PropertyArg::Ty(ty) = arg {
3113                                Some(*ty)
3114                            } else {
3115                                None
3116                            }
3117                        }) else {
3118                            continue;
3119                        };
3120                        let Some((_, elem_size)) = self.type_layout(required_ty) else {
3121                            continue;
3122                        };
3123                        let access_count = property
3124                            .args
3125                            .iter()
3126                            .rev()
3127                            .find_map(|arg| {
3128                                let PropertyArg::Expr(ContractExpr::Const(value)) = arg else {
3129                                    return None;
3130                                };
3131                                u64::try_from(*value).ok()
3132                            })
3133                            .unwrap_or(0);
3134                        self.assumptions.push(SmtPredicate::InBounds {
3135                            index: SmtTerm::Const(0),
3136                            access_count: SmtTerm::Const(access_count),
3137                            len: SmtTerm::Value(format!("precond_len_{}", place_label(&target))),
3138                        });
3139                        if elem_size > 0 && access_count > 0 {
3140                            self.assumptions.push(SmtPredicate::Custom(format!(
3141                                "InBound({}, T, {access_count}) holds (caller-contract, elem_size={elem_size})",
3142                                place_label(&target)
3143                            )));
3144                        } else {
3145                            self.assumptions.push(SmtPredicate::Custom(format!(
3146                                "InBound({}, T, {access_count}) holds (caller-contract, symbolic)",
3147                                place_label(&target)
3148                            )));
3149                        }
3150                    }
3151                    PropertyKind::Init => {
3152                        let Some(target) = (|| {
3153                            let arg = property.args.first()?;
3154                            let PropertyArg::Place(place) = arg else {
3155                                return None;
3156                            };
3157                            let mut key = PlaceKey::from_contract_place(place);
3158                            if let PlaceBaseKey::Arg(index) = key.base {
3159                                key.base = PlaceBaseKey::Local(index + 1);
3160                            }
3161                            Some(key)
3162                        })() else {
3163                            continue;
3164                        };
3165                        let Some(required_ty) = property.args.iter().find_map(|arg| {
3166                            if let PropertyArg::Ty(ty) = arg {
3167                                Some(*ty)
3168                            } else {
3169                                None
3170                            }
3171                        }) else {
3172                            continue;
3173                        };
3174                        let elements = property
3175                            .args
3176                            .iter()
3177                            .rev()
3178                            .find_map(|arg| {
3179                                let PropertyArg::Expr(ContractExpr::Const(value)) = arg else {
3180                                    return None;
3181                                };
3182                                u64::try_from(*value).ok()
3183                            })
3184                            .unwrap_or(0);
3185                        self.assumptions.push(SmtPredicate::Custom(format!(
3186                            "{} initialized for {:?}, {elements} element(s) (caller-contract)",
3187                            place_label(&target),
3188                            required_ty
3189                        )));
3190                    }
3191                    PropertyKind::ValidNum => {
3192                        if let Some(PropertyArg::Predicates(predicates)) =
3193                            property.args.first()
3194                        {
3195                            for predicate in predicates {
3196                                if let Some(smt_pred) = self.contract_predicate_to_smt(predicate) {
3197                                    if let Some(z3_bool) = self.bool_for_predicate(&smt_pred) {
3198                                        solver.assert(&z3_bool);
3199                                    }
3200                                    self.assumptions.push(smt_pred);
3201                                }
3202                            }
3203                        }
3204                    }
3205                    _ => {}
3206                },
3207                StateFact::PathCondition(_)
3208                | StateFact::Drop(_)
3209                | StateFact::LocalDead(_)
3210                | StateFact::CallEffect(_) => {}
3211            }
3212        }
3213
3214        // Assert unsigned-integer function arguments are non-negative.
3215        // Arguments are atomic inputs (MIR locals `1..=arg_count`), so this is
3216        // universally true for usize / u8..u128 and does not over-constrain any
3217        // computed value.  It is required for proving bounds goals where the
3218        // offset involves subtraction (e.g. `ptr.add(len - k)` needs `k >= 0`
3219        // together with `len >= 0`).
3220        let body = self.tcx.optimized_mir(self.checkpoint.caller);
3221        for arg in 1..=body.arg_count {
3222            let place = PlaceKey {
3223                base: PlaceBaseKey::Local(arg),
3224                fields: Vec::new(),
3225            };
3226            let Some(ty) = self.place_ty(&place) else { continue };
3227            if !is_unsigned_integral_ty(ty) { continue }
3228            let Some(term) = self.term_for_place(&place) else { continue };
3229            let zero = Int::from_u64(self.ctx, 0);
3230            solver.assert(&term.ge(&zero));
3231            self.assumptions.push(SmtPredicate::Ge(
3232                SmtTerm::Place(place),
3233                SmtTerm::Const(0),
3234            ));
3235        }
3236    }
3237
3238    /// Return the path assumptions asserted by this model.
3239    pub(crate) fn assumptions(&self) -> &[SmtPredicate] {
3240        &self.assumptions
3241    }
3242
3243    fn latest_cursor(&self) -> ValueCursor {
3244        self.forward.value_definitions.len()
3245    }
3246
3247    fn call_definition_cursor(&self, call: &CallSummary<'tcx>) -> ValueCursor {
3248        self.forward
3249            .value_definitions
3250            .iter()
3251            .find_map(|definition| {
3252                if definition.local != call.destination {
3253                    return None;
3254                }
3255                let AbstractValue::CallResult(recorded) = &definition.value else {
3256                    return None;
3257                };
3258                if recorded.func == call.func && recorded.arg_count == call.arg_count {
3259                    Some(definition.ordinal)
3260                } else {
3261                    None
3262                }
3263            })
3264            .unwrap_or_else(|| self.latest_cursor())
3265    }
3266
3267    /// Try to recover the slice index/length terms behind a pointer result.
3268    ///
3269    /// Supported forms:
3270    ///
3271    /// - `slice.as_ptr().add(index)` and wrappers summarized as `ReturnPointerAdd`
3272    /// - plain `slice.as_ptr()` / `slice.as_mut_ptr()`, treated as index `0`
3273    pub(crate) fn pointer_bounds_for_place(
3274        &mut self,
3275        place: &PlaceKey,
3276    ) -> Option<PointerBounds<'ctx>> {
3277        if let Some(call) = self.pointer_add_call_for_place(place) {
3278            let (base_arg, offset_arg) = call.effects.iter().find_map(|effect| {
3279                match effect {
3280                    crate::verify::call_summary::CallEffect::ReturnPointerAdd {
3281                        base_arg,
3282                        offset_arg,
3283                        ..
3284                    }
3285                    | crate::verify::call_summary::CallEffect::ReturnPointerSub {
3286                        base_arg,
3287                        offset_arg,
3288                        ..
3289                    } => Some((*base_arg, *offset_arg)),
3290                    _ => None,
3291                }
3292            })?;
3293            let base = call.args.get(base_arg)?;
3294            let index = call.args.get(offset_arg)?;
3295            let call_cursor = self.call_definition_cursor(&call);
3296            let base_origin =
3297                self.origin_key_for_value_before(base, call_cursor, &mut TraceSeen::new())?;
3298
3299            let index_term = self.term_for_value_at(index, call_cursor, &mut TraceSeen::new())?;
3300            let (len_term_int, len_term) = self.bounds_len_for_origin(&base_origin, Some(index))?;
3301
3302            // For pointer arithmetic on a base pointer that is a Range field,
3303            // adjust the index: base_offset +/- count instead of just count.
3304            let result_index_smt: SmtTerm;
3305            let result_index_val: Int<'ctx>;
3306            if let AbstractValue::Place(base_place) = base {
3307                let (base_smt, base_val) = self.field_projection_index(
3308                    base_place, &base_origin, &len_term,
3309                );
3310                if !matches!(&base_smt, SmtTerm::Const(0)) {
3311                    // base has a non-zero offset — adjust by the arithmetic
3312                    let is_sub = call_has_pointer_sub_effect(&call);
3313                    let (adjusted_val, adjusted_smt) = if is_sub {
3314                        let v = Int::sub(&self.ctx, &[base_val, index_term]);
3315                        let s = SmtTerm::Sub(Box::new(base_smt), Box::new(SmtTerm::Value(value_label(index))));
3316                        (v, s)
3317                    } else {
3318                        let v = Int::add(&self.ctx, &[base_val, index_term]);
3319                        let s = SmtTerm::Add(Box::new(base_smt), Box::new(SmtTerm::Value(value_label(index))));
3320                        (v, s)
3321                    };
3322                    result_index_val = adjusted_val;
3323                    result_index_smt = adjusted_smt;
3324                } else {
3325                    result_index_val = index_term;
3326                    result_index_smt = SmtTerm::Value(value_label(index));
3327                }
3328            } else {
3329                result_index_val = index_term;
3330                result_index_smt = SmtTerm::Value(value_label(index));
3331            }
3332
3333            return Some(PointerBounds {
3334                index: result_index_val,
3335                len: len_term_int,
3336                index_term: result_index_smt,
3337                len_term,
3338                origin_key: base_origin,
3339            });
3340        }
3341
3342        let value = self
3343            .resolved_value_for_place(place, &mut TraceSeen::new())
3344            .unwrap_or_else(|| AbstractValue::Place(place.clone()));
3345        let base_origin =
3346            self.origin_key_for_value_before(&value, self.latest_cursor(), &mut TraceSeen::new())?;
3347        let zero = AbstractValue::ConstInt(0);
3348        let (mut len_term_int, mut len_term) = self.bounds_len_for_origin(&base_origin, Some(&zero))?;
3349
3350        if place.fields.is_empty()
3351            && let Some(local) = place.local()
3352            && let Some(definition) = self.forward.latest_value_definition_before(local, self.latest_cursor())
3353        {
3354            if let AbstractValue::Cast(inner, cast_ty) = &definition.value {
3355                let caller = self.checkpoint.caller;
3356                if let Some(dst_pt) = pointee_ty(*cast_ty) {
3357                    let dst_size = safe_type_layout(self.tcx, caller, dst_pt).map(|(_, s)| s);
3358                    let src_ty = match &**inner {
3359                        AbstractValue::Place(inner_place)
3360                        | AbstractValue::RawPtr(inner_place)
3361                        | AbstractValue::Ref(inner_place) => {
3362                            inner_place.local().and_then(|local| {
3363                                let body = self.tcx.optimized_mir(caller);
3364                                let ty = body.local_decls[local].ty;
3365                                pointee_ty(ty)
3366                            })
3367                        }
3368                        _ => None,
3369                    };
3370                    let src_size = src_ty.and_then(|pt| safe_type_layout(self.tcx, caller, pt).map(|(_, s)| s));
3371                    let ratio_smt = match (src_size, dst_size) {
3372                        (Some(src), Some(dst)) if src > dst && src % dst == 0 => Some(SmtTerm::Const(src / dst)),
3373                        _ => {
3374                            if let (Some(src_pty), Some(dst_pty)) = (src_ty, Some(dst_pt)) {
3375                                pointee_stride_from_types(self.tcx, src_pty, dst_pty)
3376                            } else {
3377                                None
3378                            }
3379                        }
3380                    };
3381                    if let Some(ratio_smt) = ratio_smt {
3382                        let ratio_int = self.term_for_smt_term(&ratio_smt)?;
3383                        len_term_int = Int::mul(self.ctx, &[len_term_int, ratio_int]);
3384                        len_term = SmtTerm::Mul(Box::new(len_term), Box::new(ratio_smt));
3385                    }
3386                }
3387            }
3388        }
3389
3390        // Compute the correct index for field projections from Range types.
3391        // Field [0] (start) is at offset 0; field [1] (end) is at offset len.
3392        let (index_term, index_val) = self.field_projection_index(place, &base_origin, &len_term);
3393
3394        Some(PointerBounds {
3395            index: index_val,
3396            len: len_term_int,
3397            index_term,
3398            len_term,
3399            origin_key: base_origin,
3400        })
3401    }
3402
3403    /// Walk the value chain for `place` to determine if it is a field projection
3404    /// from an `as_ptr_range`/`as_mut_ptr_range` result (or an inlined equivalent).
3405    /// Returns (index_term, index_val).  Field [0] (start) → offset 0;
3406    /// field [1] (end) → offset len.
3407    fn field_projection_index(
3408        &mut self,
3409        place: &PlaceKey,
3410        origin_key: &str,
3411        len_term: &SmtTerm,
3412    ) -> (SmtTerm, Int<'ctx>) {
3413        let default_index = SmtTerm::Const(0);
3414        let default_val = Int::from_u64(self.ctx, 0);
3415
3416        let mut cur_place = place.clone();
3417        loop {
3418            if !cur_place.fields.is_empty() {
3419                let field_idx = cur_place.fields.as_slice();
3420                let mut base = cur_place.clone();
3421                base.fields.clear();
3422                if let Some(local) = base.local() {
3423                    if let Some(definition) = self.forward.latest_value_definition_before(
3424                        local,
3425                        self.latest_cursor(),
3426                    ) {
3427                        let is_range = match &definition.value {
3428                            AbstractValue::CallResult(call) => {
3429                                let prim = PrimitiveCall::classify(&call.func);
3430                                prim == Some(PrimitiveCall::AsPtrRange)
3431                                    || prim == Some(PrimitiveCall::AsMutPtrRange)
3432                            }
3433                            AbstractValue::Aggregate(_, _) => true,
3434                            _ => false,
3435                        };
3436                        if is_range {
3437                            if field_idx == [0] {
3438                                return (SmtTerm::Const(0), Int::from_u64(self.ctx, 0));
3439                            }
3440                            if field_idx == [1] {
3441                                let len_key = format!("len({})", origin_key);
3442                                let len_val = self.symbolic_len_term(&len_key);
3443                                return (SmtTerm::Value(len_key), len_val);
3444                            }
3445                        }
3446                        // Follow Cast facts for inlined aggregates
3447                        for fact in &self.forward.facts {
3448                            if let StateFact::Cast { target, source, .. } = fact {
3449                                if *target == cur_place {
3450                                    if let AbstractValue::Place(p) = source {
3451                                        cur_place = p.clone();
3452                                        // Try again with the new place
3453                                        if cur_place.fields.as_slice() == [1] {
3454                                            let mut inner_base = cur_place.clone();
3455                                            inner_base.fields.clear();
3456                                            if let Some(inner_local) = inner_base.local() {
3457                                                if let Some(inner_def) = self.forward.latest_value_definition_before(inner_local, self.latest_cursor()) {
3458                                                    if let AbstractValue::CallResult(inner_call) = &inner_def.value {
3459                                                        let inner_prim = PrimitiveCall::classify(&inner_call.func);
3460                                                        if inner_prim == Some(PrimitiveCall::AsPtrRange) || inner_prim == Some(PrimitiveCall::AsMutPtrRange) {
3461                                                            let len_key = format!("len({})", origin_key);
3462                                                            let len_val = self.symbolic_len_term(&len_key);
3463                                                            return (SmtTerm::Value(len_key), len_val);
3464                                                        }
3465                                                    }
3466                                                }
3467                                            }
3468                                        }
3469                                        continue;
3470                                    }
3471                                }
3472                            }
3473                        }
3474                    }
3475                }
3476                return (default_index, default_val);
3477            }
3478
3479            if let Some(local) = cur_place.local() {
3480                if let Some(definition) =
3481                    self.forward.latest_value_definition_before(local, self.latest_cursor())
3482                {
3483                    match &definition.value {
3484                        AbstractValue::Place(p) => {
3485                            cur_place = p.clone();
3486                            continue;
3487                        }
3488                        _ => {}
3489                    }
3490                }
3491            }
3492            return (default_index, default_val);
3493        }
3494    }
3495
3496    /// Compute the offset index for a pointer-arithmetic call result.
3497    /// result_offset = base_offset (+ or -) count
3498    fn compute_pointer_arith_index(
3499        &mut self,
3500        base: &AbstractValue<'tcx>,
3501        base_origin: &str,
3502        len_term: &SmtTerm,
3503        call: &CallSummary<'tcx>,
3504        call_cursor: ValueCursor,
3505    ) -> (SmtTerm, Int<'ctx>) {
3506        // Get the base pointer's PlaceKey
3507        let base_place = match base {
3508            AbstractValue::Place(p) => p.clone(),
3509            _ => {
3510                // Fallback: use the count as the index (old behavior)
3511                if let Some(index) = call.effects.iter().find_map(|effect| match effect {
3512                    crate::verify::call_summary::CallEffect::ReturnPointerAdd { offset_arg, .. }
3513                    | crate::verify::call_summary::CallEffect::ReturnPointerSub { offset_arg, .. } => {
3514                        call.args.get(*offset_arg)
3515                    }
3516                    _ => None,
3517                }) {
3518                    let count_smt = SmtTerm::Value(value_label(index));
3519                    let count_val = self.term_for_value_at(index, call_cursor, &mut TraceSeen::new())
3520                        .unwrap_or(Int::from_u64(self.ctx, 0));
3521                    return (count_smt, count_val);
3522                }
3523                return (SmtTerm::Const(0), Int::from_u64(self.ctx, 0));
3524            }
3525        };
3526
3527        // Compute base offset using field_projection_index
3528        let (base_idx_smt, _base_idx_val) = self.field_projection_index(
3529            &base_place, base_origin, len_term,
3530        );
3531
3532        // Get the count term
3533        let count = call.effects.iter().find_map(|effect| match effect {
3534            crate::verify::call_summary::CallEffect::ReturnPointerAdd { offset_arg, .. }
3535            | crate::verify::call_summary::CallEffect::ReturnPointerSub { offset_arg, .. } => {
3536                call.args.get(*offset_arg)
3537            }
3538            _ => None,
3539        });
3540
3541        let count_smt = if let Some(c) = count {
3542            SmtTerm::Value(value_label(c))
3543        } else {
3544            SmtTerm::Const(0)
3545        };
3546
3547        let is_sub = call_has_pointer_sub_effect(call);
3548        let result_smt = if is_sub {
3549            SmtTerm::Sub(Box::new(base_idx_smt), Box::new(count_smt))
3550        } else {
3551            SmtTerm::Add(Box::new(base_idx_smt), Box::new(count_smt))
3552        };
3553
3554        let result_val = self.term_for_smt_term(&result_smt)
3555            .unwrap_or(Int::from_u64(self.ctx, 0));
3556
3557        (result_smt, result_val)
3558    }
3559
3560    /// Recover the allocation object and element offset for a pointer-like
3561    /// place. This is intentionally small: it handles base pointers returned by
3562    /// `as_ptr`/`as_mut_ptr` and offsets produced by pointer arithmetic
3563    /// summaries.
3564    fn pointer_object_offset_for_place(&self, place: &PlaceKey) -> Option<(PlaceKey, SmtTerm)> {
3565        self.pointer_object_offset_for_place_before(
3566            place,
3567            self.latest_cursor(),
3568            &mut TraceSeen::new(),
3569        )
3570    }
3571
3572    fn pointer_object_offset_for_place_before(
3573        &self,
3574        place: &PlaceKey,
3575        cursor: ValueCursor,
3576        seen: &mut TraceSeen,
3577    ) -> Option<(PlaceKey, SmtTerm)> {
3578        if let Some(object) = self.allocated_object_for_place(place) {
3579            return Some((object, SmtTerm::Const(0)));
3580        }
3581        let seen_key = (place.clone(), cursor);
3582        if !seen.insert(seen_key) {
3583            return None;
3584        }
3585        let local = place.local()?;
3586        let definition = self.forward.latest_value_definition_before(local, cursor)?;
3587        self.pointer_object_offset_for_value(&definition.value, definition.ordinal, seen)
3588    }
3589
3590    fn pointer_object_offset_for_value(
3591        &self,
3592        value: &AbstractValue<'tcx>,
3593        cursor: ValueCursor,
3594        seen: &mut TraceSeen,
3595    ) -> Option<(PlaceKey, SmtTerm)> {
3596        match value {
3597            AbstractValue::Place(place) => {
3598                self.pointer_object_offset_for_place_before(place, cursor, seen)
3599            }
3600            AbstractValue::Cast(inner, _) => {
3601                self.pointer_object_offset_for_value(inner, cursor, seen)
3602            }
3603            AbstractValue::CallResult(call) => {
3604                if call_has_pointer_add_effect(call) || is_pointer_add_call(&call.func) {
3605                    let (base_arg, offset_arg) = call
3606                        .effects
3607                        .iter()
3608                        .find_map(|effect| {
3609                            let crate::verify::call_summary::CallEffect::ReturnPointerAdd {
3610                                base_arg,
3611                                offset_arg,
3612                                ..
3613                            } = effect
3614                            else {
3615                                return None;
3616                            };
3617                            Some((*base_arg, *offset_arg))
3618                        })
3619                        .unwrap_or((0, 1));
3620                    let call_cursor = self.call_definition_cursor(call);
3621                    let (object, base_offset) = self.pointer_object_offset_for_value(
3622                        call.args.get(base_arg)?,
3623                        call_cursor,
3624                        seen,
3625                    )?;
3626                    let offset = smt_term_for_value(call.args.get(offset_arg)?)?;
3627                    return Some((
3628                        object,
3629                        SmtTerm::Add(Box::new(base_offset), Box::new(offset)),
3630                    ));
3631                }
3632                if call_has_pointer_sub_effect(call) || is_pointer_sub_call(&call.func) {
3633                    let (base_arg, offset_arg) = call
3634                        .effects
3635                        .iter()
3636                        .find_map(|effect| {
3637                            let crate::verify::call_summary::CallEffect::ReturnPointerSub {
3638                                base_arg,
3639                                offset_arg,
3640                                ..
3641                            } = effect
3642                            else {
3643                                return None;
3644                            };
3645                            Some((*base_arg, *offset_arg))
3646                        })
3647                        .unwrap_or((0, 1));
3648                    let call_cursor = self.call_definition_cursor(call);
3649                    let (object, base_offset) = self.pointer_object_offset_for_value(
3650                        call.args.get(base_arg)?,
3651                        call_cursor,
3652                        seen,
3653                    )?;
3654                    let offset = smt_term_for_value(call.args.get(offset_arg)?)?;
3655                    return Some((
3656                        object,
3657                        SmtTerm::Sub(Box::new(base_offset), Box::new(offset)),
3658                    ));
3659                }
3660                let destination = PlaceKey {
3661                    base: PlaceBaseKey::Local(call.destination.as_usize()),
3662                    fields: Vec::new(),
3663                };
3664                self.allocated_object_for_place(&destination)
3665                    .map(|object| (object, SmtTerm::Const(0)))
3666            }
3667            _ => None,
3668        }
3669    }
3670
3671    fn allocated_object_for_place(&self, place: &PlaceKey) -> Option<PlaceKey> {
3672        // 1) KnownAllocated facts (concrete allocations).
3673        if let Some(object) = self.forward.facts.iter().find_map(|fact| match fact {
3674            StateFact::KnownAllocated {
3675                place: allocated_place,
3676                object,
3677                ..
3678            } if allocated_place == place => Some(object.clone()),
3679            _ => None,
3680        }) {
3681            return Some(object);
3682        }
3683        // 2) PointsTo facts — when a pointer (or a projection of one)
3684        //    traces to a reference, the reference is the allocation object.
3685        let base = if place.fields.is_empty() {
3686            place.clone()
3687        } else {
3688            PlaceKey { base: place.base.clone(), fields: Vec::new() }
3689        };
3690        if let Some(source) = self.forward.facts.iter().find_map(|fact| match fact {
3691            StateFact::PointsTo { pointer, source } if *pointer == base => Some(source.clone()),
3692            _ => None,
3693        }) {
3694            return Some(source);
3695        }
3696        None
3697    }
3698
3699    /// Assert that a place is known to denote a non-zero address.
3700    pub(crate) fn assert_place_non_zero(
3701        &mut self,
3702        solver: &Solver<'ctx>,
3703        place: &PlaceKey,
3704        reason: &str,
3705    ) {
3706        if let Some(term) = self.term_for_place(place) {
3707            let zero = Int::from_u64(self.ctx, 0);
3708            solver.assert(&term._eq(&zero).not());
3709            self.assumptions.push(SmtPredicate::Custom(format!(
3710                "{} != 0 ({reason})",
3711                place_label(place)
3712            )));
3713        }
3714    }
3715
3716    /// Assert known alignment for a place when its MIR type provides one.
3717    fn assert_place_alignment(&mut self, solver: &Solver<'ctx>, place: &PlaceKey) {
3718        let Some(ty) = self.place_ty(place) else {
3719            return;
3720        };
3721        let Some(align_ty) = pointee_ty(ty).or(Some(ty)) else {
3722            return;
3723        };
3724        let Some(align) = self.guaranteed_alignment(align_ty) else {
3725            return;
3726        };
3727        if align > 0 && align <= 1 {
3728            return;
3729        }
3730        if let Some(term) = self.term_for_place(place) {
3731            let zero = Int::from_u64(self.ctx, 0);
3732            let align_term = if align == 0 {
3733                self.symbolic_align_term(&format!("{align_ty:?}"))
3734            } else {
3735                Int::from_u64(self.ctx, align)
3736            };
3737            solver.assert(&term.modulo(&align_term)._eq(&zero));
3738            self.assumptions.push(SmtPredicate::Custom(format!(
3739                "{} aligned for {align_ty:?} ({} bytes)",
3740                place_label(place),
3741                if align == 0 { "symbolic".to_string() } else { align.to_string() }
3742            )));
3743        }
3744    }
3745
3746    /// Assert an explicitly summarized alignment fact.
3747    fn assert_known_alignment(
3748        &mut self,
3749        solver: &Solver<'ctx>,
3750        place: &PlaceKey,
3751        align: u64,
3752        ty_name: &str,
3753        reason: &str,
3754    ) {
3755        if align == 0 {
3756            if let Some(term) = self.term_for_place(place) {
3757                let align_term = self.symbolic_align_term(ty_name);
3758                let zero = Int::from_u64(self.ctx, 0);
3759                solver.assert(&term.modulo(&align_term)._eq(&zero));
3760                self.assumptions.push(SmtPredicate::Custom(format!(
3761                    "{} aligned for {ty_name} (symbolic, {reason})",
3762                    place_label(place)
3763                )));
3764            }
3765            return;
3766        }
3767        if align <= 1 {
3768            return;
3769        }
3770        if let Some(term) = self.term_for_place(place) {
3771            let align_term = Int::from_u64(self.ctx, align);
3772            let k = Int::new_const(self.ctx, format!("{}_ka_k", place_label(place)));
3773            solver.assert(&term._eq(&Int::mul(self.ctx, &[k, align_term.clone()])));
3774            let zero = Int::from_u64(self.ctx, 0);
3775            solver.assert(&term.modulo(&align_term)._eq(&zero));
3776            self.assumptions.push(SmtPredicate::Custom(format!(
3777                "{} aligned for {ty_name} ({align} bytes, {reason})",
3778                place_label(place)
3779            )));
3780        }
3781    }
3782
3783    /// Assert that a place is equal to a concrete layout/numeric constant.
3784    fn assert_known_const(
3785        &mut self,
3786        solver: &Solver<'ctx>,
3787        place: &PlaceKey,
3788        value: u64,
3789        reason: &str,
3790    ) {
3791        if let Some(term) = self.term_for_place(place) {
3792            let value_term = Int::from_u64(self.ctx, value);
3793            solver.assert(&term._eq(&value_term));
3794            self.assumptions.push(SmtPredicate::Custom(format!(
3795                "{} == {value} ({reason})",
3796                place_label(place)
3797            )));
3798        }
3799    }
3800
3801    /// Assert equal slice lengths for two slice-like places that alias.
3802    fn assert_length_alias(&mut self, solver: &Solver<'ctx>, left: &PlaceKey, right: &PlaceKey) {
3803        if !self.is_len_carrying_place(left) || !self.is_len_carrying_place(right) {
3804            return;
3805        }
3806        let left_label = place_label(left);
3807        let right_label = place_label(right);
3808        let lhs = self.symbolic_len_term(&format!("len({left_label})"));
3809        let rhs = self.symbolic_len_term(&format!("len({right_label})"));
3810        solver.assert(&lhs._eq(&rhs));
3811        self.assumptions.push(SmtPredicate::Eq(
3812            SmtTerm::Value(format!("len({left_label})")),
3813            SmtTerm::Value(format!("len({right_label})")),
3814        ));
3815    }
3816
3817    fn is_len_carrying_place(&self, place: &PlaceKey) -> bool {
3818        self.place_ty(place).is_some_and(is_len_carrying_ty)
3819    }
3820
3821    /// Record call-effect definitions that the term builder understands.
3822    fn record_call_effect_assumptions(&mut self, call: &CallSummary<'tcx>) {
3823        let destination = PlaceKey {
3824            base: PlaceBaseKey::Local(call.destination.as_usize()),
3825            fields: Vec::new(),
3826        };
3827        let cursor = self.call_definition_cursor(call);
3828        for effect in &call.effects {
3829            match effect {
3830                crate::verify::call_summary::CallEffect::ReturnPointerAdd {
3831                    base_arg,
3832                    offset_arg,
3833                    stride,
3834                } => {
3835                    let base_term = call
3836                        .args
3837                        .get(*base_arg)
3838                        .and_then(|v| self.term_for_value_at(v, cursor, &mut TraceSeen::new()));
3839                    let offset_term = call
3840                        .args
3841                        .get(*offset_arg)
3842                        .and_then(|v| self.term_for_value_at(v, cursor, &mut TraceSeen::new()));
3843                    if let (Some(base), Some(offset)) = (base_term, offset_term) {
3844                        let stride = Int::from_u64(self.ctx, stride.unwrap_or(1));
3845                        let term =
3846                            Int::add(self.ctx, &[base, Int::mul(self.ctx, &[offset, stride])]);
3847                        self.place_terms.insert(destination.clone(), term);
3848                    }
3849                }
3850                crate::verify::call_summary::CallEffect::ReturnPointerSub {
3851                    base_arg,
3852                    offset_arg,
3853                    stride,
3854                } => {
3855                    let base_term = call
3856                        .args
3857                        .get(*base_arg)
3858                        .and_then(|v| self.term_for_value_at(v, cursor, &mut TraceSeen::new()));
3859                    let offset_term = call
3860                        .args
3861                        .get(*offset_arg)
3862                        .and_then(|v| self.term_for_value_at(v, cursor, &mut TraceSeen::new()));
3863                    if let (Some(base), Some(offset)) = (base_term, offset_term) {
3864                        let stride = Int::from_u64(self.ctx, stride.unwrap_or(1));
3865                        let term =
3866                            Int::sub(self.ctx, &[base, Int::mul(self.ctx, &[offset, stride])]);
3867                        self.place_terms.insert(destination.clone(), term);
3868                    }
3869                }
3870                crate::verify::call_summary::CallEffect::ReturnLengthOfArg { arg } => {
3871                    let raw_source = call
3872                        .args
3873                        .get(*arg)
3874                        .and_then(|value| {
3875                            self.origin_key_for_value_before(value, cursor, &mut TraceSeen::new())
3876                        })
3877                        .or_else(|| call.args.get(*arg).map(value_label))
3878                        .unwrap_or_else(|| format!("arg{arg}"));
3879                    // Strip Deref projections (e.g. _1.* → _1) so the
3880                    // length term matches the contract's len(self) key.
3881                    let source = raw_source.split('.').next().unwrap_or(&raw_source);
3882                    let len_key = format!("len({source})");
3883                    let len_term = self.symbolic_len_term(&len_key);
3884                    self.place_terms.insert(destination.clone(), len_term);
3885                    self.assumptions.push(SmtPredicate::Eq(
3886                        SmtTerm::Place(destination.clone()),
3887                        SmtTerm::Value(len_key),
3888                    ));
3889                }
3890                crate::verify::call_summary::CallEffect::ReturnPointerFromArg { arg }
3891                | crate::verify::call_summary::CallEffect::ReturnAliasArg { arg } => {
3892                    let source_value = call.args.get(*arg);
3893                    if let Some(term) = source_value.and_then(|value| {
3894                        self.term_for_value_at(value, cursor, &mut TraceSeen::new())
3895                    }) {
3896                        self.place_terms.insert(destination.clone(), term);
3897                    }
3898                    let source = source_value
3899                        .map(value_label)
3900                        .unwrap_or_else(|| format!("arg{arg}"));
3901                    self.assumptions.push(SmtPredicate::Eq(
3902                        SmtTerm::Place(destination.clone()),
3903                        SmtTerm::Value(source),
3904                    ));
3905                }
3906                crate::verify::call_summary::CallEffect::ReturnConst { .. } => {}
3907                crate::verify::call_summary::CallEffect::ReturnTupleFieldLength {
3908                    field,
3909                    from_arg,
3910                } => {
3911                    if *field == 0 {
3912                        let cursor = self.call_definition_cursor(call);
3913                        if let Some(mid_term) = call
3914                            .args
3915                            .get(*from_arg)
3916                            .and_then(|v| {
3917                                self.term_for_value_at(v, cursor, &mut TraceSeen::new())
3918                            })
3919                        {
3920                            let label = value_label(
3921                                call.args.get(*from_arg)
3922                                    .unwrap_or(&AbstractValue::Unknown(String::new())),
3923                            );
3924                            let dest_key = PlaceKey {
3925                                base: PlaceBaseKey::Local(call.destination.as_usize()),
3926                                fields: vec![0],
3927                            };
3928                            self.place_terms.insert(dest_key.clone(), mid_term);
3929                            self.assumptions.push(SmtPredicate::Eq(
3930                                SmtTerm::Place(dest_key),
3931                                SmtTerm::Value(label),
3932                            ));
3933                        }
3934                    }
3935                }
3936                crate::verify::call_summary::CallEffect::ReturnNonZero
3937                | crate::verify::call_summary::CallEffect::ReturnAligned { .. }
3938                | crate::verify::call_summary::CallEffect::ReadMemory { .. }
3939                | crate::verify::call_summary::CallEffect::WriteMemory { .. }
3940                | crate::verify::call_summary::CallEffect::ForgetArgFacts { .. } => {}
3941            }
3942        }
3943    }
3944
3945    /// Build an SMT term for a place.
3946    pub(crate) fn term_for_place(&mut self, place: &PlaceKey) -> Option<Int<'ctx>> {
3947        self.term_for_place_before(place, self.latest_cursor(), &mut TraceSeen::new())
3948    }
3949
3950    /// Build an SMT term for a place using only definitions before `cursor`.
3951    fn term_for_place_before(
3952        &mut self,
3953        place: &PlaceKey,
3954        cursor: ValueCursor,
3955        seen: &mut TraceSeen,
3956    ) -> Option<Int<'ctx>> {
3957        let seen_key = (place.clone(), cursor);
3958        if !seen.insert(seen_key) {
3959            return None;
3960        }
3961
3962        if !place.fields.is_empty() {
3963            if let Some(term) = self.projected_term_for_place(place, cursor, seen) {
3964                return Some(term);
3965            }
3966            if let Some(term) = self.place_terms.get(place) {
3967                return Some(term.clone());
3968            }
3969            let term = Int::new_const(self.ctx, place_name(place));
3970            self.place_terms.insert(place.clone(), term.clone());
3971            return Some(term);
3972        }
3973
3974        if let Some(local) = place.local()
3975            && let Some(definition) = self.forward.latest_value_definition_before(local, cursor)
3976        {
3977            if let Some(term) = self.term_for_value_at(&definition.value, definition.ordinal, seen)
3978            {
3979                return Some(term);
3980            }
3981        }
3982
3983        if let Some(value) = self.path_value_definition_before(place, cursor)
3984            && let Some(term) = self.term_for_value_at(&value, cursor, seen)
3985        {
3986            return Some(term);
3987        }
3988
3989        if let Some(term) = self.place_terms.get(place) {
3990            return Some(term.clone());
3991        }
3992
3993        // Use a shared per-local cache so every reference to the same MIR
3994        // local (e.g. parameter `mid`) produces the identical Z3 term.
3995        if place.fields.is_empty()
3996            && let Some(local) = place.local()
3997        {
3998            let id = local.as_usize();
3999            if let Some(term) = self.local_terms.get(&id) {
4000                let term = term.clone();
4001                self.place_terms.insert(place.clone(), term.clone());
4002                return Some(term);
4003            }
4004            let term = Int::new_const(self.ctx, place_name(place));
4005            self.local_terms.insert(id, term.clone());
4006            self.place_terms.insert(place.clone(), term.clone());
4007            return Some(term);
4008        }
4009
4010        let term = Int::new_const(self.ctx, place_name(place));
4011        self.place_terms.insert(place.clone(), term.clone());
4012        Some(term)
4013    }
4014
4015    /// Build terms for well-known aggregate projections.
4016    ///
4017    /// Checked integer arithmetic is represented as `(value, overflow)` in MIR.
4018    /// For numeric reasoning we can use field `0` as the mathematical result.
4019    /// Field `1` remains a fresh value, so overflow assertions do not become
4020    /// accidental constraints on the result itself.
4021    fn projected_term_for_place(
4022        &mut self,
4023        place: &PlaceKey,
4024        cursor: ValueCursor,
4025        seen: &mut TraceSeen,
4026    ) -> Option<Int<'ctx>> {
4027        let mut base = place.clone();
4028        base.fields.clear();
4029        let local = base.local()?;
4030        let definition = self.forward.latest_value_definition_before(local, cursor)?;
4031        let value = &definition.value;
4032
4033        // Handle field projections from as_ptr_range / as_mut_ptr_range results.
4034        // Field 0 is the start pointer (== arg0's address).
4035        // Field 1 is the end pointer (== arg0's address + len of arg0).
4036        if let AbstractValue::CallResult(call) = value {
4037            let prim = PrimitiveCall::classify(&call.func);
4038            if prim == Some(PrimitiveCall::AsPtrRange)
4039                || prim == Some(PrimitiveCall::AsMutPtrRange)
4040            {
4041                let arg_index = if place.fields.as_slice() == [0] {
4042                    0 // field 0 = start = arg0.as_ptr()
4043                } else if place.fields.as_slice() == [1] {
4044                    0 // field 1 = end = arg0.as_ptr() + len (use base arg0 term)
4045                } else {
4046                    return None;
4047                };
4048                let call_cursor = self.call_definition_cursor(call);
4049                return call
4050                    .args
4051                    .get(arg_index)
4052                    .and_then(|arg| self.term_for_value_at(arg, call_cursor, seen));
4053            }
4054        }
4055
4056        if place.fields.as_slice() != [0] {
4057            return None;
4058        }
4059        let AbstractValue::Binary(op, lhs, rhs) = value else {
4060            return None;
4061        };
4062        if !matches!(
4063            op,
4064            BinOp::AddWithOverflow | BinOp::SubWithOverflow | BinOp::MulWithOverflow
4065        ) {
4066            return None;
4067        }
4068        if matches!(op, BinOp::MulWithOverflow) {
4069            if let Some(len_origin) = ptr_metadata_origin(lhs, self) {
4070                let len_key = format!("len({len_origin})");
4071                let len_term = self.symbolic_len_term(&len_key);
4072                let rhs_term = self.term_for_value_at(rhs, definition.ordinal, seen)?;
4073                return Some(Int::mul(self.ctx, &[len_term, rhs_term]));
4074            }
4075        }
4076        let lhs = self.term_for_value_at(lhs, definition.ordinal, seen)?;
4077        let rhs = self.term_for_value_at(rhs, definition.ordinal, seen)?;
4078        self.term_for_binary(*op, &lhs, &rhs)
4079    }
4080
4081    /// Build an SMT term for an abstract value.
4082    fn term_for_value(
4083        &mut self,
4084        value: &AbstractValue<'tcx>,
4085        seen: &mut TraceSeen,
4086    ) -> Option<Int<'ctx>> {
4087        self.term_for_value_at(value, self.latest_cursor(), seen)
4088    }
4089
4090    /// Build an address expression for a call summarized as pointer arithmetic.
4091    fn term_for_pointer_arith_call(
4092        &mut self,
4093        call: &CallSummary<'tcx>,
4094        cursor: ValueCursor,
4095        seen: &mut TraceSeen,
4096    ) -> Option<Int<'ctx>> {
4097        let effect = call.effects.iter().find_map(|effect| match effect {
4098            crate::verify::call_summary::CallEffect::ReturnPointerAdd {
4099                base_arg,
4100                offset_arg,
4101                stride,
4102            } => Some((false, *base_arg, *offset_arg, *stride)),
4103            crate::verify::call_summary::CallEffect::ReturnPointerSub {
4104                base_arg,
4105                offset_arg,
4106                stride,
4107            } => Some((true, *base_arg, *offset_arg, *stride)),
4108            _ => None,
4109        });
4110
4111        let (is_sub, base_arg, offset_arg, stride) = effect.or_else(|| {
4112            if is_pointer_add_call(&call.func) {
4113                Some((false, 0, 1, self.call_destination_stride(call)))
4114            } else if is_pointer_sub_call(&call.func) {
4115                Some((true, 0, 1, self.call_destination_stride(call)))
4116            } else {
4117                None
4118            }
4119        })?;
4120
4121        let base = call.args.get(base_arg)?;
4122        let offset = call.args.get(offset_arg)?;
4123        let base = self.term_for_value_at(base, cursor, seen)?;
4124        let offset = self.term_for_value_at(offset, cursor, seen)?;
4125        let stride = Int::from_u64(self.ctx, stride.unwrap_or(1));
4126        let scaled_offset = Int::mul(self.ctx, &[offset, stride]);
4127
4128        if is_sub {
4129            Some(Int::sub(self.ctx, &[base, scaled_offset]))
4130        } else {
4131            Some(Int::add(self.ctx, &[base, scaled_offset]))
4132        }
4133    }
4134
4135    /// Build a stable `len(origin)` term for calls summarized as length reads.
4136    fn term_for_length_call(
4137        &mut self,
4138        call: &CallSummary<'tcx>,
4139        cursor: ValueCursor,
4140        seen: &mut TraceSeen,
4141    ) -> Option<Int<'ctx>> {
4142        let arg = call.effects.iter().find_map(|effect| {
4143            let crate::verify::call_summary::CallEffect::ReturnLengthOfArg { arg } = effect else {
4144                return None;
4145            };
4146            Some(*arg)
4147        })?;
4148        let source = call.args.get(arg)?;
4149        let source = self
4150            .origin_key_for_value_before(source, cursor, seen)
4151            .unwrap_or_else(|| value_label(source));
4152        let len_key = format!("len({source})");
4153        Some(self.symbolic_len_term(&len_key))
4154    }
4155
4156    /// Return a cached Z3 integer symbol for a const generic parameter,
4157    /// keyed by its plain name (e.g. `N`).  Accepts either a plain name or a
4158    /// rustc debug string such as `Ty(usize, N/#1)` / `Param(N)`, so that a
4159    /// const parameter referenced from a MIR operand, a contract, and an array
4160    /// length all resolve to the same `const_<name>` symbol.
4161    fn const_param_symbol(&mut self, name_or_debug: &str) -> Int<'ctx> {
4162        let plain = const_param_name_from_debug(name_or_debug)
4163            .unwrap_or_else(|| name_or_debug.to_string());
4164        let key = format!("const_{}", sanitize_smt_name(&plain));
4165        if let Some(term) = self.const_terms.get(&key) {
4166            return term.clone();
4167        }
4168        let term = Int::new_const(self.ctx, key.as_str());
4169        self.const_terms.insert(key, term.clone());
4170        term
4171    }
4172
4173    /// Build an SMT term for an abstract value at a program point.
4174    fn term_for_value_at(
4175        &mut self,
4176        value: &AbstractValue<'tcx>,
4177        cursor: ValueCursor,
4178        seen: &mut TraceSeen,
4179    ) -> Option<Int<'ctx>> {
4180        match value {
4181            AbstractValue::ConstInt(value) => Some(Int::from_u64(self.ctx, *value as u64)),
4182            AbstractValue::ConstParam(name) => Some(self.const_param_symbol(name)),
4183            AbstractValue::Const(text) => {
4184                const_int_from_debug(text).map(|value| Int::from_u64(self.ctx, value as u64))
4185                    .or_else(|| {
4186                        // A constant operand may be a const generic parameter
4187                        // (e.g. debug `Ty(usize, N/#1)`).  Normalize its symbol
4188                        // to `const_<name>` so it unifies with the same const
4189                        // parameter referenced elsewhere (contracts, array
4190                        // lengths) instead of minting `const_Ty_usize__N__1_`.
4191                        if let Some(param) = const_param_name_from_debug(text) {
4192                            return Some(self.const_param_symbol(&param));
4193                        }
4194                        let name = sanitize_smt_name(text);
4195                        if name.is_empty() { None }
4196                        else { Some(Int::new_const(self.ctx, format!("const_{name}"))) }
4197                    })
4198            }
4199            AbstractValue::Place(place) => self.term_for_place_before(place, cursor, seen),
4200            AbstractValue::Cast(inner, _) => self.term_for_value_at(inner, cursor, seen),
4201            AbstractValue::Ref(place) | AbstractValue::RawPtr(place) => Some(Int::new_const(
4202                self.ctx,
4203                format!("addr_{}", place_name(place)),
4204            )),
4205            AbstractValue::Binary(op, lhs, rhs) => {
4206                let lhs = self.term_for_value_at(lhs, cursor, seen)?;
4207                let rhs = self.term_for_value_at(rhs, cursor, seen)?;
4208                self.term_for_binary(*op, &lhs, &rhs)
4209            }
4210            AbstractValue::CallResult(call) => {
4211                if let Some(term) = self.term_for_pointer_arith_call(call, cursor, seen) {
4212                    return Some(term);
4213                }
4214                if let Some(term) = self.term_for_length_call(call, cursor, seen) {
4215                    return Some(term);
4216                }
4217                let place = PlaceKey {
4218                    base: PlaceBaseKey::Local(call.destination.as_usize()),
4219                    fields: Vec::new(),
4220                };
4221                Some(Int::new_const(self.ctx, place_name(&place)))
4222            }
4223            AbstractValue::Unknown(_)
4224            | AbstractValue::ThreadLocal(_)
4225            | AbstractValue::Repeat(_)
4226            | AbstractValue::Nullary(_)
4227            | AbstractValue::Discriminant(_)
4228            | AbstractValue::Aggregate(_, _) => None,
4229            AbstractValue::Unary(op, inner) => match op {
4230                UnOp::PtrMetadata => {
4231                    let source = self
4232                        .origin_key_for_value_before(inner, cursor, seen)
4233                        .unwrap_or_else(|| value_label(inner));
4234                    let len_key = format!("len({source})");
4235                    Some(self.symbolic_len_term(&len_key))
4236                }
4237                _ => None,
4238            },
4239            #[cfg(not(rapx_rustc_ge_196))]
4240            AbstractValue::ShallowInitBox(_, _) => None,
4241        }
4242    }
4243
4244    /// Build an SMT integer term from a property-independent diagnostic term.
4245    fn term_for_smt_term(&mut self, term: &SmtTerm) -> Option<Int<'ctx>> {
4246        match term {
4247            SmtTerm::Place(place) => self.term_for_place(place),
4248            SmtTerm::Value(name) => {
4249                if name.starts_with("len(") {
4250                    Some(self.symbolic_len_term(name))
4251                } else {
4252                    Some(Int::new_const(self.ctx, sanitize_smt_name(name)))
4253                }
4254            }
4255            SmtTerm::Const(value) => Some(Int::from_u64(self.ctx, *value)),
4256            SmtTerm::ConstParam(text) => {
4257                if sanitize_smt_name(text).is_empty() {
4258                    return None;
4259                }
4260                Some(self.const_param_symbol(text))
4261            }
4262            SmtTerm::Add(lhs, rhs) => {
4263                let lhs = self.term_for_smt_term(lhs)?;
4264                let rhs = self.term_for_smt_term(rhs)?;
4265                Some(Int::add(self.ctx, &[lhs, rhs]))
4266            }
4267            SmtTerm::Sub(lhs, rhs) => {
4268                let lhs = self.term_for_smt_term(lhs)?;
4269                let rhs = self.term_for_smt_term(rhs)?;
4270                Some(Int::sub(self.ctx, &[lhs, rhs]))
4271            }
4272            SmtTerm::Mul(lhs, rhs) => {
4273                let lhs = self.term_for_smt_term(lhs)?;
4274                let rhs = self.term_for_smt_term(rhs)?;
4275                Some(Int::mul(self.ctx, &[lhs, rhs]))
4276            }
4277            SmtTerm::Div(lhs, rhs) => {
4278                let lhs = self.term_for_smt_term(lhs)?;
4279                let rhs = self.term_for_smt_term(rhs)?;
4280                Some(lhs.div(&rhs))
4281            }
4282            SmtTerm::Rem(lhs, rhs) => {
4283                let lhs = self.term_for_smt_term(lhs)?;
4284                let rhs = self.term_for_smt_term(rhs)?;
4285                Some(lhs.modulo(&rhs))
4286            }
4287        }
4288    }
4289
4290    /// Build a boolean term for a conjunction of shared predicates.
4291    fn bool_for_predicates(&mut self, predicates: &[SmtPredicate]) -> Option<Bool<'ctx>> {
4292        match predicates {
4293            [] => None,
4294            [predicate] => self.bool_for_predicate(predicate),
4295            predicates => {
4296                let bools = predicates
4297                    .iter()
4298                    .map(|predicate| self.bool_for_predicate(predicate))
4299                    .collect::<Option<Vec<_>>>()?;
4300                let refs = bools.iter().collect::<Vec<_>>();
4301                Some(Bool::and(self.ctx, &refs))
4302            }
4303        }
4304    }
4305
4306    /// Build a boolean term from a shared diagnostic/query predicate.
4307    fn bool_for_predicate(&mut self, predicate: &SmtPredicate) -> Option<Bool<'ctx>> {
4308        match predicate {
4309            SmtPredicate::Eq(lhs, rhs) => {
4310                let lhs = self.term_for_smt_term(lhs)?;
4311                let rhs = self.term_for_smt_term(rhs)?;
4312                Some(lhs._eq(&rhs))
4313            }
4314            SmtPredicate::Ne(lhs, rhs) => {
4315                let lhs = self.term_for_smt_term(lhs)?;
4316                let rhs = self.term_for_smt_term(rhs)?;
4317                Some(lhs._eq(&rhs).not())
4318            }
4319            SmtPredicate::Le(lhs, rhs) => {
4320                let lhs = self.term_for_smt_term(lhs)?;
4321                let rhs = self.term_for_smt_term(rhs)?;
4322                Some(lhs.le(&rhs))
4323            }
4324            SmtPredicate::Lt(lhs, rhs) => {
4325                let lhs = self.term_for_smt_term(lhs)?;
4326                let rhs = self.term_for_smt_term(rhs)?;
4327                Some(lhs.lt(&rhs))
4328            }
4329            SmtPredicate::Ge(lhs, rhs) => {
4330                let lhs = self.term_for_smt_term(lhs)?;
4331                let rhs = self.term_for_smt_term(rhs)?;
4332                Some(lhs.ge(&rhs))
4333            }
4334            SmtPredicate::Gt(lhs, rhs) => {
4335                let lhs = self.term_for_smt_term(lhs)?;
4336                let rhs = self.term_for_smt_term(rhs)?;
4337                Some(lhs.gt(&rhs))
4338            }
4339            SmtPredicate::And(predicates) => self.bool_for_predicates(predicates),
4340            SmtPredicate::Divisible { term, modulus } => {
4341                let term = self.term_for_smt_term(term)?;
4342                let modulus = Int::from_u64(self.ctx, *modulus);
4343                let zero = Int::from_u64(self.ctx, 0);
4344                Some(term.modulo(&modulus)._eq(&zero))
4345            }
4346            SmtPredicate::InBounds {
4347                index,
4348                access_count,
4349                len,
4350            } => {
4351                let index = self.term_for_smt_term(index)?;
4352                let access_count = self.term_for_smt_term(access_count)?;
4353                let len = self.term_for_smt_term(len)?;
4354                let zero = Int::from_u64(self.ctx, 0);
4355                let covered_end = Int::add(self.ctx, &[index.clone(), access_count]);
4356                Some(Bool::and(
4357                    self.ctx,
4358                    &[&index.ge(&zero), &covered_end.le(&len)],
4359                ))
4360            }
4361            SmtPredicate::NonOverlapping {
4362                left,
4363                right,
4364                left_count,
4365                right_count,
4366                elem_size,
4367            } => {
4368                let left = self.term_for_smt_term(left)?;
4369                let right = self.term_for_smt_term(right)?;
4370                let left_count = self.term_for_smt_term(left_count)?;
4371                let right_count = self.term_for_smt_term(right_count)?;
4372                let elem_size = Int::from_u64(self.ctx, *elem_size);
4373                let left_end = Int::add(
4374                    self.ctx,
4375                    &[
4376                        left.clone(),
4377                        Int::mul(self.ctx, &[left_count, elem_size.clone()]),
4378                    ],
4379                );
4380                let right_end = Int::add(
4381                    self.ctx,
4382                    &[right.clone(), Int::mul(self.ctx, &[right_count, elem_size])],
4383                );
4384                Some(Bool::or(
4385                    self.ctx,
4386                    &[&left_end.le(&right), &right_end.le(&left)],
4387                ))
4388            }
4389            SmtPredicate::Not(predicate) => Some(self.bool_for_predicate(predicate)?.not()),
4390            SmtPredicate::Custom(_) => None,
4391        }
4392    }
4393
4394    /// Assert Rust unsigned integer lower bounds for terms that appear in a
4395    /// numeric obligation.
4396    fn assert_unsigned_bounds_for_predicates(
4397        &mut self,
4398        solver: &Solver<'ctx>,
4399        predicates: &[SmtPredicate],
4400    ) {
4401        let mut seen = HashSet::new();
4402        for predicate in predicates {
4403            self.assert_unsigned_bounds_for_predicate(solver, predicate, &mut seen);
4404        }
4405    }
4406
4407    fn assert_unsigned_bounds_for_predicate(
4408        &mut self,
4409        solver: &Solver<'ctx>,
4410        predicate: &SmtPredicate,
4411        seen: &mut HashSet<PlaceKey>,
4412    ) {
4413        match predicate {
4414            SmtPredicate::Eq(lhs, rhs)
4415            | SmtPredicate::Ne(lhs, rhs)
4416            | SmtPredicate::Le(lhs, rhs)
4417            | SmtPredicate::Lt(lhs, rhs)
4418            | SmtPredicate::Ge(lhs, rhs)
4419            | SmtPredicate::Gt(lhs, rhs) => {
4420                self.assert_unsigned_bounds_for_term(solver, lhs, seen);
4421                self.assert_unsigned_bounds_for_term(solver, rhs, seen);
4422            }
4423            SmtPredicate::And(predicates) => {
4424                for predicate in predicates {
4425                    self.assert_unsigned_bounds_for_predicate(solver, predicate, seen);
4426                }
4427            }
4428            SmtPredicate::Divisible { term, .. } => {
4429                self.assert_unsigned_bounds_for_term(solver, term, seen);
4430            }
4431            SmtPredicate::InBounds {
4432                index,
4433                access_count,
4434                len,
4435            } => {
4436                self.assert_unsigned_bounds_for_term(solver, index, seen);
4437                self.assert_unsigned_bounds_for_term(solver, access_count, seen);
4438                self.assert_unsigned_bounds_for_term(solver, len, seen);
4439            }
4440            SmtPredicate::NonOverlapping {
4441                left_count,
4442                right_count,
4443                ..
4444            } => {
4445                self.assert_unsigned_bounds_for_term(solver, left_count, seen);
4446                self.assert_unsigned_bounds_for_term(solver, right_count, seen);
4447            }
4448            SmtPredicate::Not(predicate) => {
4449                self.assert_unsigned_bounds_for_predicate(solver, predicate, seen);
4450            }
4451            SmtPredicate::Custom(_) => {}
4452        }
4453    }
4454
4455    fn assert_unsigned_bounds_for_term(
4456        &mut self,
4457        solver: &Solver<'ctx>,
4458        term: &SmtTerm,
4459        seen: &mut HashSet<PlaceKey>,
4460    ) {
4461        match term {
4462            SmtTerm::Place(place) => {
4463                if !seen.insert(place.clone()) {
4464                    return;
4465                }
4466                let Some(ty) = self.place_ty(place) else {
4467                    return;
4468                };
4469                if !is_unsigned_integral_ty(ty) {
4470                    return;
4471                }
4472                let Some(int_term) = self.term_for_place(place) else {
4473                    return;
4474                };
4475                let zero = Int::from_u64(self.ctx, 0);
4476                solver.assert(&int_term.ge(&zero));
4477                self.assumptions.push(SmtPredicate::Ge(
4478                    SmtTerm::Place(place.clone()),
4479                    SmtTerm::Const(0),
4480                ));
4481            }
4482            SmtTerm::Add(lhs, rhs)
4483            | SmtTerm::Sub(lhs, rhs)
4484            | SmtTerm::Mul(lhs, rhs)
4485            | SmtTerm::Div(lhs, rhs)
4486            | SmtTerm::Rem(lhs, rhs) => {
4487                self.assert_unsigned_bounds_for_term(solver, lhs, seen);
4488                self.assert_unsigned_bounds_for_term(solver, rhs, seen);
4489            }
4490            SmtTerm::Value(_) | SmtTerm::Const(_) | SmtTerm::ConstParam(_) => {}
4491        }
4492    }
4493
4494    /// Lower a binary MIR operation to an integer term.
4495    fn term_for_binary(&self, op: BinOp, lhs: &Int<'ctx>, rhs: &Int<'ctx>) -> Option<Int<'ctx>> {
4496        let one = Int::from_u64(self.ctx, 1);
4497        let zero = Int::from_u64(self.ctx, 0);
4498        Some(match op {
4499            BinOp::Add | BinOp::AddWithOverflow => Int::add(self.ctx, &[lhs.clone(), rhs.clone()]),
4500            BinOp::Sub | BinOp::SubWithOverflow => Int::sub(self.ctx, &[lhs.clone(), rhs.clone()]),
4501            BinOp::Mul | BinOp::MulWithOverflow => Int::mul(self.ctx, &[lhs.clone(), rhs.clone()]),
4502            BinOp::Div => lhs.div(rhs),
4503            BinOp::Rem => lhs.modulo(rhs),
4504            BinOp::Eq => lhs._eq(rhs).ite(&one, &zero),
4505            BinOp::Ne => lhs._eq(rhs).not().ite(&one, &zero),
4506            BinOp::Lt => lhs.lt(rhs).ite(&one, &zero),
4507            BinOp::Le => lhs.le(rhs).ite(&one, &zero),
4508            BinOp::Gt => lhs.gt(rhs).ite(&one, &zero),
4509            BinOp::Ge => lhs.ge(rhs).ite(&one, &zero),
4510            _ => return None,
4511        })
4512    }
4513
4514    /// Return the byte stride for a typed pointer-add call destination.
4515    fn call_destination_stride(&self, call: &CallSummary<'tcx>) -> Option<u64> {
4516        let place = PlaceKey {
4517            base: PlaceBaseKey::Local(call.destination.as_usize()),
4518            fields: Vec::new(),
4519        };
4520        let destination_ty = self.place_ty(&place)?;
4521        let pointee = pointee_ty(destination_ty)?;
4522        self.type_layout(pointee).map(|(_, size)| size)
4523    }
4524
4525    /// Return the MIR type for a simple place key.
4526    fn place_ty(&self, place: &PlaceKey) -> Option<Ty<'tcx>> {
4527        if !place.fields.is_empty() {
4528            return self.forward.facts.iter().find_map(|fact| {
4529                let StateFact::Cast { target, ty, .. } = fact else {
4530                    return None;
4531                };
4532                if target == place { Some(*ty) } else { None }
4533            });
4534        }
4535        let local = match place.base {
4536            PlaceBaseKey::Return => Local::from_usize(0),
4537            PlaceBaseKey::Local(local) => Local::from_usize(local),
4538            PlaceBaseKey::Arg(_) => return None,
4539        };
4540        Some(self.tcx.optimized_mir(self.checkpoint.caller).local_decls[local].ty)
4541    }
4542
4543    fn type_layout(&self, ty: Ty<'tcx>) -> Option<(u64, u64)> {
4544        safe_type_layout(self.tcx, self.checkpoint.caller, ty)
4545    }
4546
4547    /// Return the alignment guaranteed by a concrete or generic type.
4548    fn guaranteed_alignment(&self, ty: Ty<'tcx>) -> Option<u64> {
4549        if let Some((align, _)) = self.type_layout(ty).filter(|(align, _)| *align > 0) {
4550            return Some(align);
4551        }
4552        if let Some(alignments) = self.generic_candidate_alignments(ty) {
4553            return alignments.into_iter().min();
4554        }
4555        if matches!(ty.kind(), TyKind::Param(_) | TyKind::Array(..) | TyKind::Ref(..) | TyKind::RawPtr(..)) {
4556            return Some(0);
4557        }
4558        None
4559    }
4560
4561    fn generic_candidate_alignments(&self, ty: Ty<'tcx>) -> Option<Vec<u64>> {
4562        let candidates = GenericTypeCandidates::for_def(self.tcx, self.checkpoint.caller);
4563        let alignments = candidates
4564            .candidates_for_ty(ty)?
4565            .iter()
4566            .filter_map(|candidate| self.type_layout(*candidate).map(|(align, _)| align))
4567            .filter(|align| *align > 0)
4568            .collect::<Vec<_>>();
4569        if alignments.is_empty() {
4570            None
4571        } else {
4572            Some(alignments)
4573        }
4574    }
4575
4576    /// Return the pointer-add call/effect that produced a place after copies/casts.
4577    fn pointer_add_call_for_place(&self, place: &PlaceKey) -> Option<CallSummary<'tcx>> {
4578        let value = self.resolved_value_for_place_before(
4579            place,
4580            self.latest_cursor(),
4581            &mut TraceSeen::new(),
4582        )?;
4583        match value {
4584            AbstractValue::CallResult(call)
4585                if call_has_pointer_add_effect(&call) || call_has_pointer_sub_effect(&call) =>
4586            {
4587                Some(call)
4588            }
4589            _ => None,
4590        }
4591    }
4592
4593    /// Resolve copy/cast chains for a MIR place into the value at their source.
4594    fn resolved_value_for_place(
4595        &self,
4596        place: &PlaceKey,
4597        seen: &mut TraceSeen,
4598    ) -> Option<AbstractValue<'tcx>> {
4599        self.resolved_value_for_place_before(place, self.latest_cursor(), seen)
4600    }
4601
4602    fn resolved_value_for_place_before(
4603        &self,
4604        place: &PlaceKey,
4605        cursor: ValueCursor,
4606        seen: &mut TraceSeen,
4607    ) -> Option<AbstractValue<'tcx>> {
4608        let seen_key = (place.clone(), cursor);
4609        if !seen.insert(seen_key) {
4610            return None;
4611        }
4612        if !place.fields.is_empty() {
4613            let mut base = place.clone();
4614            base.fields.clear();
4615            let local = base.local()?;
4616            if let Some(definition) = self.forward.latest_value_definition_before(local, cursor) {
4617                // Handle as_ptr_range / as_mut_ptr_range (call result)
4618                if let AbstractValue::CallResult(call) = &definition.value {
4619                    let prim = PrimitiveCall::classify(&call.func);
4620                    if prim == Some(PrimitiveCall::AsPtrRange)
4621                        || prim == Some(PrimitiveCall::AsMutPtrRange)
4622                    {
4623                        if let Some(source_arg) = call.effects.iter().find_map(|effect| {
4624                            match effect {
4625                                crate::verify::call_summary::CallEffect::ReturnAliasArg { arg }
4626                                | crate::verify::call_summary::CallEffect::ReturnPointerFromArg { arg } => Some(*arg),
4627                                _ => None,
4628                            }
4629                        }) {
4630                            let call_cursor = self.call_definition_cursor(call);
4631                            if let Some(arg_value) = call.args.get(source_arg) {
4632                                return self.resolved_value_before(arg_value, call_cursor, seen);
4633                            }
4634                        }
4635                    }
4636                }
4637                // Handle inlined as_ptr_range (aggregate of as_ptr + add results)
4638                // Find Cast facts that connect field places to their source values
4639                if let AbstractValue::Aggregate(_, _) = &definition.value {
4640                    for fact in &self.forward.facts {
4641                        if let StateFact::Cast { target, source, .. } = fact {
4642                            if *target == *place {
4643                                return self.resolved_value_before(source, definition.ordinal, seen);
4644                            }
4645                        }
4646                    }
4647                }
4648            }
4649            return Some(AbstractValue::Place(place.clone()));
4650        }
4651        let local = place.local()?;
4652        if let Some(definition) = self.forward.latest_value_definition_before(local, cursor) {
4653            return self.resolved_value_before(&definition.value, definition.ordinal, seen);
4654        }
4655        if let Some(value) = self.path_value_definition_before(place, cursor) {
4656            return self.resolved_value_before(&value, cursor, seen);
4657        }
4658        None
4659    }
4660
4661    /// Resolve copy/cast chains for an abstract value.
4662    fn resolved_value(
4663        &self,
4664        value: &AbstractValue<'tcx>,
4665        seen: &mut TraceSeen,
4666    ) -> Option<AbstractValue<'tcx>> {
4667        self.resolved_value_before(value, self.latest_cursor(), seen)
4668    }
4669
4670    fn resolved_value_before(
4671        &self,
4672        value: &AbstractValue<'tcx>,
4673        cursor: ValueCursor,
4674        seen: &mut TraceSeen,
4675    ) -> Option<AbstractValue<'tcx>> {
4676        match value {
4677            AbstractValue::Place(place) => {
4678                self.resolved_value_for_place_before(place, cursor, seen)
4679            }
4680            AbstractValue::Cast(inner, _) => self.resolved_value_before(inner, cursor, seen),
4681            _ => Some(value.clone()),
4682        }
4683    }
4684
4685    /// Recover a local definition directly from the expanded MIR path.
4686    ///
4687    /// Backward relevance keeps the proof slice intentionally small. When a
4688    /// pure call-argument temporary is not retained in the forward visit, SMT
4689    /// term construction can still recover its value by replaying assignments
4690    /// along the already-enumerated path up to the current cursor.
4691    fn path_value_definition_before(
4692        &self,
4693        place: &PlaceKey,
4694        cursor: ValueCursor,
4695    ) -> Option<AbstractValue<'tcx>> {
4696        if !place.fields.is_empty() {
4697            return None;
4698        }
4699        let local = place.local()?;
4700        let cutoff = self.path_cursor_cutoff(cursor);
4701        let body = self.tcx.optimized_mir(self.forward.checkpoint.caller);
4702        let mut latest = None;
4703
4704        for step in &self.forward.path.steps {
4705            let PathStep::Block(block) = step else {
4706                continue;
4707            };
4708            let is_cutoff_block = *block == cutoff.block;
4709            let block_data = &body.basic_blocks[*block];
4710
4711            for (statement_index, statement) in block_data.statements.iter().enumerate() {
4712                if is_cutoff_block
4713                    && let Some(cutoff_statement) = cutoff.statement_index
4714                    && statement_index >= cutoff_statement
4715                {
4716                    return latest;
4717                }
4718
4719                let rustc_middle::mir::StatementKind::Assign(assign) = &statement.kind else {
4720                    continue;
4721                };
4722                let (target, rvalue) = &**assign;
4723                if target.local == local {
4724                    latest = abstract_value_from_rvalue(rvalue);
4725                }
4726            }
4727
4728            if is_cutoff_block {
4729                return latest;
4730            }
4731        }
4732
4733        latest
4734    }
4735
4736    fn path_cursor_cutoff(&self, cursor: ValueCursor) -> PathCursorCutoff {
4737        if let Some(definition) = self.forward.value_definitions.get(cursor) {
4738            return PathCursorCutoff {
4739                block: definition.block,
4740                statement_index: definition.statement_index,
4741            };
4742        }
4743
4744        PathCursorCutoff {
4745            block: self.forward.checkpoint.block,
4746            statement_index: None,
4747        }
4748    }
4749
4750    /// Return a stable origin key for matching `as_ptr(source)` and `len(source)`.
4751    fn origin_key_for_value(
4752        &self,
4753        value: &AbstractValue<'tcx>,
4754        seen: &mut TraceSeen,
4755    ) -> Option<String> {
4756        self.origin_key_for_value_before(value, self.latest_cursor(), seen)
4757    }
4758
4759    fn origin_key_for_value_before(
4760        &self,
4761        value: &AbstractValue<'tcx>,
4762        cursor: ValueCursor,
4763        seen: &mut TraceSeen,
4764    ) -> Option<String> {
4765        let resolved = self
4766            .resolved_value_before(value, cursor, seen)
4767            .unwrap_or_else(|| value.clone());
4768        match resolved {
4769            AbstractValue::Ref(place) | AbstractValue::RawPtr(place) => Some(place_label(&place)),
4770            AbstractValue::Place(place) => self
4771                .source_from_points_to(&place)
4772                .map(|source| place_label(&source))
4773                .or_else(|| {
4774                    // Follow PointsTo(pointer→source) recursively to find
4775                    // the ultimate origin (e.g. _5 → _6 → _1).
4776                    self.forward.facts.iter().find_map(|fact| match fact {
4777                        StateFact::PointsTo { pointer, source } if *pointer == place => {
4778                            self.origin_key_for_value_before(
4779                                &AbstractValue::Place(source.clone()),
4780                                cursor,
4781                                seen,
4782                            )
4783                        }
4784                        _ => None,
4785                    })
4786                })
4787                .or_else(|| Some(place_label(&place))),
4788            AbstractValue::Cast(inner, _) => self.origin_key_for_value_before(&inner, cursor, seen),
4789            AbstractValue::CallResult(call) if is_as_ptr_call(&call.func) => {
4790                let source_arg = call.effects.iter().find_map(|effect| match effect {
4791                    crate::verify::call_summary::CallEffect::ReturnPointerFromArg { arg }
4792                    | crate::verify::call_summary::CallEffect::ReturnAliasArg { arg } => Some(*arg),
4793                    _ => None,
4794                })?;
4795                let call_cursor = self.call_definition_cursor(&call);
4796                self.origin_key_for_value_before(call.args.get(source_arg)?, call_cursor, seen)
4797            }
4798            AbstractValue::CallResult(call) => {
4799                if let Some(source_arg) = call.effects.iter().find_map(|effect| match effect {
4800                    crate::verify::call_summary::CallEffect::ReturnPointerFromArg { arg }
4801                    | crate::verify::call_summary::CallEffect::ReturnAliasArg { arg } => Some(*arg),
4802                    crate::verify::call_summary::CallEffect::ReturnPointerAdd { base_arg, .. }
4803                    | crate::verify::call_summary::CallEffect::ReturnPointerSub { base_arg, .. } => Some(*base_arg),
4804                    _ => None,
4805                }) {
4806                    let call_cursor = self.call_definition_cursor(&call);
4807                    return self.origin_key_for_value_before(
4808                        call.args.get(source_arg)?,
4809                        call_cursor,
4810                        seen,
4811                    );
4812                }
4813                let destination = PlaceKey {
4814                    base: PlaceBaseKey::Local(call.destination.as_usize()),
4815                    fields: Vec::new(),
4816                };
4817                Some(place_label(&destination))
4818            }
4819            _ => Some(value_label(&resolved)),
4820        }
4821    }
4822
4823    /// Recover a length value from a path guard that mentions `index`.
4824    fn guarded_len_for_index(
4825        &self,
4826        base_origin: &str,
4827        index: &AbstractValue<'tcx>,
4828    ) -> Option<AbstractValue<'tcx>> {
4829        let index = self
4830            .resolved_value(index, &mut HashSet::new())
4831            .unwrap_or_else(|| index.clone());
4832        for fact in &self.forward.facts {
4833            let StateFact::BranchEq { value, equals: 1, .. } = fact else {
4834                continue;
4835            };
4836            let predicate = self
4837                .resolved_value(value, &mut HashSet::new())
4838                .unwrap_or_else(|| value.clone());
4839            let AbstractValue::Binary(op, lhs, rhs) = predicate else {
4840                continue;
4841            };
4842            match op {
4843                BinOp::Lt | BinOp::Le => {
4844                    if self.value_mentions(&lhs, &index)
4845                        && self.len_matches_origin(&rhs, base_origin)
4846                    {
4847                        return Some(*rhs);
4848                    }
4849                }
4850                BinOp::Gt | BinOp::Ge => {
4851                    if self.value_mentions(&rhs, &index)
4852                        && self.len_matches_origin(&lhs, base_origin)
4853                    {
4854                        return Some(*lhs);
4855                    }
4856                }
4857                _ => {}
4858            }
4859        }
4860        None
4861    }
4862
4863    /// Return true when `haystack` contains the same resolved value as `needle`.
4864    fn value_mentions(&self, haystack: &AbstractValue<'tcx>, needle: &AbstractValue<'tcx>) -> bool {
4865        self.value_mentions_inner(haystack, needle, &mut HashSet::new())
4866    }
4867
4868    fn value_mentions_inner(
4869        &self,
4870        haystack: &AbstractValue<'tcx>,
4871        needle: &AbstractValue<'tcx>,
4872        seen: &mut HashSet<(String, String)>,
4873    ) -> bool {
4874        let haystack = self
4875            .resolved_value(haystack, &mut HashSet::new())
4876            .unwrap_or_else(|| haystack.clone());
4877        let needle = self
4878            .resolved_value(needle, &mut HashSet::new())
4879            .unwrap_or_else(|| needle.clone());
4880        let haystack_label = value_label(&haystack);
4881        let needle_label = value_label(&needle);
4882        if haystack_label == needle_label {
4883            return true;
4884        }
4885        if !seen.insert((haystack_label, needle_label)) {
4886            return false;
4887        }
4888        match haystack {
4889            AbstractValue::Cast(inner, _) | AbstractValue::Unary(_, inner) => {
4890                self.value_mentions_inner(&inner, &needle, seen)
4891            }
4892            AbstractValue::Binary(_, lhs, rhs) => {
4893                self.value_mentions_inner(&lhs, &needle, seen)
4894                    || self.value_mentions_inner(&rhs, &needle, seen)
4895            }
4896            _ => false,
4897        }
4898    }
4899
4900    /// Return true when a length-like value is the metadata/len of `base_origin`.
4901    fn len_matches_origin(&self, len: &AbstractValue<'tcx>, base_origin: &str) -> bool {
4902        self.len_matches_origin_inner(len, base_origin, &mut HashSet::new())
4903    }
4904
4905    fn len_matches_origin_inner(
4906        &self,
4907        len: &AbstractValue<'tcx>,
4908        base_origin: &str,
4909        seen: &mut HashSet<String>,
4910    ) -> bool {
4911        let label = value_label(len);
4912        if !seen.insert(label) {
4913            return false;
4914        }
4915        let resolved = self
4916            .resolved_value(len, &mut HashSet::new())
4917            .unwrap_or_else(|| len.clone());
4918        match resolved {
4919            AbstractValue::Place(place) => value_for_place(self.forward, &place)
4920                .is_some_and(|value| self.len_matches_origin_inner(value, base_origin, seen)),
4921            AbstractValue::Unary(UnOp::PtrMetadata, inner) => self
4922                .origin_key_for_value(&inner, &mut HashSet::new())
4923                .is_some_and(|origin| origin == base_origin),
4924            AbstractValue::CallResult(call) => call.effects.iter().any(|effect| {
4925                let crate::verify::call_summary::CallEffect::ReturnLengthOfArg { arg } = effect
4926                else {
4927                    return false;
4928                };
4929                call.args
4930                    .get(*arg)
4931                    .and_then(|value| self.origin_key_for_value(value, &mut HashSet::new()))
4932                    .is_some_and(|origin| origin == base_origin)
4933            }),
4934            _ => false,
4935        }
4936    }
4937
4938    /// Return the source place recorded by a `PointsTo(pointer, source)` fact.
4939    fn source_from_points_to(&self, pointer: &PlaceKey) -> Option<PlaceKey> {
4940        self.forward.facts.iter().find_map(|fact| match fact {
4941            StateFact::PointsTo {
4942                pointer: fact_pointer,
4943                source,
4944            } if fact_pointer == pointer => Some(source.clone()),
4945            _ => None,
4946        })
4947        .or_else(|| {
4948            if pointer.fields.is_empty() {
4949                return None;
4950            }
4951            let mut base = pointer.clone();
4952            base.fields.clear();
4953            self.forward.facts.iter().find_map(|fact| match fact {
4954                StateFact::PointsTo {
4955                    pointer: fact_pointer,
4956                    source,
4957                } if *fact_pointer == base => Some(source.clone()),
4958                _ => None,
4959            })
4960        })
4961    }
4962
4963    /// Candidate address/value terms for an `Init` target.
4964    ///
4965    /// Pointer targets use their value term.  By-value `MaybeUninit<T>` targets
4966    /// may be moved into a temporary before `assume_init`; in that case the
4967    /// relevant initialized storage is the address of the original place.
4968    fn init_target_terms(&mut self, place: &PlaceKey) -> Vec<Int<'ctx>> {
4969        let mut terms = Vec::new();
4970        if let Some(term) = self.term_for_place(place) {
4971            terms.push(term);
4972        }
4973        if let Some(term) = self.storage_addr_for_place(place, &mut HashSet::new()) {
4974            if !terms.iter().any(|existing| existing == &term) {
4975                terms.push(term);
4976            }
4977        }
4978        terms
4979    }
4980
4981    /// Candidate address/value terms for a known initialized write.
4982    fn init_source_terms(&mut self, place: &PlaceKey) -> Vec<Int<'ctx>> {
4983        let mut terms = Vec::new();
4984        if let Some(term) = self.term_for_place(place) {
4985            terms.push(term);
4986        }
4987        if let Some(source) = self.source_from_points_to(place)
4988            && let Some(term) = self.storage_addr_for_place(&source, &mut HashSet::new())
4989            && !terms.iter().any(|existing| existing == &term)
4990        {
4991            terms.push(term);
4992        }
4993        terms
4994    }
4995
4996    /// Return the address of the storage represented by `place`.
4997    fn storage_addr_for_place(
4998        &mut self,
4999        place: &PlaceKey,
5000        seen: &mut HashSet<PlaceKey>,
5001    ) -> Option<Int<'ctx>> {
5002        if !seen.insert(place.clone()) {
5003            return None;
5004        }
5005        if let Some(value) = value_for_place(self.forward, place) {
5006            match &value {
5007                AbstractValue::Place(inner) => {
5008                    return self.storage_addr_for_place(inner, seen);
5009                }
5010                AbstractValue::Cast(inner, _) => {
5011                    if let AbstractValue::Place(inner_place) = inner.as_ref() {
5012                        return self.storage_addr_for_place(inner_place, seen);
5013                    }
5014                }
5015                _ => {}
5016            }
5017        }
5018        if let Some(source) = self.source_from_points_to(place) {
5019            return self.storage_addr_for_place(&source, seen);
5020        }
5021        Some(Int::new_const(
5022            self.ctx,
5023            format!("addr_{}", place_name(place)),
5024        ))
5025    }
5026
5027    /// Find a retained `len(source)` call whose source matches `origin_key`.
5028    fn bounds_len_for_origin(
5029        &mut self,
5030        origin_key: &str,
5031        index: Option<&AbstractValue<'tcx>>,
5032    ) -> Option<(Int<'ctx>, SmtTerm)> {
5033        if let Some(len_place) = self.len_place_for_origin(origin_key) {
5034            return Some((self.term_for_place(&len_place)?, SmtTerm::Place(len_place)));
5035        }
5036        if let Some(index) = index
5037            && let Some(len_value) = self.guarded_len_for_index(origin_key, index)
5038        {
5039            return Some((
5040                self.term_for_value(&len_value, &mut HashSet::new())?,
5041                SmtTerm::Value(value_label(&len_value)),
5042            ));
5043        }
5044        let result = self.allocated_len_for_origin(origin_key)
5045            .filter(|&len| len > 0)
5046            .map(|len| (Int::from_u64(self.ctx, len), SmtTerm::Const(len)))
5047            .or_else(|| {
5048                if self.is_maybe_uninit_origin(origin_key) {
5049                    let len_term = Int::from_u64(self.ctx, u64::MAX / 8);
5050                    Some((len_term, SmtTerm::Const(u64::MAX / 8)))
5051                } else if self.is_slice_pointer_origin(origin_key) {
5052                    let len_key = format!("len({})", origin_key);
5053                    let len_term = self.symbolic_len_term(&len_key);
5054                    Some((len_term, SmtTerm::Value(len_key)))
5055                } else {
5056                    None
5057                }
5058            });
5059        result
5060    }
5061
5062    /// Return true if `origin_key` is the source of an `as_ptr`-like call
5063    /// (suggesting it is a slice / reference whose internal pointer was
5064    /// extracted).  For these origins a symbolic length term is safe because
5065    /// the length is naturally bounded by the reference type.
5066    fn is_slice_pointer_origin(&self, origin_key: &str) -> bool {
5067        self.forward.facts.iter().any(|fact| {
5068            let StateFact::Call(call) = fact else { return false };
5069            let is_ptr_like = is_as_ptr_call(&call.func);
5070            let is_ptr_range = PrimitiveCall::classify(&call.func)
5071                .is_some_and(|p| matches!(p, PrimitiveCall::AsPtrRange | PrimitiveCall::AsMutPtrRange));
5072            (is_ptr_like || is_ptr_range)
5073                && call.effects.iter().any(|effect| {
5074                    matches!(
5075                        effect,
5076                        crate::verify::call_summary::CallEffect::ReturnAliasArg { .. }
5077                            | crate::verify::call_summary::CallEffect::ReturnPointerFromArg { .. }
5078                    )
5079                })
5080                && call.args.iter().any(|arg| {
5081                    self.origin_key_for_value(arg, &mut HashSet::new())
5082                        .is_some_and(|key| key == origin_key)
5083                })
5084        })
5085    }
5086
5087    /// Return true if the allocation for `origin_key` is a `MaybeUninit`
5088    /// wrapper (dynamic-length array).  These have `elements == 0` in
5089    /// KnownAllocated and should use a symbolic/sentinel length.
5090    fn is_maybe_uninit_origin(&self, origin_key: &str) -> bool {
5091        self.forward.facts.iter().any(|fact| {
5092            if let StateFact::KnownAllocated { object, ty_name, elements, .. } = fact {
5093                *elements == 0
5094                    && (place_label(object) == origin_key)
5095                    && ty_name.contains("MaybeUninit")
5096            } else {
5097                false
5098            }
5099        })
5100    }
5101
5102    fn len_place_for_origin(&self, origin_key: &str) -> Option<PlaceKey> {
5103        for fact in &self.forward.facts {
5104            let StateFact::Call(call) = fact else {
5105                continue;
5106            };
5107            let Some(source_arg) = call.effects.iter().find_map(|effect| {
5108                let crate::verify::call_summary::CallEffect::ReturnLengthOfArg { arg } = effect
5109                else {
5110                    return None;
5111                };
5112                Some(*arg)
5113            }) else {
5114                continue;
5115            };
5116            let Some(source) = call.args.get(source_arg) else {
5117                continue;
5118            };
5119            let Some(key) = self.origin_key_for_value(source, &mut HashSet::new()) else {
5120                continue;
5121            };
5122            if key == origin_key {
5123                return Some(PlaceKey {
5124                    base: PlaceBaseKey::Local(call.destination.as_usize()),
5125                    fields: Vec::new(),
5126                });
5127            }
5128        }
5129        None
5130    }
5131
5132    fn allocated_len_for_origin(&self, origin_key: &str) -> Option<u64> {
5133        self.forward.facts.iter().find_map(|fact| match fact {
5134            StateFact::KnownAllocated {
5135                place,
5136                object,
5137                elements,
5138                ty_name,
5139                ..
5140            } if place_label(object) == origin_key || place_label(place) == origin_key => {
5141                // MaybeUninit<[T; N]> with const-generic N has dynamic
5142                // length — return None so guarded_len_for_index can
5143                // try the loop guard instead of using elements=1.
5144                if *elements == 0 || (ty_name.contains("MaybeUninit") && ty_name.contains('[')) {
5145                    return None;
5146                }
5147                Some(*elements)
5148            }
5149            _ => None,
5150        })
5151    }
5152
5153    fn origin_is_initialized_for_ty(&self, origin_key: &str, required_ty_name: &str) -> bool {
5154        if self.forward.facts.iter().any(|fact| {
5155            let StateFact::KnownAllocated {
5156                place,
5157                object,
5158                ty_name,
5159                ..
5160            } = fact
5161            else {
5162                return false;
5163            };
5164            if place_label(object) != origin_key && place_label(place) != origin_key {
5165                return false;
5166            }
5167            if ty_name.contains("MaybeUninit") {
5168                return false;
5169            }
5170            init_type_compatible(ty_name, required_ty_name)
5171                || self
5172                    .initialized_element_ty_for_place(object)
5173                    .is_some_and(|elem| init_type_compatible(&elem, required_ty_name))
5174        }) {
5175            return true;
5176        }
5177        if let Some(local_index) = origin_key
5178            .strip_prefix('_')
5179            .and_then(|s| s.parse::<usize>().ok())
5180        {
5181            let local = rustc_middle::mir::Local::from_usize(local_index);
5182            let ty = self.tcx.optimized_mir(self.checkpoint.caller).local_decls[local].ty;
5183            if let Some(elem_ty_name) = initialized_element_ty_name(ty) {
5184                if init_type_compatible(&elem_ty_name, required_ty_name) {
5185                    return true;
5186                }
5187            }
5188        }
5189        false
5190    }
5191
5192    fn initialized_element_ty_for_place(&self, place: &PlaceKey) -> Option<String> {
5193        let ty = self.place_ty(place)?;
5194        initialized_element_ty_name(ty)
5195    }
5196}
5197
5198/// Recovered index and length terms for a first-cut in-bounds proof.
5199pub(crate) struct PointerBounds<'ctx> {
5200    index: Int<'ctx>,
5201    len: Int<'ctx>,
5202    index_term: SmtTerm,
5203    len_term: SmtTerm,
5204    origin_key: String,
5205}
5206
5207/// Convert an operand into a place key when it names a MIR place.
5208fn operand_place(operand: &Operand<'_>) -> Option<PlaceKey> {
5209    match operand {
5210        Operand::Copy(place) | Operand::Move(place) => Some(PlaceKey::from_mir_place(place)),
5211        Operand::Constant(_) => None,
5212        #[cfg(rapx_rustc_ge_196)]
5213        Operand::RuntimeChecks(_) => None,
5214    }
5215}
5216
5217fn contract_expr_from_place_key<'tcx>(place: PlaceKey) -> ContractExpr<'tcx> {
5218    let base = match place.base {
5219        PlaceBaseKey::Return => PlaceBase::Return,
5220        PlaceBaseKey::Local(local) => PlaceBase::Local(local),
5221        PlaceBaseKey::Arg(arg) => PlaceBase::Arg(arg),
5222    };
5223    let projections = place
5224        .fields
5225        .into_iter()
5226        .map(|index| ContractProjection::Field { index, ty: None })
5227        .collect();
5228    ContractExpr::Place(ContractPlace { base, projections })
5229}
5230
5231/// Return the abstract value assigned to a place when it is tracked by local.
5232fn value_for_place<'a, 'tcx>(
5233    forward: &'a ForwardVisitResult<'tcx>,
5234    place: &PlaceKey,
5235) -> Option<&'a AbstractValue<'tcx>> {
5236    let local = place.local()?;
5237    forward.values.get(&local)
5238}
5239
5240/// Return the pointee type of raw pointers and references.
5241fn pointee_ty<'tcx>(ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
5242    match ty.kind() {
5243        TyKind::RawPtr(ty, _) | TyKind::Ref(_, ty, _) => Some(*ty),
5244        _ => None,
5245    }
5246}
5247
5248/// Return a string label for the pointee type, for type-level alias checks.
5249fn pointee_ty_str<'tcx>(ty: Ty<'tcx>) -> Option<String> {
5250    match ty.kind() {
5251        TyKind::RawPtr(inner, _) | TyKind::Ref(_, inner, _) => Some(format!("{inner:?}")),
5252        _ => None,
5253    }
5254}
5255
5256fn is_len_carrying_ty(ty: Ty<'_>) -> bool {
5257    match ty.kind() {
5258        TyKind::Ref(_, inner, _) => is_len_carrying_ty(*inner),
5259        TyKind::Slice(_) | TyKind::Str => true,
5260        _ => false,
5261    }
5262}
5263
5264fn initialized_element_ty_name<'tcx>(ty: Ty<'tcx>) -> Option<String> {
5265    let ty_name = format!("{ty:?}");
5266    if ty_name.contains("MaybeUninit") {
5267        return None;
5268    }
5269    match ty.kind() {
5270        TyKind::Ref(_, inner, _) | TyKind::RawPtr(inner, _) => {
5271            initialized_element_ty_name(*inner).or_else(|| Some(format!("{inner:?}")))
5272        }
5273        TyKind::Array(elem, _) | TyKind::Slice(elem) => Some(format!("{elem:?}")),
5274        TyKind::Adt(def, args) => {
5275            let def_name = format!("{:?}", def.did());
5276            let is_vec = def_name.contains("Vec")
5277                || ty_name.starts_with("std::vec::Vec<")
5278                || ty_name.starts_with("alloc::vec::Vec<")
5279                || ty_name.starts_with("Vec<");
5280            if is_vec {
5281                return args.iter().find_map(|arg| match arg.kind() {
5282                    GenericArgKind::Type(ty) => Some(format!("{ty:?}")),
5283                    _ => None,
5284                });
5285            }
5286            Some(ty_name)
5287        }
5288        _ => Some(ty_name),
5289    }
5290}
5291
5292fn is_unsigned_integral_ty(ty: Ty<'_>) -> bool {
5293    matches!(ty.kind(), TyKind::Uint(_))
5294}
5295
5296/// Return true when a call summary is a typed pointer addition.
5297fn is_pointer_add_call(func: &str) -> bool {
5298    PrimitiveCall::classify(func).is_some_and(PrimitiveCall::is_pointer_add_like)
5299}
5300
5301/// Return true when a call summary is a typed pointer subtraction.
5302fn is_pointer_sub_call(func: &str) -> bool {
5303    PrimitiveCall::classify(func).is_some_and(PrimitiveCall::is_pointer_sub_like)
5304}
5305
5306/// Return true when a call summary extracts a pointer from a slice-like object.
5307pub(crate) fn is_as_ptr_call(func: &str) -> bool {
5308    PrimitiveCall::classify(func).is_some_and(PrimitiveCall::is_as_ptr_like)
5309}
5310
5311/// Return true when a call summary carries pointer-add semantics.
5312fn call_has_pointer_add_effect(call: &CallSummary<'_>) -> bool {
5313    call.effects.iter().any(|effect| {
5314        matches!(
5315            effect,
5316            crate::verify::call_summary::CallEffect::ReturnPointerAdd { .. }
5317        )
5318    })
5319}
5320
5321fn call_has_pointer_sub_effect(call: &CallSummary<'_>) -> bool {
5322    call.effects.iter().any(|effect| {
5323        matches!(
5324            effect,
5325            crate::verify::call_summary::CallEffect::ReturnPointerSub { .. }
5326        )
5327    })
5328}
5329
5330fn abstract_value_from_rvalue<'tcx>(rvalue: &Rvalue<'tcx>) -> Option<AbstractValue<'tcx>> {
5331    Some(match rvalue {
5332        Rvalue::Use(operand, ..) => abstract_value_from_operand(operand),
5333        Rvalue::Repeat(operand, _) => {
5334            AbstractValue::Repeat(Box::new(abstract_value_from_operand(operand)))
5335        }
5336        Rvalue::Ref(_, _, place) => AbstractValue::Ref(PlaceKey::from_mir_place(place)),
5337        Rvalue::RawPtr(_, place) => AbstractValue::RawPtr(PlaceKey::from_mir_place(place)),
5338        Rvalue::Cast(_, operand, ty) => {
5339            AbstractValue::Cast(Box::new(abstract_value_from_operand(operand)), *ty)
5340        }
5341        Rvalue::BinaryOp(op, pair) => {
5342            let (lhs, rhs) = &**pair;
5343            AbstractValue::Binary(
5344                *op,
5345                Box::new(abstract_value_from_operand(lhs)),
5346                Box::new(abstract_value_from_operand(rhs)),
5347            )
5348        }
5349        Rvalue::UnaryOp(op, operand) => {
5350            AbstractValue::Unary(*op, Box::new(abstract_value_from_operand(operand)))
5351        }
5352        Rvalue::CopyForDeref(place) => AbstractValue::Place(PlaceKey::from_mir_place(place)),
5353        Rvalue::ThreadLocalRef(def_id) => AbstractValue::ThreadLocal(format!("{def_id:?}")),
5354        #[cfg(all(rapx_rustc_ge_193, not(rapx_rustc_ge_196)))]
5355        Rvalue::NullaryOp(op) => AbstractValue::Nullary(format!("{op:?}")),
5356        #[cfg(all(not(rapx_rustc_ge_193), not(rapx_rustc_ge_196)))]
5357        Rvalue::NullaryOp(op, _) => AbstractValue::Nullary(format!("{op:?}")),
5358        Rvalue::Discriminant(place) => AbstractValue::Discriminant(PlaceKey::from_mir_place(place)),
5359        Rvalue::Aggregate(kind, operands) => {
5360            AbstractValue::Aggregate(format!("{kind:?}"), operands.len())
5361        }
5362        #[cfg(not(rapx_rustc_ge_196))]
5363        Rvalue::ShallowInitBox(operand, ty) => {
5364            AbstractValue::ShallowInitBox(Box::new(abstract_value_from_operand(operand)), *ty)
5365        }
5366        _ => return None,
5367    })
5368}
5369
5370fn infer_element_ty<'tcx>(ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
5371    match ty.kind() {
5372        TyKind::Slice(elem_ty) => Some(*elem_ty),
5373        TyKind::Array(elem_ty, _) => Some(*elem_ty),
5374        TyKind::Ref(_, inner, _) => infer_element_ty(*inner),
5375        TyKind::RawPtr(inner_ty, _) => infer_element_ty(*inner_ty),
5376        _ => Some(ty),
5377    }
5378}
5379
5380fn abstract_value_from_operand<'tcx>(operand: &Operand<'tcx>) -> AbstractValue<'tcx> {
5381    match operand {
5382        Operand::Copy(place) | Operand::Move(place) => {
5383            AbstractValue::Place(PlaceKey::from_mir_place(place))
5384        }
5385        Operand::Constant(constant) => {
5386            let text = format!("{:?}", constant.const_);
5387            const_int_from_debug(&text)
5388                .map(AbstractValue::ConstInt)
5389                .unwrap_or(AbstractValue::Const(text))
5390        }
5391        #[cfg(rapx_rustc_ge_196)]
5392        Operand::RuntimeChecks(_) => AbstractValue::Unknown("runtime-checks".to_string()),
5393    }
5394}
5395
5396/// Stable SMT variable name for a place key.
5397fn place_name(place: &PlaceKey) -> String {
5398    let base = match place.base {
5399        PlaceBaseKey::Return => "return".to_string(),
5400        PlaceBaseKey::Local(local) => format!("local_{local}"),
5401        PlaceBaseKey::Arg(arg) => format!("arg_{arg}"),
5402    };
5403    if place.fields.is_empty() {
5404        base
5405    } else {
5406        format!(
5407            "{}_{}",
5408            base,
5409            place
5410                .fields
5411                .iter()
5412                .map(|field| field.to_string())
5413                .collect::<Vec<_>>()
5414                .join("_")
5415        )
5416    }
5417}
5418
5419/// Compact human-readable label for a MIR place key.
5420pub(crate) fn place_label(place: &PlaceKey) -> String {
5421    let base = match place.base {
5422        PlaceBaseKey::Return => "return".to_string(),
5423        PlaceBaseKey::Local(local) => format!("_{local}"),
5424        PlaceBaseKey::Arg(arg) => format!("arg{arg}"),
5425    };
5426    if place.fields.is_empty() {
5427        base
5428    } else {
5429        format!(
5430            "{}.{}",
5431            base,
5432            place
5433                .fields
5434                .iter()
5435                .map(|field| field.to_string())
5436                .collect::<Vec<_>>()
5437                .join(".")
5438        )
5439    }
5440}
5441
5442/// Compact human-readable label for an abstract value.
5443pub(crate) fn value_label(value: &AbstractValue<'_>) -> String {
5444    match value {
5445        AbstractValue::Unknown(text) => format!("unknown({text})"),
5446        AbstractValue::Place(place) => place_label(place),
5447        AbstractValue::Ref(place) => format!("&{}", place_label(place)),
5448        AbstractValue::RawPtr(place) => format!("raw({})", place_label(place)),
5449        AbstractValue::ThreadLocal(name) => format!("thread_local({name})"),
5450        AbstractValue::ConstInt(value) => value.to_string(),
5451        AbstractValue::ConstParam(name) => format!("const_{name}"),
5452        AbstractValue::Const(text) => const_int_from_debug(text)
5453            .map(|value| value.to_string())
5454            .unwrap_or_else(|| text.trim().to_string()),
5455        AbstractValue::Repeat(inner) => format!("repeat({})", value_label(inner)),
5456        AbstractValue::Cast(inner, ty) => format!("cast({}, {ty:?})", value_label(inner)),
5457        AbstractValue::Unary(op, inner) => format!("{op:?}({})", value_label(inner)),
5458        AbstractValue::Binary(op, lhs, rhs) => {
5459            format!(
5460                "({} {} {})",
5461                value_label(lhs),
5462                binop_label(*op),
5463                value_label(rhs)
5464            )
5465        }
5466        AbstractValue::Nullary(name) => name.clone(),
5467        AbstractValue::Discriminant(place) => format!("discriminant({})", place_label(place)),
5468        AbstractValue::Aggregate(name, len) => format!("{name}[{len}]"),
5469        #[cfg(not(rapx_rustc_ge_196))]
5470        AbstractValue::ShallowInitBox(inner, ty) => {
5471            format!("box({}, {ty:?})", value_label(inner))
5472        }
5473        AbstractValue::CallResult(call) if is_pointer_add_call(&call.func) => {
5474            let base = call
5475                .args
5476                .first()
5477                .map(value_label)
5478                .unwrap_or_else(|| "?".to_string());
5479            let index = call
5480                .args
5481                .get(1)
5482                .map(value_label)
5483                .unwrap_or_else(|| "?".to_string());
5484            format!("{base}.add({index})")
5485        }
5486        AbstractValue::CallResult(call) => {
5487            let destination = PlaceKey {
5488                base: PlaceBaseKey::Local(call.destination.as_usize()),
5489                fields: Vec::new(),
5490            };
5491            format!(
5492                "{} = call({})",
5493                place_label(&destination),
5494                short_func_name(&call.func)
5495            )
5496        }
5497    }
5498}
5499
5500fn smt_term_for_value(value: &AbstractValue<'_>) -> Option<SmtTerm> {
5501    match value {
5502        AbstractValue::ConstInt(value) => u64::try_from(*value).ok().map(SmtTerm::Const),
5503        AbstractValue::Const(text) => const_int_from_debug(text)
5504            .and_then(|value| u64::try_from(value).ok())
5505            .map(SmtTerm::Const)
5506            .or_else(|| {
5507                let name = sanitize_smt_name(text);
5508                if name.is_empty() {
5509                    None
5510                } else {
5511                    Some(SmtTerm::Value(format!("const_{name}")))
5512                }
5513            }),
5514        AbstractValue::Place(place) => Some(SmtTerm::Place(place.clone())),
5515        AbstractValue::Cast(inner, _) => smt_term_for_value(inner),
5516        AbstractValue::Binary(op, lhs, rhs) => {
5517            let lhs = Box::new(smt_term_for_value(lhs)?);
5518            let rhs = Box::new(smt_term_for_value(rhs)?);
5519            match op {
5520                BinOp::Add | BinOp::AddWithOverflow => Some(SmtTerm::Add(lhs, rhs)),
5521                BinOp::Sub | BinOp::SubWithOverflow => Some(SmtTerm::Sub(lhs, rhs)),
5522                BinOp::Mul | BinOp::MulWithOverflow => Some(SmtTerm::Mul(lhs, rhs)),
5523                BinOp::Div => Some(SmtTerm::Div(lhs, rhs)),
5524                BinOp::Rem => Some(SmtTerm::Rem(lhs, rhs)),
5525                _ => None,
5526            }
5527        }
5528        _ => None,
5529    }
5530}
5531
5532/// Render a compact binary operator label.
5533fn binop_label(op: BinOp) -> &'static str {
5534    match op {
5535        BinOp::Add => "+",
5536        BinOp::Sub => "-",
5537        BinOp::Mul => "*",
5538        BinOp::Div => "/",
5539        BinOp::Rem => "%",
5540        BinOp::Eq => "==",
5541        BinOp::Ne => "!=",
5542        BinOp::Lt => "<",
5543        BinOp::Le => "<=",
5544        BinOp::Gt => ">",
5545        BinOp::Ge => ">=",
5546        _ => "?",
5547    }
5548}
5549
5550/// Return the final path segment of a rustc debug function name.
5551fn short_func_name(func: &str) -> String {
5552    func.rsplit("::")
5553        .next()
5554        .unwrap_or(func)
5555        .trim_matches('"')
5556        .to_string()
5557}
5558
5559/// Extract a const generic parameter name, e.g. `Param(N)` → `"N"`.
5560fn const_param_name_from_debug(text: &str) -> Option<String> {
5561    let text = text.trim();
5562    // Format: Param(N)
5563    if let Some(rest) = text.find("Param(").map(|i| &text[i + 6..]) {
5564        let end = rest.find(')')?;
5565        let name = rest[..end].trim().to_string();
5566        if !name.is_empty() { return Some(name); }
5567    }
5568    // Format: Ty(usize, N/#1)  or  Ty(usize, N)
5569    if let Some(rest) = text.strip_prefix("Ty(") {
5570        let comma = rest.find(',')?;
5571        let after_comma = rest[comma + 1..].trim();
5572        let end = after_comma.find(')').unwrap_or(after_comma.len());
5573        let name = after_comma[..end].trim();
5574        // Strip trailing /#N index if present
5575        if let Some(slash) = name.rfind('/') {
5576            let name = name[..slash].trim();
5577            if !name.is_empty() { return Some(name.to_string()); }
5578        }
5579        if !name.is_empty() { return Some(name.to_string()); }
5580    }
5581    None
5582}
5583
5584/// Extract a small integer constant from rustc's debug representation.
5585fn const_int_from_debug(text: &str) -> Option<u128> {
5586    let text = text.trim();
5587    if text == "const true" {
5588        return Some(1);
5589    }
5590    if text == "const false" {
5591        return Some(0);
5592    }
5593    if let Some(rest) = text.strip_prefix("const ") {
5594        let digits = rest
5595            .chars()
5596            .take_while(|ch| ch.is_ascii_digit())
5597            .collect::<String>();
5598        if digits.is_empty() {
5599            return None;
5600        }
5601        return digits.parse().ok();
5602    }
5603
5604    let scalar = text.strip_prefix("Val(Scalar(0x")?;
5605    let digits = scalar
5606        .chars()
5607        .take_while(|ch| ch.is_ascii_hexdigit())
5608        .collect::<String>();
5609    if digits.is_empty() {
5610        None
5611    } else {
5612        u128::from_str_radix(&digits, 16).ok()
5613    }
5614}
5615
5616fn init_type_compatible(init_ty_name: &str, required_ty_name: &str) -> bool {
5617    if normalize_init_ty_name(init_ty_name) == normalize_init_ty_name(required_ty_name) {
5618        return true;
5619    }
5620    if let Some(array_elem) = array_elem_type(required_ty_name) {
5621        if init_type_compatible(init_ty_name, &array_elem) {
5622            return true;
5623        }
5624    }
5625    false
5626}
5627
5628fn allocated_type_compatible(allocated_ty_name: &str, required_ty_name: &str) -> bool {
5629    if normalize_init_ty_name(allocated_ty_name) == normalize_init_ty_name(required_ty_name) {
5630        return true;
5631    }
5632    if let Some(array_elem) = array_elem_type(required_ty_name) {
5633        if allocated_type_compatible(allocated_ty_name, &array_elem) {
5634            return true;
5635        }
5636    }
5637    false
5638}
5639
5640fn array_elem_type(ty_name: &str) -> Option<String> {
5641    let name = ty_name.trim();
5642    if name.starts_with('[') && name.ends_with(']') {
5643        let inner = &name[1..name.len() - 1];
5644        if let Some(semi) = inner.rfind("; ") {
5645            return Some(format!(" {}", &inner[..semi]));
5646        }
5647    }
5648    None
5649}
5650
5651fn allocation_object_invalidated<'tcx>(
5652    forward: &ForwardVisitResult<'tcx>,
5653    object: &PlaceKey,
5654) -> bool {
5655    forward.facts.iter().any(|fact| match fact {
5656        StateFact::LocalDead(local) => object.local() == Some(*local),
5657        StateFact::Drop(place) => place.overlaps(object) || object.overlaps(place),
5658        _ => false,
5659    })
5660}
5661
5662fn normalize_init_ty_name(ty_name: &str) -> String {
5663    let ty_name = ty_name.trim();
5664    for prefix in [
5665        "std::mem::MaybeUninit<",
5666        "core::mem::MaybeUninit<",
5667        "MaybeUninit<",
5668    ] {
5669        if let Some(inner) = ty_name
5670            .strip_prefix(prefix)
5671            .and_then(|s| s.strip_suffix('>'))
5672        {
5673            return normalize_init_ty_name(inner);
5674        }
5675    }
5676    if let Some(rest) = ty_name.strip_prefix('[')
5677        && rest.ends_with(']')
5678    {
5679        let inner = &rest[..rest.len() - 1];
5680        if let Some(semi_pos) = inner.rfind("; ") {
5681            return normalize_init_ty_name(&inner[..semi_pos]);
5682        }
5683        // Slice type [T] — same alignment as T.
5684        return normalize_init_ty_name(inner);
5685    }
5686    let mut result = ty_name.to_string();
5687    while let Some(slash) = result.rfind('/')
5688        && slash + 1 < result.len()
5689        && result[slash + 1..].starts_with('#')
5690    {
5691        let trimmed = result[..slash].to_string();
5692        if let Some(after_hash) = result[slash + 2..].chars().next()
5693            && after_hash.is_ascii_digit()
5694        {
5695            result = trimmed;
5696            continue;
5697        }
5698        break;
5699    }
5700    result
5701}
5702
5703/// Stable SMT identifier for diagnostic-only symbolic terms.
5704fn sanitize_smt_name(name: &str) -> String {
5705    name.chars()
5706        .map(|ch| {
5707            if ch.is_ascii_alphanumeric() || ch == '_' {
5708                ch
5709            } else {
5710                '_'
5711            }
5712        })
5713        .collect()
5714}
5715
5716fn ptr_metadata_origin<'tcx>(
5717    value: &AbstractValue<'tcx>,
5718    model: &SmtModel<'_, '_, 'tcx>,
5719) -> Option<String> {
5720    let resolved = model.resolved_value(value, &mut TraceSeen::new())
5721        .unwrap_or_else(|| value.clone());
5722    match &resolved {
5723        AbstractValue::Unary(UnOp::PtrMetadata, inner) => {
5724            let cursor = model.latest_cursor();
5725            model.origin_key_for_value_before(inner, cursor, &mut TraceSeen::new())
5726        }
5727        AbstractValue::CallResult(call) => {
5728            if call.effects.iter().any(|e| matches!(e, crate::verify::call_summary::CallEffect::ReturnLengthOfArg { .. }))
5729            {
5730                let arg = call.effects.iter().find_map(|e| match e {
5731                    crate::verify::call_summary::CallEffect::ReturnLengthOfArg { arg } => Some(*arg),
5732                    _ => None,
5733                })?;
5734                let call_cursor = model.call_definition_cursor(call);
5735                let source = call.args.get(arg)?;
5736                model.origin_key_for_value_before(source, call_cursor, &mut TraceSeen::new())
5737            } else {
5738                None
5739            }
5740        }
5741        _ => None,
5742    }
5743}
5744
5745fn pointee_stride_from_types<'tcx>(
5746    tcx: TyCtxt<'tcx>,
5747    src_pointee: Ty<'tcx>,
5748    dst_pointee: Ty<'tcx>,
5749) -> Option<SmtTerm> {
5750    use rustc_middle::ty::ConstKind;
5751    if src_pointee == dst_pointee {
5752        return Some(SmtTerm::Const(1));
5753    }
5754    if let TyKind::Array(elem, len) = src_pointee.kind()
5755        && *elem == dst_pointee
5756    {
5757        return len.try_to_target_usize(tcx).map(SmtTerm::Const).or_else(|| {
5758            if let ConstKind::Param(param) = len.kind() {
5759                Some(SmtTerm::ConstParam(param.name.to_string()))
5760            } else {
5761                None
5762            }
5763        });
5764    }
5765    None
5766}