diff options
author | Daniel Silverstone <dsilvers@digital-scurf.org> | 2020-12-05 09:37:44 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers@digital-scurf.org> | 2020-12-20 15:16:41 +0000 |
commit | b97b4b896ba98eff222d9850dc7412139b7667af (patch) | |
tree | bdc9ba55d9c868236a04197464928516e9f97450 /subplotlib-derive | |
parent | 2b850b613c2052fa5b7c0dfce99054268cffe1ba (diff) | |
download | subplot-b97b4b896ba98eff222d9850dc7412139b7667af.tar.gz |
subplotlib-derive: Initial derive macros
Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
Diffstat (limited to 'subplotlib-derive')
-rw-r--r-- | subplotlib-derive/Cargo.toml | 14 | ||||
-rw-r--r-- | subplotlib-derive/src/lib.rs | 301 |
2 files changed, 315 insertions, 0 deletions
diff --git a/subplotlib-derive/Cargo.toml b/subplotlib-derive/Cargo.toml new file mode 100644 index 0000000..787de3c --- /dev/null +++ b/subplotlib-derive/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "subplotlib-derive" +version = "0.1.0" +authors = ["Daniel Silverstone <dsilvers@digital-scurf.org>"] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = {version= "1", features=["full"]} +quote = "1" +proc-macro2 = "1" +fehler = "1" diff --git a/subplotlib-derive/src/lib.rs b/subplotlib-derive/src/lib.rs new file mode 100644 index 0000000..6570750 --- /dev/null +++ b/subplotlib-derive/src/lib.rs @@ -0,0 +1,301 @@ +use proc_macro::TokenStream; +use proc_macro2::Span; +use syn::{parse_macro_input, parse_quote, Error, FnArg, Ident, ItemFn, Pat, ReturnType, Type}; + +use quote::quote; + +use fehler::{throw, throws}; + +fn ty_is_borrow_str(ty: &Type) -> bool { + if let Type::Reference(ty) = ty { + if ty.mutability.is_none() && ty.lifetime.is_none() { + if let Type::Path(pp) = &*ty.elem { + pp.path.is_ident("str") + } else { + // not a path, so not &str + false + } + } else { + // mutable, or a lifetime stated, so not &str + false + } + } else { + // Not & so not &str + false + } +} + +fn ty_is_datafile(ty: &Type) -> bool { + if let Type::Path(ty) = ty { + ty.path.is_ident("SubplotDataFile") + } else { + false + } +} + +#[throws(Error)] +fn check_step_declaration(step: &ItemFn) { + // Step functions must be declared very simply as: + // fn stepfunctionname(context: &mut Context) + // the `mut` is optional, but the type of the context argument must + // be a borrow of some kind, its name is not important. + // If the step function takes any arguments, then they must come next + // and should be named in the usual way. If the argument starts with + // an underscore then that will be stripped during argument conversion + // so that if you're just ignoring an argument from your step you can. + // Additionally, step functions must **NOT** have a return type declared + // and must not be generic, non-rust ABI, unsafe, etc. in any way. + // Finally const makes no sense, though we won't deny it for now. + // Visibility will be taken into account when constructing the associated + // content for the step + let sig = &step.sig; + if let Some(syncness) = sig.asyncness.as_ref() { + throw!(Error::new_spanned( + syncness, + "Step functions may not be async", + )); + } + if let Some(unsafeness) = sig.unsafety.as_ref() { + throw!(Error::new_spanned( + unsafeness, + "Step functions may not be unsafe", + )); + } + if let Some(abi) = sig.abi.as_ref() { + throw!(Error::new_spanned( + abi, + "Step functions may not specify an ABI", + )); + } + if !matches!(sig.output, ReturnType::Default) { + throw!(Error::new_spanned( + &sig.output, + "Step functions may not specify a return value", + )); + } + if let Some(variadic) = sig.variadic.as_ref() { + throw!(Error::new_spanned( + variadic, + "Step functions may not be variadic", + )); + } + if !sig.generics.params.is_empty() || sig.generics.where_clause.is_some() { + throw!(Error::new_spanned( + &sig.generics, + "Step functions may not be generic", + )); + } + if let Some(arg) = sig.inputs.first() { + if let FnArg::Typed(pat) = arg { + if let Type::Reference(tr) = &*pat.ty { + if let Some(lifetime) = tr.lifetime.as_ref() { + throw!(Error::new_spanned( + lifetime, + "Step function context borrow should not be given a lifetime marker", + )); + } + } else { + throw!(Error::new_spanned( + pat, + "Step function context must be taken as a borrow", + )); + } + } else { + throw!(Error::new_spanned( + arg, + "Step functions do not take a method receiver", + )); + } + } else { + throw!(Error::new_spanned( + &sig.inputs, + "Step functions must have at least 1 argument (context)", + )); + } +} + +#[throws(Error)] +fn process_step(input: ItemFn) -> proc_macro2::TokenStream { + // Processing a step involves constructing a step builder for + // the function which returns a step object to be passed into the + // scenario system + + // A step builder consists of a struct whose fields are of the + // appropriate type, a set of pub methods to set those fields + // and then a build call which constructs the step instance with + // an appropriate closure in it + + let vis = input.vis.clone(); + let stepname = input.sig.ident.clone(); + let mutablectx = { + if let FnArg::Typed(pt) = &input.sig.inputs[0] { + if let Type::Reference(pp) = &*pt.ty { + pp.mutability.is_some() + } else { + unreachable!() + } + } else { + unreachable!() + } + }; + + let contexttype = if let Some(ty) = input.sig.inputs.first() { + if let FnArg::Typed(pt) = ty { + if let Type::Reference(rt) = &*pt.ty { + *(rt.elem).clone() + } else { + unreachable!() + } + } else { + unreachable!() + } + } else { + unreachable!() + }; + + let fields = input + .sig + .inputs + .iter() + .skip(1) + .map(|a| { + if let FnArg::Typed(pat) = a { + if let Pat::Ident(ident) = &*pat.pat { + if let Some(r) = ident.by_ref.as_ref() { + Err(Error::new_spanned(r, "ref not valid here")) + } else if let Some(subpat) = ident.subpat.as_ref() { + Err(Error::new_spanned(&subpat.1, "subpattern not valid here")) + } else { + let identstr = ident.ident.to_string(); + Ok(( + Ident::new(identstr.trim_start_matches('_'), ident.ident.span()), + (*pat.ty).clone(), + )) + } + } else { + Err(Error::new_spanned(pat, "expected a simple name here")) + } + } else { + Err(Error::new_spanned( + a, + "receiver argument unexpected in this position", + )) + } + }) + .collect::<Result<Vec<_>, _>>()?; + + // The builder's name is simply the step function's name with BUILDER_ + // prepended to it. We give that name the span of the step function name + // in case it gets referred to in an error + let buildername = Ident::new(&format!("BUILDER_{}", stepname), stepname.span()); + + let structdef = { + let structfields: Vec<_> = fields + .iter() + .map(|(id, ty)| { + let ty = if ty_is_borrow_str(&ty) { + parse_quote!(::std::string::String) + } else { + ty.clone() + }; + quote! { + #id : #ty + } + }) + .collect(); + quote! { + #[allow(non_camel_case_types)] + #[allow(unused)] + #[derive(Default)] + #vis struct #buildername { + #(#structfields),* + } + } + }; + + let structimpl = { + let fieldfns: Vec<_> = fields + .iter() + .map(|(id, ty)| { + if ty_is_borrow_str(ty) { + quote! { + pub fn #id(mut self, value: &str) -> Self { + self.#id = value.to_string(); + self + } + } + } else { + quote! { + pub fn #id(mut self, value: #ty) -> Self { + self.#id = value; + self + } + } + } + }) + .collect(); + + let buildargs: Vec<_> = fields + .iter() + .map(|(id, ty)| { + if ty_is_borrow_str(ty) { + quote! { + &self.#id + } + } else if ty_is_datafile(ty) { + quote! { + self.#id.clone() + } + } else { + quote! { + self.#id + } + } + }) + .collect(); + + let stepnamestr = format!("{}", stepname); + + let newfn = if mutablectx { + Ident::new("new_mutable", Span::call_site()) + } else { + Ident::new("new_immutable", Span::call_site()) + }; + + quote! { + impl #buildername { + #(#fieldfns)* + + pub fn build(self) -> ScenarioStep<#contexttype> { + ScenarioStep::#newfn(#stepnamestr, move |ctx| + #stepname(ctx, #(#buildargs),*)) + } + } + } + }; + + let ret = quote! { + #structdef + #structimpl + + #[throws(StepError)] + #[allow(unused)] // It's okay for step functions to not be used + #input + }; + + ret +} + +#[proc_macro_attribute] +pub fn step(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + + if let Err(e) = check_step_declaration(&input) { + return e.to_compile_error().into(); + } + + match process_step(input) { + Ok(toks) => toks.into(), + Err(e) => e.to_compile_error().into(), + } +} |