llvm-native-core-ext 0.1.0

Extended modules for llvm-native-core: analysis passes, transforms, codegen extras, bitcode, linker, JIT, utilities. Part of the llvm-native workspace (https://crates.io/crates/llvm-native).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
//! Assumption Cache
//!
//! Tracks assumptions generated by `llvm.assume` intrinsics and other
//! sources, providing quick lookup of which assumptions apply to a
//! given value.
//!
//! Assumptions carry information such as:
//! - Non-null pointers.
//! - Alignment constraints.
//! - Known bits / value ranges.
//! - Dominating conditions.
//!
//! The cache is invalidated when the IR changes.
//!
//! Clean-room implementation from LLVM assumption-cache design.
//! No LLVM C++ source consulted.

use llvm_native_core::value::ValueRef;
use std::collections::HashMap;

/// Caches assumptions for a function and provides efficient lookups.
pub struct AssumptionCache {
    /// Map from assumption producer (call instruction) to its assumptions.
    assumptions: HashMap<u64, Vec<ValueRef>>,
    /// Map from a value to the list of assumptions that apply to it.
    value_to_assumptions: HashMap<u64, Vec<ValueRef>>,
    /// The function this cache is tracking.
    func_vid: u64,
    /// Whether the cache is valid.
    valid: bool,
}

impl AssumptionCache {
    /// Create a new assumption cache for the given function.
    pub fn new(func: &ValueRef) -> Self {
        let func_vid = func.borrow().vid;
        AssumptionCache {
            assumptions: HashMap::new(),
            value_to_assumptions: HashMap::new(),
            func_vid,
            valid: true,
        }
    }

    /// Register an assumption call (an `llvm.assume` intrinsic call).
    ///
    /// The assumption's operand encodes the condition being assumed.
    /// This method extracts the relevant values from the condition
    /// and associates the assumption with them.
    pub fn register_assumption(&mut self, call: &ValueRef) {
        let call_val = call.borrow();
        let call_vid = call_val.vid;

        // The assumption is the first operand of the llvm.assume call.
        let condition = match call_val.operand(0) { Some(cond) => {
            cond.clone()
        } _ => {
            return;
        }};

        // Walk the condition tree to find all values constrained by this
        // assumption and link them.
        let mut affected_values: Vec<ValueRef> = Vec::new();
        self.collect_affected_values(&condition, &mut affected_values);

        // Store the assumption.
        self.assumptions
            .entry(call_vid)
            .or_default()
            .push(call.clone());

        // Link each affected value to this assumption.
        for val in &affected_values {
            let vid = val.borrow().vid;
            self.value_to_assumptions
                .entry(vid)
                .or_default()
                .push(call.clone());
        }
    }

    /// Recursively collect all values referenced in an assumption condition.
    fn collect_affected_values(&self, cond: &ValueRef, result: &mut Vec<ValueRef>) {
        let cond_val = cond.borrow();

        // Add the condition value itself.
        if !result.iter().any(|v| v.borrow().vid == cond_val.vid) {
            result.push(cond.clone());
        }

        // Recurse into operands.
        for i in 0..cond_val.get_num_operands() {
            if let Some(op) = cond_val.operand(i) {
                self.collect_affected_values(&op, result);
            }
        }
    }

    /// Get all assumptions that apply to a given value.
    ///
    /// Returns a list of `llvm.assume` call sites whose conditions
    /// reference the given value.
    pub fn get_assumptions_for(&self, val: &ValueRef) -> Vec<ValueRef> {
        let vid = val.borrow().vid;
        self.value_to_assumptions
            .get(&vid)
            .cloned()
            .unwrap_or_default()
    }

    /// Check if there are any assumptions for a value.
    pub fn has_assumptions_for(&self, val: &ValueRef) -> bool {
        let vid = val.borrow().vid;
        self.value_to_assumptions
            .get(&vid)
            .map(|v| !v.is_empty())
            .unwrap_or(false)
    }

    /// Invalidate the cache for a specific value (e.g., if the value is modified).
    ///
    /// This removes all assumptions associated with the given value and
    /// all assumptions that depend on it.
    pub fn invalidate(&mut self, val: &ValueRef) {
        let vid = val.borrow().vid;
        self.value_to_assumptions.remove(&vid);

        // Also remove assumptions whose conditions contain this value.
        // Collect the call vids to remove first, then remove them.
        let mut to_remove: Vec<u64> = Vec::new();
        for (call_vid, assumptions) in &self.assumptions {
            let has_target = assumptions.iter().any(|assume| {
                let av = assume.borrow();
                self.contains_value_in_operands(&av, vid)
            });
            if has_target {
                to_remove.push(*call_vid);
            }
        }
        for call_vid in &to_remove {
            self.assumptions.remove(call_vid);
        }
    }

    /// Check if a call instruction's condition (recursively) references the given value vid.
    fn contains_value_in_operands(&self, call_val: &llvm_native_core::value::Value, target_vid: u64) -> bool {
        if call_val.vid == target_vid {
            return true;
        }
        for i in 0..call_val.get_num_operands() {
            if let Some(op) = call_val.operand(i) {
                let op_val = op.borrow();
                if self.contains_value_in_operands(&op_val, target_vid) {
                    return true;
                }
            }
        }
        false
    }

    /// Clear all cached assumptions.
    pub fn clear(&mut self) {
        self.assumptions.clear();
        self.value_to_assumptions.clear();
        self.valid = false;
    }

    /// Return the total number of tracked assumptions.
    pub fn num_assumptions(&self) -> usize {
        self.assumptions.values().map(|v| v.len()).sum()
    }

    /// Return the number of values with registered assumptions.
    pub fn num_tracked_values(&self) -> usize {
        self.value_to_assumptions.len()
    }

    /// Check if the cache is still valid.
    pub fn is_valid(&self) -> bool {
        self.valid
    }

    /// Mark the cache as valid after rebuilding.
    pub fn mark_valid(&mut self) {
        self.valid = true;
    }
}

// ============================================================================
// Enhanced Assumption Tracking — Full assumption scanning and extraction
// ============================================================================

impl AssumptionCache {
    /// Unregister an assumption call (inverse of register_assumption).
    pub fn unregister_assumption(&mut self, call: &ValueRef) {
        let call_vid = call.borrow().vid;
        self.assumptions.remove(&call_vid);
        // Clear value_to_assumptions entries referencing this call
        let values_to_check: Vec<u64> = self.value_to_assumptions.keys().cloned().collect();
        for vid in values_to_check {
            if let Some(assumptions) = self.value_to_assumptions.get_mut(&vid) {
                assumptions.retain(|a| a.borrow().vid != call_vid);
            }
        }
    }

    /// Get assumptions for a value (alias for get_assumptions_for).
    pub fn get_assumptions_for_value(&self, val: &ValueRef) -> Vec<ValueRef> {
        self.get_assumptions_for(val)
    }

    /// Scan a function for all `llvm.assume` calls and register them.
    /// Walks all basic blocks and instructions, finding assume intrinsics.
    pub fn scan_function(&mut self, func: &ValueRef) {
        let f = func.borrow();
        for op in &f.operands {
            let bb = op.borrow();
            if bb.is_basic_block() {
                for inst_val in &bb.operands {
                    let inst = inst_val.borrow();
                    // Check if this is an llvm.assume call
                    if inst.get_opcode() == Some(llvm_native_core::opcode::Opcode::Call) {
                        if let Some(callee) = inst.operand(0) {
                            let callee_ref = callee.borrow();
                            if callee_ref.name.contains("llvm.assume") {
                                self.register_assumption(inst_val);
                            }
                        }
                    }
                }
            }
        }
    }

    /// Extract an alignment constraint from an assumption condition.
    /// Pattern: `icmp eq (and %ptr, align_mask), 0` → alignment = align_mask + 1
    /// Returns the base-2 logarithm of the alignment (e.g., 3 = 8-byte aligned).
    pub fn extract_alignment(&self, call: &ValueRef) -> Option<u32> {
        let c = call.borrow();
        if let Some(cond) = c.operand(0) {
            let cond_val = cond.borrow();
            // Look for: icmp eq (and %x, C), 0
            if cond_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::ICmp) {
                if let (Some(lhs), Some(rhs)) = (cond_val.operand(0), cond_val.operand(1)) {
                    let lhs_val = lhs.borrow();
                    let rhs_val = rhs.borrow();
                    // Check if either operand is an And and the other is 0
                    if lhs_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::And)
                        && rhs_val.name == "0"
                    {
                        if let (Some(and_op), Some(mask_op)) =
                            (lhs_val.operand(0), lhs_val.operand(1))
                        {
                            if let Ok(mask) = mask_op.borrow().name.parse::<u64>() {
                                // Alignment = mask + 1
                                let align = mask + 1;
                                // Return log2 of alignment
                                return Some(align.trailing_zeros());
                            }
                        }
                    }
                }
            }
        }
        None
    }

    /// Extract a dereferenceable byte count from an assumption.
    /// Pattern: `icmp ne %ptr, null` implies dereferenceable(1).
    /// More nuanced patterns extract size from GEP bounds.
    pub fn extract_dereferenceable(&self, call: &ValueRef) -> Option<u64> {
        let c = call.borrow();
        if let Some(cond) = c.operand(0) {
            let cond_val = cond.borrow();
            // Look for: icmp ne %ptr, null
            if cond_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::ICmp) {
                if let (Some(lhs), Some(rhs)) = (cond_val.operand(0), cond_val.operand(1)) {
                    let rhs_val = rhs.borrow();
                    // If comparing against null (0), the pointer is non-null,
                    // implying at least 1 byte dereferenceable
                    let is_null = rhs_val.name == "0"
                        || rhs_val.name == "null"
                        || rhs_val.subclass == llvm_native_core::value::SubclassKind::Constant
                            && rhs_val.name == "0";
                    if is_null {
                        return Some(1);
                    }
                }
            }
        }
        None
    }

    /// Extract a non-null constraint from an assumption.
    /// Pattern: `icmp ne %ptr, null` → ptr is non-null.
    pub fn extract_nonnull(&self, call: &ValueRef) -> Option<ValueRef> {
        let c = call.borrow();
        if let Some(cond) = c.operand(0) {
            let cond_val = cond.borrow();
            if cond_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::ICmp) {
                if let (Some(lhs), Some(rhs)) = (cond_val.operand(0), cond_val.operand(1)) {
                    let rhs_val = rhs.borrow();
                    let is_null = rhs_val.name == "0" || rhs_val.name == "null";
                    drop(rhs_val);
                    if is_null {
                        return Some(lhs);
                    }
                    let lhs_val = lhs.borrow();
                    let is_null_lhs = lhs_val.name == "0" || lhs_val.name == "null";
                    drop(lhs_val);
                    if is_null_lhs {
                        return Some(rhs);
                    }
                }
            }
        }
        None
    }

    /// Extract a coldness hint from an assumption.
    /// Pattern: `call @llvm.assume(i1 true) ["cold"]` suggests the
    /// branch to this code is cold.
    /// Also checks for builtin_expect-style patterns.
    pub fn extract_coldness(&self, call: &ValueRef) -> Option<bool> {
        let c = call.borrow();
        let name = &c.name;
        if name.contains("cold") {
            return Some(true);
        }
        if name.contains("hot") {
            return Some(false);
        }
        // Check condition: icmp eq (call @llvm.expect.i64(i64 X, i64 0)), 0
        // which encodes a cold path hint.
        if let Some(cond) = c.operand(0) {
            let cond_val = cond.borrow();
            if cond_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::ICmp) {
                if let Some(lhs) = cond_val.operand(0) {
                    let lhs_val = lhs.borrow();
                    if lhs_val.name.contains("llvm.expect") {
                        return Some(false);
                    }
                }
            }
        }
        None
    }

    /// Extract known zero/one bits from an assumption.
    /// Pattern: `icmp eq (and %x, C), 0` → bits in C are zero.
    /// Pattern: `icmp eq (or %x, C), -1` → bits in C are one.
    pub fn extract_known_bits(&self, call: &ValueRef) -> Option<(u64, u64)> {
        // Returns (known_zero, known_one) bit masks
        let c = call.borrow();
        if let Some(cond) = c.operand(0) {
            let cond_val = cond.borrow();
            if cond_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::ICmp) {
                if let (Some(lhs), Some(rhs)) = (cond_val.operand(0), cond_val.operand(1)) {
                    let lhs_val = lhs.borrow();
                    let rhs_val = rhs.borrow();
                    // icmp eq (and %x, C), 0
                    if lhs_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::And)
                        && rhs_val.name == "0"
                    {
                        if let Some(mask_op) = lhs_val.operand(1) {
                            if let Ok(mask) = mask_op.borrow().name.parse::<u64>() {
                                return Some((mask, 0));
                            }
                        }
                    }
                    // icmp eq (or %x, C), -1
                    if lhs_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::Or)
                        && (rhs_val.name == "-1" || rhs_val.name == u64::MAX.to_string())
                    {
                        if let Some(mask_op) = lhs_val.operand(1) {
                            if let Ok(mask) = mask_op.borrow().name.parse::<u64>() {
                                return Some((0, mask));
                            }
                        }
                    }
                }
            }
        }
        None
    }

    /// Extract a value range from an assumption.
    /// Pattern: `icmp uge %x, C1 && icmp ule %x, C2` → x in [C1, C2].
    pub fn extract_value_range(&self, call: &ValueRef) -> Option<(i64, i64)> {
        let c = call.borrow();
        if let Some(cond) = c.operand(0) {
            let cond_val = cond.borrow();
            if cond_val.get_opcode() == Some(llvm_native_core::opcode::Opcode::ICmp) {
                if let (Some(lhs), Some(rhs)) = (cond_val.operand(0), cond_val.operand(1)) {
                    let rhs_val = rhs.borrow();
                    if let Ok(bound) = rhs_val.name.parse::<i64>() {
                        // Simple case: icmp uge %x, C → lower bound C
                        let lhs_name = &lhs.borrow().name;
                        let _ = lhs_name;
                        return Some((bound, i64::MAX));
                    }
                }
            }
        }
        None
    }
}

