FIRST ยท Computation Rules
๐ This is the Advanced track ยท Theory.
In the previous page, FIRST ยท Definition & Derivation, we grabbed the definition and even derived it straight from the definition.
But we ran into the wall where, with recursion, the derivation grows infinitely long.So this page is โ a computation rule that pulls out the same FIRST without expanding the derivation directly.
(Recursion gets handled gracefully too.)
How that rule is implemented in code is over at โ FIRST ยท Implementation.
Don't try to take it all in at once.
We'll go one step at a time, starting from the easy bits.
First โ let's lay out the whole cast and begin
Before working out FIRST, let's see what's in this grammar at a glance.
A grammar's symbols come in just two kinds.
- terminal โ a token that actually shows up in the input
- nonterminal โ a rule name
Splitting the two apart in our example grammar, it looks like this.
terminal list : ( ) + * id โ 5 of them nonterminal list : Expr Term Factor โ 3 of them
FIRST is worked out for each one of these symbols.
So our job is clear โ fill in the FIRST of 5 terminals + 3 nonterminals, 8 in all.
And here's the good news โ the terminal side is almost free. Let's start there.
FIRST of a terminal โ every one is itself (done in one shot)
A terminal starts with itself.
Of course it does โ + starts with +.
How to read it first. FIRST( '(' ) = { '(' } reads as โ "the FIRST set of the terminal ( is the single (." ({ } is a set, and what's inside it are the elements.)
So the 5 terminals, all in a row โ
FIRST( '(' ) = { '(' } โ the FIRST of terminal '(' is itself FIRST( '+' ) = { '+' } โ the FIRST of terminal '+' is itself FIRST( ')' ) = { ')' } โ the FIRST of terminal ')' is itself FIRST( '*' ) = { '*' } โ the FIRST of terminal '*' is itself FIRST( id ) = { id } โ the FIRST of terminal id is itself
Summed up in one line โ FIRST(terminal a) = { a } (a terminal's FIRST is always itself).
5 terminals, and that's done. We filled in 5 of the 8 for free. ๐
FIRST of a nonterminal โ the big picture first
Now the real subject, the three nonterminals (Expr Term Factor).
Before that, let me grab the big picture first.
๐ Quick aside, one term. In
Factor : '(' Expr ')' | id, each individual alternative split by|is called a production.
It's "a single line that makes a nonterminal," likeFactor โ id. (Details in Single.)
From here on we'll use this word.
The FIRST of one nonterminal is worked out like this โ take the FIRST of each of that nonterminal's productions, and combine them all (union).
FIRST(Factor) = FIRST(Factor's production 1) โช FIRST(Factor's production 2) โช โฆ
So the question we really have to solve narrows down to one โ how do you work out the FIRST of a single production?
The answer is surprisingly simple.
It depends on what that production starts with, and there are exactly three cases.
- Case โ โ when it starts with a terminal
- Case โก โ when it starts with a nonterminal
- Case โข โ when the front can disappear (ฮต)
Let's look at them one by one, starting from the easy one.
Case โ โ when a production starts with a terminal
This is the easiest case. In fact, it's just using what we already saw above.
If the front is a terminal, that production's FIRST is exactly that terminal.
Just above, we said "a terminal's FIRST is itself," right? This is the exact same story โ the front terminal is the answer.
(It doesn't matter if there are more symbols after the production. If the front is a terminal, that's the first terminal, so it ends right there.)
For example, Factor's two productions are these.
Factor โ id front is terminal id โ FIRST = { id } Factor โ '(' Expr ')' front is terminal ( โ FIRST = { '(' }
Even though '(' Expr ')' has Expr ')' after it, it ends right at the front (.
Since Factor has only these two productions, combining them completes it right away.
FIRST(Factor) = { '(' } โช { id } = { '(', id }
First nonterminal done! ๐
Case โก โ when a production starts with a nonterminal
This time the front isn't a terminal but another nonterminal. Our grammar's Expr โ Term is exactly that โ the front is the nonterminal Term.
So what's this production's FIRST? โ we bring the front Term's FIRST over as is. That is, FIRST(Expr) = FIRST(Term).
Why is that?
Back to the definition โ FIRST is "the terminal that appears first when you derive." When you derive Expr โ Term, Term takes the front spot, so the front terminal that comes out when you expand all the way is also ultimately decided by Term.
Let's see it directly with a derivation. Expanding Expr โ Term all the way โ
Expr โ Term โ Factor โ id front terminal = id Expr โ Term โ Factor โ ( Expr ) front terminal = (
๐จ purple = nonterminal (
ExprยทTermยทFactor), teal = terminal (idยท(ยท)).
The front spot is held by Term (โ Factor โ โฆ) from start to finish, right?
So the terminals that come out at the front, id ยท (, are exactly the first terminals Term produces โ that is, FIRST(Term) itself.
So FIRST(Expr) = FIRST(Term).
(Expr's other production, Expr '+' Term, is the left recursion we'll see shortly, so it adds nothing new, and this equality holds exactly.)
๐ Generalized in one line: if the front of a production is a nonterminal โ bring that nonterminal's FIRST over as is. (Except, if that nonterminal is itself, you get snagged once โ right below.)
But โ what if that nonterminal is itself? (left recursion)
Here you get snagged once.
Look at the first production of Term : Term '*' Factor | Factor.
Term โ Term '*' Factor front is again Term โ that's itself?!
Trying to work out FIRST(Term), the front nonterminal is again Term.
That is, to work out FIRST(Term) you need FIRST(Term) โ a chicken-and-egg situation.
As is, this doesn't solve in one go. But this kind of direct left recursion (where it bites itself directly) is actually easy.
Easy recursion โ just drop that rule
Just ask what Term can start with. Term has two rules, and they differ in what the front becomes.
โ Term โ Factor front is Factor (settled!) โก Term โ Term '*' Factor front is Term again (no progress)
โก leaves a Term at the front, so it never settles the first symbol (Term just comes back as Term). So the only rule that settles the front is โ , and it makes the front a Factor. So Term always starts with a Factor, which means FIRST(Term) = FIRST(Factor) = { '(', id }.
Expr (Expr : Expr '+' Term | Term) is the same direct left recursion, so by the same reasoning it's { '(', id }.
But this "just drop the in-place rule" shortcut only works for direct left recursion like Term โ where it bites itself directly. When nonterminals bite each other in a loop (indirect left recursion), it's a different story.
A โ B โฆ B โ A โฆ
A starts with B, and that B starts with A again, so there's no directly self-biting "in-place rule" anywhere to drop. To handle even cases like this all at once, the engine doesn't distinguish direct from indirect โ it handles every nonterminal the same way, by starting from the empty set and repeating until nothing more grows.
For direct left recursion like Term, this repetition finishes in a single round, so it's practically free. Where the repetition really shows its power is when nonterminals tangle up like this โ and that comes up naturally in the next FOLLOW chapter, on this very expr grammar.
๐ก The wall of recursion where the derivation kept growing infinitely long on the previous page (Definition & Derivation) โ getting over that wall is exactly this "repetition."
Instead of expanding all the way, we grow the set little by little and stop when it stops changing.
Case โข โ when the front nonterminal can disappear (ฮต)
The last case.
In Case โก we said "bring the front nonterminal Y's FIRST over," right?
But if that Y can also disappear into nothing (ฮต), there's one more thing to take care of.
(When a nonterminal can derive even the empty string, we call it nullable.)
Why do we have to look at the next symbol too?
Again, it's the definition โ FIRST is "the terminal that appears first when you derive."
But if the front Y disappears into ฮต, the front of the derivation result is taken not by Y but by the very next symbol.
Then the first terminal that next symbol derives can come to the front too.
So we have to add the next symbol's FIRST too to Y's FIRST to get it right.
This rule of "if the front can disappear, move on to the next symbol and combine" is called โ (ring-sum).
A โ B = A (if A can't disappear โ it ends there)
(A-ฮต) โช B (if A can disappear โ drop ฮต, and add B too)
We need to show ฮต here, but our expr grammar has no nullable, so instead of expr we use a small grammar. Here's the grammar we'll use for examples in this chapter:
S โ A B A โ a | ฮต B โ b | ฮต
A grammar where A and B can each disappear into ฮต (nullable).
Let's start with A and B's FIRST. Both have an ฮต branch, so ฮต gets in.
FIRST(A) = { a, ฮต } FIRST(B) = { b, ฮต }
Now for FIRST(S). In S โ A B, since the front A can disappear, โ doesn't stop at the first slot โ it moves on to the next slot B.
FIRST(S) = FIRST(A) โ FIRST(B) = ( { a, ฮต } โ ฮต ) โช { b, ฮต } = { a, b, ฮต }
If A didn't disappear it would end at { a }, but since A can become ฮต, B's first symbol b can come to the front too. And if B disappears as well, S becomes empty entirely, so ฮต gets in too. This is โ actually at work.
Our expr grammar (Expr/Term/Factor) has no nullable, so there โ always stops right at the front symbol. Still, the rule has to include this ฮต handling to be correct.
Summary โ the three cases are really one formula
We split it into Cases โ โกโข above, but in fact these three combine into a single formula.
A production is, after all, a sequence of symbols (like Term '*' Factor).
Its FIRST is โ the FIRST of the component symbols, โ (ring-summed) in order, that's all.
FIRST(Xโ Xโ โฆ Xโ) = FIRST(Xโ) โ FIRST(Xโ) โ โฆ โ FIRST(Xโ)
So the three cases from before are just a difference in where this โ stops.
- Case โ : the first symbol is a terminal. A terminal can't be ฮต, so โ stops at the first slot and becomes
{ that terminal }. - Case โก : the first symbol is a nonterminal (that can't be ฮต). โ stops at its FIRST and becomes
FIRST(that nonterminal). - Case โข : the first symbol can be ฮต. So โ moves on to the next slot and keeps combining.
For example, โ-ing Term '*' Factor directly looks like this.
FIRST(Term '*' Factor) = FIRST(Term) โ FIRST('*') โ FIRST(Factor) = FIRST(Term) โ Term can't be ฮต, so it stops at the first slot
The code is exactly this too โ it RingSum(โ)s the symbols of a production (Concat) in order, and stops when there's no more ฮต to look at.
// FirstFollowAnalyzer [First].cs
public TerminalSet FirstSet(NonTerminalConcat singleNT, ...)
{
TerminalSet result = new TerminalSet();
foreach (var symbol in singleNT) // the production's symbols in order
{
result = result.RingSum(FirstSet(symbol, seenNT)); // โ one slot
if (!result.IsNullAble) break; // no more ฮต to look at โ stop
}
return result;
}
Verification โ run this rule on our grammar, and all three come out the same.
FIRST(Factor) = FIRST(Term) = FIRST(Expr) = { '(', id }
Exactly the same as the answer we worked out by hand on the Definition & Derivation page. โ
Finally โ take the โ we've seen so far (one production), wrap one more layer of alternative (|) combining โช around it, and this is the whole of FIRST:
FIRST(A) = โ ( FIRST(Xโ) โ FIRST(Xโ) โ โฆ )
- outer
โ: combinesA's several productions (the|alternatives). ย (= a nonterminal's FIRST = the union of all its productions' FIRST) - inner
โ: joins one production (a symbol sequence), but moves to the next slot if the front disappears. ย (= Cases โ โกโข) - bottom
FIRST(terminal a) = { a }.
In this one line โ Cases โ โกโข and alternative combining are all in there. FIRST really is just this. ๐ฏ
Next โ this rule, in code
The three cases we just saw โ starts with terminal ยท starts with nonterminal ยท ฮต โ and "repeat until nothing
changes," are baked into the FirstFollowAnalyzer code almost line for line.
Let's look on.
๐ FIRST ยท Implementation (code)
๐ Previous: FIRST ยท Definition & Derivation