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. It does not repair expressions or cross
78/// unclosed delimiters. Ambiguous UNION and multi-statement inputs fall back to
79/// a whole-input error statement, as do inputs with no recoverable clauses.
80/// Recovery work is bounded; exhausting its retry budget leaves an error suffix.
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        let Ok(head) = parse_single_query(source, &tokens[..union_index]) else {
372            continue;
373        };
374
375        let mut right_start = union_index + 1;
376        let operator = match tokens.get(right_start).map(|token| token.1) {
377            Some(ParserToken::All) => {
378                right_start += 1;
379                UnionOperator::All
380            }
381            Some(ParserToken::Distinct) => {
382                right_start += 1;
383                UnionOperator::Distinct
384            }
385            _ => UnionOperator::Default,
386        };
387        if right_start >= tokens.len() {
388            continue;
389        }
390
391        let Ok((right, mut tail)) = parse_regular_query(source, &tokens[right_start..]) else {
392            continue;
393        };
394        let operator_end = tokens[right_start - 1].2;
395        let operator = Node::new(operator, Span::new(tokens[union_index].0, operator_end));
396        let mut unions = Vec::with_capacity(1 + tail.len());
397        unions.push(UnionBranch {
398            operator,
399            query: right,
400        });
401        unions.append(&mut tail);
402        return Ok((head, unions));
403    }
404
405    Err(unsplit_error)
406}
407
408fn parse_single_query(
409    source: &str,
410    tokens: &[SpannedParserToken],
411) -> Result<SingleQuery, LalrpopError> {
412    if tokens.is_empty() {
413        return Err(ParseError::UnrecognizedEof {
414            location: 0,
415            expected: vec!["query clause".to_owned()],
416        });
417    }
418
419    let mut clauses: Vec<Clause> = Vec::new();
420    let mut cursor = 0;
421    while cursor < tokens.len() {
422        let mut candidate_ends = top_level_indices(&tokens[cursor + 1..], clause_starts)
423            .into_iter()
424            .map(|index| cursor + 1 + index)
425            .collect::<Vec<_>>();
426        candidate_ends.push(tokens.len());
427        candidate_ends.sort_unstable();
428        candidate_ends.dedup();
429
430        let mut best_error = None;
431        let mut parsed = None;
432        for end in candidate_ends {
433            let input = tokens[cursor..end].iter().copied().map(Ok);
434            match generated::ClauseRootParser::new().parse(source, input) {
435                Ok(clause) => {
436                    if matches!(&clause.kind, ClauseKind::Return(_)) && end < tokens.len() {
437                        return Err(ParseError::ExtraToken { token: tokens[end] });
438                    }
439                    parsed = Some((clause, end));
440                    break;
441                }
442                Err(error) => retain_farthest_error(&mut best_error, error),
443            }
444        }
445
446        let Some((clause, end)) = parsed else {
447            return Err(best_error.unwrap_or(ParseError::UnrecognizedEof {
448                location: tokens[cursor].0,
449                expected: vec!["query clause".to_owned()],
450            }));
451        };
452        clauses.push(clause);
453        cursor = end;
454    }
455
456    let span = Span::new(
457        tokens.first().expect("non-empty query").0,
458        tokens.last().unwrap().2,
459    );
460    Ok(Node::new(SingleQueryKind { clauses }, span))
461}
462
463fn retain_farthest_error(slot: &mut Option<LalrpopError>, candidate: LalrpopError) {
464    let candidate_location = error_location(&candidate);
465    if slot
466        .as_ref()
467        .is_none_or(|current| candidate_location >= error_location(current))
468    {
469        *slot = Some(candidate);
470    }
471}
472
473fn error_location(error: &LalrpopError) -> usize {
474    match error {
475        ParseError::InvalidToken { location } | ParseError::UnrecognizedEof { location, .. } => {
476            *location
477        }
478        ParseError::UnrecognizedToken { token, .. } | ParseError::ExtraToken { token } => token.0,
479        ParseError::User { error: location } => *location,
480    }
481}
482
483fn top_level_indices(
484    tokens: &[SpannedParserToken],
485    predicate: impl Fn(ParserToken) -> bool,
486) -> Vec<usize> {
487    let mut depth = 0usize;
488    let mut indices = Vec::new();
489    for (index, (_, token, _)) in tokens.iter().copied().enumerate() {
490        if depth == 0 && predicate(token) {
491            indices.push(index);
492        }
493        match token {
494            ParserToken::LeftParen | ParserToken::LeftBracket | ParserToken::LeftBrace => {
495                depth = depth.saturating_add(1);
496            }
497            ParserToken::RightParen | ParserToken::RightBracket | ParserToken::RightBrace => {
498                depth = depth.saturating_sub(1);
499            }
500            _ => {}
501        }
502    }
503    indices
504}
505
506fn clause_starts(token: ParserToken) -> bool {
507    matches!(
508        token,
509        ParserToken::Match
510            | ParserToken::Optional
511            | ParserToken::Unwind
512            | ParserToken::With
513            | ParserToken::Return
514            | ParserToken::Create
515            | ParserToken::Merge
516            | ParserToken::Set
517            | ParserToken::Remove
518            | ParserToken::Delete
519            | ParserToken::Detach
520            | ParserToken::Call
521    )
522}
523
524fn normalize_diagnostics(source_len: usize, diagnostics: &mut Vec<Diagnostic>) {
525    diagnostics.sort_by(|left, right| {
526        left.primary_span
527            .start
528            .cmp(&right.primary_span.start)
529            .then(left.primary_span.end.cmp(&right.primary_span.end))
530            .then(left.code.cmp(&right.code))
531            .then(left.message.cmp(&right.message))
532    });
533    diagnostics.dedup_by(|left, right| {
534        left.code == right.code
535            && left.primary_span == right.primary_span
536            && left.message == right.message
537    });
538
539    if diagnostics.len() > MAX_DIAGNOSTICS {
540        diagnostics.truncate(MAX_DIAGNOSTICS - 1);
541        diagnostics.push(Diagnostic::error(
542            DiagnosticCode::TooManyErrors,
543            "additional parser diagnostics were suppressed",
544            Span::empty(source_len),
545        ));
546    }
547}
548
549fn recovered_program(source: &str, tokens: &[Token]) -> Program {
550    let span = significant_span(tokens).unwrap_or_else(|| Span::empty(source.len()));
551    let statements = if span.is_empty() {
552        Vec::new()
553    } else {
554        vec![Node::new(StatementKind::Error(ErrorNode), span)]
555    };
556    Program::new(statements, span)
557}
558
559fn significant_span(tokens: &[Token]) -> Option<Span> {
560    let mut significant = tokens.iter().filter(|token| !token.is_trivia());
561    let first = significant.next()?;
562    let mut span = first.span;
563    for token in significant {
564        span = span.cover(token.span);
565    }
566    Some(span)
567}
568
569fn collect_delimiter_diagnostics(
570    source: &str,
571    tokens: &[Token],
572    diagnostics: &mut Vec<Diagnostic>,
573) {
574    let mut stack: Vec<(TokenKind, Span)> = Vec::new();
575
576    for token in tokens.iter().filter(|token| !token.is_trivia()) {
577        match token.kind {
578            TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => {
579                stack.push((token.kind, token.span));
580            }
581            TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => {
582                let expected_open = match token.kind {
583                    TokenKind::RightParen => TokenKind::LeftParen,
584                    TokenKind::RightBracket => TokenKind::LeftBracket,
585                    TokenKind::RightBrace => TokenKind::LeftBrace,
586                    _ => unreachable!(),
587                };
588                match stack.pop() {
589                    Some((open, _)) if open == expected_open => {}
590                    Some((open, open_span)) => diagnostics.push(
591                        Diagnostic::error(
592                            DiagnosticCode::MismatchedDelimiter,
593                            format!(
594                                "mismatched delimiter: {} does not close {}",
595                                token.kind, open
596                            ),
597                            token.span,
598                        )
599                        .with_label(open_span, "this delimiter was opened here"),
600                    ),
601                    None => diagnostics.push(Diagnostic::error(
602                        DiagnosticCode::MismatchedDelimiter,
603                        format!("unexpected closing delimiter {}", token.kind),
604                        token.span,
605                    )),
606                }
607            }
608            _ => {}
609        }
610    }
611
612    for (open, span) in stack {
613        diagnostics.push(
614            Diagnostic::error(
615                DiagnosticCode::UnclosedDelimiter,
616                format!("unclosed delimiter {open}"),
617                span,
618            )
619            .with_help(format!(
620                "add the matching delimiter before byte {}",
621                source.len()
622            )),
623        );
624    }
625}
626
627/// The user error payload is the source offset the nested fragment failed at.
628type LalrpopError = ParseError<usize, ParserToken, usize>;
629
630fn parse_diagnostic(source: &str, error: LalrpopError) -> Diagnostic {
631    match error {
632        ParseError::InvalidToken { location } => Diagnostic::error(
633            DiagnosticCode::InvalidToken,
634            "invalid token",
635            Span::empty(location.min(source.len())),
636        ),
637        ParseError::UnrecognizedEof { location, expected } => {
638            let expected = normalized_expected(expected);
639            let message = if expected.is_empty() {
640                "unexpected end of input".to_owned()
641            } else {
642                format!("unexpected end of input; expected {expected}")
643            };
644            Diagnostic::error(
645                DiagnosticCode::UnexpectedEof,
646                message,
647                Span::empty(location.min(source.len())),
648            )
649        }
650        ParseError::UnrecognizedToken {
651            token: (start, token, end),
652            expected,
653        } => {
654            let expected = normalized_expected(expected);
655            let message = if expected.is_empty() {
656                format!("unexpected token {token}")
657            } else {
658                format!("unexpected token {token}; expected {expected}")
659            };
660            Diagnostic::error(
661                DiagnosticCode::UnexpectedToken,
662                message,
663                Span::new(start, end),
664            )
665        }
666        ParseError::ExtraToken {
667            token: (start, token, end),
668        } => Diagnostic::error(
669            DiagnosticCode::ExtraToken,
670            format!("extra token {token} after the end of the statement"),
671            Span::new(start, end),
672        ),
673        ParseError::User { error: location } => Diagnostic::error(
674            DiagnosticCode::UnexpectedToken,
675            "input does not satisfy the selected syntax production",
676            Span::empty(location.min(source.len())),
677        ),
678    }
679}
680
681fn normalized_expected(mut expected: Vec<String>) -> String {
682    for item in &mut expected {
683        *item = item.trim_matches('"').to_owned();
684    }
685    expected.sort();
686    expected.dedup();
687    match expected.as_slice() {
688        [] => String::new(),
689        [only] => format!("`{only}`"),
690        [head @ .., last] => {
691            let head = head
692                .iter()
693                .map(|item| format!("`{item}`"))
694                .collect::<Vec<_>>()
695                .join(", ");
696            format!("{head}, or `{last}`")
697        }
698    }
699}
700
701fn parser_tokens(tokens: &[Token]) -> Vec<SpannedParserToken> {
702    let significant = tokens
703        .iter()
704        .filter(|token| !token.is_trivia())
705        .collect::<Vec<_>>();
706    let mut result = Vec::with_capacity(significant.len());
707    let mut index = 0;
708    while index < significant.len() {
709        if let Some(end) = escaped_parameter_end(&significant, index) {
710            result.push((
711                significant[index].span.start,
712                ParserToken::Parameter,
713                significant[end].span.end,
714            ));
715            index = end + 1;
716            continue;
717        }
718        if let Some(end) = pattern_comprehension_end(&significant, index) {
719            result.push((
720                significant[index].span.start,
721                ParserToken::PatternComprehension,
722                significant[end].span.end,
723            ));
724            index = end + 1;
725            continue;
726        }
727        if significant[index].kind == TokenKind::LeftParen
728            && is_pattern_expression_context(&significant, index)
729            && let Some(end) = pattern_expression_end(&significant, index)
730        {
731            result.push((
732                significant[index].span.start,
733                ParserToken::PatternExpression,
734                significant[end].span.end,
735            ));
736            index = end + 1;
737            continue;
738        }
739        if is_relationship_label_start(&significant, index)
740            && let Some(end) = relationship_label_end(&significant, index)
741        {
742            result.push((
743                significant[index].span.start,
744                ParserToken::RelationshipLabelExpression,
745                significant[end].span.end,
746            ));
747            index = end + 1;
748            continue;
749        }
750        if is_label_predicate_context(&significant, index)
751            && let Some(end) = label_predicate_end(&significant, index)
752        {
753            result.push((
754                significant[index].span.start,
755                ParserToken::LabelPredicate,
756                significant[end].span.end,
757            ));
758            index = end + 1;
759            continue;
760        }
761        if !is_procedure_name_start(&significant, index)
762            && let Some(end) = qualified_function_name_end(&significant, index)
763        {
764            result.push((
765                significant[index].span.start,
766                ParserToken::QualifiedFunctionName,
767                significant[end].span.end,
768            ));
769            index = end + 1;
770            continue;
771        }
772        let token = significant[index];
773        let mut parser_token = ParserToken::from(token.kind);
774        if parser_token == ParserToken::All && is_explicit_set_all(&significant, index) {
775            parser_token = ParserToken::SetAll;
776        }
777        result.push((token.span.start, parser_token, token.span.end));
778        index += 1;
779    }
780    result
781}
782
783fn escaped_parameter_end(tokens: &[&Token], start: usize) -> Option<usize> {
784    (tokens.get(start)?.kind == TokenKind::Dollar
785        && tokens.get(start + 1).is_some_and(|token| {
786            token.kind == TokenKind::EscapedIdentifier && tokens[start].span.end == token.span.start
787        }))
788    .then_some(start + 1)
789}
790
791fn qualified_function_name_end(tokens: &[&Token], start: usize) -> Option<usize> {
792    if !is_symbolic_name_kind(tokens.get(start)?.kind) {
793        return None;
794    }
795
796    let mut end = start;
797    let mut components = 1usize;
798    while tokens
799        .get(end + 1)
800        .is_some_and(|token| token.kind == TokenKind::Dot)
801        && tokens
802            .get(end + 2)
803            .is_some_and(|token| is_symbolic_name_kind(token.kind))
804    {
805        end += 2;
806        components += 1;
807    }
808
809    (components > 1
810        && tokens
811            .get(end + 1)
812            .is_some_and(|token| token.kind == TokenKind::LeftParen))
813    .then_some(end)
814}
815
816fn is_procedure_name_start(tokens: &[&Token], start: usize) -> bool {
817    let mut first_component = start;
818    while first_component >= 2
819        && tokens[first_component - 1].kind == TokenKind::Dot
820        && is_symbolic_name_kind(tokens[first_component - 2].kind)
821    {
822        first_component -= 2;
823    }
824
825    first_component
826        .checked_sub(1)
827        .and_then(|index| tokens.get(index))
828        .is_some_and(|token| token.kind == TokenKind::Keyword(Keyword::Call))
829}
830
831fn is_symbolic_name_kind(kind: TokenKind) -> bool {
832    matches!(
833        kind,
834        TokenKind::Identifier | TokenKind::EscapedIdentifier | TokenKind::Keyword(_)
835    )
836}
837
838fn is_explicit_set_all(tokens: &[&Token], index: usize) -> bool {
839    if tokens
840        .get(index + 1)
841        .is_some_and(|token| token.kind == TokenKind::LeftParen)
842    {
843        return false;
844    }
845
846    let Some(previous) = index.checked_sub(1).and_then(|index| tokens.get(index)) else {
847        return false;
848    };
849    if matches!(
850        previous.kind,
851        TokenKind::Keyword(Keyword::Return | Keyword::With)
852    ) {
853        return true;
854    }
855    if previous.kind != TokenKind::LeftParen {
856        return false;
857    }
858
859    index
860        .checked_sub(2)
861        .and_then(|index| tokens.get(index))
862        .is_some_and(|token| {
863            matches!(
864                ParserToken::from(token.kind),
865                ParserToken::Identifier
866                    | ParserToken::EscapedIdentifier
867                    | ParserToken::Count
868                    | ParserToken::Match
869                    | ParserToken::Path
870                    | ParserToken::Return
871            )
872        })
873}
874
875fn pattern_comprehension_end(tokens: &[&Token], start: usize) -> Option<usize> {
876    if tokens.get(start)?.kind != TokenKind::LeftBracket {
877        return None;
878    }
879
880    // The heuristic only counts evidence from the comprehension's pattern
881    // region: before the first top-level WHERE or `|`, at bracket depth 1.
882    // Arrows in a WHERE predicate, constructs in nested brackets, and
883    // function-call or grouping parentheses (a `(` directly after a name or
884    // a closing delimiter) are not pattern evidence — `[x IN a - f(b) - g(c) | x]`
885    // is a list comprehension, not an undirected relationship pattern.
886    let mut brackets = 0usize;
887    let mut parentheses = 0usize;
888    let mut braces = 0usize;
889    let mut pipe_at = None;
890    let mut where_seen = false;
891    let mut arrow = false;
892    let mut top_level_nodes = 0usize;
893    let mut top_level_minuses = 0usize;
894    for (index, token) in tokens.iter().enumerate().skip(start) {
895        let in_pattern_region = pipe_at.is_none() && !where_seen;
896        match token.kind {
897            TokenKind::LeftBracket => brackets += 1,
898            TokenKind::RightBracket => {
899                if brackets == 1 && parentheses == 0 && braces == 0 {
900                    let has_projection = pipe_at.is_some_and(|pipe| index > pipe + 1);
901                    let undirected_relationship = top_level_nodes >= 2 && top_level_minuses >= 2;
902                    return (has_projection && (arrow || undirected_relationship)).then_some(index);
903                }
904                brackets = brackets.saturating_sub(1);
905            }
906            TokenKind::LeftParen => {
907                let call_or_grouping = index > start
908                    && (matches!(
909                        tokens[index - 1].kind,
910                        TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace
911                    ) || is_symbolic_name_kind(tokens[index - 1].kind));
912                if in_pattern_region
913                    && brackets == 1
914                    && parentheses == 0
915                    && braces == 0
916                    && !call_or_grouping
917                {
918                    top_level_nodes += 1;
919                }
920                parentheses += 1;
921            }
922            TokenKind::RightParen => parentheses = parentheses.saturating_sub(1),
923            TokenKind::LeftBrace => braces += 1,
924            TokenKind::RightBrace => braces = braces.saturating_sub(1),
925            TokenKind::Pipe if brackets == 1 && parentheses == 0 && braces == 0 => {
926                pipe_at = Some(index)
927            }
928            // `where` followed by `=` is a contextual binding name, not the
929            // predicate keyword.
930            TokenKind::Keyword(Keyword::Where)
931                if in_pattern_region
932                    && brackets == 1
933                    && parentheses == 0
934                    && braces == 0
935                    && tokens
936                        .get(index + 1)
937                        .is_none_or(|next| next.kind != TokenKind::Equal) =>
938            {
939                where_seen = true;
940            }
941            TokenKind::LeftArrow | TokenKind::RightArrow if in_pattern_region && brackets == 1 => {
942                arrow = true;
943            }
944            TokenKind::Minus
945                if in_pattern_region && brackets == 1 && parentheses == 0 && braces == 0 =>
946            {
947                top_level_minuses += 1;
948            }
949            _ => {}
950        }
951    }
952    None
953}
954
955fn is_pattern_expression_context(tokens: &[&Token], start: usize) -> bool {
956    if start >= 2
957        && tokens[start - 1].kind == TokenKind::LeftParen
958        && matches!(
959            tokens[start - 2].kind,
960            TokenKind::Keyword(Keyword::ShortestPath | Keyword::AllShortestPaths)
961        )
962    {
963        return false;
964    }
965    if start >= 2
966        && tokens[start - 1].kind == TokenKind::LeftBrace
967        && matches!(
968            tokens[start - 2].kind,
969            TokenKind::Keyword(Keyword::Exists | Keyword::Count | Keyword::Collect)
970        )
971    {
972        return false;
973    }
974    if is_in_map_entry_value(tokens, start) {
975        return true;
976    }
977
978    // Like is_label_predicate_context, skip markers inside completed
979    // bracketed/braced constructs (a relationship-type `|` in `[:A|B]` is not
980    // an expression context), while a comprehension's projection `|` in the
981    // construct that still encloses the candidate remains visible.
982    let mut nested_brackets = 0usize;
983    let mut nested_braces = 0usize;
984    for (index, token) in tokens[..start].iter().enumerate().rev() {
985        match token.kind {
986            TokenKind::RightBracket => {
987                nested_brackets = nested_brackets.saturating_add(1);
988                continue;
989            }
990            TokenKind::LeftBracket if nested_brackets != 0 => {
991                nested_brackets = nested_brackets.saturating_sub(1);
992                continue;
993            }
994            TokenKind::RightBrace => {
995                nested_braces = nested_braces.saturating_add(1);
996                continue;
997            }
998            TokenKind::LeftBrace if nested_braces != 0 => {
999                nested_braces = nested_braces.saturating_sub(1);
1000                continue;
1001            }
1002            _ if nested_brackets != 0 || nested_braces != 0 => continue,
1003            _ => {}
1004        }
1005        if tokens
1006            .get(index + 1)
1007            .is_some_and(|next| next.kind == TokenKind::Equal)
1008        {
1009            continue;
1010        }
1011        match token.kind {
1012            TokenKind::Pipe => return true,
1013            TokenKind::Keyword(
1014                Keyword::Where
1015                | Keyword::Return
1016                | Keyword::With
1017                | Keyword::Unwind
1018                | Keyword::Set
1019                | Keyword::Delete
1020                | Keyword::Then
1021                | Keyword::Else
1022                | Keyword::When
1023                | Keyword::Case,
1024            ) => return true,
1025            TokenKind::Keyword(
1026                Keyword::Match | Keyword::Create | Keyword::Merge | Keyword::Call | Keyword::Remove,
1027            ) => return false,
1028            _ => {}
1029        }
1030    }
1031    false
1032}
1033
1034fn pattern_expression_end(tokens: &[&Token], start: usize) -> Option<usize> {
1035    if tokens.get(start)?.kind != TokenKind::LeftParen {
1036        return None;
1037    }
1038
1039    let mut node_end =
1040        matching_delimiter(tokens, start, TokenKind::LeftParen, TokenKind::RightParen)?;
1041    let mut relationships = 0usize;
1042    while let Some(after_relationship) = relationship_syntax_end(tokens, node_end + 1) {
1043        let mut next_node = after_relationship;
1044        if matches!(
1045            tokens.get(next_node).map(|token| token.kind),
1046            Some(TokenKind::Star | TokenKind::Plus | TokenKind::Question)
1047        ) {
1048            next_node += 1;
1049        } else if tokens
1050            .get(next_node)
1051            .is_some_and(|token| token.kind == TokenKind::LeftBrace)
1052        {
1053            next_node = matching_delimiter(
1054                tokens,
1055                next_node,
1056                TokenKind::LeftBrace,
1057                TokenKind::RightBrace,
1058            )? + 1;
1059        }
1060        if !tokens
1061            .get(next_node)
1062            .is_some_and(|token| token.kind == TokenKind::LeftParen)
1063        {
1064            break;
1065        }
1066        node_end = matching_delimiter(
1067            tokens,
1068            next_node,
1069            TokenKind::LeftParen,
1070            TokenKind::RightParen,
1071        )?;
1072        relationships += 1;
1073    }
1074
1075    (relationships > 0).then_some(node_end)
1076}
1077
1078fn relationship_syntax_end(tokens: &[&Token], start: usize) -> Option<usize> {
1079    let mut cursor = match (
1080        tokens.get(start).map(|token| token.kind),
1081        tokens.get(start + 1).map(|token| token.kind),
1082    ) {
1083        (Some(TokenKind::Minus | TokenKind::LeftArrow), _) => start + 1,
1084        (Some(TokenKind::Less), Some(TokenKind::Minus)) => start + 2,
1085        _ => return None,
1086    };
1087    if tokens
1088        .get(cursor)
1089        .is_some_and(|token| token.kind == TokenKind::LeftBracket)
1090    {
1091        cursor = matching_delimiter(
1092            tokens,
1093            cursor,
1094            TokenKind::LeftBracket,
1095            TokenKind::RightBracket,
1096        )? + 1;
1097    }
1098    match (
1099        tokens.get(cursor).map(|token| token.kind),
1100        tokens.get(cursor + 1).map(|token| token.kind),
1101    ) {
1102        (Some(TokenKind::Minus), Some(TokenKind::Greater)) => Some(cursor + 2),
1103        (Some(TokenKind::Minus | TokenKind::RightArrow), _) => Some(cursor + 1),
1104        _ => None,
1105    }
1106}
1107
1108fn matching_delimiter(
1109    tokens: &[&Token],
1110    start: usize,
1111    open: TokenKind,
1112    close: TokenKind,
1113) -> Option<usize> {
1114    if tokens.get(start)?.kind != open {
1115        return None;
1116    }
1117    let mut depth = 0usize;
1118    for (index, token) in tokens.iter().enumerate().skip(start) {
1119        if token.kind == open {
1120            depth += 1;
1121        } else if token.kind == close {
1122            depth = depth.saturating_sub(1);
1123            if depth == 0 {
1124                return Some(index);
1125            }
1126        }
1127    }
1128    None
1129}
1130
1131fn is_relationship_label_start(tokens: &[&Token], start: usize) -> bool {
1132    if tokens
1133        .get(start)
1134        .is_none_or(|token| token.kind != TokenKind::Colon)
1135    {
1136        return false;
1137    }
1138    match start.checked_sub(1).and_then(|index| tokens.get(index)) {
1139        Some(token) if token.kind == TokenKind::LeftBracket => true,
1140        Some(token) if is_symbolic_name_kind(token.kind) => {
1141            start >= 2 && tokens[start - 2].kind == TokenKind::LeftBracket
1142        }
1143        _ => false,
1144    }
1145}
1146
1147fn relationship_label_end(tokens: &[&Token], start: usize) -> Option<usize> {
1148    let end = label_predicate_end(tokens, start)?;
1149    if !(start + 1..=end).any(|index| tokens[index].kind == TokenKind::Colon) {
1150        return Some(end);
1151    }
1152
1153    let mut cursor = start + 1;
1154    loop {
1155        if cursor > end || !is_symbolic_name_kind(tokens[cursor].kind) {
1156            return None;
1157        }
1158        cursor += 1;
1159        if cursor > end {
1160            return Some(end);
1161        }
1162        if tokens[cursor].kind != TokenKind::Pipe
1163            || tokens
1164                .get(cursor + 1)
1165                .is_none_or(|token| token.kind != TokenKind::Colon)
1166        {
1167            return None;
1168        }
1169        cursor += 2;
1170    }
1171}
1172
1173fn is_label_predicate_context(tokens: &[&Token], start: usize) -> bool {
1174    if !matches!(
1175        tokens.get(start).map(|token| token.kind),
1176        Some(TokenKind::Colon | TokenKind::Keyword(Keyword::Is))
1177    ) {
1178        return false;
1179    }
1180    if tokens[start].kind == TokenKind::Keyword(Keyword::Is)
1181        && tokens
1182            .get(start + 1)
1183            .is_some_and(|token| token.kind == TokenKind::Keyword(Keyword::As))
1184        && tokens
1185            .get(start + 2)
1186            .is_some_and(|token| is_symbolic_name_kind(token.kind))
1187    {
1188        return false;
1189    }
1190    if tokens[start].kind == TokenKind::Colon
1191        && start >= 2
1192        && is_symbolic_name_kind(tokens[start - 1].kind)
1193        && matches!(tokens[start - 2].kind, TokenKind::LeftBrace)
1194    {
1195        return false;
1196    }
1197    if is_in_map_entry_value(tokens, start) {
1198        return true;
1199    }
1200    if tokens[start].kind == TokenKind::Colon
1201        && start >= 2
1202        && is_symbolic_name_kind(tokens[start - 1].kind)
1203        && tokens[start - 2].kind == TokenKind::Comma
1204        && is_inside_braces(tokens, start)
1205    {
1206        return false;
1207    }
1208
1209    // Ignore context markers inside a completed bracketed/braced construct.
1210    // In particular, a relationship-type `|` in `[:A|B]` must not make a
1211    // later node label look like an expression predicate. Markers in the
1212    // construct that currently encloses the candidate still have zero
1213    // relative depth and remain visible (for example a comprehension `|`).
1214    let mut nested_brackets = 0usize;
1215    let mut nested_braces = 0usize;
1216    for (index, token) in tokens[..start].iter().enumerate().rev() {
1217        match token.kind {
1218            TokenKind::RightBracket => {
1219                nested_brackets = nested_brackets.saturating_add(1);
1220                continue;
1221            }
1222            TokenKind::LeftBracket if nested_brackets != 0 => {
1223                nested_brackets = nested_brackets.saturating_sub(1);
1224                continue;
1225            }
1226            TokenKind::RightBrace => {
1227                nested_braces = nested_braces.saturating_add(1);
1228                continue;
1229            }
1230            TokenKind::LeftBrace if nested_braces != 0 => {
1231                nested_braces = nested_braces.saturating_sub(1);
1232                continue;
1233            }
1234            _ if nested_brackets != 0 || nested_braces != 0 => continue,
1235            _ => {}
1236        }
1237        if tokens
1238            .get(index + 1)
1239            .is_some_and(|next| next.kind == TokenKind::Equal)
1240        {
1241            continue;
1242        }
1243        match token.kind {
1244            TokenKind::Pipe => return true,
1245            TokenKind::Keyword(
1246                Keyword::Where
1247                | Keyword::Return
1248                | Keyword::With
1249                | Keyword::Unwind
1250                | Keyword::Delete
1251                | Keyword::Then
1252                | Keyword::Else
1253                | Keyword::When
1254                | Keyword::Case,
1255            ) => return true,
1256            TokenKind::Keyword(
1257                Keyword::Match
1258                | Keyword::Create
1259                | Keyword::Merge
1260                | Keyword::Call
1261                | Keyword::Set
1262                | Keyword::Remove,
1263            ) => return false,
1264            _ => {}
1265        }
1266    }
1267    false
1268}
1269
1270fn is_inside_braces(tokens: &[&Token], start: usize) -> bool {
1271    let mut nested = 0usize;
1272    for token in tokens[..start].iter().rev() {
1273        match token.kind {
1274            TokenKind::RightBrace => nested += 1,
1275            TokenKind::LeftBrace if nested == 0 => return true,
1276            TokenKind::LeftBrace => nested = nested.saturating_sub(1),
1277            _ => {}
1278        }
1279    }
1280    false
1281}
1282
1283fn is_in_map_entry_value(tokens: &[&Token], start: usize) -> bool {
1284    let mut open_braces = Vec::new();
1285    for (index, token) in tokens.iter().enumerate().take(start) {
1286        match token.kind {
1287            TokenKind::LeftBrace => open_braces.push(index),
1288            TokenKind::RightBrace => {
1289                let _ = open_braces.pop();
1290            }
1291            _ => {}
1292        }
1293    }
1294    let Some(open) = open_braces.last().copied() else {
1295        return false;
1296    };
1297
1298    let mut depth = 0usize;
1299    let mut after_separator = false;
1300    for token in &tokens[open + 1..start] {
1301        match token.kind {
1302            TokenKind::LeftParen | TokenKind::LeftBracket | TokenKind::LeftBrace => {
1303                depth += 1;
1304            }
1305            TokenKind::RightParen | TokenKind::RightBracket | TokenKind::RightBrace => {
1306                depth = depth.saturating_sub(1);
1307            }
1308            TokenKind::Comma if depth == 0 => after_separator = false,
1309            TokenKind::Colon if depth == 0 => after_separator = true,
1310            _ => {}
1311        }
1312    }
1313    after_separator
1314}
1315
1316fn label_predicate_end(tokens: &[&Token], start: usize) -> Option<usize> {
1317    let expression_start = match tokens.get(start)?.kind {
1318        TokenKind::Colon => start + 1,
1319        TokenKind::Keyword(Keyword::Is) => {
1320            if (matches!(
1321                tokens.get(start + 1).map(|token| token.kind),
1322                Some(TokenKind::Keyword(Keyword::Null))
1323            ) && !matches!(
1324                tokens.get(start + 2).map(|token| token.kind),
1325                Some(TokenKind::Pipe | TokenKind::Ampersand)
1326            )) || matches!(
1327                (
1328                    tokens.get(start + 1).map(|token| token.kind),
1329                    tokens.get(start + 2).map(|token| token.kind),
1330                ),
1331                (
1332                    Some(TokenKind::Keyword(Keyword::Not)),
1333                    Some(TokenKind::Keyword(Keyword::Null)),
1334                )
1335            ) {
1336                return None;
1337            }
1338            start + 1
1339        }
1340        _ => return None,
1341    };
1342    let end = label_or_end(tokens, expression_start)?;
1343    Some(end.saturating_sub(1))
1344}
1345
1346fn label_or_end(tokens: &[&Token], start: usize) -> Option<usize> {
1347    let mut cursor = label_and_end(tokens, start)?;
1348    while tokens
1349        .get(cursor)
1350        .is_some_and(|token| token.kind == TokenKind::Pipe)
1351    {
1352        cursor += 1;
1353        if tokens
1354            .get(cursor)
1355            .is_some_and(|token| token.kind == TokenKind::Colon)
1356        {
1357            cursor += 1;
1358        }
1359        cursor = label_and_end(tokens, cursor)?;
1360    }
1361    Some(cursor)
1362}
1363
1364fn label_and_end(tokens: &[&Token], start: usize) -> Option<usize> {
1365    let mut cursor = label_primary_end(tokens, start)?;
1366    while matches!(
1367        tokens.get(cursor).map(|token| token.kind),
1368        Some(TokenKind::Ampersand | TokenKind::Colon)
1369    ) {
1370        cursor = label_primary_end(tokens, cursor + 1)?;
1371    }
1372    Some(cursor)
1373}
1374
1375fn label_primary_end(tokens: &[&Token], start: usize) -> Option<usize> {
1376    let mut cursor = start;
1377    if tokens
1378        .get(cursor)
1379        .is_some_and(|token| token.kind == TokenKind::Bang)
1380    {
1381        cursor += 1;
1382    }
1383    match tokens.get(cursor)?.kind {
1384        TokenKind::Percent => Some(cursor + 1),
1385        kind if is_symbolic_name_kind(kind) => Some(cursor + 1),
1386        TokenKind::LeftParen => {
1387            let end = label_or_end(tokens, cursor + 1)?;
1388            tokens
1389                .get(end)
1390                .is_some_and(|token| token.kind == TokenKind::RightParen)
1391                .then_some(end + 1)
1392        }
1393        _ => None,
1394    }
1395}
1396
1397#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1398pub(crate) enum ParserToken {
1399    Identifier,
1400    EscapedIdentifier,
1401    Parameter,
1402    Integer,
1403    HexInteger,
1404    OctalInteger,
1405    Float,
1406    String,
1407    PatternComprehension,
1408    PatternExpression,
1409    LabelPredicate,
1410    RelationshipLabelExpression,
1411    QualifiedFunctionName,
1412    All,
1413    SetAll,
1414    AllShortestPaths,
1415    And,
1416    Any,
1417    As,
1418    Asc,
1419    Ascending,
1420    By,
1421    Call,
1422    Case,
1423    Contains,
1424    Collect,
1425    Count,
1426    Create,
1427    Delete,
1428    Desc,
1429    Descending,
1430    Detach,
1431    Distinct,
1432    Else,
1433    End,
1434    Ends,
1435    Exists,
1436    False,
1437    Group,
1438    Groups,
1439    In,
1440    Inf,
1441    Infinity,
1442    Is,
1443    Limit,
1444    Match,
1445    Merge,
1446    Nan,
1447    None,
1448    Not,
1449    Null,
1450    Offset,
1451    On,
1452    Optional,
1453    Or,
1454    Order,
1455    Path,
1456    Paths,
1457    Reduce,
1458    Remove,
1459    Return,
1460    Set,
1461    Shortest,
1462    ShortestPath,
1463    Simple,
1464    Single,
1465    Skip,
1466    Starts,
1467    Then,
1468    Trail,
1469    Trim,
1470    True,
1471    Union,
1472    Unwind,
1473    Walk,
1474    When,
1475    Where,
1476    With,
1477    Xor,
1478    Yield,
1479    Acyclic,
1480    LeftParen,
1481    RightParen,
1482    LeftBracket,
1483    RightBracket,
1484    LeftBrace,
1485    RightBrace,
1486    Comma,
1487    Dot,
1488    DotDot,
1489    Colon,
1490    DoubleColon,
1491    Semicolon,
1492    Pipe,
1493    DoublePipe,
1494    Ampersand,
1495    Question,
1496    Dollar,
1497    Plus,
1498    Minus,
1499    Star,
1500    Slash,
1501    Percent,
1502    Caret,
1503    Bang,
1504    Equal,
1505    NotEqual,
1506    Less,
1507    LessEqual,
1508    Greater,
1509    GreaterEqual,
1510    PlusEqual,
1511    FatArrow,
1512    RegexMatch,
1513    LeftArrow,
1514    RightArrow,
1515    Invalid,
1516}
1517
1518impl fmt::Display for ParserToken {
1519    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1520        write!(formatter, "`{}`", self.name())
1521    }
1522}
1523
1524impl ParserToken {
1525    const fn is_keyword_literal(self) -> bool {
1526        matches!(
1527            self,
1528            Self::False | Self::Inf | Self::Infinity | Self::Nan | Self::Null | Self::True
1529        )
1530    }
1531
1532    const fn is_contextual_name_candidate(self) -> bool {
1533        !matches!(
1534            self,
1535            Self::Identifier
1536                | Self::EscapedIdentifier
1537                | Self::Parameter
1538                | Self::Integer
1539                | Self::HexInteger
1540                | Self::OctalInteger
1541                | Self::Float
1542                | Self::String
1543                | Self::PatternComprehension
1544                | Self::PatternExpression
1545                | Self::LabelPredicate
1546                | Self::RelationshipLabelExpression
1547                | Self::QualifiedFunctionName
1548                | Self::LeftParen
1549                | Self::RightParen
1550                | Self::LeftBracket
1551                | Self::RightBracket
1552                | Self::LeftBrace
1553                | Self::RightBrace
1554                | Self::Comma
1555                | Self::Dot
1556                | Self::DotDot
1557                | Self::Colon
1558                | Self::DoubleColon
1559                | Self::Semicolon
1560                | Self::Pipe
1561                | Self::DoublePipe
1562                | Self::Ampersand
1563                | Self::Question
1564                | Self::Dollar
1565                | Self::Plus
1566                | Self::Minus
1567                | Self::Star
1568                | Self::Slash
1569                | Self::Percent
1570                | Self::Caret
1571                | Self::Bang
1572                | Self::Equal
1573                | Self::NotEqual
1574                | Self::Less
1575                | Self::LessEqual
1576                | Self::Greater
1577                | Self::GreaterEqual
1578                | Self::PlusEqual
1579                | Self::FatArrow
1580                | Self::RegexMatch
1581                | Self::LeftArrow
1582                | Self::RightArrow
1583                | Self::Invalid
1584        )
1585    }
1586
1587    const fn name(self) -> &'static str {
1588        match self {
1589            Self::Identifier => "identifier",
1590            Self::EscapedIdentifier => "escaped identifier",
1591            Self::Parameter => "parameter",
1592            Self::Integer => "integer",
1593            Self::HexInteger => "hex integer",
1594            Self::OctalInteger => "octal integer",
1595            Self::Float => "float",
1596            Self::String => "string",
1597            Self::PatternComprehension => "pattern comprehension",
1598            Self::PatternExpression => "pattern expression",
1599            Self::LabelPredicate => "label predicate",
1600            Self::RelationshipLabelExpression => "relationship label expression",
1601            Self::QualifiedFunctionName => "qualified function name",
1602            Self::All => "ALL",
1603            Self::SetAll => "ALL",
1604            Self::AllShortestPaths => "ALLSHORTESTPATHS",
1605            Self::And => "AND",
1606            Self::Any => "ANY",
1607            Self::As => "AS",
1608            Self::Asc => "ASC",
1609            Self::Ascending => "ASCENDING",
1610            Self::By => "BY",
1611            Self::Call => "CALL",
1612            Self::Case => "CASE",
1613            Self::Contains => "CONTAINS",
1614            Self::Collect => "COLLECT",
1615            Self::Count => "COUNT",
1616            Self::Create => "CREATE",
1617            Self::Delete => "DELETE",
1618            Self::Desc => "DESC",
1619            Self::Descending => "DESCENDING",
1620            Self::Detach => "DETACH",
1621            Self::Distinct => "DISTINCT",
1622            Self::Else => "ELSE",
1623            Self::End => "END",
1624            Self::Ends => "ENDS",
1625            Self::Exists => "EXISTS",
1626            Self::False => "FALSE",
1627            Self::Group => "GROUP",
1628            Self::Groups => "GROUPS",
1629            Self::In => "IN",
1630            Self::Inf => "INF",
1631            Self::Infinity => "INFINITY",
1632            Self::Is => "IS",
1633            Self::Limit => "LIMIT",
1634            Self::Match => "MATCH",
1635            Self::Merge => "MERGE",
1636            Self::Nan => "NAN",
1637            Self::None => "NONE",
1638            Self::Not => "NOT",
1639            Self::Null => "NULL",
1640            Self::Offset => "OFFSET",
1641            Self::On => "ON",
1642            Self::Optional => "OPTIONAL",
1643            Self::Or => "OR",
1644            Self::Order => "ORDER",
1645            Self::Path => "PATH",
1646            Self::Paths => "PATHS",
1647            Self::Reduce => "REDUCE",
1648            Self::Remove => "REMOVE",
1649            Self::Return => "RETURN",
1650            Self::Set => "SET",
1651            Self::Shortest => "SHORTEST",
1652            Self::ShortestPath => "SHORTESTPATH",
1653            Self::Simple => "SIMPLE",
1654            Self::Single => "SINGLE",
1655            Self::Skip => "SKIP",
1656            Self::Starts => "STARTS",
1657            Self::Then => "THEN",
1658            Self::Trail => "TRAIL",
1659            Self::Trim => "TRIM",
1660            Self::True => "TRUE",
1661            Self::Union => "UNION",
1662            Self::Unwind => "UNWIND",
1663            Self::Walk => "WALK",
1664            Self::When => "WHEN",
1665            Self::Where => "WHERE",
1666            Self::With => "WITH",
1667            Self::Xor => "XOR",
1668            Self::Yield => "YIELD",
1669            Self::Acyclic => "ACYCLIC",
1670            Self::LeftParen => "(",
1671            Self::RightParen => ")",
1672            Self::LeftBracket => "[",
1673            Self::RightBracket => "]",
1674            Self::LeftBrace => "{",
1675            Self::RightBrace => "}",
1676            Self::Comma => ",",
1677            Self::Dot => ".",
1678            Self::DotDot => "..",
1679            Self::Colon => ":",
1680            Self::DoubleColon => "::",
1681            Self::Semicolon => ";",
1682            Self::Pipe => "|",
1683            Self::DoublePipe => "||",
1684            Self::Ampersand => "&",
1685            Self::Question => "?",
1686            Self::Dollar => "$",
1687            Self::Plus => "+",
1688            Self::Minus => "-",
1689            Self::Star => "*",
1690            Self::Slash => "/",
1691            Self::Percent => "%",
1692            Self::Caret => "^",
1693            Self::Bang => "!",
1694            Self::Equal => "=",
1695            Self::NotEqual => "<>",
1696            Self::Less => "<",
1697            Self::LessEqual => "<=",
1698            Self::Greater => ">",
1699            Self::GreaterEqual => ">=",
1700            Self::PlusEqual => "+=",
1701            Self::FatArrow => "=>",
1702            Self::RegexMatch => "=~",
1703            Self::LeftArrow => "<-",
1704            Self::RightArrow => "->",
1705            Self::Invalid => "invalid token",
1706        }
1707    }
1708}
1709
1710impl From<TokenKind> for ParserToken {
1711    fn from(kind: TokenKind) -> Self {
1712        match kind {
1713            TokenKind::Keyword(keyword) => keyword_token(keyword),
1714            TokenKind::Identifier => Self::Identifier,
1715            TokenKind::EscapedIdentifier => Self::EscapedIdentifier,
1716            TokenKind::Parameter => Self::Parameter,
1717            TokenKind::Integer => Self::Integer,
1718            TokenKind::HexInteger => Self::HexInteger,
1719            TokenKind::OctalInteger => Self::OctalInteger,
1720            TokenKind::Float => Self::Float,
1721            TokenKind::String => Self::String,
1722            TokenKind::LeftParen => Self::LeftParen,
1723            TokenKind::RightParen => Self::RightParen,
1724            TokenKind::LeftBracket => Self::LeftBracket,
1725            TokenKind::RightBracket => Self::RightBracket,
1726            TokenKind::LeftBrace => Self::LeftBrace,
1727            TokenKind::RightBrace => Self::RightBrace,
1728            TokenKind::Comma => Self::Comma,
1729            TokenKind::Dot => Self::Dot,
1730            TokenKind::DotDot => Self::DotDot,
1731            TokenKind::Colon => Self::Colon,
1732            TokenKind::DoubleColon => Self::DoubleColon,
1733            TokenKind::Semicolon => Self::Semicolon,
1734            TokenKind::Pipe => Self::Pipe,
1735            TokenKind::DoublePipe => Self::DoublePipe,
1736            TokenKind::Ampersand => Self::Ampersand,
1737            TokenKind::Question => Self::Question,
1738            TokenKind::Dollar => Self::Dollar,
1739            TokenKind::Plus => Self::Plus,
1740            TokenKind::Minus => Self::Minus,
1741            TokenKind::Star => Self::Star,
1742            TokenKind::Slash => Self::Slash,
1743            TokenKind::Percent => Self::Percent,
1744            TokenKind::Caret => Self::Caret,
1745            TokenKind::Bang => Self::Bang,
1746            TokenKind::Equal => Self::Equal,
1747            TokenKind::NotEqual => Self::NotEqual,
1748            TokenKind::Less => Self::Less,
1749            TokenKind::LessEqual => Self::LessEqual,
1750            TokenKind::Greater => Self::Greater,
1751            TokenKind::GreaterEqual => Self::GreaterEqual,
1752            TokenKind::PlusEqual => Self::PlusEqual,
1753            TokenKind::FatArrow => Self::FatArrow,
1754            TokenKind::RegexMatch => Self::RegexMatch,
1755            TokenKind::LeftArrow => Self::LeftArrow,
1756            TokenKind::RightArrow => Self::RightArrow,
1757            TokenKind::Invalid => Self::Invalid,
1758            TokenKind::Whitespace | TokenKind::LineComment | TokenKind::BlockComment => {
1759                unreachable!("trivia is filtered before parser token conversion")
1760            }
1761        }
1762    }
1763}
1764
1765fn keyword_token(keyword: Keyword) -> ParserToken {
1766    match keyword {
1767        Keyword::All => ParserToken::All,
1768        Keyword::AllShortestPaths => ParserToken::AllShortestPaths,
1769        Keyword::And => ParserToken::And,
1770        Keyword::Any => ParserToken::Any,
1771        Keyword::As => ParserToken::As,
1772        Keyword::Asc => ParserToken::Asc,
1773        Keyword::Ascending => ParserToken::Ascending,
1774        Keyword::By => ParserToken::By,
1775        Keyword::Call => ParserToken::Call,
1776        Keyword::Case => ParserToken::Case,
1777        Keyword::Contains => ParserToken::Contains,
1778        Keyword::Collect => ParserToken::Collect,
1779        Keyword::Count => ParserToken::Count,
1780        Keyword::Create => ParserToken::Create,
1781        Keyword::Delete => ParserToken::Delete,
1782        Keyword::Desc => ParserToken::Desc,
1783        Keyword::Descending => ParserToken::Descending,
1784        Keyword::Detach => ParserToken::Detach,
1785        Keyword::Distinct => ParserToken::Distinct,
1786        Keyword::Else => ParserToken::Else,
1787        Keyword::End => ParserToken::End,
1788        Keyword::Ends => ParserToken::Ends,
1789        Keyword::Exists => ParserToken::Exists,
1790        Keyword::False => ParserToken::False,
1791        Keyword::Group => ParserToken::Group,
1792        Keyword::Groups => ParserToken::Groups,
1793        Keyword::In => ParserToken::In,
1794        Keyword::Inf => ParserToken::Inf,
1795        Keyword::Infinity => ParserToken::Infinity,
1796        Keyword::Is => ParserToken::Is,
1797        Keyword::Limit => ParserToken::Limit,
1798        Keyword::Match => ParserToken::Match,
1799        Keyword::Merge => ParserToken::Merge,
1800        Keyword::Nan => ParserToken::Nan,
1801        Keyword::None => ParserToken::None,
1802        Keyword::Not => ParserToken::Not,
1803        Keyword::Null => ParserToken::Null,
1804        Keyword::Offset => ParserToken::Offset,
1805        Keyword::On => ParserToken::On,
1806        Keyword::Optional => ParserToken::Optional,
1807        Keyword::Or => ParserToken::Or,
1808        Keyword::Order => ParserToken::Order,
1809        Keyword::Path => ParserToken::Path,
1810        Keyword::Paths => ParserToken::Paths,
1811        Keyword::Reduce => ParserToken::Reduce,
1812        Keyword::Remove => ParserToken::Remove,
1813        Keyword::Return => ParserToken::Return,
1814        Keyword::Set => ParserToken::Set,
1815        Keyword::Shortest => ParserToken::Shortest,
1816        Keyword::ShortestPath => ParserToken::ShortestPath,
1817        Keyword::Simple => ParserToken::Simple,
1818        Keyword::Single => ParserToken::Single,
1819        Keyword::Skip => ParserToken::Skip,
1820        Keyword::Starts => ParserToken::Starts,
1821        Keyword::Then => ParserToken::Then,
1822        Keyword::Trail => ParserToken::Trail,
1823        Keyword::Trim => ParserToken::Trim,
1824        Keyword::True => ParserToken::True,
1825        Keyword::Union => ParserToken::Union,
1826        Keyword::Unwind => ParserToken::Unwind,
1827        Keyword::Walk => ParserToken::Walk,
1828        Keyword::When => ParserToken::When,
1829        Keyword::Where => ParserToken::Where,
1830        Keyword::With => ParserToken::With,
1831        Keyword::Xor => ParserToken::Xor,
1832        Keyword::Yield => ParserToken::Yield,
1833        Keyword::Acyclic => ParserToken::Acyclic,
1834    }
1835}
1836
1837pub(crate) fn name(source: &str, start: usize, end: usize, escaped: bool) -> Name {
1838    let span = Span::new(start, end);
1839    let raw = &source[start..end];
1840    let text = if escaped {
1841        decode_delimited(raw, '`')
1842    } else {
1843        raw.to_owned()
1844    };
1845    Node::new(Identifier::new(text, escaped), span)
1846}
1847
1848pub(crate) fn qualified_function_name(source: &str, start: usize, end: usize) -> QualifiedName {
1849    let fragment = source.get(start..end).unwrap_or_default();
1850    let lexed = lexer::lex(fragment);
1851    let parts = lexed
1852        .tokens
1853        .iter()
1854        .filter(|token| !token.is_trivia() && token.kind != TokenKind::Dot)
1855        .map(|token| {
1856            let part_start = start + token.span.start;
1857            let part_end = start + token.span.end;
1858            name(
1859                source,
1860                part_start,
1861                part_end,
1862                token.kind == TokenKind::EscapedIdentifier,
1863            )
1864        })
1865        .collect();
1866    QualifiedName {
1867        parts,
1868        span: Span::new(start, end),
1869    }
1870}
1871
1872pub(crate) fn parameter(source: &str, start: usize, end: usize) -> Parameter {
1873    let raw = &source[start + 1..end];
1874    let name = if raw.starts_with('`') && raw.ends_with('`') {
1875        ParameterName::Named(decode_delimited(raw, '`'))
1876    } else if raw.bytes().all(|byte| byte.is_ascii_digit()) {
1877        ParameterName::Positional(raw.to_owned())
1878    } else {
1879        ParameterName::Named(raw.to_owned())
1880    };
1881    Parameter { name }
1882}
1883
1884pub(crate) fn string_literal(source: &str, start: usize, end: usize) -> StringLiteral {
1885    let raw = &source[start..end];
1886    let delimiter = raw.chars().next().unwrap_or('\'');
1887    let quote = if delimiter == '\'' {
1888        QuoteStyle::Single
1889    } else {
1890        QuoteStyle::Double
1891    };
1892    StringLiteral {
1893        value: decode_delimited(raw, delimiter),
1894        quote,
1895    }
1896}
1897
1898fn decode_delimited(raw: &str, delimiter: char) -> String {
1899    let delimiter_len = delimiter.len_utf8();
1900    let inner = raw
1901        .get(delimiter_len..raw.len().saturating_sub(delimiter_len))
1902        .unwrap_or_default();
1903    let mut decoded = String::with_capacity(inner.len());
1904    let mut characters = inner.chars().peekable();
1905    while let Some(character) = characters.next() {
1906        if character == delimiter && characters.peek() == Some(&delimiter) {
1907            let _ = characters.next();
1908            decoded.push(delimiter);
1909            continue;
1910        }
1911        if character != '\\' {
1912            decoded.push(character);
1913            continue;
1914        }
1915        let Some(escaped) = characters.next() else {
1916            decoded.push('\\');
1917            break;
1918        };
1919        match escaped {
1920            't' => decoded.push('\t'),
1921            'b' => decoded.push('\u{0008}'),
1922            'n' => decoded.push('\n'),
1923            'r' => decoded.push('\r'),
1924            'f' => decoded.push('\u{000c}'),
1925            '\\' => decoded.push('\\'),
1926            '\'' => decoded.push('\''),
1927            '"' => decoded.push('"'),
1928            '`' => decoded.push('`'),
1929            'u' | 'U' => {
1930                let digits = if escaped == 'u' { 4 } else { 6 };
1931                let mut scalar = 0u32;
1932                let mut complete = true;
1933                for _ in 0..digits {
1934                    match characters.next().and_then(|digit| digit.to_digit(16)) {
1935                        Some(digit) => scalar = scalar * 16 + digit,
1936                        None => {
1937                            complete = false;
1938                            break;
1939                        }
1940                    }
1941                }
1942                if complete && let Some(character) = char::from_u32(scalar) {
1943                    decoded.push(character);
1944                }
1945            }
1946            other => {
1947                // Invalid escapes are rejected by the lexer, so preserving an
1948                // unexpected spelling here is only a defensive fallback.
1949                decoded.push('\\');
1950                decoded.push(other);
1951            }
1952        }
1953    }
1954    decoded
1955}
1956
1957pub(crate) fn integer_literal(
1958    source: &str,
1959    start: usize,
1960    end: usize,
1961    radix: IntegerRadix,
1962) -> Literal {
1963    let span = Span::new(start, end);
1964    Node::new(
1965        LiteralKind::Integer(IntegerLiteral {
1966            text: source[start..end].to_owned(),
1967            radix,
1968        }),
1969        span,
1970    )
1971}
1972
1973pub(crate) fn list_singleton_or_comprehension(start: usize, end: usize, expression: Expr) -> Expr {
1974    let span = Span::new(start, end);
1975    match split_comprehension_source(expression) {
1976        ComprehensionSource::Comprehension { variable, list } => Node::new(
1977            ExprKind::ListComprehension(ListComprehension {
1978                variable,
1979                list: Box::new(list),
1980                predicate: None,
1981                projection: None,
1982            }),
1983            span,
1984        ),
1985        ComprehensionSource::Expression(expression) => {
1986            Node::new(ExprKind::List(vec![expression]), span)
1987        }
1988    }
1989}
1990
1991pub(crate) fn list_comprehension_expr(
1992    start: usize,
1993    end: usize,
1994    source_expression: Expr,
1995    predicate: Option<Expr>,
1996    projection: Option<Expr>,
1997) -> Result<Expr, LalrpopError> {
1998    let ComprehensionSource::Comprehension { variable, list } =
1999        split_comprehension_source(source_expression)
2000    else {
2001        return Err(user_parse_error(start));
2002    };
2003    Ok(Node::new(
2004        ExprKind::ListComprehension(ListComprehension {
2005            variable,
2006            list: Box::new(list),
2007            predicate: predicate.map(Box::new),
2008            projection: projection.map(Box::new),
2009        }),
2010        Span::new(start, end),
2011    ))
2012}
2013
2014enum ComprehensionSource {
2015    Comprehension { variable: Name, list: Expr },
2016    Expression(Expr),
2017}
2018
2019fn split_comprehension_source(expression: Expr) -> ComprehensionSource {
2020    let is_source = matches!(
2021        &expression.kind,
2022        ExprKind::Binary { left, operator, .. }
2023            if operator.kind == BinaryOperator::In
2024                && matches!(&left.kind, ExprKind::Variable(_))
2025    );
2026    if !is_source {
2027        return ComprehensionSource::Expression(expression);
2028    }
2029
2030    let ExprKind::Binary {
2031        left,
2032        operator: _,
2033        right,
2034    } = expression.kind
2035    else {
2036        unreachable!("the source shape was checked above")
2037    };
2038    let ExprKind::Variable(variable) = left.kind else {
2039        unreachable!("the source variable shape was checked above")
2040    };
2041    ComprehensionSource::Comprehension {
2042        variable,
2043        list: *right,
2044    }
2045}
2046
2047pub(crate) fn pattern_comprehension(
2048    source: &str,
2049    start: usize,
2050    end: usize,
2051) -> Result<Expr, LalrpopError> {
2052    let Some(fragment) = source.get(start..end) else {
2053        return Err(user_parse_error(start));
2054    };
2055    let lexed = lexer::lex(fragment);
2056    if !lexed.diagnostics.is_empty() {
2057        return Err(user_parse_error(start));
2058    }
2059    // A `|` at the comprehension's top level may be either the projection
2060    // separator or part of a label disjunction (for example `b:X|Y`). Try
2061    // each candidate as the separator, left to right; reserving it keeps the
2062    // label scanner from consuming it, and the first split that yields a
2063    // valid comprehension wins.
2064    // When every split fails, report the one that got farthest: it is the
2065    // most plausible reading, and its error points into the fragment instead
2066    // of at the comprehension as a whole.
2067    let mut best_error = None;
2068    for pipe_index in top_level_pipe_candidates(&lexed.tokens) {
2069        let tokens = nested_parser_tokens(&lexed.tokens, start, Some(pipe_index));
2070        match parse_comprehension_tokens(source, start, end, &tokens) {
2071            Ok(expression) => return Ok(expression),
2072            Err(error) => retain_farthest_error(&mut best_error, error),
2073        }
2074    }
2075    Err(best_error.unwrap_or_else(|| user_parse_error(start)))
2076}
2077
2078fn top_level_pipe_candidates(tokens: &[Token]) -> Vec<usize> {
2079    let mut candidates = Vec::new();
2080    let mut brackets = 0usize;
2081    let mut parentheses = 0usize;
2082    let mut braces = 0usize;
2083    for (index, token) in tokens.iter().enumerate() {
2084        if token.is_trivia() {
2085            continue;
2086        }
2087        match token.kind {
2088            TokenKind::LeftBracket => brackets += 1,
2089            TokenKind::RightBracket => brackets = brackets.saturating_sub(1),
2090            TokenKind::LeftParen => parentheses += 1,
2091            TokenKind::RightParen => parentheses = parentheses.saturating_sub(1),
2092            TokenKind::LeftBrace => braces += 1,
2093            TokenKind::RightBrace => braces = braces.saturating_sub(1),
2094            TokenKind::Pipe if brackets == 1 && parentheses == 0 && braces == 0 => {
2095                candidates.push(index);
2096            }
2097            _ => {}
2098        }
2099    }
2100    candidates
2101}
2102
2103fn parse_comprehension_tokens(
2104    source: &str,
2105    start: usize,
2106    end: usize,
2107    tokens: &[SpannedParserToken],
2108) -> Result<Expr, LalrpopError> {
2109    let outer_start = tokens
2110        .iter()
2111        .position(|token| token.0 == start && token.1 == ParserToken::LeftBracket)
2112        .ok_or_else(|| user_parse_error(start))?;
2113    let outer_end = tokens
2114        .iter()
2115        .rposition(|token| token.2 == end && token.1 == ParserToken::RightBracket)
2116        .ok_or_else(|| user_parse_error(start))?;
2117
2118    let pipe = find_top_level_token(tokens, outer_start + 1, outer_end, ParserToken::Pipe)
2119        .ok_or_else(|| user_parse_error(start))?;
2120
2121    let mut pattern_start = outer_start + 1;
2122    let binding = if pattern_start + 1 < pipe
2123        && tokens[pattern_start + 1].1 == ParserToken::Equal
2124        && (is_name_token(tokens[pattern_start].1)
2125            || tokens[pattern_start].1.is_contextual_name_candidate())
2126    {
2127        let token = tokens[pattern_start];
2128        pattern_start += 2;
2129        Some(name(
2130            source,
2131            token.0,
2132            token.2,
2133            token.1 == ParserToken::EscapedIdentifier,
2134        ))
2135    } else {
2136        None
2137    };
2138
2139    let where_index = find_top_level_token(tokens, pattern_start, pipe, ParserToken::Where);
2140    let pattern_end = where_index.unwrap_or(pipe);
2141    if pattern_start >= pattern_end {
2142        return Err(user_parse_error(tokens[pattern_end].0));
2143    }
2144    if pipe + 1 >= outer_end {
2145        return Err(user_parse_error(tokens[outer_end].0));
2146    }
2147    let pattern = parse_pattern_fragment(source, &tokens[pattern_start..pattern_end])?;
2148    let predicate = match where_index {
2149        Some(index) if index + 1 < pipe => Some(Box::new(parse_expr_fragment(
2150            source,
2151            &tokens[index + 1..pipe],
2152        )?)),
2153        Some(_) => return Err(user_parse_error(tokens[pipe].0)),
2154        None => None,
2155    };
2156    let projection = parse_expr_fragment(source, &tokens[pipe + 1..outer_end])?;
2157
2158    Ok(Node::new(
2159        ExprKind::PatternComprehension(PatternComprehension {
2160            binding,
2161            pattern,
2162            predicate,
2163            projection: Box::new(projection),
2164        }),
2165        Span::new(start, end),
2166    ))
2167}
2168
2169pub(crate) fn pattern_expression(
2170    source: &str,
2171    start: usize,
2172    end: usize,
2173) -> Result<Expr, LalrpopError> {
2174    let Some(fragment) = source.get(start..end) else {
2175        return Err(user_parse_error(start));
2176    };
2177    let lexed = lexer::lex(fragment);
2178    if !lexed.diagnostics.is_empty() {
2179        return Err(user_parse_error(start));
2180    }
2181    let tokens = nested_parser_tokens(&lexed.tokens, start, None);
2182    let pattern = parse_pattern_fragment(source, &tokens)?;
2183    Ok(Node::new(ExprKind::Pattern(pattern), Span::new(start, end)))
2184}
2185
2186fn parse_pattern_fragment(
2187    source: &str,
2188    tokens: &[SpannedParserToken],
2189) -> Result<Pattern, LalrpopError> {
2190    parse_tokens_with_contextual_names(tokens, |contextual| {
2191        generated::PatternFragmentParser::new().parse(source, contextual.iter().copied().map(Ok))
2192    })
2193}
2194
2195fn parse_expr_fragment(source: &str, tokens: &[SpannedParserToken]) -> Result<Expr, LalrpopError> {
2196    parse_tokens_with_contextual_names(tokens, |contextual| {
2197        generated::ExprFragmentParser::new().parse(source, contextual.iter().copied().map(Ok))
2198    })
2199}
2200
2201pub(crate) fn label_comparison_suffix(
2202    source: &str,
2203    start: usize,
2204    end: usize,
2205) -> Result<ComparisonSuffix, LalrpopError> {
2206    Ok(ComparisonSuffix::Is {
2207        negated: false,
2208        predicate: IsPredicate::Label(parse_label_expression(source, start, end, true)?),
2209        end,
2210    })
2211}
2212
2213pub(crate) fn relationship_label_expression(
2214    source: &str,
2215    start: usize,
2216    end: usize,
2217) -> Result<LabelExpression, LalrpopError> {
2218    parse_label_expression(source, start, end, true)
2219}
2220
2221fn parse_label_expression(
2222    source: &str,
2223    start: usize,
2224    end: usize,
2225    normalize_legacy_relationship: bool,
2226) -> Result<LabelExpression, LalrpopError> {
2227    let Some(fragment) = source.get(start..end) else {
2228        return Err(user_parse_error(start));
2229    };
2230    let lexed = lexer::lex(fragment);
2231    if !lexed.diagnostics.is_empty() {
2232        return Err(user_parse_error(start));
2233    }
2234    let mut tokens = lexed
2235        .tokens
2236        .iter()
2237        .filter(|token| !token.is_trivia())
2238        .map(|token| {
2239            (
2240                start + token.span.start,
2241                ParserToken::from(token.kind),
2242                start + token.span.end,
2243            )
2244        })
2245        .collect::<Vec<_>>();
2246    if normalize_legacy_relationship && !legacy_label_tokens_are_consistent(&tokens) {
2247        return Err(user_parse_error(start));
2248    }
2249    let mut normalized = Vec::with_capacity(tokens.len());
2250    for (index, mut token) in tokens.drain(..).enumerate() {
2251        if normalize_legacy_relationship
2252            && token.1 == ParserToken::Colon
2253            && normalized
2254                .last()
2255                .is_some_and(|previous: &SpannedParserToken| previous.1 == ParserToken::Pipe)
2256        {
2257            continue;
2258        }
2259        if index > 0 && token.1.is_contextual_name_candidate() {
2260            token.1 = ParserToken::Identifier;
2261        }
2262        normalized.push(token);
2263    }
2264    generated::LabelPredicateFragmentParser::new().parse(source, normalized.into_iter().map(Ok))
2265}
2266
2267fn legacy_label_tokens_are_consistent(tokens: &[SpannedParserToken]) -> bool {
2268    let has_internal_colon = tokens
2269        .iter()
2270        .enumerate()
2271        .skip(1)
2272        .any(|(_, token)| token.1 == ParserToken::Colon);
2273    if !has_internal_colon {
2274        return true;
2275    }
2276    if tokens
2277        .first()
2278        .is_none_or(|token| token.1 != ParserToken::Colon)
2279    {
2280        return false;
2281    }
2282
2283    let mut cursor = 1usize;
2284    if tokens
2285        .get(cursor)
2286        .is_none_or(|token| !is_name_token(token.1) && !token.1.is_contextual_name_candidate())
2287    {
2288        return false;
2289    }
2290    cursor += 1;
2291    let pipe_separated = tokens
2292        .get(cursor)
2293        .is_some_and(|token| token.1 == ParserToken::Pipe);
2294    while cursor < tokens.len() {
2295        if pipe_separated {
2296            if tokens
2297                .get(cursor)
2298                .is_none_or(|token| token.1 != ParserToken::Pipe)
2299                || tokens
2300                    .get(cursor + 1)
2301                    .is_none_or(|token| token.1 != ParserToken::Colon)
2302            {
2303                return false;
2304            }
2305            cursor += 2;
2306        } else {
2307            if tokens
2308                .get(cursor)
2309                .is_none_or(|token| token.1 != ParserToken::Colon)
2310            {
2311                return false;
2312            }
2313            cursor += 1;
2314        }
2315        if tokens
2316            .get(cursor)
2317            .is_none_or(|token| !is_name_token(token.1) && !token.1.is_contextual_name_candidate())
2318        {
2319            return false;
2320        }
2321        cursor += 1;
2322    }
2323    true
2324}
2325
2326fn user_parse_error(location: usize) -> LalrpopError {
2327    ParseError::User { error: location }
2328}
2329
2330/// `reserved_pipe` is a raw index into `tokens` naming a `|` that must come
2331/// through as a plain `Pipe` token (a pattern comprehension's projection
2332/// separator): forward scans starting before it see a truncated stream so
2333/// they cannot consume it, while context checks keep the full view.
2334fn nested_parser_tokens(
2335    tokens: &[Token],
2336    base: usize,
2337    reserved_pipe: Option<usize>,
2338) -> Vec<SpannedParserToken> {
2339    let significant = tokens
2340        .iter()
2341        .filter(|token| !token.is_trivia())
2342        .collect::<Vec<_>>();
2343    let boundary = reserved_pipe.map(|raw| {
2344        tokens[..raw]
2345            .iter()
2346            .filter(|token| !token.is_trivia())
2347            .count()
2348    });
2349    let mut result = Vec::with_capacity(significant.len());
2350    let mut index = 0;
2351    while index < significant.len() {
2352        let scan: &[&Token] = match boundary {
2353            Some(boundary) if index < boundary => &significant[..boundary],
2354            _ => &significant,
2355        };
2356        if let Some(end) = escaped_parameter_end(scan, index) {
2357            result.push((
2358                base + significant[index].span.start,
2359                ParserToken::Parameter,
2360                base + significant[end].span.end,
2361            ));
2362            index = end + 1;
2363            continue;
2364        }
2365        if index != 0 {
2366            if let Some(end) = pattern_comprehension_end(scan, index) {
2367                result.push((
2368                    base + significant[index].span.start,
2369                    ParserToken::PatternComprehension,
2370                    base + significant[end].span.end,
2371                ));
2372                index = end + 1;
2373                continue;
2374            }
2375            if significant[index].kind == TokenKind::LeftParen
2376                && is_pattern_expression_context(&significant, index)
2377                && let Some(end) = pattern_expression_end(scan, index)
2378            {
2379                result.push((
2380                    base + significant[index].span.start,
2381                    ParserToken::PatternExpression,
2382                    base + significant[end].span.end,
2383                ));
2384                index = end + 1;
2385                continue;
2386            }
2387            if is_relationship_label_start(&significant, index)
2388                && let Some(end) = relationship_label_end(scan, index)
2389            {
2390                result.push((
2391                    base + significant[index].span.start,
2392                    ParserToken::RelationshipLabelExpression,
2393                    base + significant[end].span.end,
2394                ));
2395                index = end + 1;
2396                continue;
2397            }
2398            if is_label_predicate_context(&significant, index)
2399                && let Some(end) = label_predicate_end(scan, index)
2400            {
2401                result.push((
2402                    base + significant[index].span.start,
2403                    ParserToken::LabelPredicate,
2404                    base + significant[end].span.end,
2405                ));
2406                index = end + 1;
2407                continue;
2408            }
2409        }
2410
2411        if !is_procedure_name_start(&significant, index)
2412            && let Some(end) = qualified_function_name_end(scan, index)
2413        {
2414            result.push((
2415                base + significant[index].span.start,
2416                ParserToken::QualifiedFunctionName,
2417                base + significant[end].span.end,
2418            ));
2419            index = end + 1;
2420            continue;
2421        }
2422
2423        let token = significant[index];
2424        let mut parser_token = ParserToken::from(token.kind);
2425        if parser_token == ParserToken::All && is_explicit_set_all(&significant, index) {
2426            parser_token = ParserToken::SetAll;
2427        }
2428        result.push((base + token.span.start, parser_token, base + token.span.end));
2429        index += 1;
2430    }
2431    result
2432}
2433
2434fn find_top_level_token(
2435    tokens: &[SpannedParserToken],
2436    start: usize,
2437    end: usize,
2438    needle: ParserToken,
2439) -> Option<usize> {
2440    let mut parentheses = 0usize;
2441    let mut brackets = 0usize;
2442    let mut braces = 0usize;
2443    for (index, (_, token, _)) in tokens.iter().copied().enumerate().take(end).skip(start) {
2444        if parentheses == 0 && brackets == 0 && braces == 0 && token == needle {
2445            return Some(index);
2446        }
2447        match token {
2448            ParserToken::LeftParen => parentheses += 1,
2449            ParserToken::RightParen => parentheses = parentheses.saturating_sub(1),
2450            ParserToken::LeftBracket => brackets += 1,
2451            ParserToken::RightBracket => brackets = brackets.saturating_sub(1),
2452            ParserToken::LeftBrace => braces += 1,
2453            ParserToken::RightBrace => braces = braces.saturating_sub(1),
2454            _ => {}
2455        }
2456    }
2457    None
2458}
2459
2460fn is_name_token(token: ParserToken) -> bool {
2461    matches!(
2462        token,
2463        ParserToken::Identifier
2464            | ParserToken::EscapedIdentifier
2465            | ParserToken::Match
2466            | ParserToken::Path
2467            | ParserToken::Return
2468    )
2469}
2470
2471pub(crate) fn query_from_pattern(pattern: Pattern, where_clause: Option<Expr>) -> Query {
2472    let span = pattern.span;
2473    let clause = Node::new(
2474        ClauseKind::Match(MatchClause {
2475            optional: false,
2476            mode: None,
2477            pattern,
2478            hints: Vec::new(),
2479            where_clause,
2480        }),
2481        span,
2482    );
2483    let head = Node::new(
2484        SingleQueryKind {
2485            clauses: vec![clause],
2486        },
2487        span,
2488    );
2489    Node::new(
2490        QueryKind::Regular(RegularQuery {
2491            head,
2492            unions: Vec::new(),
2493        }),
2494        span,
2495    )
2496}
2497
2498pub(crate) type RelationshipDetail = (
2499    Option<Name>,
2500    Option<crate::ast::LabelExpression>,
2501    Option<Quantifier>,
2502    Option<crate::ast::Expr>,
2503    Option<crate::ast::Expr>,
2504);
2505
2506pub(crate) fn relationship_factor(
2507    start: usize,
2508    end: usize,
2509    direction: RelationshipDirection,
2510    detail: Option<RelationshipDetail>,
2511    outer_quantifier: Option<Quantifier>,
2512) -> PathFactor {
2513    let (variable, labels, inner_quantifier, properties, where_clause) =
2514        detail.unwrap_or((None, None, None, None, None));
2515
2516    Node::new(
2517        PathFactorKind::Relationship(RelationshipPattern {
2518            direction: Node::new(direction, Span::new(start, end)),
2519            variable,
2520            labels,
2521            legacy_quantifier: inner_quantifier,
2522            graph_quantifier: outer_quantifier,
2523            properties: properties.map(Box::new),
2524            where_clause: where_clause.map(Box::new),
2525        }),
2526        Span::new(start, end),
2527    )
2528}
2529
2530pub(crate) fn fold_binary(mut expression: Expr, tail: Vec<(Node<BinaryOperator>, Expr)>) -> Expr {
2531    for (operator, right) in tail {
2532        let span = expression.span.cover(right.span);
2533        expression = Node::new(
2534            ExprKind::Binary {
2535                left: Box::new(expression),
2536                operator,
2537                right: Box::new(right),
2538            },
2539            span,
2540        );
2541    }
2542    expression
2543}
2544
2545pub(crate) enum ComparisonSuffix {
2546    Binary(Node<BinaryOperator>, Expr),
2547    Is {
2548        negated: bool,
2549        predicate: IsPredicate,
2550        end: usize,
2551    },
2552}
2553
2554pub(crate) fn fold_comparison(mut expression: Expr, tail: Vec<ComparisonSuffix>) -> Expr {
2555    for suffix in tail {
2556        match suffix {
2557            ComparisonSuffix::Binary(operator, right) => {
2558                let span = expression.span.cover(right.span);
2559                expression = Node::new(
2560                    ExprKind::Binary {
2561                        left: Box::new(expression),
2562                        operator,
2563                        right: Box::new(right),
2564                    },
2565                    span,
2566                );
2567            }
2568            ComparisonSuffix::Is {
2569                negated,
2570                predicate,
2571                end,
2572            } => {
2573                let span = Span::new(expression.span.start, end);
2574                expression = Node::new(
2575                    ExprKind::Is {
2576                        expression: Box::new(expression),
2577                        negated,
2578                        predicate,
2579                    },
2580                    span,
2581                );
2582            }
2583        }
2584    }
2585    expression
2586}
2587
2588pub(crate) enum PostfixSuffix {
2589    Property(Name),
2590    MapProjection {
2591        items: Vec<MapProjectionItem>,
2592        end: usize,
2593    },
2594    Index {
2595        index: Expr,
2596        end: usize,
2597    },
2598    Slice {
2599        lower: Option<Expr>,
2600        upper: Option<Expr>,
2601        end: usize,
2602    },
2603}
2604
2605pub(crate) fn fold_postfix(mut expression: Expr, suffixes: Vec<PostfixSuffix>) -> Expr {
2606    for suffix in suffixes {
2607        expression = match suffix {
2608            PostfixSuffix::Property(key) => {
2609                let span = expression.span.cover(key.span);
2610                Node::new(
2611                    ExprKind::Property {
2612                        expression: Box::new(expression),
2613                        key,
2614                    },
2615                    span,
2616                )
2617            }
2618            PostfixSuffix::MapProjection { items, end } => {
2619                let span = Span::new(expression.span.start, end);
2620                Node::new(
2621                    ExprKind::MapProjection(MapProjection {
2622                        base: Box::new(expression),
2623                        items,
2624                    }),
2625                    span,
2626                )
2627            }
2628            PostfixSuffix::Index { index, end } => {
2629                let span = Span::new(expression.span.start, end);
2630                Node::new(
2631                    ExprKind::Index {
2632                        expression: Box::new(expression),
2633                        index: Box::new(index),
2634                    },
2635                    span,
2636                )
2637            }
2638            PostfixSuffix::Slice { lower, upper, end } => {
2639                let span = Span::new(expression.span.start, end);
2640                Node::new(
2641                    ExprKind::Slice {
2642                        expression: Box::new(expression),
2643                        lower: lower.map(Box::new),
2644                        upper: upper.map(Box::new),
2645                    },
2646                    span,
2647                )
2648            }
2649        };
2650    }
2651    expression
2652}
2653
2654/// Recover only at balanced, top-level boundaries. Parse each candidate with
2655/// the same contextual-name rules as strict parsing, so keyword-shaped names
2656/// and compound clauses (OPTIONAL MATCH, MERGE actions) remain intact.
2657fn recover_clauses(source: &str, tokens: &[SpannedParserToken]) -> Option<Program> {
2658    let (tokens, terminator) = match tokens.last() {
2659        Some((start, ParserToken::Semicolon, end)) => {
2660            (&tokens[..tokens.len() - 1], Some(Span::new(*start, *end)))
2661        }
2662        _ => (tokens, None),
2663    };
2664    let first = tokens.first()?;
2665    let last = tokens.last()?;
2666    // UNION may also be a contextual name. Without a successful strict parse,
2667    // leave its interpretation to the caller rather than flattening branches.
2668    if !top_level_indices(tokens, |token| {
2669        matches!(token, ParserToken::Union | ParserToken::Semicolon)
2670    })
2671    .is_empty()
2672    {
2673        return None;
2674    }
2675    let mut boundaries = top_level_indices(tokens, clause_starts);
2676    boundaries.push(tokens.len());
2677    let mut clauses = Vec::new();
2678    let mut cursor = 0;
2679    let mut skipped = None;
2680    let mut retained = false;
2681    let mut returned = false;
2682    // Recovery is best-effort: bound additional parser work on adversarial
2683    // inputs with many candidate boundaries. Exhaustion preserves the suffix
2684    // as one error region instead of repeatedly reparsing it.
2685    let mut attempts = 128usize;
2686    while cursor < tokens.len() {
2687        let mut parsed = None;
2688        if !returned {
2689            for &end in boundaries.iter().filter(|&&end| end > cursor) {
2690                if attempts == 0 {
2691                    break;
2692                }
2693                attempts -= 1;
2694                let candidate = parse_tokens_with_contextual_names(&tokens[cursor..end], |input| {
2695                    generated::ClauseRootParser::new().parse(source, input.iter().copied().map(Ok))
2696                });
2697                if let Ok(clause) = candidate {
2698                    // Prefer the longest valid clause: shorter candidates can
2699                    // mistake a non-reserved name or MERGE action for a boundary.
2700                    parsed = Some((clause, end));
2701                }
2702            }
2703        }
2704        if let Some((clause, end)) = parsed {
2705            if let Some(start) = skipped.take() {
2706                clauses.push(Node::new(
2707                    ClauseKind::Error(ErrorNode),
2708                    Span::new(start, tokens[cursor - 1].2),
2709                ));
2710            }
2711            returned = matches!(clause.kind, ClauseKind::Return(_));
2712            clauses.push(clause);
2713            retained = true;
2714            cursor = end;
2715        } else {
2716            skipped.get_or_insert(tokens[cursor].0);
2717            cursor = if returned || attempts == 0 {
2718                tokens.len()
2719            } else {
2720                *boundaries.iter().find(|&&end| end > cursor)?
2721            };
2722        }
2723    }
2724    if !retained {
2725        return None;
2726    }
2727    if let Some(start) = skipped {
2728        clauses.push(Node::new(
2729            ClauseKind::Error(ErrorNode),
2730            Span::new(start, last.2),
2731        ));
2732    }
2733    let query_span = Span::new(first.0, last.2);
2734    let head = Node::new(SingleQueryKind { clauses }, query_span);
2735    let query = Node::new(
2736        QueryKind::Regular(RegularQuery {
2737            head,
2738            unions: Vec::new(),
2739        }),
2740        query_span,
2741    );
2742    let span = terminator.map_or(query_span, |end| query_span.cover(end));
2743    let statement = Node::new(
2744        StatementKind::Query(QueryStatement {
2745            mode: None,
2746            query,
2747            terminator,
2748        }),
2749        span,
2750    );
2751    Some(Program::new(vec![statement], span))
2752}