use proc_macro2::{Ident, Spacing, TokenStream, TokenTree};
use quote::quote;
use crate::apply::err_ty_at;
use crate::ast::*;
use crate::parse::parse_item;
use crate::parse::resolve_at_refs;
use crate::util::{Cursor, compile_error_ty, is_punct, is_single_colon, scan_stop};
pub(crate) fn parse_generic(
tokens: &[TokenTree],
) -> Option<(Vec<TokenTree>, TokenStream, Vec<TokenTree>)> {
for (i, token) in tokens.iter().enumerate() {
if let TokenTree::Group(g) = token
&& g.delimiter() == delimiter![<>]
{
if i == 0 {
return None;
}
return Some((
tokens[..i].to_vec(),
g.stream(),
tokens[i + 1..].to_vec(),
));
}
}
None
}
pub(crate) fn parse_type_params(
tokens: &[TokenTree],
) -> Option<(TokenStream, Vec<TokenTree>)> {
let TokenTree::Group(g) = tokens.first()? else {
return None;
};
if g.delimiter() != delimiter![<>] {
return None;
}
Some((g.stream(), tokens[1..].to_vec()))
}
pub(crate) fn is_trait_base(base: &[TokenTree], trait_name: Option<&Ident>) -> bool {
trait_name.is_some_and(
|name| matches!(base.last(), Some(TokenTree::Ident(last)) if last == name),
)
}
pub(crate) fn split_at_depth0(
tokens: &[TokenTree], separator: char,
) -> Vec<&[TokenTree]> {
let mut chunks = vec![];
let mut rest = tokens;
while let Some(index) = scan_stop(rest, &[separator]) {
chunks.push(&rest[..index]);
rest = &rest[index + 1..];
}
chunks.push(rest);
chunks
}
fn find_colon_at_depth0(tokens: &[TokenTree]) -> Option<usize> {
scan_stop(tokens, &[':']).filter(|&index| is_single_colon(tokens, index))
}
pub(crate) fn parse_angle_bracket_contents(
tokens: &[TokenTree], trait_name: Option<&Ident>, allow_special: bool,
) -> TyTypeParam {
let mut params = vec![];
let mut bindings = vec![];
for chunk in split_at_depth0(tokens, ',') {
if chunk.is_empty() {
continue;
}
if let Some(eq) = scan_stop(chunk, &['=']) {
if allow_special {
let name_ty =
TyPrimitive(chunk[..eq].iter().cloned().collect()).to_ty();
let value = match resolve_at_refs(&chunk[eq + 1..]) {
Ok(v) if v.is_empty() => TyPrimitive(compile_error_ty(
"batch-impl: binding `Item =` missing a value (write `Item = u32`)",
chunk[eq].span(),
))
.to_ty(),
Ok(v) => parse_item(&mut Cursor::new(&v), Op::Dash, trait_name)
.unwrap_or_else(empty),
Err(e) => TyPrimitive(e).to_ty(),
};
bindings.push((Box::new(name_ty), Box::new(value)));
} else {
params.push((
Box::new(
TyPrimitive(compile_error_ty(
"batch-impl: binding args (`Item = u32`) are only valid on a trait path (`Conv<Item = u32> X`) or in a generic declaration — a concrete type's args are a plain type list",
chunk[eq].span(),
))
.to_ty(),
),
None,
));
}
} else if let Some(colon) = find_colon_at_depth0(chunk) {
if allow_special {
params.push((
Box::new(
TyPrimitive(
chunk[..colon].iter().cloned().collect::<TokenStream>(),
)
.to_ty(),
),
Some(
if chunk[colon + 1..].is_empty() {
TyPrimitive(compile_error_ty(
"batch-impl: bound `T:` missing a bound (write `T: Clone`)",
chunk[colon].span(),
))
.to_ty()
} else {
parse_item(
&mut Cursor::new(&chunk[colon + 1..]),
Op::Dash,
trait_name,
)
.unwrap_or_else(empty)
},
),
));
} else {
params.push((
Box::new(
TyPrimitive(compile_error_ty(
"batch-impl: bound args (`T: Clone`) are only valid on a trait path or in a generic declaration (`<T: Clone> Foo`) — a concrete type's args are a plain type list",
chunk[colon].span(),
))
.to_ty(),
),
None,
));
}
} else {
let name = if matches!(
chunk.first(),
Some(TokenTree::Punct(p)) if p.as_char() == '@'
) && matches!(chunk.get(1), Some(TokenTree::Literal(_)))
&& chunk
.iter()
.any(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '.'))
{
let span = chunk[0].span();
TyPrimitive(compile_error_ty(
"batch-impl: `@N..M` range references are only valid as a where-predicate subject",
span,
))
.to_ty()
} else {
match resolve_at_refs(chunk) {
Ok(v) => parse_item(&mut Cursor::new(&v), Op::Dash, trait_name)
.unwrap_or_else(empty),
Err(e) => TyPrimitive(e).to_ty(),
}
};
params.push((Box::new(name), None));
}
}
TyTypeParam { params, bindings }
}
pub(crate) fn primitive(tokens: &[TokenTree]) -> Ty {
let span =
tokens.first().map(|t| t.span()).unwrap_or_else(proc_macro2::Span::call_site);
if let Some(e) = validate_stray_punct(tokens) {
return e;
}
if let Some(e) = validate_range(tokens) {
return e;
}
if let Some(e) = validate_start_punct(tokens) {
return e;
}
if let Some(e) = validate_adjacent(tokens) {
return e;
}
TyPrimitive(tokens.iter().cloned().collect()).to_ty().with_span(span)
}
fn validate_stray_punct(tokens: &[TokenTree]) -> Option<Ty> {
for (i, tt) in tokens.iter().enumerate() {
if let TokenTree::Punct(p) = tt {
let is_range_inclusive =
p.as_char() == '=' && i > 0 && is_punct(&tokens[i - 1], '.');
let msg = if is_range_inclusive {
None
} else {
match p.as_char() {
';' => Some(
"batch-impl: `;` is not valid in a type (it is the `batch_trait!` \
segment boundary; in `#[batch_impl]` specs are separated by `,`)",
),
'=' => Some(
"batch-impl: `=` is not valid in a type position (associated-type \
bindings like `Item = u32` belong inside a trait path's `<...>`)",
),
'@' => Some(
"batch-impl: `@` inside a type (position references like `@0` must \
start an operand, e.g. `T^@0`)",
),
'#' => Some(
"batch-impl: `#` inside a type (attributes belong at the spec start \
as `#[...]^T`; directives are expanded before parsing)",
),
_ => None,
}
};
if let Some(msg) = msg {
return Some(err_ty_at(msg, p.span()));
}
}
}
None
}
fn validate_range(tokens: &[TokenTree]) -> Option<Ty> {
if tokens.iter().any(|t| matches!(t, TokenTree::Punct(p) if p.as_char() == '.')) {
return Some(err_ty_at(
"batch-impl: a range (`..`/`..=`) in a type position needs integer endpoints (e.g. `0..=3`)",
tokens[0].span(),
));
}
None
}
fn validate_start_punct(tokens: &[TokenTree]) -> Option<Ty> {
if let Some(TokenTree::Punct(p)) = tokens.first()
&& (matches!(p.as_char(), '+' | '?')
|| (p.as_char() == '.' && p.spacing() == Spacing::Alone))
{
return Some(err_ty_at(
"batch-impl: `+`/`?`/`.` is not valid at the start of a type \
(`+`/`?` belong in bounds; a type cannot start with `.`)",
p.span(),
));
}
None
}
fn validate_adjacent(tokens: &[TokenTree]) -> Option<Ty> {
let mut prev: Option<&TokenTree> = None;
let mut prev_is_lifetime_name = false;
for tt in tokens {
let is_lifetime_name =
matches!(prev, Some(TokenTree::Punct(p)) if p.as_char() == '\'');
let prev_is_fragment = prev.is_some_and(|p| {
matches!(
p,
TokenTree::Ident(_) | TokenTree::Literal(_) | TokenTree::Group(_)
)
}) && !prev_is_lifetime_name;
let is_fragment = !is_lifetime_name
&& matches!(
tt,
TokenTree::Ident(_) | TokenTree::Literal(_) | TokenTree::Group(_)
);
let fn_head = matches!(tt, TokenTree::Ident(id) if id == "fn" || id == "unsafe" || id == "dyn")
|| matches!(prev, Some(TokenTree::Ident(id)) if id == "fn" || id == "unsafe" || id == "dyn");
let after_ident = matches!(prev, Some(TokenTree::Ident(_)));
let group_is_generic_or_call = matches!(
tt,
TokenTree::Group(g)
if g.delimiter() == proc_macro2::Delimiter::None
|| g.delimiter() == proc_macro2::Delimiter::Parenthesis
);
let generic_app = after_ident && group_is_generic_or_call;
if prev_is_fragment && is_fragment && !fn_head && !generic_app {
return Some(err_ty_at(
"batch-impl: adjacent types without an operator (missing `^` / `-` / `,`)",
tt.span(),
));
}
prev = Some(tt);
prev_is_lifetime_name = is_lifetime_name;
}
None
}
pub(crate) fn empty() -> Ty {
TyPrimitive(quote![]).to_ty()
}