From 62a5013b43692cfd43f203c02bb79147859fcf5b Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 18 Mar 2025 14:02:03 +0000 Subject: [PATCH 1/3] init --- crates/backend/src/codegen.rs | 111 ++++++- crates/cli-support/src/descriptor.rs | 65 +++- crates/cli-support/src/intrinsic.rs | 4 +- crates/cli-support/src/js/binding.rs | 200 ++++++++++-- crates/cli-support/src/wit/incoming.rs | 3 + crates/cli-support/src/wit/mod.rs | 34 ++ crates/cli-support/src/wit/outgoing.rs | 125 ++++++++ crates/cli-support/src/wit/standard.rs | 11 + crates/shared/src/identifier.rs | 5 + crates/typescript-tests/src/lib.rs | 1 + crates/typescript-tests/src/ts_generic_sig.rs | 291 ++++++++++++++++++ crates/typescript-tests/src/ts_generic_sig.ts | 88 ++++++ .../guide-supported-types-examples/Cargo.toml | 2 + 13 files changed, 893 insertions(+), 47 deletions(-) create mode 100644 crates/typescript-tests/src/ts_generic_sig.rs create mode 100644 crates/typescript-tests/src/ts_generic_sig.ts diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index 73056b70390..c7e906c09c6 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -9,8 +9,8 @@ use quote::{quote, ToTokens}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use syn::parse_quote; -use syn::spanned::Spanned; -use wasm_bindgen_shared as shared; +use syn::{spanned::Spanned, PathArguments, PathSegment, Type}; +use wasm_bindgen_shared::{self as shared, identifier::TYPE_DESCRIBE_MAP}; /// A trait for converting AST structs into Tokens and adding them to a TokenStream, /// or providing a diagnostic if conversion fails. @@ -863,22 +863,30 @@ impl TryToTokens for ast::Export { }) .to_tokens(into); + let mut args_ty_map = Vec::new(); let describe_args: TokenStream = argtys .iter() - .map(|ty| match ty { - syn::Type::Reference(reference) - if self.function.r#async && reference.mutability.is_none() => - { - let inner = &reference.elem; - quote! { - inform(LONGREF); - <#inner as WasmDescribe>::describe(); + .map(|ty| { + args_ty_map.push(map_type_describe(ty)); + match ty { + syn::Type::Reference(reference) + if self.function.r#async && reference.mutability.is_none() => + { + let inner = &reference.elem; + quote! { + inform(LONGREF); + <#inner as WasmDescribe>::describe(); + } } + _ => quote! { <#ty as WasmDescribe>::describe(); }, } - _ => quote! { <#ty as WasmDescribe>::describe(); }, }) .collect(); + // build the return type map and args type map + let ret_ty_map = map_type_describe(syn_ret); + let args_ty_map: TokenStream = args_ty_map.into_iter().collect(); + // In addition to generating the shim function above which is what // our generated JS will invoke, we *also* generate a "descriptor" // shim. This descriptor shim uses the `WasmDescribe` trait to @@ -904,6 +912,10 @@ impl TryToTokens for ast::Export { inform(#nargs); #describe_args #describe_ret + inform(#TYPE_DESCRIBE_MAP); + #ret_ty_map + inform(#TYPE_DESCRIBE_MAP); + #args_ty_map }, attrs: attrs.clone(), wasm_bindgen: &self.wasm_bindgen, @@ -1986,3 +1998,80 @@ fn respan(input: TokenStream, span: &dyn ToTokens) -> TokenStream { } new_tokens.into_iter().collect() } + +/// Recursively maps the given type to its WasmDescribe sequence +pub fn map_type_describe(typ: &Type) -> TokenStream { + let default = quote! { + inform(0u32); + inform(EXTERNREF); + }; + match typ { + Type::Path(type_path) => { + if let Some(PathSegment { ident, arguments }) = &type_path.path.segments.last() { + match arguments { + // this is the end point where a type has no more nested types + PathArguments::None => quote! { + inform(0u32); + <#type_path as WasmDescribe>::describe(); + }, + PathArguments::AngleBracketed(angle_bracket) => { + // process each generic type recursively + let mut len = angle_bracket.args.len() as u32; + // for "Result" the second arg usually does not impl WasmDescribe + // and is not needed for ts typings either, so we skip over it + if ident == "Result" { + len -= 1; + }; + let mut generics = vec![quote! { + inform(#len); + <#type_path as WasmDescribe>::describe(); + }]; + for arg in &angle_bracket.args { + if len == 0 { + break; + } + if let syn::GenericArgument::Type(generic) = arg { + generics.push(map_type_describe(generic)); + } + len -= 1; + } + generics.into_iter().collect() + } + PathArguments::Parenthesized(_) => default, + } + } else { + default + } + } + Type::Ptr(type_ptr) => map_type_describe(&type_ptr.elem), + Type::Reference(type_reference) => map_type_describe(&type_reference.elem), + Type::Paren(type_paren) => map_type_describe(&type_paren.elem), + Type::Array(type_array) => { + let inner = map_type_describe(&type_array.elem); + quote! { + inform(1); + <#type_array as WasmDescribe>::describe(); + #inner + } + } + Type::Slice(type_slice) => { + let inner = map_type_describe(&type_slice.elem); + quote! { + inform(1); + <#type_slice as WasmDescribe>::describe(); + #inner + } + } + Type::Tuple(type_tuple) => { + if type_tuple.elems.is_empty() { + quote! { + inform(0); + <#type_tuple as WasmDescribe>::describe(); + } + } else { + default + } + } + _ => default, + } +} diff --git a/crates/cli-support/src/descriptor.rs b/crates/cli-support/src/descriptor.rs index 3ddf75405b3..4d25b778516 100644 --- a/crates/cli-support/src/descriptor.rs +++ b/crates/cli-support/src/descriptor.rs @@ -1,6 +1,6 @@ use std::char; -use wasm_bindgen_shared::identifier::is_valid_ident; +use wasm_bindgen_shared::identifier::{is_valid_ident, TYPE_DESCRIBE_MAP}; macro_rules! tys { ($($a:ident)*) => (tys! { @ ($($a)*) 0 }); @@ -89,6 +89,10 @@ pub enum Descriptor { Result(Box), Unit, NonNull, + Generic { + ty: Box, + args: Box<[Descriptor]>, + }, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -97,6 +101,8 @@ pub struct Function { pub shim_idx: u32, pub ret: Descriptor, pub inner_ret: Option, + pub inner_ret_map: Option, + pub args_ty_map: Option>, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -279,20 +285,46 @@ impl Closure { impl Function { fn decode(data: &mut &[u32]) -> Function { let shim_idx = get(data); - let arguments = (0..get(data)) + let args_len = get(data); + let arguments = (0..args_len) .map(|_| Descriptor::_decode(data, false)) .collect::>(); + let ret = Descriptor::_decode(data, false); + let inner_ret = Some(Descriptor::_decode(data, false)); + + // decode inner retrun type map + let inner_ret_map = if !data.is_empty() && data[0] == TYPE_DESCRIBE_MAP { + get(data); + Some(decode_ty_map(data)) + } else { + None + }; + + // decode args type map + let args_ty_map = if !data.is_empty() && data[0] == TYPE_DESCRIBE_MAP { + get(data); + let mut args = vec![]; + for _i in 0..args_len { + args.push(decode_ty_map(data)); + } + Some(args) + } else { + None + }; + Function { arguments, shim_idx, - ret: Descriptor::_decode(data, false), - inner_ret: Some(Descriptor::_decode(data, false)), + ret, + inner_ret, + inner_ret_map, + args_ty_map, } } } impl VectorKind { - pub fn js_ty(&self) -> String { + pub fn js_ty(&self, generics: Option) -> String { match *self { VectorKind::String => "string".to_string(), VectorKind::I8 => "Int8Array".to_string(), @@ -309,9 +341,9 @@ impl VectorKind { VectorKind::Externref => "any[]".to_string(), VectorKind::NamedExternref(ref name) => { if is_valid_ident(name.as_str()) { - format!("{}[]", name) + format!("{}{}[]", name, generics.unwrap_or_default()) } else { - format!("({})[]", name) + format!("({}){}[]", name, generics.unwrap_or_default()) } } } @@ -336,3 +368,22 @@ impl VectorKind { } } } + +/// Decodes a type map recursively to a Descriptor +/// Type map is a tree like structure of a type's wasm describes +pub fn decode_ty_map(data: &mut &[u32]) -> Descriptor { + let len = get(data); + let ty = Descriptor::_decode(data, false); + if len == 0 { + ty + } else { + let mut args = vec![]; + for _i in 0..len { + args.push(decode_ty_map(data)); + } + Descriptor::Generic { + ty: Box::new(ty), + args: args.into_boxed_slice(), + } + } +} diff --git a/crates/cli-support/src/intrinsic.rs b/crates/cli-support/src/intrinsic.rs index 6141fdb4a52..ae7aa982d61 100644 --- a/crates/cli-support/src/intrinsic.rs +++ b/crates/cli-support/src/intrinsic.rs @@ -45,7 +45,9 @@ macro_rules! intrinsics { shim_idx: 0, arguments: vec![$($arg),*], ret: $ret, - inner_ret: None + inner_ret: None, + inner_ret_map: None, + args_ty_map: None, } } )* diff --git a/crates/cli-support/src/js/binding.rs b/crates/cli-support/src/js/binding.rs index 09501a67b40..dc096e7f85d 100644 --- a/crates/cli-support/src/js/binding.rs +++ b/crates/cli-support/src/js/binding.rs @@ -282,6 +282,8 @@ impl<'a, 'b> Builder<'a, 'b> { asyncness, variadic, ret_ty_override, + &adapter.params_map, + &adapter.inner_results_map, ); let js_doc = if generate_jsdoc { self.js_doc_comments( @@ -291,6 +293,8 @@ impl<'a, 'b> Builder<'a, 'b> { variadic, ret_ty_override, ret_desc, + &adapter.params_map, + &adapter.inner_results_map, ) } else { String::new() @@ -332,6 +336,8 @@ impl<'a, 'b> Builder<'a, 'b> { asyncness: bool, variadic: bool, ret_ty_override: &Option, + arg_tys_map: &Option>>, + result_ty_map: &Option, ) -> (String, Vec, Option, HashSet) { // Build up the typescript signature as well let mut omittable = true; @@ -339,11 +345,22 @@ impl<'a, 'b> Builder<'a, 'b> { let mut ts_arg_tys = Vec::new(); let mut ts_refs = HashSet::new(); for ( - AuxFunctionArgumentData { - name, ty_override, .. - }, - ty, - ) in args_data.iter().zip(arg_tys).rev() + ( + AuxFunctionArgumentData { + name, ty_override, .. + }, + ty, + ), + ty_map, + ) in args_data + .iter() + .zip(arg_tys) + .zip( + arg_tys_map + .as_ref() + .unwrap_or(&std::iter::repeat_n(None, arg_tys.len()).collect()), + ) + .rev() { // In TypeScript, we can mark optional parameters as omittable // using the `?` suffix, but only if they're not followed by @@ -357,15 +374,31 @@ impl<'a, 'b> Builder<'a, 'b> { arg.push_str(": "); ts.push_str(v); } else { + // build generics + let generics = ty_map + .as_ref() + .map(|ty| build_ts_generics(ty, TypePosition::Argument, None, true)); match ty { AdapterType::Option(ty) if omittable => { // e.g. `foo?: string | null` arg.push_str("?: "); - adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs)); + adapter2ts( + ty, + TypePosition::Argument, + &mut ts, + Some(&mut ts_refs), + generics, + ); ts.push_str(" | null"); } ty => { - adapter2ts(ty, TypePosition::Argument, &mut ts, Some(&mut ts_refs)); + adapter2ts( + ty, + TypePosition::Argument, + &mut ts, + Some(&mut ts_refs), + generics, + ); omittable = false; arg.push_str(": "); } @@ -407,12 +440,19 @@ impl<'a, 'b> Builder<'a, 'b> { } else { match result_tys.len() { 0 => ret.push_str("void"), - 1 => adapter2ts( - &result_tys[0], - TypePosition::Return, - &mut ret, - Some(&mut ts_refs), - ), + 1 => { + // build generics + let generics = result_ty_map + .as_ref() + .map(|ty| build_ts_generics(ty, TypePosition::Return, None, true)); + adapter2ts( + &result_tys[0], + TypePosition::Return, + &mut ret, + Some(&mut ts_refs), + generics, + ) + } _ => ret.push_str("[any]"), } } @@ -435,6 +475,8 @@ impl<'a, 'b> Builder<'a, 'b> { variadic: bool, ret_ty_override: &Option, ret_desc: &Option, + arg_tys_map: &Option>>, + result_ty_map: &Option, ) -> String { let (variadic_arg, fn_arg_names) = match args_data.split_last() { Some((last, args)) if variadic => (Some(last), args), @@ -445,13 +487,24 @@ impl<'a, 'b> Builder<'a, 'b> { let mut js_doc_args = Vec::new(); for ( - AuxFunctionArgumentData { - name, - ty_override, - desc, - }, - ty, - ) in fn_arg_names.iter().zip(arg_tys).rev() + ( + AuxFunctionArgumentData { + name, + ty_override, + desc, + }, + ty, + ), + ty_map, + ) in fn_arg_names + .iter() + .zip(arg_tys) + .zip( + arg_tys_map + .as_ref() + .unwrap_or(&std::iter::repeat_n(None, arg_tys.len()).collect()), + ) + .rev() { let mut arg = "@param {".to_string(); @@ -461,9 +514,13 @@ impl<'a, 'b> Builder<'a, 'b> { arg.push_str("} "); arg.push_str(name); } else { + // build generics + let generics = ty_map.as_ref().map(|v: &AdapterType| { + build_ts_generics(v, TypePosition::Argument, None, true) + }); match ty { AdapterType::Option(ty) if omittable => { - adapter2ts(ty, TypePosition::Argument, &mut arg, None); + adapter2ts(ty, TypePosition::Argument, &mut arg, None, generics); arg.push_str(" | null} "); arg.push('['); arg.push_str(name); @@ -471,7 +528,7 @@ impl<'a, 'b> Builder<'a, 'b> { } _ => { omittable = false; - adapter2ts(ty, TypePosition::Argument, &mut arg, None); + adapter2ts(ty, TypePosition::Argument, &mut arg, None, generics); arg.push_str("} "); arg.push_str(name); } @@ -501,7 +558,11 @@ impl<'a, 'b> Builder<'a, 'b> { if let Some(v) = ty_override { ret.push_str(v); } else { - adapter2ts(ty, TypePosition::Argument, &mut ret, None); + // build generics + let generics = result_ty_map + .as_ref() + .map(|ty| build_ts_generics(ty, TypePosition::Return, None, true)); + adapter2ts(ty, TypePosition::Argument, &mut ret, None, generics); } ret.push_str("} "); ret.push_str(name); @@ -1680,6 +1741,7 @@ fn adapter2ts( position: TypePosition, dst: &mut String, refs: Option<&mut HashSet>, + generics: Option, ) { match ty { AdapterType::I32 @@ -1700,24 +1762,106 @@ fn adapter2ts( AdapterType::String => dst.push_str("string"), AdapterType::Externref => dst.push_str("any"), AdapterType::Bool => dst.push_str("boolean"), - AdapterType::Vector(kind) => dst.push_str(&kind.js_ty()), + AdapterType::Vector(kind) => dst.push_str(&kind.js_ty(generics)), AdapterType::Option(ty) => { - adapter2ts(ty, position, dst, refs); + adapter2ts(ty, position, dst, refs, generics); dst.push_str(match position { TypePosition::Argument => " | null | undefined", TypePosition::Return => " | undefined", }); } - AdapterType::NamedExternref(name) => dst.push_str(name), - AdapterType::Struct(name) => dst.push_str(name), - AdapterType::Enum(name) => dst.push_str(name), + AdapterType::NamedExternref(name) => { + dst.push_str(name); + dst.push_str(&generics.unwrap_or_default()); + } + AdapterType::Struct(name) => { + dst.push_str(name); + dst.push_str(&generics.unwrap_or_default()); + } + AdapterType::Enum(name) => { + dst.push_str(name); + dst.push_str(&generics.unwrap_or_default()); + } AdapterType::StringEnum(name) => { if let Some(refs) = refs { refs.insert(TsReference::StringEnum(name.clone())); } dst.push_str(name); + dst.push_str(&generics.unwrap_or_default()); } AdapterType::Function => dst.push_str("any"), + _ => unreachable!(), + } +} + +/// Recursively builds ts generic arguments for the given type, +/// which later on will be appended to the original parent ts type +fn build_ts_generics( + ty: &AdapterType, + position: TypePosition, + generics: Option, + is_root: bool, +) -> String { + // at start, we need to check if the given type is generic or not + // and return early if not to avoid returning a non-generic type + // as a generic arg + if is_root && !is_generic(ty) { + return String::new(); + } + match ty { + AdapterType::I32 + | AdapterType::S8 + | AdapterType::S16 + | AdapterType::S32 + | AdapterType::U8 + | AdapterType::U16 + | AdapterType::U32 + | AdapterType::F32 + | AdapterType::F64 + | AdapterType::NonNull => "number".to_string(), + AdapterType::I64 + | AdapterType::S64 + | AdapterType::U64 + | AdapterType::S128 + | AdapterType::U128 => "bigint".to_string(), + AdapterType::String => "string".to_string(), + AdapterType::Externref => "any".to_string(), + AdapterType::Bool => "boolean".to_string(), + AdapterType::Vector(kind) => kind.js_ty(generics), + AdapterType::Option(ty) => { + let mut res = build_ts_generics(ty, position, generics, false); + match position { + TypePosition::Argument => res.push_str(" | null | undefined"), + TypePosition::Return => res.push_str(" | undefined"), + }; + res + } + AdapterType::NamedExternref(name) + | AdapterType::Struct(name) + | AdapterType::Enum(name) + | AdapterType::StringEnum(name) => name.clone() + generics.unwrap_or_default().as_str(), + AdapterType::Function => "any".to_string(), + AdapterType::Unit => "undefined".to_string(), + AdapterType::Generic { ty, args } => { + let mut gens = vec![]; + for arg in args { + gens.push(build_ts_generics(arg, position, None, false)); + } + if is_root { + format!("<{}>", gens.join(", ")) + } else { + build_ts_generics(ty, position, Some(format!("<{}>", gens.join(", "))), false) + } + } + } +} + +/// Determines if the given type is a generic with nested argument types +fn is_generic(ty: &AdapterType) -> bool { + match ty { + AdapterType::Generic { .. } => true, + AdapterType::Option(ty) => is_generic(ty), + _ => false, } } diff --git a/crates/cli-support/src/wit/incoming.rs b/crates/cli-support/src/wit/incoming.rs index 0309ef8c9c9..e9c7da0d0cb 100644 --- a/crates/cli-support/src/wit/incoming.rs +++ b/crates/cli-support/src/wit/incoming.rs @@ -181,6 +181,9 @@ impl InstructionBuilder<'_, '_> { Instruction::I32FromNonNull, &[AdapterType::I32], ), + + // cant be reached + Descriptor::Generic { .. } => unreachable!(), } Ok(()) } diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs index a514e875b3d..450e5593d1c 100644 --- a/crates/cli-support/src/wit/mod.rs +++ b/crates/cli-support/src/wit/mod.rs @@ -191,6 +191,8 @@ impl<'a> Context<'a> { arguments: vec![Descriptor::I32; 3], ret: Descriptor::Externref, inner_ret: None, + inner_ret_map: None, + args_ty_map: None, }; let id = self.import_adapter(*id, signature, AdapterJsImportKind::Normal)?; // Synthesize the two integer pointers we pass through which @@ -352,6 +354,8 @@ impl<'a> Context<'a> { arguments: Vec::new(), ret: Descriptor::String, inner_ret: None, + inner_ret_map: None, + args_ty_map: None, }; let id = self.import_adapter(id, descriptor, AdapterJsImportKind::Normal)?; let (path, content) = match module { @@ -812,6 +816,8 @@ impl<'a> Context<'a> { shim_idx: 0, ret: descriptor, inner_ret: None, + inner_ret_map: None, + args_ty_map: None, }, AdapterJsImportKind::Normal, )?; @@ -839,6 +845,8 @@ impl<'a> Context<'a> { shim_idx: 0, ret: Descriptor::Externref, inner_ret: None, + inner_ret_map: None, + args_ty_map: None, }, AdapterJsImportKind::Normal, )?; @@ -869,6 +877,8 @@ impl<'a> Context<'a> { shim_idx: 0, ret: Descriptor::Boolean, inner_ret: None, + inner_ret_map: None, + args_ty_map: None, }, AdapterJsImportKind::Normal, )?; @@ -954,6 +964,8 @@ impl<'a> Context<'a> { shim_idx: 0, ret: descriptor.clone(), inner_ret: Some(descriptor.clone()), + inner_ret_map: None, + args_ty_map: None, }; let getter_id = self.export_adapter(getter_id, getter_descriptor)?; self.aux.export_map.insert( @@ -988,6 +1000,8 @@ impl<'a> Context<'a> { shim_idx: 0, ret: Descriptor::Unit, inner_ret: None, + inner_ret_map: None, + args_ty_map: None, }; let setter_id = self.export_adapter(setter_id, setter_descriptor)?; self.aux.export_map.insert( @@ -1051,6 +1065,8 @@ impl<'a> Context<'a> { arguments, ret, inner_ret: None, + inner_ret_map: None, + args_ty_map: None, }; let id = self.import_adapter(import_id, signature, AdapterJsImportKind::Normal)?; self.aux.import_map.insert(id, aux_import); @@ -1263,6 +1279,8 @@ impl<'a> Context<'a> { name: import_name, kind, }, + None, + None, ); instructions.push(InstructionData { instr: Instruction::CallAdapter(f), @@ -1296,6 +1314,8 @@ impl<'a> Context<'a> { results, vec![], AdapterKind::Local { instructions }, + None, + None, ); args.cx.adapters.implements.push((import_id, core_id, id)); Ok(f) @@ -1377,6 +1397,18 @@ impl<'a> Context<'a> { // ... then the returned value being translated back + // convert args and return type map describers to adapter + let inner_results_map = signature + .inner_ret_map + .as_ref() + .and_then(outgoing::describe_map_to_adapter); + let params_map = signature.args_ty_map.as_ref().map(|args_ty_map| { + args_ty_map + .iter() + .map(outgoing::describe_map_to_adapter) + .collect() + }); + let inner_ret_output = if signature.inner_ret.is_some() { let mut inner_ret = args.cx.instruction_builder(true); inner_ret.outgoing(&signature.inner_ret.unwrap())?; @@ -1447,6 +1479,8 @@ impl<'a> Context<'a> { ret.output, inner_ret_output, AdapterKind::Local { instructions }, + inner_results_map, + params_map, )) } diff --git a/crates/cli-support/src/wit/outgoing.rs b/crates/cli-support/src/wit/outgoing.rs index b5eaeefa84b..e1208f79d13 100644 --- a/crates/cli-support/src/wit/outgoing.rs +++ b/crates/cli-support/src/wit/outgoing.rs @@ -173,6 +173,9 @@ impl InstructionBuilder<'_, '_> { Descriptor::ClampedU8 => unreachable!(), Descriptor::NonNull => self.outgoing_i32(AdapterType::NonNull), + + // canot be reached + Descriptor::Generic { .. } => unreachable!(), } Ok(()) } @@ -503,6 +506,9 @@ impl InstructionBuilder<'_, '_> { "unsupported Result type for returning from exported Rust function: {:?}", arg ), + + // canot be reached + Descriptor::Generic { .. } => unreachable!(), } Ok(()) } @@ -615,3 +621,122 @@ impl InstructionBuilder<'_, '_> { ); } } + +/// Converts a Descriptor to an AdapterType with handling generics/nested types +pub fn describe_map_to_adapter(arg: &Descriptor) -> Option { + match arg { + Descriptor::Unit => Some(AdapterType::Unit), + Descriptor::NonNull => Some(AdapterType::NonNull), + + Descriptor::I8 => Some(AdapterType::S8), + Descriptor::U8 => Some(AdapterType::U8), + Descriptor::I16 => Some(AdapterType::S16), + Descriptor::U16 => Some(AdapterType::U16), + Descriptor::I32 => Some(AdapterType::S32), + Descriptor::U32 => Some(AdapterType::U32), + Descriptor::I64 => Some(AdapterType::I64), + Descriptor::U64 => Some(AdapterType::U64), + Descriptor::I128 => Some(AdapterType::S128), + Descriptor::U128 => Some(AdapterType::U128), + Descriptor::F32 => Some(AdapterType::F32), + Descriptor::F64 => Some(AdapterType::F64), + Descriptor::Boolean => Some(AdapterType::Bool), + Descriptor::Char | Descriptor::CachedString | Descriptor::String => { + Some(AdapterType::String) + } + + Descriptor::Enum { name, .. } => Some(AdapterType::Enum(name.clone())), + Descriptor::StringEnum { name, .. } => Some(AdapterType::StringEnum(name.clone())), + Descriptor::RustStruct(class) => Some(AdapterType::Struct(class.clone())), + + Descriptor::Externref => Some(AdapterType::Externref), + Descriptor::NamedExternref(name) => Some(AdapterType::NamedExternref(name.clone())), + + Descriptor::Ref(d) | Descriptor::RefMut(d) | Descriptor::Result(d) => { + describe_map_to_adapter(d) + } + + Descriptor::Option(d) => Some(describe_map_to_adapter(d)?.option()), + Descriptor::Vector(_) => { + let kind = arg.vector_kind()?; + Some(AdapterType::Vector(kind)) + } + + // Largely synthetic and can't show up + Descriptor::ClampedU8 => unreachable!(), + + Descriptor::Function(_) | Descriptor::Closure(_) | Descriptor::Slice(_) => None, + + Descriptor::Generic { ty, args } => match &**ty { + Descriptor::Result(d) => { + if let Some(generics) = resolve_inner(args) { + Some(AdapterType::Generic { + ty: Box::new(describe_map_to_adapter(d)?), + args: generics.into_boxed_slice(), + }) + } else { + describe_map_to_adapter(d) + } + } + Descriptor::Option(d) => { + if let Some(generics) = resolve_inner(args) { + Some(AdapterType::Generic { + ty: Box::new(describe_map_to_adapter(d)?.option()), + args: generics.into_boxed_slice(), + }) + } else { + Some(describe_map_to_adapter(d)?.option()) + } + } + Descriptor::Vector(_d) => { + let kind = ty.vector_kind()?; + if let Some(generics) = resolve_inner(args) { + Some(AdapterType::Generic { + ty: Box::new(AdapterType::Vector(kind)), + args: generics.into_boxed_slice(), + }) + } else { + Some(AdapterType::Vector(kind)) + } + } + _ => { + let mut generics = vec![]; + for arg in args { + generics.push(describe_map_to_adapter(arg)?); + } + if generics.is_empty() { + describe_map_to_adapter(ty) + } else { + Some(AdapterType::Generic { + ty: Box::new(describe_map_to_adapter(ty)?), + args: generics.into_boxed_slice(), + }) + } + } + }, + } +} + +/// Handles the generic argument of rust Result, Option and Vectors types. +/// This is because these types are generic but are encoded differently throughout +/// this repo compared to other generic types +pub fn resolve_inner(args: &[Descriptor]) -> Option> { + if let Some(Descriptor::Generic { ty, args, .. }) = args.first() { + // vectors, results and options are already on level flattened, so we + // need to check one level deeper for their possible generics + match &**ty { + Descriptor::Vector(_) | Descriptor::Result(_) | Descriptor::Option(_) => { + resolve_inner(args) + } + _ => { + let mut generic_args = vec![]; + for arg in args { + generic_args.push(describe_map_to_adapter(arg)?); + } + Some(generic_args) + } + } + } else { + None + } +} diff --git a/crates/cli-support/src/wit/standard.rs b/crates/cli-support/src/wit/standard.rs index 8b3780f37fb..f27ad6b8263 100644 --- a/crates/cli-support/src/wit/standard.rs +++ b/crates/cli-support/src/wit/standard.rs @@ -32,6 +32,8 @@ pub struct Adapter { pub results: Vec, pub inner_results: Vec, pub kind: AdapterKind, + pub inner_results_map: Option, + pub params_map: Option>>, } #[derive(Debug, Clone)] @@ -95,6 +97,11 @@ pub enum AdapterType { NamedExternref(String), Function, NonNull, + Unit, + Generic { + ty: Box, + args: Box<[AdapterType]>, + }, } #[derive(Debug, Clone)] @@ -404,6 +411,8 @@ impl NonstandardWitSection { results: Vec, inner_results: Vec, kind: AdapterKind, + inner_results_map: Option, + params_map: Option>>, ) -> AdapterId { let id = AdapterId(self.adapters.len()); self.adapters.insert( @@ -414,6 +423,8 @@ impl NonstandardWitSection { results, inner_results, kind, + inner_results_map, + params_map, }, ); id diff --git a/crates/shared/src/identifier.rs b/crates/shared/src/identifier.rs index 51ebcc81f58..540f71f0576 100644 --- a/crates/shared/src/identifier.rs +++ b/crates/shared/src/identifier.rs @@ -40,3 +40,8 @@ pub fn is_valid_ident(name: &str) -> bool { } }) } + +/// An arbitrary code for describing a type map +/// This is hash of `TYPE_DESCRIBE_MAP` truncated to u32 +/// using `std::hash::DefaultHasher` +pub const TYPE_DESCRIBE_MAP: u32 = 2728578646; diff --git a/crates/typescript-tests/src/lib.rs b/crates/typescript-tests/src/lib.rs index b4edbadb4aa..c49c91ea192 100644 --- a/crates/typescript-tests/src/lib.rs +++ b/crates/typescript-tests/src/lib.rs @@ -12,6 +12,7 @@ pub mod optional_fields; pub mod simple_async_fn; pub mod simple_fn; pub mod simple_struct; +pub mod ts_generic_sig; pub mod typescript_type; pub mod usize; pub mod web_sys; diff --git a/crates/typescript-tests/src/ts_generic_sig.rs b/crates/typescript-tests/src/ts_generic_sig.rs new file mode 100644 index 00000000000..88658714d69 --- /dev/null +++ b/crates/typescript-tests/src/ts_generic_sig.rs @@ -0,0 +1,291 @@ +#![allow(clippy::all)] + +use serde::{Deserialize, Serialize}; +use wasm_bindgen::__rt::*; +use wasm_bindgen::convert::*; +use wasm_bindgen::describe::*; +use wasm_bindgen::prelude::*; +use wasm_bindgen::{JsCast, JsValue}; + +#[derive(Serialize, Deserialize)] +pub struct SomeType { + pub prop: String, +} + +#[derive(Serialize, Deserialize)] +pub struct SomeGenericType { + pub field: T, +} + +#[derive(Serialize, Deserialize)] +pub struct OtherGenericType { + pub field1: T, + pub field2: E, +} + +// some error type +pub struct Error; + +#[wasm_bindgen] +pub struct TestStruct; +#[wasm_bindgen] +impl TestStruct { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + TestStruct + } + #[wasm_bindgen] + pub async fn method1( + _arg1: SomeGenericType, + _arg2: OtherGenericType, + _arg3: &OtherGenericType, + ) -> Result>>, Option>>, Error> + { + Ok(OtherGenericType { + field1: vec![SomeGenericType { field: Some(0) }], + field2: Some(vec![String::new()]), + }) + } + #[wasm_bindgen(getter = "someProperty")] + pub fn method2( + &self, + ) -> Result>>>>, ()>, Error> { + Ok(OtherGenericType { + field1: Some(vec![SomeGenericType { + field: Some(vec![0]), + }]), + field2: (), + }) + } +} + +#[wasm_bindgen(js_name = "someFn")] +pub async fn some_fn( + _arg1: &OtherGenericType>, Option>>, + _arg2: Vec>>, +) -> Result>>>, Option>>, Error> +{ + Ok(OtherGenericType { + field1: Some(vec![SomeGenericType { field: Some(0) }]), + field2: Some(vec![String::new()]), + }) +} + +#[wasm_bindgen(js_name = "someOtherFn")] +pub fn some_other_fn( + _arg1: SomeGenericType>, +) -> Option>>>> { + Some(vec![OtherGenericType { + field1: SomeType { + prop: String::new(), + }, + field2: SomeGenericType { + field: Some(SomeType { + prop: String::new(), + }), + }, + }]) +} + +#[wasm_bindgen(js_name = "anotherFn")] +pub async fn another_fn( + _arg1: OtherGenericType>, +) -> OtherGenericType>> { + OtherGenericType { + field1: SomeType { + prop: String::new(), + }, + field2: SomeGenericType { + field: Some(SomeType { + prop: String::new(), + }), + }, + } +} + +// --- +// section below is just implementing wasm bindgen traits and ttypescript def for test structs + +// a helper macro to impl wasm traits for a given type +macro_rules! impl_wasm_traits { + ($type_name:ident $(< $($generics:ident),+ >)?) => { + impl$(<$($generics),+>)? IntoWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + type Abi = ::Abi; + fn into_abi(self) -> Self::Abi { + serde_wasm_bindgen::to_value(&self) + .map(::unchecked_from_js) + .unwrap_throw() + .into_abi() + } + } + impl$(<$($generics),+>)? OptionIntoWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + fn none() -> Self::Abi { + 0 + } + } + impl$(<$($generics),+>)? FromWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + type Abi = ::Abi; + unsafe fn from_abi(js: Self::Abi) -> Self { + serde_wasm_bindgen::from_value(JsValue::from_abi(js).into()).unwrap_throw() + } + } + impl$(<$($generics),+>)? OptionFromWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + fn is_none(js: &Self::Abi) -> bool { + *js == 0 + } + } + impl$(<$($generics),+>)? RefFromWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + type Abi = ::Abi; + type Anchor = Box; + unsafe fn ref_from_abi(js: Self::Abi) -> Self::Anchor { + Box::new(::from_abi(js)) + } + } + impl$(<$($generics),+>)? LongRefFromWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + type Abi = ::Abi; + type Anchor = Box; + unsafe fn long_ref_from_abi(js: Self::Abi) -> Self::Anchor { + Box::new(::from_abi(js)) + } + } + impl$(<$($generics),+>)? VectorIntoWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + type Abi = as IntoWasmAbi>::Abi; + fn vector_into_abi(vector: Box<[Self]>) -> Self::Abi { + js_value_vector_into_abi(vector) + } + } + impl$(<$($generics),+>)? VectorFromWasmAbi for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + type Abi = as FromWasmAbi>::Abi; + unsafe fn vector_from_abi(js: Self::Abi) -> Box<[Self]> { + js_value_vector_from_abi(js) + } + } + impl$(<$($generics),+>)? WasmDescribeVector for $type_name$(<$($generics),+>)? { + fn describe_vector() { + inform(VECTOR); + ::describe(); + } + } + impl$(<$($generics),+>)? From<$type_name$(<$($generics),+>)?> for JsValue + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + fn from(value: $type_name$(<$($generics),+>)?) -> Self { + serde_wasm_bindgen::to_value(&value).unwrap_throw() + } + } + impl$(<$($generics),+>)? TryFromJsValue for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + type Error = serde_wasm_bindgen::Error; + fn try_from_js_value(value: JsValue) -> Result { + serde_wasm_bindgen::from_value(value) + } + } + impl$(<$($generics),+>)? VectorIntoJsValue for $type_name$(<$($generics),+>)? + $(where $($generics: serde::Serialize + for<'de> serde::Deserialize<'de>, )+ )? + { + fn vector_into_jsvalue(vector: Box<[Self]>) -> JsValue { + js_value_vector_into_jsvalue(vector) + } + } + }; +} + +// impl wasm traits for test types +impl_wasm_traits!(SomeType); +impl WasmDescribe for SomeType { + fn describe() { + inform(NAMED_EXTERNREF); + inform(8u32); + inform(83u32); + inform(111u32); + inform(109u32); + inform(101u32); + inform(84u32); + inform(121u32); + inform(112u32); + inform(101u32); + } +} +#[wasm_bindgen(typescript_custom_section)] +const TYPESCRIPT_CONTENT: &str = "export interface SomeType { + prop: string; +}"; + +impl_wasm_traits!(SomeGenericType); +impl WasmDescribe for SomeGenericType { + fn describe() { + inform(NAMED_EXTERNREF); + inform(15u32); + inform(83u32); + inform(111u32); + inform(109u32); + inform(101u32); + inform(71u32); + inform(101u32); + inform(110u32); + inform(101u32); + inform(114u32); + inform(105u32); + inform(99u32); + inform(84u32); + inform(121u32); + inform(112u32); + inform(101u32); + } +} +#[wasm_bindgen(typescript_custom_section)] +const TYPESCRIPT_CONTENT: &str = "export interface SomeGenericType { + field: T; +}"; + +impl_wasm_traits!(OtherGenericType); +impl WasmDescribe for OtherGenericType { + fn describe() { + inform(NAMED_EXTERNREF); + inform(16u32); + inform(79u32); + inform(116u32); + inform(104u32); + inform(101u32); + inform(114u32); + inform(71u32); + inform(101u32); + inform(110u32); + inform(101u32); + inform(114u32); + inform(105u32); + inform(99u32); + inform(84u32); + inform(121u32); + inform(112u32); + inform(101u32); + } +} +#[wasm_bindgen(typescript_custom_section)] +const TYPESCRIPT_CONTENT: &str = "export interface OtherGenericType { + field1: T; + field2: E; +}"; + +impl From for JsValue { + fn from(_value: Error) -> Self { + JsValue::from(JsError::new("some error msg")) + } +} diff --git a/crates/typescript-tests/src/ts_generic_sig.ts b/crates/typescript-tests/src/ts_generic_sig.ts new file mode 100644 index 00000000000..6ce100346d8 --- /dev/null +++ b/crates/typescript-tests/src/ts_generic_sig.ts @@ -0,0 +1,88 @@ +import * as wbg from '../pkg/typescript_tests'; +import * as wasm from '../pkg/typescript_tests_bg.wasm'; +import { expect, test } from "@jest/globals"; + +const wasm_someFn: (a: number, b: number, c: number) => number = wasm.someFn; +const wbg_someFn: ( + arg1: wbg.OtherGenericType, string[] | null | undefined>, + arg2: wbg.SomeGenericType[] +) => Promise< + wbg.OtherGenericType< + wbg.SomeGenericType[] | undefined, + string[] | undefined + > +> = wbg.someFn; + +const wasm_someOtherFn: (a: number) => [number, number] = wasm.someOtherFn; +const wbg_someOtherFn: ( + arg1: wbg.SomeGenericType +) => wbg.OtherGenericType< + wbg.SomeType, + wbg.SomeGenericType +>[] | undefined = wbg.someOtherFn; + +const wasm_anotherFn: (a: number) => [number, number] = wasm.anotherFn; +const wbg_anotherFn: ( + arg1: wbg.OtherGenericType +) => Promise< + wbg.OtherGenericType< + wbg.SomeType, + wbg.SomeGenericType + > +> = wbg.anotherFn; + +const wasm_teststruct_method1: (a: any, b: any, c: any) => any = wasm.teststruct_method1; +const wbg_teststruct_method1: ( + arg1: wbg.SomeGenericType, + arg2: wbg.OtherGenericType, + arg3: wbg.OtherGenericType +) => Promise< + wbg.OtherGenericType< + wbg.SomeGenericType[], + string[] | undefined + > +> = wbg.TestStruct.method1; + +const wasm_teststruct_method2: (a: number) => [number, number, number] = wasm.teststruct_method2; +const wbg_teststruct_method2: wbg.OtherGenericType< + wbg.SomeGenericType[], + undefined +> = new wbg.TestStruct().someProperty; + +test("test generics in function signatures", async() => { + expect(wbg_teststruct_method2).toStrictEqual({ + field1: [{ field: [0] }], + field2: undefined, + }) + + expect(await wbg_teststruct_method1( + {field: 1}, + {field1: true, field2: "abcd"}, + {field1: BigInt(8), field2: {prop: "zxc"}} + )).toStrictEqual({ + field1: [{ field: 0 }], + field2: [""], + }) + + expect(await wbg_anotherFn( + {field1: {prop: "abcd"}, field2: Uint32Array.from([1, 2, 3])}, + )).toStrictEqual({ + field1: { prop: "" }, + field2: { field: { prop: "" } } + }) + + expect(wbg_someOtherFn( + {field: {prop: "zxc"}} + )).toStrictEqual([{ + field1: { prop: "" }, + field2: { field: { prop: "" } } + }]) + + expect(await wbg_someFn( + {field1: {field: Uint16Array.from([1, 2, 3])}, field2: ["zxc", "abcd"]}, + [{field: [{prop: "abcd"}]}], + )).toStrictEqual({ + field1: [{ field: 0 }], + field2: [""], + }) +}) diff --git a/examples/guide-supported-types-examples/Cargo.toml b/examples/guide-supported-types-examples/Cargo.toml index 84f15c3539c..58827a92856 100644 --- a/examples/guide-supported-types-examples/Cargo.toml +++ b/examples/guide-supported-types-examples/Cargo.toml @@ -11,6 +11,8 @@ crate-type = ["cdylib"] [dependencies] js-sys = { path = "../../crates/js-sys" } wasm-bindgen = { path = "../../" } +serde = { version = "1.0", features = ["derive", "rc"] } +serde-wasm-bindgen = { version = "0.6" } [lints] workspace = true From 6a3c234da8d599cce5357ba42b9341e71bfb1507 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 18 Mar 2025 14:25:28 +0000 Subject: [PATCH 2/3] Update Cargo.toml --- examples/guide-supported-types-examples/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/guide-supported-types-examples/Cargo.toml b/examples/guide-supported-types-examples/Cargo.toml index 58827a92856..84f15c3539c 100644 --- a/examples/guide-supported-types-examples/Cargo.toml +++ b/examples/guide-supported-types-examples/Cargo.toml @@ -11,8 +11,6 @@ crate-type = ["cdylib"] [dependencies] js-sys = { path = "../../crates/js-sys" } wasm-bindgen = { path = "../../" } -serde = { version = "1.0", features = ["derive", "rc"] } -serde-wasm-bindgen = { version = "0.6" } [lints] workspace = true From 60572315ff01cafc85432dbd8d48d234ca9976cc Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 18 Mar 2025 16:00:02 +0000 Subject: [PATCH 3/3] dont use std::iter --- crates/cli-support/src/js/binding.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/cli-support/src/js/binding.rs b/crates/cli-support/src/js/binding.rs index dc096e7f85d..7f8912aa36d 100644 --- a/crates/cli-support/src/js/binding.rs +++ b/crates/cli-support/src/js/binding.rs @@ -355,11 +355,7 @@ impl<'a, 'b> Builder<'a, 'b> { ) in args_data .iter() .zip(arg_tys) - .zip( - arg_tys_map - .as_ref() - .unwrap_or(&std::iter::repeat_n(None, arg_tys.len()).collect()), - ) + .zip(arg_tys_map.as_ref().unwrap_or(&vec![None; arg_tys.len()])) .rev() { // In TypeScript, we can mark optional parameters as omittable @@ -499,11 +495,7 @@ impl<'a, 'b> Builder<'a, 'b> { ) in fn_arg_names .iter() .zip(arg_tys) - .zip( - arg_tys_map - .as_ref() - .unwrap_or(&std::iter::repeat_n(None, arg_tys.len()).collect()), - ) + .zip(arg_tys_map.as_ref().unwrap_or(&vec![None; arg_tys.len()])) .rev() { let mut arg = "@param {".to_string();