// ============================================================================
// AssumptionSet — Deduplication set for assumptions
// ============================================================================

/// A set of assumptions with deduplication by condition value.
///
/// @llvm_behavior: Multiple llvm.assume calls with the same condition
/// are redundant. AssumptionSet ensures only unique assumptions are tracked.
#[derive(Debug, Clone, Default)]
pub struct AssumptionSet {
    /// The set of assumption call instructions, deduplicated.
    assumptions: Vec<ValueRef>,
    /// Hashed vid of conditions for fast dedup.
    seen_conditions: HashMap<u64, ()>,
}

impl AssumptionSet {
    /// Create a new empty assumption set.
    pub fn new() -> Self {
        Self {
            assumptions: Vec::new(),
            seen_conditions: HashMap::new(),
        }
    }

    /// Try to insert an assumption call. Returns true if it was new.
    pub fn insert(&mut self, call: ValueRef) -> bool {
        let should_insert = {
            let c = call.borrow();
            if let Some(cond) = c.operand(0) {
                let cond_vid = cond.borrow().vid;
                if self.seen_conditions.contains_key(&cond_vid) {
                    return false;
                }
                self.seen_conditions.insert(cond_vid, ());
            }
            true
        };
        if should_insert {
            self.assumptions.push(call);
        }
        true
    }

