Skip to main content

kui_core/
key.rs

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