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