All Projects → glebec → left-recursion

glebec / left-recursion

Licence: BSD-3-Clause license
Quick explanation of eliminating left recursion in Haskell parsers

Programming Languages

haskell
3896 projects

Projects that are alternatives of or similar to left-recursion

Chevrotain
Parser Building Toolkit for JavaScript
Stars: ✭ 1,795 (+4886.11%)
Mutual labels:  grammars, parsing
Ohm
A library and language for building parsers, interpreters, compilers, etc.
Stars: ✭ 3,938 (+10838.89%)
Mutual labels:  grammars, parsing
ohm-editor
An IDE for the Ohm language (JavaScript edition)
Stars: ✭ 78 (+116.67%)
Mutual labels:  grammars, parsing
autumn
A Java parser combinator library written with an unmatched feature set.
Stars: ✭ 112 (+211.11%)
Mutual labels:  grammars, parsing
CVparser
CVparser is software for parsing or extracting data out of CV/resumes.
Stars: ✭ 28 (-22.22%)
Mutual labels:  parsing
sb-dynlex
Configurable lexer for PHP featuring a fluid API.
Stars: ✭ 27 (-25%)
Mutual labels:  parsing
Whatsapp-Chat-Exporter
A customizable Android and iPhone WhatsApp database parser that will give you the history of your WhatsApp conversations in HTML and JSON. Android Backup Crypt12, Crypt14 and Crypt15 supported.
Stars: ✭ 150 (+316.67%)
Mutual labels:  parsing
FullFIX
A library for parsing FIX (Financial Information eXchange) protocol messages.
Stars: ✭ 60 (+66.67%)
Mutual labels:  parsing
pe
Fastest general-purpose parsing library for Python with a familiar API
Stars: ✭ 21 (-41.67%)
Mutual labels:  parsing
fyodor
Convert your Amazon Kindle highlights and notes into markdown (or any format).
Stars: ✭ 101 (+180.56%)
Mutual labels:  parsing
domainatrex
😈 A library for parsing TLDs from urls in Elixir
Stars: ✭ 29 (-19.44%)
Mutual labels:  parsing
attoparser
A tiny but fast java event-style markup parser.
Stars: ✭ 46 (+27.78%)
Mutual labels:  parsing
htmlparsing
htmlparsing.com, a website devoted to helping people parse HTML correctly
Stars: ✭ 29 (-19.44%)
Mutual labels:  parsing
logstash-config
logstash-config provides a parser and abstract syntax tree (AST) for the Logstash config format, written in Go
Stars: ✭ 26 (-27.78%)
Mutual labels:  parsing
CSGO-Config-Presets
🎉​ Presets of Config files for many scenarios in CS:GO
Stars: ✭ 167 (+363.89%)
Mutual labels:  cfg
biaffine-ner
Named Entity Recognition as Dependency Parsing
Stars: ✭ 293 (+713.89%)
Mutual labels:  parsing
YAPDFKit
Yet another PDF Kit for parsing and modifying PDF's. For OS X and iOS.
Stars: ✭ 27 (-25%)
Mutual labels:  parsing
OpenSIEM-Logstash-Parsing
SIEM Logstash parsing for more than hundred technologies
Stars: ✭ 140 (+288.89%)
Mutual labels:  parsing
ApexConfigs
Apex Legends configs for a competitve player
Stars: ✭ 52 (+44.44%)
Mutual labels:  cfg
puppeteer-autoscroll-down
Handle infinite scroll on websites by puppeteer
Stars: ✭ 40 (+11.11%)
Mutual labels:  parsing

Elimination of Left Recursion

👉 Note: common parsing libraries include combinators like chainl1 which explicitly handle left-recursive grammars without needing to refactor the grammar as shown here. I recommend using such combinators where provided / feasible. Leaving this repo up for reference's sake.

Parser combinators are expressive and easy to embed and reuse within an application. However, they implement recursive descent parsing algorithms, which cannot parse left-recursive grammars. Thankfully, there exists a simple technique to eliminate left recursion in most grammars.

