DCGs — Definite Clause Grammars

notation as sugar over the difference-list threading from the previous chapter, nonterminals calling nonterminals, the {}/1 escape hatch for ordinary Prolog code, and a practical worked expression-summing grammar ============================================================ -->
Course 2 · Ch 5
DCGs — Definite Clause Grammars
Prolog's own built-in parser-generator sugar — a real, practical strength unique among the languages on this site

prolog2-4 ended with a promise: the difference-list threading built by hand there reappears, hidden, inside Prolog's own --> notation. This chapter delivers on that — DCGs are Prolog's built-in grammar-writing sugar, and no other language covered on this site has anything comparable baked directly into the language itself, not layered on as a separate parser-combinator library.

The --> Notation

greeting --> [hello], [world]. ?- phrase(greeting, [hello, world]). true. ?- phrase(greeting, [hello, there]). false.

A DCG rule looks like an ordinary Prolog clause, but with --> instead of :-, and its body written as a sequence of terminals — literal list elements in square brackets — to be matched against the input, left to right. phrase/2 is how you actually run one: phrase(NonTerminal, List) checks whether List can be fully consumed by the grammar rule NonTerminal.

What --> Is Actually Sugar Over

Every DCG rule is translated, automatically, into an ordinary Prolog clause with two extra hidden arguments — an input-so-far list and a remaining-list, threaded through exactly like prolog2-4's own List-Hole pair. Conceptually, greeting --> [hello], [world]. becomes:

greeting(S0, S) :- S0 = [hello|S1], S1 = [world|S].

Each terminal peels one element off the front of the "remaining input so far" list, threading the leftover tail into the next goal — S0 to S1 to S, the exact same open-list-with-a-hole pattern the previous chapter built by hand. phrase(greeting, [hello, world]) is really just calling greeting([hello, world], []) — the whole list in, nothing left over.

Nonterminals — Rules Calling Rules

sentence --> noun_phrase, verb_phrase. noun_phrase --> [the], noun. verb_phrase --> verb, noun_phrase. noun --> [cat]. noun --> [dog]. verb --> [chased]. ?- phrase(sentence, [the, dog, chased, the, cat]). true.

A DCG rule body can reference other DCG rules, not just literal terminals — each nonterminal call receives whatever input is left after the rules before it, and passes along whatever remains after it consumes its own piece. This is exactly how a real grammar is meant to compose: small rules for individual pieces (noun, verb), combined into larger ones (noun_phrase, sentence) without any of them needing to know how the input-threading actually works underneath.

Embedding Ordinary Prolog Code — Curly Braces

even_number(N) --> [N], { number(N), 0 is N mod 2 }. ?- phrase(even_number(N), [4]). N = 4. ?- phrase(even_number(N), [7]). false.

Anything inside { } inside a DCG body is treated as an ordinary Prolog goal, run as-is rather than being interpreted as a grammar symbol to match against the input list — the escape hatch that lets a grammar rule check arithmetic, call another predicate, or do anything else Prolog can normally do, mid-parse.

A Practical Use — Summing an Expression List

expr(Sum) --> term(T), expr_rest(T, Sum). expr_rest(Acc, Sum) --> [plus], term(T), { Acc1 is Acc + T }, expr_rest(Acc1, Sum). expr_rest(Sum, Sum) --> []. term(T) --> [T], { number(T) }. ?- phrase(expr(Sum), [1, plus, 2, plus, 3]). Sum = 6.

A genuinely useful grammar, not just a toy: expr parses a term, then repeatedly looks for plus followed by another term, accumulating the running total with prolog1-7's own is/2 inside a curly-brace escape at each step, until expr_rest's empty-list base case is reached and the accumulated sum is unified with the final result.

Underlying mechanismWhat you write
Hand-written difference listsList-Hole pairs, threaded manuallyevery S0/S argument, explicitly
DCGs (--> notation)the exact same List-Hole threadinggrammar rules only — the threading is generated for you
phrase/2 and phrase/3 are how DCG rules are actually called
A DCG rule compiles to a predicate with two extra hidden arguments, so it's never called directly by its plain name — phrase(NonTerminal, List) handles that translation for you. phrase/3 additionally accepts a "leftover" argument, useful when the grammar is only meant to match a prefix of the input rather than the whole list.
Forgetting the curly braces around ordinary Prolog goals
Writing N is Acc + T directly inside a DCG body — without wrapping it in { } — doesn't run it as Prolog code at all; it gets treated as a terminal, an attempt to match a literal grammar symbol against the input list, which either fails outright or raises a type error. Every ordinary Prolog goal inside a --> body needs its own { }, without exception.

Coding Challenges

Challenge 1

Write the sentence/noun_phrase/verb_phrase grammar exactly as shown in the chapter, add a second noun (e.g. [mouse]) and a second verb (e.g. [saw]), and confirm phrase/2 accepts a new valid sentence combining them.

📄 View solution
Challenge 2

Write the expr/term/expr_rest grammar exactly as shown in the chapter, then use phrase/2 to sum a longer list such as [5, plus, 10, plus, 15, plus, 20], confirming the correct total.

📄 View solution
Challenge 3

Write a short comment demonstrating what happens if the { } braces around { Acc1 is Acc + T } in expr_rest/2 are accidentally removed, explaining specifically why Prolog then tries to treat it as a grammar terminal rather than running it as arithmetic.

📄 View solution

Chapter 5 Quick Reference

  • Rule --> Body. defines a grammar rule; terminals are written as literal lists (e.g. [hello])
  • phrase(NonTerminal, List) runs a DCG rule against List — really calling the rule's own hidden two-argument compiled form
  • Every --> rule compiles to a predicate threading an input-so-far/remaining-input pair — the exact same difference-list pattern from prolog2-4
  • DCG rules can call other DCG rules as nonterminals, composing small grammar pieces into larger ones
  • { Goal } runs Goal as ordinary Prolog code inside a DCG body, rather than treating it as a grammar terminal to match
  • Forgetting { } around a Prolog goal inside a DCG body is a common, easy-to-miss source of failures or type errors