Skip to content

Commit

Permalink
feature: Add new crate gix-momo
Browse files Browse the repository at this point in the history
To make de-monomorphizatioin easier without sacrification of code
readability.

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
  • Loading branch information
NobodyXu committed Aug 19, 2023
1 parent e83c38f commit e326fb5
Show file tree
Hide file tree
Showing 7 changed files with 475 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ members = [
"gix-packetline",
"gix-packetline-blocking",
"gix-mailmap",
"gix-momo",
"gix-note",
"gix-negotiate",
"gix-fetchhead",
Expand Down
18 changes: 18 additions & 0 deletions gix-momo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "gix-momo"
version = "0.0.0"
edition = "2021"
description = "A gix proc_macro_attribute to outline conversions from generic functions"
authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
repository = "https://github.com/Byron/gitoxide"
license = "MIT OR Apache-2.0"
include = ["src/**/*", "LICENSE-*", "CHANGELOG.md"]
rust-version = "1.65"

[lib]
proc_macro = true

[dependencies]
syn = { version = "2.0", features = ["full", "fold"] }
quote = "1.0"
proc-macro2 = "1.0"
1 change: 1 addition & 0 deletions gix-momo/LICENSE-APACHE
1 change: 1 addition & 0 deletions gix-momo/LICENSE-MIT
302 changes: 302 additions & 0 deletions gix-momo/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
use std::collections::{HashMap, HashSet};

use proc_macro::TokenStream;
use quote::quote;
use syn::{fold::Fold, punctuated::Punctuated, spanned::Spanned, *};

#[derive(Copy, Clone)]
// All conversions we support. Check references to this type for an idea how to add more.
enum Conversion<'a> {
Into(&'a Type),
TryInto(&'a Type),
AsRef(&'a Type),
AsMut(&'a Type),
}

impl<'a> Conversion<'a> {
fn target_type(&self) -> Type {
match *self {
Conversion::Into(ty) => ty.clone(),
Conversion::TryInto(ty) => ty.clone(),
Conversion::AsRef(ty) => parse_quote!(&#ty),
Conversion::AsMut(ty) => parse_quote!(&mut #ty),
}
}

fn conversion_expr(&self, i: Ident) -> Expr {
match *self {
Conversion::Into(_) => parse_quote!(#i.into()),
Conversion::TryInto(_) => parse_quote!(#i.try_into()?),
Conversion::AsRef(_) => parse_quote!(#i.as_ref()),
Conversion::AsMut(_) => parse_quote!(#i.as_mut()),
}
}
}

fn parse_bounded_type(ty: &Type) -> Option<Ident> {
if let Type::Path(TypePath { qself: None, ref path }) = ty {
if path.segments.len() == 1 {
return Some(path.segments[0].ident.clone());
}
}
None
}

fn parse_bounds(bounds: &Punctuated<TypeParamBound, Token![+]>) -> Option<Conversion> {
if bounds.len() != 1 {
return None;
}
if let TypeParamBound::Trait(ref tb) = bounds.first().unwrap() {
if let Some(seg) = tb.path.segments.iter().last() {
if let PathArguments::AngleBracketed(ref gen_args) = seg.arguments {
if let GenericArgument::Type(ref arg_ty) = gen_args.args.first().unwrap() {
if seg.ident == "Into" {
return Some(Conversion::Into(arg_ty));
} else if seg.ident == "TryInto" {
return Some(Conversion::TryInto(arg_ty));
} else if seg.ident == "AsRef" {
return Some(Conversion::AsRef(arg_ty));
} else if seg.ident == "AsMut" {
return Some(Conversion::AsMut(arg_ty));
}
}
}
}
}
None
}

// create a map from generic type to Conversion
fn parse_generics(decl: &Signature) -> (HashMap<Ident, Conversion<'_>>, Generics) {
let mut ty_conversions = HashMap::new();
let mut params = Punctuated::new();
for gp in decl.generics.params.iter() {
if let GenericParam::Type(ref tp) = gp {
if let Some(conversion) = parse_bounds(&tp.bounds) {
ty_conversions.insert(tp.ident.clone(), conversion);
continue;
}
}
params.push(gp.clone());
}
let where_clause = if let Some(ref wc) = decl.generics.where_clause {
let mut idents_to_remove = HashSet::new();
let mut predicates = Punctuated::new();
for wp in wc.predicates.iter() {
if let WherePredicate::Type(ref pt) = wp {
if let Some(ident) = parse_bounded_type(&pt.bounded_ty) {
if let Some(conversion) = parse_bounds(&pt.bounds) {
idents_to_remove.insert(ident.clone());
ty_conversions.insert(ident, conversion);
continue;
}
}
}
predicates.push(wp.clone());
}
params = params
.into_iter()
.filter(|param| {
if let GenericParam::Type(type_param) = param {
!idents_to_remove.contains(&type_param.ident)
} else {
true
}
})
.collect();
Some(WhereClause {
predicates,
..wc.clone()
})
} else {
None
};
(
ty_conversions,
Generics {
params,
where_clause,
..decl.generics.clone()
},
)
}

fn pat_to_ident(pat: &Pat) -> Ident {
if let Pat::Ident(ref pat_ident) = *pat {
return pat_ident.ident.clone();
}
unimplemented!("No non-ident patterns for now!");
}

fn pat_to_expr(pat: &Pat) -> Expr {
let ident = pat_to_ident(pat);
parse_quote!(#ident)
}

fn convert<'a>(
inputs: &'a Punctuated<FnArg, Token![,]>,
ty_conversions: &HashMap<Ident, Conversion<'a>>,
) -> (
Punctuated<FnArg, Token![,]>,
Conversions,
Punctuated<Expr, Token![,]>,
bool,
) {
let mut argtypes = Punctuated::new();
let mut conversions = Conversions {
intos: Vec::new(),
try_intos: Vec::new(),
as_refs: Vec::new(),
as_muts: Vec::new(),
};
let mut argexprs = Punctuated::new();
let mut has_self = false;
inputs.iter().for_each(|input| match input {
FnArg::Receiver(..) => {
has_self = true;
argtypes.push(input.clone());
}
FnArg::Typed(PatType {
ref pat,
ref ty,
ref colon_token,
..
}) => match **ty {
Type::ImplTrait(TypeImplTrait { ref bounds, .. }) => {
if let Some(conv) = parse_bounds(bounds) {
argtypes.push(FnArg::Typed(PatType {
attrs: Vec::new(),
pat: pat.clone(),
colon_token: *colon_token,
ty: Box::new(conv.target_type()),
}));
let ident = pat_to_ident(pat);
conversions.add(ident.clone(), conv);
argexprs.push(conv.conversion_expr(ident));
}
}
Type::Path(..) => {
if let Some(ident) = parse_bounded_type(ty) {
if let Some(conv) = ty_conversions.get(&ident) {
argtypes.push(FnArg::Typed(PatType {
attrs: Vec::new(),
pat: pat.clone(),
colon_token: *colon_token,
ty: Box::new(conv.target_type()),
}));
let ident = pat_to_ident(pat);
conversions.add(ident, *conv);
argexprs.push(conv.conversion_expr(pat_to_ident(pat)));
}
}
}
_ => {
argtypes.push(input.clone());
argexprs.push(pat_to_expr(pat));
}
},
});
(argtypes, conversions, argexprs, has_self)
}

struct Conversions {
intos: Vec<Ident>,
try_intos: Vec<Ident>,
as_refs: Vec<Ident>,
as_muts: Vec<Ident>,
}

impl Conversions {
fn add(&mut self, ident: Ident, conv: Conversion) {
match conv {
Conversion::Into(_) => self.intos.push(ident),
Conversion::TryInto(_) => self.try_intos.push(ident),
Conversion::AsRef(_) => self.as_refs.push(ident),
Conversion::AsMut(_) => self.as_muts.push(ident),
}
}
}

fn has_conversion(idents: &[Ident], expr: &Expr) -> bool {
if let Expr::Path(ExprPath { ref path, .. }) = *expr {
if path.segments.len() == 1 {
let seg = path.segments.iter().last().unwrap();
return idents.iter().any(|i| i == &seg.ident);
}
}
false
}

#[allow(clippy::collapsible_if)]
impl Fold for Conversions {
fn fold_expr(&mut self, expr: Expr) -> Expr {
//TODO: Also catch `Expr::Call` with suitable paths & args
match expr {
Expr::MethodCall(mc) if mc.args.is_empty() => match &*mc.method.to_string() {
"into" if has_conversion(&self.intos, &mc.receiver) => *mc.receiver,
"try_into" if has_conversion(&self.try_intos, &mc.receiver) => *mc.receiver,

"as_ref" if has_conversion(&self.as_refs, &mc.receiver) => *mc.receiver,
"as_mut" if has_conversion(&self.as_muts, &mc.receiver) => *mc.receiver,

_ => syn::fold::fold_expr(self, Expr::MethodCall(mc)),
},
Expr::Try(ExprTry { expr, .. }) => match *expr {
Expr::MethodCall(mc)
if mc.args.is_empty()
&& mc.method == "try_into"
&& has_conversion(&self.try_intos, &mc.receiver) =>
{
*mc.receiver
}
expr => syn::fold::fold_expr(self, expr),
},
_ => syn::fold::fold_expr(self, expr),
}
}
}

/// Generate lightweight monomorphized wrapper around main implementation.
/// May be applied to functions and methods.
#[proc_macro_attribute]
pub fn momo(_attrs: TokenStream, input: TokenStream) -> TokenStream {
//TODO: alternatively parse ImplItem::Method
momo_inner(input.into()).into()
}

fn momo_inner(code: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let fn_item: Item = match syn::parse2(code.clone()) {
Ok(input) => input,
Err(err) => return err.to_compile_error(),
};

if let Item::Fn(ref item_fn) = fn_item {
let inner_ident = syn::parse_str::<Ident>(&format!("_{}_inner", item_fn.sig.ident)).unwrap();
let (ty_conversions, generics) = parse_generics(&item_fn.sig);
let (argtypes, mut conversions, argexprs, has_self) = convert(&item_fn.sig.inputs, &ty_conversions);
let new_item = Item::Fn(ItemFn {
block: if has_self {
parse_quote!({ self.#inner_ident(#argexprs) })
} else {
parse_quote!({ #inner_ident(#argexprs) })
},
..item_fn.clone()
});
let mut new_inner_item = ItemFn {
vis: Visibility::Inherited,
sig: Signature {
ident: inner_ident,
generics,
inputs: argtypes,
..item_fn.sig.clone()
},
..item_fn.clone()
};
new_inner_item.block =
Box::new(conversions.fold_block(std::mem::replace(new_inner_item.block.as_mut(), parse_quote!({}))));
let new_inner_item = Item::Fn(new_inner_item);
quote!(#new_item #[allow(unused_mut)] #new_inner_item)
} else {
Error::new(fn_item.span(), "expected a function").to_compile_error()
}
}

0 comments on commit e326fb5

Please sign in to comment.