Grammar & Rules

A grammar names the language: a small set of rules (productions) over the tokens that Syntax & Notation defined. Rules decide what is a valid program, where meaning attaches, and where the parser will fail — so a DSL’s grammar is its contract.

Grammar Basics

Formal grammars describe strings of tokens the same way recipes describe meals: ingredients on the left, process on the right.

Productions and Symbols

A production rewrites one nonterminal into a sequence of terminals (tokens) and other nonterminals. The root rule (usually called program or start) anchors everything. Example: request = "room", name; says a request can be the keyword room followed by a name.

EBNF Notation

Extended BNF adds three conveniences: alternation | (choose one), repetition { ... } (zero or more), and optionality [ ... ]. EBNF is itself a DSL for languages — the meta-DSL this chapter teaches.

(* the whole notation in seven lines *)
request   = "room", name, [ "time-until", time ] ;
name      = ? letters, digits or dashes ? ;
time      = number, ":", number ;
number    = digit, { digit } ;
digit     = "0" | "1" | ... | "9" ;

Ambiguity, Precedence & Associativity

If a sentence has two possible trees, the language is ambiguous and users will disagree about meaning. The grammar must pick one reading.

8 minus 3 minus 2 parsed left and right associative yields different trees: (8-3)-2=3 versus 8-(3-2)=7

Figure 2 — Associativity changes the result; a grammar without a rule permits both.

Fixing with Levels

The standard cure is precedence levels: one nonterminal per binding strength. Multiplication gets its own level below addition, and each level’s rule fixes associativity explicitly:

(* resolved: lower level, tighter binding *)
expr    = term, { ( "+" | "-" ), term } ;   (* left-associative by iteration *)
term    = factor, { ( "*" | "/" ), factor } ;
factor  = number | "(", expr, ")" ;

Classic Ambiguities

The dangling else (which if owns the trailing else?), optional separators, and empty rules are the usual culprits. Each must be resolved in the grammar, never in the parser by convenience.

Grammar Classes

Not every grammar parses equally easily; two families dominate practice.

LL (top-down)

Left-to-right scan, leftmost derivation, one token of lookahead: the rule decides by the next token. Elegant to write by hand (recursive descent works on LL grammars) but left recursion must be avoided.

LR (bottom-up)

Trees are built from the leaves up; LR parsers (including LALR, as in yacc) accept a much larger language family and handle left recursion naturally — at the cost of tables and tooling instead of readable functions.

Choice of Weapon

Hand-written recursive descent is the best default for small DSLs; ANTLR, PLY/lark, or Menhir are the choice when the grammar grows or must stay provably unambiguous. Each is covered later in the DSL Implementation Languages phase: ANTLR generates recognizers from a grammar, OCaml hosts Menhir’s LR(1) parser generator inside a type-safe compiler, and Racket lets you define the language itself.

Validating the Grammar

An Example Suite

Keep a corpus of must-parse examples (the vocabulary in action) and must-reject examples. If the grammar cannot explain one of its own examples, the grammar is wrong, not the example.

Tooling Checks

EBNF checkers and parser generators will report ambiguity, useless rules (nonterminals never used), and unreachable rules. Treat each report as a design finding, because every one of them is a corner a user will find.

Example: Resolving the Grammar

Start ambiguous, end resolved. The two grammars below describe the same arithmetic — only the second one is a specification.

Ambiguous (do not ship)

(* expr = expr, "+", expr | number   — two trees for 1+2+3 *)
expr = expr, ( "+" | "-" ), expr | number ;

Resolved (ship this)

(* one level per precedence; iteration makes it left-associative *)
expr = term, { ( "+" | "-" ), term } ;
term = factor, { ( "*" | "/" ), factor } ;
factor = number | "(", expr, ")" ;

The Check

Hand-parse two examples against both: 1 + 2 + 3 and 2 * 3 + 4. The first grammar offers two trees for the former; the second offers exactly one for both — that is the whole job of this chapter in miniature.

Next Steps

Continue Phase 1

Rules produce the tree; Parsing & Trees shows how the machine builds it, and Semantics & Scoping explains what it means.

Resources