Table of Contents

FOLLOW Β· Implementation (code)

πŸŽ“ This is the advanced track Β· implementation.
In the previous FOLLOW Β· Computation rules, we saw the three rules (β‘  start symbol $ / β‘‘ the FIRST of what follows / β‘’ inherit the LHS when at the end) and the iteration.
This time we'll follow how that went into the FirstFollowAnalyzer code, 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 β‘ . ($ is EndMarker in the code.)
  • Once GetFollowSymbols(...) gathers the symbols that come right after B, we drop Ξ΅ from each one's FirstSet and merge them β†’ rule β‘‘.
    (Here FirstSet calls 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.

  • CalculateAllFirst first β†’ the FIRST sets get filled in.
  • InitFollowSet (β‘ β‘‘) β†’ FOLLOW(Expr)={$,'+',')'}, FOLLOW(Term)={'*'}, FOLLOW(Factor)={}.
  • do…while (β‘’) β†’ it propagates as Term βŠ‡ 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 of Datas, 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):

  • GetFollowSymbols picks up the B after A. FirstSet(B) = { b, Ξ΅ }, and ExceptWith(Ξ΅) strips off the Ξ΅ so only { b } goes in. (rule β‘‘'s βˆ’ Ξ΅)
  • Since B is nullable, ConCatExprUpdateFollow doesn't stop there β€” it pours the LHS (S)'s FOLLOW all the way back to the preceding A. (rule β‘’'s "propagate when it disappears")
   FOLLOW(A) = { b }      rule β‘‘ : FIRST(B) βˆ’ Ξ΅
            βˆͺ { $ }      rule β‘’ : B disappears, so inherit FOLLOW(S)
            = { b, $ }

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