These concepts are detailed here and elsewhere, but typically in the academic jargon of context-free grammars and parsing theory. In contrast, this codebase aims to demonstrate the problem and fix for those familiar with Haskell fundamentals.

The Setup

Imagine we have a small data structure representing a potentially recursive tree of subtraction expressions (similar to Hutton's Razor).

data Expr = Lit Int | Sub Expr Expr

exampleExpr = Sub (Lit 4) (Sub (Lit 3) (Lit 0))

The string language we may want to parse into this data structure could consist of digits, parens, subtraction symbols and so on:

str1 = "1"
str2 = "1-3"
str3 = "(9)"
str4 = "0-(3-8)-(((2))-(2-1))"

This is just a toy example, but it demonstrates the idea that our language (the set of legal strings) may include more tokens than are explicitly represented in our target (the result of parsing).

Grammars

To organize our thoughts we might try to draft a grammar, describing the production rules that can generate all legal strings. Grammars are often written in BNF or EBNF syntax, but this example is hopefully understandable without prior knowledge:

EXPR  = SUB | GROUP | LIT
SUB   = EXPR, "-", EXPR
GROUP = "(", EXPR, ")"
LIT   = "0" | "1" | "2" | ... | "9"

In other words,

  • "An EXPRession is either a SUBtraction, GROUP, or LITeral"
  • "A SUBtraction is an EXPRession, followed by '-', followed by an EXPRession"
  • "A GROUP is '(', followed by an EXPRession, followed by ')'"
  • "A LITeral is either '0', or '1', or... (etc.)"

Grammars consist of terminal symbols like "-" and "3", which actually appear in the language strings, and nonterminal placeholders like LIT, which do not. To build an arbitrary legal string by hand, start from the EXPR placeholder, and replace it with anything on the right side of its corresponding rule (SUB, GROUP, or LIT). Proceed with replacing nonterminal placeholders with permitted substitutions until your string consists solely of terminal symbols. For example:

EXPR
LIT
4

Or:

EXPR
GROUP
(EXPR)
(SUB)
(EXPR-EXPR)
(LIT-EXPR)
(5-EXPR)
(5-LIT)
(5-2)

Grammars and Parser Combinators

Remarkably, defining a valid grammar for a language (that is, the set of rules that can generate any legal string in the language) is almost the same as defining a working set of parsers for the language (that is, the functions which can analyze an existing string for its structure). Even though these activities (generating vs. consuming strings) are in some ways opposite, their forms are comparable.

So, the context-free production rule:

GROUP = "(", EXPR, ")"

Corresponds directly to the Haskell (via trifecta) parser:

group :: Parser Expr
group = char '(' *> expr <* char ')'

(Or in monadic style with do notation, if you prefer:)

group :: Parser Expr
group = do
  char '('
  e <- expr
  char ')'
  pure e

The Problem

The grammar shown earlier is in fact a 100% valid grammar for the expression language we wish to parse. That is, the grammar is capable of producing any arbitrary string of the language, including examples like "0-(3-8)-(((2))-(2-1))".

We want to go backwards – analyze an existing string. In src/Broken.hs, we attempt to structure our parser combinator outline according to this grammar. However, if you attempt to use that parser (not recommended!) on a simple string like "1", it will result in an infinite loop. Why? Let's review the first two lines of the grammar, and their corresponding parsers:

EXPR = SUB | GROUP | LIT
SUB  = EXPR, "-", EXPR
-- Grammar rule: EXPR = SUB | GROUP | LIT
expr :: Parser Expr
expr = sub <|> group <|> lit -- first try `sub`...

-- Grammar rule: SUB = EXPR, "-", EXPR
sub :: Parser Expr
sub = do
  e1 <- expr -- now do `expr`. WARNING: infinite recursion!
  char '-'
  e2 <- expr
  pure $ Sub e1 e2

The sequence of events when parsing a string like "1" via the expr parser is as follows:

  1. Hm, an expr might be a sub, let's try that parser.
  2. Ok, a sub begins with an expr, let's try that parser. (GOTO 1)

At this point the issue becomes quite clear! Even though this grammar is a valid one for producing arbitrary strings, it is not a useful one for parsing strings via recursive descent; it immediately enters into an infinite loop. This is because the grammar is left-recursive. Informally, a left-recursive grammar:

  • features a production rule of the form A = A ... | ... which loops on itself immediately, or...
  • a set of production rules A = B ... | ..., B = C ... | ..., C = A ... | ... which loop around eventually.

Parsers are allowed to be recursive, so long as there exists the possibility for the parser to exit the loop. A parser cannot unconditionally recurse on itself – that is recursion without a base case, a classic programming error.

Attempting a Fix

A naive attempt at solving the problem might just change the order of rules without modifying their structure. For example, perhaps we place the SUB rule at the end of EXPR?

EXPR  = GROUP | LIT | SUB
GROUP = "(", EXPR, ")"
LIT   = "0" | "1" | "2" | ... | "9"
SUB   = EXPR, "-", EXPR
expr :: Parser Expr
expr = group <|> lit <|> sub -- first try `group`...

This is again a valid grammar, but does us no good for parsing. A string like "(1)-2" would be parsed as the group "(1)" yielding Lit 1, and then stop - failing to consume the remaining "-2" string. Our parser now terminates, but without ever attempting the recursive case! We will need a different approach.

The Solution

The technique, which will work in most cases, is to identify the left-recursive path A => A ... and split it up into two stages: a "start" and "end" step. The "start" step will be mandatory; the "end" step will be effectively optional, by allowing empty results.

Before:

EXPR  = SUB | GROUP | LIT
SUB   = EXPR, "-", EXPR
...

After:

EXPR  = START, END
START = GROUP | LIT
END   = "-", EXPR | NOTHING
...

(This "NOTHING" result is typically written in grammars using the Greek letter epsilon 𝜀, and it corresponds to the empty string.)

Notice that the misbehaving SUB rule disappears entirely! It has instead been split up across the START rule (which parses a chunk of information) and the END rule (which might parse the continuation of a subtraction, with recursive right-hand expression, or might give up).

In Haskell, we can represent this "successful parse of nothing" using the famous Maybe datatype.

-- Grammar rule: EXPR = START, END
expr :: Parser Expr
expr = do
  e1 <- start
  mE2 <- end
  case mE2 of
    Nothing -> pure e1
    Just e2 -> pure $ Sub e1 e2

-- Grammar rule: START = GROUP | LIT
start :: Parser Expr
start = group <|> lit

-- Grammar rule: END = "-", EXPR | NOTHING
end :: Parser (Maybe Expr)
end = getEnd <|> pure Nothing where
  getEnd = do
    char '-'
    e <- expr
    pure $ Just e

Because end is recursive – the expr it parses itself consists of a new start and end – you can keep parsing an indefinite chain of subtractions, exactly analogous to a cons list. And just like the famous cons list, that chain of nested parses ends when you hit an empty case (<|> pure Nothing, when no - symbol is encountered).

Bubbling the information back up, our expr parser has to now react to both possibilities:

  • Either no end was encountered (i.e., no - symbol), meaning this is NOT a subtraction expression; or,
  • An end was built, in case this WAS a subtraction expression.

Step-by-Step

Let's trace through parsing the string "1" again:

  1. Hm, an expr begins with start
  2. The start is either a group (nope) or a lit (yep!)
  3. Continuing where we left off, the expr ends with end
  4. end either begins with "-" (nope) or it's nothing (yep!)
  5. So we have a successful e1 expression, and Nothing for e2; guess we just return e1 (which is Lit 1).

What about parsing a subtraction like "1-1"?

  1. Hm, an expr begins with start
  2. The start is either a group (nope) or a lit (yep!)
  3. Continuing where we left off, the expr ends with end
  4. end either begins with "-" (yep!) or it's nothing (nope)
  5. Since we matched "-", end now continues on with a new expr
  6. RECURSE: The new expr follows the same path as "1" above
  7. We have a successful e1 expression, and also a successful e2 expression; time to return a Sub e1 e2.

Conclusion

This is meant as a Haskeller-approachable introduction to the elimination of left recursion for recursive descent parsers. The full set of techniques as explained here includes additional examples and variations. I hope you find it helpful, and please let me know if I've made any mistakes.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].