about summary refs log tree commit diff
path: root/tvix/eval/builtin-macros/src/lib.rs
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, parse_quote_spanned, Attribute, FnArg, Ident, Item,
    ItemMod, LitStr, Meta, Pat, PatIdent, PatType, Token, Type,
};

/// Description of a single argument passed to a builtin
struct BuiltinArgument {
    /// The name of the argument, to be used in docstrings and error messages
    name: Ident,

    /// Type of the argument.
    ty: Box<Type>,

    /// Whether the argument should be forced before the underlying builtin
    /// function is called.
    strict: bool,

    /// Propagate catchable values as values to the function, rather than short-circuit returning
    /// them if encountered
    catch: bool,

    /// Span at which the argument was defined.
    span: Span,
}

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() != "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.
///
/// The type of each function is rewritten to receive a `Vec<Value>`, containing each `Value`
/// argument that the function receives. The body of functions is accordingly rewritten to "unwrap"
/// values from this vector and bind them to the correct names, so unless a static error occurs this
/// transformation is mostly invisible to users of the macro.
///
/// 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;
/// # use tvix_eval_builtin_macros::builtins;
///
/// #[builtins]
/// mod builtins {
///     use tvix_eval::{GenCo, ErrorKind, Value};
///
///     #[builtin("identity")]
///     pub async fn builtin_identity(co: GenCo, 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 async fn builtin_try_eval(co: GenCo, #[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 name: LitStr = 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();
                }

                // Inspect the first argument to determine if this function is
                // taking the state parameter.
                // TODO(tazjin): add a test in //tvix/eval that covers this
                let mut captures_state = false;
                if let FnArg::Typed(PatType { pat, .. }) = &f.sig.inputs[0] {
                    if let Pat::Ident(PatIdent { ident, .. }) = pat.as_ref() {
                        if *ident == "state" {
                            if state_type.is_none() {
                                panic!("builtin captures a `state` argument, but no state type was defined");
                            }

                            captures_state = true;
                        }
                    }
                }

                let mut rewritten_args = std::mem::take(&mut f.sig.inputs)
                    .into_iter()
                    .collect::<Vec<_>>();

                // Split out the value arguments from the static arguments.
                let split_idx = if captures_state { 2 } else { 1 };
                let value_args = rewritten_args.split_off(split_idx);

                let builtin_arguments = value_args
                    .into_iter()
                    .map(|arg| {
                        let span = arg.span();
                        let mut strict = true;
                        let mut catch = false;
                        let (name, ty) = match arg {
                            FnArg::Receiver(_) => {
                                return Err(quote_spanned!(span => {
                                    compile_error!("unexpected receiver argument in builtin")
                                }))
                            }
                            FnArg::Typed(PatType {
                                mut attrs, pat, ty, ..
                            }) => {
                                attrs.retain(|attr| {
                                    attr.path.get_ident().into_iter().any(|id| {
                                        if id == "lazy" {
                                            strict = false;
                                            false
                                        } else if id == "catch" {
                                            catch = true;
                                            false
                                        } else {
                                            true
                                        }
                                    })
                                });
                                match pat.as_ref() {
                                    Pat::Ident(PatIdent { ident, .. }) => {
                                        (ident.clone(), ty.clone())
                                    }
                                    _ => panic!("ignored value parameters must be named, e.g. `_x` and not just `_`"),
                                }
                            }
                        };

                        if catch && !strict {
                            return Err(quote_spanned!(span => {
                                compile_error!("Cannot mix both lazy and catch on the same argument")
                            }));
                        }

                        Ok(BuiltinArgument {
                            strict,
                            catch,
                            span,
                            name,
                            ty,
                        })
                    })
                    .collect::<Result<Vec<BuiltinArgument>, _>>();

                let builtin_arguments = match builtin_arguments {
                    Err(err) => return err.into(),

                    // reverse argument order, as they are popped from the stack
                    // slice in opposite order
                    Ok(args) => args,
                };

                // Rewrite the argument to the actual function to take a
                // `Vec<Value>`, which is then destructured into the
                // user-defined values in the function header.
                let sig_span = f.sig.span();
                rewritten_args.push(parse_quote_spanned!(sig_span=> mut values: Vec<Value>));
                f.sig.inputs = rewritten_args.into_iter().collect();

                // Rewrite the body of the function to do said argument forcing.
                //
                // This is done by creating a new block for each of the
                // arguments that evaluates it, and wraps the inner block.
                for arg in &builtin_arguments {
                    let block = &f.block;
                    let ty = &arg.ty;
                    let ident = &arg.name;

                    if arg.strict {
                        if arg.catch {
                            f.block = Box::new(parse_quote_spanned! {arg.span=> {
                                let #ident: #ty = tvix_eval::generators::request_force(&co, values.pop()
                                  .expect("Tvix bug: builtin called with incorrect number of arguments")).await;
                                #block
                            }});
                        } else {
                            f.block = Box::new(parse_quote_spanned! {arg.span=> {
                                let #ident: #ty = tvix_eval::generators::request_force(&co, values.pop()
                                  .expect("Tvix bug: builtin called with incorrect number of arguments")).await;
                                if #ident.is_catchable() {
                                    return Ok(#ident);
                                }
                                #block
                            }});
                        }
                    } else {
                        f.block = Box::new(parse_quote_spanned! {arg.span=> {
                            let #ident: #ty = values.pop()
                              .expect("Tvix bug: builtin called with incorrect number of arguments");

                            #block
                        }})
                    }
                }

                let fn_name = f.sig.ident.clone();
                let arg_count = builtin_arguments.len();
                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();
                        tvix_eval::Builtin::new(
                            #name,
                            #docstring,
                            #arg_count,
                            move |values| Gen::new(|co| tvix_eval::generators::pin_generator(#fn_name(inner_state.clone(), co, values))),
                        )
                    }});
                } else {
                    builtins.push(quote_spanned! { builtin_attr.span() => {
                        tvix_eval::Builtin::new(
                            #name,
                            #docstring,
                            #arg_count,
                            |values| Gen::new(|co| tvix_eval::generators::pin_generator(#fn_name(co, values))),
                        )
                    }});
                }
            }
        }
    }

    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()
}