Parser combinator
A parser combinator is a higher-order function that builds a parser from smaller parsers: the sequence of two parsers, the choice between them, the repetition of one. The parsers themselves are ordinary values of the programming language, so a grammar is written as a program that is its own parser, with no separate parser generator, and can use the language's abstractions: functions, modules, types. Combinators produce recursive descent parsers, which do not accept left-recursive rules and, with unlimited backtracking, can take exponential time; memoizing them, as packrat parsing does, makes them linear[7]. Parsers form a monad, and the most widely used libraries, such as Parsec for Haskell, are organized around that structure[4][5].
Overview
A parser is a function from input to a result. The simplest useful type is a function from a string and a position to either failure or a parsed value together with the position where parsing stopped:
Primitive parsers recognize one character satisfying a predicate, or the empty string. Combinators build larger parsers: tries and, if it fails, ; runs and then the parser chosen by its result ; runs as often as it succeeds. A grammar rule becomes a definition, and mutually recursive rules become mutually recursive functions, so a combinator parser is a recursive descent parser written at a higher level.
The list of successes
Wadler's formulation returns a list of results instead of an option, one for each way of parsing a prefix of the input[2]:
The empty list is failure, and choice is concatenation, so every alternative is explored and an ambiguous grammar yields every parse. Under lazy evaluation only as many results are computed as are used, and taking the first gives backtracking search.
A list-of-successes parser that splits a string into dictionary words in every possible way.
(* Wadler's list of successes: a parser returns every way of parsing aprefix of the input, each with the position after it. *)type 'a parser = string -> int -> ('a * int) listlet return x : 'a parser = fun _ i -> [ (x, i) ]let fail : 'a parser = fun _ _ -> []let ( >>= ) (p : 'a parser) (f : 'a -> 'b parser) : 'b parser =fun s i -> List.concat_map (fun (x, j) -> f x s j) (p s i)let ( let* ) = ( >>= )let ( <|> ) (p : 'a parser) (q : 'a parser) : 'a parser = fun s i -> p s i @ q s ilet string w : string parser =fun s i ->let n = String.length w inif i + n <= String.length s && String.sub s i n = w then [ (w, i + n) ] else []let rec many1 p = let* x = p in (let* xs = many1 p in return (x :: xs)) <|> return [ x ]let eof : unit parser = fun s i -> if i = String.length s then [ ((), i) ] else [](* Every way of splitting the input into words from a dictionary. *)let words dict = let word = List.fold_left (fun acc w -> acc <|> string w) fail dict inlet* ws = many1 word in let* () = eof in return ws
Running it.
4 parses of "abc":a + b + ca + bcab + cabcparses of "abd": 0
Sequencing, >>=, runs the second parser from every position where the first one stopped, which is List.concat_map. Wadler used the parser as an example of what he later identified as a monad[11].
Parsers as a monad
Return and bind satisfy the monad laws, with the parser type as the monad[4]:
Parsers are also a monad with zero and plus, the failing parser and choice, which gives many, optional parts and alternatives, as for any such monad. Every combinator in a library is written in terms of these few operations, and the monadic let* syntax makes a parser read like the grammar rule it implements.
A deterministic parser
Practical libraries return at most one result and report errors. Parsec introduced two refinements that make this efficient[5]. An alternative is tried only if the first branch failed without consuming input, so the parser commits to a branch as soon as it has read a character of it, which is the behaviour of an LL(1) parser, and memory held for backtracking can be released. Explicit backtracking over a longer prefix is requested with a combinator called try or attempt. And every failure records what the parser expected at the position where it failed, so error messages can be assembled from the grammar itself:
An arithmetic expression parser with precedence, whitespace and error messages.
(* A deterministic parser in the style of Parsec. A parser either succeedswith a value and a new position, or fails at a position with a list ofwhat it expected there. A failure after consuming input is final:alternatives are tried only if the first failed without consuming. *)type 'a reply = Ok of 'a * int | Error of int * string listtype 'a parser = string -> int -> 'a replylet return x : 'a parser = fun _ i -> Ok (x, i)let ( >>= ) (p : 'a parser) (f : 'a -> 'b parser) : 'b parser =fun s i -> match p s i with Ok (x, j) -> f x s j | Error _ as e -> elet ( let* ) = ( >>= )let ( <$> ) f p = let* x = p in return (f x)let ( <|> ) (p : 'a parser) (q : 'a parser) : 'a parser =fun s i ->match p s i with| Error (j, e1) when j = i -> (match q s i with Error (k, e2) when k = i -> Error (i, e1 @ e2) | r -> r)| r -> r(* Name what a parser expects, for error messages. *)let ( <?> ) (p : 'a parser) name : 'a parser =fun s i -> match p s i with Error (j, _) when j = i -> Error (i, [ name ]) | r -> rlet satisfy name pred : char parser =fun s i -> if i < String.length s && pred s.[i] then Ok (s.[i], i + 1) else Error (i, [ name ])let rec many p = (let* x = p in let* xs = many p in return (x :: xs)) <|> return []let spaces = many (satisfy "space" (( = ) ' '))let lexeme p = let* x = p in let* _ = spaces in return xlet symbol c = lexeme (satisfy (Printf.sprintf "%C" c) (( = ) c))let eof : unit parser = fun s i -> if i = String.length s then Ok ((), i) else Error (i, [ "end of input" ])let number =lexeme(let* d = satisfy "digit" (fun c -> '0' <= c && c <= '9') inlet* ds = many (satisfy "digit" (fun c -> '0' <= c && c <= '9')) inreturn (int_of_string (String.of_seq (List.to_seq (d :: ds)))))<?> "number"(* A left-associative chain: an operand, then any number of operator andoperand pairs, folded from the left. *)let chainl1 p op =let rec rest acc = (let* f = op in let* y = p in rest (f acc y)) <|> return acc inlet* x = p inrest xlet addop = (fun _ -> ( + )) <$> symbol '+' <|> ((fun _ -> ( - )) <$> symbol '-')let mulop = (fun _ -> ( * )) <$> symbol '*' <|> ((fun _ -> ( / )) <$> symbol '/')let rec expr s = chainl1 term addop sand term s = chainl1 factor mulop sand factor s = (number <|> (let* _ = symbol '(' in let* e = expr in let* _ = symbol ')' in return e)) slet parse s =match (let* _ = spaces in let* e = expr in let* () = eof in return e) s 0 with| Ok (v, _) -> string_of_int v| Error (i, e) -> Printf.sprintf "error at column %d: expected %s" (i + 1) (String.concat " or " (List.sort_uniq compare e))
Running it.
1 + 2 * 3 -> 78 - 3 - 2 -> 3(1 + 2) * 3 -> 92 * (3 + 4) - 1 -> 13100 / 10 / 5 -> 21 + * 2 -> error at column 5: expected '(' or number(1 + 2 -> error at column 7: expected ')'12 34 -> error at column 4: expected end of input
Precedence comes from the layering of the rules: expr is a chain of terms separated by additive operators, and term a chain of factors separated by multiplicative ones. <?> replaces the low-level expectations of a parser, such as "digit", by a name from the grammar, such as "number".
Applicative parsers
Many parsers never use a parsed value to decide what to parse next: a sequence of fixed parsers combined by a function. These need only the operations of an applicative functor, and [13]. The structure of such a parser is fixed before any input is read, so it can be analysed: Swierstra and Duponcheel computed the set of characters each alternative can start with, chose between alternatives without backtracking and repaired errors by inserting or deleting symbols[6]. A monadic parser can depend on what it has read, which some formats require:
A netstring parser: the payload's length is read first and decides how much to take.
type 'a parser = string -> int -> ('a * int) optionlet return x : 'a parser = fun _ i -> Some (x, i)let ( >>= ) (p : 'a parser) (f : 'a -> 'b parser) : 'b parser =fun s i -> match p s i with Some (x, j) -> f x s j | None -> Nonelet ( let* ) = ( >>= )let ( <|> ) (p : 'a parser) (q : 'a parser) : 'a parser =fun s i -> match p s i with Some _ as r -> r | None -> q s ilet char c : char parser = fun s i -> if i < String.length s && s.[i] = c then Some (c, i + 1) else Nonelet digit : char parser = fun s i -> if i < String.length s && '0' <= s.[i] && s.[i] <= '9' then Some (s.[i], i + 1) else Nonelet rec many p = (let* x = p in let* xs = many p in return (x :: xs)) <|> return []let take n : string parser = fun s i -> if i + n <= String.length s then Some (String.sub s i n, i + n) else None(* A netstring: a length, a colon, that many bytes, and a comma. Theparser for the payload is chosen by the value of the length. *)let netstring =let* d = digit inlet* ds = many digit inlet n = int_of_string (String.of_seq (List.to_seq (d :: ds))) inlet* _ = char ':' inlet* payload = take n inlet* _ = char ',' inreturn payloadlet netstrings = many netstring
Running it.
4 netstrings, 33 bytes consumed: "hello" | "" | "hello, world" | "a,b"
The payload of a netstring[14] may contain any byte, including the comma that terminates it, and can only be delimited by the length that precedes it. That makes the language of netstrings context-sensitive, and the parser for it needs .
Left recursion
A grammar rule is left-recursive if it can derive a string beginning with itself, like . A recursive descent parser for such a rule calls itself at the same position before consuming anything, and never terminates. The usual rewriting turns the left recursion into repetition[12]:
which in combinator form is chainl1, used above: parse one operand, then any number of operator-operand pairs, and fold them from the left. Rewriting the rule as right recursion instead also terminates, but groups the operations to the right:
The same subtraction grammar written left-recursively, right-recursively and as iteration.
type 'a parser = string -> int -> ('a * int) optionlet return x : 'a parser = fun _ i -> Some (x, i)let ( >>= ) (p : 'a parser) (f : 'a -> 'b parser) : 'b parser =fun s i -> match p s i with Some (x, j) -> f x s j | None -> Nonelet ( let* ) = ( >>= )let ( <|> ) (p : 'a parser) (q : 'a parser) : 'a parser =fun s i -> match p s i with Some _ as r -> r | None -> q s ilet char c : char parser = fun s i -> if i < String.length s && s.[i] = c then Some (c, i + 1) else Nonelet digit : int parser = fun s i -> if i < String.length s && '0' <= s.[i] && s.[i] <= '9' then Some (Char.code s.[i] - 48, i + 1) else None(* expr ::= expr '-' digit | digit -- left recursive: loops *)let rec left s = ((let* a = left in let* _ = char '-' in let* b = digit in return (a - b)) <|> digit) s(* expr ::= digit '-' expr | digit -- right recursive: wrong grouping *)let rec right s = ((let* a = digit in let* _ = char '-' in let* b = right in return (a - b)) <|> digit) s(* expr ::= digit ('-' digit)* -- iteration, folded from the left *)let iter s =let rec rest acc = (let* _ = char '-' in let* b = digit in rest (acc - b)) <|> return acc in(let* a = digit in rest a) s
Running it.
right "8-3-2" -> 7 8 - (3 - 2)iter "8-3-2" -> 3 (8 - 3) - 2left "8-3-2" -> Stack_overflow
Frost, Hafiz and Callaghan gave combinators that accept left-recursive and ambiguous grammars in polynomial time by limiting the depth of left-recursive calls and memoizing[9], and Warth, Douglass and Millstein extended packrat parsing to support left recursion[10].
Backtracking and packrat parsing
A parser that backtracks without limit may parse the same part of the input many times. In the grammar
the first alternative of parses a term and then, finding no +, fails; the second alternative parses the same term again. Each rule tries its operand twice, and each level of parentheses multiplies the work by four. Ford's packrat parsing stores the result of each rule at each position, so it is computed at most once, and parsing takes linear time for any grammar of this kind[7]:
Counting calls to factor, with plain backtracking and with memoization.
(* Unlimited backtracking can take exponential time. Grammar:expr ::= term '+' expr | termterm ::= factor '*' term | factorfactor ::= digit | '(' expr ')'The first alternative of expr parses a term and, finding no '+',discards it; the second parses the same term again. *)type 'a parser = string -> int -> ('a * int) optionlet calls = ref 0let char c : unit parser = fun s i -> if i < String.length s && s.[i] = c then Some ((), i + 1) else Nonelet digit : int parser = fun s i -> if i < String.length s && '0' <= s.[i] && s.[i] <= '9' then Some (Char.code s.[i] - 48, i + 1) else Nonelet ( <|> ) p q s i = match p s i with Some _ as r -> r | None -> q s ilet ( let* ) p f s i = match p s i with Some (x, j) -> f x s j | None -> Nonelet return x _ i = Some (x, i)(* The grammar, parameterized by how the three rules are called. *)let grammar ~expr ~term ~factor =let expr' = (let* a = term in let* () = char '+' in let* b = expr in return (a + b)) <|> term inlet term' = (let* a = factor in let* () = char '*' in let* b = term in return (a * b)) <|> factor inlet factor' = digit <|> (let* () = char '(' in let* e = expr in let* () = char ')' in return e) in(expr', term', factor')(* Plain recursive descent with backtracking. *)let rec expr s i = let e, _, _ = grammar ~expr ~term ~factor in e s iand term s i = let _, t, _ = grammar ~expr ~term ~factor in t s iand factor s i = incr calls; let _, _, f = grammar ~expr ~term ~factor in f s i(* Packrat: every rule remembers its result at every position. *)let packrat s =let memo tbl p i = match Hashtbl.find_opt tbl i with Some r -> r | None -> let r = p i in Hashtbl.add tbl i r; r inlet te = Hashtbl.create 16 and tt = Hashtbl.create 16 and tf = Hashtbl.create 16 inlet rec expr s i = memo te (fun i -> let e, _, _ = grammar ~expr ~term ~factor in e s i) iand term s i = memo tt (fun i -> let _, t, _ = grammar ~expr ~term ~factor in t s i) iand factor s i = memo tf (fun i -> incr calls; let _, _, f = grammar ~expr ~term ~factor in f s i) i inexpr s 0
Running it.
depth input backtracking packrat1 (1) 20 22 ((1)) 84 34 ((((1)))) 1364 56 ((((((1)))))) 21844 78 ((((((((1)))))))) 349524 910 ((((((((((1)))))))))) 5592404 1112 ... 89478484 13
The backtracking parser makes calls at nesting depth , and the packrat parser . The price is memory proportional to the input times the number of rules. Ford formalized the grammars that such parsers accept, with ordered choice and unlimited lookahead, as parsing expression grammars (PEGs)[8]: a combinator parser whose choice commits to the first alternative that succeeds recognizes the language of a PEG.
Comparison with parser generators
Parser generators such as yacc and Menhir take a grammar in a separate notation and produce an LR parser in a table. They accept left recursion, run in linear time, and report conflicts, points where the grammar is ambiguous or needs more lookahead, when the parser is generated. Combinator parsers are checked by the host language's type checker instead, can be abstracted over and composed like any other code, and handle context-sensitive formats, but an ambiguity is resolved silently by the order of the alternatives, and performance depends on how much the parser backtracks.
Libraries
Parsec for Haskell and its successor Megaparsec follow Leijen and Meijer's design[5]. In OCaml, Angstrom provides monadic and applicative combinators and is designed for incremental input. Rust's nom works on byte slices without copying, F# has FParsec, and Scala a parser-combinator library in its standard ecosystem. Python's pyparsing and JavaScript's Parsimmon bring the same approach to dynamically typed languages.
History
Burge described parsers as functions and combinators on them in 1975[1]. Wadler's 1985 paper on the list of successes[2] and Hutton's "Higher-order functions for parsing" of 1992[3] established the approach in lazy functional languages, and Wadler's work on monads connected it to the monad structure[11]. Hutton and Meijer's "Monadic parsing in Haskell"[4] and Swierstra and Duponcheel's error-correcting combinators[6] appeared in the late 1990s, and Parsec, in 2001, made combinators practical for real parsers with good error messages[5]. Ford introduced packrat parsing in 2002 and parsing expression grammars in 2004[7][8].
see also
- MonadIn functional programming, a monad is a type constructor m with two operations, return : a -> m a and bind : m a -> (a -> m b) -> m b, satisfying three laws. It lets code with some extra behaviour, such as failure, several results, configuration, state or I/O, be written as a sequence of ordinary steps, with the behaviour defined once in bind. The notion comes from category theory.
- ApplicativeAn applicative functor is a functor with pure : a -> f a and an operation that combines independent computations, <*> : f (a -> b) -> f a -> f b, or equivalently a product f a -> f b -> f (a * b), satisfying four laws. It sits between functors and monads: every monad is applicative, but because later steps cannot depend on earlier results, an applicative computation can collect all errors, run its parts in parallel, or be inspected before it runs.
- LR parsingBottom-up parsing driven by a table of states: the parser shifts tokens onto a stack, and reduces the top of the stack by a grammar rule when the next token says to. A grammar for which the table cannot be built without a choice has conflicts, reported as shift/reduce or reduce/reduce.
- Continuation-passing styleContinuation-passing style (CPS) is a way of writing programs in which no function returns: each takes an extra argument, its continuation, a function that represents the rest of the computation, and calls it with the result. Every call becomes a tail call, evaluation order is made explicit, and control operators such as exceptions, backtracking and call/cc become ordinary functions. Compilers for functional languages use CPS, or the closely related A-normal form, as an intermediate language.
- Brzozowski derivativeThe Brzozowski derivative of a regular expression r with respect to a character a is a regular expression for the strings w such that a w matches r. It is computed syntactically, by rules much like those of calculus, so a string can be matched by taking one derivative per character and checking whether the result matches the empty string. Up to simple algebraic identities a regular expression has finitely many derivatives, which are the states of a deterministic automaton, and the method extends directly to intersection and complement.
- Earley parsingEarley parsing is an algorithm that parses any context-free grammar, including ambiguous and left-recursive ones. For each position in the input it builds a set of items, grammar rules with a dot marking how much of the right-hand side has been recognized and the position where recognition began, using three operations: prediction adds the rules for a nonterminal the parser expects, scanning moves past a matching token, and completion advances the rules that were waiting for a nonterminal that has just been recognized. It runs in O(n³) time in general, O(n²) on unambiguous grammars and linear time on most grammars used in practice. Jay Earley published it in 1970.
- LL(1) parsingLL(1) parsing is top-down parsing that decides which grammar rule to apply by looking at a single token of input. It reads the input Left to right and builds a Leftmost derivation, choosing each rule from a table indexed by the nonterminal being expanded and the next token\; the table is computed from the FIRST and FOLLOW sets of the grammar, and a grammar is LL(1) when no entry holds two rules. LL(1) parsers run in linear time with no backtracking, and they correspond directly to recursive-descent parsers, one function per nonterminal. Left-recursive and ambiguous grammars are not LL(1) and must be rewritten.
- Pratt parsingPratt parsing, or top-down operator precedence parsing, is a technique for parsing expressions in which every operator has a numeric binding power on each side, and a single recursive function parses an expression by consuming operators for as long as they bind more tightly than a given minimum. Prefix, infix, postfix, mixfix and bracketing operators are handled by attaching to each token a null denotation, its meaning at the start of an expression, and a left denotation, its meaning after one. The method handles any number of precedence levels and both associativities in linear time with one function instead of one grammar rule per level, and new operators can be added by extending a table. Vaughan Pratt introduced it in 1973.
- Thompson's constructionThompson's construction converts a regular expression into a nondeterministic finite automaton with epsilon-transitions by structural recursion: each symbol becomes a two-state automaton, and concatenation, alternation and Kleene star combine the automata of the subexpressions by adding at most two states and a few epsilon-transitions. The resulting automaton has at most 2m states for an expression of size m, and it can be simulated on a string of length n in O(mn) time by tracking the set of states it could be in, which avoids the exponential worst case of backtracking matchers. Ken Thompson described it in 1968; it is the basis of grep, RE2 and most regular expression engines with guaranteed running time.
further reading
- [1]W. H. Burge, Recursive Programming Techniques, Addison-Wesley (1975).
- [2]P. Wadler, “How to replace failure by a list of successes”, Functional Programming Languages and Computer Architecture, LNCS 201 (1985).
- [3]G. Hutton, “Higher-order functions for parsing”, Journal of Functional Programming 2 (1992).
- [4]G. Hutton, E. Meijer, “Monadic parsing in Haskell”, Journal of Functional Programming 8 (1998).
- [5]D. Leijen, E. Meijer, “Parsec: direct style monadic parser combinators for the real world”, Technical Report UU-CS-2001-27, Utrecht University (2001).
- [6]S. D. Swierstra, L. Duponcheel, “Deterministic, error-correcting combinator parsers”, Advanced Functional Programming, LNCS 1129 (1996).
- [7]B. Ford, “Packrat parsing: simple, powerful, lazy, linear time”, ICFP (2002).
- [8]B. Ford, “Parsing expression grammars: a recognition-based syntactic foundation”, POPL (2004).
- [9]R. A. Frost, R. Hafiz, P. Callaghan, “Parser combinators for ambiguous left-recursive grammars”, Practical Aspects of Declarative Languages, LNCS 4902 (2008).
- [10]A. Warth, J. R. Douglass, T. Millstein, “Packrat parsers can support left recursion”, PEPM (2008).
- [11]P. Wadler, “Comprehending monads”, LISP and Functional Programming (1990).
- [12]A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers: Principles, Techniques, and Tools, ch. 4, Addison-Wesley (2nd ed., 2006).
- [13]C. McBride, R. Paterson, “Applicative programming with effects”, Journal of Functional Programming 18 (2008).
- [14]D. J. Bernstein, “Netstrings”, specification (1997).
last updated