Skip to content

Commit 3f0b94b

Browse files
Merge pull request #168 from fibonacci1729/targets-semver
Enable semver-compatible matching of import/export lookups in targets
2 parents 1750683 + 0539c3b commit 3f0b94b

File tree

7 files changed

+368
-9
lines changed

7 files changed

+368
-9
lines changed

crates/wac-types/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ mod aggregator;
66
mod checker;
77
mod component;
88
mod core;
9+
mod names;
910
mod package;
1011
mod targets;
1112

1213
pub use aggregator::*;
1314
pub use checker::*;
1415
pub use component::*;
1516
pub use core::*;
17+
pub use names::*;
1618
pub use package::*;
1719
pub use targets::*;

crates/wac-types/src/names.rs

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Adapted from https://github.com/bytecodealliance/wasmtime/blob/main/crates/environ/src/component/names.rs
2+
3+
use anyhow::{bail, Result};
4+
use core::hash::Hash;
5+
use indexmap::IndexMap;
6+
use 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)]
17+
pub 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+
50+
impl<K, V> NameMap<K, V>
51+
where
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+
144+
impl<K, V> Default for NameMap<K, V>
145+
where
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.
158+
pub 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.
172+
pub struct NameMapNoIntern;
173+
174+
impl 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.
200+
fn 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)]
228+
mod 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+
}

crates/wac-types/src/targets.rs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::{ExternKind, ItemKind, SubtypeChecker, Types, WorldId};
1+
use crate::{
2+
ExternKind, ItemKind, NameMap, NameMapNoIntern, SubtypeChecker, Types, World, WorldId,
3+
};
24
use std::collections::{BTreeMap, BTreeSet};
35
use std::fmt;
46

@@ -113,20 +115,17 @@ pub fn validate_target(
113115
) -> TargetValidationResult {
114116
let component_world = &types[component_world_id];
115117
let wit_world = &types[wit_world_id];
116-
// The interfaces imported implicitly through uses.
117-
let implicit_imported_interfaces = wit_world.implicit_imported_interfaces(types);
118118
let mut cache = Default::default();
119119
let mut checker = SubtypeChecker::new(&mut cache);
120-
121120
let mut report = TargetValidationReport::default();
122121

123122
// The output is allowed to import a subset of the world's imports
124123
checker.invert();
124+
125+
let world_imports = wit_world.all_imports(types);
126+
125127
for (import, item_kind) in component_world.imports.iter() {
126-
let Some(expected) = implicit_imported_interfaces
127-
.get(import.as_str())
128-
.or_else(|| wit_world.imports.get(import))
129-
else {
128+
let Some(expected) = world_imports.get(import.as_str(), &NameMapNoIntern) else {
130129
report.imports_not_in_target.insert(import.to_owned());
131130
continue;
132131
};
@@ -140,9 +139,19 @@ pub fn validate_target(
140139

141140
checker.revert();
142141

142+
let component_exports =
143+
component_world
144+
.exports
145+
.iter()
146+
.fold(NameMap::default(), |mut map, (name, item)| {
147+
// The unwrap here is safe because we allow shaddowing
148+
map.insert(name, &mut NameMapNoIntern, true, *item).unwrap();
149+
map
150+
});
151+
143152
// The output must export every export in the world
144153
for (name, expected) in &wit_world.exports {
145-
let Some(export) = component_world.exports.get(name).copied() else {
154+
let Some(export) = component_exports.get(name, &NameMapNoIntern).copied() else {
146155
report.missing_exports.insert(name.to_owned(), *expected);
147156
continue;
148157
};
@@ -156,3 +165,24 @@ pub fn validate_target(
156165

157166
report.into()
158167
}
168+
169+
impl World {
170+
/// This returns all implicit and explicit imports of the world as a mapping
171+
/// of names to item kinds. The `NameMap` here is used to enable
172+
/// semver-compatible matching of lookups
173+
fn all_imports(&self, types: &Types) -> NameMap<String, ItemKind> {
174+
let mut map = NameMap::default();
175+
let mut intern = NameMapNoIntern;
176+
// Add implicit imports from the world
177+
for (name, kind) in self.implicit_imported_interfaces(types) {
178+
// The unwrap here is safe because we allow shaddowing
179+
map.insert(name, &mut intern, true, kind).unwrap();
180+
}
181+
// Add explicit imports from the world
182+
for (name, item) in &self.imports {
183+
// The unwrap here is safe because we allow shaddowing.
184+
map.insert(name, &mut intern, true, *item).unwrap();
185+
}
186+
map
187+
}
188+
}

crates/wac-types/tests/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The `dummy_wasi_http@*.wasm`'s here were generated via the follow:
2+
```console
3+
$ wkg get wasi:http@* --format wasm
4+
$ wasm-tools component wit wit/wasi_http@*.wasm -o wasi_http@*.wit
5+
$ wasm-tools component embed --dummy wasi_http@*.wit | wasm-tools component new - -o dummy_wasi_http@*.wasm
6+
```
26.3 KB
Binary file not shown.
26.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)