Skip to main content

rapx/verify/vm/
state.rs

1//! Symbolic VM state types.
2//!
3//! The data structures that represent the symbolic execution state: symbolic
4//! values and their invariants, memory allocations, and the full execution
5//! state at a program point.
6
7use rustc_hir::def_id::DefId;
8use rustc_middle::{
9    mir::{Body, Local, Operand, Place, ProjectionElem},
10    ty::{Region, Ty, TyCtxt},
11};
12use z3::{
13    Context,
14    ast::{Array, Ast, Bool, Int},
15};
16
17use crate::compat::{FxHashMap, FxHashSet};
18use crate::verify::{def_use::PlaceKey, path_extractor::Path};
19
20/// Unique identifier for a heap or stack allocation.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub(crate) struct AllocId(pub usize);
23
24/// The structure of a pointer's byte offset, when it has one.
25///
26/// Each variant lets the verifier use a cheaper, more precise proof: `Field`
27/// carries the "field in-bounds" guarantee (`offset + size_of(field) <=
28/// size_of(container)`, e.g. `Option::as_slice`), and `Element(k)` tracks the
29/// element index so `InBound` checks `k + count <= slice_len` linearly instead
30/// of the non-linear byte form `(k+count)·S <= len·S` (undecidable in Z3 NIA
31/// for a generic `S`).
32#[derive(Clone, Debug)]
33pub(crate) enum OffsetKind<'z3> {
34    /// Compile-time field offset (`offset_of!`), including a first field at 0.
35    Field,
36    /// Element index from element-strided arithmetic (`ptr.add(k)`); the byte
37    /// offset is `element · S`.
38    Element(Int<'z3>),
39    /// A byte-strided or otherwise unclassifiable offset (`byte_add`).
40    Byte,
41}
42
43/// Pointer provenance: which allocation, at what byte offset, and (when known)
44/// what structure that offset has.
45#[derive(Clone, Debug)]
46pub(crate) struct Provenance<'z3> {
47    /// The allocation this pointer derives from.
48    pub alloc_id: AllocId,
49    /// Byte offset from the allocation base (always present). A freshly created
50    /// pointer to the base of an allocation has `offset = 0`.
51    pub offset: Int<'z3>,
52    /// The offset's structure. `None` means the pointer sits at the base
53    /// (`offset == 0`) with no further structure; `Some(..)` records whether the
54    /// offset is a compile-time field offset (`Field`), an element index from
55    /// element-strided arithmetic (`Element`), or a byte-strided offset (`Byte`).
56    pub offset_kind: Option<OffsetKind<'z3>>,
57}
58
59/// Value-level facts about a single symbolic value (nullness, alignment,
60/// bounds, and whether it has been written).  These travel *with* the value — a
61/// [`VmValue`] leaves its allocation when passed as an operand or stashed in
62/// `InlineCtx::deferred_field_writes` — so they live on the value, not the
63/// allocation.  The allocation-level counterpart to `init` is
64/// [`ContentFacts::initialized`], kept in sync by [`VmState::mark_initialized`].
65///
66/// `PartialEq`/`Eq` are deliberately *not* derived: `align_n` is a Z3 AST
67/// whose equality is structural (`Z3_is_eq_ast`), not semantic, so comparing
68/// two `ValueFacts` would silently report semantically-equal values as
69/// unequal.
70#[derive(Clone, Debug, Default)]
71pub(crate) struct ValueFacts<'z3> {
72    pub non_null: bool,
73    pub init: bool,
74    pub in_bounds: bool,
75    /// If Some(n), the value's term is known to satisfy `z3_term % n == 0`.
76    /// Set by alignment guards, Mul by power-of-two, and type alignment.
77    /// `n` is a Z3 term so that a generic type's alignment (a symbolic
78    /// `align_T`) can be carried the same way as a concrete alignment.
79    pub align_n: Option<Int<'z3>>,
80}
81
82/// A symbolic value tracked by the VM.
83///
84/// # Semantics of `z3_term`
85///
86/// - For pointer/reference types (`&T`, `*const T`, `*mut T`, `Box<T>`, etc.):
87///   `z3_term` represents the **address** in the VM's logical address space.
88/// - For scalar types (integers, `bool`, `char`): `z3_term` represents the **value**.
89/// - For aggregate types (struct, tuple, enum): `z3_term` is the base address of
90///   the stack allocation backing the aggregate.
91///
92/// When `provenance` is `Some`, the following relationship holds and is
93/// asserted into the solver at check time:
94///   `z3_term == alloc[provenance.alloc_id].base + provenance.offset`
95#[derive(Clone, Debug)]
96pub(crate) struct VmValue<'z3, 'tcx> {
97    /// The Z3 integer term (address or scalar value, see struct docs).
98    pub z3_term: Int<'z3>,
99    /// Rust type, for layout queries.
100    pub ty: Ty<'tcx>,
101    /// Which allocation this pointer derives from and at what offset.
102    pub provenance: Option<Provenance<'z3>>,
103    /// Known constraints on this value.
104    pub facts: ValueFacts<'z3>,
105    /// Extra semantics (field offset, discriminant, comparison, or binary-op
106    /// source); see [`ValueSource`].
107    pub source: ValueSource<'z3>,
108}
109
110impl<'z3, 'tcx> VmValue<'z3, 'tcx> {
111    pub(crate) fn new(term: Int<'z3>, ty: Ty<'tcx>) -> Self {
112        VmValue {
113            z3_term: term,
114            ty,
115            provenance: None,
116            facts: ValueFacts::default(),
117            source: ValueSource::None,
118        }
119    }
120
121    /// Convenience: extract the `AllocId` from provenance, if any.
122    pub(crate) fn provenance_alloc_id(&self) -> Option<AllocId> {
123        self.provenance.as_ref().map(|p| p.alloc_id)
124    }
125
126    /// Symbolic enum discriminant, if known.
127    pub(crate) fn discriminant(&self) -> Option<&Int<'z3>> {
128        match &self.source {
129            ValueSource::Discriminant(d) => Some(d),
130            _ => None,
131        }
132    }
133
134    /// Direct boolean condition of a comparison result, if any.
135    pub(crate) fn bool_cond(&self) -> Option<&Bool<'z3>> {
136        match &self.source {
137            ValueSource::Comparison { cond, .. } => Some(cond),
138            _ => None,
139        }
140    }
141
142    /// Whether this scalar is a compile-time `offset_of!` field offset.
143    pub(crate) fn is_field_offset(&self) -> bool {
144        matches!(self.source, ValueSource::FieldOffset)
145    }
146
147    /// Whether this value is a pointer (carries provenance).
148    pub(crate) fn is_pointer(&self) -> bool {
149        self.provenance.is_some()
150    }
151}
152
153/// The shape of an allocation: a single object, a slice/array buffer, or an
154/// external raw-pointer parameter.  `element_ty` (typed vs untyped) and the
155/// `parent` sub-view edge stay separate fields.
156#[derive(Clone, Debug)]
157pub(crate) enum AllocKind<'z3> {
158    /// A single object: a `Box<T>` heap object, a struct, a scalar, or an
159    /// untyped raw buffer.
160    Object,
161    /// A slice/array buffer with a known element count.
162    Slice { len: Int<'z3> },
163    /// An external raw-pointer parameter. `size` is an unconstrained symbolic
164    /// term (the caller may pass any allocation); nullability is *not* stored
165    /// here — it is tracked via the pointer term (`term == 0`) path conditions.
166    External,
167}
168
169/// The element type of an allocation's contents, either a concrete [`Ty`] or
170/// symbolic (`Generic`).
171///
172/// `Typed` carries a concrete `Ty` — the element type of a slice, the object
173/// type of a `Box<T>`/struct, or `u8` for a raw byte buffer — while `Generic`
174/// marks a symbolic element type whose concrete `Ty` cannot be determined
175/// (e.g. `from_raw_parts::<T>`; its `size` uses the shared symbolic `sizeof_T`
176/// and its length must be materialized via `set_slice_len`).
177#[derive(Clone, Debug)]
178pub(crate) enum ElementTy<'tcx> {
179    Typed(Ty<'tcx>),
180    Generic,
181}
182
183impl<'tcx> ElementTy<'tcx> {
184    /// The concrete type, if known.
185    pub(crate) fn as_ty(&self) -> Option<Ty<'tcx>> {
186        match self {
187            ElementTy::Typed(t) => Some(*t),
188            ElementTy::Generic => None,
189        }
190    }
191
192    /// Whether the element type is symbolic/unknown (no concrete `Ty`).
193    pub(crate) fn is_generic(&self) -> bool {
194        matches!(self, ElementTy::Generic)
195    }
196}
197
198impl<'tcx> From<Option<Ty<'tcx>>> for ElementTy<'tcx> {
199    fn from(o: Option<Ty<'tcx>>) -> Self {
200        match o {
201            Some(t) => ElementTy::Typed(t),
202            None => ElementTy::Generic,
203        }
204    }
205}
206
207/// Uniform facts about the *pointer elements* of a container, established by
208/// `x.iter()` for_each invariants.
209///
210/// Each fact is a property every element pointer satisfies.  The facts are
211/// anchored to the container's allocation (not the container value) because a
212/// pointer loaded from the container resolves its provenance through this
213/// allocation, so the checker finds the fact via the loaded pointer's
214/// provenance.
215#[derive(Clone, Debug, Default)]
216pub(crate) struct ForEachFacts<'z3, 'tcx> {
217    /// `Typed(iter(), T)`: every element pointer points at a valid `T`.
218    pub target_ty: Option<Ty<'tcx>>,
219    /// `Align(iter(), T)`: every element pointer is aligned to `align_of(T)`.
220    pub aligned_ty: Option<Ty<'tcx>>,
221    /// `Allocated(iter(), T, n)`: every element pointer backs `>= n` `T`
222    /// elements (`n` may be symbolic).
223    pub allocated: Option<(Ty<'tcx>, Int<'z3>)>,
224    /// `Owning(iter())`: every element pointer is the sole owner of its
225    /// pointee (mutually non-aliasing).  Established by the trusted invariant;
226    /// the aliasing *check* of the invariant itself is done by the alias
227    /// analysis, not by this flag.
228    pub owning: bool,
229}
230
231/// Per-allocation *lifecycle* facts (`dead`/`liveness`/`for_each`), kept apart
232/// from the allocation's shape metadata and `parent` edge so identity/layout
233/// and facts are separated at the type level.  The *content* facts
234/// (readability, C-string/UTF-8 trust) live on [`ContentFacts`] instead.
235#[derive(Clone, Debug, Default)]
236pub(crate) struct AllocFacts<'z3, 'tcx> {
237    /// Whether the allocation has been freed (StorageDead / Drop).
238    pub dead: bool,
239
240    /// The region this external allocation is assumed alive for, via an
241    /// `Alive(p, 'a)` contract/invariant (or `'static` for `ValidCStr`/`Allocated`
242    /// params/`'static` data).  Only consulted for *external* allocations, whose
243    /// memory is owned by the caller (so `dead` carries no liveness guarantee):
244    /// the checker rejects a use that demands a longer region than this.  `None`
245    /// means no assumption — an external allocation then fails `Alive` unless
246    /// grounded in a live reference; a VM-owned allocation is alive while
247    /// `!dead` and never sets this.
248    pub liveness: Option<Region<'tcx>>,
249
250    /// Uniform facts about this allocation's pointer elements, established by
251    /// `x.iter()` for_each invariants (`Typed`/`Align`/`Allocated`).
252    pub for_each: ForEachFacts<'z3, 'tcx>,
253}
254
255/// Per-allocation *content* facts: whether the allocation's contents are
256/// readable, and whether they were asserted to be a valid C string / UTF-8.
257/// These describe the stored bytes/values, so they live on [`MemoryContent`]
258/// next to that data, rather than on [`AllocFacts`] (lifecycle).
259#[derive(Clone, Debug, Default)]
260pub(crate) struct ContentFacts {
261    /// Whether the allocation's contents hold an initialized (readable) value:
262    /// a heap constructor (`Box::new`, `Vec`), a reference parameter's referent,
263    /// a callee return, a `write`, `ValidCStr`, or const/static byte data.
264    /// Stays `false` for uninitialized memory (`Box::new_uninit`,
265    /// `MaybeUninit::uninit`).
266    pub initialized: bool,
267
268    /// Whether this allocation was asserted valid C string via a `ValidCStr`
269    /// contract fact / struct invariant (the "no interior NUL + terminal NUL"
270    /// trust marker).
271    pub cstr_trusted: bool,
272
273    /// Whether this allocation was asserted valid UTF-8 via a `ValidString`
274    /// contract fact (the "bytes form a valid UTF-8 sequence" trust marker).
275    pub utf8_trusted: bool,
276}
277
278/// A memory allocation: a stack local, a heap object (`Box`/`Vec`), or an
279/// external raw-pointer placeholder.
280///
281/// The allocation is stored in a [`MemoryUnit`] at index `AllocId.0` (an
282/// `AllocId` is a monotonic counter that doubles as the vector index).
283#[derive(Clone, Debug)]
284pub(crate) struct Allocation<'z3, 'tcx> {
285    // ── Shape (always present) ──
286    /// Base address (fresh Z3 constant).
287    pub base: Int<'z3>,
288
289    /// Size in bytes (Z3 term, may be symbolic).
290    pub size: Int<'z3>,
291
292    /// Alignment in bytes (Z3 term, may be symbolic for a generic element
293    /// type).
294    pub align: Int<'z3>,
295
296    /// Type of the allocation's contents (concrete `Ty` or symbolic `Generic`).
297    pub element_ty: ElementTy<'tcx>,
298
299    /// The allocation shape (object vs slice vs external).
300    pub kind: AllocKind<'z3>,
301
302    // ── Facts (the allocation-level slice of the Facts layer) ──
303    /// Cross-cutting per-allocation facts (`dead`/`liveness`/`for_each`), kept
304    /// apart from the shape metadata above.
305    pub facts: AllocFacts<'z3, 'tcx>,
306
307    // ── Relationships (Option) ──
308    /// The allocation a sub-view was derived from: a slice view created by
309    /// `s[i..j]` / `s.get(range)`, `split_at` / `align_to` / `as_chunks`, or
310    /// `from_raw_parts`. Each view is a fresh `AllocId` but a window onto the
311    /// same memory as its source, so `root_alloc` follows this edge to group
312    /// aliasing views. The address linkage back to the source lives in the
313    /// pointer term (`parent_term + offset`) and provenance `offset`, not in
314    /// base arithmetic.
315    pub parent: Option<AllocId>,
316}
317
318impl<'z3, 'tcx> Allocation<'z3, 'tcx> {
319    /// Construct a fresh allocation with all live/dead/invariant flags in
320    /// their initial state.
321    pub(crate) fn new(
322        base: Int<'z3>,
323        size: Int<'z3>,
324        align: Int<'z3>,
325        element_ty: Option<Ty<'tcx>>,
326        kind: AllocKind<'z3>,
327    ) -> Self {
328        Allocation {
329            base,
330            size,
331            align,
332            element_ty: element_ty.into(),
333            kind,
334            facts: AllocFacts::default(),
335            parent: None,
336        }
337    }
338
339    /// Whether this allocation models an external raw-pointer parameter.
340    pub(crate) fn is_external(&self) -> bool {
341        matches!(self.kind, AllocKind::External)
342    }
343
344    /// The slice/array element count, if this allocation is slice data.
345    ///
346    /// Invariant: `size == len * sizeof(element_ty)` is maintained by the
347    /// callers that call [`Self::set_slice_len`] (they compute `size` from the
348    /// same `len` and push the equality as a path condition); it is *not*
349    /// enforced here. `slice_len_from_value` falls back to `size / elem_size`
350    /// when the length was never materialized.
351    pub(crate) fn slice_len(&self) -> Option<&Int<'z3>> {
352        match &self.kind {
353            AllocKind::Slice { len } => Some(len),
354            _ => None,
355        }
356    }
357
358    /// Mark this allocation as slice data with the given element count.
359    /// Callers must keep `size == len * sizeof(element_ty)` consistent.
360    pub(crate) fn set_slice_len(&mut self, len: Int<'z3>) {
361        self.kind = AllocKind::Slice { len };
362    }
363}
364
365/// Per-path facts, read afterwards by the property checker.
366///
367/// Most flags are latched at most once during path execution (a contract fact
368/// or a recognized discriminant / bounds check); `reenter` is instead derived
369/// from the input path in [`VmState::new`].  They are per-path state, not
370/// per-step: once set they are never cleared within a path.  (`has_checked_bounds`
371/// is additionally accumulated *across checkpoints* by the engine, which reads
372/// it back into the next path's flags.)
373#[derive(Clone, Copy, Debug, Default)]
374pub(crate) struct PathFacts {
375    /// Whether the current path re-enters a block (loop-unrolled), which lets
376    /// the checker exempt the unrolled iteration's "second drop".
377    pub reenter: bool,
378    /// Whether a SplitTransmute contract was asserted by the caller.
379    pub split_transmute_asserted: bool,
380    /// Whether an `Alias` hazard was accepted via the caller's contract.
381    pub alias_hazard_accepted: bool,
382    /// Whether a ChecksIndexBoundsDisjoint call was processed in any
383    /// checkpoint of this function (accumulated across checkpoints).
384    pub has_checked_bounds: bool,
385    /// Set once the path evaluated an `Iterator::next` discriminant whose
386    /// variant was known symbolically.
387    pub saw_next_discriminant: bool,
388}
389
390/// Scratch state for the *recursive* inlined-callee mechanism
391/// ([`crate::verify::vm::call::exec_inline_call`]), which unwinds via the Rust
392/// call stack, is bounded by `inline_depth`, and stashes its per-call bindings
393/// in `arg_referents`/`deferred_field_writes`.
394///
395/// (The *path-replay* mechanism — `CalleeEntry`/`CalleeExit` items — keeps its
396/// saved caller frames on [`VmState::caller_frames`] instead, which lives next to
397/// [`VmState::current_frame`] to form the frame stack.)
398///
399/// # Deferring `&mut` writes across the inline frame
400///
401/// While the callee runs, the caller's `local_alloc` (the name → allocation
402/// binding) is parked in the saved [`FrameState`], so a write through a `&mut`
403/// argument cannot resolve the caller referent local by address and land in its
404/// field values immediately.  `arg_referents` pre-resolves (before `save_frame`)
405/// which caller local each `&mut` argument points at, and `deferred_field_writes`
406/// collects the writes to replay once the caller is restored:
407///
408/// ```text
409/// struct Foo { field: i32 }
410/// fn bar(foo: &mut Foo) { foo.field = 1; }   // inlined callee
411/// fn main() {
412///     let mut x = Foo { field: 0 };
413///     bar(&mut x);                            // inline `bar`
414/// }
415/// ```
416///
417/// 1. Enter `bar`: `save_frame` parks `main`'s local_alloc; `arg_referents[0] = x`.
418/// 2. Run `bar`: `foo.field = 1` (`(*foo).field`) can no longer resolve `x` by
419///    address (the caller's address map is gone), so it pushes `(x, [0], 1)`.
420/// 3. Exit `bar`: `restore_frame` brings `main`'s local_alloc back, then the deferred
421///    write is replayed, giving `x.field == 1`.
422#[derive(Default)]
423pub(crate) struct InlineCtx<'z3, 'tcx> {
424    /// Current inlining depth (nested inlined callees), bounded by
425    /// `MAX_INLINE_DEPTH`.
426    pub inline_depth: usize,
427    /// During `exec_inline_call`, maps each callee argument index to the
428    /// *caller* local its value points at (resolved from the reference's
429    /// address term before the caller's address map is saved away).  Used by
430    /// `exec_assign` to resolve `(*self).field = val` writes through a `&mut
431    /// self` reborrow temp back to the caller's referent.
432    pub arg_referents: Vec<Option<Local>>,
433    /// Field writes through a `&mut` argument collected during
434    /// `exec_inline_call`, replayed against the caller's field values after
435    /// `restore_frame` (the caller's address map is parked while the callee
436    /// runs).  Each entry is `(caller_local, field_path, value)`, where
437    /// `caller_local` comes from `arg_referents` — the caller local the `&mut`
438    /// argument points at, not the argument itself.
439    pub deferred_field_writes: Vec<(Local, Vec<usize>, VmValue<'z3, 'tcx>)>,
440}
441
442/// The extra semantics attached to a value, beyond its term/type/provenance.
443///
444/// A single value carries at most one of these: it is either a plain value, an
445/// `offset_of!` field offset, a symbolic enum discriminant, a comparison
446/// result, or a non-comparison binary-op result.  The operands/operator are
447/// used for guard inference (tracing a switch/assert guard back to the pointer
448/// it null-checks/alignment-checks) and division-axiom injection (following
449/// dataflow edges to reach `Div`/`Rem` results).
450#[derive(Clone, Debug)]
451pub(crate) enum ValueSource<'z3> {
452    /// No extra semantics.
453    None,
454    /// A compile-time `offset_of!` field offset.
455    FieldOffset,
456    /// A symbolic enum discriminant (variant index).
457    Discriminant(Int<'z3>),
458    /// A comparison result (`Eq`/`Ne`/`Le`/`Lt`/`Ge`/`Gt`): the operands and
459    /// the direct boolean condition (`offset <= len`) carried alongside the
460    /// ite-encoded term.
461    Comparison {
462        lhs: Option<PlaceKey>,
463        rhs: Option<PlaceKey>,
464        op: rustc_middle::mir::BinOp,
465        cond: Bool<'z3>,
466    },
467    /// A non-comparison binary-op result (`Add`/`Sub`/…/`Div`/`Rem`): the
468    /// operands and operator.
469    BinaryOp {
470        lhs: Option<PlaceKey>,
471        rhs: Option<PlaceKey>,
472        op: rustc_middle::mir::BinOp,
473    },
474}
475
476impl<'z3> ValueSource<'z3> {
477    /// The `(lhs, rhs, op)` of a binary-op/comparison result, if this value is
478    /// one.
479    pub(crate) fn operands(&self) -> Option<(&Option<PlaceKey>, &Option<PlaceKey>, rustc_middle::mir::BinOp)> {
480        match self {
481            ValueSource::Comparison { lhs, rhs, op, .. } => Some((lhs, rhs, *op)),
482            ValueSource::BinaryOp { lhs, rhs, op } => Some((lhs, rhs, *op)),
483            _ => None,
484        }
485    }
486
487    /// Just the field-offset part of this source: `FieldOffset` if it is one,
488    /// otherwise `None`.
489    pub(crate) fn field_offset_only(&self) -> ValueSource<'z3> {
490        match self {
491            ValueSource::FieldOffset => ValueSource::FieldOffset,
492            _ => ValueSource::None,
493        }
494    }
495}
496
497/// A single allocation: its shape metadata ([`Allocation`]) plus its contents
498/// ([`MemoryContent`]).  Splitting the two keeps the identity/layout facts apart
499/// from the mutable memory the values live in, while colocating them in one
500/// unit so no parallel table can drift out of sync.  Facts live in three
501/// anchored layers — lifecycle ([`AllocFacts`]), content ([`ContentFacts`]),
502/// and value ([`ValueFacts`] on each [`VmValue`]) — see each type's doc.
503pub(crate) struct MemoryUnit<'z3, 'tcx> {
504    /// The allocation's shape and lifecycle facts.
505    pub(crate) allocation: Allocation<'z3, 'tcx>,
506    /// The allocation's contents (byte layer + typed-value layer) and content facts.
507    pub(crate) content: MemoryContent<'z3, 'tcx>,
508}
509
510/// The per-allocation contents: the byte layer, the typed-value layer, and the
511/// content facts.
512#[derive(Default)]
513pub(crate) struct MemoryContent<'z3, 'tcx> {
514    /// Byte value function: `byte[i] = select(array, i)` for any (possibly
515    /// symbolic) offset `i`.  `None` when no byte has been written.  Unwritten
516    /// offsets read back the `UNINIT` sentinel, so `init`/`nul` are derived
517    /// from `select`, not stored per byte.
518    pub(crate) byte_array: Option<Array<'z3>>,
519
520    /// The concrete byte offsets written to this allocation.  Z3 arrays cannot
521    /// enumerate their stored indices, so the byte-level checkers iterate this
522    /// set directly instead of scanning the allocation's (possibly symbolic or
523    /// huge) `size` range.  Only *concrete* writes are recorded: a symbolic
524    /// `byte_write` (e.g. a symbolic `ValidCStr` length) still updates the byte
525    /// array but not this set.
526    pub(crate) byte_written: FxHashSet<usize>,
527
528    /// The typed-value (Value) layer: (viewed_type, path) → value.  `path == []`
529    /// is the allocation's *whole* value (the rvalue bound to a local) and
530    /// `path == [i, ..]` is field `i`, both viewed as `viewed_type`.  The
531    /// `viewed_type` distinguishes reinterprets of the same allocation under
532    /// different ADTs (e.g. `LeafNode` vs `InternalNode` cast views), so field
533    /// index `1` resolves to `parent_idx` under `LeafNode` and `edges` under
534    /// `InternalNode` without colliding.  This is the alloc-keyed counterpart to
535    /// the byte-level [`Self::byte_array`]; the Local-keyed
536    /// `local_value`/`set_local`/`field_value`/`set_field_value` resolve a
537    /// local's backing allocation and then read/write this layer.
538    pub(crate) values: FxHashMap<(Ty<'tcx>, Vec<usize>), VmValue<'z3, 'tcx>>,
539
540    /// Content facts (`initialized`/`cstr_trusted`/`utf8_trusted`); value-level
541    /// facts live on each [`VmValue::facts`].
542    pub(crate) facts: ContentFacts,
543}
544
545/// Accumulated solver state for the current path.
546///
547/// `assertions` is the assertion stream fed to Z3; `term_caches` holds the
548/// per-phenomenon term-provenance caches that keep expressions compact and
549/// linear.  Everything is path-scoped and monotonic: it accumulates as the VM
550/// steps and is never reset within a path (or across inlined frames).
551#[derive(Default)]
552pub(crate) struct Constraints<'z3, 'tcx> {
553    /// Accumulated solver constraints along the current path: branch/guard
554    /// constraints (`SwitchInt`/`Assert`), API preconditions, and symbolic
555    /// layout facts (`sizeof_T`, `align_T`).  Asserted into the solver by
556    /// [`VmState::assert_all`] and by the property checker's feasibility
557    /// queries.
558    pub(crate) assertions: Vec<Bool<'z3>>,
559
560    /// Term-provenance caches that shape terms into a form Z3 can solve.
561    pub(crate) term_caches: TermCaches<'z3, 'tcx>,
562}
563
564/// Term-provenance caches, one per phenomenon the VM must shape by hand.
565///
566/// Z3's nonlinear integer solver (NIA) cannot rewrite degree-3 products or
567/// deeply-nested pointer chains, so the VM records the *semantic meaning* of a
568/// symbol (what it divides, which iterator it indexes, …) and uses that
569/// provenance later to emit a compact, degree-≤2 term instead.  Each cache is
570/// independent — they share only the path-scoped, monotonic lifetime.
571#[derive(Default)]
572pub(crate) struct TermCaches<'z3, 'tcx> {
573    /// `sizeof_T` for each generic type, one symbolic constant per type.  Keeps
574    /// `ptr.add` strides, `access_bytes` element sizes, and allocation sizes
575    /// consistent so that SMT can cancel the `S` factor in `InBound`.
576    pub(crate) sizes: FxHashMap<Ty<'tcx>, Int<'z3>>,
577
578    /// `align_T` for each generic type, one symbolic constant per type; linked
579    /// to the size by the layout constraint `sizeof_T % align_T == 0`.
580    pub(crate) aligns: FxHashMap<Ty<'tcx>, Int<'z3>>,
581
582    /// Terms that are the result of a bitwise `Not` (two's-complement mask).
583    /// Used to recognize `x & !(align-1)` alignment patterns in BitAnd so we
584    /// can derive `align = -mask` and emit linear bounds for the result.
585    pub(crate) not_mask_terms: FxHashSet<Int<'z3>>,
586
587    /// `quotient → dividend` for each *exact* division (`lhs % rhs == 0`),
588    /// recording which size each `exact_div` symbol divides (e.g. `us` →
589    /// `sizeof_T`).  Lets a later `us_len = (len / ts) * us` multiplication
590    /// emit the byte bound `us_len * sizeof_U <= len * sizeof_T`.
591    pub(crate) exact_div_roots: FxHashMap<Int<'z3>, Int<'z3>>,
592
593    /// `quotient → (lhs, rhs)` for each *non-exact* division, recovering the
594    /// `len` and divisor (`ts`) operands at a following `us_len = (len / ts) * us`.
595    pub(crate) div_roots: FxHashMap<Int<'z3>, (Int<'z3>, Int<'z3>)>,
596
597    /// Per-iterator element index, keyed by the *buffer* the iterator walks
598    /// (`end` field's provenance alloc id, which is frame-independent).  The
599    /// value is `(offset, base_len)`: the current element index and the total
600    /// element count (the end field's `Element` offset, `None` when unknown).
601    /// Caching both keeps `next`/`len`/`is_empty` checks linear
602    /// (`base_len - offset`) instead of a deeply-nested `((base + S) + S) …`
603    /// pointer chain that Z3's NIA cannot reason about.
604    pub(crate) iter_ptr_offset: FxHashMap<AllocId, (Int<'z3>, Option<Int<'z3>>)>,
605
606    /// `UNINIT` sentinel byte value (≥ 256, outside the `u8` value range).
607    /// The default element of every byte array; `select(array, i) != UNINIT`
608    /// is how "byte `i` was written" is decided.
609    pub(crate) uninit_byte: Option<Int<'z3>>,
610}
611
612/// The frame-scoped subset of [`VmState`]: the name → allocation binding keyed
613/// by MIR `Local`, which the callee reuses, so it must be swapped out for the
614/// duration of an inlined callee and swapped back afterwards.
615pub(crate) struct FrameState {
616    /// The function whose body we execute (the MIR is derived via
617    /// [`VmState::body`]).
618    pub(crate) current_def_id: DefId,
619
620    /// The stack allocation backing each local's place (lvalue identity). A
621    /// local's whole value lives at `path == []` and its field values at
622    /// `path == [i, ..]` in that allocation (see [`MemoryContent::values`], keyed
623    /// `(AllocId, view_ty, path)` with the local's declared type as the view
624    /// type), so there is no local-keyed value table here.
625    pub(crate) local_alloc: FxHashMap<Local, AllocId>,
626}
627
628/// The full symbolic execution state at a program point.
629///
630/// Accumulates the name → allocation bindings, allocations, and solver
631/// constraints as the VM steps through retained MIR items. The Z3 context is
632/// borrowed so a single context can be reused across property checks.
633pub(crate) struct VmState<'z3, 'tcx> {
634    // ── Shared handles (passed in at run start; not execution state, but
635    //    needed to create terms and query types during checking)
636    /// Shared Z3 context.
637    pub(crate) z3_ctx: &'z3 Context,
638
639    /// Compiler type context.
640    pub(crate) tcx: TyCtxt<'tcx>,
641
642    // ── Frame-scoped state (swapped on inline entry/exit; the exact set
643    //    captured by [`Self::save_frame`])
644    pub(crate) current_frame: FrameState,
645
646    // ── Path-scoped state (accumulates across the whole path, including
647    //    inlined frames)
648    /// Stack of saved caller frames for path-replay inlining
649    /// (`CalleeEntry`/`CalleeExit` items).  Together with [`Self::current_frame`]
650    /// (the current frame) it forms the call stack: entering an inlined callee
651    /// moves the current frame here, exiting restores it.
652    pub(crate) caller_frames: Vec<FrameState>,
653
654    /// The object space: one [`MemoryUnit`] per allocation, indexed by
655    /// `AllocId`.  Each unit carries its shape ([`Allocation`]) and its contents
656    /// (per-byte state + per-allocation typed values).
657    pub(crate) units: Vec<MemoryUnit<'z3, 'tcx>>,
658
659    /// Solver constraints and term caches accumulated along the current path.
660    pub(crate) constraints: Constraints<'z3, 'tcx>,
661
662    /// Recursive-inlining scratch state (depth and per-call temporary bindings
663    /// pushed/popped on inline entry/exit).
664    pub(crate) inline: InlineCtx<'z3, 'tcx>,
665
666    /// Per-path facts (latched while stepping, or derived in [`Self::new`]),
667    /// read by the property checker.
668    pub(crate) path_facts: PathFacts,
669}
670
671impl<'z3, 'tcx> VmState<'z3, 'tcx> {
672    /// Create a fresh VM state for executing a path.
673    pub(crate) fn new(
674        z3_ctx: &'z3 Context,
675        tcx: TyCtxt<'tcx>,
676        path: &Path,
677        caller_def_id: DefId,
678    ) -> Self {
679        // Derive the one path fact the checker needs after `run` (whether the
680        // path re-enters a block); the raw `Path` itself is not kept.
681        let reenter = path.reenters();
682        // The `UNINIT` sentinel is created once and shared by every byte array:
683        // an unwritten offset reads it back, so `select != UNINIT` decides
684        // whether a byte was written.
685        let mut constraints = Constraints::default();
686        let uninit = Int::fresh_const(z3_ctx, "uninit_byte");
687        constraints.term_caches.uninit_byte = Some(uninit.clone());
688        constraints
689            .assertions
690            .push(uninit.ge(&Int::from_u64(z3_ctx, 256)));
691        Self {
692            z3_ctx,
693            tcx,
694            current_frame: FrameState {
695                current_def_id: caller_def_id,
696                local_alloc: FxHashMap::default(),
697            },
698            caller_frames: Vec::default(),
699            units: Vec::default(),
700            inline: InlineCtx::default(),
701            constraints,
702            path_facts: PathFacts {
703                reenter,
704                ..PathFacts::default()
705            },
706        }
707    }
708
709    /// The MIR body of the current function, derived from `current_def_id`.
710    pub(crate) fn body(&self) -> &'tcx Body<'tcx> {
711        self.tcx.optimized_mir(self.current_frame.current_def_id)
712    }
713
714    /// Capture the frame-scoped state before switching to an inlined callee.
715    ///
716    /// This is the single source of truth for *what* is frame-scoped: the whole
717    /// [`FrameState`] (function identity, local bindings, and operand sources).
718    /// Both inline mechanisms (`handle_callee_entry` in path replay and
719    /// `exec_inline_call`) call this, so they can no longer drift apart.
720    pub(crate) fn save_frame(&mut self) -> FrameState {
721        FrameState {
722            current_def_id: self.current_frame.current_def_id,
723            local_alloc: std::mem::take(&mut self.current_frame.local_alloc),
724        }
725    }
726
727    /// Restore the frame-scoped state after an inlined callee returns.
728    pub(crate) fn restore_frame(&mut self, frame: FrameState) {
729        self.current_frame = frame;
730    }
731
732    /// Look up the whole value bound to a MIR local (its `path == []` slot in
733    /// the allocation backing the local).
734    pub(crate) fn local_value(&self, local: Local) -> Option<&VmValue<'z3, 'tcx>> {
735        let alloc_id = *self.current_frame.local_alloc.get(&local)?;
736        let view_ty = self.body().local_decls[local].ty;
737        self.load_value(alloc_id, view_ty, &[])
738    }
739
740    /// Bind a whole value to a MIR local (store it at `path == []` in the
741    /// allocation backing the local, allocating that stack slot on demand).
742    pub(crate) fn set_local(&mut self, local: Local, value: VmValue<'z3, 'tcx>) {
743        self.ensure_local_allocation(local);
744        let alloc_id = self.current_frame.local_alloc[&local];
745        let view_ty = self.body().local_decls[local].ty;
746        self.store_value(alloc_id, view_ty, vec![], value);
747    }
748
749    /// Mark a value as initialized (written), and sync its backing allocation's
750    /// [`ContentFacts::initialized`] fact.
751    ///
752    /// This is the single entry point that keeps the value-level
753    /// [`ValueFacts::init`] and the allocation-level [`ContentFacts::initialized`]
754    /// in sync; callers must set the pair through here rather than by hand so
755    /// the two facts cannot drift apart.
756    pub(crate) fn mark_initialized(&mut self, value: &mut VmValue<'z3, 'tcx>) {
757        value.facts.init = true;
758        if let Some(prov) = &value.provenance {
759            self.content_mut(prov.alloc_id).facts.initialized = true;
760        }
761    }
762
763    /// Get the symbolic address of a MIR local (its stack allocation's base).
764    pub(crate) fn local_address(&mut self, local: Local) -> Int<'z3> {
765        self.ensure_local_allocation(local);
766        let id = self.current_frame.local_alloc[&local];
767        self.units[id.0].allocation.base.clone()
768    }
769
770    /// Allocate a fresh symbolic object and return its ID and base address.
771    pub(crate) fn allocate(
772        &mut self,
773        size: Int<'z3>,
774        align: Int<'z3>,
775        element_ty: Option<Ty<'tcx>>,
776    ) -> (AllocId, Int<'z3>) {
777        self.allocate_internal(size, align, element_ty, AllocKind::Object)
778    }
779
780    /// Allocate a fresh external allocation (for raw-pointer parameters).
781    /// External allocations may be null and have unlimited size.
782    pub(crate) fn allocate_external(
783        &mut self,
784        size: Int<'z3>,
785        align: Int<'z3>,
786        element_ty: Option<Ty<'tcx>>,
787    ) -> (AllocId, Int<'z3>) {
788        self.allocate_internal(size, align, element_ty, AllocKind::External)
789    }
790
791    /// Allocate a slice/array data allocation with a known (possibly symbolic)
792    /// element count. Computes `size = len * elem_size` and materializes `len`
793    /// in one step, so `size` and the slice length can never diverge (the
794    /// `size == len * elem_size` invariant is established here instead of being
795    /// re-derived by every caller).
796    pub(crate) fn allocate_slice(
797        &mut self,
798        len: Int<'z3>,
799        elem_size: Int<'z3>,
800        align: Int<'z3>,
801        element_ty: Option<Ty<'tcx>>,
802    ) -> (AllocId, Int<'z3>) {
803        let size = Int::mul(self.z3_ctx, &[&len, &elem_size]);
804        let (id, base) = self.allocate(size, align, element_ty);
805        self.alloc_mut(id).set_slice_len(len);
806        (id, base)
807    }
808
809    fn allocate_internal(
810        &mut self,
811        size: Int<'z3>,
812        align: Int<'z3>,
813        element_ty: Option<Ty<'tcx>>,
814        kind: AllocKind<'z3>,
815    ) -> (AllocId, Int<'z3>) {
816        let id = AllocId(self.units.len());
817        let base = {
818            let name = format!(
819                "{}_{}",
820                if matches!(kind, AllocKind::External) {
821                    "ext"
822                } else {
823                    "heap"
824                },
825                id.0
826            );
827            Int::new_const(self.z3_ctx, name.as_str())
828        };
829        let alloc = Allocation::new(base.clone(), size, align, element_ty, kind);
830        self.units.push(MemoryUnit {
831            allocation: alloc,
832            content: MemoryContent::default(),
833        });
834        (id, base)
835    }
836
837    /// Indexed access to an allocation by its `AllocId` (the id is the index).
838    pub(crate) fn alloc(&self, id: AllocId) -> &Allocation<'z3, 'tcx> {
839        &self.units[id.0].allocation
840    }
841
842    /// Mutable indexed access to an allocation by its `AllocId`.
843    pub(crate) fn alloc_mut(&mut self, id: AllocId) -> &mut Allocation<'z3, 'tcx> {
844        &mut self.units[id.0].allocation
845    }
846
847    /// Indexed access to an allocation's contents by its `AllocId`.
848    pub(crate) fn content(&self, id: AllocId) -> &MemoryContent<'z3, 'tcx> {
849        &self.units[id.0].content
850    }
851
852    /// Mutable indexed access to an allocation's contents by its `AllocId`.
853    pub(crate) fn content_mut(&mut self, id: AllocId) -> &mut MemoryContent<'z3, 'tcx> {
854        &mut self.units[id.0].content
855    }
856
857    /// Whether `id` was asserted a valid C string via a `ValidCStr` contract
858    /// fact / struct invariant.
859    pub(crate) fn is_cstr_trusted(&self, id: AllocId) -> bool {
860        self.content(id).facts.cstr_trusted
861    }
862
863    /// Whether `id` was asserted valid UTF-8 via a `ValidString` contract fact.
864    pub(crate) fn is_utf8_trusted(&self, id: AllocId) -> bool {
865        self.content(id).facts.utf8_trusted
866    }
867
868    /// The ultimate root allocation, following `parent` chains (sub-allocations
869    /// created by `from_raw_parts` / `split_at` / `as_chunks`, whose `parent`
870    /// points at the allocation they were split from).
871    pub(crate) fn root_alloc(&self, id: AllocId) -> AllocId {
872        let mut cur = id;
873        let mut guard = 0;
874        while let Some(parent) = self.alloc(cur).parent {
875            cur = parent;
876            guard += 1;
877            // A parent chain as long as the total allocation count means a
878            // cycle; stop rather than loop forever.
879            if guard >= self.units.len() {
880                break;
881            }
882        }
883        cur
884    }
885
886    /// Create a fresh symbolic Z3 int constant (globally unique, even across
887    /// calls with the same prefix — `Z3_mk_fresh_const` auto-suffixes the name).
888    pub(crate) fn fresh_int(&self, prefix: &str) -> Int<'z3> {
889        Int::fresh_const(self.z3_ctx, prefix)
890    }
891
892    /// Get the value of a specific field within an aggregate local.
893    ///
894    /// Field values live in the allocation backing the local (the
895    /// memory-contents layer [`MemoryContent::values`]), keyed by the local's declared
896    /// type as the view type.  Returns `None` if the local has no allocation
897    /// (the field was never materialized).
898    pub(crate) fn field_value(&self, local: Local, path: &[usize]) -> Option<&VmValue<'z3, 'tcx>> {
899        let alloc_id = *self.current_frame.local_alloc.get(&local)?;
900        let view_ty = self.body().local_decls[local].ty;
901        self.load_value(alloc_id, view_ty, path)
902    }
903
904    /// Enumerate the field paths materialized for a local: every non-empty
905    /// `path` for which [`Self::field_value`] currently returns a value (the
906    /// local's allocation's fields under its declared view type).  The whole
907    /// value (`path == []`) is deliberately excluded — it is read via
908    /// [`Self::local_value`], not as a "field", so callers iterating fields do
909    /// not accidentally treat the whole value as a field projection.
910    pub(crate) fn field_paths(&self, local: Local) -> Vec<Vec<usize>> {
911        let Some(&alloc_id) = self.current_frame.local_alloc.get(&local) else {
912            return Vec::new();
913        };
914        let view_ty = self.body().local_decls[local].ty;
915        self.units[alloc_id.0]
916            .content
917            .values
918            .keys()
919            .filter(|(t, p)| *t == view_ty && !p.is_empty())
920            .map(|(_, p)| p.clone())
921            .collect()
922    }
923
924    /// The declared type of `local` in `frame`'s body.
925    fn frame_local_ty(&self, frame: &FrameState, local: Local) -> Ty<'tcx> {
926        self.tcx.optimized_mir(frame.current_def_id).local_decls[local].ty
927    }
928
929    /// Enumerate the field paths materialized for `local` in a saved caller
930    /// `frame`. Field values live in the path-scoped [`MemoryContent::values`], so they
931    /// are read via `frame`'s `local_alloc` and declared type.
932    pub(crate) fn frame_field_paths(
933        &self,
934        frame: &FrameState,
935        local: Local,
936    ) -> Vec<Vec<usize>> {
937        let Some(&alloc_id) = frame.local_alloc.get(&local) else {
938            return Vec::new();
939        };
940        let view_ty = self.frame_local_ty(frame, local);
941        self.units[alloc_id.0]
942            .content
943            .values
944            .keys()
945            .filter(|(t, p)| *t == view_ty && !p.is_empty())
946            .map(|(_, p)| p.clone())
947            .collect()
948    }
949
950    /// Read a field of `local` in a saved caller `frame`.
951    pub(crate) fn frame_field_value(
952        &self,
953        frame: &FrameState,
954        local: Local,
955        path: &[usize],
956    ) -> Option<&VmValue<'z3, 'tcx>> {
957        let alloc_id = *frame.local_alloc.get(&local)?;
958        let view_ty = self.frame_local_ty(frame, local);
959        self.load_value(alloc_id, view_ty, path)
960    }
961
962    /// Read the whole value of `local` in a saved caller `frame` (its
963    /// `path == []` slot). The value lives in the path-scoped [`MemoryContent::values`],
964    /// while the name → allocation binding lives in the saved `frame`.
965    pub(crate) fn frame_local_value(
966        &self,
967        frame: &FrameState,
968        local: Local,
969    ) -> Option<&VmValue<'z3, 'tcx>> {
970        let alloc_id = *frame.local_alloc.get(&local)?;
971        let view_ty = self.frame_local_ty(frame, local);
972        self.load_value(alloc_id, view_ty, &[])
973    }
974
975    /// Enumerate `(local, whole value)` for every local with a materialized
976    /// whole value (its `path == []` slot). This is the whole-value counterpart
977    /// to [`Self::field_paths`], used where code used to iterate `locals`.
978    pub(crate) fn all_local_values(&self) -> Vec<(Local, &VmValue<'z3, 'tcx>)> {
979        self.current_frame
980            .local_alloc
981            .keys()
982            .copied()
983            .filter_map(|local| self.local_value(local).map(|value| (local, value)))
984            .collect()
985    }
986
987    /// The buffer an `Iter`/`IterMut` at `local` walks: the provenance of its
988    /// `end` field.  This is frame-independent (the buffer allocation is
989    /// path-scoped), unlike the `local` itself, so it keys the per-iterator
990    /// element index in [`TermCaches::iter_ptr_offset`].
991    pub(crate) fn iter_buffer(&self, local: Local) -> Option<AllocId> {
992        self.field_value(local, &[1])
993            .and_then(|end| end.provenance.as_ref())
994            .map(|ep| ep.alloc_id)
995    }
996
997    /// The byte buffer walked by an iterator at `local`, possibly wrapped in
998    /// adapter types (`Cloned`/`Rev`/…).  `Rvalue::Aggregate` flattens nested
999    /// adapters, so the innermost `Iter`/`IterMut` `end` pointer is any tracked
1000    /// field whose path ends in `[1]`.  Return its allocation and end offset
1001    /// (the end offset doubles as the byte length when the element is `u8`).
1002    pub(crate) fn iter_utf8_buffer(&self, local: Local) -> Option<(AllocId, Int<'z3>)> {
1003        let mut best: Option<(usize, AllocId, Int<'z3>)> = None;
1004        for path in self.field_paths(local) {
1005            if path.last() != Some(&1) {
1006                continue;
1007            }
1008            let Some(v) = self.field_value(local, &path) else {
1009                continue;
1010            };
1011            let Some(prov) = v.provenance.as_ref() else {
1012                continue;
1013            };
1014            if best.as_ref().map_or(true, |(depth, _, _)| path.len() < *depth) {
1015                best = Some((path.len(), prov.alloc_id, prov.offset.clone()));
1016            }
1017        }
1018        best.map(|(_, id, off)| (id, off))
1019    }
1020
1021    /// The field carrying an owned value's heap pointer (`Box`/`Vec`/`String`'s
1022    /// owning pointer field), located generically via
1023    /// [`Self::container_ptr_field`] (e.g. `Box` → `[0, 0]`, `Vec` → `[0, 0, 0]`).
1024    /// Falls back to the first materialized field with heap provenance for
1025    /// owners whose deep `NonNull` leaf was not explicitly materialized (e.g. a
1026    /// `Box` produced by `into_boxed_slice`, which only records the whole value).
1027    pub(crate) fn owner_ptr_field(&self, local: Local) -> Option<&VmValue<'z3, 'tcx>> {
1028        let ty = self.body().local_decls[local].ty;
1029        if let Some((path, _)) = self.container_ptr_field(ty) {
1030            if let Some(v) = self.field_value(local, &path) {
1031                if v.provenance_alloc_id().is_some() {
1032                    return Some(v);
1033                }
1034            }
1035        }
1036        self.field_paths(local)
1037            .iter()
1038            .find_map(|path| {
1039                self.field_value(local, path)
1040                    .filter(|v| v.provenance_alloc_id().is_some())
1041            })
1042    }
1043
1044    /// Invalidate `local`'s owner-field provenance (mark it moved-out after a
1045    /// whole-place move), so a later `Owning` check does not treat it as a
1046    /// second owner of the heap allocation it no longer owns.
1047    pub(crate) fn invalidate_owner_field(&mut self, local: Local) {
1048        let ty = self.body().local_decls[local].ty;
1049        let Some((path, _)) = self.container_ptr_field(ty) else {
1050            return;
1051        };
1052        let Some(mut fv) = self.field_value(local, &path).cloned() else {
1053            return;
1054        };
1055        if fv.provenance_alloc_id().is_some() {
1056            fv.provenance = None;
1057            self.set_field_value(local, path, fv);
1058        }
1059    }
1060
1061    /// Set the value of a specific field within an aggregate local.
1062    ///
1063    /// Ensures the local has a backing allocation, then stores into the
1064    /// memory-contents layer ([`MemoryContent::values`]) keyed by the local's declared
1065    /// type as the view type.
1066    pub(crate) fn set_field_value(
1067        &mut self,
1068        local: Local,
1069        path: Vec<usize>,
1070        value: VmValue<'z3, 'tcx>,
1071    ) {
1072        let view_ty = self.body().local_decls[local].ty;
1073        self.ensure_local_allocation(local);
1074        let alloc_id = self.current_frame.local_alloc[&local];
1075        self.store_value(alloc_id, view_ty, path, value);
1076    }
1077
1078    /// Assert path conditions and invariant constraints into a solver.
1079    pub(crate) fn assert_all(&self, solver: &z3::Solver<'z3>) {
1080        for cond in &self.constraints.assertions {
1081            solver.assert(cond);
1082        }
1083        let zero = Int::from_u64(self.z3_ctx, 0);
1084        for unit in &self.units {
1085            let alloc = &unit.allocation;
1086            if !alloc.is_external() {
1087                solver.assert(&alloc.base._eq(&zero).not());
1088            }
1089            solver.assert(&alloc.size.ge(&zero));
1090            if alloc.align.simplify().as_u64() != Some(1) {
1091                solver.assert(&alloc.base.rem(&alloc.align)._eq(&zero));
1092            }
1093        }
1094
1095        // Whole values (`path == []`) and field values (`path == [i, ..]`) both
1096        // live in the memory-contents layer.  Field paths exclude the whole
1097        // value (see [`Self::field_paths`]), so assert the whole value and the
1098        // fields separately.
1099        let local_ids: Vec<Local> = self.current_frame.local_alloc.keys().copied().collect();
1100        for local in local_ids {
1101            if let Some(value) = self.local_value(local) {
1102                self.assert_value_constraints(solver, value);
1103            }
1104            for path in self.field_paths(local) {
1105                if let Some(value) = self.field_value(local, &path) {
1106                    self.assert_value_constraints(solver, value);
1107                }
1108            }
1109        }
1110    }
1111
1112    /// Assert a single symbolic value's known invariant constraints.
1113    fn assert_value_constraints(&self, solver: &z3::Solver<'z3>, value: &VmValue<'z3, 'tcx>) {
1114        let zero = Int::from_u64(self.z3_ctx, 0);
1115        if value.facts.non_null {
1116            solver.assert(&value.z3_term._eq(&zero).not());
1117        }
1118        if let Some(ref prov) = value.provenance {
1119            let alloc = self.alloc(prov.alloc_id);
1120            let expected = Int::add(self.z3_ctx, &[&alloc.base, &prov.offset]);
1121            solver.assert(&value.z3_term._eq(&expected));
1122        }
1123        if matches!(
1124            value.ty.kind(),
1125            rustc_middle::ty::TyKind::Uint(_)
1126                | rustc_middle::ty::TyKind::Bool
1127                | rustc_middle::ty::TyKind::Char
1128        ) {
1129            solver.assert(&value.z3_term.ge(&zero));
1130        }
1131        if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Bool) {
1132            let one = Int::from_u64(self.z3_ctx, 1);
1133            solver.assert(&value.z3_term.le(&one));
1134        }
1135        if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Char) {
1136            let max = Int::from_u64(self.z3_ctx, 0x10FFFF);
1137            solver.assert(&value.z3_term.le(&max));
1138        }
1139    }
1140}
1141
1142impl std::fmt::Debug for VmState<'_, '_> {
1143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1144        f.debug_struct("VmState")
1145            .field("locals_count", &self.current_frame.local_alloc.len())
1146            .field("allocations_count", &self.units.len())
1147            .field("assertions", &self.constraints.assertions.len())
1148            .finish()
1149    }
1150}
1151
1152// ── Shared value extraction ──────────────────────────────────────
1153
1154impl<'z3, 'tcx> VmState<'z3, 'tcx> {
1155    /// Extract a VmValue from a MIR operand.
1156    pub(crate) fn value_of_operand(&self, operand: &Operand<'tcx>) -> VmValue<'z3, 'tcx> {
1157        match operand {
1158            Operand::Copy(place) | Operand::Move(place) => self
1159                .value_of_place(place)
1160                .unwrap_or_else(|| self.unknown_value_for_place(place)),
1161            Operand::Constant(constant) => {
1162                let text = format!("{:?}", constant.const_);
1163                // `size_of::<T>()` / `align_of::<T>()` lower to the
1164                // `SizedTypeProperties::SIZE`/`::ALIGN` associated consts for a
1165                // generic `T`.  Bind them to the shared symbolic `sizeof_T` /
1166                // `align_T` (created by `size_sym`/`align_sym` during
1167                // `init_parameters`) so they agree with allocation sizes and
1168                // pointer strides instead of being unrelated fresh constants.
1169                if let rustc_middle::mir::Const::Unevaluated(uneval, _) = constant.const_ {
1170                    let def_name = self.tcx.def_path_str(uneval.def);
1171                    let is_size = def_name.ends_with("SizedTypeProperties::SIZE");
1172                    let is_align = def_name.ends_with("SizedTypeProperties::ALIGN");
1173                    if (is_size || is_align) && !uneval.args.is_empty() {
1174                        let ty = uneval.args.type_at(0);
1175                        let term = if is_size {
1176                            self.size_sym_read(ty)
1177                        } else {
1178                            self.align_sym_read(ty)
1179                        };
1180                        return VmValue::new(term, constant.const_.ty());
1181                    }
1182                }
1183                let int_val = crate::helpers::mir_utils::eval_const_scalar_int(
1184                    self.tcx,
1185                    &constant.const_,
1186                    &text,
1187                );
1188                let field_offset = int_val.is_none()
1189                    && crate::helpers::mir_utils::offset_of_container(self.tcx, &constant.const_)
1190                        .is_some();
1191                let term = if let Some(v) = int_val {
1192                    if v < 0 {
1193                        Int::from_i64(self.z3_ctx, v as i64)
1194                    } else {
1195                        Int::from_u64(self.z3_ctx, v as u64)
1196                    }
1197                } else {
1198                    // Create a deterministic name for const generics so
1199                    // multiple uses of the same parameter share one term.
1200                    let name = format!("const_{}", text.replace([':', '#', ' '], "_"));
1201                    Int::new_const(self.z3_ctx, name.as_str())
1202                };
1203                let ty = constant.const_.ty();
1204                VmValue {
1205                    z3_term: term,
1206                    ty,
1207                    provenance: None,
1208                    facts: ValueFacts::default(),
1209                    source: if field_offset {
1210                        ValueSource::FieldOffset
1211                    } else {
1212                        ValueSource::None
1213                    },
1214                }
1215            }
1216            #[cfg(rapx_ge_95)]
1217            Operand::RuntimeChecks(_) => VmValue::new(
1218                self.fresh_int("runtime_checks"),
1219                self.body().local_decls[Local::from_usize(0)].ty,
1220            ),
1221        }
1222    }
1223
1224    /// Read the byte at `offset` within a `repr(C)`-style ADT from its
1225    /// materialized scalar field values — the field→byte direction of the cast
1226    /// cross-view materialization.  Returns `None` when `offset` does not land
1227    /// inside a concrete scalar field (so the caller falls back to the base).
1228    fn byte_from_field(&self, alloc_id: AllocId, ty: Ty<'tcx>, offset: usize) -> Option<Int<'z3>> {
1229        let rustc_middle::ty::TyKind::Adt(adt_def, substs) = ty.kind() else {
1230            return None;
1231        };
1232        let variant = adt_def.non_enum_variant();
1233        for (idx, field_def) in variant.fields.iter().enumerate() {
1234            let field_ty = crate::helpers::mir_utils::field_ty(self.tcx, field_def, substs);
1235            let field_off = self.field_offset_in_bytes(ty, idx) as usize;
1236            let field_size = self.size_of_ty(field_ty) as usize;
1237            if offset >= field_off && offset < field_off + field_size {
1238                let fv = self.load_value(alloc_id, ty, &[idx])?;
1239                let byte_idx = offset - field_off;
1240                // Fields wider than 8 bytes (e.g. `u128`) cannot be shifted by a
1241                // `u64` divisor; fall back to the base rather than overflow.
1242                if byte_idx >= 8 {
1243                    return None;
1244                }
1245                let divisor = Int::from_u64(self.z3_ctx, 1u64 << (byte_idx * 8));
1246                let modulus = Int::from_u64(self.z3_ctx, 256);
1247                return Some(fv.z3_term.div(&divisor).rem(&modulus));
1248            }
1249        }
1250        None
1251    }
1252
1253    /// Look up the value stored at a MIR place.
1254    pub(crate) fn value_of_place(&self, place: &Place<'tcx>) -> Option<VmValue<'z3, 'tcx>> {
1255        if place.projection.is_empty() {
1256            return self.local_value(place.local).cloned();
1257        }
1258        let place_ty = place.ty(self.body(), self.tcx).ty;
1259
1260        // Collect field indices from projections
1261        let field_path: Vec<usize> = place
1262            .projection
1263            .iter()
1264            .filter_map(|proj| match proj.kind() {
1265                ProjectionElem::Field(field_idx, _) => Some(field_idx.as_usize()),
1266                _ => None,
1267            })
1268            .collect();
1269
1270        // If we have a pure field path (only Field / Downcast projections),
1271        // look up in the per-field value map first.  For `Option`/`ControlFlow`,
1272        // the variant's data is stored under the same field index as the enum
1273        // field (the discriminant is tracked separately, not in the field map),
1274        // so `(x as Some).0` resolves to `x`'s field `[0]`.
1275        let is_pure_field = place.projection.iter().all(|p| {
1276            matches!(
1277                p.kind(),
1278                ProjectionElem::Field(..) | ProjectionElem::Downcast(..)
1279            )
1280        });
1281        let has_downcast = place
1282            .projection
1283            .iter()
1284            .any(|p| matches!(p.kind(), ProjectionElem::Downcast(..)));
1285        if !field_path.is_empty() && is_pure_field {
1286            if let Some(val) = self.field_value(place.local, &field_path).cloned() {
1287                return Some(val);
1288            }
1289            if !has_downcast {
1290                // Fallback: when the base local has provenance, propagate it
1291                // to field accesses. This handles pointer-wrapper types (Box,
1292                // Unique, NonNull) where accessing inner pointer fields yields
1293                // the same provenance as the container.
1294                if let Some(base_val) = self.local_value(place.local) {
1295                    if let Some(ref prov) = base_val.provenance {
1296                        return Some(VmValue {
1297                            z3_term: base_val.z3_term.clone(),
1298                            ty: place_ty,
1299                            provenance: Some(prov.clone()),
1300                            facts: base_val.facts.clone(),
1301                            source: ValueSource::None,
1302                        });
1303                    }
1304                }
1305                return None;
1306            }
1307            // A Downcast without a materialized field falls through to the
1308            // Deref+Field / multi-element fallback below, which returns the
1309            // base local (preserving the pre-Downcast behavior instead of
1310            // forcing a fresh value).
1311        }
1312
1313        // For Deref+Field chains (e.g. (*self).ptr), strip the leading Deref
1314        // projection(s) and look up the field values with the remaining path.
1315        if !field_path.is_empty()
1316            && field_path.len() < place.projection.len()
1317            && place
1318                .projection
1319                .iter()
1320                .any(|p| matches!(p.kind(), ProjectionElem::Deref))
1321        {
1322            // Only Deref and Field projections — all non-Field must be Deref
1323            // (a full-range `Subslice` (`arr[..]`) is transparent: it converts
1324            // `[T; N]` → `[T]` without changing which field is selected, so
1325            // `(*ptr).edges[..]` still resolves to the `edges` field).
1326            let non_field_deref = place.projection.iter().all(|p| {
1327                matches!(
1328                    p.kind(),
1329                    ProjectionElem::Field(..)
1330                        | ProjectionElem::Deref
1331                        | ProjectionElem::Subslice { .. }
1332                )
1333            });
1334            if non_field_deref {
1335                if let Some(val) = self.field_value(place.local, &field_path).cloned() {
1336                    return Some(val);
1337                }
1338                // Resolve a Deref+Field access through the pointee allocation's
1339                // per-allocation field tracking (e.g. `(*leaf).len` → the
1340                // `LeafNode.len` field value materialized by
1341                // `decompose_pointee_fields`).  The viewed type (pointee) is part
1342                // of the key so reinterpret casts (e.g. `LeafNode` → `InternalNode`)
1343                // resolve to the right field view.
1344                if let Some(base_val) = self.local_value(place.local) {
1345                    if let Some(alloc_id) = base_val.provenance_alloc_id() {
1346                        let view_ty = crate::helpers::mir_utils::pointee_ty(base_val.ty)
1347                            .unwrap_or(base_val.ty);
1348                        if let Some(val) = self.load_value(alloc_id, view_ty, &field_path).cloned() {
1349                            return Some(val);
1350                        }
1351                    }
1352                }
1353            }
1354        }
1355
1356        // Handle Deref + Field projections: follow the dereference chain to
1357        // get the pointee base, then apply field offsets.
1358        // E.g. `(*self).ptr` → Deref then Field(0).
1359        let mut base = self.local_value(place.local)?.clone();
1360        for proj in place.projection.iter() {
1361            match proj.kind() {
1362                ProjectionElem::Deref => {
1363                    base.ty = place_ty;
1364                }
1365                ProjectionElem::Field(_field_idx, _) => {
1366                    // Try to get the field value from the VM's field tracking
1367                    if !field_path.is_empty() {
1368                        if let Some(val) = self.field_value(place.local, &field_path).cloned() {
1369                            return Some(val);
1370                        }
1371                    }
1372                    // Fallback: return the base with updated type info
1373                    base.ty = place_ty;
1374                }
1375                _ => {}
1376            }
1377        }
1378
1379        // Fall back to type-level resolution for an Index access whose prefix is
1380        // empty or only Deref projections (`arr[i]` / `(*slice)[i]`).  The
1381        // projection is non-empty here (the empty case returned above).
1382        let proj = place.projection.last().expect("non-empty projection");
1383        let prefix_is_deref = place.projection[..place.projection.len() - 1]
1384            .iter()
1385            .all(|p| matches!(p.kind(), ProjectionElem::Deref));
1386        if let ProjectionElem::Index(local) = proj {
1387            if prefix_is_deref {
1388                if let Some(ref prov) = base.provenance {
1389                    let alloc_id = prov.alloc_id;
1390                    // The base's type may have been overwritten to the
1391                    // element type by the Deref strip above; recover the
1392                    // pointee element type from the base local's declared
1393                    // type (`&[u8]` → `u8`, `&[T; N]` → `T`).
1394                    let decl_ty = self.body().local_decls[place.local].ty;
1395                    let inner_ty = match decl_ty.kind() {
1396                        rustc_middle::ty::TyKind::Array(inner, _) => *inner,
1397                        rustc_middle::ty::TyKind::Ref(_, inner, _) => {
1398                            match inner.kind() {
1399                                rustc_middle::ty::TyKind::Slice(e) => *e,
1400                                _ => return Some(base.clone()),
1401                            }
1402                        }
1403                        rustc_middle::ty::TyKind::Slice(e) => *e,
1404                        _ => return Some(base.clone()),
1405                    };
1406                    let elem_sz = self.size_of_ty(inner_ty) as usize;
1407                    let step = elem_sz.max(1);
1408                    if let Some(index_val) = self.local_value(*local) {
1409                        // `arr[i]` = the byte at `i * size_of(elem)`; the array
1410                        // model resolves symbolic indices via `select` directly.
1411                        let offset = Int::mul(
1412                            self.z3_ctx,
1413                            &[&index_val.z3_term, &Int::from_u64(self.z3_ctx, step as u64)],
1414                        );
1415                        // Byte-level tracking only exists once some byte of the
1416                        // allocation has been written; otherwise fall through to
1417                        // the field→byte materialization below.  Even when the
1418                        // allocation has a byte array, the byte may still read
1419                        // `UNINIT` (e.g. a struct whose fields were written in
1420                        // the value layer only), so also fall through to the
1421                        // field→byte materialization in that case.
1422                        if self.units[alloc_id.0].content.byte_array.is_some() {
1423                            let term = self.byte_read(alloc_id, &offset);
1424                            let is_uninit = offset
1425                                .simplify()
1426                                .as_u64()
1427                                .map(|off| !self.is_byte_init(alloc_id, off as usize))
1428                                .unwrap_or(false);
1429                            if !is_uninit {
1430                                return Some(VmValue {
1431                                    z3_term: term,
1432                                    ty: place_ty,
1433                                    provenance: None,
1434                                    facts: ValueFacts::default(),
1435                                    source: ValueSource::None,
1436                                });
1437                            }
1438                        }
1439                        // Field→byte direction of the cast cross-view
1440                        // materialization: the buffer was reinterpreted from a
1441                        // struct whose scalar fields were written in the value
1442                        // layer, so read the byte back out of the field value.
1443                        if let Some(off) = offset.simplify().as_u64() {
1444                            if let Some(ty) = self.alloc(alloc_id).element_ty.as_ty() {
1445                                if let Some(b) = self.byte_from_field(alloc_id, ty, off as usize) {
1446                                    return Some(VmValue {
1447                                        z3_term: b,
1448                                        ty: place_ty,
1449                                        provenance: None,
1450                                        facts: ValueFacts::default(),
1451                                        source: ValueSource::None,
1452                                    });
1453                                }
1454                            }
1455                        }
1456                    }
1457                }
1458            }
1459            return Some(base.clone());
1460        }
1461        // Any other trailing projection (Deref/Field/Downcast/…): the loop above
1462        // already traced Deref/Field and set `base.ty`, so return the base with
1463        // the place type to propagate provenance.
1464        let mut val = base;
1465        val.ty = place_ty;
1466        Some(val)
1467    }
1468
1469    /// Create an unknown value for a place.
1470    ///
1471    /// The value carries no `non_null` (or any other) assumption: a raw pointer
1472    /// whose provenance was lost may still be null, so assuming non-null here
1473    /// would let `NonNull`/null-guard checks pass unsoundly.
1474    pub(crate) fn unknown_value_for_place(&self, place: &Place<'tcx>) -> VmValue<'z3, 'tcx> {
1475        let ty = place.ty(self.body(), self.tcx).ty;
1476        VmValue::new(self.fresh_int("unknown"), ty)
1477    }
1478}