summaryrefslogtreecommitdiff
path: root/subplotlib-derive
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers@digital-scurf.org>2020-12-05 09:37:44 +0000
committerDaniel Silverstone <dsilvers@digital-scurf.org>2020-12-20 15:16:41 +0000
commitb97b4b896ba98eff222d9850dc7412139b7667af (patch)
treebdc9ba55d9c868236a04197464928516e9f97450 /subplotlib-derive
parent2b850b613c2052fa5b7c0dfce99054268cffe1ba (diff)
downloadsubplot-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.toml14
-rw-r--r--subplotlib-derive/src/lib.rs301
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(),
+ }
+}