Skip to main content

blitz_dom_api/
atom.rs

1//! String interning for names that cross a guest boundary.
2//!
3//! Nothing in this crate requires an [`AtomId`] yet. It exists now because the
4//! next binding hosts a guest that cannot cheaply pass strings, and the names
5//! it will pass most are the ones with the smallest alphabets: tag names,
6//! attribute names, and class values.
7//!
8//! # Ownership rule
9//!
10//! An [`Interner`] is owned by the binding, one per document. An [`AtomId`] is
11//! only meaningful against the interner that produced it; resolving one
12//! against any other interner is [`DomError::UnknownAtom`] at best and a
13//! silently wrong name at worst. Do not store an `AtomId` alongside a document
14//! without also storing which interner it belongs to.
15//!
16//! # Why the operations still take `&str`
17//!
18//! The binding resolves at its own boundary and calls the operation with the
19//! resolved string:
20//!
21//! ```
22//! # use blitz_dom_api::atom::Interner;
23//! let mut names = Interner::new();
24//! let class = names.intern("panel");
25//! // ... the guest hands back `class` some time later ...
26//! let name = names.resolve(class).unwrap();
27//! assert_eq!(name, "panel");
28//! ```
29//!
30//! Threading the interner through every operation instead would put a second
31//! parameter on the whole API to serve one caller, and would still not remove
32//! the resolve, only move it. Resolving one level up keeps these signatures
33//! stable for whichever guest arrives, which is the property that mattered.
34
35use std::collections::HashMap;
36
37use crate::error::DomError;
38
39/// A name interned by an [`Interner`].
40///
41/// Opaque, cheap to copy, and meaningless on its own. See the module
42/// documentation for the ownership rule.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
44pub struct AtomId(u32);
45
46impl AtomId {
47    /// The raw index, for a binding that has to send it across a boundary.
48    #[inline]
49    pub fn to_u32(self) -> u32 {
50        self.0
51    }
52
53    /// Rebuild an id from a raw index received back across a boundary.
54    ///
55    /// Not validated here: [`Interner::resolve`] is what rejects an index the
56    /// interner never issued.
57    #[inline]
58    pub fn from_u32(raw: u32) -> Self {
59        Self(raw)
60    }
61}
62
63/// A set of interned names, owned by the binding.
64#[derive(Debug, Default, Clone)]
65pub struct Interner {
66    strings: Vec<String>,
67    index: HashMap<String, AtomId>,
68}
69
70impl Interner {
71    /// An empty interner.
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Intern a name, returning the existing id if it is already known.
77    pub fn intern(&mut self, name: &str) -> AtomId {
78        if let Some(id) = self.index.get(name) {
79            return *id;
80        }
81        let id =
82            AtomId(u32::try_from(self.strings.len()).expect("more than u32::MAX interned names"));
83        self.strings.push(name.to_owned());
84        self.index.insert(name.to_owned(), id);
85        id
86    }
87
88    /// The id for a name, if it has been interned. Does not intern.
89    pub fn get(&self, name: &str) -> Option<AtomId> {
90        self.index.get(name).copied()
91    }
92
93    /// The name behind an id.
94    ///
95    /// Errors if this interner did not mint the id, which is the only defence
96    /// against an id that crossed a guest boundary and came back wrong.
97    pub fn resolve(&self, atom: AtomId) -> Result<&str, DomError> {
98        self.strings
99            .get(atom.0 as usize)
100            .map(String::as_str)
101            .ok_or(DomError::UnknownAtom(atom))
102    }
103
104    /// How many distinct names have been interned.
105    pub fn len(&self) -> usize {
106        self.strings.len()
107    }
108
109    /// Whether nothing has been interned yet.
110    pub fn is_empty(&self) -> bool {
111        self.strings.is_empty()
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn interning_the_same_name_twice_gives_one_id() {
121        let mut names = Interner::new();
122        let first = names.intern("div");
123        let second = names.intern("div");
124        assert_eq!(first, second);
125        assert_eq!(names.len(), 1);
126    }
127
128    #[test]
129    fn distinct_names_get_distinct_ids_and_resolve_back() {
130        let mut names = Interner::new();
131        let div = names.intern("div");
132        let span = names.intern("span");
133        assert_ne!(div, span);
134        assert_eq!(names.resolve(div).unwrap(), "div");
135        assert_eq!(names.resolve(span).unwrap(), "span");
136    }
137
138    #[test]
139    fn get_does_not_intern() {
140        let mut names = Interner::new();
141        assert_eq!(names.get("div"), None);
142        assert!(names.is_empty());
143        names.intern("div");
144        assert!(names.get("div").is_some());
145    }
146
147    /// The ownership rule, as a test: an id from one interner is not valid
148    /// against another, even when both hold the same names in a different
149    /// order.
150    #[test]
151    fn an_id_from_another_interner_is_rejected_or_wrong() {
152        let mut a = Interner::new();
153        let mut b = Interner::new();
154        a.intern("div");
155        let a_span = a.intern("span");
156
157        assert_eq!(b.resolve(a_span), Err(DomError::UnknownAtom(a_span)));
158
159        b.intern("span");
160        b.intern("div");
161        // Same names, opposite order: resolvable, and wrong. This is why the
162        // rule is "one interner per document", not "ids are portable".
163        assert_eq!(b.resolve(a_span).unwrap(), "div");
164    }
165}