Skip to the content.

Cypher Parser

The Fyper.Parser package provides a zero-external-dependency Cypher parser that converts Cypher query strings into Fyper’s typed AST (Fyper.Ast). This enables round-tripping (parse Cypher string -> manipulate AST -> compile back to string), query analysis, validation, and transformation workflows.

Package

Fyper.Parser  (depends on: Fyper)

Quick Start

open Fyper.Parser
open Fyper.Ast
open Fyper.CypherCompiler

// Parse a Cypher string into an AST
let query = CypherParser.parse "MATCH (p:Person) WHERE p.age > 30 RETURN p.name"
// query.Clauses:
//   [ Match([NodePattern("p", Some "Person", Map.empty)], false)
//     Where(BinOp(Property("p", "age"), Gt, Literal 30L))
//     Return([{ Expr = Property("p", "name"); Alias = None }], false) ]

// Compile it back to a Cypher string
let compiled = compile query
// compiled.Cypher = "MATCH (p:Person)\nWHERE (p.age > 30)\nRETURN p.name"

Lexer

The lexer (Fyper.Parser.Lexer) converts a Cypher string into a token list using a hand-written, character-by-character scanner. It is defined in src/Fyper.Parser/Lexer.fs.

Token Types

type Token =
    // ── Keywords ──
    | MATCH | OPTIONAL | WHERE | RETURN | WITH | CREATE | MERGE
    | DELETE | DETACH | SET | REMOVE | ORDER | BY | ASC | DESC
    | SKIP | LIMIT | UNWIND | AS | DISTINCT | UNION | ALL
    | ON | CASE | WHEN | THEN | ELSE | END
    | AND | OR | XOR | NOT | IN | IS | NULL
    | TRUE | FALSE
    | CONTAINS | STARTS | ENDS
    | EXISTS | CALL | YIELD

    // ── Symbols ──
    | LPAREN | RPAREN         // ( )
    | LBRACKET | RBRACKET     // [ ]
    | LBRACE | RBRACE         // { }
    | COLON | COMMA | DOT     // : , .
    | PIPE | STAR | PLUS      // | * +
    | MINUS | SLASH | PERCENT  // - / %
    | CARET | EQ | NEQ        // ^ = <>
    | LT | GT | LTE | GTE    // < > <= >=
    | ARROW_RIGHT             // ->
    | ARROW_LEFT              // <-
    | DASH                    // -
    | DOLLAR                  // $
    | REGEX_MATCH             // =~
    | PLUS_EQ                 // +=

    // ── Literals ──
    | STRING of string        // 'hello' or "hello"
    | INTEGER of int64        // 42
    | FLOAT of float          // 3.14
    | IDENTIFIER of string    // variable/label names
    | PARAMETER of string     // $paramName

    // ── Control ──
    | EOF
    | NEWLINE

Lexer API

module Lexer =
    /// Create a new lexer state from an input string
    val create : input: string -> LexerState

    /// Read the next token from the lexer state
    val nextToken : state: LexerState -> Token

    /// Tokenize an entire Cypher string into a token list (newlines stripped)
    val tokenize : input: string -> Token list

Lexer Features

Parser

The parser (Fyper.Parser.CypherParser) is a recursive-descent parser that converts token streams into Fyper’s Clause, Expr, and Pattern AST types. It is defined in src/Fyper.Parser/Parser.fs.

Parser API

module CypherParser =
    /// Parse a full Cypher query string into a CypherQuery AST.
    /// Parameters referenced as $name are collected into the Parameters map (with null values).
    val parse : cypher: string -> CypherQuery<obj>

    /// Parse Cypher and return only the clause list (no parameter collection).
    val parseClauses' : cypher: string -> Clause list

Supported Cypher Grammar

The parser handles the following clause types:

