Definite Clause Grammars in Prolog: From Natural Language to Fast Arithmetic Parsers
Definite Clause Grammars (DCGs) let you write declarative parsers in Prolog, but left‑recursion and complex semantic actions can hurt performance. Learn how to design clean DCGs, avoid pitfalls, use tabling, and integrate with modern Prolog tools.
24 Oct 2025, 05:13 UTC

Concrete Problem: Parsing Arithmetic Expressions in Prolog
Suppose you need to evaluate expressions like 3 + 4 * (2 - 1) inside a Prolog program. You want a parser that is both readable and fast, and you’d like to avoid writing manual list‑handling code. Definite Clause Grammars (DCGs) are a natural fit, but they come with quirks that can trip up even seasoned Prolog developers.
Thesis: Use DCGs with Careful Design and Tabling for Robust, Performant Parsing
DCGs transform natural‑language‑style rules into ordinary Prolog clauses, automatically threading a list of tokens. When you combine this with tabling (memoisation) and a disciplined grammar design, you can create parsers that are both expressive and efficient. The key is to avoid left‑recursion, structure semantic actions cleanly, and leverage built‑in predicates like phrase/2 and listing/1 for integration and debugging.
1. DCG Basics: From Syntax to Clauses
A DCG rule looks like a Prolog clause but uses the // operator to separate the grammar head from the body. The compiler rewrites it as a predicate that takes two extra arguments: the input token list and the remaining token list.
% A simple rule that matches the token 'plus'
plus // [] --> [plus].
When you write a DCG, you don’t need to manage the token list manually. The phrase/2 predicate stitches everything together:
% Parse the token list [3, plus, 4] into a term
?- phrase(expr(T), [3, plus, 4]).
T = 3 + 4.
Behind the scenes, phrase/2 calls expr/3, the compiled version of the expr rule.
2. Avoiding Left‑Recursion: The Infinite Loop Trap
Left‑recursive rules, where a nonterminal calls itself as the first element of its own body, cause standard DCG implementations to loop forever. For arithmetic expressions, a naïve grammar might look like:
expr --> expr, plus, term.
expr --> term.
Running phrase(expr(_), [3, plus, 4]) would never terminate. The fix is to rewrite the grammar using right‑recursion or to introduce an auxiliary rule that accumulates the result.
expr --> term, expr_rest.
expr_rest --> plus, term, expr_rest.
expr_rest --> [].
This pattern is common: split the left‑recursive part into a tail‑recursive helper that collects the remaining terms.
3. Tabling for Performance and Left‑Recursion
Modern Prolog systems such as SWI‑Prolog support tabling, which memoises the results of predicates. By declaring a DCG rule with :- table, you can safely use left‑recursive grammars without infinite loops, and you often gain a performance boost for repeated sub‑parses.
:- table expr/1.
expr --> expr, plus, term.
expr --> term.
After adding :- table expr/1, the parser can handle left‑recursive input and will reuse previously computed sub‑expressions, reducing redundant work.
4. Integrating with Modern Prolog Tools
- phrase/2 and phrase/3: The former takes a token list; the latter can accept an input stream or a string, converting it to tokens automatically.
- listing/1: Inspect the compiled DCG clauses to verify that the transformation matches your expectations.
- debugging utilities: Use
traceordebugto step through the generated clauses, which can be invaluable when semantic actions become complex.
Worked Example: Parsing and Evaluating Arithmetic Expressions
Below is a compact DCG that parses integer literals, addition, and multiplication, builds an abstract syntax tree (AST), and evaluates it. The grammar is written to avoid left‑recursion, and tabling is used to speed up repeated sub‑expressions.
% Enable tabling for expr/1
:- table expr/1.
% Entry point
expr(Term) --> term(Term), expr_rest(Term, Result), {Result = Term}.
expr_rest(Acc, Result) --> plus, term(T), {Acc1 is Acc + T}, expr_rest(Acc1, Result).
expr_rest(Acc, Result) --> [] , {Result = Acc}.
term(Term) --> factor(Term), term_rest(Term, Result), {Result = Term}.
term_rest(Acc, Result) --> times, factor(F), {Acc1 is Acc * F}, term_rest(Acc1, Result).
term_rest(Acc, Result) --> [] , {Result = Acc}.
factor(Term) --> [Number], {integer(Number), Term = Number}.
factor(Term) --> lparen, expr(Term), rparen.
% Token definitions
plus --> [plus].
times --> [times].
lparen --> [lparen].
rparen --> [rparen].
To parse and evaluate an expression:
% Convert a string to a token list
?- string_to_atom("3 + 4 * (2 - 1)", Atom),
atom_chars(Atom, Chars),
phrase(tokens(Tokens), Chars),
phrase(expr(Result), Tokens).
Result = 7.
In this example, tokens/1 is a helper DCG that turns characters into the token list [3, plus, 4, times, lparen, 2, minus, 1, rparen]. The parser then builds and evaluates the AST.
Trade‑off: Complexity of Semantic Actions
Embedding Prolog code inside DCG bodies (the curly‑brace {} construct) is powerful, but it can also make the grammar harder to read and debug. If you need to perform many side effects or build large data structures, consider separating the parsing phase from the semantic phase: parse to a simple AST first, then traverse it with ordinary Prolog predicates.
Actionable Closing: Build, Test, and Profile Your DCG
- Write a minimal grammar that covers your language’s core constructs.
- Run
listing/1to confirm the generated clauses match your expectations. - Profile with
time/1orstatistics/2to spot performance bottlenecks. - When left‑recursion is unavoidable, enable tabling with
:- tableand re‑profile. - Use
phrase/2orphrase/3for integration with I/O, and rely ondebugortraceto step through complex semantic actions.
By following these guidelines, you’ll harness the full expressive power of Prolog’s DCGs while keeping your parsers efficient, maintainable, and well‑integrated with modern Prolog tooling.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.