Skip to main content

kermit_algos/
hash_singleton.rs

1//! Singleton hash-trie iterator: a unary "relation" of one value, used by
2//! the const-view rewrite (see [`crate::const_rewrite`]).
3//!
4//! Analogous to [`crate::singleton::SingletonTrieIter`]. Exposes the
5//! [`HashTrieIterator`] interface around a precomputed `u64` hash of its
6//! single value so the join algorithm can intersect against `HashTrie`
7//! data without divergence. The caller (`kermit::db::hash_join`) is
8//! responsible for computing the hash via the chosen
9//! [`kermit_iters::HashStrategy`] before constructing the singleton; this
10//! file does not depend on any strategy.
11
12use kermit_iters::{HashTrieIterable, HashTrieIterator, JoinIterable};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum State {
16    Root,
17    AtValue,
18    Exhausted,
19}
20
21/// A singleton "hash trie" containing one value.
22///
23/// Implements [`HashTrieIterator`] over a single-value relation. Used by
24/// the const-view rewrite to expose a `Const_c<id> = {c<id>}` predicate as
25/// if it were a real hash-trie-backed relation.
26#[derive(Debug, Clone)]
27pub struct SingletonHashTrieIter {
28    // Kept as a sentinel of the constructed value; the join algorithm and
29    // `leaf_tuples` consume the cached `chain` instead. Phase 7 may grow
30    // direct uses (e.g., diagnostics) — allow until then.
31    #[allow(dead_code)]
32    value: usize,
33    hash: u64,
34    /// Cached one-tuple chain returned by [`HashTrieIterator::leaf_tuples`]
35    /// once the iterator has been [`open`](HashTrieIterator::open)ed.
36    /// Pre-materialized in [`new`] so the trait method can hand out a
37    /// `&[Vec<usize>]` without allocating or holding interior mutability.
38    chain: Vec<Vec<usize>>,
39    state: State,
40}
41
42impl SingletonHashTrieIter {
43    /// Construct a new singleton positioned at its root (pre-`open`).
44    ///
45    /// `hash` is the precomputed [`u64`] hash of `value` under whichever
46    /// [`kermit_iters::HashStrategy`] the caller uses for the matching
47    /// `HashTrie<H>`. Keeping the strategy out of this struct lets the
48    /// singleton remain `Sized` without a phantom parameter and lets a
49    /// generic caller (e.g. `hash_join<R, H>`) drive both the trie and
50    /// the singleton with the same hash function.
51    pub fn new(value: usize, hash: u64) -> Self {
52        Self {
53            value,
54            hash,
55            chain: vec![vec![value]],
56            state: State::Root,
57        }
58    }
59}
60
61impl HashTrieIterator for SingletonHashTrieIter {
62    fn key(&self) -> Option<u64> {
63        match self.state {
64            | State::AtValue => Some(self.hash),
65            | State::Root | State::Exhausted => None,
66        }
67    }
68
69    fn next(&mut self) -> Option<u64> {
70        if self.state == State::AtValue {
71            self.state = State::Exhausted;
72        }
73        None
74    }
75
76    fn lookup(&mut self, hash: u64) -> bool {
77        if self.state == State::AtValue && hash == self.hash {
78            true
79        } else if self.state == State::AtValue {
80            self.state = State::Exhausted;
81            false
82        } else {
83            false
84        }
85    }
86
87    fn size(&self) -> usize {
88        if self.state == State::AtValue || self.state == State::Root {
89            1
90        } else {
91            0
92        }
93    }
94
95    fn at_end(&self) -> bool { self.state == State::Exhausted }
96
97    fn open(&mut self) -> bool {
98        match self.state {
99            | State::Root => {
100                self.state = State::AtValue;
101                true
102            },
103            | State::AtValue | State::Exhausted => false,
104        }
105    }
106
107    fn up(&mut self) -> bool {
108        match self.state {
109            | State::AtValue | State::Exhausted => {
110                self.state = State::Root;
111                true
112            },
113            | State::Root => false,
114        }
115    }
116
117    fn leaf_tuples(&self) -> Option<&[Vec<usize>]> {
118        if self.state == State::AtValue {
119            Some(self.chain.as_slice())
120        } else {
121            None
122        }
123    }
124}
125
126impl JoinIterable for SingletonHashTrieIter {}
127
128impl HashTrieIterable for SingletonHashTrieIter {
129    fn hash_trie_iter(&self) -> impl HashTrieIterator { self.clone() }
130}
131
132#[cfg(test)]
133mod tests {
134    use {
135        super::*,
136        kermit_iters::{HashStrategy, SipHashStrategy},
137    };
138
139    /// Pick a concrete strategy for the tests below. The struct itself is
140    /// strategy-agnostic — what matters is that we use *the same* hash for
141    /// both construction and lookup, mirroring how `hash_join<R, H>` wires
142    /// the singleton against its matching `HashTrie<H>`.
143    fn h(value: usize) -> u64 { SipHashStrategy::hash(value) }
144
145    #[test]
146    fn new_starts_at_root() {
147        let it = SingletonHashTrieIter::new(42, h(42));
148        assert_eq!(it.state, State::Root);
149        assert!(it.key().is_none());
150    }
151
152    #[test]
153    fn open_advances_to_value() {
154        let mut it = SingletonHashTrieIter::new(42, h(42));
155        assert!(it.open());
156        assert_eq!(it.key(), Some(h(42)));
157    }
158
159    #[test]
160    fn lookup_matching_hash_succeeds() {
161        let mut it = SingletonHashTrieIter::new(42, h(42));
162        it.open();
163        assert!(it.lookup(h(42)));
164    }
165
166    #[test]
167    fn lookup_other_hash_fails_and_exhausts() {
168        let mut it = SingletonHashTrieIter::new(42, h(42));
169        it.open();
170        assert!(!it.lookup(h(99)));
171        assert!(it.at_end());
172    }
173
174    #[test]
175    fn next_after_open_exhausts() {
176        let mut it = SingletonHashTrieIter::new(42, h(42));
177        it.open();
178        assert!(it.next().is_none());
179        assert!(it.at_end());
180    }
181
182    #[test]
183    fn leaf_tuples_returns_singleton_chain() {
184        let mut it = SingletonHashTrieIter::new(42, h(42));
185        it.open();
186        let chain = it.leaf_tuples().expect("singleton's leaf chain after open");
187        assert_eq!(chain, &[vec![42]]);
188    }
189
190    #[test]
191    fn leaf_tuples_returns_none_before_open() {
192        let it = SingletonHashTrieIter::new(42, h(42));
193        assert!(it.leaf_tuples().is_none());
194    }
195}