Clause Example
MATCH MATCH (n:Person)
OPTIONAL MATCH OPTIONAL MATCH (n)-[r]->(m)
WHERE WHERE n.age > 30 AND n.name CONTAINS 'A'
RETURN RETURN n, m.name AS title
RETURN DISTINCT RETURN DISTINCT n.label
WITH WITH n, count(*) AS cnt
WITH DISTINCT WITH DISTINCT n.type AS t
CREATE CREATE (n:Person {name: 'Alice'})
MERGE MERGE (n:Person {name: 'Alice'}) ON MATCH SET n.age = 31 ON CREATE SET n.age = 30
DELETE DELETE n, m
DETACH DELETE DETACH DELETE n
SET SET n.age = 31, n:Active
REMOVE REMOVE n.temp, n:Inactive
ORDER BY ORDER BY n.age DESC, n.name ASC
SKIP SKIP 10
LIMIT LIMIT 25
UNWIND UNWIND [1, 2, 3] AS x
UNION / UNION ALL UNION ALL
CALL CALL db.labels() YIELD label

Expression Precedence

The parser implements correct operator precedence using precedence climbing:

Precedence (low to high) Operators
1 OR
2 XOR
3 AND
4 NOT (unary prefix)
5 =, <>, <, >, <=, >=, =~, IN, IS NULL, IS NOT NULL
6 CONTAINS, STARTS WITH, ENDS WITH
7 +, -
8 *, /, %
9 Unary -
10 Primary: literals, variables, property access, function calls, (...), [...], {...}, CASE, EXISTS

Expression Types Parsed

Pattern Parser

Patterns are parsed with full support for:

SET Item Parsing

The parser recognizes four SET item forms:

Syntax AST Meaning
n.prop = expr SetProperty("n", "prop", expr) Set single property
n = expr SetAllProperties("n", expr) Replace all properties
n += expr MergeProperties("n", expr) Merge properties
n:Label AddLabel("n", "Label") Add label

REMOVE Item Parsing

Syntax AST
n.prop RemoveProperty("n", "prop")
n:Label RemoveLabel("n", "Label")

CALL Clause

Supports dotted procedure names and optional YIELD:

CALL db.labels() YIELD label
CALL apoc.do.when(condition, query1, query2)

Round-Trip Example

Parse a Cypher string, inspect the AST, and compile it back:

open Fyper.Parser
open Fyper.Ast
open Fyper.CypherCompiler

let input = """
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
WHERE p.age > 30 AND m.year >= 2000
RETURN p.name AS name, m.title AS title
ORDER BY m.year DESC
LIMIT 10
"""

// Parse
let query = CypherParser.parse input

// Inspect clauses
for clause in query.Clauses do
    printfn "%A" clause
// Match([RelPattern(NodePattern("p", Some "Person", ...), ...)], false)
// Where(BinOp(BinOp(Property("p","age"), Gt, Literal 30L), And, BinOp(...)))
// Return([{Expr=Property("p","name"); Alias=Some "name"}; ...], false)
// OrderBy([(Property("m","year"), Descending)])
// Limit(Literal 10L)

// Compile back to Cypher
let result = compile query
printfn "%s" result.Cypher
// MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
// WHERE ((p.age > 30) AND (m.year >= 2000))
// RETURN p.name AS name, m.title AS title
// ORDER BY m.year DESC
// LIMIT 10

AST Manipulation Example

Parse, transform, and re-compile:

open Fyper.Parser
open Fyper.Ast
open Fyper.CypherCompiler

let query = CypherParser.parse "MATCH (p:Person) RETURN p"

// Add a WHERE clause after MATCH
let withFilter = {
    query with
        Clauses =
            query.Clauses
            |> List.collect (fun c ->
                match c with
                | Match _ -> [c; Where(BinOp(Property("p", "age"), Gt, Param "minAge"))]
                | _ -> [c])
        Parameters = query.Parameters |> Map.add "minAge" (box 25)
}

let result = compile withFilter
// MATCH (p:Person)
// WHERE (p.age > $minAge)
// RETURN p

See Also