Skip to main content

rapx/verify/vm/
state.rs

1//! Symbolic VM state types.
2//!
3//! Core data structures that represent the symbolic execution state:
4//! `VmValue` (symbolic value with invariants), `Allocation` (memory object),
5//! and `VmState` (the full execution state at a program point).
6
7use rustc_hir::def_id::DefId;
8use rustc_middle::{
9    mir::{BasicBlock, Body, Local, Operand, Place, ProjectionElem},
10    ty::{Ty, TyCtxt},
11};
12use z3::{
13    Context,
14    ast::{Ast, Bool, Int},
15};
16
17use crate::compat::{FxHashMap, FxHashSet};
18use crate::verify::{
19    def_use::PlaceKey,
20    path_extractor::Path,
21};
22
23/// Unique identifier for a heap or stack allocation.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub struct AllocId(pub usize);
26
27/// Pointer provenance: which allocation and at what byte offset.
28#[derive(Clone, Debug)]
29pub struct Provenance<'ctx> {
30    /// The allocation this pointer derives from.
31    pub alloc_id: AllocId,
32    /// Byte offset from the allocation base. A freshly created
33    /// pointer to the base of an allocation has `offset = 0`.
34    pub offset: Int<'ctx>,
35    /// Whether `offset` is a compile-time field offset (`offset_of!`).  Such an
36    /// offset always satisfies `0 <= offset` and `offset + size_of(field) <=
37    /// size_of(container)`, which the verifier uses to discharge in-bounds
38    /// checks for patterns like `Option::as_slice`.
39    pub is_field_offset: bool,
40}
41
42/// Known invariants about a symbolic value.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub struct ValueInvariants {
45    pub non_null: bool,
46    pub aligned: bool,
47    pub init: bool,
48    pub in_bounds: bool,
49    /// If Some(n), the value's term is known to satisfy `term % n == 0`.
50    /// Set by alignment guards, Mul by power-of-two, and type alignment.
51    pub align_n: Option<u64>,
52    /// Whether this scalar value is a compile-time field offset (`offset_of!`).
53    /// Propagated to a pointer's provenance when used as an `add`/`byte_add`
54    /// offset.
55    pub is_field_offset: bool,
56}
57
58/// A symbolic value tracked by the VM.
59///
60/// # Semantics of `term`
61///
62/// - For pointer/reference types (`&T`, `*const T`, `*mut T`, `Box<T>`, etc.):
63///   `term` represents the **address** in the VM's logical address space.
64/// - For scalar types (integers, `bool`, `char`): `term` represents the **value**.
65/// - For aggregate types (struct, tuple, enum): `term` is the base address of
66///   the stack allocation backing the aggregate.
67///
68/// When `provenance` is `Some`, the following relationship holds and is
69/// asserted into the solver at check time:
70///   `term == alloc[provenance.alloc_id].base + provenance.offset`
71#[derive(Clone, Debug)]
72pub struct VmValue<'ctx, 'tcx> {
73    /// The Z3 integer term (address or scalar value, see struct docs).
74    pub term: Int<'ctx>,
75    /// Rust type, for layout queries.
76    pub ty: Ty<'tcx>,
77    /// Which allocation this pointer derives from and at what offset.
78    pub provenance: Option<Provenance<'ctx>>,
79    /// Known constraints on this value.
80    pub invariants: ValueInvariants,
81}
82
83impl<'ctx, 'tcx> VmValue<'ctx, 'tcx> {
84    pub fn new(term: Int<'ctx>, ty: Ty<'tcx>) -> Self {
85        VmValue { term, ty, provenance: None, invariants: ValueInvariants::default() }
86    }
87
88    /// Convenience: extract the `AllocId` from provenance, if any.
89    pub fn provenance_alloc_id(&self) -> Option<AllocId> {
90        self.provenance.as_ref().map(|p| p.alloc_id)
91    }
92}
93
94/// A memory allocation (stack or heap).
95///
96/// The allocation is stored in `VmState::allocations` at index `AllocId.0`
97/// (an `AllocId` is a monotonic counter that doubles as the vector index).
98#[derive(Clone, Debug)]
99pub struct Allocation<'ctx, 'tcx> {
100    /// Base address (fresh Z3 constant).
101    pub base: Int<'ctx>,
102
103    /// Size in bytes (Z3 term, may be symbolic).
104    pub size: Int<'ctx>,
105
106    /// Alignment in bytes.
107    pub align: u64,
108
109    /// Element type for bounds checking.
110    pub element_ty: Option<Ty<'tcx>>,
111
112    /// True if this allocation models an external raw-pointer parameter
113    /// whose exact size and nullability are unknown.
114    pub is_external: bool,
115
116    /// Allocations that have been freed (StorageDead, Drop).
117    pub dead: bool,
118
119    /// Allocations that have been written to (initialized via write/MaybeUninit).
120    pub initialized: bool,
121
122    /// Allocations assumed alive via contract (e.g. `#[rapx::requires(Alive(ptr))]`).
123    pub alive_assumed: bool,
124
125    /// Allocations known to be a null-terminated byte buffer (a valid C
126    /// string), asserted via a `ValidCStr` contract fact or struct invariant.
127    pub nul_terminated: bool,
128
129    /// Parent allocation for sub-allocations created by split_at / from_raw_parts.
130    pub parent: Option<AllocId>,
131
132    /// Slice data allocation: for a `&[T]` reference's stack allocation, the
133    /// symbolic data allocation created for the slice contents.
134    pub slice_data: Option<AllocId>,
135}
136
137/// One-shot execution/contract flags accumulated while stepping a path.
138#[derive(Clone, Copy, Debug, Default)]
139pub(crate) struct ContractFlags {
140    /// Whether a SplitTransmute contract was asserted by the caller.
141    pub split_transmute_asserted: bool,
142    /// Whether an `Alias` hazard was accepted via the caller's contract.
143    pub alias_hazard_accepted: bool,
144    /// Whether a ChecksIndexBoundsDisjoint call was processed in any
145    /// checkpoint of this function (accumulated across checkpoints).
146    pub has_checked_bounds: bool,
147    /// Set once the path evaluated an `Iterator::next` discriminant whose
148    /// variant was known symbolically.
149    pub saw_next_discriminant: bool,
150}
151
152/// Per-byte symbolic state at a concrete offset in an allocation.
153#[derive(Clone, Debug, Default)]
154pub(crate) struct ByteInfo<'ctx> {
155    /// Symbolic value, if tracked.
156    pub value: Option<Int<'ctx>>,
157    /// Whether the byte has been explicitly written.
158    pub init: bool,
159    /// NUL knowledge: `Some(true)` known NUL, `Some(false)` known non-NUL.
160    pub nul: Option<bool>,
161}
162
163/// A saved caller context pushed during cross-function inline.  Nested
164/// inlining (a callee that itself inlines another callee) pushes multiple
165/// frames; a single `Option` slot would clobber the outer caller's context on
166/// the inner exit, so a stack is required.
167pub(crate) struct InlineFrame<'ctx, 'tcx> {
168    pub body: &'ctx Body<'tcx>,
169    pub def_id: DefId,
170    pub saved_locals: FxHashMap<Local, VmValue<'ctx, 'tcx>>,
171}
172
173/// The full symbolic execution state at a program point.
174///
175/// Accumulates locals, allocations, path conditions, and definitions
176/// as the VM steps through retained MIR items. The Z3 context is
177/// borrowed so a single context can be reused across property checks.
178pub struct VmState<'ctx, 'tcx> {
179    /// Shared Z3 context.
180    pub(crate) ctx: &'ctx Context,
181
182    /// Compiler type context.
183    pub(crate) tcx: TyCtxt<'tcx>,
184
185    /// The DefId of the function whose body we are executing.
186    pub(crate) caller_def_id: DefId,
187
188    /// The MIR body being executed.
189    pub(crate) body: &'ctx Body<'tcx>,
190
191    /// Current value bound to each MIR local.
192    pub(crate) locals: FxHashMap<Local, VmValue<'ctx, 'tcx>>,
193
194    /// Known address for each stack-allocated local.
195    pub(crate) local_addresses: FxHashMap<Local, Int<'ctx>>,
196
197    /// Allocation ID for each stack-allocated local.
198    pub(crate) local_alloc_ids: FxHashMap<Local, AllocId>,
199
200    /// All known allocations.
201    pub(crate) allocations: Vec<Allocation<'ctx, 'tcx>>,
202
203    /// Accumulated path conditions (SwitchInt branches, Assert).
204    pub(crate) path_conditions: Vec<Bool<'ctx>>,
205
206    /// Monotonic counter used to uniquify fresh symbolic constant names.
207    pub(crate) definition_count: usize,
208
209    /// The next allocation ID.
210    pub(crate) next_alloc_id: usize,
211
212    /// Track block occurrence counts for loop-carried value indexing.
213    pub(crate) block_occurrences: FxHashMap<BasicBlock, usize>,
214
215    /// Binary op sources for guard inference: destination → (lhs, rhs) place keys.
216    pub(crate) binary_op_sources: FxHashMap<PlaceKey, (Option<PlaceKey>, Option<PlaceKey>)>,
217
218    /// Direct boolean condition for a comparison result place (Le/Lt/Ge/Gt/Eq/Ne),
219    /// used to record precise switch-guard path conditions.
220    pub(crate) comparison_conds: FxHashMap<PlaceKey, Bool<'ctx>>,
221
222    /// Enum discriminant term for a local holding an `Option`-like value whose
223    /// variant is known symbolically (e.g. `Iterator::next` returns
224    /// `Some(x) iff !is_empty`). Used by `Rvalue::Discriminant` so `switchInt`
225    /// branches stay tied to the actual emptiness condition.
226    pub(crate) discriminant_terms: FxHashMap<Local, Int<'ctx>>,
227
228    /// Non-binary-op sources (select_unpredictable, etc.): destination → (lhs, rhs)
229    /// place keys.  Kept separately from `binary_op_sources` so guard inference
230    /// (infer_guard_non_null) does not treat these as pointer comparisons.
231    pub(crate) other_op_sources: FxHashMap<PlaceKey, (Option<PlaceKey>, Option<PlaceKey>)>,
232
233    /// One-shot execution/contract flags accumulated while stepping a path.
234    pub(crate) contract_flags: ContractFlags,
235
236    /// Field-level value tracking for aggregates: (local, field_indices) → value.
237    /// Example: `(local_3, [0])` is `local_3.0`, `(local_3, [0, 1])` is `local_3.0.1`.
238    pub(crate) field_values: FxHashMap<(Local, Vec<usize>), VmValue<'ctx, 'tcx>>,
239
240    /// Locals set by `iterpreter_iter_is_empty` for Iter/IterMut,
241    /// along with the field-based len expression. When a switchint
242    /// on such local takes the false (!is_empty) branch, we inject
243    /// `len >= 1` as a path condition to help Z3.
244    pub(crate) is_empty_len: FxHashMap<Local, Int<'ctx>>,
245
246    /// Cumulative ptr offset for Iter/IterMut field [0] (ptr).
247    /// Key: (struct_local). When post_inc_start advances the ptr by
248    /// `n` elements, we increment this offset instead of nesting
249    /// symbolic additions. This keeps Z3 expressions compact.
250    pub(crate) iter_ptr_offset: FxHashMap<Local, Int<'ctx>>,
251
252    /// Per-byte symbolic state: (alloc_id, concrete_byte_offset) → ByteInfo.
253    /// Populated by aggregate initialisation, pointer stores, and write call
254    /// effects. Enables byte-level reasoning for properties like ValidCStr.
255    pub(crate) bytes: FxHashMap<(AllocId, usize), ByteInfo<'ctx>>,
256
257    /// Notes from unsupported operations.
258    pub(crate) notes: Vec<String>,
259
260    /// The path being executed (for branch target resolution).
261    pub(crate) path: Option<Path>,
262
263    /// Name of the most recent call (for context-aware effects like Vec push).
264    pub(crate) last_call_name: String,
265
266    /// Current depth of the recursive `exec_inline_call` stack.  `exec_call`
267    /// re-enters inline execution with `depth = 0` on every nested call, so a
268    /// separate counter (instead of the `depth` argument) is needed to actually
269    /// bound nested inlining and avoid unbounded recursion / stack overflow.
270    pub(crate) inline_depth: usize,
271
272    /// Stack of saved caller contexts for cross-function inline.
273    /// The top of the stack is the frame of the function currently being
274    /// inlined; each entry carries the caller's body, def-id, and locals so
275    /// the caller can be restored on exit.
276    pub(crate) inline_frames: Vec<InlineFrame<'ctx, 'tcx>>,
277
278    /// Terms that are the result of a bitwise `Not` (two's-complement mask).
279    /// Used to recognize `x & !(align-1)` alignment patterns in BitAnd so we
280    /// can derive `align = -mask` and emit linear bounds for the result.
281    pub(crate) not_mask_terms: FxHashSet<Int<'ctx>>,
282}
283
284impl<'ctx, 'tcx> VmState<'ctx, 'tcx> {
285    /// Create a fresh VM state for executing a path.
286    pub fn new(
287        ctx: &'ctx Context,
288        tcx: TyCtxt<'tcx>,
289        body: &'ctx Body<'tcx>,
290        caller_def_id: DefId,
291    ) -> Self {
292        Self {
293            ctx,
294            tcx,
295            body,
296            caller_def_id,
297            locals: FxHashMap::default(),
298            local_addresses: FxHashMap::default(),
299            local_alloc_ids: FxHashMap::default(),
300            allocations: Vec::new(),
301            path_conditions: Vec::new(),
302            definition_count: 0,
303            next_alloc_id: 0,
304            block_occurrences: FxHashMap::default(),
305            binary_op_sources: FxHashMap::default(),
306            comparison_conds: FxHashMap::default(),
307            discriminant_terms: FxHashMap::default(),
308            other_op_sources: FxHashMap::default(),
309            contract_flags: ContractFlags::default(),
310            field_values: FxHashMap::default(),
311            is_empty_len: FxHashMap::default(),
312            iter_ptr_offset: FxHashMap::default(),
313            bytes: FxHashMap::default(),
314            notes: Vec::new(),
315            path: None,
316            last_call_name: String::new(),
317            inline_depth: 0,
318            inline_frames: Vec::new(),
319            not_mask_terms: FxHashSet::default(),
320        }
321    }
322
323    /// Look up the value bound to a MIR local.
324    pub fn local_value(&self, local: Local) -> Option<&VmValue<'ctx, 'tcx>> {
325        self.locals.get(&local)
326    }
327
328    /// Bind a value to a MIR local.
329    pub fn set_local(&mut self, local: Local, value: VmValue<'ctx, 'tcx>) {
330        self.locals.insert(local, value);
331    }
332
333    /// Get or create the symbolic address of a MIR local.
334    pub fn local_address(&mut self, local: Local) -> Int<'ctx> {
335        if let Some(addr) = self.local_addresses.get(&local) {
336            return addr.clone();
337        }
338        let name = format!("addr__{}", local.as_usize());
339        let addr = Int::new_const(self.ctx, name.as_str());
340        self.local_addresses.insert(local, addr.clone());
341        addr
342    }
343
344    /// Allocate a fresh symbolic object and return its ID and base address.
345    pub fn allocate(
346        &mut self,
347        size: Int<'ctx>,
348        align: u64,
349        element_ty: Option<Ty<'tcx>>,
350    ) -> (AllocId, Int<'ctx>) {
351        let id = AllocId(self.next_alloc_id);
352        self.next_alloc_id += 1;
353        let base = {
354            let name = format!("heap_{}", id.0);
355            Int::new_const(self.ctx, name.as_str())
356        };
357        let alloc = Allocation {
358            base: base.clone(),
359            size,
360            align,
361            element_ty,
362            is_external: false,
363            dead: false,
364            initialized: false,
365            alive_assumed: false,
366            nul_terminated: false,
367            parent: None,
368            slice_data: None,
369        };
370        self.allocations.push(alloc);
371        (id, base)
372    }
373
374    /// Allocate a fresh external allocation (for raw-pointer parameters).
375    /// External allocations may be null and have unlimited size.
376    pub fn allocate_external(
377        &mut self,
378        size: Int<'ctx>,
379        align: u64,
380        element_ty: Option<Ty<'tcx>>,
381    ) -> (AllocId, Int<'ctx>) {
382        let id = AllocId(self.next_alloc_id);
383        self.next_alloc_id += 1;
384        let base = {
385            let name = format!("ext_{}", id.0);
386            Int::new_const(self.ctx, name.as_str())
387        };
388        let alloc = Allocation {
389            base: base.clone(),
390            size,
391            align,
392            element_ty,
393            is_external: true,
394            dead: false,
395            initialized: false,
396            alive_assumed: false,
397            nul_terminated: false,
398            parent: None,
399            slice_data: None,
400        };
401        self.allocations.push(alloc);
402        (id, base)
403    }
404
405    /// Indexed access to an allocation by its `AllocId` (the id is the index).
406    pub(crate) fn alloc(&self, id: AllocId) -> &Allocation<'ctx, 'tcx> {
407        &self.allocations[id.0]
408    }
409
410    /// Mutable indexed access to an allocation by its `AllocId`.
411    pub(crate) fn alloc_mut(&mut self, id: AllocId) -> &mut Allocation<'ctx, 'tcx> {
412        &mut self.allocations[id.0]
413    }
414
415    /// Create a symbolic Z3 int constant.
416    pub fn fresh_int(&self, prefix: &str) -> Int<'ctx> {
417        let name = format!("{}_{}", prefix, self.definition_count);
418        Int::new_const(self.ctx, name.as_str())
419    }
420
421    /// Bump the symbolic-name uniquifier (called once per executed assignment).
422    pub fn record_definition(&mut self) {
423        self.definition_count += 1;
424    }
425
426    /// Get the value of a specific field within an aggregate local.
427    pub fn field_value(&self, local: Local, path: &[usize]) -> Option<&VmValue<'ctx, 'tcx>> {
428        self.field_values.get(&(local, path.to_vec()))
429    }
430
431    /// Set the value of a specific field within an aggregate local.
432    pub fn set_field_value(&mut self, local: Local, path: Vec<usize>, value: VmValue<'ctx, 'tcx>) {
433        self.field_values.insert((local, path), value);
434    }
435
436    /// Record a per-byte symbolic value at a concrete offset in an allocation.
437    pub fn record_byte_value(&mut self, alloc_id: AllocId, offset: usize, term: Int<'ctx>) {
438        let byte = self.bytes.entry((alloc_id, offset)).or_default();
439        byte.value = Some(term);
440        byte.init = true;
441    }
442
443    /// Mark a byte as initialized without changing its value.
444    pub fn mark_byte_init(&mut self, alloc_id: AllocId, offset: usize) {
445        self.bytes.entry((alloc_id, offset)).or_default().init = true;
446    }
447
448    /// Mark a byte as known NUL (0x00).
449    pub fn mark_byte_nul(&mut self, alloc_id: AllocId, offset: usize) {
450        self.bytes.entry((alloc_id, offset)).or_default().nul = Some(true);
451    }
452
453    /// Mark a byte as known non-NUL (!= 0x00).
454    pub fn mark_byte_non_nul(&mut self, alloc_id: AllocId, offset: usize) {
455        self.bytes.entry((alloc_id, offset)).or_default().nul = Some(false);
456    }
457
458    /// Look up a per-byte Z3 term for a concrete offset in an allocation.
459    pub fn get_byte_value(&self, alloc_id: AllocId, offset: usize) -> Option<&Int<'ctx>> {
460        self.bytes.get(&(alloc_id, offset)).and_then(|b| b.value.as_ref())
461    }
462
463    /// Check whether a byte at a concrete offset is known to be initialized.
464    pub fn is_byte_init(&self, alloc_id: AllocId, offset: usize) -> bool {
465        self.bytes.get(&(alloc_id, offset)).is_some_and(|b| b.init)
466    }
467
468    /// Check whether a byte at a concrete offset is known to be NUL.
469    pub fn is_byte_nul(&self, alloc_id: AllocId, offset: usize) -> bool {
470        self.bytes.get(&(alloc_id, offset)).is_some_and(|b| b.nul == Some(true))
471    }
472
473    /// Check whether a byte at a concrete offset is known to be non-NUL.
474    pub fn is_byte_non_nul(&self, alloc_id: AllocId, offset: usize) -> bool {
475        self.bytes.get(&(alloc_id, offset)).is_some_and(|b| b.nul == Some(false))
476    }
477
478    /// Return all known (offset, term) pairs for an allocation, sorted by offset.
479    pub fn alloc_byte_values(&self, alloc_id: AllocId) -> Vec<(usize, &Int<'ctx>)> {
480        let mut pairs: Vec<_> = self
481            .bytes
482            .iter()
483            .filter_map(|((aid, off), byte)| {
484                if *aid == alloc_id { byte.value.as_ref().map(|term| (*off, term)) } else { None }
485            })
486            .collect();
487        pairs.sort_by_key(|(off, _)| *off);
488        pairs
489    }
490
491    /// Collect all offsets known to be NUL in an allocation.
492    pub fn alloc_nul_offsets(&self, alloc_id: AllocId) -> Vec<usize> {
493        self.bytes.iter()
494            .filter_map(|((aid, off), byte)| {
495                if *aid == alloc_id && byte.nul == Some(true) { Some(*off) } else { None }
496            })
497            .collect()
498    }
499
500    /// Collect all offsets known to be non-NUL in an allocation.
501    pub fn alloc_non_nul_offsets(&self, alloc_id: AllocId) -> Vec<usize> {
502        self.bytes.iter()
503            .filter_map(|((aid, off), byte)| {
504                if *aid == alloc_id && byte.nul == Some(false) { Some(*off) } else { None }
505            })
506            .collect()
507    }
508
509    /// Copy all per-byte tracking (value, init, NUL knowledge) from one
510    /// allocation to another.
511    pub(crate) fn copy_byte_tracking(&mut self, src: AllocId, dst: AllocId) {
512        let infos: Vec<(usize, ByteInfo<'ctx>)> = self.bytes.iter()
513            .filter(|((aid, _), _)| *aid == src)
514            .map(|((_, off), byte)| (*off, byte.clone()))
515            .collect();
516        for (off, byte) in infos {
517            self.bytes.insert((dst, off), byte);
518        }
519    }
520
521    /// Assert path conditions and invariant constraints into a solver.
522    pub fn assert_all(&self, solver: &z3::Solver<'ctx>) {
523        for cond in &self.path_conditions {
524            solver.assert(cond);
525        }
526        let zero = Int::from_u64(self.ctx, 0);
527        for alloc in &self.allocations {
528            if !alloc.is_external {
529                solver.assert(&alloc.base._eq(&zero).not());
530            }
531            solver.assert(&alloc.size.ge(&zero));
532            if alloc.align > 1 {
533                let align_term = Int::from_u64(self.ctx, alloc.align);
534                solver.assert(&alloc.base.rem(&align_term)._eq(&zero));
535            }
536        }
537
538        for (_local, value) in self.locals.iter() {
539            if value.invariants.non_null {
540                solver.assert(&value.term._eq(&zero).not());
541            }
542            if let Some(ref prov) = value.provenance {
543                let alloc = self.alloc(prov.alloc_id);
544                let expected = Int::add(self.ctx, &[&alloc.base, &prov.offset]);
545                solver.assert(&value.term._eq(&expected));
546            }
547            if matches!(value.ty.kind(),
548                rustc_middle::ty::TyKind::Uint(_)
549                | rustc_middle::ty::TyKind::Bool
550                | rustc_middle::ty::TyKind::Char
551            ) {
552                solver.assert(&value.term.ge(&zero));
553            }
554            if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Bool) {
555                let one = Int::from_u64(self.ctx, 1);
556                solver.assert(&value.term.le(&one));
557            }
558            if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Char) {
559                let max = Int::from_u64(self.ctx, 0x10FFFF);
560                solver.assert(&value.term.le(&max));
561            }
562        }
563        for value in self.field_values.values() {
564            if value.invariants.non_null {
565                solver.assert(&value.term._eq(&zero).not());
566            }
567            if let Some(ref prov) = value.provenance {
568                let alloc = self.alloc(prov.alloc_id);
569                let expected = Int::add(self.ctx, &[&alloc.base, &prov.offset]);
570                solver.assert(&value.term._eq(&expected));
571            }
572            if matches!(value.ty.kind(),
573                rustc_middle::ty::TyKind::Uint(_)
574                | rustc_middle::ty::TyKind::Bool
575                | rustc_middle::ty::TyKind::Char
576            ) {
577                solver.assert(&value.term.ge(&zero));
578            }
579            if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Bool) {
580                let one = Int::from_u64(self.ctx, 1);
581                solver.assert(&value.term.le(&one));
582            }
583            if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Char) {
584                let max = Int::from_u64(self.ctx, 0x10FFFF);
585                solver.assert(&value.term.le(&max));
586            }
587        }
588    }
589}
590
591impl std::fmt::Debug for VmState<'_, '_> {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        f.debug_struct("VmState")
594            .field("locals_count", &self.locals.len())
595            .field("allocations_count", &self.allocations.len())
596            .field("path_conditions", &self.path_conditions.len())
597            .field("definitions", &self.definition_count)
598            .field("notes", &self.notes)
599            .finish()
600    }
601}
602
603// ── Shared value extraction ──────────────────────────────────────
604
605impl<'ctx, 'tcx> VmState<'ctx, 'tcx> {
606    /// Extract a VmValue from a MIR operand.
607    pub(crate) fn value_of_operand(&self, operand: &Operand<'tcx>) -> VmValue<'ctx, 'tcx> {
608        match operand {
609            Operand::Copy(place) | Operand::Move(place) => {
610                self.value_of_place(place)
611                    .unwrap_or_else(|| self.unknown_value_for_place(place))
612            }
613            Operand::Constant(constant) => {
614                let text = format!("{:?}", constant.const_);
615                let int_val = crate::helpers::mir_utils::const_scalar_int(self.tcx, &constant.const_, &text);
616                let is_field_offset = int_val.is_none()
617                    && crate::helpers::mir_utils::offset_of_container(self.tcx, &constant.const_)
618                        .is_some();
619                let term = if let Some(v) = int_val {
620                    if v < 0 {
621                        Int::from_i64(self.ctx, v as i64)
622                    } else {
623                        Int::from_u64(self.ctx, v as u64)
624                    }
625                } else {
626                    // Create a deterministic name for const generics so
627                    // multiple uses of the same parameter share one term.
628                    let name = format!("const_{}", text.replace([':', '#', ' '], "_"));
629                    Int::new_const(self.ctx, name.as_str())
630                };
631                let ty = constant.const_.ty();
632                VmValue {
633                    term,
634                    ty,
635                    provenance: None,
636                    invariants: ValueInvariants {
637                        is_field_offset,
638                        ..ValueInvariants::default()
639                    },
640                }
641            }
642            #[cfg(rapx_ge_99)]
643            Operand::RuntimeChecks(_) => {
644                VmValue::new(self.fresh_int("runtime_checks"), self.body.local_decls[Local::from_usize(0)].ty)
645            }
646        }
647    }
648
649    /// Look up the value stored at a MIR place.
650    pub(crate) fn value_of_place(&self, place: &Place<'tcx>) -> Option<VmValue<'ctx, 'tcx>> {
651        if place.projection.is_empty() {
652            return self.locals.get(&place.local).cloned();
653        }
654
655        // Collect field indices from projections
656        let field_path: Vec<usize> = place.projection.iter()
657            .filter_map(|proj| match proj.kind() {
658                ProjectionElem::Field(field_idx, _) => Some(field_idx.as_usize()),
659                _ => None,
660            })
661            .collect();
662
663        // If we have a pure field path (only Field projections), look up
664        // in the per-field value map first.
665        if !field_path.is_empty() && field_path.len() == place.projection.len() {
666            if let Some(val) = self.field_values.get(&(place.local, field_path)).cloned() {
667                return Some(val);
668            }
669            // Fallback: when the base local has provenance, propagate it
670            // to field accesses. This handles pointer-wrapper types (Box,
671            // Unique, NonNull) where accessing inner pointer fields yields
672            // the same provenance as the container.
673            if let Some(base_val) = self.locals.get(&place.local) {
674                if let Some(ref prov) = base_val.provenance {
675                    return Some(VmValue {
676                        term: base_val.term.clone(),
677                        ty: place.ty(self.body, self.tcx).ty,
678                        provenance: Some(prov.clone()),
679                        invariants: base_val.invariants,
680                    });
681                }
682            }
683            return None;
684        }
685
686        // For Deref+Field chains (e.g. (*self).ptr), strip the leading Deref
687        // projection(s) and look up field_values with the remaining field path.
688        if !field_path.is_empty() && field_path.len() < place.projection.len()
689            && place.projection.iter().any(|p| matches!(p.kind(), ProjectionElem::Deref))
690        {
691            // Only Deref and Field projections — all non-Field must be Deref.
692            let non_field_deref = place.projection.iter()
693                .all(|p| matches!(p.kind(), ProjectionElem::Field(..) | ProjectionElem::Deref));
694            if non_field_deref {
695                // Recompute field_path since the original was moved.
696                let fp: Vec<usize> = place.projection.iter()
697                    .filter_map(|proj| match proj.kind() {
698                        ProjectionElem::Field(field_idx, _) => Some(field_idx.as_usize()),
699                        _ => None,
700                    })
701                    .collect();
702                if let Some(val) = self.field_values.get(&(place.local, fp)).cloned() {
703                    return Some(val);
704                }
705            }
706        }
707
708        // Handle Deref + Field projections: follow the dereference chain to
709        // get the pointee base, then apply field offsets.
710        // E.g. `(*self).ptr` → Deref then Field(0).
711        let mut base = self.locals.get(&place.local)?.clone();
712        for proj in place.projection.iter() {
713            match proj.kind() {
714                ProjectionElem::Deref => {
715                    let _ = &base.provenance; // prov reference not yet used for value_of_place
716                    base.ty = place.ty(self.body, self.tcx).ty;
717                }
718                ProjectionElem::Field(_field_idx, _) => {
719                    // Try to get the field value from the VM's field tracking
720                    let field_indices: Vec<usize> = place.projection.iter()
721                        .filter_map(|p| match p.kind() {
722                            ProjectionElem::Field(fi, _) => Some(fi.as_usize()),
723                            _ => None,
724                        })
725                        .collect();
726                    if !field_indices.is_empty() {
727                        if let Some(val) = self.field_values.get(&(place.local, field_indices)).cloned() {
728                            return Some(val);
729                        }
730                    }
731                    // Fallback: return the base with updated type info
732                    base.ty = place.ty(self.body, self.tcx).ty;
733                }
734                _ => {}
735            }
736        }
737
738        // Fall back to type-level resolution with single-element projections
739        if place.projection.len() == 1 {
740            if let Some(proj) = place.projection.first() {
741                match proj {
742                    ProjectionElem::Index(local) => {
743                        if let Some(ref prov) = base.provenance {
744                            let alloc_id = prov.alloc_id;
745                            let byte_vals: Vec<_> = self.alloc_byte_values(alloc_id);
746                            if !byte_vals.is_empty() {
747                                let inner_ty = match base.ty.kind() {
748                                    rustc_middle::ty::TyKind::Array(inner, _) => *inner,
749                                    _ => return Some(base.clone()),
750                                };
751                                let elem_sz = self.size_of_ty(inner_ty) as usize;
752                                let step = elem_sz.max(1);
753                                if let Some(index_val) = self.locals.get(local) {
754                                    if let Some(concrete_idx) = index_val.term.as_u64() {
755                                        let offset = concrete_idx as usize * step;
756                                        let term = self
757                                            .get_byte_value(alloc_id, offset)
758                                            .cloned()
759                                            .unwrap_or_else(|| self.fresh_int("arr_elem"));
760                                        return Some(VmValue {
761                                            term,
762                                            ty: place.ty(self.body, self.tcx).ty,
763                                            provenance: None,
764                                            invariants: ValueInvariants::default(),
765                                        });
766                                    } else {
767                                        let mut chain = self.fresh_int("arr_elem");
768                                        for (offset, term) in byte_vals.iter().rev() {
769                                            let vidx = offset / step;
770                                            let idx_term = Int::from_u64(self.ctx, vidx as u64);
771                                            let cond = index_val.term._eq(&idx_term);
772                                            chain = Bool::ite(&cond, term, &chain);
773                                        }
774                                        return Some(VmValue {
775                                            term: chain,
776                                            ty: place.ty(self.body, self.tcx).ty,
777                                            provenance: None,
778                                            invariants: ValueInvariants::default(),
779                                        });
780                                    }
781                                }
782                            }
783                        }
784                        return Some(base.clone());
785                    }
786                    _ => {}
787                }
788                match proj.kind() {
789                    ProjectionElem::Deref => {
790                        let mut val = base.clone();
791                        val.ty = place.ty(self.body, self.tcx).ty;
792                        return Some(val);
793                    }
794                    ProjectionElem::Field(_field_idx, _field_ty) => {
795                        let val = base.clone();
796                        return Some(val);
797                    }
798                _ => {
799                    // Downcast or other unsupported projection: still return
800                    // the base with updated type so provenance propagates.
801                    let mut val = base.clone();
802                    val.ty = place.ty(self.body, self.tcx).ty;
803                    return Some(val);
804                }
805                }
806            }
807        }
808
809        // For multi-element projections with Deref+Field or Downcast, return
810        // the base value since we already traced through Deref above.
811        if place.projection.len() > 1
812            && place.projection.iter().any(|p| matches!(
813                p.kind(), ProjectionElem::Deref | ProjectionElem::Downcast(..)
814            ))
815        {
816            let mut val = base;
817            val.ty = place.ty(self.body, self.tcx).ty;
818            return Some(val);
819        }
820
821        None
822    }
823
824    /// Create an unknown value for a place.
825    pub(crate) fn unknown_value_for_place(&self, place: &Place<'tcx>) -> VmValue<'ctx, 'tcx> {
826        let ty = place.ty(self.body, self.tcx).ty;
827        let is_raw_ptr = matches!(ty.kind(), rustc_middle::ty::TyKind::RawPtr(..));
828        VmValue {
829            term: self.fresh_int("unknown"),
830            ty,
831            provenance: None,
832            invariants: ValueInvariants {
833                non_null: is_raw_ptr,
834                ..Default::default()
835            },
836        }
837    }
838}
839