use proc_macro2::{Literal, Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{
Expr, Ident, Path, Token, bracketed,
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
};
enum Slot {
Check(Box<Expr>),
Wildcard,
Rest(Token![..]),
}
impl Parse for Slot {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(Token![..]) {
Ok(Slot::Rest(input.parse()?))
} else if input.peek(Token![_]) {
input.parse::<Token![_]>()?;
Ok(Slot::Wildcard)
} else {
Ok(Slot::Check(Box::new(input.parse()?)))
}
}
}
pub(crate) struct ShapeInput {
krate: Path,
call: String,
tensor: Expr,
slots: Vec<Slot>,
}
impl Parse for ShapeInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let krate: Path = input.parse()?;
input.parse::<Token![,]>()?;
let call = input.fork().parse::<TokenStream>()?.to_string();
let tensor: Expr = input.parse()?;
input.parse::<Token![,]>()?;
let content;
bracketed!(content in input);
let slots: Vec<Slot> = Punctuated::<Slot, Token![,]>::parse_terminated(&content)?
.into_iter()
.collect();
let mut rests = slots.iter().filter_map(|slot| match slot {
Slot::Rest(token) => Some(token),
_ => None,
});
rests.next();
if let Some(second) = rests.next() {
return Err(syn::Error::new(
second.span(),
"at most one `..` per pattern",
));
}
Ok(ShapeInput {
krate,
call,
tensor,
slots,
})
}
}
#[derive(Clone, Copy)]
pub(crate) enum Mode {
Assert,
DebugAssert,
}
impl Mode {
fn name(self) -> &'static str {
match self {
Mode::Assert => "assert_shape!",
Mode::DebugAssert => "debug_assert_shape!",
}
}
}
pub(crate) fn expand(input: ShapeInput, mode: Mode) -> TokenStream {
let ShapeInput {
krate,
call,
tensor,
slots,
} = input;
let t = Ident::new("__tensor", Span::mixed_site());
let dims = Ident::new("__dims", Span::mixed_site());
let rank = Ident::new("__rank", Span::mixed_site());
let axis = Ident::new("__axis", Span::mixed_site());
let expected = Ident::new("__expected", Span::mixed_site());
let call = format!("{}({call})", mode.name());
let rest = slots.iter().position(|slot| matches!(slot, Slot::Rest(_)));
let (prefix, suffix): (&[Slot], &[Slot]) = match rest {
Some(position) => (&slots[..position], &slots[position + 1..]),
None => (&slots[..], &[]),
};
let check = |index: TokenStream, expr: &Expr| {
quote_spanned! { expr.span() =>
{
let #axis: usize = #index;
let #expected: usize = #expr;
::core::assert!(
#dims[#axis] == #expected,
"{}: axis {} expected {}, got {} (dims {:?})",
#call, #axis, #expected, #dims[#axis], #dims
);
}
}
};
let prefix_checks = prefix
.iter()
.enumerate()
.filter_map(|(i, slot)| match slot {
Slot::Check(expr) => {
let index = Literal::usize_unsuffixed(i);
Some(check(quote! { #index }, expr))
}
_ => None,
});
let suffix_checks = suffix
.iter()
.enumerate()
.filter_map(|(j, slot)| match slot {
Slot::Check(expr) => {
let from_end = Literal::usize_unsuffixed(suffix.len() - j);
Some(check(quote! { #rank - #from_end }, expr))
}
_ => None,
});
let rank_check = match rest {
None => {
let exact = slots.len();
quote_spanned! { tensor.span() =>
let #t = &#tensor;
let #dims: [usize; #exact] = #krate::Tensor::dims(#t);
}
}
Some(_) => {
let min = prefix.len() + suffix.len();
let min_check = (min > 0).then(|| {
quote! {
::core::assert!(
#rank >= #min,
"{}: expected rank at least {}, got {} (dims {:?})",
#call, #min, #rank, #dims
);
}
});
quote_spanned! { tensor.span() =>
let #t = &#tensor;
let #dims = #krate::Tensor::dims(#t);
let #rank = #dims.len();
#min_check
}
}
};
let body = quote! {
#rank_check
#(#prefix_checks)*
#(#suffix_checks)*
};
match mode {
Mode::DebugAssert => quote! { if ::core::cfg!(debug_assertions) { #body } },
Mode::Assert => quote! { { #body } },
}
}