Some problems of recursive descent parsers
Recursive descent parsers are a popular choice for parsing expressions, but they come with their own set of challenges. In my previous article, I introduced a recursive descent parser for expressions and discussed some of its limitations. In this follow-up, I will delve deeper into these issues and propose alternative solutions.
One major issue with recursive descent parsers is their inherent right-associativity. In the previous article, I explained how the grammar was built to avoid left-recursion, which could cause the parser to enter an infinite loop. However, this right-associativity can lead to incorrect results when parsing expressions like "5 - 1 - 2".
Instead of evaluating the expression as "(5 - 1) - 2", the parser produced "6" because it computed the rightmost subtraction first. This discrepancy arises due to the way the grammar rules are structured, which implicitly defines the associativity.
To address this problem, I proposed rewriting the grammar rules using Extended Backus-Naur Form (EBNF), which allows for more flexibility in defining rules. EBNF includes a repetition operator denoted by braces `{ ... }`, which can represent a rule being repeated 0 or more times. By using this idiom, I was able to rewrite the `expr` rule in a way that naturally results in left-associativity. This change ensures that expressions like "5 - 1 - 2" are evaluated correctly, producing the expected result of "2".
Another problem with recursive descent parsers is their efficiency, particularly when dealing with operator precedence. In recursive-descent parsers, defining operator precedence often involves using nested rules. For example, the code snippet from the EBNF-based parser demonstrates how the `term` rule is defined within the `expr` rule. This nesting of rules is necessary to establish the relative precedence of different operators, but it comes at a performance cost.
When I tested the parser with a simple input like "42", I was surprised to see that the parser generated 4 function calls, even though the input consisted of a single token. This excessive number of function calls demonstrates the inefficiency of recursive descent parsers when parsing expressions. The nesting of rules to define operator precedence forces the parser to repeatedly call itself, leading to an exponential increase in function calls as the input size grows.
To overcome this efficiency issue, I suggested using a hybrid parser approach that combines recursive descent parsing with other parsing techniques. Hybrid parsers aim to leverage the simplicity and ease of implementation of recursive descent parsers while incorporating more efficient parsing algorithms. By combining the strengths of different parsing methods, a hybrid parser can provide a general-purpose solution for parsing both simple expressions and more complex programming language constructs.
In the future, I plan to explore the implementation of such a hybrid parser in a dedicated article. This parser would address both the associativity and efficiency challenges faced by recursive descent parsers, making it a robust and versatile tool for parsing expressions and higher-level language constructs.
Written by urgent.news from Lobsters's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.