Skip to main content

plg_runtime/
copyterm.rs

1//! Relocatable off-heap term copies.
2//!
3//! One mechanism, three users: thrown error balls must survive the
4//! heap rewinding that backtracks to a catch frame; findall/3 must
5//! preserve each solution's template instance across the rewind that
6//! discards the goal's bindings; copy_term/2 is the user-facing form.
7//!
8//! A `TermBuf` stores cells exactly like the heap, except pointer
9//! payloads are indices into the buffer itself. Copy and restore are
10//! both ITERATIVE (a long list is as deep as it is long — recursion
11//! would overflow the C stack) and memoized per source cell, which
12//! preserves variable sharing and keeps cyclic terms (legal without
13//! occurs check) from looping.
14
15use crate::cell::*;
16use crate::machine::Machine;
17use std::collections::HashMap;
18
19#[derive(Clone, Default)]
20pub struct TermBuf {
21    pub cells: Vec<u64>,
22    pub root: Word,
23}
24
25/// Copy `root` (a heap word) into a relocatable buffer.
26pub fn copy_to_buf(m: &Machine, root: Word) -> TermBuf {
27    let mut buf = TermBuf::default();
28    let mut memo: HashMap<usize, usize> = HashMap::new();
29    // (source heap cell, destination buffer cell) pairs left to fill.
30    let mut work: Vec<(usize, usize)> = Vec::new();
31    buf.root = xlate_out(m, root, &mut buf.cells, &mut memo, &mut work);
32    while let Some((src, dst)) = work.pop() {
33        let w = m.heap[src];
34        buf.cells[dst] = xlate_out(m, w, &mut buf.cells, &mut memo, &mut work);
35    }
36    buf
37}
38
39fn xlate_out(
40    m: &Machine,
41    w: Word,
42    cells: &mut Vec<u64>,
43    memo: &mut HashMap<usize, usize>,
44    work: &mut Vec<(usize, usize)>,
45) -> Word {
46    let w = m.deref(w);
47    let idx = payload(w) as usize;
48    match tag_of(w) {
49        TAG_ATOM | TAG_INT => w,
50        TAG_REF => {
51            // Unbound: one buffer cell per distinct variable (sharing).
52            if let Some(&b) = memo.get(&idx) {
53                return make(TAG_REF, b as u64);
54            }
55            let b = cells.len();
56            cells.push(make(TAG_REF, b as u64)); // self-ref = unbound
57            memo.insert(idx, b);
58            make(TAG_REF, b as u64)
59        }
60        TAG_STR => {
61            if let Some(&b) = memo.get(&idx) {
62                return make(TAG_STR, b as u64);
63            }
64            let (_, arity) = unpack_functor(m.heap[idx]);
65            let b = cells.len();
66            cells.push(m.heap[idx]); // header copies verbatim
67            memo.insert(idx, b);
68            for k in 0..arity as usize {
69                cells.push(0);
70                work.push((idx + 1 + k, b + 1 + k));
71            }
72            make(TAG_STR, b as u64)
73        }
74        TAG_LST => {
75            if let Some(&b) = memo.get(&idx) {
76                return make(TAG_LST, b as u64);
77            }
78            let b = cells.len();
79            cells.push(0);
80            cells.push(0);
81            memo.insert(idx, b);
82            work.push((idx, b));
83            work.push((idx + 1, b + 1));
84            make(TAG_LST, b as u64)
85        }
86        TAG_FLT | TAG_BIG => {
87            if let Some(&b) = memo.get(&idx) {
88                return make(tag_of(w), b as u64);
89            }
90            let b = cells.len();
91            cells.push(m.heap[idx]); // raw bits copy verbatim
92            memo.insert(idx, b);
93            make(tag_of(w), b as u64)
94        }
95        _ => unreachable!("bad tag"),
96    }
97}
98
99/// Materialize a buffer back onto the machine heap; returns the root.
100pub fn restore_from_buf(m: &mut Machine, buf: &TermBuf) -> Word {
101    restore_cells(m, &buf.cells, buf.root)
102}
103
104/// Materialize a term whose `cells` use buffer-relative pointer payloads,
105/// rooted at `root`, onto the machine heap; returns the heap root. The
106/// slice-based core of `restore_from_buf` — also used by the fact-table to
107/// restore a non-immediate column from its read-only `.rodata` blob (those
108/// blobs are ground, so the `TAG_REF` arm never fires there).
109pub fn restore_cells(m: &mut Machine, cells: &[Word], root: Word) -> Word {
110    let mut memo: HashMap<usize, usize> = HashMap::new();
111    let mut work: Vec<(usize, usize)> = Vec::new();
112    let root = xlate_in(m, cells, root, &mut memo, &mut work);
113    while let Some((src, dst)) = work.pop() {
114        let w = cells[src];
115        m.heap[dst] = xlate_in(m, cells, w, &mut memo, &mut work);
116    }
117    root
118}
119
120fn xlate_in(
121    m: &mut Machine,
122    cells: &[Word],
123    w: Word,
124    memo: &mut HashMap<usize, usize>,
125    work: &mut Vec<(usize, usize)>,
126) -> Word {
127    let idx = payload(w) as usize;
128    match tag_of(w) {
129        TAG_ATOM | TAG_INT => w,
130        TAG_REF => {
131            if let Some(&h) = memo.get(&idx) {
132                return make(TAG_REF, h as u64);
133            }
134            let v = m.new_var();
135            memo.insert(idx, payload(v) as usize);
136            v
137        }
138        TAG_STR => {
139            if let Some(&h) = memo.get(&idx) {
140                return make(TAG_STR, h as u64);
141            }
142            let (_, arity) = unpack_functor(cells[idx]);
143            let h = m.heap.len();
144            m.heap.push(cells[idx]);
145            memo.insert(idx, h);
146            for k in 0..arity as usize {
147                m.heap.push(0);
148                work.push((idx + 1 + k, h + 1 + k));
149            }
150            make(TAG_STR, h as u64)
151        }
152        TAG_LST => {
153            if let Some(&h) = memo.get(&idx) {
154                return make(TAG_LST, h as u64);
155            }
156            let h = m.heap.len();
157            m.heap.push(0);
158            m.heap.push(0);
159            memo.insert(idx, h);
160            work.push((idx, h));
161            work.push((idx + 1, h + 1));
162            make(TAG_LST, h as u64)
163        }
164        TAG_FLT | TAG_BIG => {
165            if let Some(&h) = memo.get(&idx) {
166                return make(tag_of(w), h as u64);
167            }
168            let h = m.heap.len();
169            m.heap.push(cells[idx]);
170            memo.insert(idx, h);
171            make(tag_of(w), h as u64)
172        }
173        _ => unreachable!("bad tag"),
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use plg_shared::StringInterner;
181
182    fn machine() -> Box<Machine> {
183        Machine::new(StringInterner::new(), Vec::new())
184    }
185
186    #[test]
187    fn copy_restore_roundtrips_structures() {
188        let mut m = machine();
189        let x = m.new_var();
190        // f(a, X, [1, X])
191        let nil = make_atom(plg_shared::atom::ATOM_NIL);
192        let l1 = {
193            let i = m.heap.len();
194            m.heap.push(x);
195            m.heap.push(nil);
196            make(TAG_LST, i as u64)
197        };
198        let l0 = {
199            let i = m.heap.len();
200            m.heap.push(make_int(1));
201            m.heap.push(l1);
202            make(TAG_LST, i as u64)
203        };
204        let s = {
205            let i = m.heap.len();
206            m.heap.push(pack_functor(9, 3));
207            m.heap.push(make_atom(7));
208            m.heap.push(x);
209            m.heap.push(l0);
210            make(TAG_STR, i as u64)
211        };
212        let buf = copy_to_buf(&m, s);
213        // Bind X afterwards; the copy must NOT see the binding.
214        let xi = payload(x) as usize;
215        m.bind(xi, make_int(42));
216
217        let r = restore_from_buf(&mut m, &buf);
218        let ri = payload(r) as usize;
219        let (f, n) = unpack_functor(m.heap[ri]);
220        assert_eq!((f, n), (9, 3));
221        // Arg 2 (the copied X) is a FRESH unbound var, distinct from x.
222        let copied_x = m.deref(m.heap[ri + 2]);
223        assert_eq!(tag_of(copied_x), TAG_REF);
224        assert_ne!(payload(copied_x) as usize, xi);
225        // Sharing: the X inside the list is the SAME fresh var.
226        let l = m.deref(m.heap[ri + 3]);
227        let li = payload(l) as usize;
228        let l2 = m.deref(m.heap[li + 1]);
229        let l2i = payload(l2) as usize;
230        assert_eq!(m.deref(m.heap[l2i]), copied_x, "var sharing preserved");
231    }
232
233    #[test]
234    fn copy_snapshots_bindings_at_copy_time() {
235        let mut m = machine();
236        let x = m.new_var();
237        m.bind(payload(x) as usize, make_atom(5));
238        let buf = copy_to_buf(&m, x);
239        assert_eq!(buf.root, make_atom(5), "bound var copies its value");
240    }
241
242    #[test]
243    fn long_list_copy_is_iterative() {
244        // 100k-deep list: recursion would overflow the stack.
245        let mut m = machine();
246        let nil = make_atom(plg_shared::atom::ATOM_NIL);
247        let mut w = nil;
248        for i in 0..100_000 {
249            let idx = m.heap.len();
250            m.heap.push(make_int(i));
251            m.heap.push(w);
252            w = make(TAG_LST, idx as u64);
253        }
254        let buf = copy_to_buf(&m, w);
255        let r = restore_from_buf(&mut m, &buf);
256        assert_eq!(tag_of(r), TAG_LST);
257    }
258
259    #[test]
260    fn cyclic_terms_terminate() {
261        let mut m = machine();
262        let x = m.new_var();
263        // X = f(X): legal without occurs check.
264        let s = {
265            let i = m.heap.len();
266            m.heap.push(pack_functor(3, 1));
267            m.heap.push(x);
268            make(TAG_STR, i as u64)
269        };
270        m.bind(payload(x) as usize, s);
271        let buf = copy_to_buf(&m, s);
272        let r = restore_from_buf(&mut m, &buf);
273        assert_eq!(tag_of(r), TAG_STR, "cycle copied without divergence");
274    }
275}