package de.fuberlin.projectci.grammar;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Repräsentiert eine Grammatik mit Produktionen.
*
*/
public class Grammar {
// Die Liste enthält alle Produktion in der Reihenfolge, wie sie zur Grammatik hinzugefügt worden
private List<Production> productions = new ArrayList<Production>();
// speichert alle Symbole in der Reihenfolge ihres ersten Auftretens
// erleichtert die Kontrolle beim Aufbau der LR(0)-Automaten, da die Nummerierung der Zustände von der Reihenfolge der Symbole abhängt
private List<Symbol> allSymbols= new ArrayList<Symbol>();
// Speichern der Terminal Symbole
private Map<String, TerminalSymbol> name2Terminal = new HashMap<String, TerminalSymbol>();
// Speichern der Nicht Terminal Symbole
private Map<String, NonTerminalSymbol> name2NonTerminal = new HashMap<String, NonTerminalSymbol>();
// Speichert zu jedem Nichtterminal eine Liste von Produktionen, bei denen es auf der linken Regelseite vorkommt
private Map<NonTerminalSymbol, List<Production>> nonTerminal2Productions = new HashMap<NonTerminalSymbol, List<Production>>();
//Startsymbol
NonTerminalSymbol startSymbol = null;
// Map für die FirstMengen
private Map<Symbol,Set<TerminalSymbol>> firstSets = null;
// Map für die Follomengen
private Map<NonTerminalSymbol,Set<TerminalSymbol>> followSets = null;
// Set für die Terminalsymbole
private Set<TerminalSymbol> terminalSymbols = new HashSet<TerminalSymbol>();
public static final String EMPTY_STRING = "@" ;
public static final TerminalSymbol EPSILON = new TerminalSymbol(EMPTY_STRING);
/**
* Markiert das rechte Ende des Inputstrings. Wird für die Followmengen benötigt.
* Im Drachenbuch als $ notiert.
*/
public static final TerminalSymbol INPUT_ENDMARKER = new TerminalSymbol("EOF"); // 'eof'
/**
* Gibt die erste Produktion der Grammatik zurück. <br>
* 1. Produktion mit dem Startsymbol
* @return
*/
public Production getStartProduction(){
return getProductionsByLhs(getStartSymbol()).get(0);
}
/**
* Gibt eine Liste aller Produktionen in der Grammatik zurück.
* @return
*/
public List<Production> getProductions() {
return productions;
}
/**
* Gibt die Produktion an einem bestimmten Index zurück.
* @param index
* @return
*/
public Production getProductionAtIndex(int index){
return productions.get(index);
}
/**
* Gibt alle Produktionen zurück, die das übergebene NonTerminalSymbol auf der linken Seite haben.
*/
public List<Production> getProductionsByLhs(NonTerminalSymbol lhs) {
return nonTerminal2Productions.get(lhs);
}
/**
* Fügt eine neue Produktion hinzu.
* @param production
*/
public void addProduction(Production production){
if(!productions.contains(production))
productions.add(production);
// In die Liste Nonterminal Symbol --> Porduktionen einfügen
// Eintrag schon vorhanden?
List<Production> temp = nonTerminal2Productions.get(production.getLhs());
if (temp != null) {
nonTerminal2Productions.get(production.getLhs()).add(production);
} else {
temp = new LinkedList<Production>();
temp.add(production);
nonTerminal2Productions.put(production.getLhs(), temp);
}
}
/**
* Gibt für ein Nichtterminalsymbol das entsprechende Objekt zurück oder legt ein neues an.
* @param name Der Wert des Nichtterminalsymbols als String
* @return Das dazu passende Objekt
*/
public NonTerminalSymbol createNonTerminalSymbol(String name) {
// Gibt es zu diesem Token schon ein Nicht Terminalsymbol?
NonTerminalSymbol result = name2NonTerminal.get(name);
if(result == null) {
result = new NonTerminalSymbol(name);
name2NonTerminal.put(name, result);
}
if (!allSymbols.contains(result)){
allSymbols.add(result);
}
return result;
}
/**
* Gibt für ein Terminalsymbol das entsprechende Objekt zurück oder legt ein neues an.
* @param name Der Wert des Terminalsymbols als String
* @return Das dazu passende Objekt
*/
public TerminalSymbol createTerminalSymbol(String name) {
TerminalSymbol result = name2Terminal.get(name);
if(result == null) {
result = name.equals(EMPTY_STRING) ? EPSILON : new TerminalSymbol(name);
/*
if(name.equals(EMPTY_STRING))
result = EPSILON;
else
result = new TerminalSymbol(name);
*/
name2Terminal.put(name,result);
}
if (!allSymbols.contains(result)){
allSymbols.add(result);
}
terminalSymbols.add(result);
return result;
}
/**
* Legt das Startsymbol für die Grammatik fest.
* @param startSymbol
*/
public void setStartSymbol(NonTerminalSymbol startSymbol) {
if(nonTerminal2Productions.containsKey(startSymbol)) {
this.startSymbol = startSymbol;
} else {
throw new RuntimeException("Ungültiges Startsymbol");
}
}
/**
* Gibt das Startsymbol der Grammatik zurück
* @return
*/
public NonTerminalSymbol getStartSymbol() {
return startSymbol;
}
/**
* Gibt eine Menge der Nichtterminale zurück.
* @return@see java.util.Set
*/
public Set<NonTerminalSymbol> getAllNonTerminals() {
return nonTerminal2Productions.keySet();
}
/**
* Gibt die Namen der Nichtterminale zurück.
* @return Menge von Strings aller Nichtterminale.
*/
public Set<String> getAllNonterminalNames(){
return name2NonTerminal.keySet();
}
/**
* Gibt eine Menge aller Terminale zurück
* @return
*/
public Set<TerminalSymbol> getAllTerminalSymols() {
return terminalSymbols;
}
/**
* Liefert eine Liste aller Grammatiksymbole in der Reihenfolge ihres ersten Auftretens
*/
public List<Symbol> getAllSymbols() {
return allSymbols;
}
/**
* Gibt eine menschenlesbare Textdarstellung der Grammatik zurück.
*/
@Override
public String toString() {
StringBuilder grammarSB = new StringBuilder();
for(Production p : productions) {
grammarSB.append(p.toString());
grammarSB.append("\n");
}
// Das Startsymbol am Ende anzeigen
grammarSB.append("\n");
grammarSB.append("Startsymbol: ");
grammarSB.append(startSymbol);
return grammarSB.toString();
}
// ****************************************************************************
// * Implementierung einiger grundlegender Grammatik-Algorithmen
// ****************************************************************************
/*
* Überlegung für den Algorithmus für die First Mengen:
* - die First Mengen werden alle aufeinmal paralell berechnet
* - Vielleicht zuerst eine HashMap Symbol -> Set<Terminal> füllen
*/
/**
* Berechnet die Firstmenge für ein Symbol s der Grammatik
* @param s Das Symbol (Terminal oder nicht Terminal) der Grammatik
* @return Die Firstmenge für das Symbol S. Gibt null zurück, wenn es Symbol nicht gibt
*/
public Set<TerminalSymbol> first(Symbol s) {
// Wenn die FirstMengen noch nicht berechnet wurden, dann mach dies jetzt
if(firstSets == null)
firstSets = calculateFirstSets();
// Die Firstmenge für das Symbol zurück geben
return firstSets.get(s);
}
/**
* Berechnet die Firstmenge für einen String von Symbolen X1X2...Xn
* @param Eine geordnete Liste von Symbolen, die den Eingabestring darstellt.
* @return Die Firstmenge zu den String
*/
public Set<TerminalSymbol> first(List<Symbol> symbols) {
// Wenn die FirstMengen noch nicht berechnet wurden, dann mach dies jetzt
if (firstSets == null)
firstSets = calculateFirstSets();
HashSet<TerminalSymbol> resultFirstSet = new HashSet<TerminalSymbol>();
boolean containsEpsilon = false;
// Berechne die Firstmenge für einen String X1X2...Xn
for(Symbol x : symbols) {
// Die Fistmenge von x ohne ε bestimmen (Kopie notwendig)
HashSet<TerminalSymbol> firstSetOfX = new HashSet<TerminalSymbol>(firstSets.get(x));
containsEpsilon = firstSetOfX.remove(EPSILON);
// FIRST(Xi)/ε zu FIRST(X1X2...Xn) hinzufügen
resultFirstSet.addAll(firstSetOfX);
// Nur wenn FIRST(Xi) ein ε enthielt muss FIRST(Xi+1) untersucht werden
if(!containsEpsilon)
break;
}
// Wenn in allen Xi ein ε gefunden wurde, dann füge ε zu FIRST(X1X2...n) hinzu
if(containsEpsilon)
resultFirstSet.add(EPSILON);
return resultFirstSet;
}
/**
* Berechnet erschöpfend die First-Mengen für alle Symbole der Grammatik.
* Implementierung des Algorithmus aus dem Drachenbuch Kapitel 4.4.2 Seite 221 (Englische Fassung)
*
* @return Es wird ein Wörterbuch von Symbolen auf eine Menge von Terminalsymbolen zurück gegeben
*/
public Map<Symbol,Set<TerminalSymbol>> calculateFirstSets() {
firstSets = new HashMap<Symbol,Set<TerminalSymbol>>();
// 1. Für alle Terminalsymbole die Mengen erzeugen.
// Für ein Terminal t gilt FIRST(t) = {ŧ}
for(TerminalSymbol t : terminalSymbols) {
// Neue leere Menge anlegen
Set<TerminalSymbol> tempSet = new HashSet<TerminalSymbol>();
// Terminalsymbol erzeugen
tempSet.add(t);
firstSets.put(t, tempSet);
}
// Für alle Nichtterminale initialisieren
for(NonTerminalSymbol t : getAllNonTerminals()) {
firstSets.put(t, new HashSet<TerminalSymbol>());
}
// 2. Zu allen Epsilonproduktionen wird Epsilon zur FIRST Menge der LHS hinzugefügt.
// Über die Produktionen iterieren
boolean changed = true;
// Ich versuchs auch noch mal
while(changed) { // erschöpfende Ausführung
changed = false;
for(Production p : productions) {
// 2. Regel: Gibt es eine Produktion X → ε, so füge ε zu FIRST(X) hinzu.
if(p.getRhs().get(0).equals(EPSILON))
changed = changed || firstSets.get(p.getLhs()).add(EPSILON);
boolean removed = false;
// 3.Regel
for(Symbol y : p.getRhs()) {
Set<TerminalSymbol> firstY = new HashSet<TerminalSymbol>(firstSets.get(y)); // Kopie erzeugen
removed = firstY.remove(EPSILON);
changed = changed || firstSets.get(p.getLhs()).addAll(firstY); // FIRST(Yi)/ε zu FIRST(X) hinzufügen
if(!removed)
break; // Wenn kein ε vorkam, schluss
}
// ε Einfügen wenn alle FIRST(Yi) ε enthalten
if(removed)
changed = changed || firstSets.get(p.getLhs()).add(EPSILON);
}
}
return firstSets;
}
/**
* Berechnet die Follow-Menge zu einem Nicht-Terminalsymbol nach dem Algorithmus im Drachenbuch.
* Abschnitt 4.4.2 "FIRST and FOLLOW" Seite 221f (Englische, 2. Ausgabe)
*
* <b> Das Startsymbol muss hierzu gesetzt sein! </b>
*/
public Set<TerminalSymbol> follow(NonTerminalSymbol nts) {
// ohne gesetztes Startsymbol kann der Algorithmus nicht arbeiten
if(startSymbol == null)
//return null;
throw new RuntimeException("Kein Startsymbol gesetzt, kann follow Mengen nicht berechnen!");
// Ohne Firstmengen kann der Algorithmus nicht laufen
if(firstSets == null )
firstSets = calculateFirstSets();
if(followSets == null)
followSets = calculateFollowSets();
return followSets.get(nts);
}
/**
* Berechnet erschöpfend die Follow-Mengen für alle Symbole in dieser Grammatik.
* Abschnitt 4.4.2 "FIRST and FOLLOW" Seite 221f (Englische, 2. Ausgabe)
*
* <b> Das Startsymbol muss hierzu gesetzt sein! </b>
* @return
*/
public Map<NonTerminalSymbol, Set<TerminalSymbol>> calculateFollowSets() {
// ohne gesetztes Startsymbol kann der Algorithmus nicht arbeiten
if(startSymbol == null)
//return null;
throw new RuntimeException("Kein Startsymbol gesetzt, kann follow Mengen nicht berechnen!");
// Die Mengen in der Map initialisieren
followSets = new HashMap<NonTerminalSymbol,Set<TerminalSymbol>>();
for(NonTerminalSymbol nt : getAllNonTerminals()) {
followSets.put(nt, new HashSet<TerminalSymbol>());
}
// [Algorithm] 1. Regel: Füge $ (Endmarkierung) zur FOLLOW Menge des Startsymbols ein
followSets.get(startSymbol).add(INPUT_ENDMARKER);
// erschöpfende Ausführung
boolean changed = true;
while(changed) {
changed = false;
// [Algorithm] 2. Regel: für eine Produktion A → αBβ, füge alles von FIRST(β) außer ε
// zu FOLLOW(B) hinzu
//hmm wie alpha beta und b erkennen? alle präfixe durchgehen?
// ja also die Symbole(nur Nichtterminale als "B" betrachten) der Reihe nach durchgehen und jeweils den Suffix als beta nehmen?
for(Production p : productions) {
for(int i = 0; i < p.getRhs().size(); i++) {
Symbol B = p.getRhs().get(i);
NonTerminalSymbol A = p.getLhs();
if(B instanceof NonTerminalSymbol) { // an der Stelle i wäre jetzt B
// alles hinter B gehört zu β
List<Symbol> beta = p.getRhs().subList(i+1, p.getRhs().size());
// füge FIRST(β)\ε zu FOLLOW(B) hinzu
Set<TerminalSymbol> firstOfBeta = first(beta);
boolean betaHasEpsilon = firstOfBeta.remove(EPSILON);
changed = changed || followSets.get(B).addAll(firstOfBeta);
// [Algorithm] 3. Regel: für alle A → αB oder A → αBβ mit ε ∈ FIRST(β),
// füge FOLLOW(A) zu FOLLOW(B) hinzu
if(beta.isEmpty() || betaHasEpsilon){
changed = changed || followSets.get(B).addAll(followSets.get(A));
}
}
}
}
}
return followSets;
}
}