Skip to content

Commit 8f247b9

Browse files
Add importize method to Resolve (#1784)
* Add importize method to Resolve A new method `importize` is added to the Resolve. This is to allow to mutate the Resolve to state where it would resemble what a consuming component would expect to see during composition. Signed-off-by: karthik2804 <karthik.ganeshram@fermyon.com> * Updates to importize * Update the CLI to have `--importize` and `--importize-world` * Rewrite the test to use these flags and have multiple tests in one file, each with a smaller world. * Update the implementation to preserve allow-listed imports instead of removing all imports and recreating what needs to be preserved. * Enable fuzz-testing of `importize` --------- Signed-off-by: karthik2804 <karthik.ganeshram@fermyon.com> Co-authored-by: Alex Crichton <alex@alexcrichton.com>
1 parent de775dd commit 8f247b9

13 files changed

Lines changed: 830 additions & 17 deletions

crates/wit-parser/src/resolve.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,101 @@ package {name} is defined in two different locations:\n\
931931
Some(self.id_of_name(interface.package.unwrap(), interface.name.as_ref()?))
932932
}
933933

934+
/// Convert a world to an "importized" version where the world is updated
935+
/// in-place to reflect what it would look like to be imported.
936+
///
937+
/// This is a transformation which is used as part of the process of
938+
/// importing a component today. For example when a component depends on
939+
/// another component this is useful for generating WIT which can be use to
940+
/// represent the component being imported. The general idea is that this
941+
/// function will update the `world_id` specified such it imports the
942+
/// functionality that it previously exported. The world will be left with
943+
/// no exports.
944+
///
945+
/// This world is then suitable for merging into other worlds or generating
946+
/// bindings in a context that is importing the original world. This
947+
/// is intended to be used as part of language tooling when depending on
948+
/// other components.
949+
pub fn importize(&mut self, world_id: WorldId) -> Result<()> {
950+
// Collect the set of interfaces which are depended on by exports. Also
951+
// all imported types are assumed to stay so collect any interfaces
952+
// they depend on.
953+
let mut live_through_exports = IndexSet::default();
954+
for (_, export) in self.worlds[world_id].exports.iter() {
955+
if let WorldItem::Interface { id, .. } = export {
956+
self.collect_interface_deps(*id, &mut live_through_exports);
957+
}
958+
}
959+
for (_, import) in self.worlds[world_id].imports.iter() {
960+
if let WorldItem::Type(ty) = import {
961+
if let Some(dep) = self.type_interface_dep(*ty) {
962+
self.collect_interface_deps(dep, &mut live_through_exports);
963+
}
964+
}
965+
}
966+
967+
// Rename the world to avoid having it get confused with the original
968+
// name of the world. Add `-importized` to it for now. Precisely how
969+
// this new world is created may want to be updated over time if this
970+
// becomes problematic.
971+
let world = &mut self.worlds[world_id];
972+
let pkg = &mut self.packages[world.package.unwrap()];
973+
pkg.worlds.shift_remove(&world.name);
974+
world.name.push_str("-importized");
975+
pkg.worlds.insert(world.name.clone(), world_id);
976+
977+
// Trim all unnecessary imports first.
978+
world.imports.retain(|name, item| match (name, item) {
979+
// Remove imports which can't be used by import such as:
980+
//
981+
// * `import foo: interface { .. }`
982+
// * `import foo: func();`
983+
(WorldKey::Name(_), WorldItem::Interface { .. } | WorldItem::Function(_)) => false,
984+
985+
// Coarsely say that all top-level types are required to avoid
986+
// calculating precise liveness of them right now.
987+
(WorldKey::Name(_), WorldItem::Type(_)) => true,
988+
989+
// Only retain interfaces if they're needed somehow transitively
990+
// for the exports.
991+
(WorldKey::Interface(id), _) => live_through_exports.contains(id),
992+
});
993+
994+
// After all unnecessary imports are gone remove all exports and move
995+
// them all to imports, failing if there's an overlap.
996+
for (name, export) in mem::take(&mut world.exports) {
997+
match (name.clone(), world.imports.insert(name, export)) {
998+
// no previous item? this insertion was ok
999+
(_, None) => {}
1000+
1001+
// cannot overwrite an import with an export
1002+
(WorldKey::Name(name), _) => {
1003+
bail!("world export `{name}` conflicts with import of same name");
1004+
}
1005+
1006+
// interface overlap is ok and is always allowed.
1007+
(WorldKey::Interface(id), Some(WorldItem::Interface { id: other, .. })) => {
1008+
assert_eq!(id, other);
1009+
}
1010+
1011+
(WorldKey::Interface(_), _) => unreachable!(),
1012+
}
1013+
}
1014+
1015+
#[cfg(debug_assertions)]
1016+
self.assert_valid();
1017+
Ok(())
1018+
}
1019+
1020+
fn collect_interface_deps(&self, interface: InterfaceId, deps: &mut IndexSet<InterfaceId>) {
1021+
if !deps.insert(interface) {
1022+
return;
1023+
}
1024+
for dep in self.interface_direct_deps(interface) {
1025+
self.collect_interface_deps(dep, deps);
1026+
}
1027+
}
1028+
9341029
/// Returns the ID of the specified `name` within the `pkg`.
9351030
pub fn id_of_name(&self, pkg: PackageId, name: &str) -> String {
9361031
let package = &self.packages[pkg];

fuzz/src/roundtrip_wit.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,18 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> {
3434
// to avoid timing out this fuzzer with asan enabled.
3535
let mut decoded_worlds = Vec::new();
3636
for (id, world) in resolve.worlds.iter().take(20) {
37-
log::debug!("testing world {}", world.name);
37+
log::debug!("embedding world {} as in a dummy module", world.name);
3838
let mut dummy = wit_component::dummy_module(&resolve, id);
3939
wit_component::embed_component_metadata(&mut dummy, &resolve, id, StringEncoding::UTF8)
4040
.unwrap();
4141
write_file("dummy.wasm", &dummy);
4242

43+
// Decode what was just created and record it later for testing merging
44+
// worlds together.
4345
let (_, decoded) = wit_component::metadata::decode(&dummy).unwrap();
4446
decoded_worlds.push(decoded.resolve);
4547

48+
log::debug!("... componentizing the world into a binary component");
4649
let wasm = wit_component::ComponentEncoder::default()
4750
.module(&dummy)
4851
.unwrap()
@@ -55,7 +58,15 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> {
5558
.validate_all(&wasm)
5659
.unwrap();
5760

61+
log::debug!("... decoding the component itself");
5862
wit_component::decode(&wasm).unwrap();
63+
64+
// Test out importizing the world and then assert the world is still
65+
// valid.
66+
log::debug!("... importizing this world");
67+
let mut resolve2 = resolve.clone();
68+
let _ = resolve2.importize(id);
69+
resolve.assert_valid();
5970
}
6071

6172
if decoded_worlds.len() < 2 {

src/bin/wasm-tools/component.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use anyhow::{bail, Context, Result};
44
use clap::Parser;
55
use std::collections::HashMap;
66
use std::io::Read;
7+
use std::mem;
78
use std::path::{Path, PathBuf};
89
use wasm_encoder::reencode::{Error, Reencode, ReencodeComponent, RoundtripReencoder};
910
use wasm_encoder::ModuleType;
@@ -499,6 +500,30 @@ pub struct WitOpts {
499500
)]
500501
json: bool,
501502

503+
/// Generates WIT to import the component specified to this command.
504+
///
505+
/// This flags requires that the input is a binary component, not a
506+
/// wasm-encoded WIT package. This will then generate a WIT world and output
507+
/// that. The returned world will have imports corresponding to the exports
508+
/// of the component which is input.
509+
///
510+
/// This is similar to `--importize-world`, but is used with components.
511+
#[clap(long, conflicts_with = "importize_world")]
512+
importize: bool,
513+
514+
/// Generates a WIT world to import a component which corresponds to the
515+
/// selected world.
516+
///
517+
/// This flag is used to indicate that the input is a WIT package and the
518+
/// world passed here is the name of a WIT `world` within the package. The
519+
/// output of the command will be the same WIT world but one that's
520+
/// importing the selected world. This effectively moves the world's exports
521+
/// to imports.
522+
///
523+
/// This is similar to `--importize`, but is used with WIT packages.
524+
#[clap(long, conflicts_with = "importize", value_name = "WORLD")]
525+
importize_world: Option<String>,
526+
502527
/// Features to enable when parsing the `wit` option.
503528
///
504529
/// This flag enables the `@unstable` feature in WIT documents where the
@@ -521,7 +546,13 @@ impl WitOpts {
521546

522547
/// Executes the application.
523548
fn run(self) -> Result<()> {
524-
let decoded = self.decode_input()?;
549+
let mut decoded = self.decode_input()?;
550+
551+
if self.importize {
552+
self.importize(&mut decoded, None)?;
553+
} else if self.importize_world.is_some() {
554+
self.importize(&mut decoded, self.importize_world.as_deref())?;
555+
}
525556

526557
// Now that the WIT document has been decoded, it's time to emit it.
527558
// This interprets all of the output options and performs such a task.
@@ -605,6 +636,30 @@ impl WitOpts {
605636
}
606637
}
607638

639+
fn importize(&self, decoded: &mut DecodedWasm, world: Option<&str>) -> Result<()> {
640+
let (resolve, world_id) = match (&mut *decoded, world) {
641+
(DecodedWasm::Component(resolve, world), None) => (resolve, *world),
642+
(DecodedWasm::Component(..), Some(_)) => {
643+
bail!(
644+
"the `--importize-world` flag is not compatible with a \
645+
component input, use `--importize` instead"
646+
);
647+
}
648+
(DecodedWasm::WitPackage(resolve, id), world) => {
649+
let world = resolve.select_world(*id, world)?;
650+
(resolve, world)
651+
}
652+
};
653+
// let pkg = decoded.package();
654+
// let world_id = decoded.resolve().select_world(main, None)?;
655+
resolve
656+
.importize(world_id)
657+
.context("failed to move world exports to imports")?;
658+
let resolve = mem::take(resolve);
659+
*decoded = DecodedWasm::Component(resolve, world_id);
660+
Ok(())
661+
}
662+
608663
fn emit_wasm(&self, decoded: &DecodedWasm) -> Result<()> {
609664
assert!(self.wasm || self.wat);
610665
assert!(self.out_dir.is_none());

tests/cli.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -149,27 +149,32 @@ fn execute(cmd: &mut Command, stdin: Option<&[u8]>, should_fail: bool) -> Result
149149

150150
let output = p.wait_with_output()?;
151151

152-
if !output.status.success() {
153-
if !should_fail {
154-
bail!(
155-
"{cmd:?} failed:
156-
status: {}
157-
stdout: {}
158-
stderr: {}",
159-
output.status,
160-
String::from_utf8_lossy(&output.stdout),
161-
String::from_utf8_lossy(&output.stderr)
162-
);
152+
let mut failure = None;
153+
match output.status.code() {
154+
Some(0) => {
155+
if should_fail {
156+
failure = Some("succeeded instead of failed");
157+
}
163158
}
164-
} else if should_fail {
159+
Some(1) | Some(2) => {
160+
if !should_fail {
161+
failure = Some("failed");
162+
}
163+
}
164+
_ => failure = Some("unknown exit code"),
165+
}
166+
if let Some(msg) = failure {
165167
bail!(
166-
"{cmd:?} succeeded instead of failed
167-
stdout: {}
168-
stderr: {}",
168+
"{cmd:?} {msg}:
169+
status: {}
170+
stdout: {}
171+
stderr: {}",
172+
output.status,
169173
String::from_utf8_lossy(&output.stdout),
170174
String::from_utf8_lossy(&output.stderr)
171175
);
172176
}
177+
173178
Ok(output)
174179
}
175180

tests/cli/importize.wit

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// RUN[simple]: component wit --importize-world simple %
2+
// RUN[simple-component]: component embed --dummy --world simple % | \
3+
// component wit --importize
4+
// RUN[with-deps]: component wit --importize-world with-deps %
5+
// RUN[simple-toplevel]: component wit --importize-world simple-toplevel %
6+
// RUN[toplevel-deps]: component wit --importize-world toplevel-deps %
7+
// FAIL[fail1]: component wit --importize-world fail1 %
8+
// RUN[trim-imports]: component wit --importize-world trim-imports %
9+
// RUN[tricky-import]: component wit --importize-world tricky-import %
10+
11+
package importize:importize;
12+
13+
interface t {
14+
resource r;
15+
}
16+
interface bar {
17+
use t.{r};
18+
record foo {
19+
x: string
20+
}
21+
importize: func(name: r);
22+
}
23+
24+
interface qux {
25+
use bar.{foo};
26+
blah: func(boo: foo);
27+
}
28+
29+
interface something-else-dep {
30+
type t = u32;
31+
}
32+
33+
world simple {
34+
export t;
35+
}
36+
37+
world with-deps {
38+
export qux;
39+
}
40+
41+
world simple-toplevel {
42+
export foo: func();
43+
export something: interface {
44+
foo: func();
45+
}
46+
}
47+
48+
world toplevel-deps {
49+
type s = u32;
50+
export bar: func() -> s;
51+
export something-else: interface {
52+
use something-else-dep.{t};
53+
bar: func() -> t;
54+
}
55+
}
56+
57+
world fail1 {
58+
type foo = u32;
59+
export foo: func() -> foo;
60+
}
61+
62+
interface a {}
63+
interface b {}
64+
65+
world trim-imports {
66+
import a;
67+
import foo: func();
68+
import bar: interface {}
69+
type t = u32;
70+
export b;
71+
}
72+
73+
interface with-dep {
74+
type t = u32;
75+
}
76+
77+
world tricky-import {
78+
use with-dep.{t};
79+
export f: func() -> t;
80+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
error: failed to move world exports to imports
2+
3+
Caused by:
4+
0: world export `foo` conflicts with import of same name
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package root:root;
2+
3+
world root-importized {
4+
import importize:importize/t;
5+
}
6+
package importize:importize {
7+
interface t {
8+
resource r;
9+
}
10+
world simple {
11+
export t;
12+
}
13+
}

0 commit comments

Comments
 (0)