use crate::util::*;
use crate::{FfiFunction, FfiType, RustType, TsModule};
use dodo_bindgen_rust_ir::{ItemFn, Receiver};
use heck::ToLowerCamelCase as _;
use proc_macro2::TokenStream;
use syn::Ident;
use super::ty::TypeExt as _;
#[derive(Clone, Debug, Default)]
pub struct FunctionDefs {
pub functions: Vec<TsMethod>,
}
#[derive(Clone, Debug)]
pub struct TsMethod {
pub non_blocking: bool,
pub ident: Ident,
pub inputs: Vec<RustType>,
pub output: RustType,
pub assoc: Option<Receiver>,
pub ffi_ident: Ident,
pub self_ty: Option<Ident>,
}
pub trait ItemFnExt {
fn unwrap(self, module: &mut TsModule, class: Option<(Ident, Receiver)>) -> TsMethod;
}
impl ItemFnExt for ItemFn {
fn unwrap(self, module: &mut TsModule, class: Option<(Ident, Receiver)>) -> TsMethod {
let mut ffi_inputs = Vec::new();
let mut inputs = Vec::new();
for arg in self.inputs {
ffi_inputs.push(FfiType::from(arg.abi));
inputs.push(arg.ty.unwrap(module));
}
let output = self.output.unwrap(module);
let ffi_ident = format_ident!("{}", self.symbol);
module.ffi_lib.interface.push_fn(FfiFunction {
ident: ffi_ident.clone(),
inputs: ffi_inputs,
output: FfiType::from(self.output_abi),
non_blocking: self.non_blocking,
});
let (self_ty, assoc) = class.unzip();
let method = TsMethod {
non_blocking: self.non_blocking,
ident: format_ident!("{}", self.name.to_lower_camel_case()),
inputs,
output,
assoc,
ffi_ident,
self_ty,
};
module.uses_pin |= method.needs_pin();
method
}
}
impl FunctionDefs {
pub fn push(&mut self, method: TsMethod) {
self.functions.push(method);
}
}
fn return_stmt(output: &RustType) -> TokenStream {
match output {
RustType::Void => TokenStream::new(),
RustType::Numeric(_)
| RustType::Boolean
| RustType::FnPtr(_)
| RustType::Ptr(_)
| RustType::PtrMut(_)
| RustType::Ref(_)
| RustType::RefMut(_)
| RustType::Unsupported => quote! {
return out! as #output;
},
RustType::Custom { from_rust, .. } => {
let value = raw(from_rust.replace("{}", "out"));
quote! {
return #value;
}
},
RustType::UserDefined(ident) => quote! {
return Reflect.construct(RustPrototype, [out!], #ident) as #ident;
},
rest => quote! {
return new #rest(out!) as #output;
},
}
}
impl TsMethod {
pub fn needs_pin(&self) -> bool {
self.non_blocking
&& self
.inputs
.iter()
.any(|input| matches!(input, RustType::Custom { .. }))
}
fn is_constructor(&self) -> bool {
self.ident == "new"
&& self.assoc == Some(Receiver::Static)
&& !self.non_blocking
&& self
.self_ty
.as_ref()
.is_some_and(|class| self.output == RustType::UserDefined(class.clone()))
}
pub fn print(&self) -> TokenStream {
let TsMethod {
ident,
inputs,
output,
assoc,
ffi_ident,
self_ty,
non_blocking,
} = self;
let inputs: Vec<&RustType> = inputs.iter().filter(|ty| **ty != RustType::Void).collect();
let explicit = match assoc {
Some(Receiver::Ref | Receiver::RefMut | Receiver::Owned) => &inputs[1..],
Some(Receiver::Static) | None => &inputs[..],
};
let borrow = |sync: TokenStream, async_: TokenStream| {
if *non_blocking {
quote! { using borrow = await #async_(this); }
} else {
quote! { using borrow = #sync(this); }
}
};
let mut static_ = TokenStream::new();
let mut pre = Vec::new();
let mut call_args = Vec::new();
match assoc {
Some(Receiver::Static) => static_ = quote! { static },
Some(Receiver::Ref) => {
pre.push(borrow(
quote! { __dodo_borrow },
quote! { __dodo_borrow_async },
));
call_args.push(quote! { borrow.ptr });
},
Some(Receiver::RefMut) => {
pre.push(borrow(
quote! { __dodo_borrow_mut },
quote! { __dodo_borrow_mut_async },
));
call_args.push(quote! { borrow.ptr });
},
Some(Receiver::Owned) => {
pre.push(if *non_blocking {
quote! { const ptr = await __dodo_take_async(this); }
} else {
quote! { const ptr = this.take(); }
});
call_args.push(quote! { ptr });
},
None => (),
}
let mut fn_args = Vec::new();
let mut pinned = Vec::new();
for (i, input) in explicit.iter().enumerate() {
let arg = format_ident!("arg_{i}");
fn_args.push(quote! { #arg: #input });
let value = match input {
RustType::Numeric(_)
| RustType::Boolean
| RustType::FnPtr(_)
| RustType::Ptr(_)
| RustType::PtrMut(_)
| RustType::Ref(_)
| RustType::RefMut(_)
| RustType::Unsupported => quote! { #arg },
RustType::Custom { to_rust, .. } => {
raw(to_rust.replace("{}", &arg.to_string())).to_token_stream()
},
_ => quote! { #arg.take() },
};
if *non_blocking && matches!(input, RustType::Custom { .. }) {
let temp = format_ident!("pin_{i}");
pre.push(quote! { const #temp = #value; });
pinned.push(quote! { #temp });
pinned.push(quote! { #arg });
call_args.push(quote! { #temp });
} else {
call_args.push(value);
}
}
let (async_, await_) = if *non_blocking {
(quote! { async }, quote! { await })
} else {
(TokenStream::new(), TokenStream::new())
};
let fn_output = match (output, non_blocking) {
(RustType::Void, false) => TokenStream::new(),
(rest, true) => quote! { : Promise<#rest> },
(rest, false) => quote! { : #rest },
};
let call = quote! { #await_ symbols.#ffi_ident(#(#call_args),*) };
if self.is_constructor() {
return quote! {
constructor(#(#fn_args),*) {
super(#call);
}
};
}
let call = match output {
RustType::Void => quote! { #call; },
_ => quote! { const out = #call; },
};
let ret = return_stmt(output);
let body = if pinned.is_empty() {
quote! {
#(#pre)*
#call
#ret
}
} else {
quote! {
#(#pre)*
const pinned = [#(#pinned),*];
__dodo_pinned.add(pinned);
try {
#call
#ret
} finally {
__dodo_pinned.delete(pinned);
}
}
};
let head = if self_ty.is_some() {
quote! { #static_ #async_ #ident }
} else {
quote! { export #async_ function #ident }
};
quote! {
#head(#(#fn_args),*) #fn_output {
#body
}
}
}
}
impl ToTokens for FunctionDefs {
fn to_tokens(&self, tokens: &mut TokenStream) {
for method in &self.functions {
tokens.extend(method.print());
}
}
}
#[cfg(test)]
mod print_tests {
use super::*;
use crate::TsModule;
use dodo_bindgen_rust_ir::{Abi, Arg, Type, TypeNumeric};
fn arg(ty: Type, abi: Abi) -> Arg {
Arg {
name: "arg".to_string(),
ty,
abi,
}
}
fn func(inputs: Vec<Arg>, output: Type, output_abi: Abi) -> ItemFn {
ItemFn {
name: "test_fn".to_string(),
symbol: "__dodo_crate_test_fn".to_string(),
doc: String::new(),
non_blocking: false,
inputs,
output,
output_abi,
}
}
fn print(item: ItemFn) -> String {
let mut module = TsModule::default();
module.ffi_lib.lib_name = "lib".to_string();
let method = item.unwrap(&mut module, None);
module.functions.push(method);
deno_fmt(format!(
"{}\n{}",
module.ffi_lib.to_token_stream(),
module.functions.to_token_stream()
))
}
#[test]
fn test_empty() {
insta::assert_snapshot!(print(func(vec![], Type::Void, Abi::Void)));
}
#[test]
fn test_return() {
let u8_ty = Type::Numeric(TypeNumeric::U8);
insta::assert_snapshot!(print(func(
vec![arg(u8_ty.clone(), Abi::U8)],
u8_ty,
Abi::U8
)));
}
fn custom(ts_type: &str, to_rust: &str, from_rust: &str) -> Type {
Type::Custom {
ts_type: ts_type.to_string(),
to_rust: to_rust.to_string(),
from_rust: from_rust.to_string(),
helper: String::new(),
}
}
#[test]
fn test_many_args() {
let custom = Type::Object {
module_path: "crate".to_string(),
name: "CustomType".to_string(),
};
insta::assert_snapshot!(print(func(
vec![
arg(
Type::RefMut(Box::new(Type::Slice(Box::new(Type::Numeric(
TypeNumeric::U8,
))))),
Abi::Pointer,
),
arg(Type::Ref(Box::new(Type::Str)), Abi::Pointer),
arg(Type::Box(Box::new(custom)), Abi::Pointer),
arg(Type::Vec(Box::new(Type::Bool)), Abi::Pointer),
],
Type::Void,
Abi::Void,
)));
}
#[test]
fn test_custom() {
insta::assert_snapshot!(print(func(
vec![arg(
custom("string", "`${{}}`.codePointAt(0)!", "''"),
Abi::U32,
)],
custom(
"Float64Array<ArrayBuffer>",
"",
"__dodo_decode_vec({}, Float64Array)",
),
Abi::Pointer,
)));
}
#[test]
fn test_non_blocking_pins_buffers() {
let mut item = func(
vec![
arg(
custom("Uint8Array<ArrayBuffer>", "__dodo_view({})", ""),
Abi::Buffer,
),
arg(custom("string", "__dodo_encode_str({})", ""), Abi::Buffer),
],
Type::Void,
Abi::Void,
);
item.non_blocking = true;
let mut module = TsModule::default();
let method = item.unwrap(&mut module, None);
assert!(module.uses_pin);
module.functions.push(method);
insta::assert_snapshot!(deno_fmt(module.functions.to_token_stream().to_string()));
}
}