diff options
Diffstat (limited to 'tvix/nix-compat-derive')
-rw-r--r-- | tvix/nix-compat-derive/Cargo.toml | 32 | ||||
-rw-r--r-- | tvix/nix-compat-derive/default.nix | 5 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/de.rs | 272 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/attrs.rs | 358 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/ctx.rs | 50 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/inputs.rs | 110 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/mod.rs | 183 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/internal/symbol.rs | 32 | ||||
-rw-r--r-- | tvix/nix-compat-derive/src/lib.rs | 348 |
9 files changed, 1390 insertions, 0 deletions
diff --git a/tvix/nix-compat-derive/Cargo.toml b/tvix/nix-compat-derive/Cargo.toml new file mode 100644 index 000000000000..dea8ab5ab159 --- /dev/null +++ b/tvix/nix-compat-derive/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "nix-compat-derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[features] +external = [] +default = ["external"] + + +[dependencies] +proc-macro2 = { version = "1.0.86", features = ["proc-macro"] } +quote = { version = "1.0.36", features = ["proc-macro"] } +syn = { version = "2.0.72", features = ["full", "extra-traits"] } + +[dev-dependencies] +hex-literal = "0.4.1" +pretty_assertions = "1.4.0" +rstest = "0.19.0" +tokio-test = "0.4.3" + +[dev-dependencies.tokio] +version = "^1.38" +features = ["io-util", "macros"] + +[dev-dependencies.nix-compat] +path = "../nix-compat" +default-features = false +features = ["async", "wire", "test"] diff --git a/tvix/nix-compat-derive/default.nix b/tvix/nix-compat-derive/default.nix new file mode 100644 index 000000000000..e6636e7f2510 --- /dev/null +++ b/tvix/nix-compat-derive/default.nix @@ -0,0 +1,5 @@ +{ depot, lib, ... }: + +depot.tvix.crates.workspaceMembers.nix-compat-derive.build.override { + runTests = true; +} diff --git a/tvix/nix-compat-derive/src/de.rs b/tvix/nix-compat-derive/src/de.rs new file mode 100644 index 000000000000..ee79ea9d1012 --- /dev/null +++ b/tvix/nix-compat-derive/src/de.rs @@ -0,0 +1,272 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::spanned::Spanned; +use syn::{DeriveInput, Generics, Path, Type}; + +use crate::internal::attrs::Default; +use crate::internal::inputs::RemoteInput; +use crate::internal::{attrs, Container, Context, Data, Field, Remote, Style, Variant}; + +pub fn expand_nix_deserialize(nnixrs: Path, input: &mut DeriveInput) -> syn::Result<TokenStream> { + let cx = Context::new(); + let cont = Container::from_ast(&cx, nnixrs, input); + cx.check()?; + let cont = cont.unwrap(); + + let ty = cont.ident_type(); + let body = nix_deserialize_body(&cont); + let crate_path = cont.crate_path(); + + Ok(nix_deserialize_impl( + crate_path, + &ty, + &cont.original.generics, + body, + )) +} + +pub fn expand_nix_deserialize_remote( + crate_path: Path, + input: &RemoteInput, +) -> syn::Result<TokenStream> { + let cx = Context::new(); + let remote = Remote::from_ast(&cx, crate_path, input); + cx.check()?; + let remote = remote.unwrap(); + + let crate_path = remote.crate_path(); + let body = nix_deserialize_body_from(crate_path, &remote.attrs).expect("From tokenstream"); + let generics = Generics::default(); + Ok(nix_deserialize_impl(crate_path, remote.ty, &generics, body)) +} + +fn nix_deserialize_impl( + crate_path: &Path, + ty: &Type, + generics: &Generics, + body: TokenStream, +) -> TokenStream { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + quote! { + #[automatically_derived] + impl #impl_generics #crate_path::nix_daemon::de::NixDeserialize for #ty #ty_generics + #where_clause + { + #[allow(clippy::manual_async_fn)] + fn try_deserialize<R>(reader: &mut R) -> impl ::std::future::Future<Output=Result<Option<Self>, R::Error>> + Send + '_ + where R: ?Sized + #crate_path::nix_daemon::de::NixRead + Send, + { + #body + } + } + } +} + +fn nix_deserialize_body_from( + crate_path: &syn::Path, + attrs: &attrs::Container, +) -> Option<TokenStream> { + if let Some(span) = attrs.from_str.as_ref() { + Some(nix_deserialize_from_str(crate_path, span.span())) + } else if let Some(type_from) = attrs.type_from.as_ref() { + Some(nix_deserialize_from(type_from)) + } else { + attrs + .type_try_from + .as_ref() + .map(|type_try_from| nix_deserialize_try_from(crate_path, type_try_from)) + } +} + +fn nix_deserialize_body(cont: &Container) -> TokenStream { + if let Some(tokens) = nix_deserialize_body_from(cont.crate_path(), &cont.attrs) { + tokens + } else { + match &cont.data { + Data::Struct(style, fields) => nix_deserialize_struct(*style, fields), + Data::Enum(variants) => nix_deserialize_enum(variants), + } + } +} + +fn nix_deserialize_struct(style: Style, fields: &[Field<'_>]) -> TokenStream { + let read_fields = fields.iter().map(|f| { + let field = f.var_ident(); + let ty = f.ty; + let read_value = quote_spanned! { + ty.span()=> if first__ { + first__ = false; + if let Some(v) = reader.try_read_value::<#ty>().await? { + v + } else { + return Ok(None); + } + } else { + reader.read_value::<#ty>().await? + } + }; + if let Some(version) = f.attrs.version.as_ref() { + let default = match &f.attrs.default { + Default::Default => quote_spanned!(ty.span()=>::std::default::Default::default), + Default::Path(path) => path.to_token_stream(), + _ => panic!("No default for versioned field"), + }; + quote! { + let #field : #ty = if (#version).contains(&reader.version().minor()) { + #read_value + } else { + #default() + }; + } + } else { + quote! { + let #field : #ty = #read_value; + } + } + }); + + let field_names = fields.iter().map(|f| f.var_ident()); + let construct = match style { + Style::Struct => { + quote! { + Self { #(#field_names),* } + } + } + Style::Tuple => { + quote! { + Self(#(#field_names),*) + } + } + Style::Unit => quote!(Self), + }; + quote! { + #[allow(unused_assignments)] + async move { + let mut first__ = true; + #(#read_fields)* + Ok(Some(#construct)) + } + } +} + +fn nix_deserialize_variant(variant: &Variant<'_>) -> TokenStream { + let ident = variant.ident; + let read_fields = variant.fields.iter().map(|f| { + let field = f.var_ident(); + let ty = f.ty; + let read_value = quote_spanned! { + ty.span()=> if first__ { + first__ = false; + if let Some(v) = reader.try_read_value::<#ty>().await? { + v + } else { + return Ok(None); + } + } else { + reader.read_value::<#ty>().await? + } + }; + if let Some(version) = f.attrs.version.as_ref() { + let default = match &f.attrs.default { + Default::Default => quote_spanned!(ty.span()=>::std::default::Default::default), + Default::Path(path) => path.to_token_stream(), + _ => panic!("No default for versioned field"), + }; + quote! { + let #field : #ty = if (#version).contains(&reader.version().minor()) { + #read_value + } else { + #default() + }; + } + } else { + quote! { + let #field : #ty = #read_value; + } + } + }); + let field_names = variant.fields.iter().map(|f| f.var_ident()); + let construct = match variant.style { + Style::Struct => { + quote! { + Self::#ident { #(#field_names),* } + } + } + Style::Tuple => { + quote! { + Self::#ident(#(#field_names),*) + } + } + Style::Unit => quote!(Self::#ident), + }; + let version = &variant.attrs.version; + quote! { + #version => { + #(#read_fields)* + Ok(Some(#construct)) + } + } +} + +fn nix_deserialize_enum(variants: &[Variant<'_>]) -> TokenStream { + let match_variant = variants + .iter() + .map(|variant| nix_deserialize_variant(variant)); + quote! { + #[allow(unused_assignments)] + async move { + let mut first__ = true; + match reader.version().minor() { + #(#match_variant)* + } + } + } +} + +fn nix_deserialize_from(ty: &Type) -> TokenStream { + quote_spanned! { + ty.span() => + async move { + if let Some(value) = reader.try_read_value::<#ty>().await? { + Ok(Some(<Self as ::std::convert::From<#ty>>::from(value))) + } else { + Ok(None) + } + } + } +} + +fn nix_deserialize_try_from(crate_path: &Path, ty: &Type) -> TokenStream { + quote_spanned! { + ty.span() => + async move { + use #crate_path::nix_daemon::de::Error; + if let Some(item) = reader.try_read_value::<#ty>().await? { + <Self as ::std::convert::TryFrom<#ty>>::try_from(item) + .map_err(Error::invalid_data) + .map(Some) + } else { + Ok(None) + } + } + } +} + +fn nix_deserialize_from_str(crate_path: &Path, span: Span) -> TokenStream { + quote_spanned! { + span => + async move { + use #crate_path::nix_daemon::de::Error; + if let Some(buf) = reader.try_read_bytes().await? { + let s = ::std::str::from_utf8(&buf) + .map_err(Error::invalid_data)?; + <Self as ::std::str::FromStr>::from_str(s) + .map_err(Error::invalid_data) + .map(Some) + } else { + Ok(None) + } + } + } +} diff --git a/tvix/nix-compat-derive/src/internal/attrs.rs b/tvix/nix-compat-derive/src/internal/attrs.rs new file mode 100644 index 000000000000..dbc959d1e917 --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/attrs.rs @@ -0,0 +1,358 @@ +use quote::ToTokens; +use syn::meta::ParseNestedMeta; +use syn::parse::Parse; +use syn::{parse_quote, Attribute, Expr, ExprLit, ExprPath, Lit, Token}; + +use super::symbol::{Symbol, CRATE, DEFAULT, FROM, FROM_STR, NIX, TRY_FROM, VERSION}; +use super::Context; + +#[derive(Debug, PartialEq, Eq)] +pub enum Default { + None, + #[allow(clippy::enum_variant_names)] + Default, + Path(ExprPath), +} + +impl Default { + pub fn is_none(&self) -> bool { + matches!(self, Default::None) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Field { + pub default: Default, + pub version: Option<syn::ExprRange>, +} + +impl Field { + pub fn from_ast(ctx: &Context, attrs: &Vec<Attribute>) -> Field { + let mut version = None; + let mut default = Default::None; + for attr in attrs { + if attr.path() != NIX { + continue; + } + if let Err(err) = attr.parse_nested_meta(|meta| { + if meta.path == VERSION { + version = parse_lit(ctx, &meta, VERSION)?; + } else if meta.path == DEFAULT { + if meta.input.peek(Token![=]) { + if let Some(path) = parse_lit(ctx, &meta, DEFAULT)? { + default = Default::Path(path); + } + } else { + default = Default::Default; + } + } else { + let path = meta.path.to_token_stream().to_string(); + return Err(meta.error(format_args!("unknown nix field attribute '{}'", path))); + } + Ok(()) + }) { + eprintln!("{:?}", err.span().source_text()); + ctx.syn_error(err); + } + } + if version.is_some() && default.is_none() { + default = Default::Default; + } + + Field { default, version } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Variant { + pub version: syn::ExprRange, +} + +impl Variant { + pub fn from_ast(ctx: &Context, attrs: &Vec<Attribute>) -> Variant { + let mut version = parse_quote!(..); + for attr in attrs { + if attr.path() != NIX { + continue; + } + if let Err(err) = attr.parse_nested_meta(|meta| { + if meta.path == VERSION { + if let Some(v) = parse_lit(ctx, &meta, VERSION)? { + version = v; + } + } else { + let path = meta.path.to_token_stream().to_string(); + return Err( + meta.error(format_args!("unknown nix variant attribute '{}'", path)) + ); + } + Ok(()) + }) { + eprintln!("{:?}", err.span().source_text()); + ctx.syn_error(err); + } + } + + Variant { version } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Container { + pub from_str: Option<syn::Path>, + pub type_from: Option<syn::Type>, + pub type_try_from: Option<syn::Type>, + pub crate_path: Option<syn::Path>, +} + +impl Container { + pub fn from_ast(ctx: &Context, attrs: &Vec<Attribute>) -> Container { + let mut type_from = None; + let mut type_try_from = None; + let mut crate_path = None; + let mut from_str = None; + + for attr in attrs { + if attr.path() != NIX { + continue; + } + if let Err(err) = attr.parse_nested_meta(|meta| { + if meta.path == FROM { + type_from = parse_lit(ctx, &meta, FROM)?; + } else if meta.path == TRY_FROM { + type_try_from = parse_lit(ctx, &meta, TRY_FROM)?; + } else if meta.path == FROM_STR { + from_str = Some(meta.path); + } else if meta.path == CRATE { + crate_path = parse_lit(ctx, &meta, CRATE)?; + } else { + let path = meta.path.to_token_stream().to_string(); + return Err( + meta.error(format_args!("unknown nix variant attribute '{}'", path)) + ); + } + Ok(()) + }) { + eprintln!("{:?}", err.span().source_text()); + ctx.syn_error(err); + } + } + + Container { + from_str, + type_from, + type_try_from, + crate_path, + } + } +} + +pub fn get_lit_str( + ctx: &Context, + meta: &ParseNestedMeta, + attr: Symbol, +) -> syn::Result<Option<syn::LitStr>> { + let expr: Expr = meta.value()?.parse()?; + let mut value = &expr; + while let Expr::Group(e) = value { + value = &e.expr; + } + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = value + { + Ok(Some(s.clone())) + } else { + ctx.error_spanned( + expr, + format_args!("expected nix attribute {} to be string", attr), + ); + Ok(None) + } +} + +pub fn parse_lit<T: Parse>( + ctx: &Context, + meta: &ParseNestedMeta, + attr: Symbol, +) -> syn::Result<Option<T>> { + match get_lit_str(ctx, meta, attr)? { + Some(lit) => Ok(Some(lit.parse()?)), + None => Ok(None), + } +} + +#[cfg(test)] +mod test { + use syn::{parse_quote, Attribute}; + + use crate::internal::Context; + + use super::*; + + #[test] + fn parse_field_version() { + let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(version="..34")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Default, + version: Some(parse_quote!(..34)), + } + ); + } + + #[test] + fn parse_field_default() { + let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(default)])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Default, + version: None, + } + ); + } + + #[test] + fn parse_field_default_path() { + let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(default="Default::default")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Path(parse_quote!(Default::default)), + version: None, + } + ); + } + + #[test] + fn parse_field_both() { + let attrs: Vec<Attribute> = + vec![parse_quote!(#[nix(version="..", default="Default::default")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Path(parse_quote!(Default::default)), + version: Some(parse_quote!(..)), + } + ); + } + + #[test] + fn parse_field_both_rev() { + let attrs: Vec<Attribute> = + vec![parse_quote!(#[nix(default="Default::default", version="..")])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::Path(parse_quote!(Default::default)), + version: Some(parse_quote!(..)), + } + ); + } + + #[test] + fn parse_field_no_attr() { + let attrs: Vec<Attribute> = vec![]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::None, + version: None, + } + ); + } + + #[test] + fn parse_field_no_subattrs() { + let attrs: Vec<Attribute> = vec![parse_quote!(#[nix()])]; + let ctx = Context::new(); + let field = Field::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + field, + Field { + default: Default::None, + version: None, + } + ); + } + + #[test] + fn parse_variant_version() { + let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(version="..34")])]; + let ctx = Context::new(); + let variant = Variant::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + variant, + Variant { + version: parse_quote!(..34), + } + ); + } + + #[test] + fn parse_variant_no_attr() { + let attrs: Vec<Attribute> = vec![]; + let ctx = Context::new(); + let variant = Variant::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + variant, + Variant { + version: parse_quote!(..), + } + ); + } + + #[test] + fn parse_variant_no_subattrs() { + let attrs: Vec<Attribute> = vec![parse_quote!(#[nix()])]; + let ctx = Context::new(); + let variant = Variant::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + variant, + Variant { + version: parse_quote!(..), + } + ); + } + + #[test] + fn parse_container_try_from() { + let attrs: Vec<Attribute> = vec![parse_quote!(#[nix(try_from="u64")])]; + let ctx = Context::new(); + let container = Container::from_ast(&ctx, &attrs); + ctx.check().unwrap(); + assert_eq!( + container, + Container { + from_str: None, + type_from: None, + type_try_from: Some(parse_quote!(u64)), + crate_path: None, + } + ); + } +} diff --git a/tvix/nix-compat-derive/src/internal/ctx.rs b/tvix/nix-compat-derive/src/internal/ctx.rs new file mode 100644 index 000000000000..ba770e044bc2 --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/ctx.rs @@ -0,0 +1,50 @@ +use std::cell::RefCell; +use std::fmt; +use std::thread::panicking; + +use quote::ToTokens; + +pub struct Context { + errors: RefCell<Option<Vec<syn::Error>>>, +} + +impl Context { + pub fn new() -> Context { + Context { + errors: RefCell::new(Some(Vec::new())), + } + } + + pub fn syn_error(&self, error: syn::Error) { + self.errors + .borrow_mut() + .as_mut() + .take() + .unwrap() + .push(error); + } + + pub fn error_spanned<T: ToTokens, D: fmt::Display>(&self, tokens: T, message: D) { + self.syn_error(syn::Error::new_spanned(tokens, message)); + } + + pub fn check(&self) -> syn::Result<()> { + let mut iter = self.errors.borrow_mut().take().unwrap().into_iter(); + let mut err = match iter.next() { + None => return Ok(()), + Some(err) => err, + }; + for next_err in iter { + err.combine(next_err); + } + Err(err) + } +} + +impl Drop for Context { + fn drop(&mut self) { + if self.errors.borrow().is_some() && !panicking() { + panic!("Context dropped without checking errors"); + } + } +} diff --git a/tvix/nix-compat-derive/src/internal/inputs.rs b/tvix/nix-compat-derive/src/internal/inputs.rs new file mode 100644 index 000000000000..097a141a5d7c --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/inputs.rs @@ -0,0 +1,110 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RemoteInput { + pub attrs: Vec<syn::Attribute>, + pub ident: syn::Type, +} + +impl syn::parse::Parse for RemoteInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let attrs = input.call(syn::Attribute::parse_outer)?; + + let ident = input.parse::<syn::Type>()?; + Ok(RemoteInput { attrs, ident }) + } +} + +impl quote::ToTokens for RemoteInput { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + fn is_outer(attr: &&syn::Attribute) -> bool { + match attr.style { + syn::AttrStyle::Outer => true, + syn::AttrStyle::Inner(_) => false, + } + } + for attr in self.attrs.iter().filter(is_outer) { + attr.to_tokens(tokens); + } + self.ident.to_tokens(tokens); + } +} + +#[cfg(test)] +mod test { + use syn::parse_quote; + //use syn::parse::Parse; + + use super::*; + + #[test] + fn test_input() { + let p: RemoteInput = parse_quote!(u64); + assert_eq!( + p, + RemoteInput { + attrs: vec![], + ident: parse_quote!(u64), + } + ); + } + + #[test] + fn test_input_attr() { + let p: RemoteInput = parse_quote!( + #[nix] + u64 + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[nix])], + ident: parse_quote!(u64), + } + ); + } + + #[test] + fn test_input_attr_multiple() { + let p: RemoteInput = parse_quote!( + #[nix] + #[hello] + u64 + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[nix]), parse_quote!(#[hello])], + ident: parse_quote!(u64), + } + ); + } + + #[test] + fn test_input_attr_full() { + let p: RemoteInput = parse_quote!( + #[nix(try_from = "u64")] + usize + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[nix(try_from="u64")])], + ident: parse_quote!(usize), + } + ); + } + + #[test] + fn test_input_attr_other() { + let p: RemoteInput = parse_quote!( + #[muh] + u64 + ); + assert_eq!( + p, + RemoteInput { + attrs: vec![parse_quote!(#[muh])], + ident: parse_quote!(u64), + } + ); + } +} diff --git a/tvix/nix-compat-derive/src/internal/mod.rs b/tvix/nix-compat-derive/src/internal/mod.rs new file mode 100644 index 000000000000..20b243221619 --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/mod.rs @@ -0,0 +1,183 @@ +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::Token; + +pub mod attrs; +mod ctx; +pub mod inputs; +mod symbol; + +pub use ctx::Context; + +pub struct Field<'a> { + pub member: syn::Member, + pub ty: &'a syn::Type, + pub attrs: attrs::Field, + pub original: &'a syn::Field, +} + +impl<'a> Field<'a> { + pub fn from_ast(ctx: &Context, idx: usize, field: &'a syn::Field) -> Field<'a> { + let attrs = attrs::Field::from_ast(ctx, &field.attrs); + let member = match &field.ident { + Some(id) => syn::Member::Named(id.clone()), + None => syn::Member::Unnamed(idx.into()), + }; + Field { + member, + attrs, + ty: &field.ty, + original: field, + } + } + + pub fn var_ident(&self) -> syn::Ident { + match &self.member { + syn::Member::Named(name) => name.clone(), + syn::Member::Unnamed(idx) => { + syn::Ident::new(&format!("field{}", idx.index), self.original.span()) + } + } + } +} + +pub struct Variant<'a> { + pub ident: &'a syn::Ident, + pub attrs: attrs::Variant, + pub style: Style, + pub fields: Vec<Field<'a>>, + //pub original: &'a syn::Variant, +} + +impl<'a> Variant<'a> { + pub fn from_ast(ctx: &Context, variant: &'a syn::Variant) -> Self { + let attrs = attrs::Variant::from_ast(ctx, &variant.attrs); + let (style, fields) = match &variant.fields { + syn::Fields::Named(fields) => (Style::Struct, fields_ast(ctx, &fields.named)), + syn::Fields::Unnamed(fields) => (Style::Tuple, fields_ast(ctx, &fields.unnamed)), + syn::Fields::Unit => (Style::Unit, Vec::new()), + }; + Variant { + ident: &variant.ident, + attrs, + style, + fields, + //original: variant, + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum Style { + Struct, + Tuple, + Unit, +} + +pub enum Data<'a> { + Enum(Vec<Variant<'a>>), + Struct(Style, Vec<Field<'a>>), +} + +pub struct Container<'a> { + pub ident: &'a syn::Ident, + pub attrs: attrs::Container, + pub data: Data<'a>, + pub crate_path: syn::Path, + pub original: &'a syn::DeriveInput, +} + +impl<'a> Container<'a> { + pub fn from_ast( + ctx: &Context, + crate_path: syn::Path, + input: &'a mut syn::DeriveInput, + ) -> Option<Container<'a>> { + let attrs = attrs::Container::from_ast(ctx, &input.attrs); + let data = match &input.data { + syn::Data::Struct(s) => match &s.fields { + syn::Fields::Named(fields) => { + Data::Struct(Style::Struct, fields_ast(ctx, &fields.named)) + } + syn::Fields::Unnamed(fields) => { + Data::Struct(Style::Tuple, fields_ast(ctx, &fields.unnamed)) + } + syn::Fields::Unit => Data::Struct(Style::Unit, Vec::new()), + }, + syn::Data::Enum(e) => { + let variants = e + .variants + .iter() + .map(|variant| Variant::from_ast(ctx, variant)) + .collect(); + Data::Enum(variants) + } + syn::Data::Union(u) => { + ctx.error_spanned(u.union_token, "Union not supported by nixrs"); + return None; + } + }; + Some(Container { + ident: &input.ident, + attrs, + data, + crate_path, + original: input, + }) + } + + pub fn crate_path(&self) -> &syn::Path { + if let Some(crate_path) = self.attrs.crate_path.as_ref() { + crate_path + } else { + &self.crate_path + } + } + + pub fn ident_type(&self) -> syn::Type { + let path: syn::Path = self.ident.clone().into(); + let tp = syn::TypePath { qself: None, path }; + tp.into() + } +} + +pub struct Remote<'a> { + pub attrs: attrs::Container, + pub ty: &'a syn::Type, + pub crate_path: syn::Path, +} + +impl<'a> Remote<'a> { + pub fn from_ast( + ctx: &Context, + crate_path: syn::Path, + input: &'a inputs::RemoteInput, + ) -> Option<Remote<'a>> { + let attrs = attrs::Container::from_ast(ctx, &input.attrs); + if attrs.from_str.is_none() && attrs.type_from.is_none() && attrs.type_try_from.is_none() { + ctx.error_spanned(input, "Missing from_str, from or try_from attribute"); + return None; + } + Some(Remote { + ty: &input.ident, + attrs, + crate_path, + }) + } + + pub fn crate_path(&self) -> &syn::Path { + if let Some(crate_path) = self.attrs.crate_path.as_ref() { + crate_path + } else { + &self.crate_path + } + } +} + +fn fields_ast<'a>(ctx: &Context, fields: &'a Punctuated<syn::Field, Token![,]>) -> Vec<Field<'a>> { + fields + .iter() + .enumerate() + .map(|(idx, field)| Field::from_ast(ctx, idx, field)) + .collect() +} diff --git a/tvix/nix-compat-derive/src/internal/symbol.rs b/tvix/nix-compat-derive/src/internal/symbol.rs new file mode 100644 index 000000000000..ed3fe304eb5d --- /dev/null +++ b/tvix/nix-compat-derive/src/internal/symbol.rs @@ -0,0 +1,32 @@ +use std::fmt; + +use syn::Path; + +#[derive(Copy, Clone)] +pub struct Symbol(&'static str); + +pub const NIX: Symbol = Symbol("nix"); +pub const VERSION: Symbol = Symbol("version"); +pub const DEFAULT: Symbol = Symbol("default"); +pub const FROM: Symbol = Symbol("from"); +pub const TRY_FROM: Symbol = Symbol("try_from"); +pub const FROM_STR: Symbol = Symbol("from_str"); +pub const CRATE: Symbol = Symbol("crate"); + +impl PartialEq<Symbol> for Path { + fn eq(&self, word: &Symbol) -> bool { + self.is_ident(word.0) + } +} + +impl<'a> PartialEq<Symbol> for &'a Path { + fn eq(&self, word: &Symbol) -> bool { + self.is_ident(word.0) + } +} + +impl fmt::Display for Symbol { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(self.0) + } +} diff --git a/tvix/nix-compat-derive/src/lib.rs b/tvix/nix-compat-derive/src/lib.rs new file mode 100644 index 000000000000..5c0dd5c2d480 --- /dev/null +++ b/tvix/nix-compat-derive/src/lib.rs @@ -0,0 +1,348 @@ +//! # Using derive +//! +//! 1. [Overview](#overview) +//! 3. [Attributes](#attributes) +//! 1. [Container attributes](#container-attributes) +//! 1. [`#[nix(from_str)]`](#nixfrom_str) +//! 2. [`#[nix(from = "FromType")]`](#nixfrom--fromtype) +//! 3. [`#[nix(try_from = "FromType")]`](#nixtry_from--fromtype) +//! 4. [`#[nix(crate = "...")]`](#nixcrate--) +//! 2. [Variant attributes](#variant-attributes) +//! 1. [`#[nix(version = "range")]`](#nixversion--range) +//! 3. [Field attributes](#field-attributes) +//! 1. [`#[nix(version = "range")]`](#nixversion--range-1) +//! 2. [`#[nix(default)]`](#nixdefault) +//! 3. [`#[nix(default = "path")]`](#nixdefault--path) +//! +//! ## Overview +//! +//! This crate contains derive macros and function-like macros for implementing +//! `NixDeserialize` with less boilerplate. +//! +//! ### Examples +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Unnamed(u64, String); +//! ``` +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Fields { +//! number: u64, +//! message: String, +//! }; +//! ``` +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Ignored; +//! ``` +//! +//! ## Attributes +//! +//! To customize the derived trait implementations you can add +//! [attributes](https://doc.rust-lang.org/reference/attributes.html) +//! to containers, fields and variants. +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(crate="nix_compat")] // <-- This is a container attribute +//! struct Fields { +//! number: u64, +//! #[nix(version="..20")] // <-- This is a field attribute +//! message: String, +//! }; +//! +//! #[derive(NixDeserialize)] +//! #[nix(crate="nix_compat")] // <-- This is also a container attribute +//! enum E { +//! #[nix(version="..10")] // <-- This is a variant attribute +//! A(u64), +//! #[nix(version="10..")] // <-- This is also a variant attribute +//! B(String), +//! } +//! ``` +//! +//! ### Container attributes +//! +//! ##### `#[nix(from_str)]` +//! +//! When `from_str` is specified the fields are all ignored and instead a +//! `String` is first deserialized and then `FromStr::from_str` is used +//! to convert this `String` to the container type. +//! +//! This means that the container must implement `FromStr` and the error +//! returned from the `from_str` must implement `Display`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(from_str)] +//! struct MyString(String); +//! impl std::str::FromStr for MyString { +//! type Err = String; +//! fn from_str(s: &str) -> Result<Self, Self::Err> { +//! if s != "bad string" { +//! Ok(MyString(s.to_string())) +//! } else { +//! Err("Got a bad string".to_string()) +//! } +//! } +//! } +//! ``` +//! +//! ##### `#[nix(from = "FromType")]` +//! +//! When `from` is specified the fields are all ignored and instead a +//! value of `FromType` is first deserialized and then `From::from` is +//! used to convert from this value to the container type. +//! +//! This means that the container must implement `From<FromType>` and +//! `FromType` must implement `NixDeserialize`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(from="usize")] +//! struct MyValue(usize); +//! impl From<usize> for MyValue { +//! fn from(val: usize) -> Self { +//! MyValue(val) +//! } +//! } +//! ``` +//! +//! ##### `#[nix(try_from = "FromType")]` +//! +//! With `try_from` a value of `FromType` is first deserialized and then +//! `TryFrom::try_from` is used to convert from this value to the container +//! type. +//! +//! This means that the container must implement `TryFrom<FromType>` and +//! `FromType` must implement `NixDeserialize`. +//! The error returned from `try_from` also needs to implement `Display`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! #[nix(try_from="usize")] +//! struct WrongAnswer(usize); +//! impl TryFrom<usize> for WrongAnswer { +//! type Error = String; +//! fn try_from(val: usize) -> Result<Self, Self::Error> { +//! if val != 42 { +//! Ok(WrongAnswer(val)) +//! } else { +//! Err("Got the answer to life the universe and everything".to_string()) +//! } +//! } +//! } +//! ``` +//! +//! ##### `#[nix(crate = "...")]` +//! +//! Specify the path to the `nix-compat` crate instance to use when referring +//! to the API in the generated code. This is usually not needed. +//! +//! ### Variant attributes +//! +//! ##### `#[nix(version = "range")]` +//! +//! Specifies the protocol version range where this variant is used. +//! When deriving an enum the `version` attribute is used to select which +//! variant of the enum to deserialize. The range is for minor version and +//! the version ranges of all variants combined must cover all versions +//! without any overlap or the first variant that matches is selected. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! #[derive(NixDeserialize)] +//! enum Testing { +//! #[nix(version="..=18")] +//! OldVersion(u64), +//! #[nix(version="19..")] +//! NewVersion(String), +//! } +//! ``` +//! +//! ### Field attributes +//! +//! ##### `#[nix(version = "range")]` +//! +//! Specifies the protocol version range where this field is included. +//! The range is for minor version. For example `version = "..20"` +//! includes the field in protocol versions `1.0` to `1.19` and skips +//! it in version `1.20` and above. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Field { +//! number: u64, +//! #[nix(version="..20")] +//! messsage: String, +//! } +//! ``` +//! +//! ##### `#[nix(default)]` +//! +//! When a field is skipped because the active protocol version falls +//! outside the range specified in [`#[nix(version = "range")]`](#nixversion--range-1) +//! this attribute indicates that `Default::default()` should be used +//! to get a value for the field. This is also the default +//! when you only specify [`#[nix(version = "range")]`](#nixversion--range-1). +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Field { +//! number: u64, +//! #[nix(version="..20", default)] +//! messsage: String, +//! } +//! ``` +//! +//! ##### `#[nix(default = "path")]` +//! +//! When a field is skipped because the active protocol version falls +//! outside the range specified in [`#[nix(version = "range")]`](#nixversion--range-1) +//! this attribute indicates that the function in `path` should be called to +//! get a default value for the field. The given function must be callable +//! as `fn() -> T`. +//! For example `default = "my_value"` would call `my_value()` and `default = +//! "AType::empty"` would call `AType::empty()`. +//! +//! ###### Example +//! +//! ```rust +//! # use nix_compat_derive::NixDeserialize; +//! # +//! #[derive(NixDeserialize)] +//! struct Field { +//! number: u64, +//! #[nix(version="..20", default="missing_string")] +//! messsage: String, +//! } +//! +//! fn missing_string() -> String { +//! "missing string".to_string() +//! } +//! ``` + +use internal::inputs::RemoteInput; +use proc_macro::TokenStream; +use syn::{parse_quote, DeriveInput}; + +mod de; +mod internal; + +#[cfg(not(feature = "external"))] +#[proc_macro_derive(NixDeserialize, attributes(nix))] +pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream { + let mut input = syn::parse_macro_input!(item as DeriveInput); + let nnixrs: syn::Path = parse_quote!(crate); + de::expand_nix_deserialize(nnixrs, &mut input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[cfg(feature = "external")] +#[proc_macro_derive(NixDeserialize, attributes(nix))] +pub fn derive_nix_deserialize(item: TokenStream) -> TokenStream { + let mut input = syn::parse_macro_input!(item as DeriveInput); + let nnixrs: syn::Path = parse_quote!(::nix_compat); + de::expand_nix_deserialize(nnixrs, &mut input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Macro to implement `NixDeserialize` on a type. +/// Sometimes you can't use the deriver to implement `NixDeserialize` +/// (like when dealing with types in Rust standard library) but don't want +/// to implement it yourself. So this macro can be used for those situations +/// where you would derive using `#[nix(from_str)]`, +/// `#[nix(from = "FromType")]` or `#[nix(try_from = "FromType")]` if you +/// could. +/// +/// #### Example +/// +/// ```rust +/// # use nix_compat_derive::nix_deserialize_remote; +/// # +/// struct MyU64(u64); +/// +/// impl From<u64> for MyU64 { +/// fn from(value: u64) -> Self { +/// Self(value) +/// } +/// } +/// +/// nix_deserialize_remote!(#[nix(from="u64")] MyU64); +/// ``` +#[cfg(not(feature = "external"))] +#[proc_macro] +pub fn nix_deserialize_remote(item: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(item as RemoteInput); + let crate_path = parse_quote!(crate); + de::expand_nix_deserialize_remote(crate_path, &input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Macro to implement `NixDeserialize` on a type. +/// Sometimes you can't use the deriver to implement `NixDeserialize` +/// (like when dealing with types in Rust standard library) but don't want +/// to implement it yourself. So this macro can be used for those situations +/// where you would derive using `#[nix(from_str)]`, +/// `#[nix(from = "FromType")]` or `#[nix(try_from = "FromType")]` if you +/// could. +/// +/// #### Example +/// +/// ```rust +/// # use nix_compat_derive::nix_deserialize_remote; +/// # +/// struct MyU64(u64); +/// +/// impl From<u64> for MyU64 { +/// fn from(value: u64) -> Self { +/// Self(value) +/// } +/// } +/// +/// nix_deserialize_remote!(#[nix(from="u64")] MyU64); +/// ``` +#[cfg(feature = "external")] +#[proc_macro] +pub fn nix_deserialize_remote(item: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(item as RemoteInput); + let crate_path = parse_quote!(::nix_compat); + de::expand_nix_deserialize_remote(crate_path, &input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} |