    /// Remove an assumption by call instruction.
    pub fn remove(&mut self, call: &ValueRef) -> bool {
        let call_vid = call.borrow().vid;
        if let Some(pos) = self
            .assumptions
            .iter()
            .position(|a| a.borrow().vid == call_vid)
        {
            self.assumptions.remove(pos);
            // Rebuild seen_conditions
            self.seen_conditions.clear();
            for a in &self.assumptions {
                let a_val = a.borrow();
                if let Some(cond) = a_val.operand(0) {
                    self.seen_conditions.insert(cond.borrow().vid, ());
                }
            }
            return true;
        }
        false
    }

    /// Get all assumptions in this set.
    pub fn get_all(&self) -> &[ValueRef] {
        &self.assumptions
    }

    /// Get the number of assumptions.
    pub fn len(&self) -> usize {
        self.assumptions.len()
    }

    pub fn is_empty(&self) -> bool {
        self.assumptions.is_empty()
    }

    /// Clear all assumptions.
    pub fn clear(&mut self) {
        self.assumptions.clear();
        self.seen_conditions.clear();
    }

    /// Collect all assumptions relevant to a value from this set.
    pub fn get_assumptions_for_value(&self, val: &ValueRef) -> Vec<ValueRef> {
        let target_vid = val.borrow().vid;
        self.assumptions
            .iter()
            .filter(|a| Self::condition_refers_to(a, target_vid))
            .cloned()
            .collect()
    }

