Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

Designing Context-Free Grammars

Recommended resources

Hello! Welcome back to our course on the Theory of Computation.

In our last lesson, we explored how to represent derivations visually using parse trees and how to use them to identify grammatical ambiguity. We focused on analyzing existing grammars. Today, we shift our focus from analysis to synthesis. Our goal is to develop the skills to design Context-Free Grammars (CFGs) for key context-free languages, such as palindromes and balanced parentheses.

Designing a CFG is a creative process that blends logic and pattern recognition. Given your background in software engineering and AI, you can think of it as being similar to designing a recursive algorithm. We need to identify the simplest cases (base cases) and then define rules for building more complex strings from simpler ones (the recursive step).

1. The Core Strategy: Recursive Design

The most effective way to design a CFG is to think about the language's structure recursively. For any given language, ask yourself two questions:

  1. What are the smallest possible strings in this language? These will form your base cases, which are typically handled by rules that produce terminals or the empty string, . This is the termination condition for your recursion.
  2. How can I build larger strings in the language from smaller strings already in the language? This will inform your recursive rules, where a non-terminal appears on both the left and right side of a production.

Let's apply this strategy to some fundamental language patterns.


2. Pattern 1: Matching Symmetric Symbols

Many context-free languages involve some form of symmetry or matching of symbols from the outside in. The canonical example is the language .

  • Base Case: The smallest string is for , which is .
  • Recursive Step: If we have a string that is already in the language, we can create a new, larger string by wrapping it with an a at the front and a b at the end: ab = .

This logic translates directly into the following grammar, where S is the start symbol:

  • (Recursive step)
  • (Base case)

The following video provides a concise walkthrough of this example and others, which will serve as our guide for several patterns in this lesson.

Context-Free Grammars (CFGs): 15 Examples

This video, "Context-Free Grammars (CFGs): 15 Examples" from Easy Theory, begins by demonstrating the recursive design principle with the language aⁿbⁿ. This is a perfect starting point.

Watch the first example in the video from 00:00 to 02:18. Pay close attention to how the host identifies the base case (empty string) and the recursive case (aSb) to construct the grammar.

Palindromes: A Generalization of Symmetry

A palindrome is a string that reads the same forwards and backwards (e.g., racecar). This is a generalization of the a^n b^n pattern. Let's design a CFG for the language of all palindromes over the alphabet .

  • Recursive Step: If a string w is a palindrome, then 0w0 and 1w1 are also palindromes.
  • Base Cases: What are the smallest palindromes? The empty string () is a palindrome (for even-length cases). The single characters 0 and 1 are also palindromes (for odd-length cases).

This gives us the grammar:

The video below explains this construction very clearly.

Context-Free Grammars (CFGs): 5 Easy Examples

This clip from "Context-Free Grammars (CFGs): 5 Easy Examples" by Easy Theory focuses directly on building a grammar for palindromes. It does an excellent job explaining the need for both even and odd length base cases.

Watch the segment from 05:56 to 09:08. Notice how the rules 0S0 and 1S1 handle the recursion, while 0, 1, and ε handle the different base cases.

To solidify this, here is a visual breakdown of how this grammar generates both even and odd length palindromes.

Context-Free Grammar for Palindromes (Even and Odd Length)
This image shows the CFG for palindromes over {a, b} and provides step-by-step derivations for several example strings, illustrating how the rules generate both even and odd length palindromes.

3. Pattern 2: Sequential and Nested Structures

Not all languages are built from the outside in. Some are built by concatenating valid strings or nesting them. The classic example is the language of balanced parentheses.

A string of parentheses is balanced if:

  1. It is the empty string. (Base Case)
  2. It is of the form (w), where w is a balanced string. (Nesting)
  3. It is of the form xy, where x and y are both balanced strings. (Concatenation)

This definition leads to the grammar:

As we discussed in the previous lesson, the S -> SS rule makes this grammar ambiguous. For example, the string ()() has two different leftmost derivations. While correct, this ambiguity is often undesirable. An alternative, unambiguous grammar for the same language is:

This version generates a balanced string (S) and then, optionally, another balanced string S immediately after it, avoiding the ambiguity of the SS rule.

The following video segment discusses the balanced parentheses problem and highlights the ambiguity issue.

Context-Free Grammars (CFGs): 15 Examples

Let's return to the "15 Examples" video. This segment covers the balanced parentheses language, including a discussion of the ambiguous SS rule and how to construct an unambiguous version.

Watch the segment from 41:18 to 44:10. Focus on the two ways to define balanced parentheses recursively (nesting vs. concatenation) and how they translate into different CFG rules.


4. Pattern 3: Advanced Composition Techniques

