Skip to main content

open_cypher/
parser.rs

1//! Strict and recovering parser entry points.
2
3use std::fmt;
4
5use lalrpop_util::ParseError;
6
7use crate::ast::{
8    BinaryOperator, Clause, ClauseKind, ErrorNode, Expr, ExprKind, Identifier, IntegerLiteral,
9    IntegerRadix, IsPredicate, LabelExpression, ListComprehension, Literal, LiteralKind,
10    MapProjection, MapProjectionItem, MatchClause, Name, Node, Parameter, ParameterName,
11    PathFactor, PathFactorKind, Pattern, PatternComprehension, Program, QualifiedName, Quantifier,
12    Query, QueryKind, QueryStatement, QuoteStyle, RegularQuery, RelationshipDirection,
13    RelationshipPattern, SingleQuery, SingleQueryKind, StatementKind, StringLiteral, UnionBranch,
14    UnionOperator,
15};
16use crate::diagnostic::{Diagnostic, DiagnosticCode, ParseErrors};
17use crate::lexer;
18use crate::span::Span;
19use crate::token::{Keyword, Token, TokenKind};
20
21// `cfg(mutants)` is declared in Cargo.toml but never set, so rustc drops this
22// attribute and no `mutants` dependency is needed (an attribute macro could
23// not be applied to an out-of-line module anyway). cargo-mutants reads the
24// attribute textually and skips the module before loading the 5 MB LALRPOP
25// output; without it every run spends minutes discovering mutants in the
26// generated file only to discard them via the exclude glob afterwards.
27#[cfg_attr(mutants, mutants::skip)]
28#[path = "generated/cypher.rs"]
29#[allow(clippy::all)]
30mod generated;
31
32const MAX_DIAGNOSTICS: usize = 32;
33
34/// A parsed program and its complete, trivia-preserving token stream.
35#[derive(Clone, Debug, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct ParsedProgram {
38    /// The owned syntax tree.
39    pub program: Program,
40    /// Every token, including whitespace and comments.
41    pub tokens: Vec<Token>,
42}
43
44/// The result of a recovering parse.
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub struct ParseOutcome<T> {
48    /// The recovered value, when the selected entry point can construct one.
49    ///
50    /// [`parse_recovering`] currently always returns a `ParsedProgram`: valid
51    /// input produces the parsed tree, while invalid input preserves recoverable
52    /// clauses or falls back to a whole-input error statement.
53    pub value: Option<T>,
54    /// Lexer and parser diagnostics in deterministic source order.
55    pub diagnostics: Vec<Diagnostic>,
56}
57
58/// Parses one complete source input, rejecting lexical or syntactic errors.
59pub fn parse(source: &str) -> Result<ParsedProgram, ParseErrors> {
60    let outcome = parse_recovering(source);
61    if outcome.diagnostics.iter().any(Diagnostic::is_error) {
62        return Err(ParseErrors::new(outcome.diagnostics));
63    }
64
65    outcome.value.ok_or_else(|| {
66        ParseErrors::new(vec![Diagnostic::error(
67            DiagnosticCode::Internal,
68            "the parser did not produce a program",
69            Span::empty(source.len()),
70        )])
71    })
72}
73
74/// Parses while retaining diagnostics and always constructing a program root.
75///
76/// Recovery retains valid top-level clauses and inserts [`ClauseKind::Error`]
77/// placeholders for skipped regions. Missing operands use [`ExprKind::Error`].
78/// UNION branches and operators are retained; valid clauses
79/// take precedence over splitting contextual names. Multi-statement inputs and
80/// inputs with no recoverable clauses fall back to an error statement. Delimiters are not repaired.
81#[must_use]
82pub fn parse_recovering(source: &str) -> ParseOutcome<ParsedProgram> {
83    let lexed = lexer::lex(source);
84    let mut diagnostics = lexed.diagnostics.clone();
85    let significant = parser_tokens(&lexed.tokens);
86
87    let parsed = parse_program_with_contextual_names(source, &significant);
88    let program = match parsed {
89        Ok(program) => Some(program),
90        Err(error) => {
91            diagnostics.push(parse_diagnostic(source, error));
92            None
93        }
94    };
95
96    if program.is_none() || !diagnostics.is_empty() {
97        collect_delimiter_diagnostics(source, &lexed.tokens, &mut diagnostics);
98    }
99
100    normalize_diagnostics(source.len(), &mut diagnostics);
101
102    let program = program.unwrap_or_else(|| {
103        recover_clauses(source, &significant)
104            .unwrap_or_else(|| recovered_program(source, &lexed.tokens))
105    });
106    ParseOutcome {
107        value: Some(ParsedProgram {
108            program,
109            tokens: lexed.tokens,
110        }),
111        diagnostics,
112    }
113}
114
115type SpannedParserToken = (usize, ParserToken, usize);
116
117fn parse_program_with_contextual_names(
118    source: &str,
119    tokens: &[SpannedParserToken],
120) -> Result<Program, LalrpopError> {
121    parse_tokens_with_contextual_names(tokens, |contextual| parse_program(source, contextual))
122}
123
124fn parse_tokens_with_contextual_names<T>(
125    tokens: &[SpannedParserToken],
126    mut parse_tokens: impl FnMut(&[SpannedParserToken]) -> Result<T, LalrpopError>,
127) -> Result<T, LalrpopError> {
128    let mut contextual = tokens.to_vec();
129    let mut resolved = vec![false; tokens.len()];
130    for index in 0..tokens.len() {
131        let follows_name_marker = index
132            .checked_sub(1)
133            .and_then(|previous| tokens.get(previous))
134            .is_some_and(|token| matches!(token.1, ParserToken::As | ParserToken::Dot));
135        if is_aliased_bare_expression(tokens, index) && tokens[index].1.is_keyword_literal() {
136            // Preserve the native literal interpretation and keep later retry
137            // errors from reclassifying this already-valid projection item.
138            resolved[index] = true;
139            continue;
140        }
141        if tokens[index].1.is_contextual_name_candidate()
142            && ((follows_name_marker
143                && !(tokens[index].1 == ParserToken::As && tokens[index - 1].1 == ParserToken::As))
144                || is_solo_projection_keyword(tokens, index)
145                || is_aliased_bare_expression(tokens, index))
146        {
147            resolved[index] = true;
148            contextual[index].1 = ParserToken::Identifier;
149        }
150    }
151
152    let initial_error = match parse_tokens(&contextual) {
153        Ok(value) => return Ok(value),
154        Err(error) => error,
155    };
156
157    let mut best_error = initial_error.clone();
158    let mut error = initial_error;
159
160    // The generated state machines already reinterpret a keyword when its
161    // native action is an immediate error and the identifier action is valid.
162    // A smaller set of constructs has delayed ambiguity: the keyword shifts as
163    // syntax, then fails at a later token or EOF (for example a bare COUNT).
164    // Reclassify the nearest remaining candidate and retry those cases.
165    // Each pass commits one lexical-context signature (the keyword and its two
166    // neighboring tokens), so repeated non-reserved words in the same name
167    // position are handled together without conflating distinct token kinds.
168    // Valid queries can use an arbitrary number of non-reserved names, and
169    // malformed input is bounded by the finite set of signatures instead of
170    // an exponential search through keyword subsets.
171    loop {
172        if error_location(&error) >= error_location(&best_error) {
173            best_error = error.clone();
174        }
175
176        let location = error_location(&error);
177        let candidate = tokens
178            .iter()
179            .enumerate()
180            .filter(|(index, (_, token, _))| {
181                token.is_contextual_name_candidate() && !resolved[*index]
182            })
183            .map(|(index, (start, _, end))| {
184                let distance = if location < *start {
185                    *start - location
186                } else {
187                    location.saturating_sub(*end)
188                };
189                let exact = (*start..*end).contains(&location);
190                let followed_by_as = tokens
191                    .get(index + 1)
192                    .is_some_and(|token| token.1 == ParserToken::As);
193                let priority = if exact && (tokens[index].1 != ParserToken::As || followed_by_as) {
194                    0
195                } else if followed_by_as && tokens[index].1 != ParserToken::As {
196                    1
197                } else if exact {
198                    2
199                } else {
200                    3
201                };
202                (priority, distance, index)
203            })
204            .min();
205        let Some((_, _, index)) = candidate else {
206            break;
207        };
208
209        let signature = contextual_name_signature(tokens, index);
210        let mut reclassified = Vec::new();
211        for peer in 0..tokens.len() {
212            if !resolved[peer]
213                && tokens[peer].1.is_contextual_name_candidate()
214                && contextual_name_signature(tokens, peer) == signature
215            {
216                resolved[peer] = true;
217                contextual[peer].1 = ParserToken::Identifier;
218                reclassified.push(peer);
219            }
220        }
221        let next_error = match parse_tokens(&contextual) {
222            Ok(value) => return Ok(value),
223            Err(next_error) => next_error,
224        };
225        // A retry counts as progress only when it fails farther along, and
226        // not when it fails on a token this pass just reclassified: that
227        // means the identifier reading is invalid there, so the earlier error
228        // (often inside a nested fragment) is the one worth reporting.
229        let next_location = error_location(&next_error);
230        let lands_on_reclassified = reclassified
231            .iter()
232            .any(|&peer| (tokens[peer].0..tokens[peer].2).contains(&next_location));
233        if next_location > location && !lands_on_reclassified {
234            error = next_error;
235        } else {
236            break;
237        }
238    }
239
240    Err(best_error)
241}
242
243fn is_aliased_bare_expression(tokens: &[SpannedParserToken], index: usize) -> bool {
244    // A one-token projection item followed by AS cannot be one of the keyword
245    // constructs with delayed ambiguity (CASE, COUNT, EXISTS, and so on).
246    // Resolve every such item in one scan. The caller separately preserves the
247    // established interpretation of the six keyword-backed literal spellings.
248    index
249        .checked_sub(1)
250        .and_then(|previous| tokens.get(previous))
251        .is_some_and(|token| {
252            matches!(
253                token.1,
254                ParserToken::Return | ParserToken::With | ParserToken::Yield | ParserToken::Comma
255            )
256        })
257        && tokens
258            .get(index + 1)
259            .is_some_and(|token| token.1 == ParserToken::As)
260}
261
262fn is_solo_projection_keyword(tokens: &[SpannedParserToken], index: usize) -> bool {
263    if !matches!(tokens[index].1, ParserToken::SetAll | ParserToken::Distinct)
264        || !index
265            .checked_sub(1)
266            .and_then(|previous| tokens.get(previous))
267            .is_some_and(|token| matches!(token.1, ParserToken::Return | ParserToken::With))
268    {
269        return false;
270    }
271
272    tokens.get(index + 1).is_none_or(|token| {
273        matches!(
274            token.1,
275            ParserToken::As
276                | ParserToken::Comma
277                | ParserToken::Order
278                | ParserToken::Offset
279                | ParserToken::Skip
280                | ParserToken::Limit
281                | ParserToken::Where
282                | ParserToken::Union
283                | ParserToken::Semicolon
284        ) || clause_starts(token.1)
285    })
286}
287
288fn contextual_name_signature(
289    tokens: &[SpannedParserToken],
290    index: usize,
291) -> (Option<ParserToken>, ParserToken, Option<ParserToken>) {
292    (
293        index
294            .checked_sub(1)
295            .and_then(|previous| tokens.get(previous))
296            .map(|token| token.1),
297        tokens[index].1,
298        tokens.get(index + 1).map(|token| token.1),
299    )
300}
301
302fn parse_program(source: &str, tokens: &[SpannedParserToken]) -> Result<Program, LalrpopError> {
303    if tokens.is_empty() {
304        return Ok(Program::new(Vec::new(), Span::empty(0)));
305    }
306
307    let (query_tokens, terminator) = match tokens.last() {
308        Some((start, ParserToken::Semicolon, end)) => {
309            (&tokens[..tokens.len() - 1], Some(Span::new(*start, *end)))
310        }
311        _ => (tokens, None),
312    };
313
314    if query_tokens.is_empty() {
315        return Err(ParseError::UnrecognizedEof {
316            location: terminator.map_or(0, |span| span.start),
317            expected: vec!["query clause".to_owned()],
318        });
319    }
320
321    let (head, unions) = match parse_regular_query(source, query_tokens) {
322        Ok(query) => query,
323        Err(regular_error) => {
324            let input = query_tokens.iter().copied().map(Ok);
325            let clause = match generated::StandaloneCallRootParser::new().parse(source, input) {
326                Ok(clause) => clause,
327                Err(_) => return Err(regular_error),
328            };
329            let span = clause.span;
330            (
331                Node::new(
332                    SingleQueryKind {
333                        clauses: vec![clause],
334                    },
335                    span,
336                ),
337                Vec::new(),
338            )
339        }
340    };
341    let query_span = Span::new(
342        query_tokens.first().expect("non-empty query").0,
343        query_tokens.last().expect("non-empty query").2,
344    );
345    let query = Node::new(
346        QueryKind::Regular(RegularQuery { head, unions }),
347        query_span,
348    );
349    let statement_span = terminator.map_or(query_span, |span| query_span.cover(span));
350    let statement = Node::new(
351        StatementKind::Query(QueryStatement {
352            mode: None,
353            query,
354            terminator,
355        }),
356        statement_span,
357    );
358    Ok(Program::new(vec![statement], statement_span))
359}
360
361fn parse_regular_query(
362    source: &str,
363    tokens: &[SpannedParserToken],
364) -> Result<(SingleQuery, Vec<UnionBranch>), LalrpopError> {
365    let unsplit_error = match parse_single_query(source, tokens) {
366        Ok(query) => return Ok((query, Vec::new())),
367        Err(error) => error,
368    };
369
370    for union_index in top_level_indices(tokens, |token| token == ParserToken::Union) {
371        // A token error is independent of tokens after that lookahead. A split
372        // beyond it cannot turn the same invalid prefix into a single query.
373        // Keep EOF and user errors on the exhaustive path: extending those
374        // candidates can change the result or its diagnostic.
375        if let ParseError::UnrecognizedToken { token, .. } = &unsplit_error
376            && tokens[union_index].0 > token.0
377        {
378            break;
379        }
380        let Ok(head) = parse_single_query(source, &tokens[..union_index]) else {
381            continue;
382        };
383
384        let mut right_start = union_index + 1;
385        let operator = match tokens.get(right_start).map(|token| token.1) {
386            Some(ParserToken::All) => {
387                right_start += 1;
388                UnionOperator::All
389            }
390            Some(ParserToken::Distinct) => {
391                right_start += 1;
392                UnionOperator::Distinct
393            }
394            _ => UnionOperator::Default,
395        };
396        if right_start >= tokens.len() {
397            continue;
398        }
399
400        let Ok((right, mut tail)) = parse_regular_query(source, &tokens[right_start..]) else {
401            continue;
402        };
403        let operator_end = tokens[right_start - 1].2;
404        let operator = Node::new(operator, Span::new(tokens[union_index].0, operator_end));
405        let mut unions = Vec::with_capacity(1 + tail.len());
406        unions.push(UnionBranch {
407            operator,
408            query: right,
409        });
410        unions.append(&mut tail);
411        return Ok((head, unions));
412    }
413
414    Err(unsplit_error)
415}
416
417fn parse_single_query(
418    source: &str,
419    tokens: &[SpannedParserToken],
420) -> Result<SingleQuery, LalrpopError> {
421    if tokens.is_empty() {
422        return Err(ParseError::UnrecognizedEof {
423            location: 0,
424            expected: vec!["query clause".to_owned()],
425        });
426    }
427
428    let mut clauses: Vec<Clause> = Vec::new();
429    let mut cursor = 0;
430    while cursor < tokens.len() {
431        let mut candidate_ends = top_level_indices(&tokens[cursor + 1..], clause_starts)
432            .into_iter()
433            .map(|index| cursor + 1 + index)
434            .collect::<Vec<_>>();
435        candidate_ends.push(tokens.len());
436        candidate_ends.sort_unstable();
437        candidate_ends.dedup();
438
439        let mut best_error = None;
440        let mut parsed = None;
441        for end in candidate_ends {
442            #[cfg(test)]
443            STRICT_CLAUSE_ATTEMPTS.with(|attempts| attempts.set(attempts.get() + 1));
444            let input = tokens[cursor..end].iter().copied().map(Ok);
445            match generated::ClauseRootParser::new().parse(source, input) {
446                Ok(clause) => {
447                    if matches!(&clause.kind, ClauseKind::Return(_)) && end < tokens.len() {
448                        return Err(ParseError::ExtraToken { token: tokens[end] });
449                    }
450                    parsed = Some((clause, end));
451                    break;
452                }
453                // The generated LR parser has already rejected a concrete
454                // lookahead token. Appending tokens cannot change that action,
455                // so all longer candidates would repeat the same failure.
456                Err(error @ ParseError::UnrecognizedToken { .. }) => return Err(error),
457                Err(error) => retain_farthest_error(&mut best_error, error),
458            }
459        }
460
461        let Some((clause, end)) = parsed else {
462            return Err(best_error.unwrap_or(ParseError::UnrecognizedEof {
463                location: tokens[cursor].0,
464                expected: vec!["query clause".to_owned()],
465            }));
466        };
467        clauses.push(clause);
468        cursor = end;
469    }
470
471    let span = Span::new(
472        tokens.first().expect("non-empty query").0,
473        tokens.last().unwrap().2,
474    );
475    Ok(Node::new(SingleQueryKind { clauses }, span))
476}
477
478fn retain_farthest_error(slot: &mut Option<LalrpopError>, candidate: LalrpopError) {
479    let candidate_location = error_location(&candidate);
480    if slot
481        .as_ref()
482        .is_none_or(|current| candidate_location >= error_location(current))
483    {
484        *slot = Some(candidate);
485    }
486}
487
488fn error_location(error: &LalrpopError) -> usize {
489    match error {
490        ParseError::InvalidToken { location } | ParseError::UnrecognizedEof { location, .. } => {
491            *location
492        }
493        ParseError::UnrecognizedToken { token, .. } | ParseError::ExtraToken { token } => token.0,
494        ParseError::User { error: location } => *location,
495    }
496}
497
498fn top_level_indices(
499    tokens: &[SpannedParserToken],
500    predicate: impl Fn(ParserToken) -> bool,
501) -> Vec<usize> {
502    let mut depth = 0usize;
503    let mut indices = Vec::new();
504    for (index, (_, token, _)) in tokens.iter().copied().enumerate() {
505        if depth == 0 && predicate(token) {
506            indices.push(index);
507        }
508        match token {
509            ParserToken::LeftParen | ParserToken::LeftBracket | ParserToken::LeftBrace => {
510                depth = depth.saturating_add(1);
511            }
512            ParserToken::RightParen | ParserToken::RightBracket | ParserToken::RightBrace => {
513                depth = depth.saturating_sub(1);
514            }
515            _ => {}
516        }
517    }
518    indices
519}
520
521fn clause_starts(token: ParserToken) -> bool {
522    matches!(
523        token,
524        ParserToken::Match
525            | ParserToken::Optional
526            | ParserToken::Unwind
527            | ParserToken::With
528            | ParserToken::Return
529            | ParserToken::Create
530            | ParserToken::Merge
531            | ParserToken::Set
532            | ParserToken::Remove
533            | ParserToken::Delete
534            | ParserToken::Detach
535            | ParserToken::Call
536    )
537}
538
539fn normalize_diagnostics(source_len: usize, diagnostics: &mut Vec<Diagnostic>) {
540    diagnostics.sort_by(|left, right| {
541        left.primary_span
542            .start
543            .cmp(&right.primary_span.start)
544            .then(left.primary_span.end.cmp(&right.primary_span.end))
545            .then(left.code.cmp(&right.code))
546            .then(left.message.cmp(&right.message))
547    });
548    diagnostics.dedup_by(|left, right| {
549        left.code == right.code
550            && left.primary_span == right.primary_span
551            && left.message == right.message
552    });
553
554    if diagnostics.len() > MAX_DIAGNOSTICS {
555        diagnostics.truncate(MAX_DIAGNOSTICS - 1);
556        diagnostics.push(Diagnostic::error(
557            DiagnosticCode::TooManyErrors,
558            "additional parser diagnostics were suppressed",
559            Span::empty(source_len),
560        ));
561    }
562}
563
564fn recovered_program(source: &str, tokens: &[Token]) -> Program {
565    let span = significant_span(tokens).unwrap_or_else(|| Span::empty(source.len()));
566    let statements = if span.is_empty() {
567        Vec::new()
568    } else {
569        vec![Node::new(StatementKind::Error(ErrorNode), span)]
570    };
571    Program::new(statements, span)
572}
573
574fn significant_span(tokens: &[Token]) -> Option<Span> {
575    let mut significant = tokens.iter().filter(|token| !token.is_trivia());
576    let first = significant.next()?;
577    let mut span = first.span;
578    for token in significant {
579        span = span.cover(token.span);
580    }
581    Some(span)
582}
583
584fn collect_delimiter_diagnostics(
585    source: &str,
586    tokens: &[Token],
587    diagnostics: &mut Vec<Diagnostic>,
588) {
589    let mut stack: Vec<(TokenKind, Span)> = Vec::new();
590
591    for token in tokens.iter().filter(|token| !token.is_trivia()) {
592        match token.kind {
593            TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => {
594                stack.push((token.kind, token.span));
595            }
596            TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => {
597                let expected_open = match token.kind {
598                    TokenKind::RightParen => TokenKind::LeftParen,
599                    TokenKind::RightBracket => TokenKind::LeftBracket,
600                    TokenKind::RightBrace => TokenKind::LeftBrace,
601                    _ => unreachable!(),
602                };
603                match stack.pop() {
604                    Some((open, _)) if open == expected_open => {}
605                    Some((open, open_span)) => diagnostics.push(
606                        Diagnostic::error(
607                            DiagnosticCode::MismatchedDelimiter,
608                            format!(
609                                "mismatched delimiter: {} does not close {}",
610                                token.kind, open
611                            ),
612                            token.span,
613                        )
614                        .with_label(open_span, "this delimiter was opened here"),
615                    ),
616                    None => diagnostics.push(Diagnostic::error(
617                        DiagnosticCode::MismatchedDelimiter,
618                        format!("unexpected closing delimiter {}", token.kind),
619                        token.span,
620                    )),
621                }
622            }
623            _ => {}
624        }
625    }
626
627    for (open, span) in stack {
628        diagnostics.push(
629            Diagnostic::error(
630                DiagnosticCode::UnclosedDelimiter,
631                format!("unclosed delimiter {open}"),
632                span,
633            )
634            .with_help(format!(
635                "add the matching delimiter before byte {}",
636                source.len()
637            )),
638        );
639    }
640}
641
642/// The user error payload is the source offset the nested fragment failed at.
643type LalrpopError = ParseError<usize, ParserToken, usize>;
644
645fn parse_diagnostic(source: &str, error: LalrpopError) -> Diagnostic {
646    match error {
647        ParseError::InvalidToken { location } => Diagnostic::error(
648            DiagnosticCode::InvalidToken,
649            "invalid token",
650            Span::empty(location.min(source.len())),
651        ),
652        ParseError::UnrecognizedEof { location, expected } => {
653            let expected = normalized_expected(expected);
654            let message = if expected.is_empty() {
655                "unexpected end of input".to_owned()
656            } else {
657                format!("unexpected end of input; expected {expected}")
658            };
659            Diagnostic::error(
660                DiagnosticCode::UnexpectedEof,
661                message,
662                Span::empty(location.min(source.len())),
663            )
664        }
665        ParseError::UnrecognizedToken {
666            token: (start, token, end),
667            expected,
668        } => {
669            let expected = normalized_expected(expected);
670            let message = if expected.is_empty() {
671                format!("unexpected token {token}")
672            } else {
673                format!("unexpected token {token}; expected {expected}")
674            };
675            Diagnostic::error(
676                DiagnosticCode::UnexpectedToken,
677                message,
678                Span::new(start, end),
679            )
680        }
681        ParseError::ExtraToken {
682            token: (start, token, end),
683        } => Diagnostic::error(
684            DiagnosticCode::ExtraToken,
685            format!("extra token {token} after the end of the statement"),
686            Span::new(start, end),
687        ),
688        ParseError::User { error: location } => Diagnostic::error(
689            DiagnosticCode::UnexpectedToken,
690            "input does not satisfy the selected syntax production",
691            Span::empty(location.min(source.len())),
692        ),
693    }
694}
695
696fn normalized_expected(mut expected: Vec<String>) -> String {
697    expected.retain(|item| item.trim_matches('"') != "error expression");
698    for item in &mut expected {
699        *item = item.trim_matches('"').to_owned();
700    }
701    expected.sort();
702    expected.dedup();
703    match expected.as_slice() {
704        [] => String::new(),
705        [only] => format!("`{only}`"),
706        [head @ .., last] => {
707            let head = head
708                .iter()
709                .map(|item| format!("`{item}`"))
710                .collect::<Vec<_>>()
711                .join(", ");
712            format!("{head}, or `{last}`")
713        }
714    }
715}
716
717fn parser_tokens(tokens: &[Token]) -> Vec<SpannedParserToken> {
718    let significant = tokens
719        .iter()
720        .filter(|token| !token.is_trivia())
721        .collect::<Vec<_>>();
722    let mut result = Vec::with_capacity(significant.len());
723    let mut index = 0;
724    while index < significant.len() {
725        if let Some(end) = escaped_parameter_end(&significant, index) {
726            result.push((
727                significant[index].span.start,
728                ParserToken::Parameter,
729                significant[end].span.end,
730            ));
731            index = end + 1;
732            continue;
733        }
734        if let Some(end) = pattern_comprehension_end(&significant, index) {
735            result.push((
736                significant[index].span.start,
737                ParserToken::PatternComprehension,
738                significant[end].span.end,
739            ));
740            index = end + 1;
741            continue;
742        }
743        if significant[index].kind == TokenKind::LeftParen
744            && is_pattern_expression_context(&significant, index)
745            && let Some(end) = pattern_expression_end(&significant, index)
746        {
747            result.push((
748                significant[index].span.start,
749                ParserToken::PatternExpression,
750                significant[end].span.end,
751            ));
752            index = end + 1;
753            continue;
754        }
755        if is_relationship_label_start(&significant, index)
756            && let Some(end) = relationship_label_end(&significant, index)
757        {
758            result.push((
759                significant[index].span.start,
760                ParserToken::RelationshipLabelExpression,
761                significant[end].span.end,
762            ));
763            index = end + 1;
764            continue;
765        }
766        if is_label_predicate_context(&significant, index)
767            && let Some(end) = label_predicate_end(&significant, index)
768        {
769            result.push((
770                significant[index].span.start,
771                ParserToken::LabelPredicate,
772                significant[end].span.end,
773            ));
774            index = end + 1;
775            continue;
776        }
777        if !is_procedure_name_start(&significant, index)
778            && let Some(end) = qualified_function_name_end(&significant, index)
779        {
780            result.push((
781                significant[index].span.start,
782                ParserToken::QualifiedFunctionName,
783                significant[end].span.end,
784            ));
785            index = end + 1;
786            continue;
787        }
788        let token = significant[index];
789        let mut parser_token = ParserToken::from(token.kind);
790        if parser_token == ParserToken::All && is_explicit_set_all(&significant, index) {
791            parser_token = ParserToken::SetAll;
792        }
793        result.push((token.span.start, parser_token, token.span.end));
794        index += 1;
795    }
796    result
797}
798
799fn escaped_parameter_end(tokens: &[&Token], start: usize) -> Option<usize> {
800    (tokens.get(start)?.kind == TokenKind::Dollar
801        && tokens.get(start + 1).is_some_and(|token| {
802            token.kind == TokenKind::EscapedIdentifier && tokens[start].span.end == token.span.start
803        }))
804    .then_some(start + 1)
805}
806
807fn qualified_function_name_end(tokens: &[&Token], start: usize) -> Option<usize> {
808    if !is_symbolic_name_kind(tokens.get(start)?.kind) {
809        return None;
810    }
811
812    let mut end = start;
813    let mut components = 1usize;
814    while tokens
815        .get(end + 1)
816        .is_some_and(|token| token.kind == TokenKind::Dot)
817        && tokens
818            .get(end + 2)
819            .is_some_and(|token| is_symbolic_name_kind(token.kind))
820    {
821        end += 2;
822        components += 1;
823    }
824
825    (components > 1
826        && tokens
827            .get(end + 1)
828            .is_some_and(|token| token.kind == TokenKind::LeftParen))
829    .then_some(end)
830}
831
832fn is_procedure_name_start(tokens: &[&Token], start: usize) -> bool {
833    let mut first_component = start;
834    while first_component >= 2
835        && tokens[first_component - 1].kind == TokenKind::Dot
836        && is_symbolic_name_kind(tokens[first_component - 2].kind)
837    {
838        first_component -= 2;
839    }
840
841    first_component
842        .checked_sub(1)
843        .and_then(|index| tokens.get(index))
844        .is_some_and(|token| token.kind == TokenKind::Keyword(Keyword::Call))
845}
846
847fn is_symbolic_name_kind(kind: TokenKind) -> bool {
848    matches!(
849        kind,
850        TokenKind::Identifier | TokenKind::EscapedIdentifier | TokenKind::Keyword(_)
851    )
852}
853
854fn is_explicit_set_all(tokens: &[&Token], index: usize) -> bool {
855    if tokens
856        .get(index + 1)
857        .is_some_and(|token| token.kind == TokenKind::LeftParen)
858    {
859        return false;
860    }
861
862    let Some(previous) = index.checked_sub(1).and_then(|index| tokens.get(index)) else {
863        return false;
864    };
865    if matches!(
866        previous.kind,
867        TokenKind::Keyword(Keyword::Return | Keyword::With)
868    ) {
869        return true;
870    }
871    if previous.kind != TokenKind::LeftParen {
872        return false;
873    }
874
875    index
876        .checked_sub(2)
877        .and_then(|index| tokens.get(index))
878        .is_some_and(|token| {
879            matches!(
880                ParserToken::from(token.kind),
881                ParserToken::Identifier
882                    | ParserToken::EscapedIdentifier
883                    | ParserToken::Count
884                    | ParserToken::Match
885                    | ParserToken::Path
886                    | ParserToken::Return
887            )
888        })
889}
890
891fn pattern_comprehension_end(tokens: &[&Token], start: usize) -> Option<usize> {
892    if tokens.get(start)?.kind != TokenKind::LeftBracket {
893        return None;
894    }
895
896    // The heuristic only counts evidence from the comprehension's pattern
897    // region: before the first top-level WHERE or `|`, at bracket depth 1.
898    // Arrows in a WHERE predicate, constructs in nested brackets, and
899    // function-call or grouping parentheses (a `(` directly after a name or
900    // a closing delimiter) are not pattern evidence — `[x IN a - f(b) - g(c) | x]`
901    // is a list comprehension, not an undirected relationship pattern.
902    let mut brackets = 0usize;
903    let mut parentheses = 0usize;
904    let mut braces = 0usize;
905    let mut pipe_at = None;
906    let mut where_seen = false;
907    let mut arrow = false;
908    let mut top_level_nodes = 0usize;
909    let mut top_level_minuses = 0usize;
910    for (index, token) in tokens.iter().enumerate().skip(start) {
911        let in_pattern_region = pipe_at.is_none() && !where_seen;
912        match token.kind {
913            TokenKind::LeftBracket => brackets += 1,
914            TokenKind::RightBracket => {
915                if brackets == 1 && parentheses == 0 && braces == 0 {
916                    let has_projection = pipe_at.is_some_and(|pipe| index > pipe + 1);
917                    let undirected_relationship = top_level_nodes >= 2 && top_level_minuses >= 2;
918                    return (has_projection && (arrow || undirected_relationship)).then_some(index);
919                }
920                brackets = brackets.saturating_sub(1);
921            }
922            TokenKind::LeftParen => {
923                let call_or_grouping = index > start
924                    && (matches!(
925                        tokens[index - 1].kind,
926                        TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace
927                    ) || is_symbolic_name_kind(tokens[index - 1].kind));
928                if in_pattern_region
929                    && brackets == 1
930                    && parentheses == 0
931                    && braces == 0
932                    && !call_or_grouping
933                {
934                    top_level_nodes += 1;
935                }
936                parentheses += 1;
937            }
938            TokenKind::RightParen => parentheses = parentheses.saturating_sub(1),
939            TokenKind::LeftBrace => braces += 1,
940            TokenKind::RightBrace => braces = braces.saturating_sub(1),
941            TokenKind::Pipe if brackets == 1 && parentheses == 0 && braces == 0 => {
942                pipe_at = Some(index)
943            }
944            // `where` followed by `=` is a contextual binding name, not the
945            // predicate keyword.
946            TokenKind::Keyword(Keyword::Where)
947                if in_pattern_region
948                    && brackets == 1
949                    && parentheses == 0
950                    && braces == 0
951                    && tokens
952                        .get(index + 1)
953                        .is_none_or(|next| next.kind != TokenKind::Equal) =>
954            {
955                where_seen = true;
956            }
957            TokenKind::LeftArrow | TokenKind::RightArrow if in_pattern_region && brackets == 1 => {
958                arrow = true;
959            }
960            TokenKind::Minus
961                if in_pattern_region && brackets == 1 && parentheses == 0 && braces == 0 =>
962            {
963                top_level_minuses += 1;
964            }
965            _ => {}
966        }
967    }
968    None
969}
970
971fn is_pattern_expression_context(tokens: &[&Token], start: usize) -> bool {
972    if start >= 2
973        && tokens[start - 1].kind == TokenKind::LeftParen
974        && matches!(
975            tokens[start - 2].kind,
976            TokenKind::Keyword(Keyword::ShortestPath | Keyword::AllShortestPaths)
977        )
978    {
979        return false;
980    }
981    if start >= 2
982        && tokens[start - 1].kind == TokenKind::LeftBrace
983        && matches!(
984            tokens[start - 2].kind,
985            TokenKind::Keyword(Keyword::Exists | Keyword::Count | Keyword::Collect)
986        )
987    {
988        return false;
989    }
990    if is_in_map_entry_value(tokens, start) {
991        return true;
992    }
993
994    // Like is_label_predicate_context, skip markers inside completed
995    // bracketed/braced constructs (a relationship-type `|` in `[:A|B]` is not
996    // an expression context), while a comprehension's projection `|` in the
997    // construct that still encloses the candidate remains visible.
998    let mut nested_brackets = 0usize;
999    let mut nested_braces = 0usize;
1000    for (index, token) in tokens[..start].iter().enumerate().rev() {
1001        match token.kind {
1002            TokenKind::RightBracket => {
1003                nested_brackets = nested_brackets.saturating_add(1);
1004                continue;
1005            }
1006            TokenKind::LeftBracket if nested_brackets != 0 => {
1007                nested_brackets = nested_brackets.saturating_sub(1);
1008                continue;
1009            }
1010            TokenKind::RightBrace => {
1011                nested_braces = nested_braces.saturating_add(1);
1012                continue;
1013            }
1014            TokenKind::LeftBrace if nested_braces != 0 => {
1015                nested_braces = nested_braces.saturating_sub(1);
1016                continue;
1017            }
1018            _ if nested_brackets != 0 || nested_braces != 0 => continue,
1019            _ => {}
1020        }
1021        if tokens
1022            .get(index + 1)
1023            .is_some_and(|next| next.kind == TokenKind::Equal)
1024        {
1025            continue;
1026        }
1027        match token.kind {
1028            TokenKind::Pipe => return true,
1029            TokenKind::Keyword(
1030                Keyword::Where
1031                | Keyword::Return
1032                | Keyword::With
1033                | Keyword::Unwind
1034                | Keyword::Set
1035                | Keyword::Delete
1036                | Keyword::Then
1037                | Keyword::Else
1038                | Keyword::When
1039                | Keyword::Case,
1040            ) => return true,
1041            TokenKind::Keyword(
1042                Keyword::Match | Keyword::Create | Keyword::Merge | Keyword::Call | Keyword::Remove,
1043            ) => return false,
1044            _ => {}
1045        }
1046    }
1047    false
1048}
1049
1050fn pattern_expression_end(tokens: &[&Token], start: usize) -> Option<usize> {
1051    if tokens.get(start)?.kind != TokenKind::LeftParen {
1052        return None;
1053    }
1054
1055    let mut node_end =
1056        matching_delimiter(tokens, start, TokenKind::LeftParen, TokenKind::RightParen)?;
1057    let mut relationships = 0usize;
1058    while let Some(after_relationship) = relationship_syntax_end(tokens, node_end + 1) {
1059        let mut next_node = after_relationship;
1060        if matches!(
1061            tokens.get(next_node).map(|token| token.kind),
1062            Some(TokenKind::Star | TokenKind::Plus | TokenKind::Question)
1063        ) {
1064            next_node += 1;
1065        } else if tokens
1066            .get(next_node)
1067            .is_some_and(|token| token.kind == TokenKind::LeftBrace)
1068        {
1069            next_node = matching_delimiter(
1070                tokens,
1071                next_node,
1072                TokenKind::LeftBrace,
1073                TokenKind::RightBrace,
1074            )? + 1;
1075        }
1076        if !tokens
1077            .get(next_node)
1078            .is_some_and(|token| token.kind == TokenKind::LeftParen)
1079        {
1080            break;
1081        }
1082        node_end = matching_delimiter(
1083            tokens,
1084            next_node,
1085            TokenKind::LeftParen,
1086            TokenKind::RightParen,
1087        )?;
1088        relationships += 1;
1089    }
1090
1091    (relationships > 0).then_some(node_end)
1092}
1093
1094fn relationship_syntax_end(tokens: &[&Token], start: usize) -> Option<usize> {
1095    let mut cursor = match (
1096        tokens.get(start).map(|token| token.kind),
1097        tokens.get(start + 1).map(|token| token.kind),
1098    ) {
1099        (Some(TokenKind::Minus | TokenKind::LeftArrow), _) => start + 1,
1100        (Some(TokenKind::Less), Some(TokenKind::Minus)) => start + 2,
1101        _ => return None,
1102    };
1103    if tokens
1104        .get(cursor)
1105        .is_some_and(|token| token.kind == TokenKind::LeftBracket)
1106    {
1107        cursor = matching_delimiter(
1108            tokens,
1109            cursor,
1110            TokenKind::LeftBracket,
1111            TokenKind::RightBracket,
1112        )? + 1;
1113    }
1114    match (
1115        tokens.get(cursor).map(|token| token.kind),
1116        tokens.get(cursor + 1).map(|token| token.kind),
1117    ) {
1118        (Some(TokenKind::Minus), Some(TokenKind::Greater)) => Some(cursor + 2),
1119        (Some(TokenKind::Minus | TokenKind::RightArrow), _) => Some(cursor + 1),
1120        _ => None,
1121    }
1122}
1123
1124fn matching_delimiter(
1125    tokens: &[&Token],
1126    start: usize,
1127    open: TokenKind,
1128    close: TokenKind,
1129) -> Option<usize> {
1130    if tokens.get(start)?.kind != open {
1131        return None;
1132    }
1133    let mut depth = 0usize;
1134    for (index, token) in tokens.iter().enumerate().skip(start) {
1135        if token.kind == open {
1136            depth += 1;
1137        } else if token.kind == close {
1138            depth = depth.saturating_sub(1);
1139            if depth == 0 {
1140                return Some(index);
1141            }
1142        }
1143    }
1144    None
1145}
1146
1147fn is_relationship_label_start(tokens: &[&Token], start: usize) -> bool {
1148    if tokens
1149        .get(start)
1150        .is_none_or(|token| token.kind != TokenKind::Colon)
1151    {
1152        return false;
1153    }
1154    match start.checked_sub(1).and_then(|index| tokens.get(index)) {
1155        Some(token) if token.kind == TokenKind::LeftBracket => true,
1156        Some(token) if is_symbolic_name_kind(token.kind) => {
1157            start >= 2 && tokens[start - 2].kind == TokenKind::LeftBracket
1158        }
1159        _ => false,
1160    }
1161}
1162
1163fn relationship_label_end(tokens: &[&Token], start: usize) -> Option<usize> {
1164    let end = label_predicate_end(tokens, start)?;
1165    if !(start + 1..=end).any(|index| tokens[index].kind == TokenKind::Colon) {
1166        return Some(end);
1167    }
1168
1169    let mut cursor = start + 1;
1170    loop {
1171        if cursor > end || !is_symbolic_name_kind(tokens[cursor].kind) {
1172            return None;
1173        }
1174        cursor += 1;
1175        if cursor > end {
1176            return Some(end);
1177        }
1178        if tokens[cursor].kind != TokenKind::Pipe
1179            || tokens
1180                .get(cursor + 1)
1181                .is_none_or(|token| token.kind != TokenKind::Colon)
1182        {
1183            return None;
1184        }
1185        cursor += 2;
1186    }
1187}
1188
1189fn is_label_predicate_context(tokens: &[&Token], start: usize) -> bool {
1190    if !matches!(
1191        tokens.get(start).map(|token| token.kind),
1192        Some(TokenKind::Colon | TokenKind::Keyword(Keyword::Is))
1193    ) {
1194        return false;
1195    }
1196    if tokens[start].kind == TokenKind::Keyword(Keyword::Is)
1197        && tokens
1198            .get(start + 1)
1199            .is_some_and(|token| token.kind == TokenKind::Keyword(Keyword::As))
1200        && tokens
1201            .get(start + 2)
1202            .is_some_and(|token| is_symbolic_name_kind(token.kind))
1203    {
1204        return false;
1205    }
1206    if tokens[start].kind == TokenKind::Colon
1207        && start >= 2
1208        && is_symbolic_name_kind(tokens[start - 1].kind)
1209        && matches!(tokens[start - 2].kind, TokenKind::LeftBrace)
1210    {
1211        return false;
1212    }
1213    if is_in_map_entry_value(tokens, start) {
1214        return true;
1215    }
1216    if tokens[start].kind == TokenKind::Colon
1217        && start >= 2
1218        && is_symbolic_name_kind(tokens[start - 1].kind)
1219        && tokens[start - 2].kind == TokenKind::Comma
1220        && is_inside_braces(tokens, start)
1221    {
1222        return false;
1223    }
1224
1225    // Ignore context markers inside a completed bracketed/braced construct.
1226    // In particular, a relationship-type `|` in `[:A|B]` must not make a
1227    // later node label look like an expression predicate. Markers in the
1228    // construct that currently encloses the candidate still have zero
1229    // relative depth and remain visible (for example a comprehension `|`).
1230    let mut nested_brackets = 0usize;
1231    let mut nested_braces = 0usize;
1232    for (index, token) in tokens[..start].iter().enumerate().rev() {
1233        match token.kind {
1234            TokenKind::RightBracket => {
1235                nested_brackets = nested_brackets.saturating_add(1);
1236                continue;
1237            }
1238            TokenKind::LeftBracket if nested_brackets != 0 => {
1239                nested_brackets = nested_brackets.saturating_sub(1);
1240                continue;
1241            }
1242            TokenKind::RightBrace => {
1243                nested_braces = nested_braces.saturating_add(1);
1244                continue;
1245            }
1246            TokenKind::LeftBrace if nested_braces != 0 => {
1247                nested_braces = nested_braces.saturating_sub(1);
1248                continue;
1249            }
1250            _ if nested_brackets != 0 || nested_braces != 0 => continue,
1251            _ => {}
1252        }
1253        if tokens
1254            .get(index + 1)
1255            .is_some_and(|next| next.kind == TokenKind::Equal)
1256        {
1257            continue;
1258        }
1259        match token.kind {
1260            TokenKind::Pipe => return true,
1261            TokenKind::Keyword(
1262                Keyword::Where
1263                | Keyword::Return
1264                | Keyword::With
1265                | Keyword::Unwind
1266                | Keyword::Delete
1267                | Keyword::Then
1268                | Keyword::Else
1269                | Keyword::When
1270                | Keyword::Case,
1271            ) => return true,
1272            TokenKind::Keyword(
1273                Keyword::Match
1274                | Keyword::Create
1275                | Keyword::Merge
1276                | Keyword::Call
1277                | Keyword::Set
1278                | Keyword::Remove,
1279            ) => return false,
1280            _ => {}
1281        }
1282    }
1283    false
1284}
1285
1286fn is_inside_braces(tokens: &[&Token], start: usize) -> bool {
1287    let mut nested = 0usize;
1288    for token in tokens[..start].iter().rev() {
1289        match token.kind {
1290            TokenKind::RightBrace => nested += 1,
1291            TokenKind::LeftBrace if nested == 0 => return true,
1292            TokenKind::LeftBrace => nested = nested.saturating_sub(1),
1293            _ => {}
1294        }
1295    }
1296    false
1297}
1298
1299fn is_in_map_entry_value(tokens: &[&Token], start: usize) -> bool {
1300    let mut open_braces = Vec::new();
1301    for (index, token) in tokens.iter().enumerate().take(start) {
1302        match token.kind {
1303            TokenKind::LeftBrace => open_braces.push(index),
1304            TokenKind::RightBrace => {
1305                let _ = open_braces.pop();
1306            }
1307            _ => {}
1308        }
1309    }
1310    let Some(open) = open_braces.last().copied() else {
1311        return false;
1312    };
1313
1314    let mut depth = 0usize;
1315    let mut after_separator = false;
1316    for token in &tokens[open + 1..start] {
1317        match token.kind {
1318            TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => {
1319                depth += 1;
1320            }
1321            TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => {
1322                depth = depth.saturating_sub(1);
1323            }
1324            TokenKind::Comma if depth == 0 => after_separator = false,
1325            TokenKind::Colon if depth == 0 => after_separator = true,
1326            _ => {}
1327        }
1328    }
1329    after_separator
1330}
1331
1332fn label_predicate_end(tokens: &[&Token], start: usize) -> Option<usize> {
1333    let expression_start = match tokens.get(start)?.kind {
1334        TokenKind::Colon => start + 1,
1335        TokenKind::Keyword(Keyword::Is) => {
1336            if (matches!(
1337                tokens.get(start + 1).map(|token| token.kind),
1338                Some(TokenKind::Keyword(Keyword::Null))
1339            ) && !matches!(
1340                tokens.get(start + 2).map(|token| token.kind),
1341                Some(TokenKind::Pipe | TokenKind::Ampersand)
1342            )) || matches!(
1343                (
1344                    tokens.get(start + 1).map(|token| token.kind),
1345                    tokens.get(start + 2).map(|token| token.kind),
1346                ),
1347                (
1348                    Some(TokenKind::Keyword(Keyword::Not)),
1349                    Some(TokenKind::Keyword(Keyword::Null)),
1350                )
1351            ) {
1352                return None;
1353            }
1354            start + 1
1355        }
1356        _ => return None,
1357    };
1358    let end = label_or_end(tokens, expression_start)?;
1359    Some(end.saturating_sub(1))
1360}
1361
1362fn label_or_end(tokens: &[&Token], start: usize) -> Option<usize> {
1363    let mut cursor = label_and_end(tokens, start)?;
1364    while tokens
1365        .get(cursor)
1366        .is_some_and(|token| token.kind == TokenKind::Pipe)
1367    {
1368        cursor += 1;
1369        if tokens
1370            .get(cursor)
1371            .is_some_and(|token| token.kind == TokenKind::Colon)
1372        {
1373            cursor += 1;
1374        }
1375        cursor = label_and_end(tokens, cursor)?;
1376    }
1377    Some(cursor)
1378}
1379
1380fn label_and_end(tokens: &[&Token], start: usize) -> Option<usize> {
1381    let mut cursor = label_primary_end(tokens, start)?;
1382    while matches!(
1383        tokens.get(cursor).map(|token| token.kind),
1384        Some(TokenKind::Ampersand | TokenKind::Colon)
1385    ) {
1386        cursor = label_primary_end(tokens, cursor + 1)?;
1387    }
1388    Some(cursor)
1389}
1390
1391fn label_primary_end(tokens: &[&Token], start: usize) -> Option<usize> {
1392    let mut cursor = start;
1393    if tokens
1394        .get(cursor)
1395        .is_some_and(|token| token.kind == TokenKind::Bang)
1396    {
1397        cursor += 1;
1398    }
1399    match tokens.get(cursor)?.kind {
1400        TokenKind::Percent => Some(cursor + 1),
1401        kind if is_symbolic_name_kind(kind) => Some(cursor + 1),
1402        TokenKind::LeftParen => {
1403            let end = label_or_end(tokens, cursor + 1)?;
1404            tokens
1405                .get(end)
1406                .is_some_and(|token| token.kind == TokenKind::RightParen)
1407                .then_some(end + 1)
1408        }
1409        _ => None,
1410    }
1411}
1412
1413#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1414pub(crate) enum ParserToken {
1415    // Only injected by recovery; never produced from source tokens.
1416    ErrorExpression,
1417    Identifier,
1418    EscapedIdentifier,
1419    Parameter,
1420    Integer,
1421    HexInteger,
1422    OctalInteger,
1423    Float,
1424    String,
1425    PatternComprehension,
1426    PatternExpression,
1427    LabelPredicate,
1428    RelationshipLabelExpression,
1429    QualifiedFunctionName,
1430    All,
1431    SetAll,
1432    AllShortestPaths,
1433    And,
1434    Any,
1435    As,
1436    Asc,
1437    Ascending,
1438    By,
1439    Call,
1440    Case,
1441    Contains,
1442    Collect,
1443    Count,
1444    Create,
1445    Delete,
1446    Desc,
1447    Descending,
1448    Detach,
1449    Distinct,
1450    Else,
1451    End,
1452    Ends,
1453    Exists,
1454    False,
1455    Group,
1456    Groups,
1457    In,
1458    Inf,
1459    Infinity,
1460    Is,
1461    Limit,
1462    Match,
1463    Merge,
1464    Nan,
1465    None,
1466    Not,
1467    Null,
1468    Offset,
1469    On,
1470    Optional,
1471    Or,
1472    Order,
1473    Path,
1474    Paths,
1475    Reduce,
1476    Remove,
1477    Return,
1478    Set,
1479    Shortest,
1480    ShortestPath,
1481    Simple,
1482    Single,
1483    Skip,
1484    Starts,
1485    Then,
1486    Trail,
1487    Trim,
1488    True,
1489    Union,
1490    Unwind,
1491    Walk,
1492    When,
1493    Where,
1494    With,
1495    Xor,
1496    Yield,
1497    Acyclic,
1498    LeftParen,
1499    RightParen,
1500    LeftBracket,
1501    RightBracket,
1502    LeftBrace,
1503    RightBrace,
1504    Comma,
1505    Dot,
1506    DotDot,
1507    Colon,
1508    DoubleColon,
1509    Semicolon,
1510    Pipe,
1511    DoublePipe,
1512    Ampersand,
1513    Question,
1514    Dollar,
1515    Plus,
1516    Minus,
1517    Star,
1518    Slash,
1519    Percent,
1520    Caret,
1521    Bang,
1522    Equal,
1523    NotEqual,
1524    Less,
1525    LessEqual,
1526    Greater,
1527    GreaterEqual,
1528    PlusEqual,
1529    FatArrow,
1530    RegexMatch,
1531    LeftArrow,
1532    RightArrow,
1533    Invalid,
1534}
1535
1536impl fmt::Display for ParserToken {
1537    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1538        write!(formatter, "`{}`", self.name())
1539    }
1540}
1541
1542impl ParserToken {
1543    const fn is_keyword_literal(self) -> bool {
1544        matches!(
1545            self,
1546            Self::False | Self::Inf | Self::Infinity | Self::Nan | Self::Null | Self::True
1547        )
1548    }
1549
1550    const fn is_contextual_name_candidate(self) -> bool {
1551        !matches!(
1552            self,
1553            Self::Identifier
1554                | Self::ErrorExpression
1555                | Self::EscapedIdentifier
1556                | Self::Parameter
1557                | Self::Integer
1558                | Self::HexInteger
1559                | Self::OctalInteger
1560                | Self::Float
1561                | Self::String
1562                | Self::PatternComprehension
1563                | Self::PatternExpression
1564                | Self::LabelPredicate
1565                | Self::RelationshipLabelExpression
1566                | Self::QualifiedFunctionName
1567                | Self::LeftParen
1568                | Self::RightParen
1569                | Self::LeftBracket
1570                | Self::RightBracket
1571                | Self::LeftBrace
1572                | Self::RightBrace
1573                | Self::Comma
1574                | Self::Dot
1575                | Self::DotDot
1576                | Self::Colon
1577                | Self::DoubleColon
1578                | Self::Semicolon
1579                | Self::Pipe
1580                | Self::DoublePipe
1581                | Self::Ampersand
1582                | Self::Question
1583                | Self::Dollar
1584                | Self::Plus
1585                | Self::Minus
1586                | Self::Star
1587                | Self::Slash
1588                | Self::Percent
1589                | Self::Caret
1590                | Self::Bang
1591                | Self::Equal
1592                | Self::NotEqual
1593                | Self::Less
1594                | Self::LessEqual
1595                | Self::Greater
1596                | Self::GreaterEqual
1597                | Self::PlusEqual
1598                | Self::FatArrow
1599                | Self::RegexMatch
1600                | Self::LeftArrow
1601                | Self::RightArrow
1602                | Self::Invalid
1603        )
1604    }
1605
1606    const fn name(self) -> &'static str {
1607        match self {
1608            Self::ErrorExpression => "error expression",
1609            Self::Identifier => "identifier",
1610            Self::EscapedIdentifier => "escaped identifier",
1611            Self::Parameter => "parameter",
1612            Self::Integer => "integer",
1613            Self::HexInteger => "hex integer",
1614            Self::OctalInteger => "octal integer",
1615            Self::Float => "float",
1616            Self::String => "string",
1617            Self::PatternComprehension => "pattern comprehension",
1618            Self::PatternExpression => "pattern expression",
1619            Self::LabelPredicate => "label predicate",
1620            Self::RelationshipLabelExpression => "relationship label expression",
1621            Self::QualifiedFunctionName => "qualified function name",
1622            Self::All => "ALL",
1623            Self::SetAll => "ALL",
1624            Self::AllShortestPaths => "ALLSHORTESTPATHS",
1625            Self::And => "AND",
1626            Self::Any => "ANY",
1627            Self::As => "AS",
1628            Self::Asc => "ASC",
1629            Self::Ascending => "ASCENDING",
1630            Self::By => "BY",
1631            Self::Call => "CALL",
1632            Self::Case => "CASE",
1633            Self::Contains => "CONTAINS",
1634            Self::Collect => "COLLECT",
1635            Self::Count => "COUNT",
1636            Self::Create => "CREATE",
1637            Self::Delete => "DELETE",
1638            Self::Desc => "DESC",
1639            Self::Descending => "DESCENDING",
1640            Self::Detach => "DETACH",
1641            Self::Distinct => "DISTINCT",
1642            Self::Else => "ELSE",
1643            Self::End => "END",
1644            Self::Ends => "ENDS",
1645            Self::Exists => "EXISTS",
1646            Self::False => "FALSE",
1647            Self::Group => "GROUP",
1648            Self::Groups => "GROUPS",
1649            Self::In => "IN",
1650            Self::Inf => "INF",
1651            Self::Infinity => "INFINITY",
1652            Self::Is => "IS",
1653            Self::Limit => "LIMIT",
1654            Self::Match => "MATCH",
1655            Self::Merge => "MERGE",
1656            Self::Nan => "NAN",
1657            Self::None => "NONE",
1658            Self::Not => "NOT",
1659            Self::Null => "NULL",
1660            Self::Offset => "OFFSET",
1661            Self::On => "ON",
1662            Self::Optional => "OPTIONAL",
1663            Self::Or => "OR",
1664            Self::Order => "ORDER",
1665            Self::Path => "PATH",
1666            Self::Paths => "PATHS",
1667            Self::Reduce => "REDUCE",
1668            Self::Remove => "REMOVE",
1669            Self::Return => "RETURN",
1670            Self::Set => "SET",
1671            Self::Shortest => "SHORTEST",
1672            Self::ShortestPath => "SHORTESTPATH",
1673            Self::Simple => "SIMPLE",
1674            Self::Single => "SINGLE",
1675            Self::Skip => "SKIP",
1676            Self::Starts => "STARTS",
1677            Self::Then => "THEN",
1678            Self::Trail => "TRAIL",
1679            Self::Trim => "TRIM",
1680            Self::True => "TRUE",
1681            Self::Union => "UNION",
1682            Self::Unwind => "UNWIND",
1683            Self::Walk => "WALK",
1684            Self::When => "WHEN",
1685            Self::Where => "WHERE",
1686            Self::With => "WITH",
1687            Self::Xor => "XOR",
1688            Self::Yield => "YIELD",
1689            Self::Acyclic => "ACYCLIC",
1690            Self::LeftParen => "(",
1691            Self::RightParen => ")",
1692            Self::LeftBracket => "[",
1693            Self::RightBracket => "]",
1694            Self::LeftBrace => "{",
1695            Self::RightBrace => "}",
1696            Self::Comma => ",",
1697            Self::Dot => ".",
1698            Self::DotDot => "..",
1699            Self::Colon => ":",
1700            Self::DoubleColon => "::",
1701            Self::Semicolon => ";",
1702            Self::Pipe => "|",
1703            Self::DoublePipe => "||",
1704            Self::Ampersand => "&",
1705            Self::Question => "?",
1706            Self::Dollar => "$",
1707            Self::Plus => "+",
1708            Self::Minus => "-",
1709            Self::Star => "*",
1710            Self::Slash => "/",
1711            Self::Percent => "%",
1712            Self::Caret => "^",
1713            Self::Bang => "!",
1714            Self::Equal => "=",
1715            Self::NotEqual => "<>",
1716            Self::Less => "<",
1717            Self::LessEqual => "<=",
1718            Self::Greater => ">",
1719            Self::GreaterEqual => ">=",
1720            Self::PlusEqual => "+=",
1721            Self::FatArrow => "=>",
1722            Self::RegexMatch => "=~",
1723            Self::LeftArrow => "<-",
1724            Self::RightArrow => "->",
1725            Self::Invalid => "invalid token",
1726        }
1727    }
1728}
1729
1730impl From<TokenKind> for ParserToken {
1731    fn from(kind: TokenKind) -> Self {
1732        match kind {
1733            TokenKind::Keyword(keyword) => keyword_token(keyword),
1734            TokenKind::Identifier => Self::Identifier,
1735            TokenKind::EscapedIdentifier => Self::EscapedIdentifier,
1736            TokenKind::Parameter => Self::Parameter,
1737            TokenKind::Integer => Self::Integer,
1738            TokenKind::HexInteger => Self::HexInteger,
1739            TokenKind::OctalInteger => Self::OctalInteger,
1740            TokenKind::Float => Self::Float,
1741            TokenKind::String => Self::String,
1742            TokenKind::LeftParen => Self::LeftParen,
1743            TokenKind::RightParen => Self::RightParen,
1744            TokenKind::LeftBracket => Self::LeftBracket,
1745            TokenKind::RightBracket => Self::RightBracket,
1746            TokenKind::LeftBrace => Self::LeftBrace,
1747            TokenKind::RightBrace => Self::RightBrace,
1748            TokenKind::Comma => Self::Comma,
1749            TokenKind::Dot => Self::Dot,
1750            TokenKind::DotDot => Self::DotDot,
1751            TokenKind::Colon => Self::Colon,
1752            TokenKind::DoubleColon => Self::DoubleColon,
1753            TokenKind::Semicolon => Self::Semicolon,
1754            TokenKind::Pipe => Self::Pipe,
1755            TokenKind::DoublePipe => Self::DoublePipe,
1756            TokenKind::Ampersand => Self::Ampersand,
1757            TokenKind::Question => Self::Question,
1758            TokenKind::Dollar => Self::Dollar,
1759            TokenKind::Plus => Self::Plus,
1760            TokenKind::Minus => Self::Minus,
1761            TokenKind::Star => Self::Star,
1762            TokenKind::Slash => Self::Slash,
1763            TokenKind::Percent => Self::Percent,
1764            TokenKind::Caret => Self::Caret,
1765            TokenKind::Bang => Self::Bang,
1766            TokenKind::Equal => Self::Equal,
1767            TokenKind::NotEqual => Self::NotEqual,
1768            TokenKind::Less => Self::Less,
1769            TokenKind::LessEqual => Self::LessEqual,
1770            TokenKind::Greater => Self::Greater,
1771            TokenKind::GreaterEqual => Self::GreaterEqual,
1772            TokenKind::PlusEqual => Self::PlusEqual,
1773            TokenKind::FatArrow => Self::FatArrow,
1774            TokenKind::RegexMatch => Self::RegexMatch,
1775            TokenKind::LeftArrow => Self::LeftArrow,
1776            TokenKind::RightArrow => Self::RightArrow,
1777            TokenKind::Invalid => Self::Invalid,
1778            TokenKind::Whitespace | TokenKind::LineComment | TokenKind::BlockComment => {
1779                unreachable!("trivia is filtered before parser token conversion")
1780            }
1781        }
1782    }
1783}
1784
1785fn keyword_token(keyword: Keyword) -> ParserToken {
1786    match keyword {
1787        Keyword::All => ParserToken::All,
1788        Keyword::AllShortestPaths => ParserToken::AllShortestPaths,
1789        Keyword::And => ParserToken::And,
1790        Keyword::Any => ParserToken::Any,
1791        Keyword::As => ParserToken::As,
1792        Keyword::Asc => ParserToken::Asc,
1793        Keyword::Ascending => ParserToken::Ascending,
1794        Keyword::By => ParserToken::By,
1795        Keyword::Call => ParserToken::Call,
1796        Keyword::Case => ParserToken::Case,
1797        Keyword::Contains => ParserToken::Contains,
1798        Keyword::Collect => ParserToken::Collect,
1799        Keyword::Count => ParserToken::Count,
1800        Keyword::Create => ParserToken::Create,
1801        Keyword::Delete => ParserToken::Delete,
1802        Keyword::Desc => ParserToken::Desc,
1803        Keyword::Descending => ParserToken::Descending,
1804        Keyword::Detach => ParserToken::Detach,
1805        Keyword::Distinct => ParserToken::Distinct,
1806        Keyword::Else => ParserToken::Else,
1807        Keyword::End => ParserToken::End,
1808        Keyword::Ends => ParserToken::Ends,
1809        Keyword::Exists => ParserToken::Exists,
1810        Keyword::False => ParserToken::False,
1811        Keyword::Group => ParserToken::Group,
1812        Keyword::Groups => ParserToken::Groups,
1813        Keyword::In => ParserToken::In,
1814        Keyword::Inf => ParserToken::Inf,
1815        Keyword::Infinity => ParserToken::Infinity,
1816        Keyword::Is => ParserToken::Is,
1817        Keyword::Limit => ParserToken::Limit,
1818        Keyword::Match => ParserToken::Match,
1819        Keyword::Merge => ParserToken::Merge,
1820        Keyword::Nan => ParserToken::Nan,
1821        Keyword::None => ParserToken::None,
1822        Keyword::Not => ParserToken::Not,
1823        Keyword::Null => ParserToken::Null,
1824        Keyword::Offset => ParserToken::Offset,
1825        Keyword::On => ParserToken::On,
1826        Keyword::Optional => ParserToken::Optional,
1827        Keyword::Or => ParserToken::Or,
1828        Keyword::Order => ParserToken::Order,
1829        Keyword::Path => ParserToken::Path,
1830        Keyword::Paths => ParserToken::Paths,
1831        Keyword::Reduce => ParserToken::Reduce,
1832        Keyword::Remove => ParserToken::Remove,
1833        Keyword::Return => ParserToken::Return,
1834        Keyword::Set => ParserToken::Set,
1835        Keyword::Shortest => ParserToken::Shortest,
1836        Keyword::ShortestPath => ParserToken::ShortestPath,
1837        Keyword::Simple => ParserToken::Simple,
1838        Keyword::Single => ParserToken::Single,
1839        Keyword::Skip => ParserToken::Skip,
1840        Keyword::Starts => ParserToken::Starts,
1841        Keyword::Then => ParserToken::Then,
1842        Keyword::Trail => ParserToken::Trail,
1843        Keyword::Trim => ParserToken::Trim,
1844        Keyword::True => ParserToken::True,
1845        Keyword::Union => ParserToken::Union,
1846        Keyword::Unwind => ParserToken::Unwind,
1847        Keyword::Walk => ParserToken::Walk,
1848        Keyword::When => ParserToken::When,
1849        Keyword::Where => ParserToken::Where,
1850        Keyword::With => ParserToken::With,
1851        Keyword::Xor => ParserToken::Xor,
1852        Keyword::Yield => ParserToken::Yield,
1853        Keyword::Acyclic => ParserToken::Acyclic,
1854    }
1855}
1856
1857pub(crate) fn name(source: &str, start: usize, end: usize, escaped: bool) -> Name {
1858    let span = Span::new(start, end);
1859    let raw = &source[start..end];
1860    let text = if escaped {
1861        decode_delimited(raw, '`')
1862    } else {
1863        raw.to_owned()
1864    };
1865    Node::new(Identifier::new(text, escaped), span)
1866}
1867
1868pub(crate) fn qualified_function_name(source: &str, start: usize, end: usize) -> QualifiedName {
1869    let fragment = source.get(start..end).unwrap_or_default();
1870    let lexed = lexer::lex(fragment);
1871    let parts = lexed
1872        .tokens
1873        .iter()
1874        .filter(|token| !token.is_trivia() && token.kind != TokenKind::Dot)
1875        .map(|token| {
1876            let part_start = start + token.span.start;
1877            let part_end = start + token.span.end;
1878            name(
1879                source,
1880                part_start,
1881                part_end,
1882                token.kind == TokenKind::EscapedIdentifier,
1883            )
1884        })
1885        .collect();
1886    QualifiedName {
1887        parts,
1888        span: Span::new(start, end),
1889    }
1890}
1891
1892pub(crate) fn parameter(source: &str, start: usize, end: usize) -> Parameter {
1893    let raw = &source[start + 1..end];
1894    let name = if raw.starts_with('`') && raw.ends_with('`') {
1895        ParameterName::Named(decode_delimited(raw, '`'))
1896    } else if raw.bytes().all(|byte| byte.is_ascii_digit()) {
1897        ParameterName::Positional(raw.to_owned())
1898    } else {
1899        ParameterName::Named(raw.to_owned())
1900    };
1901    Parameter { name }
1902}
1903
1904pub(crate) fn string_literal(source: &str, start: usize, end: usize) -> StringLiteral {
1905    let raw = &source[start..end];
1906    let delimiter = raw.chars().next().unwrap_or('\'');
1907    let quote = if delimiter == '\'' {
1908        QuoteStyle::Single
1909    } else {
1910        QuoteStyle::Double
1911    };
1912    StringLiteral {
1913        value: decode_delimited(raw, delimiter),
1914        quote,
1915    }
1916}
1917
1918fn decode_delimited(raw: &str, delimiter: char) -> String {
1919    let delimiter_len = delimiter.len_utf8();
1920    let inner = raw
1921        .get(delimiter_len..raw.len().saturating_sub(delimiter_len))
1922        .unwrap_or_default();
1923    let mut decoded = String::with_capacity(inner.len());
1924    let mut characters = inner.chars().peekable();
1925    while let Some(character) = characters.next() {
1926        if character == delimiter && characters.peek() == Some(&delimiter) {
1927            let _ = characters.next();
1928            decoded.push(delimiter);
1929            continue;
1930        }
1931        if character != '\\' {
1932            decoded.push(character);
1933            continue;
1934        }
1935        let Some(escaped) = characters.next() else {
1936            decoded.push('\\');
1937            break;
1938        };
1939        match escaped {
1940            't' => decoded.push('\t'),
1941            'b' => decoded.push('\u{0008}'),
1942            'n' => decoded.push('\n'),
1943            'r' => decoded.push('\r'),
1944            'f' => decoded.push('\u{000c}'),
1945            '\\' => decoded.push('\\'),
1946            '\'' => decoded.push('\''),
1947            '"' => decoded.push('"'),
1948            '`' => decoded.push('`'),
1949            'u' | 'U' => {
1950                let digits = if escaped == 'u' { 4 } else { 6 };
1951                let mut scalar = 0u32;
1952                let mut complete = true;
1953                for _ in 0..digits {
1954                    match characters.next().and_then(|digit| digit.to_digit(16)) {
1955                        Some(digit) => scalar = scalar * 16 + digit,
1956                        None => {
1957                            complete = false;
1958                            break;
1959                        }
1960                    }
1961                }
1962                if complete && let Some(character) = char::from_u32(scalar) {
1963                    decoded.push(character);
1964                }
1965            }
1966            other => {
1967                // Invalid escapes are rejected by the lexer, so preserving an
1968                // unexpected spelling here is only a defensive fallback.
1969                decoded.push('\\');
1970                decoded.push(other);
1971            }
1972        }
1973    }
1974    decoded
1975}
1976
1977pub(crate) fn integer_literal(
1978    source: &str,
1979    start: usize,
1980    end: usize,
1981    radix: IntegerRadix,
1982) -> Literal {
1983    let span = Span::new(start, end);
1984    Node::new(
1985        LiteralKind::Integer(IntegerLiteral {
1986            text: source[start..end].to_owned(),
1987            radix,
1988        }),
1989        span,
1990    )
1991}
1992
1993pub(crate) fn list_singleton_or_comprehension(start: usize, end: usize, expression: Expr) -> Expr {
1994    let span = Span::new(start, end);
1995    match split_comprehension_source(expression) {
1996        ComprehensionSource::Comprehension { variable, list } => Node::new(
1997            ExprKind::ListComprehension(ListComprehension {
1998                variable,
1999                list: Box::new(list),
2000                predicate: None,
2001                projection: None,
2002            }),
2003            span,
2004        ),
2005        ComprehensionSource::Expression(expression) => {
2006            Node::new(ExprKind::List(vec![expression]), span)
2007        }
2008    }
2009}
2010
2011pub(crate) fn list_comprehension_expr(
2012    start: usize,
2013    end: usize,
2014    source_expression: Expr,
2015    predicate: Option<Expr>,
2016    projection: Option<Expr>,
2017) -> Result<Expr, LalrpopError> {
2018    let ComprehensionSource::Comprehension { variable, list } =
2019        split_comprehension_source(source_expression)
2020    else {
2021        return Err(user_parse_error(start));
2022    };
2023    Ok(Node::new(
2024        ExprKind::ListComprehension(ListComprehension {
2025            variable,
2026            list: Box::new(list),
2027            predicate: predicate.map(Box::new),
2028            projection: projection.map(Box::new),
2029        }),
2030        Span::new(start, end),
2031    ))
2032}
2033
2034enum ComprehensionSource {
2035    Comprehension { variable: Name, list: Expr },
2036    Expression(Expr),
2037}
2038
2039fn split_comprehension_source(expression: Expr) -> ComprehensionSource {
2040    let is_source = matches!(
2041        &expression.kind,
2042        ExprKind::Binary { left, operator, .. }
2043            if operator.kind == BinaryOperator::In
2044                && matches!(&left.kind, ExprKind::Variable(_))
2045    );
2046    if !is_source {
2047        return ComprehensionSource::Expression(expression);
2048    }
2049
2050    let ExprKind::Binary {
2051        left,
2052        operator: _,
2053        right,
2054    } = expression.kind
2055    else {
2056        unreachable!("the source shape was checked above")
2057    };
2058    let ExprKind::Variable(variable) = left.kind else {
2059        unreachable!("the source variable shape was checked above")
2060    };
2061    ComprehensionSource::Comprehension {
2062        variable,
2063        list: *right,
2064    }
2065}
2066
2067pub(crate) fn pattern_comprehension(
2068    source: &str,
2069    start: usize,
2070    end: usize,
2071) -> Result<Expr, LalrpopError> {
2072    let Some(fragment) = source.get(start..end) else {
2073        return Err(user_parse_error(start));
2074    };
2075    let lexed = lexer::lex(fragment);
2076    if !lexed.diagnostics.is_empty() {
2077        return Err(user_parse_error(start));
2078    }
2079    // A `|` at the comprehension's top level may be either the projection
2080    // separator or part of a label disjunction (for example `b:X|Y`). Try
2081    // each candidate as the separator, left to right; reserving it keeps the
2082    // label scanner from consuming it, and the first split that yields a
2083    // valid comprehension wins.
2084    // When every split fails, report the one that got farthest: it is the
2085    // most plausible reading, and its error points into the fragment instead
2086    // of at the comprehension as a whole.
2087    let mut best_error = None;
2088    for pipe_index in top_level_pipe_candidates(&lexed.tokens) {
2089        let tokens = nested_parser_tokens(&lexed.tokens, start, Some(pipe_index));
2090        match parse_comprehension_tokens(source, start, end, &tokens) {
2091            Ok(expression) => return Ok(expression),
2092            Err(error) => retain_farthest_error(&mut best_error, error),
2093        }
2094    }
2095    Err(best_error.unwrap_or_else(|| user_parse_error(start)))
2096}
2097
2098fn top_level_pipe_candidates(tokens: &[Token]) -> Vec<usize> {
2099    let mut candidates = Vec::new();
2100    let mut brackets = 0usize;
2101    let mut parentheses = 0usize;
2102    let mut braces = 0usize;
2103    for (index, token) in tokens.iter().enumerate() {
2104        if token.is_trivia() {
2105            continue;
2106        }
2107        match token.kind {
2108            TokenKind::LeftBracket => brackets += 1,
2109            TokenKind::RightBracket => brackets = brackets.saturating_sub(1),
2110            TokenKind::LeftParen => parentheses += 1,
2111            TokenKind::RightParen => parentheses = parentheses.saturating_sub(1),
2112            TokenKind::LeftBrace => braces += 1,
2113            TokenKind::RightBrace => braces = braces.saturating_sub(1),
2114            TokenKind::Pipe if brackets == 1 && parentheses == 0 && braces == 0 => {
2115                candidates.push(index);
2116            }
2117            _ => {}
2118        }
2119    }
2120    candidates
2121}
2122
2123fn parse_comprehension_tokens(
2124    source: &str,
2125    start: usize,
2126    end: usize,
2127    tokens: &[SpannedParserToken],
2128) -> Result<Expr, LalrpopError> {
2129    let outer_start = tokens
2130        .iter()
2131        .position(|token| token.0 == start && token.1 == ParserToken::LeftBracket)
2132        .ok_or_else(|| user_parse_error(start))?;
2133    let outer_end = tokens
2134        .iter()
2135        .rposition(|token| token.2 == end && token.1 == ParserToken::RightBracket)
2136        .ok_or_else(|| user_parse_error(start))?;
2137
2138    let pipe = find_top_level_token(tokens, outer_start + 1, outer_end, ParserToken::Pipe)
2139        .ok_or_else(|| user_parse_error(start))?;
2140
2141    let mut pattern_start = outer_start + 1;
2142    let binding = if pattern_start + 1 < pipe
2143        && tokens[pattern_start + 1].1 == ParserToken::Equal
2144        && (is_name_token(tokens[pattern_start].1)
2145            || tokens[pattern_start].1.is_contextual_name_candidate())
2146    {
2147        let token = tokens[pattern_start];
2148        pattern_start += 2;
2149        Some(name(
2150            source,
2151            token.0,
2152            token.2,
2153            token.1 == ParserToken::EscapedIdentifier,
2154        ))
2155    } else {
2156        None
2157    };
2158
2159    let where_index = find_top_level_token(tokens, pattern_start, pipe, ParserToken::Where);
2160    let pattern_end = where_index.unwrap_or(pipe);
2161    if pattern_start >= pattern_end {
2162        return Err(user_parse_error(tokens[pattern_end].0));
2163    }
2164    if pipe + 1 >= outer_end {
2165        return Err(user_parse_error(tokens[outer_end].0));
2166    }
2167    let pattern = parse_pattern_fragment(source, &tokens[pattern_start..pattern_end])?;
2168    let predicate = match where_index {
2169        Some(index) if index + 1 < pipe => Some(Box::new(parse_expr_fragment(
2170            source,
2171            &tokens[index + 1..pipe],
2172        )?)),
2173        Some(_) => return Err(user_parse_error(tokens[pipe].0)),
2174        None => None,
2175    };
2176    let projection = parse_expr_fragment(source, &tokens[pipe + 1..outer_end])?;
2177
2178    Ok(Node::new(
2179        ExprKind::PatternComprehension(PatternComprehension {
2180            binding,
2181            pattern,
2182            predicate,
2183            projection: Box::new(projection),
2184        }),
2185        Span::new(start, end),
2186    ))
2187}
2188
2189pub(crate) fn pattern_expression(
2190    source: &str,
2191    start: usize,
2192    end: usize,
2193) -> Result<Expr, LalrpopError> {
2194    let Some(fragment) = source.get(start..end) else {
2195        return Err(user_parse_error(start));
2196    };
2197    let lexed = lexer::lex(fragment);
2198    if !lexed.diagnostics.is_empty() {
2199        return Err(user_parse_error(start));
2200    }
2201    let tokens = nested_parser_tokens(&lexed.tokens, start, None);
2202    let pattern = parse_pattern_fragment(source, &tokens)?;
2203    Ok(Node::new(ExprKind::Pattern(pattern), Span::new(start, end)))
2204}
2205
2206fn parse_pattern_fragment(
2207    source: &str,
2208    tokens: &[SpannedParserToken],
2209) -> Result<Pattern, LalrpopError> {
2210    parse_tokens_with_contextual_names(tokens, |contextual| {
2211        generated::PatternFragmentParser::new().parse(source, contextual.iter().copied().map(Ok))
2212    })
2213}
2214
2215fn parse_expr_fragment(source: &str, tokens: &[SpannedParserToken]) -> Result<Expr, LalrpopError> {
2216    parse_tokens_with_contextual_names(tokens, |contextual| {
2217        generated::ExprFragmentParser::new().parse(source, contextual.iter().copied().map(Ok))
2218    })
2219}
2220
2221pub(crate) fn label_comparison_suffix(
2222    source: &str,
2223    start: usize,
2224    end: usize,
2225) -> Result<ComparisonSuffix, LalrpopError> {
2226    Ok(ComparisonSuffix::Is {
2227        negated: false,
2228        predicate: IsPredicate::Label(parse_label_expression(source, start, end, true)?),
2229        end,
2230    })
2231}
2232
2233pub(crate) fn relationship_label_expression(
2234    source: &str,
2235    start: usize,
2236    end: usize,
2237) -> Result<LabelExpression, LalrpopError> {
2238    parse_label_expression(source, start, end, true)
2239}
2240
2241fn parse_label_expression(
2242    source: &str,
2243    start: usize,
2244    end: usize,
2245    normalize_legacy_relationship: bool,
2246) -> Result<LabelExpression, LalrpopError> {
2247    let Some(fragment) = source.get(start..end) else {
2248        return Err(user_parse_error(start));
2249    };
2250    let lexed = lexer::lex(fragment);
2251    if !lexed.diagnostics.is_empty() {
2252        return Err(user_parse_error(start));
2253    }
2254    let mut tokens = lexed
2255        .tokens
2256        .iter()
2257        .filter(|token| !token.is_trivia())
2258        .map(|token| {
2259            (
2260                start + token.span.start,
2261                ParserToken::from(token.kind),
2262                start + token.span.end,
2263            )
2264        })
2265        .collect::<Vec<_>>();
2266    if normalize_legacy_relationship && !legacy_label_tokens_are_consistent(&tokens) {
2267        return Err(user_parse_error(start));
2268    }
2269    let mut normalized = Vec::with_capacity(tokens.len());
2270    for (index, mut token) in tokens.drain(..).enumerate() {
2271        if normalize_legacy_relationship
2272            && token.1 == ParserToken::Colon
2273            && normalized
2274                .last()
2275                .is_some_and(|previous: &SpannedParserToken| previous.1 == ParserToken::Pipe)
2276        {
2277            continue;
2278        }
2279        if index > 0 && token.1.is_contextual_name_candidate() {
2280            token.1 = ParserToken::Identifier;
2281        }
2282        normalized.push(token);
2283    }
2284    generated::LabelPredicateFragmentParser::new().parse(source, normalized.into_iter().map(Ok))
2285}
2286
2287fn legacy_label_tokens_are_consistent(tokens: &[SpannedParserToken]) -> bool {
2288    let has_internal_colon = tokens
2289        .iter()
2290        .enumerate()
2291        .skip(1)
2292        .any(|(_, token)| token.1 == ParserToken::Colon);
2293    if !has_internal_colon {
2294        return true;
2295    }
2296    if tokens
2297        .first()
2298        .is_none_or(|token| token.1 != ParserToken::Colon)
2299    {
2300        return false;
2301    }
2302
2303    let mut cursor = 1usize;
2304    if tokens
2305        .get(cursor)
2306        .is_none_or(|token| !is_name_token(token.1) && !token.1.is_contextual_name_candidate())
2307    {
2308        return false;
2309    }
2310    cursor += 1;
2311    let pipe_separated = tokens
2312        .get(cursor)
2313        .is_some_and(|token| token.1 == ParserToken::Pipe);
2314    while cursor < tokens.len() {
2315        if pipe_separated {
2316            if tokens
2317                .get(cursor)
2318                .is_none_or(|token| token.1 != ParserToken::Pipe)
2319                || tokens
2320                    .get(cursor + 1)
2321                    .is_none_or(|token| token.1 != ParserToken::Colon)
2322            {
2323                return false;
2324            }
2325            cursor += 2;
2326        } else {
2327            if tokens
2328                .get(cursor)
2329                .is_none_or(|token| token.1 != ParserToken::Colon)
2330            {
2331                return false;
2332            }
2333            cursor += 1;
2334        }
2335        if tokens
2336            .get(cursor)
2337            .is_none_or(|token| !is_name_token(token.1) && !token.1.is_contextual_name_candidate())
2338        {
2339            return false;
2340        }
2341        cursor += 1;
2342    }
2343    true
2344}
2345
2346fn user_parse_error(location: usize) -> LalrpopError {
2347    ParseError::User { error: location }
2348}
2349
2350/// `reserved_pipe` is a raw index into `tokens` naming a `|` that must come
2351/// through as a plain `Pipe` token (a pattern comprehension's projection
2352/// separator): forward scans starting before it see a truncated stream so
2353/// they cannot consume it, while context checks keep the full view.
2354fn nested_parser_tokens(
2355    tokens: &[Token],
2356    base: usize,
2357    reserved_pipe: Option<usize>,
2358) -> Vec<SpannedParserToken> {
2359    let significant = tokens
2360        .iter()
2361        .filter(|token| !token.is_trivia())
2362        .collect::<Vec<_>>();
2363    let boundary = reserved_pipe.map(|raw| {
2364        tokens[..raw]
2365            .iter()
2366            .filter(|token| !token.is_trivia())
2367            .count()
2368    });
2369    let mut result = Vec::with_capacity(significant.len());
2370    let mut index = 0;
2371    while index < significant.len() {
2372        let scan: &[&Token] = match boundary {
2373            Some(boundary) if index < boundary => &significant[..boundary],
2374            _ => &significant,
2375        };
2376        if let Some(end) = escaped_parameter_end(scan, index) {
2377            result.push((
2378                base + significant[index].span.start,
2379                ParserToken::Parameter,
2380                base + significant[end].span.end,
2381            ));
2382            index = end + 1;
2383            continue;
2384        }
2385        if index != 0 {
2386            if let Some(end) = pattern_comprehension_end(scan, index) {
2387                result.push((
2388                    base + significant[index].span.start,
2389                    ParserToken::PatternComprehension,
2390                    base + significant[end].span.end,
2391                ));
2392                index = end + 1;
2393                continue;
2394            }
2395            if significant[index].kind == TokenKind::LeftParen
2396                && is_pattern_expression_context(&significant, index)
2397                && let Some(end) = pattern_expression_end(scan, index)
2398            {
2399                result.push((
2400                    base + significant[index].span.start,
2401                    ParserToken::PatternExpression,
2402                    base + significant[end].span.end,
2403                ));
2404                index = end + 1;
2405                continue;
2406            }
2407            if is_relationship_label_start(&significant, index)
2408                && let Some(end) = relationship_label_end(scan, index)
2409            {
2410                result.push((
2411                    base + significant[index].span.start,
2412                    ParserToken::RelationshipLabelExpression,
2413                    base + significant[end].span.end,
2414                ));
2415                index = end + 1;
2416                continue;
2417            }
2418            if is_label_predicate_context(&significant, index)
2419                && let Some(end) = label_predicate_end(scan, index)
2420            {
2421                result.push((
2422                    base + significant[index].span.start,
2423                    ParserToken::LabelPredicate,
2424                    base + significant[end].span.end,
2425                ));
2426                index = end + 1;
2427                continue;
2428            }
2429        }
2430
2431        if !is_procedure_name_start(&significant, index)
2432            && let Some(end) = qualified_function_name_end(scan, index)
2433        {
2434            result.push((
2435                base + significant[index].span.start,
2436                ParserToken::QualifiedFunctionName,
2437                base + significant[end].span.end,
2438            ));
2439            index = end + 1;
2440            continue;
2441        }
2442
2443        let token = significant[index];
2444        let mut parser_token = ParserToken::from(token.kind);
2445        if parser_token == ParserToken::All && is_explicit_set_all(&significant, index) {
2446            parser_token = ParserToken::SetAll;
2447        }
2448        result.push((base + token.span.start, parser_token, base + token.span.end));
2449        index += 1;
2450    }
2451    result
2452}
2453
2454fn find_top_level_token(
2455    tokens: &[SpannedParserToken],
2456    start: usize,
2457    end: usize,
2458    needle: ParserToken,
2459) -> Option<usize> {
2460    let mut parentheses = 0usize;
2461    let mut brackets = 0usize;
2462    let mut braces = 0usize;
2463    for (index, (_, token, _)) in tokens.iter().copied().enumerate().take(end).skip(start) {
2464        if parentheses == 0 && brackets == 0 && braces == 0 && token == needle {
2465            return Some(index);
2466        }
2467        match token {
2468            ParserToken::LeftParen => parentheses += 1,
2469            ParserToken::RightParen => parentheses = parentheses.saturating_sub(1),
2470            ParserToken::LeftBracket => brackets += 1,
2471            ParserToken::RightBracket => brackets = brackets.saturating_sub(1),
2472            ParserToken::LeftBrace => braces += 1,
2473            ParserToken::RightBrace => braces = braces.saturating_sub(1),
2474            _ => {}
2475        }
2476    }
2477    None
2478}
2479
2480fn is_name_token(token: ParserToken) -> bool {
2481    matches!(
2482        token,
2483        ParserToken::Identifier
2484            | ParserToken::EscapedIdentifier
2485            | ParserToken::Match
2486            | ParserToken::Path
2487            | ParserToken::Return
2488    )
2489}
2490
2491pub(crate) fn query_from_pattern(pattern: Pattern, where_clause: Option<Expr>) -> Query {
2492    let span = pattern.span;
2493    let clause = Node::new(
2494        ClauseKind::Match(MatchClause {
2495            optional: false,
2496            mode: None,
2497            pattern,
2498            hints: Vec::new(),
2499            where_clause,
2500        }),
2501        span,
2502    );
2503    let head = Node::new(
2504        SingleQueryKind {
2505            clauses: vec![clause],
2506        },
2507        span,
2508    );
2509    Node::new(
2510        QueryKind::Regular(RegularQuery {
2511            head,
2512            unions: Vec::new(),
2513        }),
2514        span,
2515    )
2516}
2517
2518pub(crate) type RelationshipDetail = (
2519    Option<Name>,
2520    Option<crate::ast::LabelExpression>,
2521    Option<Quantifier>,
2522    Option<crate::ast::Expr>,
2523    Option<crate::ast::Expr>,
2524);
2525
2526pub(crate) fn relationship_factor(
2527    start: usize,
2528    end: usize,
2529    direction: RelationshipDirection,
2530    detail: Option<RelationshipDetail>,
2531    outer_quantifier: Option<Quantifier>,
2532) -> PathFactor {
2533    let (variable, labels, inner_quantifier, properties, where_clause) =
2534        detail.unwrap_or((None, None, None, None, None));
2535
2536    Node::new(
2537        PathFactorKind::Relationship(RelationshipPattern {
2538            direction: Node::new(direction, Span::new(start, end)),
2539            variable,
2540            labels,
2541            legacy_quantifier: inner_quantifier,
2542            graph_quantifier: outer_quantifier,
2543            properties: properties.map(Box::new),
2544            where_clause: where_clause.map(Box::new),
2545        }),
2546        Span::new(start, end),
2547    )
2548}
2549
2550pub(crate) fn fold_binary(mut expression: Expr, tail: Vec<(Node<BinaryOperator>, Expr)>) -> Expr {
2551    for (operator, right) in tail {
2552        let span = expression.span.cover(right.span);
2553        expression = Node::new(
2554            ExprKind::Binary {
2555                left: Box::new(expression),
2556                operator,
2557                right: Box::new(right),
2558            },
2559            span,
2560        );
2561    }
2562    expression
2563}
2564
2565pub(crate) enum ComparisonSuffix {
2566    Binary(Node<BinaryOperator>, Expr),
2567    Is {
2568        negated: bool,
2569        predicate: IsPredicate,
2570        end: usize,
2571    },
2572}
2573
2574pub(crate) fn fold_comparison(mut expression: Expr, tail: Vec<ComparisonSuffix>) -> Expr {
2575    for suffix in tail {
2576        match suffix {
2577            ComparisonSuffix::Binary(operator, right) => {
2578                let span = expression.span.cover(right.span);
2579                expression = Node::new(
2580                    ExprKind::Binary {
2581                        left: Box::new(expression),
2582                        operator,
2583                        right: Box::new(right),
2584                    },
2585                    span,
2586                );
2587            }
2588            ComparisonSuffix::Is {
2589                negated,
2590                predicate,
2591                end,
2592            } => {
2593                let span = Span::new(expression.span.start, end);
2594                expression = Node::new(
2595                    ExprKind::Is {
2596                        expression: Box::new(expression),
2597                        negated,
2598                        predicate,
2599                    },
2600                    span,
2601                );
2602            }
2603        }
2604    }
2605    expression
2606}
2607
2608pub(crate) enum PostfixSuffix {
2609    Property(Name),
2610    MapProjection {
2611        items: Vec<MapProjectionItem>,
2612        end: usize,
2613    },
2614    Index {
2615        index: Expr,
2616        end: usize,
2617    },
2618    Slice {
2619        lower: Option<Expr>,
2620        upper: Option<Expr>,
2621        end: usize,
2622    },
2623}
2624
2625pub(crate) fn fold_postfix(mut expression: Expr, suffixes: Vec<PostfixSuffix>) -> Expr {
2626    for suffix in suffixes {
2627        expression = match suffix {
2628            PostfixSuffix::Property(key) => {
2629                let span = expression.span.cover(key.span);
2630                Node::new(
2631                    ExprKind::Property {
2632                        expression: Box::new(expression),
2633                        key,
2634                    },
2635                    span,
2636                )
2637            }
2638            PostfixSuffix::MapProjection { items, end } => {
2639                let span = Span::new(expression.span.start, end);
2640                Node::new(
2641                    ExprKind::MapProjection(MapProjection {
2642                        base: Box::new(expression),
2643                        items,
2644                    }),
2645                    span,
2646                )
2647            }
2648            PostfixSuffix::Index { index, end } => {
2649                let span = Span::new(expression.span.start, end);
2650                Node::new(
2651                    ExprKind::Index {
2652                        expression: Box::new(expression),
2653                        index: Box::new(index),
2654                    },
2655                    span,
2656                )
2657            }
2658            PostfixSuffix::Slice { lower, upper, end } => {
2659                let span = Span::new(expression.span.start, end);
2660                Node::new(
2661                    ExprKind::Slice {
2662                        expression: Box::new(expression),
2663                        lower: lower.map(Box::new),
2664                        upper: upper.map(Box::new),
2665                    },
2666                    span,
2667                )
2668            }
2669        };
2670    }
2671    expression
2672}
2673
2674/// Recover only at balanced, top-level boundaries. Parse each candidate with
2675/// the same contextual-name rules as strict parsing, so keyword-shaped names
2676/// and compound clauses (OPTIONAL MATCH, MERGE actions) remain intact.
2677fn recover_clauses(source: &str, tokens: &[SpannedParserToken]) -> Option<Program> {
2678    let (tokens, terminator) = match tokens.last() {
2679        Some((start, ParserToken::Semicolon, end)) => {
2680            (&tokens[..tokens.len() - 1], Some(Span::new(*start, *end)))
2681        }
2682        _ => (tokens, None),
2683    };
2684    let first = tokens.first()?;
2685    let last = tokens.last()?;
2686    if !top_level_indices(tokens, |token| token == ParserToken::Semicolon).is_empty() {
2687        return None;
2688    }
2689    let union_indices: Vec<_> = top_level_indices(tokens, |token| token == ParserToken::Union)
2690        .into_iter()
2691        .filter(|&index| {
2692            !index.checked_sub(1).is_some_and(|previous| {
2693                matches!(
2694                    tokens[previous].1,
2695                    ParserToken::As | ParserToken::Dot | ParserToken::Colon | ParserToken::Call
2696                )
2697            })
2698        })
2699        .collect();
2700    let mut boundaries = top_level_indices(tokens, clause_starts);
2701    boundaries.extend(&union_indices);
2702    boundaries.sort_unstable();
2703    boundaries.push(tokens.len());
2704    let mut clauses = Vec::new();
2705    let mut branches = Vec::new();
2706    let mut operators = Vec::new();
2707    let mut branch_start = first.0;
2708    let mut cursor = 0;
2709    let mut skipped = None;
2710    let mut retained = false;
2711    let mut returned = false;
2712    // Recovery is best-effort: bound additional parser work on adversarial
2713    // inputs with many candidate boundaries. Exhaustion preserves the suffix
2714    // as one error region instead of repeatedly reparsing it.
2715    let mut attempts = 128usize;
2716    while cursor < tokens.len() {
2717        // A successful clause candidate can consume UNION as a contextual name.
2718        // Only a UNION left at the cursor is an operator, and only at top level
2719        // outside explicit name positions (even when their clause is malformed).
2720        // Charge each split to the same budget as clause parsing; empty branches
2721        // must not allow an unbounded number of recovery nodes.
2722        if attempts > 0 && union_indices.binary_search(&cursor).is_ok() {
2723            attempts -= 1;
2724            let end = if clauses.is_empty() && skipped.is_none() {
2725                branch_start
2726            } else {
2727                tokens[cursor - 1].2
2728            };
2729            if let Some(start) = skipped.take() {
2730                clauses.push(Node::new(
2731                    ClauseKind::Error(ErrorNode),
2732                    Span::new(start, end),
2733                ));
2734            }
2735            branches.push(recovered_branch(clauses, Span::new(branch_start, end)));
2736            clauses = Vec::new();
2737            let operator_start = tokens[cursor].0;
2738            cursor += 1;
2739            let kind = match tokens.get(cursor).map(|token| token.1) {
2740                Some(ParserToken::All) => {
2741                    cursor += 1;
2742                    UnionOperator::All
2743                }
2744                Some(ParserToken::Distinct) => {
2745                    cursor += 1;
2746                    UnionOperator::Distinct
2747                }
2748                _ => UnionOperator::Default,
2749            };
2750            let operator_end = tokens[cursor - 1].2;
2751            operators.push(Node::new(kind, Span::new(operator_start, operator_end)));
2752            branch_start = tokens.get(cursor).map_or(operator_end, |token| token.0);
2753            returned = false;
2754            continue;
2755        }
2756        let mut parsed = None;
2757        if !returned {
2758            let mut failed = Vec::new();
2759            for &end in boundaries.iter().filter(|&&end| end > cursor) {
2760                if attempts == 0 {
2761                    break;
2762                }
2763                attempts -= 1;
2764                let candidate = parse_tokens_with_contextual_names(&tokens[cursor..end], |input| {
2765                    generated::ClauseRootParser::new().parse(source, input.iter().copied().map(Ok))
2766                });
2767                if let Ok(clause) = candidate {
2768                    // Prefer the longest valid clause: shorter candidates can
2769                    // mistake a non-reserved name or MERGE action for a boundary.
2770                    parsed = Some((clause, end));
2771                } else if let Err(error) = candidate {
2772                    failed.push((end, error));
2773                }
2774            }
2775            // A valid clause always wins over a repaired one, including clauses
2776            // that consume keyword-shaped names or compound-clause boundaries.
2777            if parsed.is_none() {
2778                for (end, error) in failed {
2779                    if let Some(clause) = recover_expression_operands(
2780                        source,
2781                        &tokens[cursor..end],
2782                        error,
2783                        &mut attempts,
2784                    ) {
2785                        parsed = Some((clause, end));
2786                        break;
2787                    }
2788                }
2789            }
2790        }
2791        if let Some((clause, end)) = parsed {
2792            if let Some(start) = skipped.take() {
2793                clauses.push(Node::new(
2794                    ClauseKind::Error(ErrorNode),
2795                    Span::new(start, tokens[cursor - 1].2),
2796                ));
2797            }
2798            returned = matches!(clause.kind, ClauseKind::Return(_));
2799            clauses.push(clause);
2800            retained = true;
2801            cursor = end;
2802        } else {
2803            skipped.get_or_insert(tokens[cursor].0);
2804            cursor = if attempts == 0 {
2805                tokens.len()
2806            } else if returned {
2807                union_indices
2808                    .iter()
2809                    .copied()
2810                    .find(|&index| index > cursor)
2811                    .unwrap_or(tokens.len())
2812            } else {
2813                *boundaries.iter().find(|&&end| end > cursor)?
2814            };
2815        }
2816    }
2817    if !retained {
2818        return None;
2819    }
2820    if let Some(start) = skipped {
2821        clauses.push(Node::new(
2822            ClauseKind::Error(ErrorNode),
2823            Span::new(start, last.2),
2824        ));
2825    }
2826    let query_span = Span::new(first.0, last.2);
2827    branches.push(recovered_branch(clauses, Span::new(branch_start, last.2)));
2828    let mut branches = branches.into_iter();
2829    let head = branches.next()?;
2830    let unions = operators
2831        .into_iter()
2832        .zip(branches)
2833        .map(|(operator, query)| UnionBranch { operator, query })
2834        .collect();
2835    let query = Node::new(
2836        QueryKind::Regular(RegularQuery { head, unions }),
2837        query_span,
2838    );
2839    let span = terminator.map_or(query_span, |end| query_span.cover(end));
2840    let statement = Node::new(
2841        StatementKind::Query(QueryStatement {
2842            mode: None,
2843            query,
2844            terminator,
2845        }),
2846        span,
2847    );
2848    Some(Program::new(vec![statement], span))
2849}
2850
2851fn recovered_branch(mut clauses: Vec<Clause>, span: Span) -> SingleQuery {
2852    if clauses.is_empty() {
2853        clauses.push(Node::new(ClauseKind::Error(ErrorNode), span));
2854    }
2855    Node::new(SingleQueryKind { clauses }, span)
2856}
2857
2858/// Insert only expressions explicitly expected by the grammar. No source token
2859/// is removed or reinterpreted as a name or delimiter. The initial strict error
2860/// remains the public diagnostic; these retries only construct the partial AST.
2861fn recover_expression_operands(
2862    source: &str,
2863    tokens: &[SpannedParserToken],
2864    mut error: LalrpopError,
2865    attempts: &mut usize,
2866) -> Option<Clause> {
2867    if *attempts == 0 {
2868        return None;
2869    }
2870    let mut repaired = tokens.to_vec();
2871    for _ in 0..4 {
2872        if *attempts == 0 {
2873            return None;
2874        }
2875        let (mut index, mut location, expected) = match &error {
2876            ParseError::UnrecognizedEof { expected, .. } => {
2877                (repaired.len(), repaired.last()?.2, expected)
2878            }
2879            ParseError::UnrecognizedToken { token, expected } => {
2880                if token.1 == ParserToken::ErrorExpression {
2881                    return None;
2882                }
2883                (
2884                    repaired.iter().position(|candidate| candidate == token)?,
2885                    token.0,
2886                    expected,
2887                )
2888            }
2889            _ => return None,
2890        };
2891        if !expected
2892            .iter()
2893            .any(|item| item.trim_matches('"') == "error expression")
2894        {
2895            // Contextual-name parsing can consume AS as the missing operand,
2896            // then fail on the alias. Try the explicit alias interpretation
2897            // only immediately after an operator; the grammar validates it.
2898            let alias_marker = index.checked_sub(1)?;
2899            let operator = alias_marker.checked_sub(1)?;
2900            if repaired[alias_marker].1 != ParserToken::As
2901                || !matches!(
2902                    repaired[operator].1,
2903                    ParserToken::Plus
2904                        | ParserToken::Minus
2905                        | ParserToken::Star
2906                        | ParserToken::Slash
2907                        | ParserToken::Percent
2908                        | ParserToken::Caret
2909                        | ParserToken::And
2910                        | ParserToken::Or
2911                        | ParserToken::Xor
2912                        | ParserToken::Not
2913                        | ParserToken::Equal
2914                        | ParserToken::NotEqual
2915                        | ParserToken::Less
2916                        | ParserToken::LessEqual
2917                        | ParserToken::Greater
2918                        | ParserToken::GreaterEqual
2919                )
2920            {
2921                return None;
2922            }
2923            index = alias_marker;
2924            location = repaired[index].0;
2925        }
2926        repaired.insert(index, (location, ParserToken::ErrorExpression, location));
2927        *attempts -= 1;
2928        match parse_tokens_with_contextual_names(&repaired, |input| {
2929            generated::ClauseRootParser::new().parse(source, input.iter().copied().map(Ok))
2930        }) {
2931            Ok(clause) => return Some(clause),
2932            Err(next) => error = next,
2933        }
2934    }
2935    None
2936}
2937
2938#[cfg(test)]
2939thread_local! {
2940    static STRICT_CLAUSE_ATTEMPTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2941}
2942
2943#[cfg(test)]
2944mod union_performance_tests {
2945    use super::*;
2946
2947    #[test]
2948    fn malformed_union_chains_use_linear_clause_parse_attempts() {
2949        for branches in [16, 128, 512] {
2950            let source = format!("{}RETURN", "RETURN 1 UNION ALL ".repeat(branches));
2951            let tokens = parser_tokens(&lexer::lex(&source).tokens);
2952            STRICT_CLAUSE_ATTEMPTS.with(|attempts| attempts.set(0));
2953            let error = parse_regular_query(&source, &tokens).unwrap_err();
2954            assert_eq!(error_location(&error), source.find("UNION").unwrap());
2955            let attempts = STRICT_CLAUSE_ATTEMPTS.with(|attempts| attempts.get());
2956            assert!(
2957                attempts <= 2 * branches + 1,
2958                "{branches} branches used {attempts} clause parses"
2959            );
2960        }
2961    }
2962}
2963
2964#[cfg(test)]
2965mod expression_recovery_tests {
2966    use super::*;
2967
2968    #[test]
2969    fn expression_retries_obey_the_remaining_shared_budget() {
2970        let source = "RETURN [1 +, 2 +]";
2971        let tokens = parser_tokens(&lexer::lex(source).tokens);
2972        let error = parse_tokens_with_contextual_names(&tokens, |input| {
2973            generated::ClauseRootParser::new().parse(source, input.iter().copied().map(Ok))
2974        })
2975        .unwrap_err();
2976        for available in [0, 1, 2] {
2977            let mut attempts = available;
2978            let result = recover_expression_operands(source, &tokens, error.clone(), &mut attempts);
2979            assert_eq!(result.is_some(), available == 2);
2980            assert_eq!(attempts, 0);
2981        }
2982    }
2983}