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}