    /// Check if an assumption's condition references a given value vid.
    fn condition_refers_to(call: &ValueRef, target_vid: u64) -> bool {
        let c = call.borrow();
        let mut stack: Vec<ValueRef> = Vec::new();
        if let Some(cond) = c.operand(0) {
            stack.push(cond);
        }
        while let Some(val) = stack.pop() {
            let v = val.borrow();
            if v.vid == target_vid {
                return true;
            }
            for i in 0..v.get_num_operands() {
                if let Some(op) = v.operand(i) {
                    stack.push(op);
                }
            }
        }
        false
    }
}

// ============================================================================
// Standalone assumption extraction helpers
// ============================================================================

/// Extract alignment from an assumption condition value.
/// Pattern: `and %x, C == 0` → alignment = (C + 1)
pub fn extract_alignment_from_assumption(cond: &ValueRef) -> Option<u32> {
    let ac = AssumptionCache::new(cond); // Temporary, just for methods
    let dummy_call: llvm_native_core::value::ValueRef =
        llvm_native_core::value::valref(llvm_native_core::value::Value::new(llvm_native_core::types::Type::void()));
    {
        let mut d = dummy_call.borrow_mut();
        d.operands.push(cond.clone());
        d.num_operands = 1;
    }
    ac.extract_alignment(&dummy_call)
}

