use proc_macro2::{TokenStream, TokenTree};
use quote::ToTokens;
use crate::preprocess::varseg::{is_varseg_type, varseg_prefix};
#[derive(Clone, Debug)]
pub(crate) struct VarSeg {
pub(crate) prefix: String,
pub(crate) start: usize,
pub(crate) len: usize,
}
#[derive(Default)]
pub(crate) struct Mapping {
slots: Vec<(String, TokenStream)>,
}
impl Mapping {
fn bind(&mut self, name: &str, value: TokenStream) -> Result<(), ShapeError> {
if let Some((_, old)) = self.slots.iter().find(|(n, _)| n == name) {
if old.to_string() != value.to_string() {
return Err(ShapeError::InconsistentBinding(name.to_string(), old.clone(), value));
}
return Ok(());
}
self.slots.push((name.to_string(), value));
Ok(())
}
pub(crate) fn entries(&self) -> &[(String, TokenStream)] {
&self.slots
}
pub(crate) fn merge(&mut self, other: Mapping) -> Result<(), ShapeError> {
for (name, value) in other.slots {
self.bind(&name, value)?;
}
Ok(())
}
}
#[derive(Debug)]
pub(crate) enum ShapeError {
ShapeMismatch(String),
InconsistentBinding(String, TokenStream, TokenStream),
}
impl ShapeError {
pub(crate) fn message(&self) -> String {
match self {
ShapeError::ShapeMismatch(why) => {
format!(
"batch-impl: `impl{{...}}` template cannot destructure the target type ({why})"
)
}
ShapeError::InconsistentBinding(name, old, new) => format!(
"batch-impl: binding slot `{}` is bound to different subtrees \
across merged `impl{{...}}` templates (`{}` vs `{}`)",
name, old, new
),
}
}
}
pub(crate) fn match_shape(
template: &syn::Type, leaf: &syn::Type,
) -> Result<(Mapping, Vec<VarSeg>), ShapeError> {
let mut map = Mapping::default();
let mut segs = vec![];
match_ty(template, leaf, &mut map, &mut segs)?;
Ok((map, segs))
}
pub(crate) fn apply_mapping(tokens: TokenStream, entries: &[(String, TokenStream)]) -> TokenStream {
tokens
.into_iter()
.flat_map(|tt| match tt {
TokenTree::Ident(id) => {
let s = id.to_string();
match entries.iter().find(|(name, _)| name.as_str() == s) {
Some((_, repl)) => repl.clone(),
None => TokenStream::from(TokenTree::Ident(id)),
}
}
TokenTree::Group(g) => {
let inner = apply_mapping(g.stream(), entries);
let mut ng = proc_macro2::Group::new(g.delimiter(), inner);
ng.set_span(g.span());
TokenStream::from(TokenTree::Group(ng))
}
other => TokenStream::from(other),
})
.collect()
}
fn is_bare_ident(tp: &syn::TypePath) -> bool {
tp.qself.is_none()
&& tp.path.segments.len() == 1
&& matches!(tp.path.segments[0].arguments, syn::PathArguments::None)
}
fn varseg_ident(tp: &syn::Type) -> Option<&syn::Ident> {
let syn::Type::Path(p) = tp else { return None };
(p.path.segments.len() == 1).then(|| &p.path.segments[0].ident)
}
fn bare_path_ident(expr: &syn::Expr) -> Option<String> {
let syn::Expr::Path(ep) = expr else { return None };
if ep.qself.is_some()
|| ep.path.segments.len() != 1
|| !matches!(ep.path.segments[0].arguments, syn::PathArguments::None)
{
return None;
}
Some(ep.path.segments[0].ident.to_string())
}
fn match_ty(
template: &syn::Type, leaf: &syn::Type, map: &mut Mapping, segs: &mut Vec<VarSeg>,
) -> Result<(), ShapeError> {
match template {
syn::Type::Path(tp) if is_bare_ident(tp) => {
let name = &tp.path.segments[0].ident;
if is_varseg_type(template) {
return Err(ShapeError::ShapeMismatch(
"a variadic segment (`ident@..`) is only supported as a tuple element \
inside an `impl{...}` template"
.into(),
));
}
if let syn::Type::Path(lp) = leaf
&& is_bare_ident(lp)
&& lp.path.segments[0].ident == *name
{
return Ok(());
}
map.bind(&name.to_string(), leaf.to_token_stream())
}
syn::Type::Path(tp) => {
let syn::Type::Path(lp) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is a path but the target is not".into(),
));
};
if tp.qself.is_some() || lp.qself.is_some() {
return Err(ShapeError::ShapeMismatch(
"qualified paths (`<T as Trait>::...`) are not supported in templates".into(),
));
}
if tp.path.segments.len() != lp.path.segments.len() {
return Err(ShapeError::ShapeMismatch(format!(
"path segment count differs (template `{}` has {}, target has {})",
template.to_token_stream(),
tp.path.segments.len(),
lp.path.segments.len(),
)));
}
for (tseg, lseg) in tp.path.segments.iter().zip(lp.path.segments.iter()) {
if tseg.ident != lseg.ident {
map.bind(&tseg.ident.to_string(), lseg.ident.to_token_stream())?;
}
match (&tseg.arguments, &lseg.arguments) {
(syn::PathArguments::None, syn::PathArguments::None) => {}
(
syn::PathArguments::AngleBracketed(t),
syn::PathArguments::AngleBracketed(l),
) => {
if t.args.len() != l.args.len() {
return Err(ShapeError::ShapeMismatch(format!(
"generic arity differs (template `{}` has {} args, target has {})",
template.to_token_stream(),
t.args.len(),
l.args.len(),
)));
}
for (ta, la) in t.args.iter().zip(l.args.iter()) {
match (ta, la) {
(
syn::GenericArgument::Type(tt),
syn::GenericArgument::Type(lt),
) => match_ty(tt, lt, map, segs)?,
(
syn::GenericArgument::Lifetime(tl),
syn::GenericArgument::Lifetime(ll),
) => {
if tl.ident != "_" && tl.ident != ll.ident {
return Err(ShapeError::ShapeMismatch(format!(
"generic argument differs (template `{}` vs target `{}`)",
ta.to_token_stream(),
la.to_token_stream(),
)));
}
}
_ => {
if ta.to_token_stream().to_string()
!= la.to_token_stream().to_string()
{
return Err(ShapeError::ShapeMismatch(format!(
"generic argument differs (template `{}` vs target `{}`)",
ta.to_token_stream(),
la.to_token_stream(),
)));
}
}
}
}
}
(
syn::PathArguments::Parenthesized(t),
syn::PathArguments::Parenthesized(l),
) => {
if t.to_token_stream().to_string() != l.to_token_stream().to_string() {
return Err(ShapeError::ShapeMismatch(
"parenthesized generic arguments differ".into(),
));
}
}
_ => {
return Err(ShapeError::ShapeMismatch(format!(
"generic argument shape differs at segment `{}`",
tseg.ident,
)));
}
}
}
Ok(())
}
syn::Type::Reference(t) => {
let syn::Type::Reference(l) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is a reference but the target is not".into(),
));
};
if t.mutability.is_some() != l.mutability.is_some() {
return Err(ShapeError::ShapeMismatch("reference mutability differs".into()));
}
match_ty(&t.elem, &l.elem, map, segs)
}
syn::Type::Tuple(t) => {
let syn::Type::Tuple(l) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is a tuple but the target is not".into(),
));
};
let seg_count = t.elems.iter().filter(|e| is_varseg_type(e)).count();
if seg_count > 0 {
let fixed = t.elems.len() - seg_count;
if l.elems.len() < fixed {
return Err(ShapeError::ShapeMismatch(format!(
"tuple arity differs (template has {} fixed elements, target has {})",
fixed,
l.elems.len(),
)));
}
let remaining = l.elems.len() - fixed;
if remaining % seg_count != 0 {
return Err(ShapeError::ShapeMismatch(format!(
"variadic segments cannot be split evenly: target tuple has {} \
elements after {} fixed, split across {} segments",
remaining, fixed, seg_count,
)));
}
let seg_len = remaining / seg_count;
let mut leaf_idx = 0;
for te in &t.elems {
if is_varseg_type(te) {
let Some(ident) = varseg_ident(te) else {
return Err(ShapeError::ShapeMismatch(
"malformed variadic segment placeholder".into(),
));
};
let Some(prefix) = varseg_prefix(ident) else {
return Err(ShapeError::ShapeMismatch(
"malformed variadic segment placeholder".into(),
));
};
if segs.iter().any(|s| s.prefix == prefix) {
return Err(ShapeError::ShapeMismatch(format!(
"duplicate variadic segment prefix `{}` (each \
`ident@..` in one template must be unique)",
prefix,
)));
}
segs.push(VarSeg { prefix: prefix.clone(), start: leaf_idx, len: seg_len });
for k in 0..seg_len {
let name = format!("{}{}", prefix, leaf_idx + k);
map.bind(&name, l.elems[leaf_idx + k].to_token_stream())?;
}
leaf_idx += seg_len;
} else {
match_ty(te, &l.elems[leaf_idx], map, segs)?;
leaf_idx += 1;
}
}
return Ok(());
}
if t.elems.len() != l.elems.len() {
return Err(ShapeError::ShapeMismatch(format!(
"tuple arity differs (template has {}, target has {})",
t.elems.len(),
l.elems.len(),
)));
}
for (te, le) in t.elems.iter().zip(l.elems.iter()) {
match_ty(te, le, map, segs)?;
}
Ok(())
}
syn::Type::Array(t) => {
let syn::Type::Array(l) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is an array but the target is not".into(),
));
};
if let Some(name) = bare_path_ident(&t.len) {
map.bind(&name, l.len.to_token_stream())?;
} else if t.len.to_token_stream().to_string() != l.len.to_token_stream().to_string() {
return Err(ShapeError::ShapeMismatch("array length differs".into()));
}
match_ty(&t.elem, &l.elem, map, segs)
}
syn::Type::Slice(t) => {
let syn::Type::Slice(l) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is a slice but the target is not".into(),
));
};
match_ty(&t.elem, &l.elem, map, segs)
}
syn::Type::Ptr(t) => {
let syn::Type::Ptr(l) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is a pointer but the target is not".into(),
));
};
let mut_eq = matches!(
(&t.mutability, &l.mutability),
(syn::PointerMutability::Const(_), syn::PointerMutability::Const(_))
| (syn::PointerMutability::Mut(_), syn::PointerMutability::Mut(_))
);
if !mut_eq {
return Err(ShapeError::ShapeMismatch("pointer mutability differs".into()));
}
match_ty(&t.elem, &l.elem, map, segs)
}
syn::Type::Paren(t) => {
let syn::Type::Paren(l) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is a parenthesized type but the target is not".into(),
));
};
match_ty(&t.elem, &l.elem, map, segs)
}
syn::Type::Group(t) => {
let syn::Type::Group(l) = leaf else {
return Err(ShapeError::ShapeMismatch(
"the template is a grouped type but the target is not".into(),
));
};
match_ty(&t.elem, &l.elem, map, segs)
}
other => {
if other.to_token_stream().to_string() != leaf.to_token_stream().to_string() {
return Err(ShapeError::ShapeMismatch(format!(
"template `{}` does not match target `{}`",
other.to_token_stream(),
leaf.to_token_stream(),
)));
}
Ok(())
}
}
}