1
use std::alloc::Layout;
2
use std::alloc::LayoutError;
3
use std::fmt;
4
use std::hash::Hash;
5
use std::mem::ManuallyDrop;
6
use std::mem::offset_of;
7
use std::ptr;
8
use std::ptr::NonNull;
9
use std::ptr::slice_from_raw_parts_mut;
10

            
11
use equivalent::Equivalent;
12
use merc_unsafety::Erasable;
13
use merc_unsafety::ErasedPtr;
14
use merc_unsafety::SliceDst;
15
use merc_unsafety::repr_c;
16

            
17
use crate::ATermRef;
18
use crate::Symb;
19
use crate::SymbolRef;
20
use crate::Term;
21
use crate::Transmutable;
22

            
23
/// The underlying type of terms that are maximally shared.
24
///
25
/// # Details
26
///
27
/// Uses a C representation and is a dynamically sized type for compact memory
28
/// usage, implementing [SliceDst] and [Erasable]. This allows us to avoid
29
/// storing the length and capacity of an underlying vector. As such this is
30
/// even more compact than `smallvec`. Arguments are stored as [ATermRef] slices.
31
#[repr(C)]
32
pub struct SharedTerm {
33
    symbol: SymbolRef<'static>,
34
    arguments: [ATermRef<'static>],
35
}
36

            
37
impl PartialEq for SharedTerm {
38
    fn eq(&self, other: &Self) -> bool {
39
        self.symbol == other.symbol && self.arguments == other.arguments
40
    }
41
}
42

            
43
impl Eq for SharedTerm {}
44

            
45
/// Note that the length is stored in the symbol's arity
46
unsafe impl SliceDst for SharedTerm {
47
2
    fn layout_for(len: usize) -> Result<Layout, LayoutError> {
48
2
        let header_layout = Layout::new::<SymbolRef<'static>>();
49
2
        let slice_layout = Layout::array::<ATermRef<'static>>(len)?;
50

            
51
2
        repr_c(&[header_layout, slice_layout])
52
2
    }
53

            
54
72681
    fn retype(ptr: std::ptr::NonNull<[()]>) -> NonNull<Self> {
55
72681
        unsafe { NonNull::new_unchecked(ptr.as_ptr() as *mut _) }
56
72681
    }
57

            
58
    fn length(&self) -> usize {
59
        self.symbol().arity()
60
    }
61
}
62

            
63
unsafe impl Erasable for SharedTerm {
64
    fn erase(this: NonNull<Self>) -> ErasedPtr {
65
        this.cast()
66
    }
67

            
68
    unsafe fn unerase(this: ErasedPtr) -> NonNull<Self> {
69
        unsafe {
70
            // Wrap the by-value read in ManuallyDrop: dropping the temporary would decrement the
71
            // symbol's debug reference counter (an Arc) that the read never incremented.
72
            let symbol: ManuallyDrop<SymbolRef> = ptr::read(this.as_ptr().cast());
73
            let len = symbol.arity();
74

            
75
            let raw = NonNull::new_unchecked(slice_from_raw_parts_mut(this.as_ptr().cast(), len));
76
            Self::retype(raw)
77
        }
78
    }
79
}
80

            
81
impl fmt::Debug for SharedTerm {
82
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83
        write!(
84
            f,
85
            "SharedTerm {{ symbol: {:?}, arguments: {:?} }}",
86
            self.symbol,
87
            self.arguments()
88
        )
89
    }
90
}
91

            
92
impl SharedTerm {
93
    /// Returns the symbol of the term.
94
17060622831
    pub fn symbol(&self) -> &SymbolRef<'_> {
95
17060622831
        &self.symbol
96
17060622831
    }
97

            
98
    /// Returns the arguments of the term.
99
930354417
    pub fn arguments(&self) -> &[ATermRef<'_>] {
100
        // SAFETY: The returned lifetime is bound to the borrow of `self` by the signature.
101
930354417
        unsafe { self.arguments.transmute_lifetime() }
102
930354417
    }
103

            
104
    /// Returns a unique index for this shared term.
105
293050567
    pub fn index(&self) -> usize {
106
293050567
        self as *const Self as *const u8 as usize
107
293050567
    }
108

            
109
    /// Returns the length for a [SharedTermLookup]
110
    pub(crate) fn length_for(object: &SharedTermLookup) -> usize {
111
        object.arguments.len()
112
    }
113

            
114
    /// Constructs an uninitialised ptr from a [SharedTermLookup]
