parser: un-inline error message generation

This change cuts 40 to 150 instructions per parsing function that
generates a `ErrorContext`, and reduces the stack usage per function 500
or more bytes.

Also, it's less code you have to read and type per error message.

<details>

<summary>Benchmark results (benches/from_str.rs)</summary>

```text
librustdoc/all          time:   [177.24 µs 177.92 µs 178.56 µs]
                        thrpt:  [79.081 MiB/s 79.369 MiB/s 79.674 MiB/s]
                 change:
                        time:   [−2.3057% −2.0764% −1.8236%] (p = 0.00 < 0.05)
                        thrpt:  [+1.8575% +2.1204% +2.3601%]
                        Performance has improved.

librustdoc/item_info    time:   [3.1792 µs 3.1890 µs 3.1973 µs]
                        thrpt:  [49.216 MiB/s 49.344 MiB/s 49.495 MiB/s]
                 change:
                        time:   [−4.3188% −3.9734% −3.6065%] (p = 0.00 < 0.05)
                        thrpt:  [+3.7414% +4.1378% +4.5137%]
                        Performance has improved.

librustdoc/item_union   time:   [18.237 µs 18.297 µs 18.369 µs]
                        thrpt:  [53.734 MiB/s 53.945 MiB/s 54.123 MiB/s]
                 change:
                        time:   [−3.6142% −3.2857% −2.9748%] (p = 0.00 < 0.05)
                        thrpt:  [+3.0660% +3.3974% +3.7497%]
                        Performance has improved.

librustdoc/page         time:   [83.761 µs 84.102 µs 84.414 µs]
                        thrpt:  [73.355 MiB/s 73.627 MiB/s 73.927 MiB/s]
                 change:
                        time:   [−2.4929% −2.2196% −1.9439%] (p = 0.00 < 0.05)
                        thrpt:  [+1.9825% +2.2700% +2.5566%]
                        Performance has improved.

librustdoc/print_item   time:   [9.5466 µs 9.5528 µs 9.5603 µs]
                        thrpt:  [98.756 MiB/s 98.834 MiB/s 98.898 MiB/s]
                 change:
                        time:   [−1.4294% −1.2085% −0.9713%] (p = 0.00 < 0.05)
                        thrpt:  [+0.9808% +1.2233% +1.4501%]
                        Change within noise threshold.

librustdoc/short_item_info
                        time:   [8.8031 µs 8.8127 µs 8.8204 µs]
                        thrpt:  [102.72 MiB/s 102.80 MiB/s 102.92 MiB/s]
                 change:
                        time:   [−4.3207% −3.9712% −3.6035%] (p = 0.00 < 0.05)
                        thrpt:  [+3.7382% +4.1355% +4.5158%]
                        Performance has improved.

librustdoc/sidebar      time:   [20.181 µs 20.218 µs 20.253 µs]
                        thrpt:  [60.933 MiB/s 61.036 MiB/s 61.150 MiB/s]
                 change:
                        time:   [−2.5827% −2.3381% −2.0839%] (p = 0.00 < 0.05)
                        thrpt:  [+2.1283% +2.3940% +2.6512%]
                        Performance has improved.

librustdoc/source       time:   [7.2338 µs 7.2377 µs 7.2423 µs]
                        thrpt:  [101.79 MiB/s 101.85 MiB/s 101.91 MiB/s]
                 change:
                        time:   [−3.1453% −2.8930% −2.6317%] (p = 0.00 < 0.05)
                        thrpt:  [+2.7028% +2.9792% +3.2475%]
                        Performance has improved.

librustdoc/type_layout_size
                        time:   [4.3755 µs 4.3774 µs 4.3792 µs]
                        thrpt:  [61.847 MiB/s 61.874 MiB/s 61.900 MiB/s]
                 change:
                        time:   [−6.6822% −6.5162% −6.3593%] (p = 0.00 < 0.05)
                        thrpt:  [+6.7912% +6.9704% +7.1607%]
                        Performance has improved.

librustdoc/type_layout  time:   [14.297 µs 14.316 µs 14.331 µs]
                        thrpt:  [187.86 MiB/s 188.06 MiB/s 188.30 MiB/s]
                 change:
                        time:   [−3.2479% −2.8916% −2.6244%] (p = 0.00 < 0.05)
                        thrpt:  [+2.6952% +2.9777% +3.3569%]
                        Performance has improved.
```

</details>
This commit is contained in:
René Kijewski 2025-06-24 19:34:23 +02:00
parent 758a75bc54
commit 6c32499533
4 changed files with 160 additions and 218 deletions

View File

