Closure Β· How to Compute
π This is an advanced track chapter.
In the previous Closure Β· Definition β we saidCLOSURE(I)is "the smallest set closed under β‘."
The definition only tells us "which set it is." This page shows how you actually compute it β and we'll do it by building, by hand, the real start stateIβof our example grammar, one step at a time.
The method
The method itself is simple.
Start from some item set
I, and apply rule β‘ one step at a time until there's nothing left to add.
The key is "until there's nothing left to add." The set grows one step at a time, and at some moment nothing new comes in β that's when it's closed, and you stop.
It's exactly the same flavor as the fixed point (repeat until nothing changes) we saw in FIRST/FOLLOW.
Before that β where do we start? (the augmented grammar)
Closure starts from a single start item. Where does that one item come from?
Here a small device shows up. An LR parser tacks exactly one start rule onto the very top of the original grammar β
Accept β Expr. (Expr is the original start symbol of our grammar.)
A grammar with one extra start rule laid on top like this is called an augmented grammar.
Why tack on such a thing? β so the parser can cleanly tell that "now the whole input is done."
The original start symbol Expr shows up all over the inside of expressions (the Expr in Expr '+' Term, the Expr in '(' Expr ')', and so on). So finishing a single Expr doesn't mean the whole input is done β it might just be part of a bigger expression.
But if you lay on a single Accept that isn't used anywhere else β then the very moment you finish Accept β Expr is exactly "the whole thing is done (accept)", and you know it at a glance. (How this "done signal" actually gets used is something we'll tie up in the parse table chapter. For now we just use it as a starting point.)
In plain terms β picture a shipping box. A box can have yet another smaller box inside it (just as our Expr has another Expr inside, like in '(' Expr ')'). Then it gets confusing "which one is the outermost box."
So you put one more outer box wrapping the whole thing and write on it "this is the final outer wrapping."
That outer box is exactly Accept β Expr β once even the outer box is closed (completed), you know at a glance that "it's all done (accept)!"
So the starting item is just one β Accept β β’ Expr.
The result of running closure on it is precisely the start state Iβ. Let's watch it grow with our own eyes.
One step at a time β watching Iβ grow
Before we follow along, let me lay the augmented grammar out here. (So that at each step we can pin down right away which production comes in.)
Accept β Expr Expr β Expr '+' Term | Term Term β Term '*' Factor | Factor Factor β '(' Expr ')' | id
Start β 1 item. We begin from a single virtual start item.
Accept β β’ Expr
Step 1. After the dot in Accept β β’ Expr comes Expr.
Looking at the Expr line in the grammar, its productions are the two Expr β Expr '+' Term and Expr β Term.
We add these two to the set β with the dot at the very front, since we haven't read anything yet.
Accept β β’ Expr Expr β β’ Expr '+' Term β new Expr β β’ Term β new
β 3 items.
Step 2. Let's look at what comes after the dot in the two that just came in.
- After the dot in
Expr β β’ TermcomesTerm. In the grammar,Term's productions are the twoTerm β Term '*' FactorandTerm β Factor. We add these two (with the dot at the very front). - After the dot in
Expr β β’ Expr '+' TermcomesExprtoo β butExprwas already expanded in Step 1 (those two rules are already in the set). So there's nothing new to add.
Accept β β’ Expr Expr β β’ Expr '+' Term Expr β β’ Term Term β β’ Term '*' Factor β new Term β β’ Factor β new
β 5 items.
Step 3. Again, let's look at what comes after the dot in the two that just came in.
- After the dot in
Term β β’ FactorcomesFactor. In the grammar,Factor's productions are the twoFactor β '(' Expr ')'andFactor β id. We add these two. - After the dot in
Term β β’ Term '*' FactorcomesTermβ already expanded, so nothing new.
Accept β β’ Expr Expr β β’ Expr '+' Term Expr β β’ Term Term β β’ Term '*' Factor Term β β’ Factor Factor β β’ '(' Expr ')' β new Factor β β’ id β new
β 7 items.
Step 4. Let's look at the remaining dots. After the dot in Factor β β’ '(' Expr ')' comes '(', and after the dot in
Factor β β’ id comes id β both are terminals.
A terminal has no production to start from, so there's nothing to expand. And nothing left to add either β it's closed. Done!
Wrapping up β this is Iβ
The set grew 1 β 3 β 5 β 7, and then stopped, because there was nothing left to grow.
This final 7-item closed set is precisely the start state Iβ of our grammar.
Iβ = CLOSURE( { Accept β β’Expr } )
= the 7 items where the productions of AcceptΒ·ExprΒ·TermΒ·Factor all gather with the dot at the very front
"Everything that can come at the very front" is what's gathered into Iβ β the ( and id after the dot in
Factor β β’'(' Expr ')' and Factor β β’id are exactly the terminals you can read first. (Hey, that's the same as
FIRST(Expr) = { '(', id }! That's no coincidence.)
One more step β the author writes this process as 'recursion'
Just now we watched the set grow sideways one step at a time. The author's design notes write the same computation
in a slightly more compact form β a recursive shape where Closure contains another Closure inside it.
Once you know how to read it, it isn't hard.
Closure({ β¦ })is a mark meaning "these items still need more expanding."- The red symbol is the symbol after the dot we're currently expanding (the marker).
- Each time we go down one line, the productions of that red nonterminal get pulled in as a new
Closure({ β¦ }). (Items already sorted out on an earlier line are omitted, and all gathered on the very last line.)
Closure({ Accept β β’ Expr })
= { Accept β β’ Expr, Closure({ Expr β β’ Expr '+' Term, Expr β β’ Term }) }
= { Expr β β’ Expr '+' Term, Expr β β’ Term, Closure({ Term β β’ Term '*' Factor, Term β β’ Factor }) }
= { Term β β’ Term '*' Factor, Term β β’ Factor, Closure({ Factor β β’ '(' Expr ')', Factor β β’ id }) }
= { Factor β β’ '(' Expr ')', Factor β β’ id } (after the dot is '(' Β· id β terminals, so it stops)
= { Acceptββ’Expr, Exprββ’Expr'+'Term, Exprββ’Term, Termββ’Term'*'Factor, Termββ’Factor, Factorββ’'('Expr')', Factorββ’id } = Iβ
At the very end the Closure({ β¦ }) disappeared, didn't it? That means there's nothing left to expand β that is, it's
closed. And that's Iβ.
(The promise that "a nonterminal already expanded won't be expanded again" holds here too β which is why the Expr in
Expr β β’Expr '+' Term and the Term in Term β β’Term '*' Factor don't get caught as markers. Otherwise it would loop
forever.)
And this recursive shape of Closure calling Closure is the spitting image of the code in the next
implementation, result.UnionWith(Closure( β¦ )). It's just what we wrote by hand, moved straight
into code.
(The author's original notes are drawn with their own test grammar S' β G, G β E = E | f, β¦, but the expansion
principle is exactly the same.)
Next
This "one step at a time until it closes" that we ran by hand β how did the code do it?
π Closure Β· Implementation
π Previously: Closure Β· Definition