More complex languages often require combining these basic patterns. This is where using multiple non-terminals becomes essential. You can think of non-terminals as "helper functions" or "subroutines" that are responsible for generating a specific part of the language.

Chaining Variables for Sequential Dependencies

Consider the language . Here, every a and every b must be matched by a c. The a's come before the b's.

We can't use a single symmetric rule. The solution is to handle the a's first, then hand off to another variable to handle the b's.

  1. Generate a's and c's symmetrically: S -> aSc.
  2. Once all a's are generated, switch to a new non-terminal, let's call it B, to generate b's and c's symmetrically.
  3. The base case is ε.

This gives the grammar:

  • (Match an a with a c, or switch to matching b's)
  • (Match a b with a c, or terminate)

Watch the following clip to see this technique in action.

Context-Free Grammars (CFGs): 15 Examples

The "15 Examples" video provides an excellent explanation of how to chain variables to handle sequential dependencies.

Watch from 06:39 to 09:17. The key idea here is using one variable S to handle the a-c matching and then transitioning to another variable X to handle the b-c matching.

Other Powerful Techniques

The "15 Examples" video contains several other excellent patterns. I encourage you to watch these short clips to add more tools to your CFG design toolkit.

Context-Free Grammars (CFGs): 15 Examples

These final clips from the same video demonstrate how to handle logical 'OR' conditions and nested, non-adjacent dependencies.

Please watch the following three segments: Union ('OR' condition) (26:46 - 29:32): For a language like {aⁱbʲcᵏ | i=j or j=k}. The strategy is to create separate grammars for each condition and combine them with a rule S -> A | B. Nested Dependencies (29:32 - 31:04): For a language like {aⁿbᵐcᵐdⁿ}. This shows how to handle matching pairs that are not adjacent, using a nested recursive structure S -> aSd | B, B -> bBc | ε. Independent Parts (04:48 - 06:39): For a language like {aⁿbᵐaⁿ}. This demonstrates how to handle parts of a string that are independent of each other (the bᵐ part) by delegating to a separate variable that generates a regular language (B -> bB | ε).

Test your understanding!

Let's combine some of these patterns. Design a Context-Free Grammar for the language .

For example, a cdc bb (), a d bb (), aa ccddcc bbbb () are in .
The string acb is not in because the number of b's is not twice the number of a's.
The string a cd bb is not in because cd is not a palindrome.
The string ε is not in because .

Hint: Think about the overall structure first (a...bb). What needs to go in the middle? How can you delegate the generation of w to a helper non-terminal?

Show answer

Let's break down the design process.

  1. Outer Structure: The language has a symmetric component, but it's not a 1-to-1 match. For every one a at the beginning, we need two b's at the end. The recursive step for the main structure is therefore .

  2. Inner Structure: In the middle of the a's and b's, we need a palindrome w over {c, d}. We can create a separate non-terminal, let's call it W, to generate this palindrome. The grammar for W would be the standard palindrome grammar: .

  3. Connecting the Parts: The S \to aSbb rule creates the outer structure. When do we generate the palindrome w? After all the a's and b's have been laid down. This means the base case for the S recursion should be to generate W. So, we replace the typical ε base case with W.

  4. Putting it together:

    • (The rule S -> aWbb is the base case for n=1, stopping the recursion)

    Let's refine the S rule. The condition is , so we can't generate W directly from S. The recursion must happen at least once.
    The rule handles the recursion. The base case must generate the structure for , which is a W bb. So the grammar is:

    Let's trace aa cddc bbbb ():

This grammar correctly generates the language.


Conclusion

In this lesson, we moved from analyzing grammars to designing them. We saw that building a CFG is a recursive process that involves identifying base cases and recursive rules for generating larger strings.

Key Takeaways:

  • Recursive Design: The core strategy is to define a language in terms of itself. Find the smallest strings (base cases) and figure out how to build larger strings from smaller ones (recursive steps).
  • Symmetric Matching: For languages like aⁿbⁿ and palindromes, use rules like to build from the outside in.
  • Sequential/Nested Structures: For languages like balanced parentheses, rules like can generate nested and concatenated strings.
  • Composition with Helper Variables: For complex languages, use different non-terminals as subroutines to handle different parts of the string, such as chaining them for sequential dependencies () or combining them for logical 'OR' conditions ().

Preview of the Next Lesson:
Our design process can sometimes lead to grammars that are overly complex, containing redundant or useless rules. Before we can use these grammars in practical applications like parsing, we need to clean them up. In the next lesson, we will learn about algorithms to simplify CFGs by removing useless symbols, -productions, and unit productions. This "grammar hygiene" is an essential step in the formal language toolkit.