1
//! Small, deterministic checks of the term pool's unsafe paths — construction, shared-pointer
2
//! identity, argument access, protection across garbage collection, and the `Send` wrapper.
3
//!
4
//! Unlike the randomized stress tests (which are `#[cfg_attr(miri, ignore)]` because they are far
5
//! too slow), these are cheap enough to run under miri, so they exercise the pointer/transmute and
6
//! protection-set code with Stacked/Tree Borrows checking.
7

            
8
use merc_aterm::ATerm;
9
use merc_aterm::ATermSend;
10
use merc_aterm::Symb;
11
use merc_aterm::Symbol;
12
use merc_aterm::Term;
13
use merc_aterm::storage::THREAD_TERM_POOL;
14

            
15
/// Builds the term `f(a, g(a))` from freshly created symbols on every call.
16
8
fn build_sample() -> ATerm {
17
8
    let a = ATerm::constant(&Symbol::new("a", 0));
18
8
    let g = ATerm::with_args(&Symbol::new("g", 1), &[a.copy()]).protect();
19
8
    ATerm::with_args(&Symbol::new("f", 2), &[a.copy(), g.copy()]).protect()
20
8
}
21

            
22
#[test]
23
1
fn test_miri_maximal_sharing() {
24
    // Two structurally equal terms must resolve to the same shared node: structural equality
25
    // implies pointer (index) equality.
26
1
    let first = build_sample();
27
1
    let second = build_sample();
28
1
    assert_eq!(first, second);
29
1
    assert_eq!(
30
1
        first.index(),
31
1
        second.index(),
32
        "maximal sharing should reuse the same node"
33
    );
34

            
35
    // A structurally different term is a distinct node.
36
1
    let other = ATerm::constant(&Symbol::new("a", 0));
37
1
    assert_ne!(first.index(), other.index());
38
1
}
39

            
40
#[test]
41
1
fn test_miri_term_arguments() {
42
1
    let term = build_sample();
43

            
44
1
    assert_eq!(term.get_head_symbol().name(), "f");
45
1
    assert_eq!(term.get_head_symbol().arity(), 2);
46
1
    assert_eq!(term.arguments().len(), 2);
47

            
48
    // arg(0) is the constant `a`.
49
1
    let arg0 = term.arg(0);
50
1
    assert_eq!(arg0.get_head_symbol().name(), "a");
51
1
    assert_eq!(arg0.arguments().len(), 0);
52

            
53
    // arg(1) is `g(a)`, whose only argument is again `a`, shared with arg(0).
54
1
    let arg1 = term.arg(1);
55
1
    assert_eq!(arg1.get_head_symbol().name(), "g");
56
1
    let nested = arg1.arg(0);
57
1
    assert_eq!(nested.get_head_symbol().name(), "a");
58
1
    assert_eq!(
59
1
        nested.index(),
60
1
        arg0.index(),
61
        "the inner `a` is the same shared node as arg(0)"
62
    );
63
1
}
64

            
65
#[test]
66
1
fn test_miri_protection_survives_gc() {
67
    // A protected term must survive forced garbage collection with its structure intact.
68
1
    let term = build_sample();
69

            
70
1
    THREAD_TERM_POOL.with(|tp| {
71
1
        tp.force_collect_garbage();
72
1
        tp.force_collect_garbage();
73
1
    });
74

            
75
1
    assert_eq!(term.get_head_symbol().name(), "f");
76
1
    assert_eq!(term.arg(1).arg(0).get_head_symbol().name(), "a");
77

            
78
    // Rebuilding the same term after GC still yields the same shared node.
79
1
    assert_eq!(term.index(), build_sample().index());
80
1
}
81

            
82
#[test]
83
1
fn test_miri_send_roundtrip() {
84
    // The `Send` wrapper keeps its term alive across GC, even on a single thread, and converting
85
    // back to a protected `ATerm` yields the original shared node.
86
1
    let term = build_sample();
87
1
    let send = ATermSend::from(build_sample());
88

            
89
1
    THREAD_TERM_POOL.with(|tp| tp.force_collect_garbage());
90

            
91
1
    assert_eq!(send.get_head_symbol().name(), "f");
92
1
    assert_eq!(send.protect().index(), term.index());
93
1
}
94

            
95
#[test]
96
1
fn test_miri_term_iterator() {
97
    // The subterm iterator traverses every edge without deduplicating shared nodes, so `f(a, g(a))`
98
    // visits f, a, g(a) and the shared `a` again: four nodes in total.
99
1
    let term = build_sample();
100
1
    assert_eq!(term.iter().count(), 4);
101
1
}
102

            
103
/// `ATermArgs` previously did not override `size_hint`, so its lower bound was 0
104
/// and there was no upper bound. Adapters such as `Skip` and `zip` use `size_hint`
105
/// to implement their own `ExactSizeIterator::len`, so without the fix they would
106
/// return 0 regardless of the actual remaining count.
107
#[test]
108
1
fn test_aterm_args_size_hint_is_exact() {
109
    // `h(a, b, c)` has arity 3, giving a three-element `ATermArgs` iterator.
110
1
    let a = ATerm::constant(&Symbol::new("a_sh", 0));
111
1
    let b = ATerm::constant(&Symbol::new("b_sh", 0));
112
1
    let c = ATerm::constant(&Symbol::new("c_sh", 0));
113
1
    let term = ATerm::with_args(&Symbol::new("h_sh", 3), &[a.copy(), b.copy(), c.copy()]).protect();
114

            
115
    // Fresh iterator: all 3 arguments remain.
116
1
    let mut iter = term.arguments();
117
1
    assert_eq!(iter.size_hint(), (3, Some(3)), "size_hint before advancing");
118
1
    assert_eq!(iter.len(), 3, "ExactSizeIterator::len before advancing");
119

            
120
    // Consume one argument; 2 remain.
121
1
    iter.next();
122
1
    assert_eq!(iter.size_hint(), (2, Some(2)), "size_hint after one next()");
123
1
    assert_eq!(iter.len(), 2, "ExactSizeIterator::len after one next()");
124

            
125
    // `skip` is built on top of `size_hint`; with the fix it must still report the
126
    // correct remaining length.
127
1
    let after_skip = term.arguments().skip(1);
128
1
    assert_eq!(
129
1
        after_skip.len(),
130
        2,
131
        "skip(1) on a 3-argument iterator must report len 2"
132
    );
133
1
}