@ -11,8 +11,8 @@ use winnow::token::take_until;
use crate::node::CondTest; use crate::node::CondTest;
use crate::{ use crate::{
CharLit, ErrorContext, Level, Num, ParseErr, ParseResult, PathOrIdentifier, Span, StrLit, CharLit, Level, Num, ParseResult, PathOrIdentifier, Span, StrLit, StrPrefix, WithSpan,
StrPrefix, WithSpan, char_lit, filter, identifier, keyword, not_suffix_with_hash, num_lit, char_lit, cut_error, filter, identifier, keyword, not_suffix_with_hash, num_lit,
path_or_identifier, skip_ws0, skip_ws1, str_lit, ws, path_or_identifier, skip_ws0, skip_ws1, str_lit, ws,
}; };
@ -50,22 +50,22 @@ struct Allowed {
super_keyword: bool, super_keyword: bool,
} }
fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(), ParseErr<'a>> { fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> ParseResult<'a, ()> {
match &expr.inner { match &expr.inner {
&Expr::Var(name) => { &Expr::Var(name) => {
// List can be found in rust compiler "can_be_raw" function (although in our case, it's // List can be found in rust compiler "can_be_raw" function (although in our case, it's
// also used in cases like `match`, so `self` is allowed in this case). // also used in cases like `match`, so `self` is allowed in this case).
if (!allowed.super_keyword && name == "super") || matches!(name, "crate" | "Self") { if (!allowed.super_keyword && name == "super") || matches!(name, "crate" | "Self") {
Err(err_reserved_identifier(name)) err_reserved_identifier(name)
} else if !allowed.underscore && name == "_" { } else if !allowed.underscore && name == "_" {
Err(err_underscore_identifier(name)) err_underscore_identifier(name)
} else { } else {
Ok(()) Ok(())
} }
} }
&Expr::IsDefined(var) | &Expr::IsNotDefined(var) => { &Expr::IsDefined(var) | &Expr::IsNotDefined(var) => {
if var == "_" { if var == "_" {
Err(err_underscore_identifier(var)) err_underscore_identifier(var)
} else { } else {
Ok(()) Ok(())
} }
@ -73,7 +73,7 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
Expr::Path(path) => { Expr::Path(path) => {
if let &[name] = path.as_slice() { if let &[name] = path.as_slice() {
if !crate::can_be_variable_name(name) { if !crate::can_be_variable_name(name) {
return Err(err_reserved_identifier(name)); return err_reserved_identifier(name);
} }
} }
Ok(()) Ok(())
@ -86,9 +86,9 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
} }
Expr::AssociatedItem(elem, associated_item) => { Expr::AssociatedItem(elem, associated_item) => {
if associated_item.name == "_" { if associated_item.name == "_" {
Err(err_underscore_identifier(associated_item.name)) err_underscore_identifier(associated_item.name)
} else if !crate::can_be_variable_name(associated_item.name) { } else if !crate::can_be_variable_name(associated_item.name) {
Err(err_reserved_identifier(associated_item.name)) err_reserved_identifier(associated_item.name)
} else { } else {
check_expr(elem, Allowed::default()) check_expr(elem, Allowed::default())
} }
@ -126,7 +126,7 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
} }
Ok(()) Ok(())
} }
Expr::ArgumentPlaceholder => Err(ErrMode::Cut(ErrorContext::new("unreachable", expr.span))), Expr::ArgumentPlaceholder => cut_error!("unreachable", expr.span),
Expr::BoolLit(_) Expr::BoolLit(_)
| Expr::NumLit(_, _) | Expr::NumLit(_, _)
| Expr::StrLit(_) | Expr::StrLit(_)
@ -140,18 +140,14 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
} }
} }
fn err_underscore_identifier(name: &str) -> ErrMode<ErrorContext<'_>> { #[inline(always)]
ErrMode::Cut(ErrorContext::new( fn err_underscore_identifier<T>(name: &str) -> ParseResult<'_, T> {
"reserved keyword `_` cannot be used here", cut_error!("reserved keyword `_` cannot be used here", name)
name,
))
} }
fn err_reserved_identifier(name: &str) -> ErrMode<ErrorContext<'_>> { #[inline(always)]
ErrMode::Cut(ErrorContext::new( fn err_reserved_identifier<T>(name: &str) -> ParseResult<'_, T> {
format!("`{name}` cannot be used as an identifier"), cut_error!(format!("`{name}` cannot be used as an identifier"), name)
name,
))
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -244,10 +240,7 @@ impl<'a> Expr<'a> {
)) ))
.parse_next(i)?; .parse_next(i)?;
if has_named_arguments && !matches!(*expr, Self::NamedArgument(_, _)) { if has_named_arguments && !matches!(*expr, Self::NamedArgument(_, _)) {
Err(ErrMode::Cut(ErrorContext::new( cut_error!("named arguments must always be passed last", start)
"named arguments must always be passed last",
start,
)))
} else { } else {
Ok(expr) Ok(expr)
} }
@ -283,10 +276,10 @@ impl<'a> Expr<'a> {
start, start,
)) ))
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!("named argument `{argument}` was passed more than once"), format!("named argument `{argument}` was passed more than once"),
start, start
))) )
} }
} }
@ -357,13 +350,13 @@ impl<'a> Expr<'a> {
let expr = WithSpan::new(Expr::BinOp(Box::new(BinOp { op, lhs: expr, rhs })), start); let expr = WithSpan::new(Expr::BinOp(Box::new(BinOp { op, lhs: expr, rhs })), start);
if let Some((op2, _)) = opt(right).parse_next(i)? { if let Some((op2, _)) = opt(right).parse_next(i)? {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"comparison operators cannot be chained; \ "comparison operators cannot be chained; \
consider using explicit parentheses, e.g. `(_ {op} _) {op2} _`" consider using explicit parentheses, e.g. `(_ {op} _) {op2} _`"
), ),
op, op,
))); );
} }
Ok(expr) Ok(expr)
@ -386,10 +379,10 @@ impl<'a> Expr<'a> {
let data = opt((ws1, '~', ws1, |i: &mut _| Expr::muldivmod(i, level))).parse_next(i)?; let data = opt((ws1, '~', ws1, |i: &mut _| Expr::muldivmod(i, level))).parse_next(i)?;
if let Some((t1, _, t2, expr)) = data { if let Some((t1, _, t2, expr)) = data {
if t1.is_none() || t2.is_none() { if t1.is_none() || t2.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"the concat operator `~` must be surrounded by spaces", "the concat operator `~` must be surrounded by spaces",
start, start,
))); );
} }
Ok(Some(expr)) Ok(Some(expr))
} else { } else {
@ -426,18 +419,18 @@ impl<'a> Expr<'a> {
if crate::PRIMITIVE_TYPES.contains(&target) { if crate::PRIMITIVE_TYPES.contains(&target) {
return Ok(WithSpan::new(Self::As(Box::new(lhs), target), start)); return Ok(WithSpan::new(Self::As(Box::new(lhs), target), start));
} else if target.is_empty() { } else if target.is_empty() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"`as` operator expects the name of a primitive type on its right-hand side", "`as` operator expects the name of a primitive type on its right-hand side",
before_keyword.trim_start(), before_keyword.trim_start(),
))); );
} else { } else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"`as` operator expects the name of a primitive type on its right-hand \ "`as` operator expects the name of a primitive type on its right-hand \
side, found `{target}`" side, found `{target}`"
), ),
before_keyword.trim_start(), before_keyword.trim_start(),
))); );
} }
} }
_ => { _ => {
@ -449,11 +442,11 @@ impl<'a> Expr<'a> {
let rhs = opt(terminated(opt(keyword("not")), ws(keyword("defined")))).parse_next(i)?; let rhs = opt(terminated(opt(keyword("not")), ws(keyword("defined")))).parse_next(i)?;
let ctor = match rhs { let ctor = match rhs {
None => { None => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"expected `defined` or `not defined` after `is`", "expected `defined` or `not defined` after `is`",
// We use `start` to show the whole `var is` thing instead of the current token. // We use `start` to show the whole `var is` thing instead of the current token.
start, start,
))); );
} }
Some(None) => Self::IsDefined, Some(None) => Self::IsDefined,
Some(Some(_)) => Self::IsNotDefined, Some(Some(_)) => Self::IsNotDefined,
@ -461,16 +454,13 @@ impl<'a> Expr<'a> {
let var_name = match *lhs { let var_name = match *lhs {
Self::Var(var_name) => var_name, Self::Var(var_name) => var_name,
Self::AssociatedItem(_, _) => { Self::AssociatedItem(_, _) => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"`is defined` operator can only be used on variables, not on their fields", "`is defined` operator can only be used on variables, not on their fields",
start, start,
))); );
} }
_ => { _ => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("`is defined` operator can only be used on variables", start);
"`is defined` operator can only be used on variables",
start,
)));
} }
}; };
Ok(WithSpan::new(ctor(var_name), start)) Ok(WithSpan::new(ctor(var_name), start))
@ -642,10 +632,7 @@ fn token_xor<'a>(i: &mut &'a str) -> ParseResult<'a> {
if good { if good {
Ok("^") Ok("^")
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!("the binary XOR operator is called `xor` in askama", *i)
"the binary XOR operator is called `xor` in askama",
*i,
)))
} }
} }
@ -654,10 +641,7 @@ fn token_bitand<'a>(i: &mut &'a str) -> ParseResult<'a> {
if good { if good {
Ok("&") Ok("&")
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!("the binary AND operator is called `bitand` in askama", *i)
"the binary AND operator is called `bitand` in askama",
*i,
)))
} }
} }
@ -784,10 +768,7 @@ impl<'a> Suffix<'a> {
let before = *i; let before = *i;
let (token, token_span) = ws(opt(token).with_taken()).parse_next(i)?; let (token, token_span) = ws(opt(token).with_taken()).parse_next(i)?;
let Some(token) = token else { let Some(token) = token else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("expected valid tokens in macro call", token_span);
"expected valid tokens in macro call",
token_span,
)));
}; };
let close_token = match token { let close_token = match token {
Token::SomeOther => continue, Token::SomeOther => continue,
@ -800,14 +781,14 @@ impl<'a> Suffix<'a> {
let open_token = open_list.pop().unwrap(); let open_token = open_list.pop().unwrap();
if open_token != close_token { if open_token != close_token {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"expected `{}` but found `{}`", "expected `{}` but found `{}`",
open_token.as_close_char(), open_token.as_close_char(),
close_token.as_close_char(), close_token.as_close_char(),
), ),
token_span, token_span,
))); );
} else if open_list.is_empty() { } else if open_list.is_empty() {
return Ok(Suffix::MacroCall(&start[..start.len() - before.len()])); return Ok(Suffix::MacroCall(&start[..start.len() - before.len()]));
} }
@ -844,13 +825,13 @@ impl<'a> Suffix<'a> {
)) ))
.parse_next(i)?; .parse_next(i)?;
if opt(take_until(.., '\n')).parse_next(i)?.is_none() { if opt(take_until(.., '\n')).parse_next(i)?.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"you are probably missing a line break to end {}comment", "you are probably missing a line break to end {}comment",
if is_doc_comment { "doc " } else { "" } if is_doc_comment { "doc " } else { "" }
), ),
start, start,
))); );
} }
Ok(()) Ok(())
} }
@ -864,13 +845,13 @@ impl<'a> Suffix<'a> {
)) ))
.parse_next(i)?; .parse_next(i)?;
if opt(take_until(.., "*/")).parse_next(i)?.is_none() { if opt(take_until(.., "*/")).parse_next(i)?.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"missing `*/` to close block {}comment", "missing `*/` to close block {}comment",
if is_doc_comment { "doc " } else { "" } if is_doc_comment { "doc " } else { "" }
), ),
start, start,
))); );
} }
Ok(()) Ok(())
} }
@ -881,10 +862,10 @@ impl<'a> Suffix<'a> {
let prefix = identifier.parse_next(i)?; let prefix = identifier.parse_next(i)?;
let hashes: usize = repeat(.., '#').parse_next(i)?; let hashes: usize = repeat(.., '#').parse_next(i)?;
if hashes >= 256 { if hashes >= 256 {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"a maximum of 255 hashes `#` are allowed with raw strings", "a maximum of 255 hashes `#` are allowed with raw strings",
prefix, prefix,
))); );
} }
let str_kind = match prefix { let str_kind = match prefix {
@ -897,10 +878,10 @@ impl<'a> Suffix<'a> {
_ if hashes == 0 => return Ok(()), _ if hashes == 0 => return Ok(()),
// reserved prefix: reject // reserved prefix: reject
_ => { _ => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("reserved prefix `{}#`", prefix.escape_debug()), format!("reserved prefix `{}#`", prefix.escape_debug()),
prefix, prefix,
))); );
} }
}; };
@ -908,10 +889,7 @@ impl<'a> Suffix<'a> {
// got a raw string // got a raw string
let Some((inner, j)) = i.split_once(&format!("\"{:#<hashes$}", "")) else { let Some((inner, j)) = i.split_once(&format!("\"{:#<hashes$}", "")) else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("unterminated raw string", prefix);
"unterminated raw string",
prefix,
)));
}; };
*i = j; *i = j;
@ -927,7 +905,7 @@ impl<'a> Suffix<'a> {
None => None, None => None,
}; };
if let Some(msg) = msg { if let Some(msg) = msg {
return Err(ErrMode::Cut(ErrorContext::new(msg, prefix))); return cut_error!(msg, prefix);
} }
not_suffix_with_hash(i)?; not_suffix_with_hash(i)?;
@ -940,31 +918,31 @@ impl<'a> Suffix<'a> {
if str_kind.is_some() { if str_kind.is_some() {
// an invalid raw identifier like `cr#async` // an invalid raw identifier like `cr#async`
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!( format!(
"reserved prefix `{}#`, only `r#` is allowed with raw identifiers", "reserved prefix `{}#`, only `r#` is allowed with raw identifiers",
prefix.escape_debug(), prefix.escape_debug(),
), ),
prefix, prefix,
))) )
} else if hashes > 1 { } else if hashes > 1 {
// an invalid raw identifier like `r##async` // an invalid raw identifier like `r##async`
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
"only one `#` is allowed in raw identifier delimitation", "only one `#` is allowed in raw identifier delimitation",
prefix, prefix,
))) )
} else { } else {
// a raw identifier like `r#async` // a raw identifier like `r#async`
Ok(()) Ok(())
} }
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!( format!(
"prefix `{}#` is only allowed with raw identifiers and raw strings", "prefix `{}#` is only allowed with raw identifiers and raw strings",
prefix.escape_debug(), prefix.escape_debug(),
), ),
prefix, prefix,
))) )
} }
} }
@ -972,10 +950,10 @@ impl<'a> Suffix<'a> {
let start = *i; let start = *i;
'#'.parse_next(i)?; '#'.parse_next(i)?;
if opt('"').parse_next(i)?.is_some() { if opt('"').parse_next(i)?.is_some() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"unprefixed guarded string literals are reserved for future use", "unprefixed guarded string literals are reserved for future use",
start, start,
))); );
} }
Ok(Token::SomeOther) Ok(Token::SomeOther)
} }
@ -1044,10 +1022,7 @@ impl<'a> Suffix<'a> {
|i: &mut _| { |i: &mut _| {
let name = alt((digit1, identifier)).parse_next(i)?; let name = alt((digit1, identifier)).parse_next(i)?;
if !crate::can_be_variable_name(name) { if !crate::can_be_variable_name(name) {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(format!("`{name}` cannot be used as an identifier"), name)
format!("`{name}` cannot be used as an identifier"),
name,
)))
} else { } else {
Ok(name) Ok(name)
} }
@ -1094,10 +1069,9 @@ impl<'a> Suffix<'a> {
fn ensure_macro_name(name: &str) -> ParseResult<'_, ()> { fn ensure_macro_name(name: &str) -> ParseResult<'_, ()> {
match name { match name {
"crate" | "super" | "Self" | "self" => Err(ErrMode::Cut(ErrorContext::new( "crate" | "super" | "Self" | "self" => {
format!("`{name}` is not a valid macro name"), cut_error!(format!("`{name}` is not a valid macro name"), name)
name, }
))),
_ => Ok(()), _ => Ok(()),
} }
} }
@ -1122,17 +1096,17 @@ impl<'i> TyGenerics<'i> {
if let &[name] = path.as_slice() { if let &[name] = path.as_slice() {
if matches!(name, "super" | "self" | "crate") { if matches!(name, "super" | "self" | "crate") {
// `Self` and `_` are allowed // `Self` and `_` are allowed
return Err(err_reserved_identifier(name)); return err_reserved_identifier(name);
} }
} else { } else {
for (idx, &name) in path.iter().enumerate() { for (idx, &name) in path.iter().enumerate() {
if name == "_" { if name == "_" {
// `_` is never allowed // `_` is never allowed
return Err(err_underscore_identifier(name)); return err_underscore_identifier(name);
} else if idx > 0 && matches!(name, "super" | "self" | "Self" | "crate") { } else if idx > 0 && matches!(name, "super" | "self" | "Self" | "crate") {
// At the front of the path, "super" | "self" | "Self" | "crate" are allowed. // At the front of the path, "super" | "self" | "Self" | "crate" are allowed.
// Inside the path, they are not allowed. // Inside the path, they are not allowed.
return Err(err_reserved_identifier(name)); return err_reserved_identifier(name);
} }
} }
} }

View File

@ -405,10 +405,7 @@ fn num_lit<'a>(i: &mut &'a str) -> ParseResult<'a, Num<'a>> {
{ {
Ok(value) Ok(value)
} else { } else {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(format!("unknown {kind} suffix `{suffix}`"), start)
format!("unknown {kind} suffix `{suffix}`"),
start,
)))
} }
} }
@ -421,10 +418,7 @@ fn num_lit<'a>(i: &mut &'a str) -> ParseResult<'a, Num<'a>> {
.parse_next(i)?; .parse_next(i)?;
match opt(separated_digits(base, false)).parse_next(i)? { match opt(separated_digits(base, false)).parse_next(i)? {
Some(_) => Ok(()), Some(_) => Ok(()),
None => Err(ErrMode::Cut(ErrorContext::new( None => cut_error!(format!("expected digits after `{kind}`"), start),
format!("expected digits after `{kind}`"),
start,
))),
} }
}); });
@ -436,10 +430,12 @@ fn num_lit<'a>(i: &mut &'a str) -> ParseResult<'a, Num<'a>> {
let (kind, op) = (one_of(['e', 'E']), opt(one_of(['+', '-']))).parse_next(i)?; let (kind, op) = (one_of(['e', 'E']), opt(one_of(['+', '-']))).parse_next(i)?;
match opt(separated_digits(10, op.is_none())).parse_next(i)? { match opt(separated_digits(10, op.is_none())).parse_next(i)? {
Some(_) => Ok(()), Some(_) => Ok(()),
None => Err(ErrMode::Cut(ErrorContext::new( None => {
format!("expected decimal digits, `+` or `-` after exponent `{kind}`"), cut_error!(
start, format!("expected decimal digits, `+` or `-` after exponent `{kind}`"),
))), start,
)
}
} }
}) })
.parse_next(i)?; .parse_next(i)?;
@ -578,12 +574,12 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
if has_lf { if has_lf {
continue; continue;
} else { } else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"a bare CR (Mac linebreak) is not allowed in string literals, \ "a bare CR (Mac linebreak) is not allowed in string literals, \
use NL (Unix linebreak) or CRNL (Windows linebreak) instead, \ use NL (Unix linebreak) or CRNL (Windows linebreak) instead, \
or type `\\r` explicitly", or type `\\r` explicitly",
start, start,
))); );
} }
} }
Sequence::Close => break, Sequence::Close => break,
@ -611,16 +607,10 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
match u32::from_str_radix(code, 16).unwrap() { match u32::from_str_radix(code, 16).unwrap() {
0 => contains_null = true, 0 => contains_null = true,
0xd800..0xe000 => { 0xd800..0xe000 => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("unicode escape must not be a surrogate", start);
"unicode escape must not be a surrogate",
start,
)));
} }
0x110000.. => { 0x110000.. => {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("unicode escape must be at most 10FFFF", start);
"unicode escape must be at most 10FFFF",
start,
)));
} }
128.. => contains_unicode_character = true, 128.. => contains_unicode_character = true,
_ => {} _ => {}
@ -653,10 +643,7 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
let lit = opt(terminated(inner.with_taken(), '"')).parse_next(i)?; let lit = opt(terminated(inner.with_taken(), '"')).parse_next(i)?;
let Some((mut lit, content)) = lit else { let Some((mut lit, content)) = lit else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("unclosed or broken string", start);
"unclosed or broken string",
start,
)));
}; };
lit.content = content; lit.content = content;
lit.prefix = prefix; lit.prefix = prefix;
@ -677,7 +664,7 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
None => lit.contains_high_ascii.then_some("out of range hex escape"), None => lit.contains_high_ascii.then_some("out of range hex escape"),
}; };
if let Some(msg) = msg { if let Some(msg) = msg {
return Err(ErrMode::Cut(ErrorContext::new(msg, start))); return cut_error!(msg, start);
} }
not_suffix_with_hash(i)?; not_suffix_with_hash(i)?;
@ -686,10 +673,10 @@ fn str_lit<'a>(i: &mut &'a str) -> ParseResult<'a, StrLit<'a>> {
fn not_suffix_with_hash<'a>(i: &mut &'a str) -> ParseResult<'a, ()> { fn not_suffix_with_hash<'a>(i: &mut &'a str) -> ParseResult<'a, ()> {
if let Some(suffix) = opt((identifier, '#').take()).parse_next(i)? { if let Some(suffix) = opt((identifier, '#').take()).parse_next(i)? {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"you are missing a space to separate two string literals", "you are missing a space to separate two string literals",
suffix, suffix,
))); );
} }
Ok(()) Ok(())
} }
@ -704,10 +691,10 @@ fn str_lit_without_prefix<'a>(i: &mut &'a str) -> ParseResult<'a> {
None => None, None => None,
}; };
if let Some(kind) = kind { if let Some(kind) = kind {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("expected an unprefixed normal string, not a {kind}"), format!("expected an unprefixed normal string, not a {kind}"),
start, start,
))); );
} }
Ok(lit.content) Ok(lit.content)
@ -740,15 +727,12 @@ fn char_lit<'a>(i: &mut &'a str) -> ParseResult<'a, CharLit<'a>> {
let Some(s) = s else { let Some(s) = s else {
i.reset(&start); i.reset(&start);
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("empty character literal", *i);
"empty character literal",
*i,
)));
}; };
let mut is = s; let mut is = s;
let Ok(c) = Char::parse(&mut is) else { let Ok(c) = Char::parse(&mut is) else {
i.reset(&start); i.reset(&start);
return Err(ErrMode::Cut(ErrorContext::new("invalid character", *i))); return cut_error!("invalid character", *i);
}; };
let (nb, max_value, err1, err2) = match c { let (nb, max_value, err1, err2) = match c {
@ -776,11 +760,11 @@ fn char_lit<'a>(i: &mut &'a str) -> ParseResult<'a, CharLit<'a>> {
let Ok(nb) = u32::from_str_radix(nb, 16) else { let Ok(nb) = u32::from_str_radix(nb, 16) else {
i.reset(&start); i.reset(&start);
return Err(ErrMode::Cut(ErrorContext::new(err1, *i))); return cut_error!(err1, *i);
}; };
if nb > max_value { if nb > max_value {
i.reset(&start); i.reset(&start);
return Err(ErrMode::Cut(ErrorContext::new(err2, *i))); return cut_error!(err2, *i);
} }
Ok(CharLit { Ok(CharLit {
@ -1117,17 +1101,16 @@ impl Level<'_> {
count: 1, count: 1,
}) })
} else { } else {
Err(Self::_fail(i)) Self::_fail(i)
} }
} }
#[cold] #[inline(always)]
#[inline(never)] fn _fail<T>(i: &str) -> ParseResult<'_, T> {
fn _fail(i: &str) -> ParseErr<'_> { cut_error!(
ErrMode::Cut(ErrorContext::new(
"your template code is too deeply nested, or the last expression is too complex", "your template code is too deeply nested, or the last expression is too complex",
i, i,
)) )
} }
} }
@ -1153,7 +1136,7 @@ impl LevelGuard<'_> {
self.count += 1; self.count += 1;
Ok(()) Ok(())
} else { } else {
Err(Level::_fail(i)) Level::_fail(i)
} }
} }
} }
@ -1437,6 +1420,30 @@ fn is_rust_keyword(ident: &str) -> bool {
false false
} }
macro_rules! cut_error {
($message:expr, $span:expr $(,)?) => {{
use ::std::convert::Into;
use ::std::option::Option::Some;
$crate::cut_context_err(
#[cold]
#[inline(always)]
move || $crate::ErrorContext {
span: Into::into($span),
message: Some(Into::into($message)),
},
)
}};
}
pub(crate) use cut_error;
#[cold]
#[inline(never)]
fn cut_context_err<'a, T>(gen_err: impl FnOnce() -> ErrorContext<'a>) -> ParseResult<'a, T> {
Err(ErrMode::Cut(gen_err()))
}
#[cfg(not(windows))] #[cfg(not(windows))]
#[cfg(test)] #[cfg(test)]
mod test { mod test {

View File

@ -11,8 +11,8 @@ use winnow::token::{any, rest, take_until};
use winnow::{ModalParser, Parser}; use winnow::{ModalParser, Parser};
use crate::{ use crate::{
ErrorContext, Expr, Filter, ParseResult, Span, State, Target, WithSpan, filter, identifier, ErrorContext, Expr, Filter, ParseResult, Span, State, Target, WithSpan, cut_error, filter,
is_rust_keyword, keyword, skip_ws0, str_lit_without_prefix, ws, identifier, is_rust_keyword, keyword, skip_ws0, str_lit_without_prefix, ws,
}; };
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -56,12 +56,12 @@ impl<'a> Node<'a> {
opt(|i: &mut _| unexpected_tag(i, s)).parse_next(i)?; opt(|i: &mut _| unexpected_tag(i, s)).parse_next(i)?;
let is_eof = opt(eof).parse_next(i)?; let is_eof = opt(eof).parse_next(i)?;
if is_eof.is_none() { if is_eof.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"cannot parse entire template\n\ "cannot parse entire template\n\
you should never encounter this error\n\ you should never encounter this error\n\
please report this error to <https://github.com/askama-rs/askama/issues>", please report this error to <https://github.com/askama-rs/askama/issues>",
*i, *i,
))); );
} }
Ok(result) Ok(result)
} }
@ -132,10 +132,7 @@ impl<'a> Node<'a> {
let start = *i; let start = *i;
let (pws, _, nws) = p.parse_next(i)?; let (pws, _, nws) = p.parse_next(i)?;
if !s.is_in_loop() { if !s.is_in_loop() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("you can only `break` inside a `for` loop", start);
"you can only `break` inside a `for` loop",
start,
)));
} }
Ok(Self::Break(WithSpan::new(Ws(pws, nws), start))) Ok(Self::Break(WithSpan::new(Ws(pws, nws), start)))
} }
@ -150,10 +147,7 @@ impl<'a> Node<'a> {
let start = *i; let start = *i;
let (pws, _, nws) = p.parse_next(i)?; let (pws, _, nws) = p.parse_next(i)?;
if !s.is_in_loop() { if !s.is_in_loop() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("you can only `continue` inside a `for` loop", start);
"you can only `continue` inside a `for` loop",
start,
)));
} }
Ok(Self::Continue(WithSpan::new(Ws(pws, nws), start))) Ok(Self::Continue(WithSpan::new(Ws(pws, nws), start)))
} }
@ -256,7 +250,7 @@ fn unexpected_raw_tag<'a>(kind: Option<&'static str>, i: &mut &'a str) -> ParseR
tag if tag.starts_with("end") => format!("unexpected closing tag `{tag}`"), tag if tag.starts_with("end") => format!("unexpected closing tag `{tag}`"),
tag => format!("unknown node `{tag}`"), tag => format!("unknown node `{tag}`"),
}; };
Err(ErrMode::Cut(ErrorContext::new(msg, *i))) cut_error!(msg, *i)
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -488,10 +482,10 @@ fn check_block_start<'a>(
expected: &str, expected: &str,
) -> ParseResult<'a, ()> { ) -> ParseResult<'a, ()> {
if i.is_empty() { if i.is_empty() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("expected `{expected}` to terminate `{node}` node, found nothing"), format!("expected `{expected}` to terminate `{node}` node, found nothing"),
start, start,
))); );
} }
(|i: &mut _| s.tag_block_start(i)).parse_next(i) (|i: &mut _| s.tag_block_start(i)).parse_next(i)
} }
@ -622,10 +616,7 @@ fn check_duplicated_name<'a>(
i: &'a str, i: &'a str,
) -> Result<(), crate::ParseErr<'a>> { ) -> Result<(), crate::ParseErr<'a>> {
if !names.insert(arg_name) { if !names.insert(arg_name) {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(format!("duplicated argument `{arg_name}`"), i);
format!("duplicated argument `{arg_name}`"),
i,
)));
} }
Ok(()) Ok(())
} }
@ -658,10 +649,7 @@ impl<'a> Macro<'a> {
.parse_next(i)?; .parse_next(i)?;
match args { match args {
Some((args, Some(_))) => Ok(args), Some((args, Some(_))) => Ok(args),
Some((_, None)) => Err(ErrMode::Cut(ErrorContext::new( Some((_, None)) => cut_error!("expected `)` to close macro argument list", *i),
"expected `)` to close macro argument list",
*i,
))),
None => Ok(None), None => Ok(None),
} }
}; };
@ -682,10 +670,7 @@ impl<'a> Macro<'a> {
); );
let (pws1, _, (name, params, nws1, _)) = start.parse_next(i)?; let (pws1, _, (name, params, nws1, _)) = start.parse_next(i)?;
if is_rust_keyword(name) { if is_rust_keyword(name) {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(format!("'{name}' is not a valid name for a macro"), start_s);
format!("'{name}' is not a valid name for a macro"),
start_s,
)));
} }
if let Some(ref params) = params { if let Some(ref params) = params {
@ -698,13 +683,13 @@ impl<'a> Macro<'a> {
for (new_arg_name, default_value) in iter.by_ref() { for (new_arg_name, default_value) in iter.by_ref() {
check_duplicated_name(&mut names, new_arg_name, start_s)?; check_duplicated_name(&mut names, new_arg_name, start_s)?;
if default_value.is_none() { if default_value.is_none() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"all arguments following `{arg_name}` should have a default \ "all arguments following `{arg_name}` should have a default \
value, `{new_arg_name}` doesn't have a default value" value, `{new_arg_name}` doesn't have a default value"
), ),
start_s, start_s,
))); );
} }
} }
} }
@ -877,10 +862,7 @@ impl<'a> Call<'a> {
match args { match args {
Some((args, Some(_))) => Ok(args), Some((args, Some(_))) => Ok(args),
Some((_, None)) => Err(ErrMode::Cut(ErrorContext::new( Some((_, None)) => cut_error!("expected `)` to close call argument list", *i),
"expected `)` to close call argument list",
*i,
))),
None => Ok(None), None => Ok(None),
} }
}; };
@ -989,10 +971,10 @@ impl<'a> Match<'a> {
arms.push(arm); arms.push(arm);
} }
if arms.is_empty() { if arms.is_empty() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"`match` nodes must contain at least one `when` node and/or an `else` case", "`match` nodes must contain at least one `when` node and/or an `else` case",
start, start,
))); );
} }
Ok(WithSpan::new( Ok(WithSpan::new(
@ -1079,13 +1061,13 @@ fn check_end_name<'a>(
return Ok(end_name); return Ok(end_name);
} }
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
match name.is_empty() && !end_name.is_empty() { match name.is_empty() && !end_name.is_empty() {
true => format!("unexpected name `{end_name}` in `end{kind}` tag for unnamed `{kind}`"), true => format!("unexpected name `{end_name}` in `end{kind}` tag for unnamed `{kind}`"),
false => format!("expected name `{name}` in `end{kind}` tag, found `{end_name}`"), false => format!("expected name `{name}` in `end{kind}` tag, found `{end_name}`"),
}, },
before, before,
))) )
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@ -1231,20 +1213,20 @@ impl<'a> Let<'a> {
} }
}; };
if let Some(kind) = kind { if let Some(kind) = kind {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"when you forward-define a variable, you cannot use {kind} in place of \ "when you forward-define a variable, you cannot use {kind} in place of \
a variable name" a variable name"
), ),
start, start,
))); );
} }
} }
if is_mut.is_some() && !matches!(var, Target::Name(_)) { if is_mut.is_some() && !matches!(var, Target::Name(_)) {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
"you can only use the `mut` keyword with a variable name", "you can only use the `mut` keyword with a variable name",
start, start,
))); );
} }
Ok(WithSpan::new( Ok(WithSpan::new(
@ -1404,7 +1386,7 @@ impl<'a> Comment<'a> {
let mut ws = Ws(None, None); let mut ws = Ws(None, None);
if content.len() == 1 && matches!(content, "-" | "+" | "~") { if content.len() == 1 && matches!(content, "-" | "+" | "~") {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"ambiguous whitespace stripping\n\ "ambiguous whitespace stripping\n\
use `{}{content} {content}{}` to apply the same whitespace stripping on both \ use `{}{content} {content}{}` to apply the same whitespace stripping on both \
@ -1412,7 +1394,7 @@ impl<'a> Comment<'a> {
s.syntax.comment_start, s.syntax.comment_end, s.syntax.comment_start, s.syntax.comment_end,
), ),
start, start,
))); );
} else if content.len() >= 2 { } else if content.len() >= 2 {
ws.0 = Whitespace::parse_char(content.chars().next().unwrap_or_default()); ws.0 = Whitespace::parse_char(content.chars().next().unwrap_or_default());
ws.1 = Whitespace::parse_char(content.chars().next_back().unwrap_or_default()); ws.1 = Whitespace::parse_char(content.chars().next_back().unwrap_or_default());
@ -1439,10 +1421,10 @@ fn end_node<'a, 'g: 'a>(
Ok(actual) Ok(actual)
} else if actual.starts_with("end") { } else if actual.starts_with("end") {
i.reset(&start); i.reset(&start);
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!("expected `{expected}` to terminate `{node}` node, found `{actual}`"), format!("expected `{expected}` to terminate `{node}` node, found `{actual}`"),
*i, *i,
))) )
} else { } else {
i.reset(&start); i.reset(&start);
fail.parse_next(i) fail.parse_next(i)

View File

@ -5,8 +5,8 @@ use winnow::{ModalParser, Parser};
use crate::{ use crate::{
CharLit, ErrorContext, Num, ParseErr, ParseResult, PathOrIdentifier, State, StrLit, WithSpan, CharLit, ErrorContext, Num, ParseErr, ParseResult, PathOrIdentifier, State, StrLit, WithSpan,
bool_lit, can_be_variable_name, char_lit, identifier, is_rust_keyword, keyword, num_lit, bool_lit, can_be_variable_name, char_lit, cut_error, identifier, is_rust_keyword, keyword,
path_or_identifier, str_lit, ws, num_lit, path_or_identifier, str_lit, ws,
}; };
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -112,18 +112,12 @@ impl<'a> Target<'a> {
if let [name] = path.as_slice() { if let [name] = path.as_slice() {
// If the path only contains one item, we need to check the name. // If the path only contains one item, we need to check the name.
if !can_be_variable_name(name) { if !can_be_variable_name(name) {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(format!("`{name}` cannot be used as an identifier"), *name);
format!("`{name}` cannot be used as an identifier"),
*name,
)));
} }
} else { } else {
// Otherwise we need to check every element but the first. // Otherwise we need to check every element but the first.
if let Some(name) = path.iter().skip(1).find(|n| !can_be_variable_name(n)) { if let Some(name) = path.iter().skip(1).find(|n| !can_be_variable_name(n)) {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(format!("`{name}` cannot be used as an identifier"), *name);
format!("`{name}` cannot be used as an identifier"),
*name,
)));
} }
} }
@ -163,20 +157,17 @@ impl<'a> Target<'a> {
if let Some(rest) = rest { if let Some(rest) = rest {
let chr = peek(ws(opt(one_of([',', ':'])))).parse_next(i)?; let chr = peek(ws(opt(one_of([',', ':'])))).parse_next(i)?;
if let Some(chr) = chr { if let Some(chr) = chr {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!( format!(
"unexpected `{chr}` character after `..`\n\ "unexpected `{chr}` character after `..`\n\
note that in a named struct, `..` must come last to ignore other members" note that in a named struct, `..` must come last to ignore other members"
), ),
*i, *i,
))); );
} }
if let Target::Rest(ref s) = rest.0 { if let Target::Rest(ref s) = rest.0 {
if s.inner.is_some() { if s.inner.is_some() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("`@ ..` cannot be used in struct", s.span);
"`@ ..` cannot be used in struct",
s.span,
)));
} }
} }
return Ok((rest.1, rest.0)); return Ok((rest.1, rest.0));
@ -191,10 +182,7 @@ impl<'a> Target<'a> {
if src == "_" { if src == "_" {
*i = start; *i = start;
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("cannot use placeholder `_` as source in named struct", *i);
"cannot use placeholder `_` as source in named struct",
*i,
)));
} }
let target = match target { let target = match target {
@ -216,20 +204,17 @@ impl<'a> Target<'a> {
fn verify_name<'a>(input: &'a str, name: &'a str) -> Result<Target<'a>, ErrMode<ErrorContext<'a>>> { fn verify_name<'a>(input: &'a str, name: &'a str) -> Result<Target<'a>, ErrMode<ErrorContext<'a>>> {
if is_rust_keyword(name) { if is_rust_keyword(name) {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!("cannot use `{name}` as a name: it is a rust keyword"), format!("cannot use `{name}` as a name: it is a rust keyword"),
input, input,
))) )
} else if !can_be_variable_name(name) { } else if !can_be_variable_name(name) {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(format!("`{name}` cannot be used as an identifier"), input)
format!("`{name}` cannot be used as an identifier"),
input,
)))
} else if name.starts_with("__askama") { } else if name.starts_with("__askama") {
Err(ErrMode::Cut(ErrorContext::new( cut_error!(
format!("cannot use `{name}` as a name: it is reserved for `askama`"), format!("cannot use `{name}` as a name: it is reserved for `askama`"),
input, input,
))) )
} else { } else {
Ok(Target::Name(name)) Ok(Target::Name(name))
} }
@ -250,10 +235,7 @@ fn collect_targets<'a, T>(
let targets = opt(separated(1.., one, ws(',')).map(|v: Vec<_>| v)).parse_next(i)?; let targets = opt(separated(1.., one, ws(',')).map(|v: Vec<_>| v)).parse_next(i)?;
let Some(targets) = targets else { let Some(targets) = targets else {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("expected comma separated list of members", *i);
"expected comma separated list of members",
*i,
)));
}; };
let (has_comma, has_end) = (opt_comma, opt_end).parse_next(i)?; let (has_comma, has_end) = (opt_comma, opt_end).parse_next(i)?;
@ -262,7 +244,7 @@ fn collect_targets<'a, T>(
true => format!("expected member, or `{delim}` as terminator"), true => format!("expected member, or `{delim}` as terminator"),
false => format!("expected `,` for more members, or `{delim}` as terminator"), false => format!("expected `,` for more members, or `{delim}` as terminator"),
}; };
return Err(ErrMode::Cut(ErrorContext::new(msg, *i))); return cut_error!(msg, *i);
} }
let singleton = !has_comma && targets.len() == 1; let singleton = !has_comma && targets.len() == 1;
@ -279,16 +261,13 @@ fn only_one_rest_pattern<'a>(
for target in &targets { for target in &targets {
if let Target::Rest(s) = target { if let Target::Rest(s) = target {
if !allow_named_rest && s.inner.is_some() { if !allow_named_rest && s.inner.is_some() {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!("`@ ..` is only allowed in slice patterns", s.span);
"`@ ..` is only allowed in slice patterns",
s.span,
)));
} }
if found_rest { if found_rest {
return Err(ErrMode::Cut(ErrorContext::new( return cut_error!(
format!("`..` can only be used once per {type_kind} pattern"), format!("`..` can only be used once per {type_kind} pattern"),
s.span, s.span,
))); );
} }
found_rest = true; found_rest = true;
} }