Skip to content

Commit 2d3a410

Browse files
authored
Merge pull request #3446 from itowlson/dep-wits-functions-in-worlds
Fix WIT extractor ignoring world-level functions
2 parents 4ba04b9 + ad3588a commit 2d3a410

1 file changed

Lines changed: 335 additions & 6 deletions

File tree

  • crates/dependency-wit/src

crates/dependency-wit/src/lib.rs

Lines changed: 335 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ pub async fn extract_wits_into(
3333
Ok(())
3434
}
3535

36+
enum ImportKind {
37+
Function(spin_serde::KebabId),
38+
WholePackage,
39+
Interface(spin_serde::KebabId),
40+
}
41+
3642
pub async fn extract_wits(
3743
source: impl Iterator<Item = (&DependencyName, &ComponentDependency)>,
3844
app_root: impl AsRef<Path>,
@@ -49,10 +55,13 @@ pub async fn extract_wits(
4955

5056
// TODO: figure out what to do if we import two itfs from same dep
5157
for (index, (dependency_name, dependency)) in source.enumerate() {
52-
let import_name = match dependency_name {
53-
DependencyName::Plain(_) => None,
58+
let import_kind = match dependency_name {
59+
DependencyName::Plain(name) => ImportKind::Function(name.clone()),
5460
DependencyName::Package(dependency_package_name) => {
55-
dependency_package_name.interface.as_ref()
61+
match &dependency_package_name.interface {
62+
Some(itf) => ImportKind::Interface(itf.clone()),
63+
None => ImportKind::WholePackage,
64+
}
5665
}
5766
};
5867

@@ -75,9 +84,18 @@ pub async fn extract_wits(
7584
let importised = importize(decoded, Some(&impo_world))
7685
.with_context(|| format!("failed to map importize dependency {dependency_name}"))?;
7786

78-
let imports = match import_name {
79-
None => all_imports(&importised),
80-
Some(itf) => one_import(&importised, itf.as_ref())?,
87+
let imports = match &import_kind {
88+
ImportKind::WholePackage => all_imports(&importised),
89+
ImportKind::Interface(itf) => one_import(&importised, itf.as_ref())?,
90+
ImportKind::Function(_) => Default::default(),
91+
};
92+
let func_import = match &import_kind {
93+
ImportKind::Function(f) => one_func_import(&importised, f.as_ref())?,
94+
_ => Default::default(),
95+
};
96+
let type_imports = match &import_kind {
97+
ImportKind::Function(_) => world_type_imports(&importised),
98+
_ => Default::default(),
8199
};
82100

83101
// Capture WITs for all packages used in the importised thing.
@@ -142,6 +160,36 @@ pub async fn extract_wits(
142160
});
143161
}
144162
}
163+
if let Some(mut func) = func_import {
164+
// Remap type IDs in the function to reference the aggregating resolve
165+
for param in &mut func.params {
166+
if let wit_parser::Type::Id(id) = &mut param.ty {
167+
*id = remap.map_type(*id, Span::default())?;
168+
}
169+
}
170+
if let Some(wit_parser::Type::Id(id)) = &mut func.result {
171+
*id = remap.map_type(*id, Span::default())?;
172+
}
173+
174+
// Add world-level type definitions that the function depends on
175+
let aggregating_world = aggregating_resolve
176+
.worlds
177+
.get_mut(aggregating_world_id)
178+
.context("aggregated dependency world doesn't exist")?;
179+
for (name, type_id) in &type_imports {
180+
let mapped_id = remap.map_type(*type_id, Span::default())?;
181+
let wk = wit_parser::WorldKey::Name(name.clone());
182+
let world_item = wit_parser::WorldItem::Type {
183+
id: mapped_id,
184+
span: Span::default(),
185+
};
186+
aggregating_world.imports.insert(wk, world_item);
187+
}
188+
189+
let wk = wit_parser::WorldKey::Name(func.name.clone());
190+
let world_item = wit_parser::WorldItem::Function(func);
191+
aggregating_world.imports.insert(wk, world_item);
192+
}
145193
}
146194

147195
// Text for the root package and world(s)
@@ -321,6 +369,13 @@ fn as_interface(wi: &wit_parser::WorldItem) -> Option<wit_parser::InterfaceId> {
321369
}
322370
}
323371

372+
fn as_func(wi: &wit_parser::WorldItem) -> Option<&wit_parser::Function> {
373+
match wi {
374+
wit_parser::WorldItem::Function(func) => Some(func),
375+
_ => None,
376+
}
377+
}
378+
324379
fn one_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Vec<wit_parser::InterfaceId>> {
325380
let id = wasm
326381
.resolve()
@@ -332,6 +387,47 @@ fn one_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Vec<wit_parser::
332387
Ok(vec![id])
333388
}
334389

390+
fn world_type_imports(wasm: &DecodedWasm) -> Vec<(String, wit_parser::TypeId)> {
391+
wasm.resolve()
392+
.worlds
393+
.iter()
394+
.flat_map(|(_wid, w)| {
395+
w.imports.iter().filter_map(|(wk, wi)| {
396+
if let wit_parser::WorldItem::Type { id, .. } = wi {
397+
let name = match wk {
398+
wit_parser::WorldKey::Name(n) => n.clone(),
399+
wit_parser::WorldKey::Interface(_) => return None,
400+
};
401+
Some((name, *id))
402+
} else {
403+
None
404+
}
405+
})
406+
})
407+
.collect()
408+
}
409+
410+
fn one_func_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Option<wit_parser::Function>> {
411+
let funcs = wasm
412+
.resolve()
413+
.worlds
414+
.iter()
415+
.flat_map(|w| {
416+
w.1.imports
417+
.iter()
418+
.flat_map(|(_wk, wi)| as_func(wi))
419+
.filter(|f| f.name == name)
420+
})
421+
.collect::<Vec<_>>();
422+
423+
// This shouldn't happen because we are using the compiled Wasm so there should
424+
// be only one world in play. But belt and braces.
425+
if funcs.len() > 1 {
426+
anyhow::bail!("Dependency exports more than one function named {name}");
427+
}
428+
Ok(funcs.first().cloned().cloned())
429+
}
430+
335431
fn read_wasm(wasm_bytes: &[u8]) -> anyhow::Result<DecodedWasm> {
336432
if wasmparser::Parser::is_component(wasm_bytes) {
337433
wit_component::decode(wasm_bytes)
@@ -493,4 +589,237 @@ mod test {
493589

494590
Ok(())
495591
}
592+
593+
#[tokio::test]
594+
async fn world_level_func_extracted() -> anyhow::Result<()> {
595+
let tempdir = tempfile::TempDir::new()?;
596+
let dep_file = tempdir.path().join("crimes.wasm");
597+
598+
let dep_wit = "package my:crimes@1.0.0;\n\nworld crimes {\n export is-curse: func(s: string) -> bool;\n}";
599+
let dep_wasm = generate_dummy_component(dep_wit, "crimes");
600+
tokio::fs::write(&dep_file, &dep_wasm).await?;
601+
602+
let dep_name = DependencyName::Plain("is-curse".to_string().try_into().unwrap());
603+
let dep_src = ComponentDependency::Local {
604+
path: dep_file,
605+
export: None,
606+
};
607+
let deps = std::iter::once((&dep_name, &dep_src));
608+
609+
let wit = extract_wits(deps, ".").await?;
610+
611+
let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
612+
613+
assert_eq!(1, resolve.packages.len()); // root:component - world-level funcs don't retain their package when importised
614+
let (_rc_pkg_id, rc_pkg) = resolve
615+
.packages
616+
.iter()
617+
.find(|(_, p)| p.name.to_string() == "root:component")
618+
.expect("should have had `root:component`");
619+
620+
let root_world_id = rc_pkg
621+
.worlds
622+
.get("root")
623+
.expect("should have had root world");
624+
let root_world = resolve
625+
.worlds
626+
.get(*root_world_id)
627+
.expect("should have had root world at that id");
628+
629+
let func = root_world
630+
.imports
631+
.iter()
632+
.filter_map(|(_, wi)| as_func(wi))
633+
.find(|f| f.name == "is-curse")
634+
.expect("is-curse function does not appear in root imports");
635+
636+
assert_eq!(1, func.params.len());
637+
assert_eq!(wit_parser::Type::String, func.params.first().unwrap().ty);
638+
assert_eq!(wit_parser::Type::Bool, func.result.unwrap());
639+
640+
Ok(())
641+
}
642+
643+
#[tokio::test]
644+
async fn world_level_func_with_record_param() -> anyhow::Result<()> {
645+
let tempdir = tempfile::TempDir::new()?;
646+
let dep_file = tempdir.path().join("greeter.wasm");
647+
648+
let dep_wit = r#"package my:greeter@1.0.0;
649+
650+
world greeter {
651+
record person {
652+
name: string,
653+
age: u32,
654+
}
655+
export greet: func(who: person) -> string;
656+
}"#;
657+
let dep_wasm = generate_dummy_component(dep_wit, "greeter");
658+
tokio::fs::write(&dep_file, &dep_wasm).await?;
659+
660+
let dep_name = DependencyName::Plain("greet".to_string().try_into().unwrap());
661+
let dep_src = ComponentDependency::Local {
662+
path: dep_file,
663+
export: None,
664+
};
665+
let deps = std::iter::once((&dep_name, &dep_src));
666+
667+
let wit = extract_wits(deps, ".").await?;
668+
669+
let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
670+
671+
let (_rc_pkg_id, rc_pkg) = resolve
672+
.packages
673+
.iter()
674+
.find(|(_, p)| p.name.to_string() == "root:component")
675+
.expect("should have had `root:component`");
676+
677+
let root_world_id = rc_pkg
678+
.worlds
679+
.get("root")
680+
.expect("should have had root world");
681+
let root_world = resolve
682+
.worlds
683+
.get(*root_world_id)
684+
.expect("should have had root world at that id");
685+
686+
let func = root_world
687+
.imports
688+
.iter()
689+
.filter_map(|(_, wi)| as_func(wi))
690+
.find(|f| f.name == "greet")
691+
.expect("greet function does not appear in root imports");
692+
693+
assert_eq!(1, func.params.len());
694+
// The param should be a user-defined type (record)
695+
assert!(
696+
matches!(func.params.first().unwrap().ty, wit_parser::Type::Id(_)),
697+
"expected record param to be Type::Id"
698+
);
699+
assert_eq!(wit_parser::Type::String, func.result.unwrap());
700+
701+
Ok(())
702+
}
703+
704+
#[tokio::test]
705+
async fn world_level_func_with_record_result() -> anyhow::Result<()> {
706+
let tempdir = tempfile::TempDir::new()?;
707+
let dep_file = tempdir.path().join("lookup.wasm");
708+
709+
let dep_wit = r#"package my:lookup@1.0.0;
710+
711+
world lookup {
712+
record info {
713+
value: string,
714+
found: bool,
715+
}
716+
export lookup: func(key: string) -> info;
717+
}"#;
718+
let dep_wasm = generate_dummy_component(dep_wit, "lookup");
719+
tokio::fs::write(&dep_file, &dep_wasm).await?;
720+
721+
let dep_name = DependencyName::Plain("lookup".to_string().try_into().unwrap());
722+
let dep_src = ComponentDependency::Local {
723+
path: dep_file,
724+
export: None,
725+
};
726+
let deps = std::iter::once((&dep_name, &dep_src));
727+
728+
let wit = extract_wits(deps, ".").await?;
729+
730+
let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
731+
732+
let (_rc_pkg_id, rc_pkg) = resolve
733+
.packages
734+
.iter()
735+
.find(|(_, p)| p.name.to_string() == "root:component")
736+
.expect("should have had `root:component`");
737+
738+
let root_world_id = rc_pkg
739+
.worlds
740+
.get("root")
741+
.expect("should have had root world");
742+
let root_world = resolve
743+
.worlds
744+
.get(*root_world_id)
745+
.expect("should have had root world at that id");
746+
747+
let func = root_world
748+
.imports
749+
.iter()
750+
.filter_map(|(_, wi)| as_func(wi))
751+
.find(|f| f.name == "lookup")
752+
.expect("lookup function does not appear in root imports");
753+
754+
assert_eq!(1, func.params.len());
755+
assert_eq!(wit_parser::Type::String, func.params.first().unwrap().ty);
756+
// The result should be a user-defined type (record)
757+
assert!(
758+
matches!(func.result, Some(wit_parser::Type::Id(_))),
759+
"expected record result to be Type::Id"
760+
);
761+
762+
Ok(())
763+
}
764+
765+
#[tokio::test]
766+
async fn world_level_func_with_enum_param() -> anyhow::Result<()> {
767+
let tempdir = tempfile::TempDir::new()?;
768+
let dep_file = tempdir.path().join("color.wasm");
769+
770+
let dep_wit = r#"package my:colors@1.0.0;
771+
772+
world colors {
773+
enum color {
774+
red,
775+
green,
776+
blue,
777+
}
778+
export color-name: func(c: color) -> string;
779+
}"#;
780+
let dep_wasm = generate_dummy_component(dep_wit, "colors");
781+
tokio::fs::write(&dep_file, &dep_wasm).await?;
782+
783+
let dep_name = DependencyName::Plain("color-name".to_string().try_into().unwrap());
784+
let dep_src = ComponentDependency::Local {
785+
path: dep_file,
786+
export: None,
787+
};
788+
let deps = std::iter::once((&dep_name, &dep_src));
789+
790+
let wit = extract_wits(deps, ".").await?;
791+
792+
let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
793+
794+
let (_rc_pkg_id, rc_pkg) = resolve
795+
.packages
796+
.iter()
797+
.find(|(_, p)| p.name.to_string() == "root:component")
798+
.expect("should have had `root:component`");
799+
800+
let root_world_id = rc_pkg
801+
.worlds
802+
.get("root")
803+
.expect("should have had root world");
804+
let root_world = resolve
805+
.worlds
806+
.get(*root_world_id)
807+
.expect("should have had root world at that id");
808+
809+
let func = root_world
810+
.imports
811+
.iter()
812+
.filter_map(|(_, wi)| as_func(wi))
813+
.find(|f| f.name == "color-name")
814+
.expect("color-name function does not appear in root imports");
815+
816+
assert_eq!(1, func.params.len());
817+
assert!(
818+
matches!(func.params.first().unwrap().ty, wit_parser::Type::Id(_)),
819+
"expected enum param to be Type::Id"
820+
);
821+
assert_eq!(wit_parser::Type::String, func.result.unwrap());
822+
823+
Ok(())
824+
}
496825
}

0 commit comments

Comments
 (0)