diff --git a/Cargo.lock b/Cargo.lock index c52cfb3b9..9d0f023d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "cpp_demangle" -version = "0.4.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" dependencies = [ "cfg-if", ] @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.133.1" +version = "0.134.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b4982ef9fa54ec9eee841e891e7ddc5434be1250e88de31572e000c888f30b" +checksum = "6867e99f068454b0c81c50b807af88aa80aaf383105e2be26675d81fc4278466" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.133.1" +version = "0.134.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529143118c4eeb58c39ecb02319557d512be6c61348486422974ab8e3906b8a8" +checksum = "3cf66017aed971344230199ad35ca01c955cf824a0e65fed5240452d2a302445" dependencies = [ "serde", "serde_derive", @@ -163,9 +163,9 @@ dependencies = [ [[package]] name = "cranelift-entity" -version = "0.133.1" +version = "0.134.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5300c49cf940526fe771517b3b3eabd5d0ff164ee61698579cf403fe8d3af3c" +checksum = "584ecabe1462593a9cba9efbf9022e0987421ddb548989b455ab8bdd444b743c" dependencies = [ "cranelift-bitset", "serde", @@ -541,12 +541,12 @@ dependencies = [ "heck 0.5.0", "log", "semver", - "wasm-encoder 0.253.0", - "wasmparser 0.253.0", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", "wasmtime-environ", "wit-bindgen-core", "wit-component", - "wit-parser 0.253.0", + "wit-parser 0.254.0", ] [[package]] @@ -1020,29 +1020,29 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasm-encoder" -version = "0.251.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser 0.251.0", + "wasmparser 0.252.0", ] [[package]] name = "wasm-encoder" -version = "0.253.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59972d6cd272259de647b7c1f1912e45e289c75ffd4be04e10695507cd7e1b59" +checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" dependencies = [ "leb128fmt", - "wasmparser 0.253.0", + "wasmparser 0.254.0", ] [[package]] name = "wasm-metadata" -version = "0.253.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3f45816ef616806f48498bcd831377de578c4fa51db0c83ab8ceb78cc13523b" +checksum = "b01df5f3b4ca7881e843f3bc0fb8a3905d79c68692250dcb8e33e698705ccdb6" dependencies = [ "anyhow", "auditable-serde", @@ -1053,8 +1053,8 @@ dependencies = [ "serde_json", "spdx", "url", - "wasm-encoder 0.253.0", - "wasmparser 0.253.0", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", ] [[package]] @@ -1062,14 +1062,14 @@ name = "wasm-tools-js" version = "0.1.0" dependencies = [ "anyhow", - "wasm-encoder 0.253.0", + "wasm-encoder 0.254.0", "wasm-metadata", - "wasmparser 0.253.0", - "wasmprinter 0.253.0", + "wasmparser 0.254.0", + "wasmprinter 0.254.0", "wat", "wit-bindgen", "wit-component", - "wit-parser 0.253.0", + "wit-parser 0.254.0", ] [[package]] @@ -1085,9 +1085,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.251.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ "bitflags 2.13.0", "hashbrown 0.17.1", @@ -1098,9 +1098,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.253.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19db11f87d2486580e1e8b6f494c54df7e0566b87d0b599db843c24019667339" +checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" dependencies = [ "bitflags 2.13.0", "hashbrown 0.17.1", @@ -1110,31 +1110,31 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.251.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8798c1a699bd25648b6708eefe94d97c6f9891febb94b42cca1f7a4b086ea64e" +checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.251.0", + "wasmparser 0.252.0", ] [[package]] name = "wasmprinter" -version = "0.253.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cafccdb2ba2cffd9c4f96b88a9710651d63266e858d54be262a141b528b26dac" +checksum = "64e3ba11e024f504698f87b629b2b66c22a1231758de7607754963f7d198be39" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.253.0", + "wasmparser 0.254.0", ] [[package]] name = "wasmtime-environ" -version = "46.0.1" +version = "47.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45863de41977ec6453e859cf843d456fa3fcb45a659b66d16e794f90ec4f5b7" +checksum = "9915e3d90c36a16e0fec8be746ee87e21cf0d4a017ecf09c49b7d1cc6f31b2b6" dependencies = [ "anyhow", "cpp_demangle", @@ -1154,25 +1154,26 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.251.0", - "wasmparser 0.251.0", - "wasmprinter 0.251.0", + "wasm-encoder 0.252.0", + "wasmparser 0.252.0", + "wasmprinter 0.252.0", "wasmtime-internal-component-util", "wasmtime-internal-core", ] [[package]] name = "wasmtime-internal-component-util" -version = "46.0.1" +version = "47.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819ad5abd5822a22dbf4014475cdfd1fe790707761cd732d74aaa3ba4d5ba489" +checksum = "f45bf943cff1d2f1976bac6a241ca4ecaf29a6efd5f1ad89e7722439fa5c392f" [[package]] name = "wasmtime-internal-core" -version = "46.0.1" +version = "47.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc28372e36eaf8cf70faa83b5779137f7e99c8d18569a125d1580e735cc9e4d" +checksum = "ea33e1f9685be82f90223bebcf2ffefb6b596ea3d82a3c42384527b9c6f5b447" dependencies = [ + "anyhow", "hashbrown 0.17.1", "libm", "serde", @@ -1180,22 +1181,22 @@ dependencies = [ [[package]] name = "wast" -version = "253.0.0" +version = "254.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3264542f8965c5d84fb1085d924bfba9a6314bb228eff13a2de14d7627664d0" +checksum = "e7ed4dfc8f6b9fc38b231065e2cdfbf7359af5ab945990abf09658dcc63c3e32" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.2", - "wasm-encoder 0.253.0", + "wasm-encoder 0.254.0", ] [[package]] name = "wat" -version = "1.253.0" +version = "1.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bfc5ce906144200c972ec617470aa35bd847472e170b26dde3e80541c674055" +checksum = "7127f7f9b8f127c879991cecd35f494e4628bae1b0874c681414d8d8831e952c" dependencies = [ "wast", ] @@ -1250,29 +1251,29 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c5e45f6d4cfaca727c1c48989ab3e05bb289bf84fbad226e1cfbbef2c04b7f" +checksum = "a301904d6657d6364c758d869e5389d05d393b16d5b65db60b4f03cbe71bb80d" dependencies = [ "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen-core" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cf25dc4bb1981c16aa549ec66c6762b7752bfb8b7c3b3936ae13100298dc72" +checksum = "48521bb96e56cbb9e031ad306c80dc20e89e69e49ada02781ee06f60fbcb545f" dependencies = [ "anyhow", "heck 0.5.0", - "wit-parser 0.253.0", + "wit-parser 0.254.0", ] [[package]] name = "wit-bindgen-rust" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "407ee2b474af1a366766fe91b8255b7935e5e3c3afb9c304e91ac3d154287ff1" +checksum = "3df3fe9b9a0066f82fd0c2f07f79d0ffc0f85c53fa5f998516d8722712479042" dependencies = [ "anyhow", "heck 0.5.0", @@ -1286,9 +1287,9 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" -version = "0.59.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37386eba32427684ffe37a0f765f63ce2ece03efb1ad28c23cf69562fdc67ec0" +checksum = "d689c4bc9d6af067c651cfba444766343c9a4bdef15fad1e2efab95c0c277af0" dependencies = [ "anyhow", "prettyplease", @@ -1301,9 +1302,9 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.253.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbd2500ac3488489ee8c6e59b79d7e47e6da5bfb019efd35d5dca57b78af624" +checksum = "b0e65bb94c369b3c4741ce3d1d2704b1fec93db7c540df0e521a097e7ceeb5be" dependencies = [ "anyhow", "bitflags 2.13.0", @@ -1312,11 +1313,11 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.253.0", + "wasm-encoder 0.254.0", "wasm-metadata", - "wasmparser 0.253.0", + "wasmparser 0.254.0", "wat", - "wit-parser 0.253.0", + "wit-parser 0.254.0", ] [[package]] @@ -1358,9 +1359,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.253.0" +version = "0.254.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d997b8e5920fcbeec742b58e583325d6419a6aca617ae8075c406a61c65ba8a" +checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -1372,7 +1373,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser 0.253.0", + "wasmparser 0.254.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f47215376..0a8dbc086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,21 +44,21 @@ tokio = { version = "1.50.0", default-features = false } webidl2wit = { version = "0.1.1", default-features = false } xshell = { version = "0.2.7", default-features = false } -wasmtime = { version = "46.0.1", default-features = false } -wasmtime-environ = { version = "46.0.1", features= [ "component-model", "compile" ] } +wasmtime = { version = "47.0.2", default-features = false } +wasmtime-environ = { version = "47.0.2", features= [ "component-model", "compile", "anyhow" ] } -wasm-encoder = { version = "0.253.0", default-features = false } -wasm-metadata = { version = "0.253.0", default-features = false } -wasmparser = { version = "0.253.0", default-features = false } -wasmprinter = { version = "0.253.0", default-features = false } -wit-component = { version = "0.253.0", features = ["dummy-module"] } -wit-parser = { version = "0.253.0", default-features = false } +wasm-encoder = { version = "0.254.0", default-features = false } +wasm-metadata = { version = "0.254.0", default-features = false } +wasmparser = { version = "0.254.0", default-features = false } +wasmprinter = { version = "0.254.0", default-features = false } +wit-component = { version = "0.254.0", features = ["dummy-module"] } +wit-parser = { version = "0.254.0", default-features = false } -wat = { version = "1.253.0", default-features = false } +wat = { version = "1.254.0", default-features = false } -wit-bindgen = { version = "0.59.0", default-features = false } -wit-bindgen-core = { version = "0.59.0", default-features = false } +wit-bindgen = { version = "0.60.0", default-features = false } +wit-bindgen-core = { version = "0.60.0", default-features = false } -wast = { version = "253.0.0", default-features = false, features = [ "component-model" ] } +wast = { version = "254.0.0", default-features = false, features = [ "component-model" ] } js-component-bindgen = { version = "2.0.11", path = "./crates/js-component-bindgen" } diff --git a/crates/js-component-bindgen/src/core.rs b/crates/js-component-bindgen/src/core.rs index 52b0a838f..6cdb196df 100644 --- a/crates/js-component-bindgen/src/core.rs +++ b/crates/js-component-bindgen/src/core.rs @@ -52,10 +52,6 @@ use wasmparser::{ use wasmtime_environ::component::CoreDef; use wasmtime_environ::{EntityIndex, MemoryIndex, ModuleTranslation, PrimaryMap}; -fn unimplemented_try_table() -> wasm_encoder::Instruction<'static> { - unimplemented!() -} - pub enum Translation<'a> { Normal(ModuleTranslation<'a>), Augmented { @@ -790,7 +786,7 @@ macro_rules! define_translate { (mk F32Const $v:ident) => (F32Const(Ieee32::from(f32::from_bits($v.bits())))); (mk F64Const $v:ident) => (F64Const(Ieee64::from(f64::from_bits($v.bits())))); (mk V128Const $v:ident) => (V128Const($v.i128())); - (mk TryTable $v:ident) => (unimplemented_try_table()); + (mk TryTable $v:ident) => (TryTable($v.0, $v.1)); (mk MemoryGrow $($x:tt)*) => ({ if true { unimplemented!() } Nop }); @@ -836,7 +832,7 @@ macro_rules! define_translate { $arg.targets().map(|i| i.unwrap()).collect::>().into(), $arg.default(), )); - (map $self:ident $arg:ident try_table) => {$arg}; + (map $self:ident $arg:ident try_table) => {$self.try_table($arg)}; (map $self:ident $arg:ident struct_type_index) => {$self.remap(Item::Type, $arg).unwrap()}; (map $self:ident $arg:ident field_index) => {$arg}; (map $self:ident $arg:ident array_type_index) => {$self.remap(Item::Type, $arg).unwrap()}; @@ -894,6 +890,31 @@ impl Translator<'_, '_> { wasmparser::BlockType::FuncType(i) => wasm_encoder::BlockType::FunctionType(i), } } + + /// Re-encode a `try_table` immediate. Wasmtime's FACT-generated adapters + /// wrap lowerings in exception barriers (`try_table` + `catch_all`), and + /// tags are not remapped by augmentation, so catches pass through as-is. + fn try_table( + &self, + tt: wasmparser::TryTable, + ) -> ( + wasm_encoder::BlockType, + std::borrow::Cow<'static, [wasm_encoder::Catch]>, + ) { + let catches = tt + .catches + .iter() + .map(|c| match *c { + wasmparser::Catch::One { tag, label } => wasm_encoder::Catch::One { tag, label }, + wasmparser::Catch::OneRef { tag, label } => { + wasm_encoder::Catch::OneRef { tag, label } + } + wasmparser::Catch::All { label } => wasm_encoder::Catch::All { label }, + wasmparser::Catch::AllRef { label } => wasm_encoder::Catch::AllRef { label }, + }) + .collect::>(); + (self.blockty(tt.ty), catches.into()) + } fn heapty(&self, _ty: wasmparser::HeapType) -> wasm_encoder::HeapType { unimplemented!() } diff --git a/crates/js-component-bindgen/src/lib.rs b/crates/js-component-bindgen/src/lib.rs index df086998e..cdbbbbac5 100644 --- a/crates/js-component-bindgen/src/lib.rs +++ b/crates/js-component-bindgen/src/lib.rs @@ -61,7 +61,7 @@ pub struct Transpiled { pub struct ComponentInfo { pub imports: Vec, - pub exports: Vec<(String, wasmtime_environ::component::Export)>, + pub exports: Vec<(String, transpile_bindgen::ExportKind)>, } pub fn generate_types( @@ -127,17 +127,27 @@ pub fn transpile(component: &[u8], opts: TranspileOpts) -> Result { // // This does not require the correct execution of the related features post-transpilation, // but without the right features specified, components won't load at all. - let mut validator = wasmtime_environ::wasmparser::Validator::new_with_features( - WasmFeatures::WASM3 - | WasmFeatures::WIDE_ARITHMETIC - | WasmFeatures::COMPONENT_MODEL - | WasmFeatures::CM_ASYNC - | WasmFeatures::CM_MORE_ASYNC_BUILTINS - | WasmFeatures::CM_ASYNC_STACKFUL - | WasmFeatures::CM_ERROR_CONTEXT - | WasmFeatures::CM_FIXED_LENGTH_LISTS - | WasmFeatures::CM_MAP, - ); + let mut features = WasmFeatures::WASM3 + | WasmFeatures::WIDE_ARITHMETIC + | WasmFeatures::COMPONENT_MODEL + | WasmFeatures::CM_ASYNC + | WasmFeatures::CM_MORE_ASYNC_BUILTINS + | WasmFeatures::CM_ASYNC_STACKFUL + | WasmFeatures::CM_ERROR_CONTEXT + | WasmFeatures::CM_FIXED_LENGTH_LISTS + | WasmFeatures::CM_MAP + | WasmFeatures::CM_IMPLEMENTS; + + // Unless the target engine is known to support the exception handling + // proposal, mask exception handling off: with the feature enabled, + // wasmtime-environ's FACT-generated adapters wrap calls in exception + // barriers (`try_table`), which only runs behind a flag (e.g. + // --experimental-wasm-exnref) in today's JS engines. + if !opts.supports_wasm_exnref { + features = features.difference(WasmFeatures::EXCEPTIONS); + } + + let mut validator = wasmtime_environ::wasmparser::Validator::new_with_features(features); let mut types = ComponentTypesBuilder::new(&validator); diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index f969c1f3b..11cc7aa84 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -17,7 +17,8 @@ use wasmtime_environ::component::{ TypeResourceTableIndex, TypeStreamTableIndex, }; use wasmtime_environ::component::{ - ExtractCallback, NameMapNoIntern, Transcode, TypeComponentLocalErrorContextTableIndex, + ExtractCallback, ImportIndex, NameMapNoIntern, Transcode, + TypeComponentLocalErrorContextTableIndex, }; use wasmtime_environ::{EntityIndex, PrimaryMap}; use wit_bindgen_core::abi::{self, LiftLower}; @@ -116,6 +117,15 @@ pub struct TranspileOpts { /// Whether the core module(s) to be wrapped were actually transpiled from Wasm to JS (asm.js) and thus need shimming for i64 #[builder(default)] pub asmjs: bool, + /// Whether the target JS engine supports the exception handling proposal + /// (`try_table`/exnref). + /// + /// When disabled (the default), the exceptions feature is masked off + /// during component validation so that wasmtime-environ's FACT-generated + /// adapters do not wrap calls in exception barriers, which would only run + /// behind a flag in today's JS engines. + #[builder(default)] + pub supports_wasm_exnref: bool, } #[derive(Default, Clone, Debug)] @@ -1163,10 +1173,11 @@ impl<'a> Instantiator<'a, '_> { let mut instance_flag_defs = String::new(); for used in self.used_instance_flags.borrow().iter() { let i = used.as_u32(); + // As of wasmtime-environ 47 the per-instance flags global holds a + // single boolean `may_leave` flag, so initialize it to `1`. uwriteln!( &mut instance_flag_defs, - "const instanceFlags{i} = new WebAssembly.Global({{ value: \"i32\", mutable: true }}, {});", - wasmtime_environ::component::FLAG_MAY_LEAVE + "const instanceFlags{i} = new WebAssembly.Global({{ value: \"i32\", mutable: true }}, 1);", ); } self.src.js_init.prepend_str(&instance_flag_defs); @@ -2981,6 +2992,90 @@ impl<'a> Instantiator<'a, '_> { .0 } + /// Returns the local JS class name for an imported resource attached to + /// the lowered import `import_index` (as the receiver class of a + /// method/static or the target of a constructor). + /// + /// The same interface -- and thus the same `wit-parser` resource type -- + /// may be imported more than once under different labels via the + /// component model `implements` feature, each import with its own + /// resource table. The class must therefore be resolved through the + /// specific import instance rather than through the wit type, which is + /// shared by all labels. + fn imported_resource_name(&mut self, import_index: ImportIndex, resource: TypeId) -> String { + let resolve = self.resolve; + let types = self.types; + let component = self.component; + let resource = crate::dealias(resolve, resource); + let resource_wit_name = resolve.types[resource].name.as_ref().unwrap(); + if let ( + _, + ComponentExtern { + ty: TypeDef::ComponentInstance(inst), + .. + }, + ) = &component.import_types[import_index] + && let Some(ComponentExtern { + ty: TypeDef::Resource(rt_idx), + .. + }) = types[*inst].exports.get(resource_wit_name) + { + let rid = types[*rt_idx].unwrap_concrete_ty(); + return self + .bindgen + .local_names + .get_or_create(rid, &resource_wit_name.to_upper_camel_case()) + .0 + .to_string(); + } + // World-level resource imports (and any other shape) are uniquely + // identified by their wit type. + Instantiator::resource_name( + resolve, + &mut self.bindgen.local_names, + resource, + &self.imports_resource_types, + ) + .to_string() + } + + /// Finds the component import that provides the given resource table. + /// + /// Returns the import name along with whether the resource is provided + /// by an imported instance (`true`) or directly by a world-level + /// resource import (`false`). + /// + /// The same interface may be imported under multiple labels (component + /// model `implements` feature), each label with its own resource table, + /// so the import is identified through the wasmtime resource index + /// rather than through the wit type's owning interface, which is shared + /// by all labels. + fn find_import_providing_resource( + &self, + resource_idx: ResourceIndex, + ) -> Option<(&'a str, bool)> { + let component = self.component; + let types = self.types; + for (_, (imp_name, extern_)) in component.import_types.iter() { + match &extern_.ty { + TypeDef::ComponentInstance(inst) => { + for (_, export) in types[*inst].exports.iter() { + if let TypeDef::Resource(rt) = &export.ty + && types[*rt].unwrap_concrete_ty() == resource_idx + { + return Some((imp_name.as_str(), true)); + } + } + } + TypeDef::Resource(rt) if types[*rt].unwrap_concrete_ty() == resource_idx => { + return Some((imp_name.as_str(), false)); + } + _ => {} + } + } + None + } + fn lower_import(&mut self, index: LoweredIndex, import: RuntimeImportIndex) { let (options, trampoline, func_ty) = self.lowering_options[index]; @@ -3025,8 +3120,17 @@ impl<'a> Instantiator<'a, '_> { &self.async_imports, ); + // A labeled import of a named interface (the component model + // `implements` feature) falls back to a mapping for the implemented + // interface id when the label itself has no mapping, so that e.g. + // WASI shim mappings apply to labeled imports as well. + let implements = self.resolve.implements_value( + world_key, + &self.resolve.worlds[self.world].imports[world_key], + ); + // Nested interfaces only currently possible through mapping - let (import_specifier, maybe_iface_member) = map_import( + let (import_specifier, maybe_iface_member) = map_import_with_implements( &self.bindgen.opts.map, if iface_name.is_some() { import_name @@ -3054,6 +3158,7 @@ impl<'a> Instantiator<'a, '_> { FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => import_name, } }, + implements.as_deref(), ); // Create mappings for resources @@ -3109,12 +3214,7 @@ impl<'a> Instantiator<'a, '_> { FunctionKind::Static(resource_id) => ( format!( "{}.{}", - Instantiator::resource_name( - self.resolve, - &mut self.bindgen.local_names, - resource_id, - &self.imports_resource_types - ), + self.imported_resource_name(*import_index, resource_id), func.item_name().to_lower_camel_case() ), CallType::Standard, @@ -3123,12 +3223,7 @@ impl<'a> Instantiator<'a, '_> { FunctionKind::AsyncStatic(resource_id) => ( format!( "{}.{}", - Instantiator::resource_name( - self.resolve, - &mut self.bindgen.local_names, - resource_id, - &self.imports_resource_types - ), + self.imported_resource_name(*import_index, resource_id), func.item_name().to_lower_camel_case() ), CallType::AsyncStandard, @@ -3137,12 +3232,7 @@ impl<'a> Instantiator<'a, '_> { FunctionKind::Constructor(resource_id) => ( format!( "new {}", - Instantiator::resource_name( - self.resolve, - &mut self.bindgen.local_names, - resource_id, - &self.imports_resource_types - ) + self.imported_resource_name(*import_index, resource_id) ), CallType::Standard, ), @@ -3266,12 +3356,7 @@ impl<'a> Instantiator<'a, '_> { FunctionKind::Method(resource_id) | FunctionKind::AsyncMethod(resource_id) => { format!( "{}.prototype.{callee_name}", - Instantiator::resource_name( - self.resolve, - &mut self.bindgen.local_names, - resource_id, - &self.imports_resource_types - ) + self.imported_resource_name(*import_index, resource_id) ) } }; @@ -3352,13 +3437,7 @@ impl<'a> Instantiator<'a, '_> { | FunctionKind::Constructor(tid) => { let ty = &self.resolve.types[tid]; let class_name = ty.name.as_ref().unwrap().to_upper_camel_case(); - let resource_name = Instantiator::resource_name( - self.resolve, - &mut self.bindgen.local_names, - tid, - &self.imports_resource_types, - ) - .to_string(); + let resource_name = self.imported_resource_name(*import_index, tid); (class_name, resource_name) } }; @@ -3598,44 +3677,86 @@ impl<'a> Instantiator<'a, '_> { let resource_name = ty.name.as_ref().unwrap().to_upper_camel_case(); let local_name = if imported { - let (world_key, iface_name) = match ty.owner { - wit_parser::TypeOwner::World(world) => ( - self.resolve.worlds[world] - .imports - .iter() - .find(|&(_, item)| matches!(*item, WorldItem::Type { id, .. } if id == t)) - .unwrap() - .0 - .clone(), - None, - ), - wit_parser::TypeOwner::Interface(iface) => { - match &self.resolve.interfaces[iface].name { - Some(name) => (WorldKey::Interface(iface), Some(name.as_str())), - None => { - let key = self.resolve.worlds[self.world] - .imports - .iter() - .find(|&(_, item)| match item { - WorldItem::Interface { id, .. } => *id == iface, - _ => false, - }) - .unwrap() - .0; - ( - key.clone(), - match key { - WorldKey::Name(name) => Some(name.as_str()), - WorldKey::Interface(_) => None, - }, - ) + // Resolve the specific import that provides this resource table: + // see [`Instantiator::find_import_providing_resource`]. + let imported_resource_entry = self.find_import_providing_resource(resource_idx); + + let (world_key, iface_name) = match imported_resource_entry { + // Resource provided by an imported instance: derive the + // interface member name from the world key shape. + Some((imp_name, _is_from_instance @ true)) => { + let key = self.imports[imp_name].clone(); + let iface_name = match &key { + WorldKey::Name(name) => Some(name.clone()), + WorldKey::Interface(_) => { + match &self.resolve.worlds[self.world].imports[&key] { + WorldItem::Interface { id, .. } => { + self.resolve.interfaces[*id].name.clone() + } + _ => None, + } } - } + }; + (key, iface_name) + } + // A world-level `import x: resource` style import + Some((imp_name, _is_from_instance @ false)) => { + (self.imports[imp_name].clone(), None) } - wit_parser::TypeOwner::None => unimplemented!(), + // Fall back to locating the import through the wit type's + // owner; reachable only if the resource table doesn't appear + // in the component's import types. + None => match ty.owner { + wit_parser::TypeOwner::World(world) => ( + self.resolve.worlds[world] + .imports + .iter() + .find( + |&(_, item)| matches!(item, WorldItem::Type { id, .. } if *id == t), + ) + .unwrap() + .0 + .clone(), + None, + ), + wit_parser::TypeOwner::Interface(iface) => { + let key = self.resolve.worlds[self.world] + .imports + .iter() + .find(|&(_, item)| match item { + WorldItem::Interface { id, .. } => *id == iface, + _ => false, + }) + .map(|(key, _)| key) + .unwrap_or_else(|| { + panic!( + "unable to find world import for interface [{}]", + self.resolve.interfaces[iface] + .name + .as_deref() + .unwrap_or("") + ) + }); + ( + key.clone(), + match key { + WorldKey::Name(name) => Some(name.clone()), + WorldKey::Interface(_) => { + self.resolve.interfaces[iface].name.clone() + } + }, + ) + } + wit_parser::TypeOwner::None => unimplemented!(), + }, }; + let iface_name = iface_name.as_deref(); let import_name = self.resolve.name_world_key(&world_key); + let implements = self.resolve.worlds[self.world] + .imports + .get(&world_key) + .and_then(|item| self.resolve.implements_value(&world_key, item)); let (local_name, _) = self .bindgen .local_names @@ -3643,9 +3764,14 @@ impl<'a> Instantiator<'a, '_> { let local_name_str = local_name.to_string(); - // Nested interfaces only currently possible through mapping - let (import_specifier, maybe_iface_member) = - map_import(&self.bindgen.opts.map, &import_name); + // Nested interfaces only currently possible through mapping; must + // resolve to the same specifier as the owning interface's + // functions, including the `implements` mapping fallback. + let (import_specifier, maybe_iface_member) = map_import_with_implements( + &self.bindgen.opts.map, + &import_name, + implements.as_deref(), + ); // Ensure that the import exists self.ensure_import( @@ -3678,7 +3804,17 @@ impl<'a> Instantiator<'a, '_> { // If the the resource already exists, then ensure that it is exactly the same as the // value we're attempting to insert if let Some(existing) = resource_map.get(&resource_id) { - assert_eq!(*existing, entry); + // The same wit resource type may be imported more than once under + // different labels (component model `implements` feature), giving + // it one resource table per label. Shared type-keyed maps keep + // the first table encountered; maps built per lowered function + // only ever see the table of that function's own instance. + if *existing != entry { + assert!( + imported && existing.imported, + "conflicting resource tables for non-imported resource" + ); + } return; } @@ -4809,6 +4945,33 @@ fn resolve_wildcard_mapping(key: &str, mapping: &str, impt: &str) -> Option>, + impt: &str, + implements: Option<&str>, +) -> (String, Option) { + let (specifier, iface_member) = map_import(map, impt); + if specifier == impt + && iface_member.is_none() + && let Some(target) = implements + { + let (mapped, member) = map_import(map, target); + // An unmapped name is returned with just its version stripped + let target_sans_version = match target.find('@') { + Some(version_idx) => &target[0..version_idx], + None => target, + }; + if mapped != target_sans_version || member.is_some() { + return (mapped, member); + } + } + (specifier, iface_member) +} + fn map_import(map: &Option>, impt: &str) -> (String, Option) { let impt_sans_version = match impt.find('@') { Some(version_idx) => &impt[0..version_idx], diff --git a/crates/js-component-bindgen/src/ts_bindgen.rs b/crates/js-component-bindgen/src/ts_bindgen.rs index aa1163740..22fcf1e9a 100644 --- a/crates/js-component-bindgen/src/ts_bindgen.rs +++ b/crates/js-component-bindgen/src/ts_bindgen.rs @@ -547,10 +547,14 @@ impl TsBindgen { files, GeneratedTypeMeta { is_export: false }, ); + // Alias under the import's own name: a named interface may be + // imported under one or more plain labels via the component model + // `implements` feature, in which case each label gets its own + // alias of the shared interface module. uwriteln!( self.src, "export type * as {} from '{import_path}'; // import {}", - id_name.to_upper_camel_case(), + name.to_upper_camel_case(), id_name ); } diff --git a/crates/test-components/Cargo.lock b/crates/test-components/Cargo.lock index 116d59a6b..582b2b135 100644 --- a/crates/test-components/Cargo.lock +++ b/crates/test-components/Cargo.lock @@ -210,7 +210,7 @@ dependencies = [ "cargo_metadata", "heck", "tokio", - "wit-component", + "wit-component 0.251.0", ] [[package]] @@ -452,6 +452,16 @@ dependencies = [ "wasmparser 0.252.0", ] +[[package]] +name = "wasm-encoder" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" +dependencies = [ + "leb128fmt", + "wasmparser 0.254.0", +] + [[package]] name = "wasm-metadata" version = "0.251.0" @@ -464,6 +474,18 @@ dependencies = [ "wasmparser 0.251.0", ] +[[package]] +name = "wasm-metadata" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01df5f3b4ca7881e843f3bc0fb8a3905d79c68692250dcb8e33e698705ccdb6" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", +] + [[package]] name = "wasmparser" version = "0.251.0" @@ -486,6 +508,18 @@ dependencies = [ "indexmap", ] +[[package]] +name = "wasmparser" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + [[package]] name = "wast" version = "252.0.0" @@ -525,9 +559,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a43552cfa071f246cfd99e5dbb23710dfe7336b3259e09339818483359470749" +checksum = "a301904d6657d6364c758d869e5389d05d393b16d5b65db60b4f03cbe71bb80d" dependencies = [ "futures", "wit-bindgen-rust-macro", @@ -535,36 +569,36 @@ dependencies = [ [[package]] name = "wit-bindgen-core" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4738d1c9a78e97bc7f664bfafd5d8e67d7bb26faa5c41e6d628e8bbdad3ec351" +checksum = "48521bb96e56cbb9e031ad306c80dc20e89e69e49ada02781ee06f60fbcb545f" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.254.0", ] [[package]] name = "wit-bindgen-rust" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1130ce1f531bc9f9a75922244aa773bf5e2117fda1ef4a86b9f98d6b8135eb46" +checksum = "3df3fe9b9a0066f82fd0c2f07f79d0ffc0f85c53fa5f998516d8722712479042" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", - "wasm-metadata", + "wasm-metadata 0.254.0", "wit-bindgen-core", - "wit-component", + "wit-component 0.254.0", ] [[package]] name = "wit-bindgen-rust-macro" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07296369e4d598e7e79b64eef66f724d83324ea671bcf677d78fc5cf92604ae5" +checksum = "d689c4bc9d6af067c651cfba444766343c9a4bdef15fad1e2efab95c0c277af0" dependencies = [ "anyhow", "prettyplease", @@ -589,10 +623,29 @@ dependencies = [ "serde_derive", "serde_json", "wasm-encoder 0.251.0", - "wasm-metadata", + "wasm-metadata 0.251.0", "wasmparser 0.251.0", "wat", - "wit-parser", + "wit-parser 0.251.0", +] + +[[package]] +name = "wit-component" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0e65bb94c369b3c4741ce3d1d2704b1fec93db7c540df0e521a097e7ceeb5be" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.254.0", + "wasm-metadata 0.254.0", + "wasmparser 0.254.0", + "wit-parser 0.254.0", ] [[package]] @@ -614,6 +667,25 @@ dependencies = [ "wasmparser 0.251.0", ] +[[package]] +name = "wit-parser" +version = "0.254.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.254.0", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/crates/test-components/Cargo.toml b/crates/test-components/Cargo.toml index 94be0e089..eee456b4a 100644 --- a/crates/test-components/Cargo.toml +++ b/crates/test-components/Cargo.toml @@ -26,5 +26,5 @@ futures = { version = "0.3.32", default-features = false } heck = { version = "0.5.0", default-features = false } tokio = { version = "1.50.0", default-features = false } wasmtime = { version = "45.0.1", default-features = false } -wit-bindgen = { version = "0.58.0", default-features = false } +wit-bindgen = { version = "0.60.0", default-features = false } wit-component = { version = "0.251.0", default-features = false } diff --git a/crates/test-components/artifacts/build.rs b/crates/test-components/artifacts/build.rs index 28fd643db..774d25ae2 100644 --- a/crates/test-components/artifacts/build.rs +++ b/crates/test-components/artifacts/build.rs @@ -51,8 +51,8 @@ async fn main() -> Result<()> { .context("failed to build artifacts")?; // Copy all p3 components to packages/jco/output - let rust_test_components_dir = - cargo_manifest_dir.join("../../../packages/jco-transpile/test/fixtures/generated/rust-test-components"); + let rust_test_components_dir = cargo_manifest_dir + .join("../../../packages/jco-transpile/test/fixtures/generated/rust-test-components"); tokio::fs::create_dir_all(&rust_test_components_dir) .await .context("failed to create rust test component output dir")?; diff --git a/crates/test-components/src/bin/implements_labels.rs b/crates/test-components/src/bin/implements_labels.rs new file mode 100644 index 000000000..16d309766 --- /dev/null +++ b/crates/test-components/src/bin/implements_labels.rs @@ -0,0 +1,28 @@ +//! Exercises the component model `implements` feature: the same interface +//! imported under two different labels, and exported under a label. + +mod bindings { + use super::Component; + wit_bindgen::generate!({ + world: "implements-labels", + }); + export!(Component); +} + +struct Component; + +impl bindings::Guest for Component { + fn echo_both(msg: String) -> (String, String) { + (bindings::first::echo(&msg), bindings::second::echo(&msg)) + } +} + +impl bindings::exports::relay::Guest for Component { + fn echo(msg: String) -> String { + // Relay through the `first` labeled import + bindings::first::echo(&msg) + } +} + +// Stub only to ensure this works as a binary +fn main() {} diff --git a/crates/test-components/wit/all.wit b/crates/test-components/wit/all.wit index 93b2c6634..07f4d40b6 100644 --- a/crates/test-components/wit/all.wit +++ b/crates/test-components/wit/all.wit @@ -636,3 +636,14 @@ world cm-maps { import cm-maps-host; export cm-maps-api; } + +interface echoer { + echo: func(msg: string) -> string; +} + +world implements-labels { + import first: echoer; + import second: echoer; + export relay: echoer; + export echo-both: func(msg: string) -> tuple; +} diff --git a/packages/jco-transpile/test/cm/implements.ts b/packages/jco-transpile/test/cm/implements.ts new file mode 100644 index 000000000..b4aee5032 --- /dev/null +++ b/packages/jco-transpile/test/cm/implements.ts @@ -0,0 +1,205 @@ +/* global Buffer */ +import { join, dirname } from 'node:path'; +import { mkdir, writeFile, rm } from 'node:fs/promises'; +import { pathToFileURL } from 'node:url'; + +import { suite, test, assert } from 'vitest'; + +import { WIT_FIXTURE_DIR, getTmpDir, setupAsyncTest } from '../helpers.js'; +import { LOCAL_TEST_COMPONENTS_DIR } from '../common.js'; +import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation'; + +import { transpileBytes } from '../../src/index.js'; +import { componentEmbed, componentNew, componentWit, print } from '../../src/wasm-tools.js'; + +const IMPLEMENTS_WIT_DIR = join(WIT_FIXTURE_DIR, 'implements'); + +async function buildDummyComponent(worldName: string): Promise { + const embedded = await componentEmbed({ + dummy: true, + witPath: IMPLEMENTS_WIT_DIR, + world: worldName, + }); + return await componentNew(embedded); +} + +/** Write a transpiled file map plus extra stub modules into a fresh tmp dir + * and dynamically import the given entrypoint, running its instantiation. */ +async function importTranspiled( + files: Record, + stubs: Record, + entrypoint: string, + run: (mod: Record) => void | Promise, +) { + const dir = await getTmpDir(); + try { + for (const [name, bytes] of Object.entries(files)) { + const path = join(dir, name); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, bytes); + } + for (const [name, source] of Object.entries(stubs)) { + await writeFile(join(dir, name), source); + } + const mod = await import(pathToFileURL(join(dir, entrypoint)).href); + await run(mod); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +const STORE_STUB = `export class Bucket { get(k) { return undefined; } set(k, v) {} } +export function open(name) { return new Bucket(); }`; + +suite('Implements (labeled imports/exports)', () => { + test.concurrent('labeled imports & exports (e2e)', async () => { + const { cleanup, instance } = await setupAsyncTest({ + component: { + path: join(LOCAL_TEST_COMPONENTS_DIR, 'implements-labels.wasm'), + imports: { + ...new WASIShim().getImportObject(), + first: { echo: (msg: string) => `first:${msg}` }, + second: { echo: (msg: string) => `second:${msg}` }, + }, + }, + }); + + // Each labeled import routes to its own implementation + assert.deepEqual(instance.echoBoth('hello'), ['first:hello', 'second:hello']); + // The labeled export relays through the `first` labeled import + assert.strictEqual(instance.relay.echo('hi'), 'first:hi'); + + await cleanup(); + }); + + test.concurrent('binary encoding carries implements annotations', async () => { + const component = await buildDummyComponent('imports-labeled'); + const output = await print(component); + assert.ok( + output.includes('(import "primary" (implements "test:implements/logger")'), + 'labeled import `primary` carries an implements annotation', + ); + assert.ok( + output.includes('(import "secondary" (implements "test:implements/logger")'), + 'the same interface may be imported under a second label', + ); + assert.ok( + output.includes('(import "kv" (implements "test:implements/store")'), + 'resource-bearing labeled import carries an implements annotation', + ); + assert.ok( + output.includes('"events" (implements "test:implements/logger")'), + 'labeled export carries an implements annotation', + ); + }); + + test.concurrent('transpile labeled imports', async () => { + const component = await buildDummyComponent('imports-labeled'); + const { files, imports, exports } = await transpileBytes(component, { + name: 'labeled', + }); + assert.deepStrictEqual([...imports].sort(), ['kv', 'primary', 'secondary']); + assert.deepStrictEqual(exports, [['events', 'instance']]); + const source = Buffer.from(files['labeled.js']).toString(); + assert.ok(source.includes("from 'primary'")); + assert.ok(source.includes("from 'secondary'")); + // The bucket resource binds to the `kv` label import + assert.ok(source.includes("from 'kv'")); + }); + + test.concurrent('transpile labeled imports with interface-id mapping', async () => { + const component = await buildDummyComponent('imports-labeled'); + const { files, imports } = await transpileBytes(component, { + name: 'labeled', + map: { + 'test:implements/logger': './my-logger.js', + secondary: './secondary.js', + }, + }); + // A mapping for the implemented interface id applies to labeled + // imports of that interface, while a label-specific mapping wins. + assert.ok(imports.includes('./my-logger.js')); + assert.ok(imports.includes('./secondary.js')); + assert.ok(!imports.includes('primary')); + const source = Buffer.from(files['labeled.js']).toString(); + assert.ok(source.includes("from './my-logger.js'")); + assert.ok(source.includes("from './secondary.js'")); + }); + + test.concurrent('transpile same resource interface under multiple labels', async () => { + const component = await buildDummyComponent('multi-label-stores'); + const { files, imports } = await transpileBytes(component, { + name: 'multistore', + emitTypescriptDeclarations: false, + map: { + a: './a-store.js', + b: './b-store.js', + }, + }); + assert.deepStrictEqual([...imports].sort(), ['./a-store.js', './b-store.js']); + const source = Buffer.from(files['multistore.js']).toString(); + // Each label binds its own resource class from its own module + const aImport = source.match(/import \{([^}]*)\} from '\.\/a-store\.js'/); + const bImport = source.match(/import \{([^}]*)\} from '\.\/b-store\.js'/); + assert.ok(aImport && bImport, 'both labels are imported'); + const bucketEntries = (importList: string) => + importList.split(',').filter((entry) => entry.trim().startsWith('Bucket')).length; + assert.strictEqual(bucketEntries(aImport[1]), 1, 'label `a` imports exactly one Bucket class binding'); + assert.strictEqual(bucketEntries(bImport[1]), 1, 'label `b` imports exactly one Bucket class binding'); + // The generated module parses and instantiates with per-label stubs + await importTranspiled( + files, + { 'a-store.js': STORE_STUB, 'b-store.js': STORE_STUB }, + 'multistore.js', + (mod) => { + assert.ok(mod, 'module instantiated'); + }, + ); + }); + + test.concurrent('instantiates labeled imports at runtime', async () => { + const component = await buildDummyComponent('imports-labeled'); + const { files } = await transpileBytes(component, { + name: 'labeled', + emitTypescriptDeclarations: false, + map: { + primary: './log.js', + secondary: './log.js', + kv: './store.js', + }, + }); + await importTranspiled( + files, + { + 'log.js': 'export function log(msg) {}', + 'store.js': STORE_STUB, + }, + 'labeled.js', + (mod) => { + assert.deepStrictEqual(Object.keys(mod), ['events']); + assert.strictEqual(typeof (mod.events as Record).log, 'function'); + }, + ); + }); +}); + +suite('External IDs', () => { + test.concurrent('binary encoding carries external-id annotations', async () => { + const component = await buildDummyComponent('with-external-ids'); + const output = await print(component); + assert.ok(output.includes('(external-id "com.example:ids/primary")'), 'labeled import carries its external-id'); + assert.ok(output.includes('(external-id "com.example:ids/events")'), 'labeled export carries its external-id'); + }); + + test.concurrent('external-id round-trips through embed/new/wit', async () => { + const component = await buildDummyComponent('with-external-ids'); + // Both explicitly-annotated items keep their IDs; the unannotated + // sibling import has none. + const output = await print(component); + assert.strictEqual((output.match(/external-id/g) || []).length, 2); + // WIT extracted back from the binary retains the attributes + const wit = await componentWit(component); + assert.strictEqual((wit.match(/@external-id\(/g) || []).length, 2); + assert.ok(wit.includes('import primary: test:implements/logger')); + }); +}); diff --git a/packages/jco-transpile/test/fixtures/wit/implements/main.wit b/packages/jco-transpile/test/fixtures/wit/implements/main.wit new file mode 100644 index 000000000..888c61a62 --- /dev/null +++ b/packages/jco-transpile/test/fixtures/wit/implements/main.wit @@ -0,0 +1,40 @@ +package test:implements; + +interface logger { + log: func(msg: string); +} + +interface store { + resource bucket { + get: func(key: string) -> option; + set: func(key: string, value: string); + } + open: func(name: string) -> bucket; +} + +/// A world exercising the component model `implements` feature 🏷️: +/// named (labeled) imports/exports of interfaces, allowing the same +/// interface to be imported more than once under different labels. +world imports-labeled { + import primary: logger; + import secondary: logger; + import kv: store; + export events: logger; +} + +/// Same shape with @external-id attributes attached to labeled +/// imports/exports. +world with-external-ids { + @external-id("com.example:ids/primary") + import primary: logger; + import secondary: logger; + @external-id("com.example:ids/events") + export events: logger; +} + +/// The same resource-bearing interface imported under multiple labels: +/// each label must get its own resource table and class binding. +world multi-label-stores { + import a: store; + import b: store; +} diff --git a/packages/jco-transpile/test/typegen.ts b/packages/jco-transpile/test/typegen.ts index eea9d46aa..e222cfcaf 100644 --- a/packages/jco-transpile/test/typegen.ts +++ b/packages/jco-transpile/test/typegen.ts @@ -88,4 +88,31 @@ suite('Type Generation', () => { assert.include(worldDeclarationContent, 'export type Result'); assert.include(worldDeclarationContent, 'declare module'); }); + + // Labeled imports via the component model `implements` feature alias + // each label to the shared interface module. + test('labeled imports (implements) alias each label', async () => { + const files = await generateHostTypes(`${WIT_FIXTURE_DIR}/implements`, { + worldName: 'test:implements/imports-labeled', + }); + const worldDtsKey = Object.keys(files).find((f) => f.endsWith('imports-labeled.d.ts')); + assert.ok(worldDtsKey, 'world d.ts was generated'); + const worldDts = Buffer.from(files[worldDtsKey]).toString(); + assert.include(worldDts, "export type * as Primary from './interfaces/test-implements-logger.js'"); + assert.include(worldDts, "export type * as Secondary from './interfaces/test-implements-logger.js'"); + assert.include(worldDts, "export type * as Kv from './interfaces/test-implements-store.js'"); + // The shared interface module is generated once + assert.ok(files['interfaces/test-implements-logger.d.ts']); + }); + + test('worlds with external-ids', async () => { + const files = await generateHostTypes(`${WIT_FIXTURE_DIR}/implements`, { + worldName: 'test:implements/with-external-ids', + }); + const worldDtsKey = Object.keys(files).find((f) => f.endsWith('with-external-ids.d.ts')); + assert.ok(worldDtsKey, 'world d.ts was generated'); + const worldDts = Buffer.from(files[worldDtsKey]).toString(); + assert.include(worldDts, 'export type * as Primary'); + assert.include(worldDts, 'export type * as Secondary'); + }); });