FOLLOW Β· Implementation (code)
π This is the advanced track Β· implementation.
In the previous FOLLOW Β· Computation rules, we saw the three rules (β start symbol$/ β‘ theFIRSTof what follows / β’ inherit the LHS when at the end) and the iteration.
This time we'll follow how that went into theFirstFollowAnalyzercode, almost line by line.
Try it β the public API (one line)
It's the same entrance as FIRST.
A single call gives you FIRST and FOLLOW together.
var parser = new LALRParser(grammar);
foreach (var item in parser.GetFirstAndFollow())
{
Console.WriteLine($"{item.Symbol}");
Console.WriteLine($" FOLLOW = {item.Follow}");
}
Where to store it β Datas
public RelationData Datas { get; private set; } = new(); // stores FOLLOW per nonterminal
This is the per-nonterminal FOLLOW ledger, corresponding to FIRST's _cache.
Once the computation is done, we pull the results out of here.
β Β· β‘ initial values β InitFollowSet
Rule β (start symbol $) and rule β‘ (the FIRST β Ξ΅ of what comes right after) both live in this one method.
public TerminalSet InitFollowSet(NonTerminal nonTerminal, HashSet<NonTerminal> nonTerminalSet)
{
TerminalSet result = new TerminalSet();
if (nonTerminal.IsStartSymbol) result.Add(new EndMarker()); // ββ rule β : if it's the start symbol, $
foreach (var symbol in GetFollowSymbols(nonTerminalSet, nonTerminal)) // ββ rule β‘ : the symbols right after
{
var firstSet = FirstSet(symbol); // the FIRST of what follows
firstSet.ExceptWith(new TerminalSet(new Epsilon())); // drop Ξ΅
result.UnionWith(firstSet); // add it to FOLLOW
}
return result;
}
if (nonTerminal.IsStartSymbol) result.Add(new EndMarker())β this is rule β . ($isEndMarkerin the code.)- Once
GetFollowSymbols(...)gathers the symbols that come right afterB, we dropΞ΅from each one'sFirstSetand merge them β rule β‘.
(HereFirstSetcalls the FIRST calculator directly. That's why FIRST has to be completely finished before FOLLOW.)
Finding the symbol right after β FindNextSymbolSet
GetFollowSymbols scans the whole grammar and collects the results of FindNextSymbolSet.
This is the code that finds "right after B."
foreach (var symbol in singleNT)
{
if (bFind) // if we've already passed B
{
result.Add(symbol); // take the symbol after it
if (!FirstSet(symbol).IsNullAble) break; // stop if it can't become Ξ΅
}
else if (symbol == findSymbol) bFind = true; // here we found B
}
From the point we meet B (findSymbol), we collect symbols, but the moment we hit a symbol that cannot disappear (not Ξ΅) we stop.
This is exactly the "FIRST of Ξ²" gathering from computation rule β‘.
β’ inherit, repeatedly β ConCatExprUpdateFollow
Rule β’ ("if it's at the very end, inherit the LHS's FOLLOW") is here.
private bool ConCatExprUpdateFollow(NonTerminalSingle contents, TerminalSet followSet)
{
for (int i = contents.Count - 1; i >= 0; i--) // β from the very end (right) toward the left
{
var symbol = contents[i];
if (symbol is Terminal) break; // stop at a terminal (terminals have no FOLLOW)
this.Datas[symbol as NonTerminal].UnionWith(followSet); // pour in the LHS's FOLLOW
β¦
if (!FirstSet(symbol).IsNullAble) break; // stop if this nonterminal can't become Ξ΅
}
β¦
}
followSet is exactly the FOLLOW of the LHS (A).
Walking the production from the very end (right), we pour FOLLOW(A) into the nonterminal at the end β rule β’.
If that nonterminal can disappear (Ξ΅), we keep pouring into the nonterminal before it as well; if it can't disappear, we stop β the computation rule's
"if Ξ² disappears, go further back" is exactly this one line, if (!IsNullAble) break;.
(UpdateFollow is the wrapper that runs the above over every production of a single nonterminal.)
The full driver β CalculateAllFollow
public void CalculateAllFollow(HashSet<NonTerminal> nonTerminals)
{
CalculateAllFirst(nonTerminals); // β FIRST first (because rule β‘ uses FIRST)
foreach (var nt in nonTerminals)
Datas.Add(nt, InitFollowSet(nt, nonTerminals)); // β initial values from rules β Β·β‘
do
{
bChange = false;
foreach (var d in Datas)
if (UpdateFollow(d.Key, d.Value)) bChange = true; // β rule β’ inheritance
}
while (bChange); // until nothing changes (fixpoint)
}
The computation rules are here in exactly the same order β FIRST first β initial values from rules β Β·β‘ β repeat rule β’ until nothing changes.
The first line, CalculateAllFirst, nails down in code the fact that "FOLLOW uses FIRST as its ingredient."
Reading out the result β Follow
public TerminalSet Follow(NonTerminal nonTerminal) => this.Datas[nonTerminal]; // just a lookup
It only reads out from the finished Datas.
The public API GetFirstAndFollow() calls this and fills in item.Follow.
Formula β code at a glance
| Computation rule | Code |
|---|---|
Rule β β start symbol $ |
if (IsStartSymbol) result.Add(new EndMarker()) |
Rule β‘ β the FIRST β Ξ΅ of what follows |
GetFollowSymbols + FirstSet(symbol).ExceptWith(Ξ΅) |
| Rule β’ β inherit the LHS when at the end | ConCatExprUpdateFollow (UnionWith(followSet) from the right) |
| FIRST as the ingredient (first) | CalculateAllFollow's first line CalculateAllFirst |
| Fixpoint iteration | do { β¦ } while(bChange) |
Following it with the example
Run CalculateAllFollow on our grammar β and it flows exactly the way we did it by hand in the computation rules.
CalculateAllFirstfirst β theFIRSTsets get filled in.InitFollowSet(β β‘) βFOLLOW(Expr)={$,'+',')'},FOLLOW(Term)={'*'},FOLLOW(Factor)={}.doβ¦while(β’) β it propagates asTerm β FOLLOW(Expr),Factor β FOLLOW(Term), so both become{$,'+',')','*'}.- When running it more produces no change,
bChange = falseβ stop. β
(How many rounds the propagation takes splits into one or two depending on the processing order ofDatas, but the values after it stops are the same in any order.)
Let's see Ξ΅ (nullable) run in the code too. Since expr has no nullable, the trace above touches neither rule β‘'s ExceptWith(Ξ΅) nor rule β’'s "propagate back to the preceding nonterminal when it disappears." Here's the grammar we'll use for this section:
S β A B A β a | Ξ΅ B β b | Ξ΅
Let's follow FOLLOW(A) through the code (the symbol after A is B):
GetFollowSymbolspicks up theBafterA.FirstSet(B) = { b, Ξ΅ }, andExceptWith(Ξ΅)strips off the Ξ΅ so only{ b }goes in. (rule β‘'sβ Ξ΅)- Since
Bis nullable,ConCatExprUpdateFollowdoesn't stop there β it pours the LHS (S)'s FOLLOW all the way back to the precedingA. (rule β’'s "propagate when it disappears")
FOLLOW(A) = { b } rule β‘ : FIRST(B) β Ξ΅ βͺ { $ } rule β’ : B disappears, so inherit FOLLOW(S) = { b, $ }
At a glance β the full FOLLOW-related spec
This is the FOLLOW-side skeleton of FirstFollowAnalyzer.
The logic is emptied out, showing only what's there.
public partial class FirstFollowAnalyzer // (FOLLOW side)
{
public RelationData Datas { get; }
// ββ read out βββββββββββββββββββββββββββββββ
public TerminalSet Follow(NonTerminal nonTerminal); // lookup in Datas
// ββ compute βββββββββββββββββββββββββββββββββ
public TerminalSet InitFollowSet(NonTerminal nt, HashSet<NonTerminal> all); // rules β Β·β‘
public SymbolSet GetFollowSymbols(HashSet<NonTerminal> all, NonTerminal nt); // the symbols right after B
public void CalculateAllFollow(HashSet<NonTerminal> nonTerminals); // FIRST first + initial values + repeat
// (private) UpdateFollow Β· ConCatExprUpdateFollow Β· FindNextSymbolSet β rule β’ + "finding what's after"
}
Next chapter
We've finished FIRST and FOLLOW all the way through β definition Β· derivation Β· computation rules Β· code.
These two are exactly the core ingredients that build the parse table.
Next come the LR item, which expresses "how far into this rule the LR parser has read so far," and the canonical collection (the states) β those come together and finally become that famous parse table.
π LR item
π Previously: FOLLOW Β· Computation rules