package de.fuberlin.projectci.parseTable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import de.fuberlin.commons.util.LogFactory;
import de.fuberlin.projectci.grammar.Grammar;
import de.fuberlin.projectci.grammar.NonTerminalSymbol;
import de.fuberlin.projectci.grammar.Production;
import de.fuberlin.projectci.grammar.Symbol;
import de.fuberlin.projectci.grammar.TerminalSymbol;
/**
* Baut eine SLR-Parsetabelle (Action- und Goto-Tabelle) zu einer erweiterten Grammatik.
*/
public class SLRParseTableBuilder extends ParseTableBuilder {
private static Logger logger = LogFactory.getLogger(SLRParseTableBuilder.class);
private static final String ELSE="else";
public SLRParseTableBuilder(Grammar grammar) {
super(grammar);
}
/**
* Baut eine Parsetabelle nach dem Algorithmus aus dem Drachenbuch (Algorithmus 4.32/ 2. deutsche Auflage)
*
<code>
1. Konstruieren Sie C={I0, I1, ..., In}, die Sammlung der LR(0)-Item-Mengen für G0
2. Der Zustand i wird aus Ii erstellt. Die Parseraktionen für Zustand i werden wie folgt ermittelt:
a) Wenn [A → α.aβ] in Ii und GOTO(Ii,a) = Ij, dann setze ACTION[i,a] auf "shift j". Hier muss a ein Terminal sein.
b) Wenn [A → α.] in Ii, setze ACTION[i,a] für alle a in FOLLOW(A) auf "reduce A → α". Hier darf A nicht S0 sein.
c) Wenn [S0 → S.] in Ii, setze ACTION[i,$] auf "akzeptieren"
Ergeben sich aus diesen Regeln Konflikte, sprechen wir davon, dass es sich nicht um eine SLR(1)-Grammatik handelt.
In diesem Fall erstellt der Algorithmus keinen Parser.
3. Die GOTO-Übergänge für Zustand i werden für alle Nichtterminale A nach folgender Regel konstruiert:
Wenn GOTO(Ii,A)=Ij, dann GOTO(i,A)=j.
4. Alle nicht durch die Regeln (2) und (3) definierten Einträge werden auf "Fehler" gesetzt.
5. Der Ausgangszustand des Parsers ist derjenige, der aus der Item-Menge konstruiert wurde, die [S0 → .S] enthält.
</code>
*/
public ParseTable buildParseTable() throws InvalidGrammarException {
Map<Set<LR0Item>, State> itemSet2State=new HashMap<Set<LR0Item>, State>();
Set<NonTerminalSymbol> allNonTerminalSymbols = getGrammar().getAllNonTerminals();
Production startProduction=getGrammar().getStartProduction(); // S0 → S
LR0Item startItem=new LR0Item(startProduction, 0); // [S0 → .S] -- Zum Erkennen des Startzustands
LR0Item acceptanceItem=new LR0Item(startProduction, startProduction.getRhs().size()); // [S0 → S.] -- Zum Erkennen des akzeptierenden Zustands
ParseTable parseTable=new ParseTable();
// 1. Konstruieren Sie C={I0, I1, ..., In}, die Sammlung der LR(0)-Item-Mengen für G0
List<Set<LR0Item>> cannonicalCollectionOfLR0Items = cannonicalCollectionOfLR0Items();
// 2. Der Zustand i wird aus Ii erstellt.
for (int i = 0; i < cannonicalCollectionOfLR0Items.size(); i++) {
Set<LR0Item> anItemSet = cannonicalCollectionOfLR0Items.get(i);
State aState=new State(i);
itemSet2State.put(anItemSet, aState);
// 5. Der Ausgangszustand des Parsers ist derjenige, der aus der Item-Menge konstruiert wurde, die [S0 → .S] enthält.
if (anItemSet.contains(startItem)){
parseTable.setInitialState(aState);
}
}
for (int i = 0; i < cannonicalCollectionOfLR0Items.size(); i++) {
Set<LR0Item> anItemSet = cannonicalCollectionOfLR0Items.get(i);
State aState=itemSet2State.get(anItemSet);
for (LR0Item anItem : anItemSet) {
// 2a) Wenn [A → α.aβ] in Ii und GOTO(Ii,a) = Ij, dann setze ACTION[i,a] auf "shift j". Hier muss a ein Terminal sein.
if (anItem.getNextSymbol()!=null && anItem.getNextSymbol() instanceof TerminalSymbol){
TerminalSymbol nextSymbol=(TerminalSymbol) anItem.getNextSymbol();
Set<LR0Item> targetItemSet=gotoSet(anItemSet, nextSymbol);
if (targetItemSet.size()>0){
State targetState=itemSet2State.get(targetItemSet);
Action shiftAction=new ShiftAction(targetState);
Action existingAction=parseTable.getAction(aState, nextSymbol);
if (existingAction!=null && !(existingAction instanceof ErrorAction) && !existingAction.equals(shiftAction)){ // Konflikt erkannt
// Beim Dangling-Else-Problem wird ein shift-reduce-Konflikt zugunsten des shift aufgelöst (Vgl. Drachenbuch S. 336f)
if (ELSE.equals(nextSymbol.getName()) && existingAction instanceof ReduceAction){
logger.info("Resolve Dangling-Else Shift-Reduce-Conflict for the benefit of Shift: State="+aState+", symbol="+nextSymbol+
", shiftAction="+shiftAction+", existingAction="+existingAction);
}
else{
throw new InvalidGrammarException("No SLR(1) Grammar due to action table conflict: State="+aState+", symbol="+nextSymbol+
", shiftAction="+shiftAction+", existingAction="+existingAction);
}
}
parseTable.getActionTableForState(aState).setActionForTerminalSymbol(shiftAction, nextSymbol);
}
}
// 2b) Wenn [A → α.] in Ii, setze ACTION[i,a] für alle a in FOLLOW(A) auf "reduce A → α". Hier darf A nicht S0 sein.
if (anItem.getNextSymbol()==null && !anItem.getProduction().getLhs().equals(getGrammar().getStartSymbol())){
NonTerminalSymbol lhs = anItem.getProduction().getLhs();
Action reduceAction=new ReduceAction(anItem.getProduction());
Set<TerminalSymbol> followers = getGrammar().follow(lhs);
for (TerminalSymbol aTerminalSymbol : followers) {
boolean takeReduceAction=true;
Action existingAction=parseTable.getAction(aState, aTerminalSymbol);
if (existingAction!=null && !(existingAction instanceof ErrorAction) && !existingAction.equals(reduceAction)){ // Konflikt erkannt
// Beim Dangling-Else-Problem wird ein shift-reduce-Konflikt zugunsten des shift aufgelöst (Vgl. Drachenbuch S. 336f)
if (ELSE.equals(aTerminalSymbol.getName()) && existingAction instanceof ShiftAction){
// Existierende ShiftAction wird einfach nicht durch die ReduceAction ersetzt
takeReduceAction=false;
logger.info("Ignoring Dangling-Else Shift-Reduce-Conflict: State="+aState+", symbol="+aTerminalSymbol+
", reduceAction="+reduceAction+", existingAction="+existingAction);
}
else{
throw new InvalidGrammarException("No SLR(1) Grammar due to action table conflict: State="+aState+", symbol="+aTerminalSymbol+
", reduceAction="+reduceAction+", existingAction="+existingAction);
}
}
if (takeReduceAction){
parseTable.getActionTableForState(aState).setActionForTerminalSymbol(reduceAction, aTerminalSymbol);
}
}
}
// 2c) Wenn [S0 → S.] in Ii, setze ACTION[i,$] auf "akzeptieren"
if (anItemSet.contains(acceptanceItem)){
Action acceptAction=new AcceptAction();
Action existingAction=parseTable.getAction(aState, Grammar.INPUT_ENDMARKER);
if (existingAction!=null && !(existingAction instanceof ErrorAction) && !existingAction.equals(acceptAction)){ // Konflikt erkannt
throw new InvalidGrammarException("No SLR(1) Grammar due to action table conflict: State="+aState+", symbol="+Grammar.INPUT_ENDMARKER+
", acceptAction="+acceptAction+", existingAction="+existingAction);
}
parseTable.getActionTableForState(aState).setActionForTerminalSymbol(acceptAction, Grammar.INPUT_ENDMARKER);
}
// 3. Wenn GOTO(Ii,A)=Ij, dann GOTO(i,A)=j. (für alle Nichtterminale A)
for (NonTerminalSymbol aNonTerminalSymbol : allNonTerminalSymbols) {
Set<LR0Item> aGotoSet = gotoSet(anItemSet, aNonTerminalSymbol);
if (aGotoSet.size()>0){
State targetState=itemSet2State.get(aGotoSet);
Goto aGoto=new Goto(targetState);
Goto existingGoto=parseTable.getGoto(aState, aNonTerminalSymbol);
if (existingGoto!=null && ! existingGoto.equals(aGoto)){ // Konflikt erkannt
throw new InvalidGrammarException("No SLR(1) Grammar due to goto table conflict: State="+aState+", symbol="+aNonTerminalSymbol+
", goto="+aGoto+", existingGoto="+existingGoto);
}
parseTable.getGotoTableForState(aState).setGotoForNonTerminalSymbol(aGoto, aNonTerminalSymbol);
}
}
}
// 4. Alle nicht durch die Regeln (2) und (3) definierten Einträge werden auf "Fehler" gesetzt.
// Hier ist nichts zu tun, da ParseTable so implementiert ist, dass bei einem fehlenden Eintrag
// automatisch eine ErrorAction zurückgegeben wird.
}
return parseTable;
}
/**
* Berechnet das Closure-Set zu einer LR0Item-Menge (Drachenbuch: Abbildung 4.32/ 2. deutsche Auflage)
<code>
SetOfItems CLOSURE(I) {
J=I;
repeat
for ( jedes Item A → α.Bβ in J )
for ( jede Produktion B → γ von G)
if (B → .γ ist nicht in J)
füge B → .γ zu J hinzu
until keine Items mehr in einer Runde zu J hinzugefügt wurden
return J;
}
</code>
*/
public Set<LR0Item> closure(Set<LR0Item> items) {
List<LR0Item> result=new ArrayList<LR0Item>(items);
boolean added= false;
do{
added=false;
for (int i = 0; i < result.size(); i++) {
LR0Item anItem = result.get(i);
Symbol nextSymbol = anItem.getNextSymbol();
if(nextSymbol == null)
continue;
else if(nextSymbol instanceof TerminalSymbol)
continue;
List<Production> productions = getGrammar().getProductionsByLhs((NonTerminalSymbol) nextSymbol);
for(Production aProduction: productions){
LR0Item itemCandidate = new LR0Item(aProduction, 0);
if(!result.contains(itemCandidate)){
result.add(itemCandidate);
added = true;
}
}
}
}
while(added);
return new HashSet<LR0Item>(result);
}
/**
* Berechnet die Hülle von Items, die unmittelbar nach dem Lesen eines gegebenen
* Symbols aus einer gegebenen Item-Menge folgen. (auf Lr(0) angepasster Algorithmus für LR(1) aus Drachenbuch (eng): Abbildung 4.40
*
* @param items Item-Menge, zu der für jedes Element ein "Folge-Item" gesucht wird.
* @param s oberstes Stack-Symbol
* @return Hülle der gefundenen "Folge"-Item-Menge
*
<code>
SetOfItems GOTO(I,X) {
J=leere Menge;
for ( jedes Item [A → α.Xβ] in I )
füge Item [A → αX.β] zu J hinzu;
return CLOSURE(J);
}
</code>
*
*/
public Set<LR0Item> gotoSet(Set<LR0Item> items, Symbol s) {
Set<LR0Item> J = new HashSet<LR0Item>();
for(LR0Item item : items) {
if(s.equals(item.getNextSymbol())){ // nach Punkt soll Symbol s folgen
LR0Item newItem = new LR0Item(item.getProduction(), (item.getIndex()+1));
// da nur aus geg. Set (items) neue Items mit verschobenem Punkt erstellt werden,
// können keine doppelten Elemente in Set J auftreten
J.add(newItem);
}
}
return closure(J);
}
/**
* Berechnet die kannonische Sammlung von LR0Items.(Drachembuch: Abbildung 4.33/ 2. deutsche Auflage)
<code>
void items(G0){
C = {CLOSURE({[S0 --> .S]})};
repeat
for ( jede Item-Menge I in C )
for ( jedes Grammatiksymbol X )
if ( GOTO(I,X) ist nicht leer und nicht in C )
füge GOTO(I,X) zu C hinzu;
until es werden keine neuen Item-Mengen mehr in einer Runde zu C hinzugefügt
}
</code>
*/
public List<Set<LR0Item>> cannonicalCollectionOfLR0Items() {
List<Production> startProductions=this.getGrammar().getProductionsByLhs(getGrammar().getStartSymbol());
if (startProductions.size()==0){
throw new IllegalStateException("cannonicalCollectionOfLR0Items failed to determine start production");
}
if (startProductions.size()>1){
throw new IllegalStateException("cannonicalCollectionOfLR0Items failed to determine unique start production");
}
List<Symbol> allSymbols=getGrammar().getAllSymbols();
Set<LR0Item> startSet=new HashSet<LR0Item>();
startSet.add(new LR0Item(startProductions.get(0), 0)); // [S0 --> .S]
List<Set<LR0Item>> cannonicalCollection= new ArrayList<Set<LR0Item>>(); // C
cannonicalCollection.add(closure(startSet)); // C = {CLOSURE({[S0 --> .S]})};
boolean anyItemSetAdded=false;
do{
anyItemSetAdded=false;
// Achtung: cannonicalCollection muss über den Index iteriert werden, weil Itemmengen hinzugefügt werden
for (int i = 0; i < cannonicalCollection.size(); i++) { // for ( jede Item-Menge I in C )
Set<LR0Item> anItemSet = cannonicalCollection.get(i);
for (Symbol aSymbol : allSymbols) { // for ( jedes Grammatiksymbol X )
Set<LR0Item> aGotoSet=gotoSet(anItemSet, aSymbol);
if (!aGotoSet.isEmpty() && !cannonicalCollection.contains(aGotoSet)){ // if ( GOTO(I,X) ist nicht leer und nicht in C )
cannonicalCollection.add(aGotoSet);// füge GOTO(I,X) zu C hinzu;
anyItemSetAdded=true;
}
}
}
}
while(anyItemSetAdded); // es werden keine neuen Item-Mengen mehr in einer Runde zu C hinzugefügt
return cannonicalCollection;
}
}