DDL: Konzepte, Organisation und Dateien

Der DDL-Compiler ist in C++ implementiert. Als Hilfsmittel werden die Programme flex und bison verwendet.
Zur Verwaltung der Dateien und ihrer Abhängigkeiten bei der Übersetzung zum ausführbaren DDL-Compiler-Programm wird ein sogenanntes Makefile verwendet.

flex

Flex ist eine in der Funktionalität erweiterte GNU-Version von lex. Mit flex kann man in einfacher Weise endliche Automaten beschreiben. Dieser wird hier zur lexikalischen Analyse (Tokenisierung) einer ddl-Datei eingesetzt.

Erste Aufgabe des Programmierers ist es, mit Hilfe regulärer Ausdrücke die Token zu definieren, die erkannt werden sollen. Flex übersetzt dies in einen C/C++-Quellcode, der entsprechend die einzelnen Token-Arten (wie Schlüsselwörter, Bezeichner, Konstanten) durch int-Werte repräsentiert.

Der Programmierer kann nun dem jeweils erkannten Token zusätzlich zum Typ noch einen Wert zuweisen, so hat "1234" etwa den Typ Integer-Konstante und den Wert 1234. Der Programmierer muß diese Übersetzung von der als Token erkannten Zeichenkette in einen Wert als C/C++-Code zur Verfügung stellen.

Flex baut diesen Code entsprechend in den endlichen Automaten ein, der dann in der Form benutzt wird, daß ihm eine Eingabedatei übergeben wird und er eine Funktion zur Verfügung stellt, über die nacheinander die in der Datei enthaltenen Token mit Typ und Wert abgefragt werden können.

bison

Bison ist eine in der Funktionalität erweiterte GNU-Version von yacc. Mit bison kann man in einfacher Weise Parser (Programme zur Erkennung von kontextfreien Grammatiken) beschreiben. Dieser wird hier zur strukturellen Analyse einer ddl-Datei eingesetzt. Dabei wird eine Datenstruktur aufgebaut, die dem Baum der kontextfreien Grammatik entspricht. Der Baum wird durch einen Baum von Klassenobjekten repräsentiert, die über Memberfunktionen die semantische Analyse rekursiv vornehmen können. Ziel ist es dabei insbesondere, einen C++-Quellcode zu erzeugen, der die Domain repräsentiert und zur Erzeugung eines Domain-spezifischen Editors und Simulators verwendet werden kann.

Der Programmierer muß die verwendeten Token-Typen (Terminale) und die Regeln der kontextfreien Grammatik angeben. Die flex-Schnittstellenfunktion wird verwendet um die Terminale und deren zugehörige Werte einzulesen. Der Programmierer muß nun in C/C++-Code angeben, wie bei Anwendung einer KFG-Regel dem entstehenden Nichtterminal ein Wert zugeordnet werden soll. In unserem Fall wird dabei normalerweise mehrere Bäume zu einem mit einer neuen Wurzel zusammengefaßt.

Flex übersetzt diese Spezifikation des Programmierers in einen C/C++-Quelltext, der eine Funktion zur Verfügung stellt, mit dem der Wert des Startsymbols der Grammatik herausgefunden wird. In unserem Fall wird dadurch der komplette Syntaxbaum des ddl-Programms aufgebaut. Wie oben erwähnt besitzt dieser schon selbst die Fähigkeit zur Semantikanalyse.
Das Hauptprogramm muß also "nur" die von bison zur Verfügung gestellte Funktion aufrufen und danach die Wurzel des Syntaxbaums anweisen, die Semantikanalyse durchzuführen.

Dateien

Folgende Dateien werden vom Programmierer bearbeitet:

Deklarations-DateiImplementations-DateiAufgabe
Makefile Automatische Übersetzung des DDL-Compilers
Src/ddl_main.cc C++-Hauptprogramm, das den Parsevorgang und die Semantische Analyse auslöst
Includes/ddl_common.hh Deklarationen, die sowohl im DDL-Compiler als auch im Simulator zur Verfügung stehen müssen
Includes/komp_tools.hh Deklarationen, die im Simulator zur Verfügung stehen müssen; in den vom DDL-Compiler erzeugten C++-Code wird automatisch ein #include auf diese Datei eingefügt
Includes/error.hh Src/error.cc Fehler-Behandlung
Includes/cstring.hh Src/cstring.cc String-Klasse, die statt der C++-Strings (char*) benutzt wird
Includes/ddl_ds.hh Datenstrukturen, die vom DDL-Compiler benutzt werden.
Includes/ddl_nodes.hh Src/ddl_nodes.cc Datenstrukturen des DDL-Compilers für den Syntax-Baum
Src/ddl_lexer.l flex-Quellcode
Src/ddl_parser.y bison-Quellcode

Makefile

Die Benutzung eines Makefiles für normale C++-Projekte wird als bekannt vorausgesetzt.

Zusätzlich müssen die Regeln für die Übersetzung des flex- und des bison-Quellcodes angegeben werden:

[...]

.y.cc:
	$(YACC) $(YFLAGS) -o$@ $<

.l.cc:
	$(LEX) $(LFLAGS) -o$@ $<

[...]

$(DDLSRCDIR)/ddl_lexer.o: $(DDLSRCDIR)/ddl_lexer.cc \\
                          $(DDLSRCDIR)/ddl_parser.cc.h \\
                          $(DDLINCLUDEDIR)/ddl_nodes.hh 

[...]

$(DDLSRCDIR)/ddl_lexer.cc: $(DDLSRCDIR)/ddl_lexer.l

$(DDLSRCDIR)/ddl_parser.o: $(DDLSRCDIR)/ddl_parser.cc

[...]

$(DDLSRCDIR)/ddl_parser.cc: $(DDLSRCDIR)/ddl_parser.y

$(DDLSRCDIR)/ddl_parser.cc.h: $(DDLSRCDIR)/ddl_parser.y
	$(YACC) $(YFLAGS) -o$(DDLSRCDIR)/ddl_parser.cc $(DDLSRCDIR)/ddl_parser.y

[...]

Als Besonderheit fällt hier die Datei ddl_parser.cc.h auf. Diese wird von bison automatisch erstellt und wird als #include in die flex-Quellcode-Datei eingefügt.

In bison werden symbolische Namen für Token vereinbart, die für den erzeugten C++-Code in int-Werte übersetzt werden. Gleichzeitig erzeugt bison die Datei ddl_parser.cc.h, welche die symbolischen Namen und ihre int-Übersetzungen als #defines enthält. Damit können im flex-Quellcode die selben Namen verwendet werden und es ist sichergestellt, daß die Überstzung konsisten ist.

Da die #include-Anweisung textuell in den von flex erzeugten C++-Code übernommen wird, besteht die Abhängigkeit erst bei der Erzeugung der .o-Datei, also der Anwendung des C++-Compilers, nicht schon bei der Erzeugung des C++-Codes durch flex.