Skip to content

Commit 51ade65

Browse files
authored
Implement merging semver-compatible interfaces in imports (#1815)
* Implement merging semver-compatible interfaces in imports This commit is the first half and the meat of the implementation of #1774 where, by default, WIT processing tools will now merge world imports when possible based on semver versions. The precise shape of how this is done has gone through a few iterations and this is the end result I've ended up settling on. This should handle all the various cases we're interested in and continue to produce valid worlds within a `Resolve` (with the help of `elaborate_world` added in #1800). CLI tooling has been updated with flags to configure this behavior but the behavior is now enabled by default. * Add broken test case for wit-component * Match wasm/component import names based on versions This commit implements the final bit of #1774 where the `wit-component` crate will now match imports based on semver version in addition to the previous exact-name matching that was performed previously. * Don't panic on duplicate shims in wit-component This is now possible with import version matching so refactor the internal data structures to better support insertion of possibly duplicate shims. Most of wit-component was already ready for this refactoring, it was just the initial generation of shims that needed to be reorganized slightly. * wip * Elaborate all worlds after semver merging Any interface could be modified, so elaborate all of them to fixup anything that needs new imports. * Only merge once at the end, not continuously This commit updates how the semver-merging bits of `Resolve` work by moving it towards the end of the encoding process rather than once-per-world-merged. That helps both deduplicate work and fix some asserts I'm seeing that are tripping if a `Resolve` is import-merged and then merged again elsewhere. This additionally simplifies some APIs because the boolean of what to do isn't threaded to quite so many locations. * Fix test * Update interface deps of exports too Dependencies might depend on replaced interfaces so the dependency edges there need to be updated in the same manner as imports. * Fix fuzzer build Also ensure to at least try to test the new merging function.
1 parent 9eaaf5f commit 51ade65

36 files changed

+1838
-189
lines changed

crates/wit-component/src/encoding.rs

Lines changed: 99 additions & 91 deletions
Large diffs are not rendered by default.

crates/wit-component/src/linking.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,11 @@ pub struct Linker {
12221222
///
12231223
/// If `None`, use `DEFAULT_STACK_SIZE_BYTES`.
12241224
stack_size: Option<u32>,
1225+
1226+
/// This affects how when to WIT worlds are merged together, for example
1227+
/// from two different libraries, whether their imports are unified when the
1228+
/// semver version ranges for interface allow it.
1229+
merge_imports_based_on_semver: Option<bool>,
12251230
}
12261231

12271232
impl Linker {
@@ -1269,6 +1274,16 @@ impl Linker {
12691274
self
12701275
}
12711276

1277+
/// This affects how when to WIT worlds are merged together, for example
1278+
/// from two different libraries, whether their imports are unified when the
1279+
/// semver version ranges for interface allow it.
1280+
///
1281+
/// This is enabled by default.
1282+
pub fn merge_imports_based_on_semver(mut self, merge: bool) -> Self {
1283+
self.merge_imports_based_on_semver = Some(merge);
1284+
self
1285+
}
1286+
12721287
/// Encode the component and return the bytes
12731288
pub fn encode(mut self) -> Result<Vec<u8>> {
12741289
if self.use_built_in_libdl {
@@ -1415,9 +1430,11 @@ impl Linker {
14151430
self.stack_size.unwrap_or(DEFAULT_STACK_SIZE_BYTES),
14161431
);
14171432

1418-
let mut encoder = ComponentEncoder::default()
1419-
.validate(self.validate)
1420-
.module(&env_module)?;
1433+
let mut encoder = ComponentEncoder::default().validate(self.validate);
1434+
if let Some(merge) = self.merge_imports_based_on_semver {
1435+
encoder = encoder.merge_imports_based_on_semver(merge);
1436+
};
1437+
encoder = encoder.module(&env_module)?;
14211438

14221439
for (name, module) in &self.adapters {
14231440
encoder = encoder.adapter(name, module)?;

crates/wit-component/src/metadata.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@
4444
use crate::validation::BARE_FUNC_MODULE_NAME;
4545
use crate::{DecodedWasm, StringEncoding};
4646
use anyhow::{bail, Context, Result};
47-
use indexmap::IndexMap;
47+
use indexmap::{IndexMap, IndexSet};
4848
use std::borrow::Cow;
4949
use wasm_encoder::{
5050
ComponentBuilder, ComponentExportKind, ComponentType, ComponentTypeRef, CustomSection,
5151
};
5252
use wasm_metadata::Producers;
5353
use wasmparser::{BinaryReader, Encoding, Parser, Payload};
54-
use wit_parser::{Package, PackageName, Resolve, World, WorldId, WorldItem};
54+
use wit_parser::{Package, PackageName, Resolve, World, WorldId, WorldItem, WorldKey};
5555

5656
const CURRENT_VERSION: u8 = 0x04;
5757
const CUSTOM_SECTION_NAME: &str = "wit-component-encoding";
@@ -298,7 +298,10 @@ impl Bindgen {
298298
///
299299
/// Note that at this time there's no support for changing string encodings
300300
/// between metadata.
301-
pub fn merge(&mut self, other: Bindgen) -> Result<WorldId> {
301+
///
302+
/// This function returns the set of exports that the main world of
303+
/// `other` added to the world in `self`.
304+
pub fn merge(&mut self, other: Bindgen) -> Result<IndexSet<WorldKey>> {
302305
let Bindgen {
303306
resolve,
304307
world,
@@ -315,6 +318,7 @@ impl Bindgen {
315318
.merge(resolve)
316319
.context("failed to merge WIT package sets together")?
317320
.map_world(world, None)?;
321+
let exports = self.resolve.worlds[world].exports.keys().cloned().collect();
318322
self.resolve
319323
.merge_worlds(world, self.world)
320324
.context("failed to merge worlds from two documents")?;
@@ -349,7 +353,7 @@ impl Bindgen {
349353
}
350354
}
351355

352-
Ok(world)
356+
Ok(exports)
353357
}
354358
}
355359

crates/wit-component/src/validation.rs

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,80 @@ impl ImportMap {
414414
resolve: &Resolve,
415415
items: &IndexMap<WorldKey, WorldItem>,
416416
) -> Result<(WorldKey, InterfaceId)> {
417-
let key = world_key(resolve, module);
418-
match items.get(&key) {
419-
Some(WorldItem::Interface { id, .. }) => Ok((key, *id)),
420-
Some(WorldItem::Function(_) | WorldItem::Type(_)) => {
421-
bail!("import `{module}` is not an interface")
417+
// First see if this is a bare name
418+
let bare_name = WorldKey::Name(module.to_string());
419+
if let Some(WorldItem::Interface { id, .. }) = items.get(&bare_name) {
420+
return Ok((bare_name, *id));
421+
}
422+
423+
// ... and if this isn't a bare name then it's time to do some parsing
424+
// related to interfaces, versions, and such. First up the `module` name
425+
// is parsed as a normal component name from `wasmparser` to see if it's
426+
// of the "interface kind". If it's not then that means the above match
427+
// should have been a hit but it wasn't, so an error is returned.
428+
let kebab_name = ComponentName::new(module, 0);
429+
let name = match kebab_name.as_ref().map(|k| k.kind()) {
430+
Ok(ComponentNameKind::Interface(name)) => name,
431+
_ => bail!("module requires an import interface named `{module}`"),
432+
};
433+
434+
// Prioritize an exact match based on versions, so try that first.
435+
let pkgname = PackageName {
436+
namespace: name.namespace().to_string(),
437+
name: name.package().to_string(),
438+
version: name.version(),
439+
};
440+
if let Some(pkg) = resolve.package_names.get(&pkgname) {
441+
if let Some(id) = resolve.packages[*pkg]
442+
.interfaces
443+
.get(name.interface().as_str())
444+
{
445+
let key = WorldKey::Interface(*id);
446+
if items.contains_key(&key) {
447+
return Ok((key, *id));
448+
}
449+
}
450+
}
451+
452+
// If an exact match wasn't found then instead search for the first
453+
// match based on versions. This means that a core wasm import for
454+
// "1.2.3" might end up matching an interface at "1.2.4", for example.
455+
// (or "1.2.2", depending on what's available).
456+
for (key, _) in items {
457+
let id = match key {
458+
WorldKey::Interface(id) => *id,
459+
WorldKey::Name(_) => continue,
460+
};
461+
// Make sure the interface names match
462+
let interface = &resolve.interfaces[id];
463+
if interface.name.as_ref().unwrap() != name.interface().as_str() {
464+
continue;
465+
}
466+
467+
// Make sure the package name (without version) matches
468+
let pkg = &resolve.packages[interface.package.unwrap()];
469+
if pkg.name.namespace != pkgname.namespace || pkg.name.name != pkgname.name {
470+
continue;
471+
}
472+
473+
let module_version = match name.version() {
474+
Some(version) => version,
475+
None => continue,
476+
};
477+
let pkg_version = match &pkg.name.version {
478+
Some(version) => version,
479+
None => continue,
480+
};
481+
482+
// Test if the two semver versions are compatible
483+
let module_compat = PackageName::version_compat_track(&module_version);
484+
let pkg_compat = PackageName::version_compat_track(pkg_version);
485+
if module_compat == pkg_compat {
486+
return Ok((key.clone(), id));
422487
}
423-
None => bail!("module requires an import interface named `{module}`"),
424488
}
489+
490+
bail!("module requires an import interface named `{module}`")
425491
}
426492

427493
fn classify_import_with_library(
@@ -907,29 +973,6 @@ pub fn validate_adapter_module(
907973
Ok(ret)
908974
}
909975

910-
fn world_key(resolve: &Resolve, name: &str) -> WorldKey {
911-
let kebab_name = ComponentName::new(name, 0);
912-
let (pkgname, interface) = match kebab_name.as_ref().map(|k| k.kind()) {
913-
Ok(ComponentNameKind::Interface(name)) => {
914-
let pkgname = PackageName {
915-
namespace: name.namespace().to_string(),
916-
name: name.package().to_string(),
917-
version: name.version(),
918-
};
919-
(pkgname, name.interface().as_str())
920-
}
921-
_ => return WorldKey::Name(name.to_string()),
922-
};
923-
match resolve
924-
.package_names
925-
.get(&pkgname)
926-
.and_then(|p| resolve.packages[*p].interfaces.get(interface))
927-
{
928-
Some(id) => WorldKey::Interface(*id),
929-
None => WorldKey::Name(name.to_string()),
930-
}
931-
}
932-
933976
fn valid_resource_drop(
934977
func_name: &str,
935978
ty: &FuncType,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
(module
2+
(import "a:b/[email protected]" "[constructor]r" (func (result i32)))
3+
(import "a:b/[email protected]" "[resource-drop]r" (func (param i32)))
4+
(import "a:b/[email protected]" "[method]r.x" (func (param i32)))
5+
(import "a:b/[email protected]" "x" (func (param i32 i32)))
6+
(import "a:b/[email protected]" "y" (func))
7+
8+
(func (export "f"))
9+
)
10+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package foo:foo;
2+
3+
world adapt-old {
4+
import a:b/c@0.1.1;
5+
}
6+
7+
package a:b@0.1.1 {
8+
interface c {
9+
x: func(x: string);
10+
y: func();
11+
12+
resource r {
13+
constructor();
14+
15+
x: func();
16+
}
17+
}
18+
}
19+
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
(component
2+
(type (;0;)
3+
(instance
4+
(export (;0;) "r" (type (sub resource)))
5+
(type (;1;) (own 0))
6+
(type (;2;) (func (result 1)))
7+
(export (;0;) "[constructor]r" (func (type 2)))
8+
(type (;3;) (func (param "x" string)))
9+
(export (;1;) "x" (func (type 3)))
10+
)
11+
)
12+
(import "a:b/[email protected]" (instance (;0;) (type 0)))
13+
(core module (;0;)
14+
(type (;0;) (func))
15+
(type (;1;) (func (result i32)))
16+
(type (;2;) (func (param i32)))
17+
(type (;3;) (func (param i32 i32)))
18+
(import "old" "f" (func (;0;) (type 0)))
19+
(import "a:b/[email protected]" "[constructor]r" (func (;1;) (type 1)))
20+
(import "a:b/[email protected]" "[resource-drop]r" (func (;2;) (type 2)))
21+
(import "a:b/[email protected]" "x" (func (;3;) (type 3)))
22+
(memory (;0;) 1)
23+
(export "memory" (memory 0))
24+
(@producers
25+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
26+
(processed-by "my-fake-bindgen" "123.45")
27+
)
28+
)
29+
(core module (;1;)
30+
(type (;0;) (func))
31+
(export "f" (func 0))
32+
(func (;0;) (type 0))
33+
)
34+
(core module (;2;)
35+
(type (;0;) (func))
36+
(type (;1;) (func (param i32 i32)))
37+
(table (;0;) 2 2 funcref)
38+
(export "0" (func $adapt-old-f))
39+
(export "1" (func $indirect-a:b/[email protected]))
40+
(export "$imports" (table 0))
41+
(func $adapt-old-f (;0;) (type 0)
42+
i32.const 0
43+
call_indirect (type 0)
44+
)
45+
(func $indirect-a:b/[email protected] (;1;) (type 1) (param i32 i32)
46+
local.get 0
47+
local.get 1
48+
i32.const 1
49+
call_indirect (type 1)
50+
)
51+
(@producers
52+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
53+
)
54+
)
55+
(core module (;3;)
56+
(type (;0;) (func))
57+
(type (;1;) (func (param i32 i32)))
58+
(import "" "0" (func (;0;) (type 0)))
59+
(import "" "1" (func (;1;) (type 1)))
60+
(import "" "$imports" (table (;0;) 2 2 funcref))
61+
(elem (;0;) (i32.const 0) func 0 1)
62+
(@producers
63+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
64+
)
65+
)
66+
(core instance (;0;) (instantiate 2))
67+
(alias core export 0 "0" (core func (;0;)))
68+
(core instance (;1;)
69+
(export "f" (func 0))
70+
)
71+
(alias export 0 "[constructor]r" (func (;0;)))
72+
(core func (;1;) (canon lower (func 0)))
73+
(alias export 0 "r" (type (;1;)))
74+
(core func (;2;) (canon resource.drop 1))
75+
(alias core export 0 "1" (core func (;3;)))
76+
(core instance (;2;)
77+
(export "[constructor]r" (func 1))
78+
(export "[resource-drop]r" (func 2))
79+
(export "x" (func 3))
80+
)
81+
(core instance (;3;) (instantiate 0
82+
(with "old" (instance 1))
83+
(with "a:b/[email protected]" (instance 2))
84+
)
85+
)
86+
(alias core export 3 "memory" (core memory (;0;)))
87+
(core instance (;4;) (instantiate 1))
88+
(alias core export 0 "$imports" (core table (;0;)))
89+
(alias core export 4 "f" (core func (;4;)))
90+
(alias export 0 "x" (func (;1;)))
91+
(core func (;5;) (canon lower (func 1) (memory 0) string-encoding=utf8))
92+
(core instance (;5;)
93+
(export "$imports" (table 0))
94+
(export "0" (func 4))
95+
(export "1" (func 5))
96+
)
97+
(core instance (;6;) (instantiate 3
98+
(with "" (instance 5))
99+
)
100+
)
101+
(@producers
102+
(processed-by "wit-component" "$CARGO_PKG_VERSION")
103+
)
104+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package root:component;
2+
3+
world root {
4+
import a:b/[email protected];
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
(module
2+
(import "old" "f" (func))
3+
4+
(import "a:b/[email protected]" "[constructor]r" (func (result i32)))
5+
(import "a:b/[email protected]" "[resource-drop]r" (func (param i32)))
6+
(import "a:b/[email protected]" "x" (func (param i32 i32)))
7+
8+
(memory (export "memory") 1)
9+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package foo:foo;
2+
3+
world module {
4+
import a:b/c@0.1.0;
5+
}
6+
7+
package a:b@0.1.0 {
8+
interface c {
9+
resource r {
10+
constructor();
11+
}
12+
x: func(x: string);
13+
}
14+
}

0 commit comments

Comments
 (0)