Earley parsing
Earley parsing is a parsing algorithm for arbitrary context-free grammars[1]. Unlike LL and LR parsers, it accepts every grammar, whether ambiguous, left-recursive or right-recursive, without transformation. It reads the input once, from left to right, and after each token it records every way in which a grammar rule could be partly recognized so far: an Earley item is a rule with a dot in its right-hand side, marking how much has been recognized, and the input position where recognition of the rule began. Three operations fill in the sets of items. Prediction adds the rules of a nonterminal the parser is about to look for, scanning moves the dot past a token that matches the input, and completion, when a rule has been recognized in full, advances every item that was waiting for its nonterminal. The algorithm takes time in the worst case, on unambiguous grammars, and linear time on most grammars used for programming languages. Jay Earley developed it in his 1968 thesis and published it in 1970[2][1].
Items and sets
Let the input be , with positions to between the tokens. The parser builds sets . An item in records that derives the input between positions and , and that an starting at position is consistent with the input before it:
Initially contains for every rule of the start symbol, and the input is accepted if contains a completed item . The sets are filled in order by three rules[1]:
Prediction is top-down: it proposes only rules that could continue what has been read. Completion is bottom-up: it combines a recognized nonterminal with the items that were waiting for it. Because an item is added to a set at most once, a left-recursive rule, which would send a recursive-descent parser into an infinite loop, is predicted once per position and then used as often as needed.
An Earley recognizer. Each set is filled by a work queue\; a hash table per set prevents duplicate items.
(* Earley's algorithm. An item (rule, dot, origin) in set j says thatrule's right-hand side, up to the dot, derives the input from positionorigin to position j. *)type sym = T of string | N of stringtype item = { rule : int; dot : int; origin : int }type grammar = { start : string; rules : (string * sym array) array }let nullable g =let nul = Hashtbl.create 8 and changed = ref true inwhile !changed dochanged := false;Array.iter (fun (a, rhs) ->if not (Hashtbl.mem nul a)&& Array.for_all (function N b -> Hashtbl.mem nul b | T _ -> false) rhsthen (Hashtbl.replace nul a (); changed := true)) g.rulesdone;fun a -> Hashtbl.mem nul alet work = ref 0 (* items examined, including duplicates *)let recognize g (input : string array) =let n = Array.length input inlet nul = nullable g inlet sets = Array.init (n + 1) (fun _ -> ref []) inlet seen = Array.init (n + 1) (fun _ -> Hashtbl.create 64) inlet add j it =incr work;if not (Hashtbl.mem seen.(j) it) then beginHashtbl.add seen.(j) it (); sets.(j) := it :: !(sets.(j)); true endelse false inlet queue = Queue.create () inlet push j it = if add j it then Queue.add it queue inlet next_sym it = let rhs = snd g.rules.(it.rule) inif it.dot < Array.length rhs then Some rhs.(it.dot) else None inArray.iteri (fun r (a, _) -> if a = g.start then push 0 { rule = r; dot = 0; origin = 0 }) g.rules;for j = 0 to n dowhile not (Queue.is_empty queue) dolet it = Queue.pop queue inmatch next_sym it with| Some (N b) ->(* predict: start every rule for b here; if b can derive theempty string, also move past it (Aycock and Horspool) *)Array.iteri (fun r (a, _) -> if a = b then push j { rule = r; dot = 0; origin = j }) g.rules;if nul b then push j { it with dot = it.dot + 1 }| Some (T t) ->(* scan: move past a terminal that matches the next token *)if j < n && input.(j) = t then ignore (add (j + 1) { it with dot = it.dot + 1 })| None ->(* complete: advance every item in the origin set waiting for this nonterminal *)let a = fst g.rules.(it.rule) inList.iter (fun w -> if next_sym w = Some (N a) then push j { w with dot = w.dot + 1 })(List.rev !(sets.(it.origin)))done;if j < n then List.iter (fun it -> Queue.add it queue) (List.rev !(sets.(j + 1)))done;let accepted = List.exists (fun it ->it.origin = 0 && fst g.rules.(it.rule) = g.start && next_sym it = None) !(sets.(n)) in(accepted, Array.map (fun s -> List.rev !s) sets)let show_item g it =let a, rhs = g.rules.(it.rule) inlet syms = Array.to_list (Array.map (function T x | N x -> x) rhs) inlet before = List.filteri (fun i _ -> i < it.dot) symsand after = List.filteri (fun i _ -> i >= it.dot) syms inPrintf.sprintf "%s → %s (%d)" a (String.concat " " (before @ ["•"] @ after)) it.origin
Empty rules
Nullable nonterminals, which can derive the empty string, need care. A nonterminal may be completed in with origin before all the items in waiting for have been added, and those items would then never be advanced. Implementations that follow Earley's original description closely get this case wrong. Aycock and Horspool gave the simple fix used above: when an item predicts a nullable nonterminal, the dot is also moved past it immediately[3].
Example
The sets for n + n * n under the ambiguous grammar E → E + E | E * E | n. The number in parentheses is the origin.
open Earley(* An ambiguous grammar: E → E + E | E * E | n. *)let ambiguous = { start = "E"; rules = [|"E", [| N "E"; T "+"; N "E" |];"E", [| N "E"; T "*"; N "E" |];"E", [| T "n" |] |] }
Running it.
S0E → • E + E (0)E → • E * E (0)E → • n (0)S1 after nE → n • (0)E → E • + E (0)E → E • * E (0)S2 after +E → E + • E (0)E → • E + E (2)E → • E * E (2)E → • n (2)S3 after nE → n • (2)E → E + E • (0)E → E • + E (2)E → E • * E (2)E → E • + E (0)E → E • * E (0)S4 after *E → E * • E (2)E → E * • E (0)E → • E + E (4)E → • E * E (4)E → • n (4)S5 after nE → n • (4)E → E * E • (2)E → E * E • (0)E → E • + E (4)E → E • * E (4)E → E + E • (0)E → E • + E (2)E → E • * E (2)E → E • + E (0)E → E • * E (0)accepted: true
In there are two completed items for starting at 0, reached in different ways: combines the from 0 to 3 with the from 4 to 5, and combines the from 0 to 1 with the from 2 to 5, which is itself completed by . These are the two parse trees of the sentence.
Parse forests
The recognizer's sets contain all the information needed to recover the parse trees, but an ambiguous sentence can have exponentially many. The sum with operators under the ambiguous grammar has the Catalan number of parse trees, one for each way to bracket it.
Counting the parse trees from the sets, without building them.
open Earleylet ambiguous = E1_grammar.ambiguous(* Count the parse trees of a grammar without empty rules, using thechart to find where each nonterminal can end: a completed item(A → γ •, i) in set m means that A derives the input from i to m. *)let count g input sets =let ends = Hashtbl.create 64 inArray.iteri (fun m s -> List.iter (fun it ->let a, rhs = g.rules.(it.rule) inif it.dot = Array.length rhs then Hashtbl.replace ends (a, it.origin, m) ()) s) sets;let memo = Hashtbl.create 64 inlet rec nt a i j =match Hashtbl.find_opt memo (a, i, j) with| Some c -> c| None ->let c = ref 0 inArray.iter (fun (b, rhs) -> if b = a then c := !c + seq rhs 0 i j) g.rules;Hashtbl.add memo (a, i, j) !c; !cand seq rhs k i j =if k = Array.length rhs then (if i = j then 1 else 0)else match rhs.(k) with| T t -> if i < j && input.(i) = t then seq rhs (k + 1) (i + 1) j else 0| N b ->(* in a grammar without empty rules every symbol covers at leastone token, which also rules out cycles *)let c = ref 0 and rest = Array.length rhs - k - 1 infor m = i + 1 to j - rest doif Hashtbl.mem ends (b, i, m) then c := !c + nt b i m * seq rhs (k + 1) m jdone; !cinnt g.start 0 (Array.length input)
Running it.
operators parse trees1 12 23 54 145 4210 1679620 6564120420
A parser therefore returns a shared packed parse forest (SPPF) rather than a list of trees: a graph in which each node is a nonterminal with a span, as in the figure, and a node with several derivations has several packed children. Its size is polynomial in the length of the input. Earley's original method for extracting derivations could produce spurious ones that the grammar does not allow, as Tomita observed\; Scott showed how to build a correct SPPF during recognition[5][8].
Complexity
Each set contains items with origins to , and there are dotted rules, so each set has items and all the sets together . Prediction and scanning take constant time per item, but completing an item with origin examines the items of , giving time overall. Earley showed that on unambiguous grammars each item is added in only one way, so the time is , and that on many grammars, including most LR() grammars, the sets have bounded size and the time is linear[1].
Items created and total work, including duplicate attempts, on three grammars for the same language, as the input doubles.
open Earleylet left = { start = "E"; rules = [| "E", [| N "E"; T "+"; T "n" |]; "E", [| T "n" |] |] }let right = { start = "E"; rules = [| "E", [| T "n"; T "+"; N "E" |]; "E", [| T "n" |] |] }
Running it.
grammar tokens items workleft-recursive 201 304 306left-recursive 401 604 606left-recursive 801 1204 1206right-recursive 201 5554 5554right-recursive 401 21104 21104right-recursive 801 82204 82204ambiguous 201 20806 202912ambiguous 401 81606 1475812ambiguous 801 323206 11231612
The left-recursive grammar is parsed in linear time. The right-recursive one, although unambiguous and LR(1), makes the sets grow linearly, since each holds an item for every earlier , which gives quadratic time. Leo showed how to avoid this by memoizing the chains of completions that right recursion produces, which makes Earley parsing linear on every LR() grammar[4]. The ambiguous grammar has a quadratic number of items and a cubic amount of work: doubling the input multiplies the work by about 7.5.
The cubic bound is not the best known for general context-free parsing. The CYK algorithm, found independently by Cocke, Kasami and Younger, also takes cubic time but requires the grammar in Chomsky normal form[7][6]. Valiant reduced recognition to Boolean matrix multiplication, giving and better with faster multiplication algorithms[9], and Lee showed a converse: substantially subcubic parsing would give fast Boolean matrix multiplication[10]. Graham, Harrison and Ruzzo gave a variant of Earley's algorithm that combines it with the CYK table[13].
Uses
Generalized parsing is used where grammars are written for people rather than for parser generators, or where the language is ambiguous. Natural language processing has long used Earley and chart parsers\; Stolcke extended Earley's algorithm to probabilistic grammars, where it also computes the probability of each prefix of the input[12]. Libraries such as Marpa, nearley and Lark offer Earley parsing for programming-language tools, where it lets a grammar be written in its natural form, left recursion and all, and reports ambiguity instead of a conflict. Generalized LR parsing, which Tomita developed for natural language, is the main alternative[8]. For deterministic grammars, LL and LR parsers remain faster by a constant factor.
History
Jay Earley presented the algorithm in his 1968 thesis at Carnegie Mellon University, supervised by Robert Floyd, and published it in Communications of the ACM in 1970[2][1]. It followed the cubic-time CYK algorithm of Kasami and of Younger[7][6] and, unlike CYK, needed no normal form and was efficient on the grammars of programming languages. Graham, Harrison and Ruzzo studied its relation to CYK in 1980[13], Leo made it linear on all LR() grammars in 1991[4], and Aycock and Horspool fixed its handling of empty rules and made it fast in practice in 2002[3]. Scott's construction of shared packed parse forests from Earley recognizers followed in 2008[5]. Grune and Jacobs's textbook gives a comprehensive account[11].
see also
- 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.
- 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.
- Parser combinatorA parser combinator is a higher-order function that builds a parser from smaller parsers: a sequence of two parsers, a choice between them, a repetition of one. Parsers are ordinary values in the host language, so a grammar is written as a program that is itself the parser, with no separate generator. Parser combinators produce recursive descent parsers; they cannot use left-recursive rules directly, and with unlimited backtracking can take exponential time, which memoization (packrat parsing) reduces to linear.
- 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.
further reading
- [1]J. Earley, “An efficient context-free parsing algorithm”, Communications of the ACM 13 (1970).
- [2]J. Earley, An Efficient Context-Free Parsing Algorithm, PhD thesis, Carnegie Mellon University (1968).
- [3]J. Aycock, R. N. Horspool, “Practical Earley parsing”, The Computer Journal 45 (2002).
- [4]J. M. I. M. Leo, “A general context-free parsing algorithm running in linear time on every LR(k) grammar without using lookahead”, Theoretical Computer Science 82 (1991).
- [5]E. Scott, “SPPF-style parsing from Earley recognisers”, Electronic Notes in Theoretical Computer Science 203 (2008).
- [6]D. H. Younger, “Recognition and parsing of context-free languages in time n³”, Information and Control 10 (1967).
- [7]T. Kasami, “An efficient recognition and syntax-analysis algorithm for context-free languages”, report AFCRL-65-758, Air Force Cambridge Research Laboratory (1965).
- [8]M. Tomita, Efficient Parsing for Natural Language, Kluwer (1985).
- [9]L. G. Valiant, “General context-free recognition in less than cubic time”, Journal of Computer and System Sciences 10 (1975).
- [10]L. Lee, “Fast context-free grammar parsing requires fast Boolean matrix multiplication”, Journal of the ACM 49 (2002).
- [11]D. Grune, C. J. H. Jacobs, Parsing Techniques: A Practical Guide, ch. 7, Springer (2nd ed., 2008).
- [12]A. Stolcke, “An efficient probabilistic context-free parsing algorithm that computes prefix probabilities”, Computational Linguistics 21 (1995).
- [13]S. L. Graham, M. A. Harrison, W. L. Ruzzo, “An improved context-free recognizer”, ACM Transactions on Programming Languages and Systems 2 (1980).
last updated