Skip to main content

kui_core/
key.rs

1//! [`Key`]: stable node identity across frames.
2//!
3//! A key is a hash of the path from the root: each node's key is its
4//! parent's mixed with either a string label or a sibling index. The same
5//! logical widget therefore gets the same key every frame, retained state
6//! (focus, scroll offsets, edit buffers, tweens) is looked up by it, and a
7//! binding can reproduce a key from strings alone.
8//!
9//! ```rust
10//! use kui_core::Key;
11//!
12//! let list = Key::ROOT.str("list");
13//! assert_eq!(list.index(3), Key::ROOT.str("list").index(3));
14//! assert_ne!(list.str("3"), list.index(3)); // labels and indices never clash
15//! ```
16
17/// A node's identity: the hash of its path from the root. `Key::ROOT` is
18/// the tree's root; [`Key::str`] and [`Key::index`] derive children.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct Key(pub u64);
21
22/// The FNV-1a basis every hash in the core starts from — keys, the text
23/// cache's identities, the access tree's digest, the corpus digest.
24pub const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
25pub const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
26
27/// FNV-1a over `bytes`, continuing from `h`. The one spelling of the
28/// mixer: a digest that must stay bit-stable across versions (a key, the
29/// access tree's change detector, the corpus digest) is stable because
30/// it is this function and nothing else. A byte a round with a multiply
31/// on the chain — a nanosecond a byte — which is the right cost for a
32/// label and the wrong one for a megabyte of text: that goes through
33/// [`hash_bulk`] first.
34#[inline]
35pub fn fnv(mut h: u64, bytes: &[u8]) -> u64 {
36    for &b in bytes {
37        h ^= b as u64;
38        h = h.wrapping_mul(FNV_PRIME);
39    }
40    h
41}
42
43/// Below this many bytes the content is mixed by [`fnv`] as it is: the
44/// word hash's set-up costs more than the bytes.
45const BULK_MIN: usize = 32;
46
47/// A word-wide hash of `bytes` (eight a round where [`fnv`] takes one),
48/// for the bulk a text cache key is made of. Fx's round (rotate, xor,
49/// multiply) with the length mixed first and murmur's finalizer after, so
50/// a tail of zero bytes and a shorter text differ and every input bit
51/// reaches every output bit. Not a digest anything keeps across versions.
52#[inline]
53pub fn hash_bulk(bytes: &[u8]) -> u64 {
54    const K: u64 = 0x517c_c1b7_2722_0a95;
55    let mut h = FNV_OFFSET ^ (bytes.len() as u64).wrapping_mul(K);
56    let (words, tail) = bytes.as_chunks::<8>();
57    for w in words {
58        h = (h.rotate_left(5) ^ u64::from_le_bytes(*w)).wrapping_mul(K);
59    }
60    if !tail.is_empty() {
61        let mut t = [0u8; 8];
62        t[..tail.len()].copy_from_slice(tail);
63        h = (h.rotate_left(5) ^ u64::from_le_bytes(t)).wrapping_mul(K);
64    }
65    h ^= h >> 33;
66    h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
67    h ^= h >> 33;
68    h = h.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
69    h ^ (h >> 33)
70}
71
72/// Mixes content into a key: short content by the byte, long content
73/// through [`hash_bulk`] and its eight bytes by the byte.
74#[inline]
75pub fn mix_content(h: u64, bytes: &[u8]) -> u64 {
76    if bytes.len() < BULK_MIN {
77        fnv(h, bytes)
78    } else {
79        fnv(h, &hash_bulk(bytes).to_le_bytes())
80    }
81}
82
83impl Key {
84    pub const ROOT: Key = Key(FNV_OFFSET);
85
86    fn mix_bytes(h: u64, bytes: &[u8]) -> u64 {
87        fnv(h, bytes)
88    }
89
90    /// Child key derived from a string label.
91    pub fn str(self, label: &str) -> Key {
92        // Tag byte separates the str/index namespaces.
93        let h = Self::mix_bytes(self.0 ^ 0x53, label.as_bytes());
94        Key(h)
95    }
96
97    /// Child key derived from a sibling index (auto-keys).
98    pub fn index(self, i: u64) -> Key {
99        let h = Self::mix_bytes(self.0 ^ 0x49, &i.to_le_bytes());
100        Key(h)
101    }
102}
103
104/// The `.str`-keyed nodes of one frame with the labels they were opened
105/// under — what `Core::key_of` resolves a name through. A binding that
106/// holds only strings cannot rebuild a key: the path from the root runs
107/// through auto-keyed ancestors it cannot spell. So the build records
108/// `(key, label)` as it goes, into one `Vec` and one `String` that are
109/// cleared, not dropped, between frames — a frame that keys a thousand
110/// rows allocates nothing after its first.
111#[derive(Default)]
112pub(crate) struct LabelIndex {
113    /// The key, the label's span in `text`, and the origin the node was
114    /// opened under — so a lookup can prefer the asker's own nodes (a
115    /// guest's `key_of` is asked from inside its fill; see `find_label`).
116    entries: Vec<(Key, u32, u32, crate::tree::OriginId)>,
117    text: String,
118}
119
120impl LabelIndex {
121    pub(crate) fn clear(&mut self) {
122        self.entries.clear();
123        self.text.clear();
124    }
125
126    pub(crate) fn push(&mut self, key: Key, label: &str, origin: crate::tree::OriginId) {
127        let start = self.text.len() as u32;
128        self.text.push_str(label);
129        self.entries.push((key, start, label.len() as u32, origin));
130    }
131
132    /// The label `key` was opened under, if it was opened by label.
133    pub(crate) fn label_of(&self, key: Key) -> Option<&str> {
134        self.entries
135            .iter()
136            .find(|(k, ..)| *k == key)
137            .map(|(_, start, len, _)| &self.text[*start as usize..(*start + *len) as usize])
138    }
139
140    /// Every `(key, label)`, in tree order: what a trace indexes once
141    /// rather than scanning per node.
142    pub(crate) fn iter(&self) -> impl Iterator<Item = (Key, &str)> + '_ {
143        self.entries
144            .iter()
145            .map(|(k, start, len, _)| (*k, &self.text[*start as usize..(*start + *len) as usize]))
146    }
147
148    /// The keys opened under `label` and the origin each was opened
149    /// under, in tree order.
150    pub(crate) fn find<'a>(
151        &'a self,
152        label: &'a str,
153    ) -> impl Iterator<Item = (Key, crate::tree::OriginId)> + 'a {
154        self.entries
155            .iter()
156            .filter(move |(_, start, len, _)| {
157                *len as usize == label.len()
158                    && &self.text[*start as usize..(*start + *len) as usize] == label
159            })
160            .map(|(k, _, _, o)| (*k, *o))
161    }
162
163    /// `find`, narrowed to the asker. A guest sees the keys it opened
164    /// under `label` and no one else's: labels are unique among siblings,
165    /// not across a frame, and a guest cannot know what the host or
166    /// another guest called its nodes (a script's env is a reading of its
167    /// own view). The host, whose frame it is, sees its
168    /// own first and everyone's when it opened none. Only a clash within
169    /// what the asker sees is an ambiguity.
170    /// Answers the first key in tree order and how many there were, so
171    /// the caller can warn of a clash — without allocating, since `key_of`
172    /// is asked from view code every frame.
173    pub(crate) fn find_for(
174        &self,
175        label: &str,
176        origin: crate::tree::OriginId,
177    ) -> (Option<Key>, usize) {
178        let mut mine = self.find(label).filter(|(_, o)| *o == origin);
179        let first = mine.next();
180        if let Some((k, _)) = first {
181            return (Some(k), 1 + mine.count());
182        }
183        if origin != crate::tree::OriginId::HOST {
184            return (None, 0);
185        }
186        let mut all = self.find(label);
187        match all.next() {
188            Some((k, _)) => (Some(k), 1 + all.count()),
189            None => (None, 0),
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    /// The word hash tells apart what a word-at-a-time reading could
199    /// confuse: a text and the same with a zero byte after it, a change
200    /// in the tail, a change on a word boundary, and the two sides of
201    /// `mix_content`'s threshold.
202    #[test]
203    fn the_bulk_hash_separates_tails_and_lengths() {
204        let a = b"0123456789abcdef0123456789abcdef0123456789abcdef";
205        let mut with_zero = a.to_vec();
206        with_zero.push(0);
207        assert_ne!(hash_bulk(a), hash_bulk(&with_zero));
208        let mut tail = a.to_vec();
209        tail[47] ^= 1;
210        assert_ne!(hash_bulk(a), hash_bulk(&tail));
211        let mut word = a.to_vec();
212        word[8] ^= 1;
213        assert_ne!(hash_bulk(a), hash_bulk(&word));
214        assert_eq!(hash_bulk(a), hash_bulk(a.as_ref()));
215        assert_ne!(hash_bulk(b""), hash_bulk(b"\0"));
216        assert_ne!(hash_bulk(b"abcdefg"), hash_bulk(b"abcdefg\0"));
217        // Either side of the threshold is a function of the bytes.
218        let short = b"0123456789abcdef0123456789abcde";
219        let long = b"0123456789abcdef0123456789abcdef";
220        assert_eq!(mix_content(FNV_OFFSET, short), fnv(FNV_OFFSET, short));
221        assert_eq!(
222            mix_content(FNV_OFFSET, long),
223            fnv(FNV_OFFSET, &hash_bulk(long).to_le_bytes())
224        );
225        assert_ne!(
226            mix_content(FNV_OFFSET, short),
227            mix_content(FNV_OFFSET, long)
228        );
229    }
230
231    #[test]
232    fn label_index_finds_in_tree_order_and_clears() {
233        use crate::tree::OriginId;
234        let mut idx = LabelIndex::default();
235        idx.push(Key::ROOT.str("a"), "a", OriginId::HOST);
236        idx.push(Key::ROOT.str("ab"), "ab", OriginId::HOST);
237        idx.push(Key::ROOT.index(0).str("a"), "a", OriginId(1));
238        let a: Vec<Key> = idx.find("a").map(|(k, _)| k).collect();
239        assert_eq!(a, [Key::ROOT.str("a"), Key::ROOT.index(0).str("a")]);
240        // The asker's own; a guest sees no one else's, the host sees
241        // everyone's when it opened none.
242        assert_eq!(
243            idx.find_for("a", OriginId::HOST),
244            (Some(Key::ROOT.str("a")), 1)
245        );
246        assert_eq!(
247            idx.find_for("a", OriginId(1)),
248            (Some(Key::ROOT.index(0).str("a")), 1)
249        );
250        assert_eq!(idx.find_for("a", OriginId(2)), (None, 0));
251        assert_eq!(idx.find_for("ab", OriginId(1)), (None, 0));
252        assert_eq!(
253            idx.find_for("ab", OriginId::HOST),
254            (Some(Key::ROOT.str("ab")), 1)
255        );
256        idx.push(Key::ROOT.str("g"), "g", OriginId(1));
257        idx.push(Key::ROOT.index(1).str("g"), "g", OriginId(2));
258        assert_eq!(
259            idx.find_for("g", OriginId::HOST),
260            (Some(Key::ROOT.str("g")), 2),
261            "the host sees both guests' and hears of the clash"
262        );
263        assert_eq!(idx.find("ab").count(), 1, "a prefix is not a match");
264        assert_eq!(idx.find("b").count(), 0);
265        assert_eq!(idx.label_of(Key::ROOT.str("ab")), Some("ab"));
266        assert_eq!(idx.label_of(Key::ROOT.str("zz")), None);
267        idx.clear();
268        assert_eq!(idx.find("a").count(), 0);
269        assert_eq!(idx.label_of(Key::ROOT.str("ab")), None);
270    }
271
272    #[test]
273    fn keys_are_stable() {
274        let a = Key::ROOT.str("panel").index(3);
275        let b = Key::ROOT.str("panel").index(3);
276        assert_eq!(a, b);
277    }
278
279    #[test]
280    fn keys_distinguish_paths() {
281        assert_ne!(Key::ROOT.str("a"), Key::ROOT.str("b"));
282        assert_ne!(Key::ROOT.index(0), Key::ROOT.index(1));
283        assert_ne!(Key::ROOT.str("a").str("b"), Key::ROOT.str("b").str("a"));
284        // str("1") and index(1) must not collide.
285        assert_ne!(Key::ROOT.str("1"), Key::ROOT.index(1));
286        // Nesting matters: ("ab") != ("a")("b")
287        assert_ne!(Key::ROOT.str("ab"), Key::ROOT.str("a").str("b"));
288    }
289
290    #[test]
291    fn no_collisions_over_many_indices() {
292        use std::collections::HashSet;
293        let mut seen = HashSet::new();
294        for scope in 0..100u64 {
295            let s = Key::ROOT.index(scope);
296            for i in 0..100u64 {
297                assert!(seen.insert(s.index(i)), "collision at {scope}/{i}");
298            }
299        }
300    }
301}