115
1
    pub(crate) unsafe fn construct(ptr: *mut SharedTerm, object: &SharedTermLookup) {
116
1
        let header_layout = Layout::new::<SymbolRef<'static>>();
117
1
        let slice_layout =
118
1
            Layout::array::<ATermRef<'static>>(object.arguments.len()).expect("Layout should not exceed isize");
119

            
120
1
        let (_, slice_offset) = header_layout
121
1
            .extend(slice_layout)
122
1
            .expect("Layout should not exceed isize");
123
        unsafe {
124
1
            ptr.cast::<SymbolRef<'static>>()
125
1
                .write(SymbolRef::from_index(object.symbol.shared()));
126

            
127
2
            for (index, argument) in object.arguments.iter().enumerate() {
128
2
                ptr.byte_offset(slice_offset as isize)
129
2
                    .cast::<ATermRef<'static>>()
130
2
                    .add(index)
131
2
                    .write(ATermRef::from_index(argument.shared()));
132
2
            }
133
        }
134
1
    }
135
}
136

            
137
impl Hash for SharedTerm {
138
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
139
        self.symbol.hash(state);
140
        self.arguments.hash(state);
141
    }
142
}
143

            
144
/// A cheap reference to the elements of a [SharedTerm] that can be used for
145
/// lookup of terms without allocating.
146
pub(crate) struct SharedTermLookup<'a> {
147
    pub(crate) symbol: SymbolRef<'a>,
148
    pub(crate) arguments: &'a [ATermRef<'a>],
149
}
150

            
151
impl Equivalent<SharedTerm> for SharedTermLookup<'_> {
152
    fn equivalent(&self, other: &SharedTerm) -> bool {
153
        self.symbol == other.symbol && self.arguments == &other.arguments
154
    }
155
}
156

            
157
/// This Hash implement must be the same as for [SharedTerm]
158
impl Hash for SharedTermLookup<'_> {
159
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
160
        self.symbol.hash(state);
161
        self.arguments.hash(state);
162
    }
163
}
164

            
165
// `symbol` must be at offset 0 in all term representations so that any pointer to a term.
166
const _: () = assert!(offset_of!(SharedTerm, symbol) == 0);
167

            
168
#[cfg(test)]
169
mod tests {
170
    use allocator_api2::alloc::Global;
171

            
172
    use merc_unsafety::AllocatorDst;
173
    #[cfg(not(debug_assertions))]
174
    use merc_unsafety::SliceDst;
175

            
176
    use crate::ATerm;
177
    use crate::Symbol;
178
    use crate::Term;
179
    use crate::storage::SharedTerm;
180
    use crate::storage::SharedTermLookup;
181

            
182
    #[test]
183
    #[cfg(not(debug_assertions))]
184
    fn test_shared_symbol_size() {
185
        // Cannot be a const assertion since the size depends on the length.
186
        assert_eq!(
187
            SharedTerm::layout_for(0)
188
                .expect("The layout should not overflow")
189
                .size(),
190
            1 * std::mem::size_of::<usize>(),
191
            "A SharedTerm without arguments should be the same size as the Symbol"
192
        );
193

            
194
        // TODO: Shared terms are still too large.
195
        // assert_eq!(
196
        //     SharedTerm::layout_for(2)
197
        //         .expect("The layout should not overflow")
198
        //         .size(),
199
        //     3 * std::mem::size_of::<usize>(),
200
        //     "A SharedTerm with arity two should be the same size as the Symbol and two ATermRef arguments"
201
        // );
202
    }
203

            
204
    #[test]
205
1
    fn test_shared_term_lookup() {
206
1
        let symbol = Symbol::new("a", 2);
207

            
208
1
        let term = ATerm::constant(&Symbol::new("b", 0));
209

            
210
1
        let lookup = SharedTermLookup {
211
1
            symbol: symbol.copy(),
212
1
            arguments: &[term.copy(), term.copy()],
213
1
        };
214

            
215
1
        let ptr = Global.allocate_slice_dst(2).expect("Could not allocate slice dst");
216

            
217
        unsafe {
218
1
            SharedTerm::construct(ptr.as_ptr(), &lookup);
219
1
            assert_eq!(
220
1
                *ptr.as_ref().symbol(),
221
1
                symbol.copy(),
222
                "The symbol should match the lookup symbol"
223
            );
224
1
            assert_eq!(
225
1
                ptr.as_ref().arguments()[0],
226
1
                term.copy(),
227
                "The arguments should match the lookup arguments"
228
            );
229
1
            assert_eq!(
230
1
                ptr.as_ref().arguments()[1],
231
1
                term.copy(),
232
                "The arguments should match the lookup arguments"
233
            );
234
        }
235

            
236
1
        Global.deallocate_slice_dst(ptr, 2);
237
1
    }
238
}