extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; use syn::parse::Parse; use syn::spanned::Spanned; use syn::{ parse2, parse_macro_input, parse_quote, Attribute, FnArg, Ident, Item, ItemMod, LitStr, Meta, Pat, PatIdent, PatType, Token, Type, }; struct BuiltinArgs { name: LitStr, } impl Parse for BuiltinArgs { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { Ok(BuiltinArgs { name: input.parse()?, }) } } fn extract_docstring(attrs: &[Attribute]) -> Option<String> { // Rust docstrings are transparently written pre-macro expansion into an attribute that looks // like: // // #[doc = "docstring here"] // // Multi-line docstrings yield multiple attributes in order, which we assemble into a single // string below. #[allow(dead_code)] #[derive(Debug)] struct Docstring { eq: Token![=], doc: LitStr, } impl Parse for Docstring { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { Ok(Self { eq: input.parse()?, doc: input.parse()?, }) } } attrs .iter() .filter(|attr| attr.path.get_ident().into_iter().any(|id| id == "doc")) .filter_map(|attr| parse2::<Docstring>(attr.tokens.clone()).ok()) .map(|docstring| docstring.doc.value()) .reduce(|mut fst, snd| { if snd.is_empty() { // An empty string represents a spacing newline that was added in the // original doc comment. fst.push_str("\n\n"); } else { fst.push_str(&snd); } fst }) } /// Parse arguments to the `builtins` macro itself, such as `#[builtins(state = Rc<State>)]`. fn parse_module_args(args: TokenStream) -> Option<Type> { if args.is_empty() { return None; } let meta: Meta = syn::parse(args).expect("could not parse arguments to `builtins`-attribute"); let name_value = match meta { Meta::NameValue(nv) => nv, _ => panic!("arguments to `builtins`-attribute must be of the form `name = value`"), }; if name_value.path.get_ident().unwrap().to_string() != "state" { return None; } if let syn::Lit::Str(type_name) = name_value.lit { let state_type: Type = syn::parse_str(&type_name.value()).expect("failed to parse builtins state type"); return Some(state_type); } panic!("state attribute must be a quoted Rust type"); } /// Mark the annotated module as a module for defining Nix builtins. /// /// An optional type definition may be specified as an argument (e.g. `#[builtins(Rc<State>)]`), /// which will add a parameter to the `builtins` function of that type which is passed to each /// builtin upon instantiation. Using this, builtins that close over some external state can be /// written. /// /// A function `fn builtins() -> Vec<Builtin>` will be defined within the annotated module, /// returning a list of [`tvix_eval::Builtin`] for each function annotated with the `#[builtin]` /// attribute within the module. If a `state` type is specified, the `builtins` function will take a /// value of that type. /// /// Each invocation of the `#[builtin]` annotation within the module should be passed a string /// literal for the name of the builtin. /// /// # Examples /// ```ignore /// # use tvix_eval_builtin_macros::builtins; /// # mod value { /// # pub use tvix_eval::Builtin; /// # } /// /// #[builtins] /// mod builtins { /// use tvix_eval::{ErrorKind, Value, VM}; /// /// #[builtin("identity")] /// pub fn builtin_identity(_vm: &mut VM, x: Value) -> Result<Value, ErrorKind> { /// Ok(x) /// } /// /// // Builtins can request their argument not be forced before being called by annotating the /// // argument with the `#[lazy]` attribute /// /// #[builtin("tryEval")] /// pub fn builtin_try_eval(vm: &mut VM, #[lazy] x: Value) -> Result<Value, ErrorKind> { /// todo!() /// } /// } /// ``` #[proc_macro_attribute] pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream { let mut module = parse_macro_input!(item as ItemMod); // parse the optional state type, which users might want to pass to builtins let state_type = parse_module_args(args); let (_, items) = match &mut module.content { Some(content) => content, None => { return (quote_spanned!(module.span() => compile_error!("Builtin modules must be defined in-line") )) .into(); } }; let mut builtins = vec![]; for item in items.iter_mut() { if let Item::Fn(f) = item { if let Some(builtin_attr_pos) = f .attrs .iter() .position(|attr| attr.path.get_ident().iter().any(|id| *id == "builtin")) { let builtin_attr = f.attrs.remove(builtin_attr_pos); let BuiltinArgs { name } = match builtin_attr.parse_args() { Ok(args) => args, Err(err) => return err.into_compile_error().into(), }; if f.sig.inputs.len() <= 1 { return (quote_spanned!( f.sig.inputs.span() => compile_error!("Builtin functions must take at least two arguments") )) .into(); } // Determine if this function is taking the state parameter. let mut args_iter = f.sig.inputs.iter_mut().peekable(); let mut captures_state = false; if let Some(FnArg::Typed(PatType { pat, .. })) = args_iter.peek() { if let Pat::Ident(PatIdent { ident, .. }) = pat.as_ref() { if ident.to_string() == "state" { if state_type.is_none() { panic!("builtin captures a `state` argument, but no state type was defined"); } captures_state = true; } } } // skip state and/or VM args .. let skip_num = if captures_state { 2 } else { 1 }; let builtin_arguments = args_iter .skip(skip_num) .map(|arg| { let mut strict = true; let name = match arg { FnArg::Receiver(_) => { return Err(quote_spanned!(arg.span() => { compile_error!("Unexpected receiver argument in builtin") })) } FnArg::Typed(PatType { attrs, pat, .. }) => { attrs.retain(|attr| { attr.path.get_ident().into_iter().any(|id| { if id == "lazy" { strict = false; false } else { true } }) }); match pat.as_ref() { Pat::Ident(PatIdent { ident, .. }) => ident.to_string(), _ => "unknown".to_string(), } } }; Ok(quote_spanned!(arg.span() => { crate::BuiltinArgument { strict: #strict, name: #name, } })) }) .collect::<Result<Vec<_>, _>>(); let builtin_arguments = match builtin_arguments { Ok(args) => args, Err(err) => return err.into(), }; let fn_name = f.sig.ident.clone(); let num_args = f.sig.inputs.len() - skip_num; let args = (0..num_args) .map(|n| Ident::new(&format!("arg_{n}"), Span::call_site())) .collect::<Vec<_>>(); let mut reversed_args = args.clone(); reversed_args.reverse(); let docstring = match extract_docstring(&f.attrs) { Some(docs) => quote!(Some(#docs)), None => quote!(None), }; if captures_state { builtins.push(quote_spanned! { builtin_attr.span() => { let inner_state = state.clone(); crate::Builtin::new( #name, &[#(#builtin_arguments),*], #docstring, move |mut args: Vec<crate::Value>, vm: &mut crate::VM| { #(let #reversed_args = args.pop().unwrap();)* #fn_name(inner_state.clone(), vm, #(#args),*) } ) }}); } else { builtins.push(quote_spanned! { builtin_attr.span() => { crate::Builtin::new( #name, &[#(#builtin_arguments),*], #docstring, |mut args: Vec<crate::Value>, vm: &mut crate::VM| { #(let #reversed_args = args.pop().unwrap();)* #fn_name(vm, #(#args),*) } ) }}); } } } } if let Some(state_type) = state_type { items.push(parse_quote! { pub fn builtins(state: #state_type) -> Vec<(&'static str, Value)> { vec![#(#builtins),*].into_iter().map(|b| (b.name(), Value::Builtin(b))).collect() } }); } else { items.push(parse_quote! { pub fn builtins() -> Vec<(&'static str, Value)> { vec![#(#builtins),*].into_iter().map(|b| (b.name(), Value::Builtin(b))).collect() } }); } module.into_token_stream().into() }