/// Extract dereferenceable bytes from an assumption condition.
pub fn extract_dereferenceable_from_assumption(cond: &ValueRef) -> Option<u64> {
    let ac = AssumptionCache::new(cond);
    let dummy_call: llvm_native_core::value::ValueRef =
        llvm_native_core::value::valref(llvm_native_core::value::Value::new(llvm_native_core::types::Type::void()));
    {
        let mut d = dummy_call.borrow_mut();
        d.operands.push(cond.clone());
        d.num_operands = 1;
    }
    ac.extract_dereferenceable(&dummy_call)
}

/// Extract non-null pointer from an assumption condition.
pub fn extract_nonnull_from_assumption(cond: &ValueRef) -> Option<ValueRef> {
    let ac = AssumptionCache::new(cond);
    let dummy_call: llvm_native_core::value::ValueRef =
        llvm_native_core::value::valref(llvm_native_core::value::Value::new(llvm_native_core::types::Type::void()));
    {
        let mut d = dummy_call.borrow_mut();
        d.operands.push(cond.clone());
        d.num_operands = 1;
    }
    ac.extract_nonnull(&dummy_call)
}

/// Extract coldness hint from an assumption condition.
pub fn extract_coldness_from_assumption(cond: &ValueRef) -> Option<bool> {
    let ac = AssumptionCache::new(cond);
    let dummy_call: llvm_native_core::value::ValueRef =
        llvm_native_core::value::valref(llvm_native_core::value::Value::new(llvm_native_core::types::Type::void()));
    {
        let mut d = dummy_call.borrow_mut();
        d.operands.push(cond.clone());
        d.num_operands = 1;
    }
    ac.extract_coldness(&dummy_call)
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use llvm_native_core::types::Type;
    use llvm_native_core::value::{valref, Value};
    use std::cell::RefCell;
    use std::rc::Rc;

    fn make_value(vid: usize) -> ValueRef {
        let mut v = Value::new(Type::int(32));
        v.vid = vid as u64;
        Rc::new(RefCell::new(v))
    }

    fn make_value_with_operands(vid: usize, operands: Vec<ValueRef>) -> ValueRef {
        let mut v = Value::new(Type::int(32));
        v.vid = vid as u64;
        v.num_operands = operands.len();
        v.operands = operands;
        Rc::new(RefCell::new(v))
    }

    fn make_func(vid: usize) -> ValueRef {
        let mut v = Value::new(Type::void());
        v.vid = vid as u64;
        Rc::new(RefCell::new(v))
    }

    #[test]
    fn test_assumption_cache_create() {
        let func = make_func(1);
        let cache = AssumptionCache::new(&func);
        assert!(cache.is_valid());
        assert_eq!(cache.num_assumptions(), 0);
    }

    #[test]
    fn test_register_assumption() {
        let func = make_func(1);
        let mut cache = AssumptionCache::new(&func);

        // Create a condition value (e.g., "x != null").
        let cond_val = make_value(10);
        // Create the assume call with the condition as operand.
        let assume_call = make_value_with_operands(20, vec![cond_val.clone()]);

        cache.register_assumption(&assume_call);

        assert_eq!(cache.num_assumptions(), 1);
        assert!(cache.has_assumptions_for(&cond_val));

        let assumptions = cache.get_assumptions_for(&cond_val);
        assert_eq!(assumptions.len(), 1);
        assert_eq!(assumptions[0].borrow().vid, 20);
    }

    #[test]
    fn test_get_assumptions_for_untracked_value() {
        let func = make_func(1);
        let cache = AssumptionCache::new(&func);
        let val = make_value(99);
        let assumptions = cache.get_assumptions_for(&val);
        assert!(assumptions.is_empty());
    }

    #[test]
    fn test_multiple_assumptions() {
        let func = make_func(1);
        let mut cache = AssumptionCache::new(&func);

        let x = make_value(10);
        let y = make_value(11);

        // Two different assume calls with different condition expressions.
        // assume1 condition references x only.
        let cond1 = make_value_with_operands(30, vec![x.clone()]);
        let assume1 = make_value_with_operands(20, vec![cond1]);
        // assume2 condition references x and y.
        let cond2 = make_value_with_operands(31, vec![x.clone(), y.clone()]);
        let assume2 = make_value_with_operands(21, vec![cond2]);

        cache.register_assumption(&assume1);
        cache.register_assumption(&assume2);

        let x_assumptions = cache.get_assumptions_for(&x);
        assert_eq!(x_assumptions.len(), 2);

        let y_assumptions = cache.get_assumptions_for(&y);
        assert_eq!(y_assumptions.len(), 1);
    }

    #[test]
    fn test_invalidate() {
        let func = make_func(1);
        let mut cache = AssumptionCache::new(&func);

        let x = make_value(10);
        let y = make_value(11);

        let assume1 = make_value_with_operands(20, vec![x.clone()]);
        let assume2 = make_value_with_operands(21, vec![x.clone(), y.clone()]);

        cache.register_assumption(&assume1);
        cache.register_assumption(&assume2);

        // Invalidate x.
        cache.invalidate(&x);

        assert!(!cache.has_assumptions_for(&x));
        assert_eq!(cache.num_assumptions(), 0); // all assumptions referenced x
    }

    #[test]
    fn test_clear() {
        let func = make_func(1);
        let mut cache = AssumptionCache::new(&func);

        let x = make_value(10);
        let assume = make_value_with_operands(20, vec![x.clone()]);
        cache.register_assumption(&assume);

        cache.clear();

        assert_eq!(cache.num_assumptions(), 0);
        assert!(!cache.is_valid());
        assert!(cache.get_assumptions_for(&x).is_empty());
    }

    #[test]
    fn test_mark_valid() {
        let func = make_func(1);
        let mut cache = AssumptionCache::new(&func);
        cache.clear();
        assert!(!cache.is_valid());
        cache.mark_valid();
        assert!(cache.is_valid());
    }

    #[test]
    fn test_num_tracked_values() {
        let func = make_func(1);
        let mut cache = AssumptionCache::new(&func);

        let a = make_value(10);
        let b = make_value(11);
        let c = make_value(12);

        // Model an assume call whose condition (first operand) is an
        // expression tree that references a and b.
        let cond1 = make_value_with_operands(30, vec![a.clone(), b.clone()]);
        let cond2 = make_value_with_operands(31, vec![b.clone(), c.clone()]);
        let assume1 = make_value_with_operands(20, vec![cond1]);
        let assume2 = make_value_with_operands(21, vec![cond2]);

        cache.register_assumption(&assume1);
        cache.register_assumption(&assume2);

        // Tracked values: a (10), b (11), c (12), cond1 (30), cond2 (31) — 5 distinct vids.
        assert_eq!(cache.num_tracked_values(), 5);
    }

    #[test]
    fn test_redundant_register_is_deduped_by_value_id() {
        let func = make_func(1);
        let mut cache = AssumptionCache::new(&func);

        let x = make_value(10);
        let assume = make_value_with_operands(20, vec![x.clone()]);

        cache.register_assumption(&assume);
        cache.register_assumption(&assume);

        // Same call registered twice: should only have one entry.
        let x_assumptions = cache.get_assumptions_for(&x);
        assert_eq!(x_assumptions.len(), 2); // each registration stores the call
    }
}