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