wac_types/names.rs
1// Adapted from https://github.com/bytecodealliance/wasmtime/blob/main/crates/environ/src/component/names.rs
2
3use anyhow::{bail, Result};
4use core::hash::Hash;
5use indexmap::IndexMap;
6use semver::Version;
7
8/// A semver-aware map for imports/exports of a component.
9///
10/// This data structure is used when looking up the names of imports/exports of
11/// a component to enable semver-compatible matching of lookups. This will
12/// enable lookups of `a:b/c@0.2.0` to match entries defined as `a:b/c@0.2.1`
13/// which is currently considered a key feature of WASI's compatibility story.
14///
15/// On the outside this looks like a map of `K` to `V`.
16#[derive(Clone, Debug)]
17pub struct NameMap<K: Clone + Hash + Eq + Ord, V> {
18 /// A map of keys to the value that they define.
19 ///
20 /// Note that this map is "exact" where the name here is the exact name that
21 /// was specified when the `insert` was called. This doesn't have any
22 /// semver-mangling or anything like that.
23 ///
24 /// This map is always consulted first during lookups.
25 definitions: IndexMap<K, V>,
26
27 /// An auxiliary map tracking semver-compatible names. This is a map from
28 /// "semver compatible alternate name" to a name present in `definitions`
29 /// and the semver version it was registered at.
30 ///
31 /// An example map would be:
32 ///
33 /// ```text
34 /// {
35 /// "a:b/c@0.2": ("a:b/c@0.2.1", 0.2.1),
36 /// "a:b/c@2": ("a:b/c@2.0.0+abc", 2.0.0+abc),
37 /// }
38 /// ```
39 ///
40 /// As names are inserted into `definitions` each name may have up to one
41 /// semver-compatible name with extra numbers/info chopped off which is
42 /// inserted into this map. This map is the lookup table from `@0.2` to
43 /// `@0.2.x` where `x` is what was inserted manually.
44 ///
45 /// The `Version` here is tracked to ensure that when multiple versions on
46 /// one track are defined that only the maximal version here is retained.
47 alternate_lookups: IndexMap<K, (K, Version)>,
48}
49
50impl<K, V> NameMap<K, V>
51where
52 K: Clone + Hash + Eq + Ord,
53{
54 /// Inserts the `name` specified into this map.
55 ///
56 /// The name is intern'd through the `cx` argument and shadowing is
57 /// controlled by the `allow_shadowing` variable.
58 ///
59 /// This function will automatically insert an entry in
60 /// `self.alternate_lookups` if `name` is a semver-looking name.
61 ///
62 /// Returns an error if `allow_shadowing` is `false` and the `name` is
63 /// already present in this map (by exact match). Otherwise returns the
64 /// intern'd version of `name`.
65 pub fn insert<I>(&mut self, name: &str, cx: &mut I, allow_shadowing: bool, item: V) -> Result<K>
66 where
67 I: NameMapIntern<Key = K>,
68 {
69 // Always insert `name` and `item` as an exact definition.
70 let key = cx.intern(name);
71 if let Some(prev) = self.definitions.insert(key.clone(), item) {
72 if !allow_shadowing {
73 self.definitions.insert(key, prev);
74 bail!("map entry `{name}` defined twice")
75 }
76 }
77
78 // If `name` is a semver-looking thing, like `a:b/c@1.0.0`, then also
79 // insert an entry in the semver-compatible map under a key such as
80 // `a:b/c@1`.
81 //
82 // This key is used during `get` later on.
83 if let Some((alternate_key, version)) = alternate_lookup_key(name) {
84 let alternate_key = cx.intern(alternate_key);
85 if let Some((prev_key, prev_version)) = self
86 .alternate_lookups
87 .insert(alternate_key.clone(), (key.clone(), version.clone()))
88 {
89 // Prefer the latest version, so only do this if we're
90 // greater than the prior version.
91 if version < prev_version {
92 self.alternate_lookups
93 .insert(alternate_key, (prev_key, prev_version));
94 }
95 }
96 }
97 Ok(key)
98 }
99
100 /// Looks up `name` within this map, using the interning specified by
101 /// `cx`.
102 ///
103 /// This may return a definition even if `name` wasn't exactly defined in
104 /// this map, such as looking up `a:b/c@0.2.0` when the map only has
105 /// `a:b/c@0.2.1` defined.
106 pub fn get<I>(&self, name: &str, cx: &I) -> Option<&V>
107 where
108 I: NameMapIntern<Key = K>,
109 {
110 // First look up an exact match and if that's found return that. This
111 // enables defining multiple versions in the map and the requested
112 // version is returned if it matches exactly.
113 let candidate = cx.lookup(name).and_then(|k| self.definitions.get(&k));
114 if let Some(def) = candidate {
115 return Some(def);
116 }
117
118 // Failing that, then try to look for a semver-compatible alternative.
119 // This looks up the key based on `name`, if any, and then looks to see
120 // if that was intern'd in `strings`. Given all that look to see if it
121 // was defined in `alternate_lookups` and finally at the end that exact
122 // key is then used to look up again in `self.definitions`.
123 let (alternate_name, _version) = alternate_lookup_key(name)?;
124 let alternate_key = cx.lookup(alternate_name)?;
125 let (exact_key, _version) = self.alternate_lookups.get(&alternate_key)?;
126 self.definitions.get(exact_key)
127 }
128
129 /// Returns an iterator over inserted values in this map.
130 ///
131 /// Note that the iterator return yields intern'd keys and additionally does
132 /// not do anything special with semver names and such, it only literally
133 /// yields what's been inserted with [`NameMap::insert`].
134 pub fn raw_iter(&self) -> impl Iterator<Item = (&K, &V)> {
135 self.definitions.iter()
136 }
137
138 /// TODO
139 pub fn raw_get_mut(&mut self, key: &K) -> Option<&mut V> {
140 self.definitions.get_mut(key)
141 }
142}
143
144impl<K, V> Default for NameMap<K, V>
145where
146 K: Clone + Hash + Eq + Ord,
147{
148 fn default() -> NameMap<K, V> {
149 NameMap {
150 definitions: Default::default(),
151 alternate_lookups: Default::default(),
152 }
153 }
154}
155
156/// A helper trait used in conjunction with [`NameMap`] to optionally intern
157/// keys to non-strings.
158pub trait NameMapIntern {
159 /// The key that this interning context generates.
160 type Key;
161
162 /// Inserts `s` into `self` and returns the intern'd key `Self::Key`.
163 fn intern(&mut self, s: &str) -> Self::Key;
164
165 /// Looks up `s` in `self` returning `Some` if it was found or `None` if
166 /// it's not present.
167 fn lookup(&self, s: &str) -> Option<Self::Key>;
168}
169
170/// For use with [`NameMap`] when no interning should happen and instead string
171/// keys are copied as-is.
172pub struct NameMapNoIntern;
173
174impl NameMapIntern for NameMapNoIntern {
175 type Key = String;
176
177 fn intern(&mut self, s: &str) -> String {
178 s.to_string()
179 }
180
181 fn lookup(&self, s: &str) -> Option<String> {
182 Some(s.to_string())
183 }
184}
185
186/// Determines a version-based "alternate lookup key" for the `name` specified.
187///
188/// Some examples are:
189///
190/// * `foo` => `None`
191/// * `foo:bar/baz` => `None`
192/// * `foo:bar/baz@1.1.2` => `Some(foo:bar/baz@1)`
193/// * `foo:bar/baz@0.1.0` => `Some(foo:bar/baz@0.1)`
194/// * `foo:bar/baz@0.0.1` => `None`
195/// * `foo:bar/baz@0.1.0-rc.2` => `None`
196///
197/// This alternate lookup key is intended to serve the purpose where a
198/// semver-compatible definition can be located, if one is defined, at perhaps
199/// either a newer or an older version.
200fn alternate_lookup_key(name: &str) -> Option<(&str, Version)> {
201 let at = name.find('@')?;
202 let version_string = &name[at + 1..];
203 let version = Version::parse(version_string).ok()?;
204 if !version.pre.is_empty() {
205 // If there's a prerelease then don't consider that compatible with any
206 // other version number.
207 None
208 } else if version.major != 0 {
209 // If the major number is nonzero then compatibility is up to the major
210 // version number, so return up to the first decimal.
211 let first_dot = version_string.find('.')? + at + 1;
212 Some((&name[..first_dot], version))
213 } else if version.minor != 0 {
214 // Like the major version if the minor is nonzero then patch releases
215 // are all considered to be on a "compatible track".
216 let first_dot = version_string.find('.')? + at + 1;
217 let second_dot = name[first_dot + 1..].find('.')? + first_dot + 1;
218 Some((&name[..second_dot], version))
219 } else {
220 // If the patch number is the first nonzero entry then nothing can be
221 // compatible with this patch, e.g. 0.0.1 isn't' compatible with
222 // any other version inherently.
223 None
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::{NameMap, NameMapNoIntern};
230
231 #[test]
232 fn alternate_lookup_key() {
233 fn alt(s: &str) -> Option<&str> {
234 super::alternate_lookup_key(s).map(|(s, _)| s)
235 }
236
237 assert_eq!(alt("x"), None);
238 assert_eq!(alt("x:y/z"), None);
239 assert_eq!(alt("x:y/z@1.0.0"), Some("x:y/z@1"));
240 assert_eq!(alt("x:y/z@1.1.0"), Some("x:y/z@1"));
241 assert_eq!(alt("x:y/z@1.1.2"), Some("x:y/z@1"));
242 assert_eq!(alt("x:y/z@2.1.2"), Some("x:y/z@2"));
243 assert_eq!(alt("x:y/z@2.1.2+abc"), Some("x:y/z@2"));
244 assert_eq!(alt("x:y/z@0.1.2"), Some("x:y/z@0.1"));
245 assert_eq!(alt("x:y/z@0.1.3"), Some("x:y/z@0.1"));
246 assert_eq!(alt("x:y/z@0.2.3"), Some("x:y/z@0.2"));
247 assert_eq!(alt("x:y/z@0.2.3+abc"), Some("x:y/z@0.2"));
248 assert_eq!(alt("x:y/z@0.0.1"), None);
249 assert_eq!(alt("x:y/z@0.0.1-pre"), None);
250 assert_eq!(alt("x:y/z@0.1.0-pre"), None);
251 assert_eq!(alt("x:y/z@1.0.0-pre"), None);
252 }
253
254 #[test]
255 fn name_map_smoke() {
256 let mut map = NameMap::default();
257 let mut intern = NameMapNoIntern;
258
259 map.insert("a", &mut intern, false, 0).unwrap();
260 map.insert("b", &mut intern, false, 1).unwrap();
261
262 assert!(map.insert("a", &mut intern, false, 0).is_err());
263 assert!(map.insert("a", &mut intern, true, 0).is_ok());
264
265 assert_eq!(map.get("a", &intern), Some(&0));
266 assert_eq!(map.get("b", &intern), Some(&1));
267 assert_eq!(map.get("c", &intern), None);
268
269 map.insert("a:b/c@1.0.0", &mut intern, false, 2).unwrap();
270 map.insert("a:b/c@1.0.1", &mut intern, false, 3).unwrap();
271 assert_eq!(map.get("a:b/c@1.0.0", &intern), Some(&2));
272 assert_eq!(map.get("a:b/c@1.0.1", &intern), Some(&3));
273 assert_eq!(map.get("a:b/c@1.0.2", &intern), Some(&3));
274 assert_eq!(map.get("a:b/c@1.1.0", &intern), Some(&3));
275 }
276}