Lerninhalte finden
Features
Entdecke
© StudySmarter 2024, all rights reserved.
Tokenisierung und Lexikalische AnalyseBetrachte die Konzepte der Tokenisierung und der Rolle eines Lexers bei der lexikalischen Analyse. Diese Begriffe sind zentral für das Verständnis der Übersetzung von Programmiersprachen in kompilierten Programmen.Tokens sind die kleinsten syntaktisch unterscheidbaren Einheiten in einer Programmiersprache und können verschiedene Arten haben, darunter Bezeichner, Schlüsselwörter, Literale, Operatoren und Trennzeichen. Der Lexer, ein Werkzeug zur lexikalischen Analyse, nimmt Quellcode-Text als Eingabe und gibt eine Folge von Tokens aus. Jedes Token hat das Format <type, value>
. Diese Übung fordert dich dazu auf, die Konzepte der Tokenisierung und des Einsatzes eines Lexers zu untersuchen und anzuwenden.
<type, value>
, Quellcode, und Ausgabe.Lösung:
<type, value>
ausgegeben. Dabei gibt type
den Typ des Tokens (z. B. Schlüsselwort oder Operator) und value
den genauen Wert bzw. Inhalt des Tokens an. Die Aufgabe des Lexers ist es also, einen Input-Stream des Quellcodes in eine Folge von Tokens umzuwandeln, die als Output für die weitere Analyse und Verarbeitung durch den Compiler oder Interpreter dient.
int main() { int a = 5; a = a + 10; print(a);}
<type, value>
-Format auf.Lösung:
int main() { int a = 5; a = a + 10; print(a);}
<type, value>
-Format:
<type, value>
aus. Beachte, dass der Quellcode nicht vollständig analysiert sein muss – die Fokussierung auf einen kleinen Ausschnitt ist ausreichend.'your code actual here'
Lösung:
class Lexer: def __init__(self, source_code): self.source_code = source_code self.tokens = [] self.current_char = '' self.index = 0 self.token_types = { 'keywords': ['int', 'print'], 'operators': ['=', '+'], 'delimiters': ['(', ')', '{', '}', ';'] } def next_char(self): if self.index < len(self.source_code): self.current_char = self.source_code[self.index] self.index += 1 else: self.current_char = None def add_token(self, token_type, value): self.tokens.append((token_type, value)) def get_tokens(self): self.next_char() while self.current_char is not None: if self.current_char.isspace(): self.next_char() elif self.current_char.isalpha(): self.collect_identifier_or_keyword() elif self.current_char.isdigit(): self.collect_number() elif self.current_char in self.token_types['operators']: self.add_token('operator', self.current_char) self.next_char() elif self.current_char in self.token_types['delimiters']: self.add_token('delimiter', self.current_char) self.next_char() else: self.next_char() return self.tokens def collect_identifier_or_keyword(self): identifier = '' while self.current_char is not None and self.current_char.isalnum(): identifier += self.current_char self.next_char() if identifier in self.token_types['keywords']: self.add_token('keyword', identifier) else: self.add_token('identifier', identifier) def collect_number(self): number = '' while self.current_char is not None and self.current_char.isdigit(): number += self.current_char self.next_char() self.add_token('literal', number)# Gegebener Quellcodesource_code = 'int main() { int a = 5; a = a + 10; print(a);}'# Lexer initialisieren und Tokens erhaltenlexer = Lexer(source_code)result = lexer.get_tokens()# Tokens ausgebenfor token in result: print(f'<{token[0]}, {token[1]}>')
<type, value>
-Format ausgibt.
Es soll eine Textverarbeitungssoftware entwickelt werden, die eine Vielzahl von Textmustern erkennen und verarbeiten kann. Verwenden Sie dazu reguläre Ausdrücke, um folgende Aufgaben zu bewältigen:
Teilaufgabe 1: Schreiben Sie einen regulären Ausdruck, der alle gültigen E-Mail-Adressen erkennt. Eine gültige E-Mail-Adresse besteht aus:
Geben Sie auch ein Beispieltext an und markieren Sie die erkannten E-Mail-Adressen.
Lösung:
Um einen regulären Ausdruck zu schreiben, der gültige E-Mail-Adressen erkennt, müssen wir die angegebenen Regeln beachten:
Hier ist der entsprechende reguläre Ausdruck:
/[\w\.]+@[\w\.]+/
Dieser reguläre Ausdruck erkennt gültige E-Mail-Adressen, die wie folgt aufgebaut sind:
Hier ist ein Beispieltext mit E-Mail-Adressen:
Bitte kontaktieren Sie uns unter info@example.com oder support.team@service.co.uk für weitere Informationen.
Erkannte E-Mail-Adressen:
Teilaufgabe 2: Schreiben Sie einen regulären Ausdruck, der alle Wörter in einem Text erkennt, die:
Geben Sie ein entsprechendes Beispiel an und markieren Sie die erkannten Wörter im Text.
Lösung:
Um einen regulären Ausdruck zu schreiben, der alle Wörter erkennt, die mit dem Buchstaben 'A' beginnen und genau 5 Zeichen lang sind, können wir wie folgt vorgehen:
Der reguläre Ausdruck sieht folgendermaßen aus:
\bA\w{4}\b
Hier ist eine Aufschlüsselung des regulären Ausdrucks:
Hier ist ein Beispieltext mit Wörtern:
Der Arzt stellte sicher, dass alle Akten aktualisiert wurden. Auch der Akira wurde informiert.
Erkannte Wörter:
Teilaufgabe 3: Schreiben Sie einen regulären Ausdruck, um Datumsangaben im Format DD/MM/YYYY zu extrahieren. Stellen Sie sicher, dass der reguläre Ausdruck:
Geben Sie ein Beispieltext an und markieren Sie die extrahierten Datumsangaben.
Lösung:
Um Datumsangaben im Format DD/MM/YYYY zu extrahieren, müssen wir sicherstellen, dass der reguläre Ausdruck die spezifischen Bereiche für Tag (01-31), Monat (01-12) und Jahr (vier Ziffern) korrekt erkennt.
Hier ist der entsprechende reguläre Ausdruck:
\b(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/\d{4}\b
Hier ist eine Erklärung des regulären Ausdrucks:
Hier ist ein Beispieltext mit Datumsangaben:
Die Veranstaltung findet am 12/05/2023 statt. Ein weiteres Meeting wurde für den 01/01/2022 geplant.
Erkannte Datumsangaben:
In diesem Bereich wirst Du eine kontextfreie Grammatik (CFG) analysieren und eine Parsing-Strategie anwenden. Betrachte die folgende CFG, die eine einfache arithmetische Sprache definiert:
Teilaufgabe 1: Bestimme für die gegebenen Produktionsregeln die First- und Follow-Mengen für jedes der Nicht-Terminalen (S, E, T, F).
Lösung:
Um die First- und Follow-Mengen für die gegebenen Produktionsregeln zu bestimmen, gehen wir Schritt für Schritt vor.
Die First-Menge eines Nicht-Terminals enthält die Terminalsymbole, die als erstes Symbol in irgendeiner Ableitung der Produktionsregeln erscheinen können.
Die Follow-Menge eines Nicht-Terminals enthält die Terminalsymbole, die unmittelbar nach diesem Nicht-Terminal in irgendeiner Ableitung der Produktionsregeln erscheinen können.
Teilaufgabe 2: Erstelle einen rekursiven Abstieg-Parser (Top-Down Parsing) für diese Grammatik. Zeige den Parsing-Prozess für den Eingabestring 'id + id * id'.
def parse_S(): # Deine Implementierung hierdef parse_E(): # Deine Implementierung hierdef parse_T(): # Deine Implementierung hierdef parse_F(): # Deine Implementierung hierdef main(): input_string = 'id + id * id' # Call starting function parse_S()if __name__ == '__main__': main()
Lösung:
Ein rekursiver Abstieg-Parser ist eine einfache und intuitive Methode zur Analyse einer kontextfreien Grammatik. Wir implementieren einen solchen Parser für die gegebene Grammatik und zeigen den Parsing-Prozess für den Eingabestring 'id + id * id'
.
Der Parser wird vier Funktionen enthalten, eine für jedes Nicht-Terminal in der Grammatik (S, E, T, und F). Wir beginnen mit der Definition einiger globaler Variablen, um den Eingabestring und den aktuellen Index zu verfolgen.
input_string = '' index = 0 def parse_S(): print("Parsing S") parse_E() if index == len(input_string): print("Parsing erfolgreich!") else: print("Parsing fehlgeschlagen. Unverarbeiteter Eingabe-String: " + input_string[index:]) def parse_E(): print("Parsing E") parse_T() while match('+'): advance() parse_T() def parse_T(): print("Parsing T") parse_F() while match('*'): advance() parse_F() def parse_F(): print("Parsing F") if match('id'): advance() elif match('('): advance() parse_E() if match(')'): advance() else: error() else: error() def match(token): global index return input_string[index:index+len(token)] == token def advance(): global index index += 1 def error(): print("Parsing error at index " + str(index) + ": " + input_string[index:]) exit(1) def main(): global input_string input_string = 'id + id * id'.replace(' ', '') # Whitespace entfernen parse_S() if __name__ == '__main__': main()
Im Detail betrachtet:
parse_S()
: Sie beginnt den Parsing-Prozess, indem sie parse_E()
aufruft. Nach der Rückkehr überprüft sie, ob der gesamte Eingabestring verarbeitet wurde.parse_E()
: Sie analysiert ein E. Dies beginnt mit der Analyse von T. Falls danach ein '+' folgt, wird erneut T analysiert.parse_T()
: Sie analysiert ein T. Dies beginnt mit der Analyse von F. Falls danach ein '*' folgt, wird erneut F analysiert.parse_F()
: Sie analysiert ein F. Es kann entweder 'id'
oder ein Ausdruck in Klammern sein. Falls ein '('
folgt, wird parse_E()
aufgerufen und anschließend muss ein ')'
folgen, sonst gibt es einen Fehler.match(token)
überprüft, ob das aktuelle Token passt, und advance()
bewegt den Zeiger im Eingabestring vorwärts.Durch das Ersetzen von Leerzeichen in main()
und mit dem Parsing-Prozess für 'id + id * id'
wird der Parser den string erfolgreich analysieren und die Struktur der Grammatik überprüfen.
Teilaufgabe 3: Baue die LR(0)-Parsing-Tabelle für die gegebene Grammatik auf. (Hinweis: Zustände und Goto/Action-Tabellen berücksichtigen).
Lösung:
LR(0)-Parsing-Tabellen bestehen aus zwei Hauptkomponenten: der Zustandsmenge (item sets) und den Parsing-Tabellen (Action- und Goto-Tabellen). Lass uns Schritt für Schritt durchgehen, um diese zu erstellen.
Zuerst müssen wir die Items für jedes Nicht-Terminal erstellen und dann die Zustände finden.
Die gegebene Grammatik ist:
Wir fügen ein erweitertes Startsymbol hinzu:
Erstellen der Items:
Der Anfangszustand umfassen die Closure von S' → .S:
Von hier aus leiten wir die restlichen Zustände ab:
Die Action- und Goto-Tabellen werden aus den abgeleiteten Zuständen erstellt.
Eine grobe Skizze der Parsing-Tabellen könnte wie folgt aussehen:
Action-Tabelle:
Zustand | id | ( | ) | + | * | $ |
---|---|---|---|---|---|---|
0 | s4 | s7 | ||||
1 | acc | |||||
2 | s5 | r1 | ||||
3 | r3 | s6 | r3 | |||
4 | r6 | r6 | r6 | r6 | ||
5 | s4 | s7 | ||||
6 | s4 | s7 | ||||
7 | s4 | s7 |
Goto-Tabelle:
Zustand | S | E | T | F |
---|---|---|---|---|
0 | 1 | 2 | 3 | 4 |
1 | ||||
2 | ||||
3 | ||||
4 | ||||
5 | 6 | 4 | ||
6 | 4 | |||
7 | 2 | 3 | 4 |
Zusätzliche Zustände und Einträge in den Tabellen werden entsprechend den Übergängen und Reduktionen hinzugefügt.
Beachte, dass dies eine vereinfachte und teilweise Darstellung der Parsing-Tabellen ist. Der vollständige Aufbau der Zustände und Tabellen erfordert das Durchlaufen der Items und die Wiederholung der Verschiebungen und Reduktionen, bis alle Zustände und Einträge ausgearbeitet sind.
Teilaufgabe 4: Zeige den Parsing-Prozess des Bottom-Up Parsing (Shift-Reduce) für den Eingabestring 'id + id * id' unter Verwendung der zuvor erstellten LR(0)-Parsing-Tabelle.
Lösung:
Um den Parsing-Prozess des Bottom-Up Parsing (Shift-Reduce) für den Eingabestring 'id + id * id'
zu zeigen, verwenden wir die zuvor erstellte LR(0)-Parsing-Tabelle. Der spektakuläre Parsing-Prozess wird durch eine Verarbeitungstabelle dargestellt, die die Schritte des Shifting und Reduzierens beschreibt.
Meine LR(0)-Parsing-Tabelle sieht wie folgt aus:
Zustand | id | ( | ) | + | * | $ |
---|---|---|---|---|---|---|
0 | s4 | s7 | ||||
1 | acc | |||||
2 | s5 | r1 | ||||
3 | r3 | s6 | r3 | |||
4 | r6 | r6 | r6 | r6 | ||
5 | s4 | s7 | ||||
6 | s4 | s7 | ||||
7 | s4 | s7 |
Zustand | S | E | T | F |
---|---|---|---|---|
0 | 1 | 2 | 3 | 4 |
1 | ||||
2 | ||||
3 | ||||
4 | ||||
5 | 6 | 4 | ||
6 | 4 | |||
7 | 2 | 3 | 4 |
Der Eingabestring ist 'id + id * id'
. Wir werden ihn Zeichen für Zeichen durchgehen und den Stack sowie die Aktionen aufzeichnen.
Schritt | Stack | Eingabe | Aktion |
---|---|---|---|
1 | 0 | id + id * id$ | Shift 4 |
2 | 0 id 4 | + id * id$ | Reduce F → id (r6) |
3 | 0 F 3 | + id * id$ | Reduce T → F (r3) |
4 | 0 T 2 | + id * id$ | Reduce E → T (r1) |
5 | 0 E 1 | + id * id$ | Shift 5 |
6 | 0 E 1 + 5 | id * id$ | Shift 4 |
7 | 0 E 1 + 5 id 4 | * id$ | Reduce F → id (r6) |
8 | 0 E 1 + 5 F 3 | * id$ | Reduce T → F (r3) |
9 | 0 E 1 + 5 T 2 | * id$ | Shift 6 |
10 | 0 E 1 + 5 T 2 * 6 | id$ | Shift 4 |
11 | 0 E 1 + 5 T 2 * 6 id 4 | $ | Reduce F → id (r6) |
12 | 0 E 1 + 5 T 2 * 6 F 3 | $ | Reduce T → T * F (r2) |
13 | 0 E 1 + 5 T 2 | $ | Reduce E → E + T (r1) |
14 | 0 E 1 | $ | Accept |
Der Parsing-Prozess für den Eingabestring 'id + id * id'
ist erfolgreich. Jede Zeile des Parsing-Prozesses zeigt die Aktionen (Shift oder Reduce) und den Zustand des Stacks sowie der verbleibenden Eingabe im Detail an.
Recursive-Descent-Parser und LR-Parser sind Parsing-Techniken zur Analyse und Verarbeitung von Programmiersprachen; sie helfen, Quellcode in eine abstrakte Syntaxbaumstruktur umzuwandeln.
Erkläre detailliert die Funktionsweise eines Recursive-Descent-Parsers und bespreche, warum er hauptsächlich für LL(k)-Grammatiken effektiv ist. Gehe dabei konkret auf die Ausführungspunkte ein, an denen rekursive Aufrufe stattfinden.
Lösung:
Ein Recursive-Descent-Parser ist eine Art Top-Down-Parser, der zur syntaktischen Analyse von Quellcode verwendet wird. Er ist relativ einfach zu implementieren und basiert auf einer rekursiven Funktion oder Prozedur für jede Nichtterminalregel in der Grammatik. Hier sind die wichtigsten Schritte, die die Funktionsweise eines Recursive-Descent-Parsers erklären:
Ein Recursive-Descent-Parser ist hauptsächlich für die Analyse von LL(k)-Grammatiken geeignet, aus folgenden Gründen:
Die rekursiven Aufrufe in einem Recursive-Descent-Parser finden konkret an den Punkten statt, an denen die Grammatikregeln andere Nichtterminale referenzieren. Hier ein Beispiel:
A → BC
. Wenn die Funktion parseA()
aufgerufen wird, ruft diese Funktion parseB()
und danach parseC()
auf. Diese rekursiven Aufrufe folgen direkt der Struktur der Grammatik.B → b | d
, prüft die Funktion parseB()
, ob das nächste Token b
oder d
ist und ruft entsprechend die passende Funktion auf, die dann die jeweilige Ableitung vervollständigt.Stell dir vor, du hast eine Grammatik, die nicht LL(1) ist. Beschreibe die Herausforderungen und Einschränkungen, die einem Recursive-Descent-Parser bei der Analyse einer solchen Grammatik begegnen könnten.
Lösung:
Ein Recursive-Descent-Parser ist besonders gut geeignet für die Analyse von LL(1)-Grammatiken, da diese deterministisch sind und einfache Entscheidungen auf Basis eines einzigen Lookahead-Tokens treffen können. Wenn es jedoch darum geht, eine Grammatik zu analysieren, die nicht LL(1) ist, stößt ein Recursive-Descent-Parser auf viele Herausforderungen und Einschränkungen. Hier sind einige der wichtigsten Punkte:
A → A α | β
kann von einem rekursiven Descent-Parser nicht verarbeitet werden, da der Parser ständig die Regel für A
erneut aufrufen würde, ohne je zu einer Basisregel (wie β
) zu gelangen.A → A + b | cDiese Regel ist linksrekursiv und führt dazu, dass der Recursive-Descent-Parser endlos rekursiv
A
aufruft. Ein typischer Ansatz zur Beseitigung der Linksrekursion ist erforderlich, was die Grammatik ändern oder spezielle Techniken einführen würde.B → b | b + cEin Recursive-Descent-Parser wird Schwierigkeiten haben zu entscheiden, ob er
B → b
oder B → b + c
verwenden soll, wenn das aktuelle Token b
ist. Dies erfordert mehr Lookahead-Tokens, um eine eindeutige Entscheidung zu treffen.Zusammenfassend lässt sich sagen, dass ein Recursive-Descent-Parser auch wenn er einfach zu implementieren ist, für die Analyse von Grammatiken, die nicht LL(1) sind, erheblich eingeschränkt ist. Die Hauptprobleme liegen in der Unfähigkeit, Mehrdeutigkeiten zu behandeln, der Gefahr von Endlosschleifen bei Linksrekursion, dem Bedarf an Rückverfolgung und der Notwendigkeit von mehreren Lookahead-Tokens. Diese Einschränkungen machen ihn für komplexere Grammatiken weniger geeignet im Vergleich zu mächtigeren Parsern wie LR-Parsern.
Entwerfe eine Parsing-Tabelle für einen einfachen LR(0)-Parser für die folgende Grammatik:
'S -> A a | b''A -> A b | a'Erstelle das dazugehörige Zustandsdiagramm (Automat) und erläutere den Parsing-Prozess für die Eingabezeichenfolge 'b'.
Lösung:
Wir werden eine Parsing-Tabelle und ein Zustandsdiagramm für die folgende Grammatik entwerfen:
S → A a | b A → A b | a
Zuerst erstellen wir die LR(0)-Items für die Grammatik:
S' → S
I0: S' → . S S → . A a S → . b A → . A b A → . a
Wir erweitern diese Gruppe durch die Verschiebung des Punktes für alle Produktionen:
I1: S' → S .
I2: S → b .
I3: S → A . a A → . A b A → . a
I4: A → a .
I5: A → A . b A → . A b A → . a
I6: A → A b .
Erstellen der Übergänge zwischen den Zuständen durch Verschieben des Punktes:
Von I0: Mit S zu I1 Mit b zu I2 Mit A zu I3
Von I3: Mit a zu I4 Mit A zu I5
Von I5: Mit b zu I6 Mit A zu I5
Basierend auf den LR(0)-Items und den Zuständen können wir die Parsing-Tabelle erstellen:
Zustand | a | b | $ | S | A |
---|---|---|---|---|---|
0 | S2 | 1 | 3 | ||
1 | Accept | ||||
2 | Reduce S → b | ||||
3 | S4 | 5 | |||
4 | Reduce A → a | ||||
5 | S4 | S6 | 5 | ||
6 | Reduce A → A b |
I0 --b--> I2 I0 --S--> I1 I0 --A--> I3 I3 --a--> I4 I3 --A--> I5 I5 --b--> I6
Die Eingabezeichenfolge ist 'b'. Wir folgen der Parsing-Tabelle, um den LR(0)-Parsing-Prozess zu erklären:
Der Parsing-Prozess für die Eingabezeichenfolge 'b' endet erfolgreich, indem die Eingabe als gültig akzeptiert wird.
Vergleiche die Vor- und Nachteile von Recursive-Descent-Parsern und LR-Parsern hinsichtlich ihrer Anwendbarkeit, Flexibilität und Effizienz. In deiner Diskussion, gehe konkret auf deren Einsatzmöglichkeiten in modernen Compilern ein und unterfüttere deine Argumente mit Beispielen.
Lösung:
In diesem Abschnitt vergleichen wir Recursive-Descent-Parser (RD-Parser) und LR-Parser hinsichtlich Anwendbarkeit, Flexibilität und Effizienz. Wir werden uns ihre Vor- und Nachteile im Detail ansehen und deren Einsatzmöglichkeiten in modernen Compilern diskutieren.
In modernen Compilern werden sowohl RD-Parser als auch LR-Parser verwendet, abhängig von den spezifischen Anforderungen der zu parsenden Sprache und der gewünschten Effizienz:
Zusammenfassend haben sowohl Recursive-Descent-Parser als auch LR-Parser ihre eigenen Vor- und Nachteile. RD-Parser sind einfacher zu implementieren und gut für kleinere, weniger komplexe Sprachkonstrukte, während LR-Parser mächtiger und effizienter bei der Verarbeitung komplexer und realitätsnaher Programmiersprachen sind. Bei der Auswahl der Parsing-Technik muss man sich dem spezifischen Kontext und den Anforderungen des Projektes bewusst sein.
Mit unserer kostenlosen Lernplattform erhältst du Zugang zu Millionen von Dokumenten, Karteikarten und Unterlagen.
Kostenloses Konto erstellenDu hast bereits ein Konto? Anmelden