1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 Error, FnArg, ItemFn, LitStr, Meta, Pat, PatIdent, ReturnType, Token, Type, parse_macro_input,
5 punctuated::Punctuated,
6};
7
8#[proc_macro_attribute]
9pub fn pd_host_function(attr: TokenStream, item: TokenStream) -> TokenStream {
10 let args = parse_macro_input!(attr with Punctuated::<Meta, Token![,]>::parse_terminated);
11 match expand_pd_host_function(args, parse_macro_input!(item as ItemFn)) {
12 Ok(tokens) => tokens.into(),
13 Err(err) => err.to_compile_error().into(),
14 }
15}
16
17fn expand_pd_host_function(
18 attr: Punctuated<Meta, Token![,]>,
19 mut item: ItemFn,
20) -> Result<proc_macro2::TokenStream, Error> {
21 parse_name_arg(&attr)?;
22 let docs = doc_string(&item.attrs);
23 for input in &item.sig.inputs {
24 validate_param(input)?;
25 }
26 validate_return_type(&item.sig.output)?;
27
28 if is_abi_declaration_only(&item) {
29 return Ok(quote!(#item));
30 }
31 if docs.trim().is_empty() {
32 return Err(Error::new_spanned(
33 &item.sig.ident,
34 "#[pd_host_function] requires /// doc comments",
35 ));
36 }
37
38 let (wrapper_name, impl_name) = wrapper_and_impl_names(&item.sig.ident);
39 if item.sig.ident != impl_name {
40 item.sig.ident = impl_name.clone();
41 }
42 let wrapper = generate_vm_wrapper(&item, &wrapper_name)?;
43 Ok(quote! {
44 #item
45 #wrapper
46 })
47}
48
49fn parse_name_arg(args: &Punctuated<Meta, Token![,]>) -> Result<LitStr, Error> {
50 let Some(Meta::NameValue(name_value)) = args.first() else {
51 return Err(Error::new(
52 proc_macro2::Span::call_site(),
53 "expected #[pd_host_function(name = \"...\")]",
54 ));
55 };
56 if !name_value.path.is_ident("name") {
57 return Err(Error::new_spanned(
58 &name_value.path,
59 "expected #[pd_host_function(name = \"...\")]",
60 ));
61 }
62 match &name_value.value {
63 syn::Expr::Lit(expr_lit) => {
64 if let syn::Lit::Str(value) = &expr_lit.lit {
65 Ok(value.clone())
66 } else {
67 Err(Error::new_spanned(
68 &expr_lit.lit,
69 "callable name must be a string literal",
70 ))
71 }
72 }
73 other => Err(Error::new_spanned(
74 other,
75 "callable name must be a string literal",
76 )),
77 }
78}
79
80fn doc_string(attrs: &[syn::Attribute]) -> String {
81 attrs
82 .iter()
83 .filter_map(|attr| {
84 if !attr.path().is_ident("doc") {
85 return None;
86 }
87 match &attr.meta {
88 Meta::NameValue(name_value) => match &name_value.value {
89 syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
90 syn::Lit::Str(value) => Some(value.value().trim().to_string()),
91 _ => None,
92 },
93 _ => None,
94 },
95 _ => None,
96 }
97 })
98 .filter(|line| !line.is_empty())
99 .collect::<Vec<_>>()
100 .join("\n")
101}
102
103fn validate_param(arg: &FnArg) -> Result<(), Error> {
104 let FnArg::Typed(pat_type) = arg else {
105 return Err(Error::new_spanned(arg, "methods are not supported"));
106 };
107 if is_vm_context_type(&pat_type.ty) {
108 return Ok(());
109 }
110 let Pat::Ident(PatIdent { .. }) = pat_type.pat.as_ref() else {
111 return Err(Error::new_spanned(
112 &pat_type.pat,
113 "callable parameters must use identifier patterns",
114 ));
115 };
116 type_label(&pat_type.ty)?;
117 Ok(())
118}
119
120fn validate_return_type(output: &ReturnType) -> Result<(), Error> {
121 match output {
122 ReturnType::Default => Ok(()),
123 ReturnType::Type(_, ty) => {
124 type_label(ty)?;
125 Ok(())
126 }
127 }
128}
129
130fn is_abi_declaration_only(item: &ItemFn) -> bool {
131 let [stmt] = item.block.stmts.as_slice() else {
132 return false;
133 };
134 let syn::Stmt::Expr(expr, None) = stmt else {
135 return false;
136 };
137 let syn::Expr::Macro(expr_macro) = expr else {
138 return false;
139 };
140 expr_macro.mac.path.is_ident("unreachable")
141}
142
143fn generate_vm_wrapper(
144 item: &ItemFn,
145 wrapper_name: &syn::Ident,
146) -> Result<proc_macro2::TokenStream, Error> {
147 let impl_name = &item.sig.ident;
148 let mut wrapper_params = Vec::<proc_macro2::TokenStream>::new();
149 let mut call_args = Vec::<proc_macro2::TokenStream>::new();
150 let mut imm_extract_stmts = Vec::<proc_macro2::TokenStream>::new();
151 let mut mut_extract_stmts = Vec::<proc_macro2::TokenStream>::new();
152 let mutable_wrapper_name = syn::Ident::new(&format!("{wrapper_name}_mut"), wrapper_name.span());
153 let has_vm = item.sig.inputs.iter().any(|input| match input {
154 FnArg::Typed(pat_type) => is_vm_context_type(&pat_type.ty),
155 FnArg::Receiver(_) => false,
156 });
157 if has_vm {
158 wrapper_params.push(quote!(vm: &mut super::super::Vm));
159 call_args.push(quote!(vm));
160 }
161 let imm_wrapper_params = {
162 let mut params = wrapper_params.clone();
163 params.push(quote!(args: &[super::super::Value]));
164 params
165 };
166 let mut_wrapper_params = {
167 let mut params = wrapper_params.clone();
168 params.push(quote!(args: &mut [super::super::Value]));
169 params
170 };
171
172 let mut arg_index = 0usize;
173 for input in &item.sig.inputs {
174 let FnArg::Typed(pat_type) = input else {
175 return Err(Error::new_spanned(input, "methods are not supported"));
176 };
177 if is_vm_context_type(&pat_type.ty) {
178 continue;
179 }
180 let Pat::Ident(PatIdent { ident, .. }) = pat_type.pat.as_ref() else {
181 return Err(Error::new_spanned(
182 &pat_type.pat,
183 "callable parameters must use identifier patterns",
184 ));
185 };
186 let ty = &pat_type.ty;
187 let label = LitStr::new(
188 &format!("{} {}", wrapper_name, ident),
189 proc_macro2::Span::call_site(),
190 );
191 let index = syn::Index::from(arg_index);
192 imm_extract_stmts.push(quote! {
193 let #ident = super::borrow_arg::<#ty>(args, #index, #label)?;
194 });
195 let extractor = if uses_taken_extractor(ty) {
196 quote!(super::take_arg::<#ty>(args, #index, #label)?)
197 } else {
198 quote!(super::borrow_arg::<#ty>(&*args, #index, #label)?)
199 };
200 mut_extract_stmts.push(quote! {
201 let #ident = #extractor;
202 });
203 call_args.push(quote!(#ident));
204 arg_index += 1;
205 }
206
207 let wrapper_output = wrapper_output_type(&item.sig.output)?;
208 let call_expr = if return_is_vm_result(&item.sig.output) {
209 quote!(#impl_name(#(#call_args),*))
210 } else {
211 quote!(Ok(#impl_name(#(#call_args),*)))
212 };
213
214 Ok(quote! {
215 #[allow(dead_code)]
216 pub(super) fn #wrapper_name(#(#imm_wrapper_params),*) -> #wrapper_output {
217 #(#imm_extract_stmts)*
218 #call_expr
219 }
220
221 #[allow(dead_code)]
222 pub(super) fn #mutable_wrapper_name(#(#mut_wrapper_params),*) -> #wrapper_output {
223 #(#mut_extract_stmts)*
224 #call_expr
225 }
226 })
227}
228
229fn wrapper_and_impl_names(name: &syn::Ident) -> (syn::Ident, syn::Ident) {
230 let original = name.to_string();
231 match original.strip_suffix("_impl") {
232 Some(prefix) => (
233 syn::Ident::new(prefix, name.span()),
234 syn::Ident::new(&original, name.span()),
235 ),
236 None => (
237 syn::Ident::new(&original, name.span()),
238 syn::Ident::new(&format!("{original}_impl"), name.span()),
239 ),
240 }
241}
242
243fn wrapper_output_type(output: &ReturnType) -> Result<proc_macro2::TokenStream, Error> {
244 if let Some(inner) = vm_result_inner_type(output)? {
245 return Ok(quote!(super::super::VmResult<#inner>));
246 }
247
248 match output {
249 ReturnType::Default => Ok(quote!(super::super::VmResult<()>)),
250 ReturnType::Type(_, ty) => Ok(quote!(super::super::VmResult<#ty>)),
251 }
252}
253
254fn vm_result_inner_type(output: &ReturnType) -> Result<Option<Type>, Error> {
255 let ReturnType::Type(_, ty) = output else {
256 return Ok(None);
257 };
258 unwrap_vm_result_type(ty)
259}
260
261fn unwrap_vm_result_type(ty: &Type) -> Result<Option<Type>, Error> {
262 match ty {
263 Type::Group(group) => unwrap_vm_result_type(&group.elem),
264 Type::Paren(paren) => unwrap_vm_result_type(&paren.elem),
265 Type::Reference(reference) => unwrap_vm_result_type(&reference.elem),
266 Type::Path(path) => {
267 let Some(segment) = path.path.segments.last() else {
268 return Ok(None);
269 };
270 if !matches!(
271 segment.ident.to_string().as_str(),
272 "VmResult" | "HostResult"
273 ) {
274 return Ok(None);
275 }
276 let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
277 return Err(Error::new_spanned(
278 &segment.arguments,
279 format!("{}<T> requires one generic argument", segment.ident),
280 ));
281 };
282 let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
283 return Err(Error::new_spanned(
284 args,
285 format!("{}<T> requires one type argument", segment.ident),
286 ));
287 };
288 Ok(Some(inner.clone()))
289 }
290 _ => Ok(None),
291 }
292}
293
294fn return_is_vm_result(output: &ReturnType) -> bool {
295 vm_result_inner_type(output)
296 .expect("pd_host_function return type should already be validated")
297 .is_some()
298}
299
300fn type_label(ty: &Type) -> Result<String, Error> {
301 match ty {
302 Type::Group(group) => type_label(&group.elem),
303 Type::Paren(paren) => type_label(&paren.elem),
304 Type::Reference(reference) => type_label(&reference.elem),
305 Type::Slice(slice) => match slice.elem.as_ref() {
306 Type::Path(path) => {
307 let Some(segment) = path.path.segments.last() else {
308 return Err(Error::new_spanned(slice, "unsupported callable type"));
309 };
310 if segment.ident == "u8" {
311 Ok("bytes".to_string())
312 } else {
313 Err(Error::new_spanned(slice, "unsupported callable type"))
314 }
315 }
316 _ => Err(Error::new_spanned(slice, "unsupported callable type")),
317 },
318 Type::Tuple(tuple) if tuple.elems.is_empty() => Ok("null".to_string()),
319 Type::Path(path) => {
320 let Some(segment) = path.path.segments.last() else {
321 return Err(Error::new_spanned(path, "unsupported callable type"));
322 };
323 let ident = segment.ident.to_string();
324 match ident.as_str() {
325 "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64"
326 | "u128" | "usize" => Ok("int".to_string()),
327 "f32" | "f64" => Ok("float".to_string()),
328 "bool" => Ok("bool".to_string()),
329 "String" | "str" | "VmStringRef" => Ok("string".to_string()),
330 "Bytes" | "VmBytes" | "VmBytesRef" | "VmBytesHandle" => Ok("bytes".to_string()),
331 "Any" | "AnyValue" | "Value" | "VmValueRef" | "VmValueOwned" => {
332 Ok("any".to_string())
333 }
334 "Array" | "VmArray" | "VmArrayRef" | "VmArrayHandle" => Ok("array".to_string()),
335 "Map" | "VmMap" | "VmMapRef" | "VmMapHandle" => Ok("map".to_string()),
336 "Number" | "NumberValue" => Ok("number".to_string()),
337 "Unknown" | "UnknownValue" => Ok("unknown".to_string()),
338 "CallOutcome" => Ok("unknown".to_string()),
339 "Option" => {
340 let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
341 return Err(Error::new_spanned(
342 &segment.arguments,
343 "Option<T> requires one generic argument",
344 ));
345 };
346 let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
347 return Err(Error::new_spanned(
348 args,
349 "Option<T> requires one type argument",
350 ));
351 };
352 let inner_label = type_label(inner)?;
353 Ok(format!("{inner_label} | null"))
354 }
355 "VmResult" | "BuiltinResult" | "HostResult" => {
356 let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
357 return Err(Error::new_spanned(
358 &segment.arguments,
359 format!("{ident}<T> requires one generic argument"),
360 ));
361 };
362 let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
363 return Err(Error::new_spanned(
364 args,
365 format!("{ident}<T> requires one type argument"),
366 ));
367 };
368 type_label(inner)
369 }
370 "Vec" => type_label_for_vec(segment),
371 _ => Err(Error::new_spanned(
372 path,
373 format!("unsupported callable type '{ident}'"),
374 )),
375 }
376 }
377 _ => Err(Error::new_spanned(ty, "unsupported callable type")),
378 }
379}
380
381fn type_label_for_vec(segment: &syn::PathSegment) -> Result<String, Error> {
382 let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
383 return Err(Error::new_spanned(
384 &segment.arguments,
385 "Vec<T> requires one generic argument",
386 ));
387 };
388 let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
389 return Err(Error::new_spanned(
390 args,
391 "Vec<T> requires one type argument",
392 ));
393 };
394 match inner {
395 Type::Tuple(tuple) if tuple.elems.len() == 2 => {
396 let lhs = tuple
397 .elems
398 .first()
399 .expect("tuple should contain first element");
400 let rhs = tuple
401 .elems
402 .last()
403 .expect("tuple should contain second element");
404 if is_value_type(lhs) && is_value_type(rhs) {
405 Ok("map".to_string())
406 } else {
407 Err(Error::new_spanned(
408 inner,
409 "unsupported Vec tuple type in callable metadata",
410 ))
411 }
412 }
413 _ if is_value_type(inner) => Ok("array".to_string()),
414 _ => {
415 let inner_label = type_label(inner)?;
416 Err(Error::new_spanned(
417 inner,
418 format!("unsupported Vec return type '{inner_label}'"),
419 ))
420 }
421 }
422}
423
424fn is_value_type(ty: &Type) -> bool {
425 match ty {
426 Type::Group(group) => is_value_type(&group.elem),
427 Type::Paren(paren) => is_value_type(&paren.elem),
428 Type::Reference(reference) => is_value_type(&reference.elem),
429 Type::Path(path) => path
430 .path
431 .segments
432 .last()
433 .is_some_and(|segment| segment.ident == "Value"),
434 _ => false,
435 }
436}
437
438fn is_vm_context_type(ty: &Type) -> bool {
439 match ty {
440 Type::Group(group) => is_vm_context_type(&group.elem),
441 Type::Paren(paren) => is_vm_context_type(&paren.elem),
442 Type::Reference(reference) => is_vm_context_type(&reference.elem),
443 Type::Path(path) => path
444 .path
445 .segments
446 .last()
447 .is_some_and(|segment| segment.ident == "Vm"),
448 _ => false,
449 }
450}
451
452fn uses_taken_extractor(ty: &Type) -> bool {
453 match ty {
454 Type::Group(group) => uses_taken_extractor(&group.elem),
455 Type::Paren(paren) => uses_taken_extractor(&paren.elem),
456 Type::Reference(_) => false,
457 Type::Path(path) => path.path.segments.last().is_some_and(|segment| {
458 matches!(
459 segment.ident.to_string().as_str(),
460 "Value"
461 | "AnyValue"
462 | "UnknownValue"
463 | "VmArray"
464 | "VmBytes"
465 | "VmMap"
466 | "VmArrayHandle"
467 | "VmBytesHandle"
468 | "VmMapHandle"
469 | "VmValueOwned"
470 )
471 }),
472 _ => false,
473 }
474}