Entwicklung eines MiniJavaOO-Compilers für

Werbung
Fachhochschule Aachen
Fachbereich Elektrotechnik und Informationstechnik
Entwicklung eines MiniJavaOO-Compilers
für JVM-Bytecode
BACHELORARBEIT
Zur Erlangung des akademischen Grades
Bachelor of Science
in Informatik
Aachen, den 14. Juli 2009
Name:
Matrikelnummer:
Erstgutachter:
Zweitgutachter:
Michael Pasdziernik
317677
Prof. Dr. rer. nat. Heinrich Faßbender
Prof. Dr.-Ing. Martin Oßmann
Erklärung
Hiermit versichere ich, daß ich die vorliegende Arbeit selbständig verfaßt und keine anderen
als die angegebenen Quellen und Hilfsmittel benutzt habe, daß alle Stellen der Arbeit, die
wörtlich oder sinngemäß aus anderen Quellen übernommen wurden, als solche kenntlich
gemacht sind und daß die Arbeit in gleicher oder ähnlicher Form noch keiner Prüfungsbehörde vorgelegt wurde.
Aachen, den 14. Juli 2009
Inhaltsverzeichnis
1 Einleitung
1.1 Motivation und Ziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.3 Beispielprogramm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
11
12
14
2 Hintergrund
2.1 Die Programmiersprache MiniJavaOO . . . .
2.1.1 Sprachelemente von MiniJavaFunProc
2.1.2 Sprachelemente von MiniJavaOO . . .
2.1.3 Ergänzungen zur Sprachspezifikation .
2.2 Die Java Virtual Machine . . . . . . . . . . .
2.2.1 Architektur . . . . . . . . . . . . . . .
2.2.2 Erzeugen von Bytecode . . . . . . . .
2.3 Auswahl eines Compilergenerators . . . . . .
2.3.1 Anforderungen und Alternativen . . .
2.3.2 ANTLR . . . . . . . . . . . . . . . . .
2.4 Agiles Projektmanagement . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
18
18
19
20
21
22
22
24
25
25
26
30
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
34
34
37
37
38
40
45
47
47
49
49
54
55
55
58
65
66
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Der Compiler
3.1 Aufbau und Phasen . . . . . . . . . . . . . . . . . . .
3.2 Parser und abstrakte Syntax . . . . . . . . . . . . . .
3.2.1 Spracherweiterung über semantische Prädikate
3.2.2 Grenzen des LL(∗)-Parsergenerators . . . . . .
3.2.3 Abstrakter Syntaxbaum . . . . . . . . . . . . .
3.2.4 Fehlerbehandlung . . . . . . . . . . . . . . . . .
3.3 Symboltabellen und Typsystem . . . . . . . . . . . . .
3.3.1 Design . . . . . . . . . . . . . . . . . . . . . . .
3.3.2 Traversieren des AST . . . . . . . . . . . . . .
3.3.3 Aufbau der Symboltabellen . . . . . . . . . . .
3.3.4 Fehlerbehandlung . . . . . . . . . . . . . . . . .
3.4 Semantische Analyse . . . . . . . . . . . . . . . . . . .
3.4.1 Semantische Prüfungen . . . . . . . . . . . . .
3.4.2 Namensauflösung . . . . . . . . . . . . . . . . .
3.5 Generieren von Jasmin-Assemblercode . . . . . . . . .
3.5.1 Struktur des Assemblercodes . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
Inhaltsverzeichnis
3.5.2 Funktionsweise des Code-Emitters . . . . . . . . . . . . . . . . . . .
3.5.3 Ermitteln der Größe von Methoden-Stacks . . . . . . . . . . . . . . .
Generieren und Verifizieren von JVM-Bytecode . . . . . . . . . . . . . . . .
68
72
74
4 Testgetriebene Compiler-Entwicklung
4.1 Tests mit gUnit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Tests mit jUnit und StringTemplate . . . . . . . . . . . . . . . . . . . . . .
77
77
82
5 Zusammenfassung und Ausblick
87
Literaturverzeichnis
90
3.6
A Syntax und statische Semantik von MiniJavaOO
A.1 Programmstruktur . . . . . . . . . . . . . . . .
A.2 Klassen . . . . . . . . . . . . . . . . . . . . . .
A.3 Typen, Literale, Bezeichner und Namen . . . .
A.4 Referenzieren von Variablen und Methoden . .
A.5 Variablen . . . . . . . . . . . . . . . . . . . . .
A.6 Methoden . . . . . . . . . . . . . . . . . . . . .
A.7 Ausdrücke . . . . . . . . . . . . . . . . . . . . .
A.8 Blöcke und Statements . . . . . . . . . . . . . .
A.9 Syntaxdiagramme . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
91
92
93
96
97
98
99
101
102
105
B Grammatiken
B.1 Lexer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.2 Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.3 AST-Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
112
112
114
117
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
C Übersetzungsbeispiele
119
C.1 Binärbaum-Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
C.2 Ackermann-Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
4
Abbildungsverzeichnis
1.1
1.2
1.3
1.4
Die MiniJavaOO-Compiler . . . . . . . . . .
Der MiniJavaOO-Compiler für die JVM . .
Entarteter Binärbaum . . . . . . . . . . . .
Binärbaum-Programm: Klasse Nil im AST
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
10
12
14
16
2.1
2.2
2.3
2.4
2.5
2.6
DEA zur Regelentscheidung von unendlichem k
Debugging mit ANTLRWorks . . . . . . . . . .
ANTLR IDE in Eclipse . . . . . . . . . . . . .
Sprint Backlog in Agilo for Scrum . . . . . . .
Sprint Burndown in Agilo for Scrum . . . . . .
Timeline in Agilo for Scrum . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
27
28
29
31
32
32
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
3.10
3.11
3.12
3.13
3.14
3.15
3.16
3.17
3.18
3.19
3.20
3.21
3.22
3.23
Analyse-Phasen des Compilers . . . . . . . . . . . . . . . . . . . . . . . .
Synthese-Phasen des Compilers . . . . . . . . . . . . . . . . . . . . . . . .
Klassen zur Steuerung des Compilers . . . . . . . . . . . . . . . . . . . . .
Parser- und Lexer-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . .
DEA zur Regelentscheidung mit semantischen Prädikaten . . . . . . . . .
Klasse AstNode zur Symbol-Annotation . . . . . . . . . . . . . . . . . . .
Binärbaum-Programm: Globale Prozedur und main-Methode im AST . .
Binärbaum-Programm: Deklaration von Instanzvariablen im AST . . . . .
Ackermann-Programm: Ausdrücke im AST . . . . . . . . . . . . . . . . .
Exception-Klassen zur Fehlerbehandlung . . . . . . . . . . . . . . . . . . .
Klassen für Symbole und Symboltabellen . . . . . . . . . . . . . . . . . .
Klassen für das Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . .
Klassen der AST-Parser zum Aufbau der Symboltabellen . . . . . . . . .
Binärbaum-Programm: Symboltabellen . . . . . . . . . . . . . . . . . . . .
Binärbaum-Programm: Annotation der Deklarationen im AST . . . . . .
Klassen des AST-Parsers für die semantische Analyse . . . . . . . . . . .
Klasse SemantikCheckWalkerBase . . . . . . . . . . . . . . . . . . . .
Binärbaum-Programm: Annotationen im AST nach der Namensauflösung
Symbolsuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Klassen für den Zugriffsschutz . . . . . . . . . . . . . . . . . . . . . . . . .
Klassen des AST-Parsers für den Code-Emitter . . . . . . . . . . . . . . .
Java-API des Jasmin-Assemblers . . . . . . . . . . . . . . . . . . . . . . .
Klassen des Bytecode-Verifiers . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
34
35
36
37
38
41
42
44
45
46
48
48
50
53
54
55
56
62
63
65
68
75
75
5
Abbildungsverzeichnis
6
4.1
Klassen für die jUnit-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . .
C.1
C.2
C.3
C.4
C.5
Binärbaum-Programm: Globale Sprachelemente
Binärbaum-Programm: Klasse Baum im AST .
Binärbaum-Programm: Klasse Nil im AST . .
Ackermann-Programm im AST 1 . . . . . . . .
Ackermann-Programm im AST 2 . . . . . . . .
im AST
. . . . .
. . . . .
. . . . .
. . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
83
119
120
120
126
127
Listings
1.1
1.2
Beispielprogramm: Binärbaum in MiniJavaOO . . . . . . . . . . . . . . . .
Beispielprogramm: Klasse Nil in Assemblercode . . . . . . . . . . . . . . .
14
17
2.1
Beispielprogramm: Ackermann-Funktion in MiniJavaFunProc . . . . . . . .
19
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
3.10
3.11
3.12
3.13
3.14
3.15
3.16
3.17
3.18
3.19
3.20
3.21
3.22
3.23
3.24
3.25
3.26
3.27
3.28
Binärbaum-Programm: Globale Prozedur und main-Methode . . . . . . .
Parser-Regel: Übersetzen von Variablendeklarationen in abstrakte Syntax
Binärbaum-Programm: Deklaration von Instanzvariablen . . . . . . . . .
Parser-Regel: Übersetzen von Summen-Ausdrücken in abstrakte Syntax .
Ackermann-Programm: Ausdrücke . . . . . . . . . . . . . . . . . . . . . .
Lexer-Regel für das ANY-Token zur Fehlerbehandlung . . . . . . . . . . .
AST-Parser-Regel zum Aufbau der Klassentabelle . . . . . . . . . . . . .
AST-Parser-Regel zum Aufbau der Symboltabelle: Block . . . . . . . . . .
AST-Parser-Regel zum Aufbau der Symboltabelle: Instanzvariable . . . .
AST-Parser-Regel zur Prüfung der Zuweisung . . . . . . . . . . . . . . . .
AST-Parser-Regel zur Namensauflösung . . . . . . . . . . . . . . . . . . .
Methode zum Auflösen qualifizierter Namen . . . . . . . . . . . . . . . . .
Methoden zur Suche in den Symboltabellen . . . . . . . . . . . . . . . . .
Methode getSymbolRecursive . . . . . . . . . . . . . . . . . . . . . .
Methode checkAccess zur Zugriffsschutz-Prüfung . . . . . . . . . . . .
Methode accessOk zur Zugriffsschutz-Prüfung . . . . . . . . . . . . . . .
Binärbaum-Programm: Klassendefinition und Instanzvariablen . . . . . .
Binärbaum-Programm: Methode setValue in Assemblercode . . . . . .
Binärbaum-Programm: Methode inorder in Assemblercode . . . . . . .
Binärbaum-Programm: Konstruktor in Assemblercode . . . . . . . . . . .
Binärbaum-Programm: if-Statement . . . . . . . . . . . . . . . . . . . .
Binärbaum-Programm: if-Statement in Assemblercode . . . . . . . . . .
AST-Parser-Regel: if-Statement im Code-Emitter . . . . . . . . . . . . .
Templates: if-Statement im Code-Emitter . . . . . . . . . . . . . . . . .
Ein Stack unbekannter Größe in Assemblercode . . . . . . . . . . . . . . .
Template: print-Statement im Code-Emitter . . . . . . . . . . . . . . . .
AST-Parser-Regel: print-Statement im Code-Emitter . . . . . . . . . . .
AST-Parser-Regel: Methodenaufruf im Code-Emitter . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
42
43
43
44
45
47
50
51
52
57
59
61
63
64
64
65
66
66
67
68
69
69
70
71
72
73
73
74
4.1
gUnit-Test: Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
78
7
Listings
8
4.2
4.3
4.4
4.5
4.6
4.7
4.8
4.9
4.10
gUnit-Test: if und while im Parser . . . . . . .
gUnit-Test: Lokale Konstanten im Parser . . . .
gUnit-Test: Klasse im Parser . . . . . . . . . . .
Anpassen von gUnit an die Klasse AstNode . . .
gUnit-Test: AST-Parser für die Symboltabellen .
jUnit-Test: Semantik der Methodenüberlagerung
Templates: Test der Methodenüberlagerung . . .
jUnit-Test: Methodenaufruf im Code-Emitter . .
Templates: Test eines Methodenaufrufs . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
79
79
79
80
81
84
84
85
85
A.1
A.2
A.3
A.4
Beispielprogramm . . . . . . . . . . . . .
main-Methode zum Programmstart . . .
Einzelnes Statement zum Programmstart
Minimalbeispiel einer Klasse . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
91
93
93
95
C.1
C.2
C.3
C.4
Binärbaum-Programm: Globale Sprachelemente in Assemblercode .
Binärbaum-Programm: Klasse Baum in Assemblercode . . . . . . .
Binärbaum-Programm: Klasse Nil in Assemblercode . . . . . . . .
Ackermann-Programm in Assemblercode . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
121
123
124
127
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Zusammenfassung
In dieser Bachelorarbeit wird die Entwicklung eines Mehrphasen-Compilers mit Hilfe des
compilergenerierenden Frameworks ANTLR beschrieben. Quellsprache ist die objektorientierte Sprache MiniJavaOO. Zielsprache ist Bytecode für die Java Virtual Machine. Der
Compiler realisiert die typischen Phasen, aufgeteilt in ein Frontend und ein Backend, und
nutzt zur internen Verarbeitung des Quellprogramms eine abstrakte Syntax. Neben der
Implementierung des Compilers werden die testgetriebene Entwicklung sowie die Planung
und Steuerung der Programmierung mit agilen Methoden betrachtet.
1 Einleitung
Die vorliegende Arbeit befaßt sich mit der Entwicklung eines Mehrphasen-Compilers zur
Übersetzung der objektorientierten Programmiersprache MiniJavaOO in Bytecode für die
Java Virtual Machine (JVM).
MiniJavaOO ist eine im Rahmen des Praxisprojekts zusammen mit Manuel Rieke entworfene Erweiterung von MiniJavaFunProc 1 . Die Programmiersprache MiniJavaFunProc ist
prozedural und in Syntax bzw. Semantik an Java angelehnt. Sie dient dazu, während des
Compilerbau-Praktikums an der FH Aachen einen Einphasen-Compiler auf der Basis einer
LL(1)-Grammatik zu realisieren.
Die Erweiterungen sind in einer gemeinsam erstellten Sprachspezifikation (Anhang A) festgehalten, auf deren Grundlage zwei Compiler (Abbildung 1.1) entwickelt werden, die mit
unterschiedlichem Fokus verschiedene Aspekte des Compilerbaus beleuchten.
!
"!
#$
%&'
%&'
(
)*
+
+
+
0/22
-)3
!
,-,./01
.,
.,
/01
.,
!!! ! !!!
! !!!! !!
!!! ! !!!
! !!!! !!
!!! ! !!!
! !!!! !!
!!! ! !!!
! !!!! !!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
! !! !
!
! !! !
!
! !! !
!
! !! !
!
!
!
!
!
!
!
!
!
! !!
! !
! !!
! !
! !!
! !
! !!
! !
!
!
!
!
!
!
!
!
Abbildung 1.1: Die MiniJavaOO-Compiler
Manuel Riekes Arbeit [10] erweitert den im Praktikum entwickelten MiniJavaFunProcCompiler zur Übersetzung von MiniJavaOO in Assemblercode für die M32-CPU-Simulationsumgebung 2 . Der Fokus der Arbeit liegt auf der Umsetzung objektorientierter Sprachkonzepte mit dem Befehlssatz eines registerbasierten Prozessors.
1
2
Siehe http://www.fassbender.fh-aachen.de/Download/Compilerbau/Folien/
Siehe http://www.ossmann.fh-aachen.de/Down load/m32.zip. Die M32-CPU-Simulationsumgebung dient
der Vermittlung von Grundlagen zur System- und Assemblerprogrammierung im Praktikum zur Vorlesung Architektur von Rechnersystemen und Betriebssystemkonzepte.
10
1.1 Motivation und Ziele
Der in der vorliegenden Arbeit beschriebene Compiler ist eine Neuentwicklung mit Hilfe
des compilererzeugenden Frameworks ANTLR 3 . Ziel ist die Entwicklung eines typischen
Mehrphasen-Compilers, der intern eine abstrakte Syntax in Form einer Baumstruktur
nutzt. Möglichst alle Phasen sollen mit Hilfsmitteln von ANTLR realisiert werden. Der
Compiler übersetzt MiniJavaFunProc und MiniJavaOO auf die für objektorientierte Sprachen spezialisierte virtuelle Stackmaschine JVM. Die Programme sind somit direkt auf
Rechnern mit installierter JVM ausführbar.
1.1 Motivation und Ziele
Neben der Entwicklung von Compilern und Interpretern für allgemeine Programmiersprachen sind die Techniken des Compilerbaus in vielen Bereichen der Softwareentwicklung
von großem Nutzen. Komplexen Problemen kann oft mit den Mitteln der metalinguistischen Abstraktion und durch den Einsatz domänenspezifischer Sprachen (Domain Specific
Languages, kurz DSL) begegnet werden (vgl. [1], Kapitel 4, S. 375 ff.), weshalb neuere Programmiersprachen entsprechende Techniken in die Sprache integrieren (z. B. interne DSLs
in Ruby 4 oder Parser-Kombinatoren in Scala 5 ). Durch die intensive Auseinandersetzung
mit der Entwicklung eines Compilers im Rahmen der Bachelorarbeit können Kompetenzen
erworben werden, die für die Tätigkeit des Softwareentwicklers nützlich sind.
Konkret werden mit der Implementierung des MiniJavaOO-Compilers nachstehende Ziele
verfolgt:
Weiterentwicklung einer Programmiersprache Durch die Erweiterung der Programmiersprache MiniJavaFunProc um objektorientierte und weitere Sprachelemente, wie z. B.
die Möglichkeit zur Deklaration lokaler Variablen an beliebiger Stelle innerhalb von
Blöcken, wird die Spezifikation der Syntax wie auch der semantischen Einschränkungen einer formalen Sprache geübt und das Verständnis für die Konzepte der Objektorientierung vertieft.
Realisierung eines Mehrphasen-Compilers Aufbauend auf den im Praktikum gemachten Erfahrungen in der Compilerprogrammierung, wird ein Mehrphasen-Compiler
entwickelt, der den Anforderungen an die Übersetzung einer typischen Programmiersprache gerecht wird. Es werden verschiedene Techniken des Compilerbaus praktisch
umgesetzt, wozu unterstützend auf ein compilererzeugendes Framework zurückgegriffen wird und Methoden der ingenieursmäßigen Softwareentwicklung, wie agiles
Projektmanagement und testgetriebene Entwicklung, zur Anwendung kommen.
3
Siehe http://antlr.org/
Siehe http://www.artima.com/rubycs/articles/ruby as dsl.html
5
Siehe http://debasishg.blogspot.com/2008/04/external-dsls-made-easy-with-scala.html
4
11
1 Einleitung
Wissensvertiefung: Java Plattform Java ist von besonderer Relevanz in der professionellen Softwareentwicklung. Im TIOBE-Index6 zur Verbreitung von Programmiersprachen rangiert Java seit Jahren auf den oberen Plätzen. Auch abstrakte Stackmaschinen wie die JVM oder auch die Common Language Runtime (CLR) des Microsoft
.NET Frameworks liegen im Trend und werden immer häufiger als Zielplattform für
Programmiersprachen gewählt. Durch die zur Umsetzung des Compilers erforderliche
Auseinandersetzung mit den entsprechenden Sprach- und Maschinenspezifikationen
([3], [4] und [11]) wird das im Laufe des Studiums erlangte Wissen in diesem Bereich
vertieft.
1.2 Überblick
Der MiniJavaOO-Compiler (Abbildung 1.2) folgt der typischen Aufteilung in ein Frontend und ein Backend, die den Übersetzungsprozeß in mehreren sequenziell ausgeführten
Phasen organisiert (vgl. [14], S. 146 ff.). Dabei wird die Eingabe mehrfach durchlaufen.
Um dies effizient zu gestalten, übersetzt die erste Phase das MiniJavaOO-Programm in
eine abstrakte Syntax mit der Struktur eines Baums. Die Aufteilung der Übersetzung auf
mehrere Phasen ist insbesondere zur Prüfung der statischen Semantik nötig, z. B. kann mit
einem Einphasen-Compiler eine Typangabe nicht überprüft werden, falls sie sich auf eine
erst danach deklarierte Klasse bezieht.
!
"!
#$
$
$%&%
$'
$'
$(
!!! ! !!!
! !!!! !!
!!! ! !!!
! !!!! !!
!!! ! !!!
! !!!! !!
!!! ! !!!
! !!!! !!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
!
! !! !
!
! !! !
!
! !! !
!
! !! !
!
!
!
!
!
!
!
!
!
! !!
! !
! !!
! !
! !!
! !
! !!
! !
!
!
!
!
!
!
!
!
Abbildung 1.2: Der MiniJavaOO-Compiler für die JVM
Zur Übersetzung durchläuft ein MiniJavaOO-Programm den Compiler wie folgt:
• Parser und Lexer prüfen die lexikalische und syntaktische Korrektheit des Quellprogramms und übersetzen es in die zur Weiterverarbeitung notwendige abstrakte Syntax. Abschnitt 1.3 zeigt ein Beispiel für ein MiniJavaOO-Programm und die
Übersetzung in abstrakte Syntax.
6
Der TIOBE-Index bewertet die Verbreitung einer Programmiersprache anhand der Anzahl auf
sie spezialisierter Software-Ingenieure, Kurs-Angebote und Anbieter von Zusatzprodukten. Auf
http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html ist der monatlich aktualisierte Index zu finden, auf http://www.tiobe.com/index.php/content/paperinfo/tpci/tpci definition.htm findet
sich eine Erläuterung der Bewertungskriterien.
12
1.2 Überblick
• Es folgen zwei Durchläufe zum Aufbau der für die semantische Analyse nötigen Symboltabellen. Zunächst werden Symbole für die Klassendeklarationen angelegt, damit
die verfügbaren Typen bekannt sind, anschließend für die übrigen Deklarationen im
Programm.
• Dann erfolgt die Phase der semantischen Analyse, die z. B. die Gültigkeit von Bezeichnern und Typangaben prüft oder Typkompatibilität und Zugriffsschutz sicherstellt.
Ergebnis der Phasen bis hierher sind ein abstrakter Syntaxbaum7 (Abstract Syntax Tree,
kurz AST), welcher die Anweisungsstruktur des Quellprogramms widerspiegelt, sowie Symboltabellen, welche die Deklarationsstruktur des Quellprogramms widerspiegeln. Durch
Symbol-Annotationen im AST sind beide Strukturen zur Weiterverarbeitung miteinander
verknüpft. Es folgt die Erzeugung von Bytecode in drei Phasen:
• Zunächst wird aus den Informationen im AST und in den Symboltabellen Assemblercode erzeugt. In Abschnitt 1.3 findet sich auch ein Beispiel für JVM-Assemblercode.
• Der Assemblercode wird in Bytecode übersetzt.
• Der Bytecode durchläuft einen Verifier, der die Einhaltung aller strukturellen und
semantischen Bedingungen der JVM überprüft.
Zur Vorstellung der Programmiersprache MiniJavaOO, der Implementierung der einzelnen
Compiler-Phasen und des Entwicklungsprozesses ist die Arbeit in folgende Kapitel unterteilt:
In Kapitel 2 werden die Quellsprache MiniJavaOO und die Zielmaschine JVM des Compilers vorgestellt. Es werden auch die für den hier beschriebenen Compiler spezifischen
MiniJavaOO-Erweiterungen erläutert, die nicht Teil der im Anhang angegebenen Spezifikation sind. Weiterhin werden die Auswahl eines compilererzeugenden Frameworks wie
auch mögliche Hilfen zur Erstellung von JVM-Bytecode diskutiert und die Planung bzw.
Steuerung der Entwicklung mit Hilfe agiler Methoden dargelegt.
Kapitel 3 stellt die einzelnen Phasen des Compilers und ihre Implementierung vor und geht
näher auf einzelne, besonders interessante Aspekte anhand von Beispielen ein.
In Kapitel 4 werden die mit der testgetriebenen Compiler-Entwicklung gemachten Erfahrungen erläutert.
Kapitel 5 bietet schließlich eine Zusammenfassung der Arbeit und einen Ausblick auf mögliche Weiterentwicklungen bzw. Verbesserungen des Compilers.
In den Anhängen finden sich die Spezifikation der Syntax und der statischen Semantik von
MiniJavaOO, die als Anforderungskatalog für die Analysephase des Compilers dient, die
7
Der abstrakte Syntaxbaum dient der Weiterverarbeitung des Quellprogramms im Compiler und stellt
dessen logische Struktur dar. Er ist zu unterscheiden vom Syntax- oder Parsebaum, der die Ableitung
des Quellprogramms darstellt.
13
1 Einleitung
Lexer- und Parser-Grammatiken des Compilers in EBNF-Schreibweise (Extended BackusNaur Form 8 ) sowie die abstrakten Syntaxbäume und der Assemblercode zu den in diesem
und im nächsten Kapitel vorgestellten MiniJavaOO-Programmen.
1.3 Beispielprogramm
Um einen ersten Eindruck von MiniJavaOO, der abstrakten Syntax und dem Assemblercode
für die JVM zu vermitteln, zeigt Listing 1.1 ein MiniJavaOO-Programm zur Realisierung
des in Abbildung 1.3 gezeigten entarteten Binärbaums nach dem Composite-Pattern 9 .
Abbildung 1.3: Entarteter Binärbaum
Ein Baum wird durch miteinander verknüpfte Objekte der Klasse Baum (Zeilen 40 bis 59)
dargestellt. Der Wert eines Knotens ist eine Integer-Zahl zwischen 0 und 9. Ein Blatt ist
ein Knoten, dessen rechter und linker Teilbaum Objekte der Klasse Nil (Zeilen 62 bis 75)
sind. Die Nil-Knoten werden in der Abbildung als Rechtecke dargestellt. Ein Nil-Knoten
hat den Wert 10 und überlagert die Methoden setvalue und inorder der Superklasse
Baum.
In der globalen Funktion erzeugeBaum (Zeilen 5 bis 24) wird in einer while-Schleife
ein entarteter Baum mit fünf Knoten erzeugt und zurückgegeben. Die globale Prozedur
printBaum (Zeilen 27 bis 32) nimmt einen Baum als Parameter entgegen und gibt diesen
zunächst ganz und daraufhin nur teilweise in Inorder aus.
Die main-Prozedur (Zeilen 35 bis 38) ruft die Funktion erzeugeBaum auf und weist den
zurückgelieferten Baum der in Zeile 2 deklarierten globalen Variablen zu. Danach wird der
Baum durch Aufruf der Prozedur printBaum auf dem Bildschirm ausgegeben.
Listing 1.1: (Beispielprogramm: Binärbaum in MiniJavaOO)
1
2
// globale Variable
Baum baum;
3
8
9
Siehe http://de.wikipedia.org/wiki/Erweiterte Backus-Naur-Form
Siehe http://sourcemaking.com/design patterns/composite
14
1.3 Beispielprogramm
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// globale Funktion
Baum erzeugeBaum() {
int i = 0;
Baum b = new Baum();
Baum aktuell = b;
// erzeuge 5 Knoten
while i < 4 {
aktuell.setValue(i);
// rechts immer ein Nil-Knoten
aktuell.r = new Nil();
// links immer der naechste Knoten
aktuell.l = new Baum();
aktuell = aktuell.l;
i++;
}
// Abschluss des Baums mit Nil-Knoten
aktuell.setValue(i);
aktuell.l = new Nil();
aktuell.r = new Nil();
return b;
}
25
26
27
28
29
30
31
32
// globale Prozedur
void printBaum(Baum b) {
// gibt den gesamten Baum in Inorder aus
b.inorder();
// gibt nur die Knoten 4 und 3 aus
b.l.l.l.inorder();
}
33
34
35
36
37
38
// Hauptprogramm
main {
baum = erzeugeBaum();
printBaum(baum);
}
39
40
41
42
43
44
45
46
47
48
49
50
class Baum extends Object {
// rechter und linker Teilbaum
public Baum r, l;
// Wert des Knotens
protected int v;
// einziger, obligatorischer Konstruktor
Baum() {}
public void setValue(int v) {
if v <= 10 {
if v >= 0 {
this.v = v;
15
1 Einleitung
}
51
}
52
}
public void inorder() {
this.l.inorder();
print(this.v);
this.r.inorder();
}
53
54
55
56
57
58
59
}
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Unterklasse von Baum
class Nil extends Baum {
Nil() {
this.v = 10;
}
// ueberlagerte Methode
// Wert muss 10 bleiben!
public void setValue(int v) {
}
// ueberlagerte Methode
// Rekursion beenden, keine Ausgabe
// sonst Null-Pointer-Exception
public void inorder() {
}
}
In Abbildung 1.4 wird der abstrakte Syntaxbaum der Klasse Nil, so wie er vom Parser für
die Weiterverarbeitung des Quellprogramms im Compiler erzeugt wird, dargestellt. Unterhalb des class-Knotens finden sich, von links nach rechts betrachtet, zunächst die Angabe
des Klassennamens und der Superklasse, daraufhin der Konstruktor, die Instanzvariablen
und schließlich die Instanzmethoden. Der Teilbaum FIELDS hat keine weiteren Knoten,
da in der Klasse keine Instanzvariablen deklariert wurden.
class
Nil
extends
Nil
Baum
this
v
METHODS
BLOCK
=
QUALIFIEDNAME
FIELDS
METHOD
MODIFIER
10
public
void
setValue
METHOD
PARAMS
PARAM
int
BLOCK
MODIFIER
void
inorder
public
v
Abbildung 1.4: Binärbaum-Programm: Klasse Nil im AST
16
PARAMS
BLOCK
1.3 Beispielprogramm
Listing 1.2 zeigt den vom Compiler erzeugten JVM-Assemblercode. In den Zeilen 2 und 3
sind der Klassenname und die Superklasse definiert. Die Zeilen 5 bis 15 zeigen die beiden
Methoden setValue und inorder, deren Methodenrümpfe jeweils leer sind. In den Zeilen
17 bis 26 wird schließlich der Konstruktor der Klasse dargestellt. Der Konstruktor der
Superklasse Baum wird in den Zeilen 20 und 21 aufgerufen, daraufhin folgt die Übersetzung
von this.v = 10; in den Zeilen 22 bis 24.
Listing 1.2: (Beispielprogramm: Klasse Nil in Assemblercode)
1
2
3
.class public Nil
.super Baum
4
5
6
7
8
9
.method public setValue(I)V
.limit stack 0
.limit locals 2
return
.end method
10
11
12
13
14
15
.method public inorder()V
.limit stack 0
.limit locals 1
return
.end method
16
17
18
19
20
21
22
23
24
25
26
.method public <init>()V
.limit stack 2
.limit locals 1
aload_0
invokespecial Baum/<init>()V
aload_0
ldc 10
putfield Baum/v I
return
.end method
Die Darstellungen und Listings für die anderen Klassen finden sich in Anhang C.1. Im Laufe
der Arbeit werden Ausschnitte dieses Beispielprogramms aufgegriffen, um verschiedene
Aspekte des Übersetzungsprozesses zu erläutern.
17
2 Hintergrund
Bevor auf die Implementierung des MiniJavaOO-Compilers eingegangen wird, liefern die
Abschnitte dieses Kapitels das nötige Hintergrundwissen zur Programmiersprache MiniJavaOO bzw. zur Zielmaschine JVM.
Zusätzlich werden die Möglichkeiten zur Generierung von Bytecode für die JVM vorgestellt,
die Auswahl des zur Programmierung eingesetzten Compilergenerators ANTLR diskutiert
sowie die Planung und Steuerung des Entwicklungsprozesses mit agilen Methoden beschrieben.
2.1 Die Programmiersprache MiniJavaOO
Wie bereits in der Einleitung beschrieben, ist die Programmiersprache MiniJavaOO die
Quellsprache für zwei während des Praxisprojekts entwickelte Compiler. Daraus ergeben
sich folgende Rahmenbedingungen:
• Die Sprache ist kompatibel zu MiniJavaFunProc. Jedes MiniJavaFunProc-Programm
ist auch ein MiniJavaOO-Programm.
• Die Sprache läßt sich mit einer kontextfreien LL(1)-Grammatik darstellen. Dies ist
wegen des von Manuel Rieke eingesetzten Parsergenerators JavaCC 1 (siehe Abschnitt 2.3.2) nötig.
• Die Sprache enthält die wichtigsten Konstrukte zur Objektorientierung.
• Die Spracherweiterungen orientieren sich an der Java-Syntax.
Es folgt ein Überblick über die von MiniJavaFunProc geerbten Sprachelemente sowie die
spezifischen MiniJavaOO-Erweiterungen. Die formale Spezifikation der Sprache, bestehend
aus den Syntaxregeln in EBNF-Notation und den Bedingungen der statischen Semantik,
ist in Anhang A enthalten. Dort ist auch die Syntax in Form von Diagrammen zu finden.
1
Siehe https://javacc.dev.java.net/
18
2.1 Die Programmiersprache MiniJavaOO
2.1.1 Sprachelemente von MiniJavaFunProc
MiniJavaFunProc ist prozedural und in der Syntax an Java angelehnt. Die Sprache ist
darauf ausgelegt, die Grundlagen des Compilerbaus zu vermitteln. Der Sprachumfang ist
entsprechend klein, es gibt folgende Sprachelemente:
• Integer als Datentyp
• Globale Variablen und Konstanten
• Globale Funktionen und Prozeduren mit der Möglichkeit zum rekursiven Aufruf
• Lokale Variablen und Konstanten, deren Deklaration im Methodenrumpf vor den
Statements stattfinden muß.
• Ein globales Statement, mit dem die Ausführung des Programms beginnt.
• Die Kontrollstrukturen if und while mit einfachen logischen Ausdrücken
• Verschachtelte arithmetische Ausdrücke
• Strukturierung von Statements in Blöcken
• Ein print-Statement
In Listing 2.1 wird ein MiniJavaFunProc-Programm angegeben, welches Beispiele für alle
Elemente der Sprache enthält.
Die globale Prozedur (Zeilen 7 bis 20) führt die Berechnung zweier verschachtelter arithmetischer Ausdrücke durch und gibt das Ergebnis auf dem Bildschim aus. Die globale
Funktion in den Zeilen 23 bis 36 ist rekursiv aufgebaut und berechnet mit Hilfe einer lokalen Variablen und verschachtelten if-Kontrollstrukturen die Ackermann-Funktion2 für
die Aufruf-Parameter. Das globale Statement ist ein Block (Zeilen 39 bis 48), der in einer
while-Schleife die Ackermann-Funktion und anschließend die globale Prozedur aufruft.
Die Übersetzung des Programms in abstrakte Syntax und in Assemblercode für die JVM
finden sich in Anhang C.2.
Listing 2.1: (Beispielprogramm: Ackermann-Funktion in MiniJavaFunProc)
1
2
3
4
// globale Konstante
final int b = 5;
// globale Variable
int i;
5
6
7
8
9
// globale Prozedur
void berechneAusdruck() {
// lokale Variable
final int x = 9;
2
http://de.wikipedia.org/wiki/Ackermannfunktion
19
2 Hintergrund
//lokale Konstante
int ergebnis;
// Block als Statement der Prozedur
{
// verschachtelte arithmetische Ausdruecke
ergebnis = x * x + ((56 + 9) / 3 - 5);
// Bildschirmausgaben
print(ergebnis);
print(1 + 2 + 3 + 4 + 5 * 6);
}
10
11
12
13
14
15
16
17
18
19
20
}
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// globale, rekursive Funktion mit Parametern
func ackermann(int a, int b) {
// lokale Variable
int ret = 0;
// if als Statement der Funktion
if b == 0 {
ret = 1;
} else if a == 0 {
if b == 1 ret = 2;
else ret = b + 2;
} else {
ret = ackermann(a-1, ackermann(a,b-1));
}
return ret;
}
37
38
39
40
41
42
43
44
45
46
47
48
// Block als globales Statement zum Programmstart
{
i = 0;
while i + 2 < b {
// Funktionsaufruf als Ausdruck im print-Statement
print(ackermann(i, i + 2));
i = i + 1;
}
// Prozeduraufruf
berechneAusdruck();
}
2.1.2 Sprachelemente von MiniJavaOO
MiniJavaOO fügt der Sprache MiniJavaFunProc einen Teil der Objektorientierung von
Java hinzu und beseitigt syntaktische und semantische Beschränkungen. Folgende Sprachelemente sind neu:
20
2.1 Die Programmiersprache MiniJavaOO
• Objektreferenzen als weiterer primitiver Datentyp
• Klassen mit Einfachvererbung sowie Object als Basis-Klasse
• Instanzvariablen und Instanzmethoden
• Zugriffsschutz für Instanzvariablen und Instanzmethoden
• Polymorphie, Methodenüberlagerung und dynamisches Binden
• Die Möglichkeit, lokale Variablen an beliebiger Stelle innerhalb von Blöcken zu deklarieren
• Die Operatoren ++, -- und %
Das MiniJavaOO-Programm aus Kapitel 1 zeigt wesentliche Merkmale der objektorientierten Erweiterungen.
2.1.3 Ergänzungen zur Sprachspezifikation
Um einige erweiterte Möglichkeiten von ANTLR auszutesten, ergeben sich für den hier
vorgestellten Compiler Änderungen gegenüber der MiniJavaOO-Sprachspezifikation aus
Anhang A. Sie schränken die Syntax und die Semantik nicht ein, so daß ein der Spezifikation
entsprechendes Programm korrekt übersetzt wird. Ein Programm, welches sich die hier
angegebenen Erweiterungen zunutze macht, ist jedoch nicht mit Manuel Riekes Compiler
übersetzbar.
Initialisierung von Variablen
Anders als in Java werden lokale Variablen in MiniJavaOO automatisch mit 0 bzw. null
initialisiert. Um die Kompatibilität zu Java zu erhöhen, kann dieses Verhalten durch eine
Option beim Aufruf des Compilers abgeschaltet werden. Hieraus ergibt sich eine ergänzende
semantische Bedingung:
Statische Semantik 2.1 (Zugriff auf lokale Variablen)
(1) Vor dem Zugriff auf lokale Variablen müssen diese initialisiert werden (vgl. [11], S. 35).
Zudem können Variablen bei der Deklaration mit dem Wert einer anderen Variablen initialisiert werden. Es ergibt sich folgende Änderung für die Regel SingleVariable aus Syntax A.13:
Syntax 2.2 (Initialisierung von Variablen bei der Deklaration)
SingleVariable → SimpleName
( = ( Name
| NUMBER
| ConstructorCall ) )?
Zusätzlich werden die semantischen Bedingungen in A.14 erweitert:
21
2 Hintergrund
Statische Semantik 2.3 (Initialisierung von Variablen bei der Deklaration)
(1) Name muß eine Variable referenzieren.
Syntaxvereinfachungen
Durch die Möglichkeiten des ANTLR-Parsergenerators, die in Abschnitt 2.3.2 noch beschrieben werden, wird die Syntax von MiniJavaOO an zwei Stellen vereinfacht:
• Das Schlüsselwort func ist bei der Definition einer globalen Methode optional. Damit
entspricht die Syntax derjenigen von Instanzmethoden. Regel A.9 (10) fällt somit weg.
• Increment- und Decrement-Statements sind auch in Präfix-Schreibweise zulässig. Hierdurch wird Regel A.11 (3) hinfällig.
2.2 Die Java Virtual Machine
Nachdem im vorherigen Abschnitt ein Überblick über die Quellsprache des Compilers gegeben wurde, soll nun die Zielmaschine JVM näher betrachtet werden.
Die Java Virtual Machine ist eine abstrakte Stackmaschine, deren Struktur und Befehlsvorrat auf die Übersetzung statisch typisierter, objektorientierter Programmiersprachen,
insbesondere Java, ausgelegt sind. Die JVM ist keine konkret implementierte virtuelle Maschine, sondern die Spezifikation einer Schnittstelle zur Ausführung von in .class-Dateien
organisiertem Bytecode.
Relevant für ihre Implementierung ist die Java Virtual Machine Specification 2nd Edition
[4], die auch online eingesehen werden kann3 . In ihr werden die Syntax und die Semantik von
Bytecode definiert und das Verhalten der Maschine beim Laden und Linken von Klassen
erläutert. Neben der Definition der Schnittstelle werden außerdem Hinweise zur CompilerEntwicklung und zur Implementierung der Maschine gegeben.
2.2.1 Architektur
Die JVM hat viele Parallelen zu Java. Sie kennt Klassen und Interfaces und implementiert
die gleichen primitiven Typen und Zugriffsrechte. Klassen können statische bzw. dynamische Felder und Methoden sowie Konstruktoren enthalten. Weiter gibt es eine direkte
Unterstützung für Exceptions und nebenläufige Programmierung über Monitore.
.class-Dateien
Jede .class-Datei enthält eine Klasse oder ein Interface. Neben den Methodenimplementierungen besteht diese aus Verwaltungsinformationen in einem sogenannten Constant Pool.
3
Siehe http://java.sun.com/docs/books/jvms/
22
2.2 Die Java Virtual Machine
Der Constant Pool dient zur Laufzeit als eine Art Symboltabelle für die Klasse und beschreibt z. B. die Instanzvariablen und Zugriffsrechte. (Vgl. [5], S. 50 f. und [4], S. 93 ff.)
Das Laden einer Klasse ist ein umfangreicher Prozeß, der sich rekursiv über alle Superklassen erstreckt. So wird u. a. die globale Symboltabelle der JVM um die Informationen
des Constant Pool der Klassen ergänzt, werden Klassenvariablen initialisiert und Klasseninitialisierer ausgeführt. Auch werden durch einen Verifier die strukturellen und semantischen Bedingungen des Bytecodes sowie die Typsicherheit überprüft. Als sichere virtuelle
Maschine vertraut die JVM also nicht den Compilern. (Vgl. [4], S. 140 ff. und S. 155 ff.)
Datentypen und Befehlsvorrat
Wie in Java sind die primitiven Typen boolean, byte, short, int, long, char, float
und double sowie der reference-Typ implementiert. Zusätzlich existiert für den Methodenrücksprung der Typ returnAddress. Die Floatingpoint-Arithmetik entspricht dem
üblichen Standard IEEE 754. (Vgl. [4], S. 61 ff.)
Die JVM-Befehle operieren immer auf einem festgelegten Datentyp. Der Additionsbefehl
iadd beispielsweise erwartet zwei int-Werte auf dem Stack, während fadd float-Werte
erwartet. Ansonsten werden von der JVM Exceptions geworfen. Durch die Begrenzung auf
255 verschiedene Befehle werden nicht alle Datentypen gleichberechtigt unterstützt, so daß
gegebenenfalls Typkonversionen vorgenommen werden müssen.
Der Befehlsvorrat beinhaltet folgende Arten von Operationen (vgl. [4], S. 80 ff.):
• Transfer von Werten zwischen lokalen Variablen, Instanzvariablen und dem Stack
• Vielfältige arithmetische Operationen sowie direkte Stackmanipulationen
• Typkonversionen, Erzeugen und Casten von Arrays und Objekten
• Vielfältige bedingte und unbedingte Sprungbefehle und eine switch-ähnliche Operation
• Aufruf von Instanz- und Klassenmethoden und Konstruktoren sowie Rücksprungbefehle
• Befehle zum Exception-Handling und zur Synchronisation von Threads
Datenbereiche zur Laufzeit
Jeder Thread hat einen eigenen Program Counter (Befehlsregister) sowie einen eigenen
Stack. Auf dem Stack wird für jede laufende Methode ein Frame abgelegt.
Ein Frame enthält die Methodenargumente, lokale Variablen, Informationen für den Rücksprung sowie einen Operand Stack zur Berechnung von Ausdrücken. Die Größe des Operand
Stack wird pro Methode fest angegeben und muß daher vom Compiler berechnet werden.
23
2 Hintergrund
Der Heap nimmt alle instanzspezifischen Informationen von Objekten und Arrays, wie z. B.
Werte von Variablen, auf und ist allen Threads gemein.
In der Methode Area und dem Runtime Constant Pool, die auch von allen Threads geteilt
werden, sind alle statischen Informationen zu Klassen und Interfaces enthalten. Dies sind
u. a. die Namen von Feldern und Methoden, Zugriffsrechte und die Implementierungen der
Methoden.
(Vgl. [4], S. 67 ff.)
2.2.2 Erzeugen von Bytecode
Es gibt drei verschiedene Vorgehensweisen, um mit einem Compiler Bytecode für die JVM
zu erzeugen. Man kann, entsprechend der Spezifikation in [4], direkt binären Bytecode
erzeugen. Dies ist jedoch sehr aufwendig und fehleranfällig. Die zweite Möglichkeit ist die
Nutzung von Bibliotheken, die von den Details des Bytecodes abstrahieren. Der dritte,
hier eingeschlagene Weg nutzt eine Assemblersprache, um nahe an der Spezifikation zu
entwickeln, ohne aber die genaue binäre Darstellung der .class-Dateien kennen zu müssen.
Bibliotheken für Java Bekannte Java-Biblioteheken sind die Byte Code Engineering Library 4 , kurz BCEL, die Teil des Apache Jacarta-Projekts ist, sowie die ASM Library 5
der ObjectWeb Middleware. Beide Bibliotheken stellen Bytecode als Baumstruktur
ähnlich dem DOM6 (Document Object Model ) dar und erlauben so die Erzeugung,
Manipulation, Analyse und Verifikation von .class-Dateien.
Noch ein Abstraktionsniveau höher ist die Bibliothek Cojen 7 anzusiedeln. Mittels
einer Builder-Klasse werden von der JVM auszuführende Aktionen spezifiziert, aus
denen Cojen optimierte Befehlsketten generiert. Cojen wurde von Wolfram Fischer im
Compilerbau-Praktikum genutzt, um MiniJavaFunProc auf die JVM zu übersetzen.
Der Jasmin-Assembler Von Sun wird kein Assembler für JVM-Bytecode zur Verfügung
gestellt. Es gibt einen Disassembler, an dessen Syntax sich der für den MiniJavaOOCompiler genutzte Jasmin-Assembler 8 orientiert. Ursprünglich als Beigabe des Buchs
Java Virtual Machine [5] entwickelt, wird er seitdem stetig an Veränderungen der
JVM angepaßt. Die Assembleranweisungen abstrahieren von der binären Darstellung der Datenstrukturen zur Definition der Klassen, Felder und Methoden in .classDateien, während die eigentlichen Maschinenbefehle innerhalb der Methodenrümpfe
entsprechend den in der Spezifikation angegebenen Mnemonics 9 notiert werden.
4
Siehe http://jakarta.apache.org/bcel/index.html
Siehe http://asm.ow2.org/
6
Baumdarstellung von HMTL-Seiten und JavaScript-Code innerhalb eines Webbrowsers.
7
Siehe http://cojen.sourceforge.net/
8
Siehe http://jasmin.sourceforge.net/
9
Ein Mnemonic ist die textuelle Notation einer binären Maschineninstruktion.
5
24
2.3 Auswahl eines Compilergenerators
2.3 Auswahl eines Compilergenerators
Zur Entwicklung des MiniJavaOO-Compilers soll ein Compilergenerator eingesetzt werden, der neben der Lexer- und Parsergeneration weitere Aspekte der Entwicklung eines
Mehrphasen-Compilers unterstützt. Im Vorfeld des Praxisprojekts wurde ANTLR unter
verschiedenen Alternativen ausgewählt.
2.3.1 Anforderungen und Alternativen
Basis für die Suche nach einem geeigneten Framework waren die folgenden Anforderungen:
• Der Compiler soll mit Java entwickelt werden. Dies ermöglicht es, Teile des im Praktikum entwickelten Compilers wiederzuverwenden.
• Der Parsergenerator soll auf der Grundlage einer LL(k)-Grammatik in EBNF-Notation einen Top-Down-Parser erzeugen. Somit kann auf den theoretischen Grundlagen
der Vorlesung Compilerbau und der im Praktikum entwickelten LL(1)-Grammatik
aufgebaut werden.
• Gute und ausführliche Dokumention sowie Support durch eine lebendige Anwendergemeinschaft sollen eine zügige und effiziente Entwicklung unterstützen. Das Framework
soll ausgereift sein und aktiv weiterentwickelt werden. Als Anhaltspunkte dienen die
Versionsgeschichte und die Mailingliste des Projekts.
• Unterstützung für die Erzeugung und Traversierung eines abstrakten Syntaxbaums
und ein Plugin für Eclipse 10 wären wünschenswert.
Neben ANTLR wurden folgende Compiler-Frameworks näher betrachtet:
JavaCC Bekanntester Parsergenerator in der Java-Welt ist JavaCC, welcher auf LL(1)Grammatiken beruht. Bei Bedarf können einzelne Regeln auf LL(k) mit festem k
umgestellt werden. Das Erzeugen eines AST wird durch JJTree unterstützt, zur Traversierung muß ein Java-Interface nach dem Visitor-Pattern 11 implementiert werden.
Die Dokumentation beschränkt sich auf eine Referenz für die Grammatik-Notation
und die Java-API sowie einige Tutorials. Das Nachrichtenaufkommen auf der Mailingliste ist sehr hoch, seit mindestens 2004 erscheinen regelmäßig neue Versionen. Der
Parsergenerator JavaCC wird von Manuel Rieke zur Entwicklung des MiniJavaOOCompilers für M32-Assemblercode eingesetzt.
10
11
Siehe http://www.eclipse.org/
Siehe http://sourcemaking.com/design patterns/visitor
25
2 Hintergrund
Coco/R Coco/R 12 ist ein an der Johannes Kepler Universität Linz entwickelter LL(1)Parsergenerator für Java, C# wie auch viele weitere Sprachen. Auch hier können
einzelne Regeln auf LL(k) mit festem k umgestellt werden. Zusätzlich können Alternativen über semantische Prädikate entschieden werden. Die Dokumentation ist sehr
umfangreich, u. a. wird Coco/R in [12] ausführlich behandelt. Es stehen Plugins für
Eclipse, NetBeans 13 und weitere Entwicklungsumgebungen zur Verfügung. Coco/R
wird seit mindestens 2003 entwickelt, es erscheinen regelmäßig neue Versionen. Eine
Mailingliste für Anwender existiert nicht.
Grammatica Grammatica 14 ist ein LL(k)-Parsergenerator für beliebige k. Von Interesse
ist die strikte Trennung von Grammatik und Java-Sourcecode: Der Sourcecode für
die Regel-Aktionen wird in einer von der Parserklasse abgeleiteten Klasse notiert.
Es existiert nahezu keine Dokumentation. Die Mailingsliste wird wenig genutzt. Die
erste stabile Version stammt aus dem Jahr 2003, zwischen August 2003 und März
2009 sind keine neuen Versionen erschienen.
2.3.2 ANTLR
ANTLR wird seit 1989 von Terence Parr an der University of San Francisco entwickelt und
unterstützt neben Java viele weitere Sprachen. Die Software ist ausgereift und sehr gut
dokumentiert. Neben einem Wiki und einer API-Referenz ist vor allem das Handbuch [8]
hervorzuheben. Das Projekt hat die im Vergleich zu den anderen Parsergeneratoren aktivste
Mailingliste.
Besondere Features sind außerdem:
LL(*)-Parsergenerator Bei LL(k)-Parsergeneratoren muß in der Grammatik global oder
für jede Regel ein fester Wert für k angegeben werden, auf dessen Grundlage die LookAhead-Entscheidung für die Regelalternativen der kontextfreien Grammatik getroffen wird. LL(∗) ermöglicht auch unendliches k. Hierzu erzeugt ANTLR Parser, deren Look-Ahead-Entscheidung auf zyklischen deterministischen endlichen Automaten
(DEA) basiert. Beispiel 2.1 verdeutlicht den Sachverhalt: Die Regel für Prozedurdefinitionen soll zwischen konkreten und abstrakten Prozeduren unterscheiden (vgl. [8],
S. 255).
Beispiel 2.1 (Look-Ahead-Entscheidung mit unendlichem k)
procedure → void IDENT ( params? ) ;
| void IDENT ( params? ) block
params → int IDENT
12
( , int IDENT )∗
Siehe http://www.ssw.uni-linz.ac.at/Research/Projects/Coco/
Siehe http://www.netbeans.org/
14
Siehe http://grammatica.percederberg.net/index.html
13
26
2.3 Auswahl eines Compilergenerators
Durch den Kleene-Stern in der Regel params hat die Regel procedure einen unendlichen Look-Ahead. In der Praxis haben Methoden jedoch eine begrenzte Anzahl Parameter, so daß der von ANTLR generierte zyklische DEA (Abbildung 2.1) effizient
zwischen den beiden Regelalternativen von procedure entscheiden kann.
s10
’int’
s9
IDENT
’,’
’,’
s11
’}’
’;’
’int’
s0
’void’
s1
IDENT
s2
’{’
s4
IDENT
’}’
s6
s3
’}’
s5
s7=>1
’{’
s8=>2
Abbildung 2.1: DEA zur Regelentscheidung von unendlichem k
In [8], Kapitel 11 und [7] wird das LL(∗)-Parsen ausführlich beschrieben. Es erlaubt
die Formulierung weitaus natürlicherer Grammatiken, als dies mit LL(k)-Parsergeneratoren möglich ist. Trotz der Möglichkeiten bleiben auch viele Einschränkungen
des Top-Down-Parsens erhalten, so sind z. B. linksrekursive Regeln nicht umsetzbar (vgl. [8], S. 264 ff.). Effizient werden die von ANTLR generierten Parser durch
Linear Approximate Lookahead (vgl. [7], Folie 7), wodurch die Komplexität von LookAhead-Entscheidungen für LL(k) mit k > 1 von exponentiellem auf linearen Aufwand
reduziert wird.
Für Sprachkonstrukte, die auch mit LL(∗) nicht umsetzbar sind, bietet ANTLR semantische und syntaktische Prädikate. Laut Aussage von Terence Parr kann ANTLR
auf diese Weise mit nahezu jeder kontextfreien Grammatik und sogar mit vielen kontextsensitiven Sprachkonstrukten umgehen (vgl. S. [8], 283 ff.).
Unterstützung für abstrakte Syntaxbäume ANTLR-Grammatiken bieten direkte Unterstützung für die Übersetzung eines Quellprogramms in eine abstrakte Syntax und die
Ausgabe eines entsprechenden, mit den Token des Lexers annotierten abstrakten Syntaxbaums. Der AST kann dann mit einem Tree-Parser traversiert werden. Dieser kann
von ANTLR auf der Basis einer Parser-Grammatik für die abstrakte Syntax erzeugt
werden.
Template-Engine für den Code-Emitter Weiterhin unterstützen ANTLR-Grammatiken
die Ausgabe von Text über die Template-Engine StringTemplate 15 . Dies vereinfacht
die Ausgabe vom Zielcode, da StringTemplate entsprechend der Struktur einer kontextfreien Grammatik einzelne Textfragmente rekursiv und ineinander verschachtelt
ausgeben kann.
15
Siehe http://www.stringtemplate.org/
27
2 Hintergrund
Entwicklungsumgebungen Mit ANTLRWorks 16 gibt es eine spezielle IDE für die Grammatik-Entwicklung. Besonders hilfreich sind der integrierte Debugger mit Einzelschrittmodus und verschiedene Visualisierungsmöglichkeiten. Es gibt Darstellungen
für die EBNF-Regeln als Syntaxdiagramm und die DEAs der Look-Ahead-Entscheidungen. Außerdem können sowohl Parsebäume als auch abstrakte Syntaxbäume visualisiert werden. Abbildung 2.2 zeigt den Debugger in ANTLRWorks. Im EditorBereich ist ein Breakpoint gesetzt, an dem sich der Debugger-Lauf gerade befindet.
Darunter sind das Quellprogramm, der aktuell aufgebaute Parsebaum sowie eine Auflistung der durchlaufenen Regeln zu sehen.
Abbildung 2.2: Debugging mit ANTLRWorks
Zur Integration in Eclipse kann zwischen drei verschiedenen Plugins gewählt werden,
zur Entwicklung des MiniJavaOO-Compilers wurde ANTLR IDE 17 eingesetzt.
Abbildung 2.3 zeigt die ANTLR IDE innerhalb von Eclipse. Im Editor ist eine Grammatik geöffnet, deren Regeln in der Outline aufgelistet sind. Unter dem Editor ist die
16
17
Siehe http://www.antlr.org/works/index.html
Siehe http://antlrv3ide.sourceforge.net/
28
2.3 Auswahl eines Compilergenerators
ANTLR-Console zu sehen.
Abbildung 2.3: ANTLR IDE in Eclipse
Tests mit gUnit In ANTLR integriert ist das Testframework gUnit 18 , welches es erlaubt,
Parser- und Baumparser-Grammatiken zu testen. Die Tests werden als EingabeAusgabe-Paare formuliert, die gUnit mittels eines Parser-Laufs verifiziert. gUnit bietet einen Interpreter, der die Testfälle mit Hilfe der Java-Reflection-API überprüft,
wie auch einen Generator für jUnit 19 .
18
19
Siehe http://www.antlr.org/wiki/display/ANTLR3/gUnit+-+Grammar+Unit+Testing
Siehe http://www.junit.org/
29
2 Hintergrund
2.4 Agiles Projektmanagement
Zur Planung und Steuerung der Entwicklung des Compilers werden Praktiken und Artefakte der agilen Methode Scrum (siehe [2], S.149 ff.) genutzt. Da Scrum auf ein Entwicklerteam und unterschiedliche Rollen ausgelegt ist, ist eine ganzheitliche Umsetzung
der Methode während des Praxisprojekts nicht möglich. Wie im folgenden erläutert wird,
können dennoch Teile der Methode erfolgreich durch einen einzelnen Entwickler angewandt
werden. Zur Planung und Steuerung des Prozesses kommt die webbasierte Software Agilo
for Scrum 20 zum Einsatz.
Der Entwicklungsprozeß ist in drei Phasen, jeweils durch einen Meilenstein repräsentiert,
aufgeteilt. Zum Erreichen der Meilensteine wird in Sprints zu je einer Woche entwickelt.
Ein Sprint ist eine abgeschlossene Iteration, in der zu Beginn des Sprints geplante Features
umgesetzt werden. Ein Feature ist fertiggestellt, wenn es programmiert und ausführlich
getestet wurde. Montags wird zunächst die Sprintplanung durchgeführt, in der, mit Blick
auf das bisher Erreichte, die Ziele für die laufende Woche festgelegt werden. Während des
Sprints kann der Fortschritt stetig durch einen Soll-Ist-Vergleich überwacht werden.
Meilensteine
Zu Beginn des Praxisprojekts werden folgende Meilensteine festgelegt:
1. Fertigstellung des MiniJavaOO-Parsers: Der Parser erzeugt einen abstrakten Syntaxbaum. Die MiniJavaOO-Erweiterung kann deaktiviert werden, um einen
reinen MiniJavaFunProc-Parser zu erhalten. Zur Fertigstellung werden zwei Sprints
benötigt.
2. Fertigstellung des MiniJavaFunProc-Compilers: Alle Compiler-Phasen bis zur
Erstellung von JVM-Bytecode sind für die Sprache MiniJavaFunProc fertiggestellt.
So wird die Gesamtarchitektur des Compilers zunächst an einer kleineren Menge
Sprachelemente erprobt, deren praktische Umsetzung bereits aus dem Praktikum
bekannt ist. Es entsteht eine Art vertikaler Prototyp. Der Meilenstein wird nach
weiteren drei Sprints erreicht.
3. Fertigstellung des MiniJavaOO-Compilers: Alle Compiler-Phasen bis zur Erstellung von JVM-Bytecode sind für die Spracherweiterung MiniJavaOO fertiggestellt. Das Gesamtbild ist durch den vorherigen Meilenstein bekannt, die einzelnen
Compiler-Phasen werden nun um die MiniJavaOO-Sprachelemente erweitert. Nach
weiteren fünf Sprints wird der letzte Meilenstein erreicht und der Compiler somit
fertiggestellt.
20
Siehe http://www.agile42.com/cms/pages/agilo/
30
2.4 Agiles Projektmanagement
Sprintplanung
Die im Sprint zu realisierenden Features werden als User Stories formuliert, in Story Points
bewertet und im sogenannten Sprint Backlog festgehalten. Ein Story Point entspricht etwa einem halben Entwicklungstag. Jede User Story wird in einzelne Tasks aufgegliedert,
die dann jeweils in Stunden geschätzt werden. Zeigt sich nach der Aufgliederung, daß einzelne User Stories über- oder unterschätzt wurden, werden die Story Points entsprechend
angepaßt. Die Planung ist beendet, wenn User Stories und die ihnen zugeordneten Tasks
mit einem Gesamtaufwand von etwa zehn Story Points bzw. 25 bis 30 Stunden im Sprint
Backlog liegen. Der Planungsprozeß dauert etwa einen halben Tag.
Abbildung 2.4 zeigt das Sprint Backlog des neunten Sprints. Es sind drei User Stories mit
den jeweiligen untergeordneten Tasks zu sehen. Das Backlog zeigt den Zustand am Ende des
Sprints, daher werden die User Stories durchgestrichen angezeigt und so als fertiggestellt
markiert. Die verbleibende Zeit wird jeweils mit null Stunden angegeben.
Abbildung 2.4: Sprint Backlog in Agilo for Scrum
Sprintdurchführung
Während der Woche wird der Entwicklungsfortschritt stetig im Sprint Backlog festgehalten.
Hierzu werden die Schätzungen des zur Fertigstellung der Tasks verbliebenen Aufwands regelmäßig nach oben oder unter korrigiert. Im Burndown Chart werden die Veränderungen
der Schätzungen im Verhältnis zum Zeitverlauf visualisiert. Eine Trendlinie gibt über den
vermutlichen Erfolg bzw. Mißerfolg des Sprints Auskunft. In Abbildung 2.5 ist das Burndown Chart nach Ende des neunten Sprints abgebildet.
31
2 Hintergrund
Abbildung 2.5: Sprint Burndown in Agilo for Scrum
Integration der Versionsverwaltung
Weiterhin hilfreich ist die Integration der Versionsverwaltung Subversion 21 in Agilo. In
den Commits kann Bezug auf die Tasks des Sprints genommen werden, so daß Sprints
und zugehörige Versionen der entwickelten Software in der Weboberfläche miteinander
verknüpft sind. In der Timeline-Ansicht (Abbildung 2.6) werden Ereignisse im Repository
und Veränderungen in der Projektplanung im zeitlichen Bezug zueinander angezeigt.
Abbildung 2.6: Timeline in Agilo for Scrum
21
Siehe http://subversion.tigris.org/
32
2.4 Agiles Projektmanagement
Im Beispiel wurde Task 79 am 30. Mai 2009 um 10:58 Uhr durch einen Commit ins
Subversion-Repository abgeschlossen. Die Quellcodeveränderungen im entsprechenden Changeset 41 können direkt über die Weboberfläche von Agilo for Scrum eingesehen werden.
Durch die beschriebenen agilen Praktiken kann der Compiler flexibel und dennoch planvoll entwickelt werden. Die Meilensteine bieten eine Grobplanung für das Gesamtprojekt,
während jede Woche die Erfahrungen der Vorwoche in die aktuelle Sprintplanung einfließen können. Ein Projektmanagement auf der Grundlage der Netzplantechnik oder von
Gantt-Charts ist hier aufgrund der geringen Erfahrung mit der Softwareentwicklung im
allgemeinen wie auch mit der Compiler-Entwicklung im speziellen unrealistisch.
33
3 Der Compiler
Nachdem im vorherigen Kapitel die Quell- und Zielsprache des Compilers sowie die zur
Entwicklung eingesetzten Hilfsmittel vorgestellt wurden, folgt nun eine Beschreibung der
Architektur und Implementierung des Compilers.
Zunächst werden der Aufbau und die Aufgaben der einzelnen Phasen erläutert, dann wird
jede Phase in einem eigenen Abschnitt detailiert betrachtet.
3.1 Aufbau und Phasen
Der MiniJavaOO-Compiler ist ein Mehrphasen-Compiler, aufgeteilt in ein Frontend und
ein Backend. Das Frontend dient der Analyse des Quellprogramms und durchläuft drei
Phasen (Abbildung 3.1).
!
"!
#$
Abbildung 3.1: Analyse-Phasen des Compilers
Parser und Lexer prüfen die Syntax des Quellprogramms und transformieren es in abstrakte Syntax. Diese gibt die logische Struktur des Programms in verdichteter Form
34
3.1 Aufbau und Phasen
in einer Baumstruktur, dem AST, wieder. Der AST ist mit den Token aus der Lexerphase annotiert und kann von den nachgelagerten Compiler-Phasen effizient traversiert werden. (Abschnitt 3.2)
Aufbau der Symboltabellen und der Klassentabelle Diese Phase wird mittels zweier
AST-Parser realisiert. In einem ersten Durchlauf wird eine Klassentabelle aufgebaut,
und die Knoten der Klassendefinitionen werden mit den Klassensymbolen annotiert.
Im zweiten Durchlauf entstehen die Symboltabellen für Klassen, Methoden und Blöcke, die entsprechend der Struktur des Programms miteinander verknüpft sind. Auch
in dieser Phase werden die Knoten der Defintionen mit den jeweiligen Symbolen annotiert. (Abschnitt 3.3)
Semantische Analyse Die semantische Analyse überprüft die Bedingungen der statischen
Semantik (siehe Anhang A). Es werden im Programm vorkommende einfache und
qualifizierte Namen für Klassen, Methoden und Variablen mit Hilfe der Symboltabellen aufgelöst und die entsprechenden Knoten mit den Symbolen annotiert. Weiterhin
finden Typ-, Scope- und Zugriffsprüfungen statt. Auch diese Phase wird mit einem
AST-Parser realisiert. (Abschnitt 3.4)
Der annotierte AST dient als Eingabe für das Backend des Compilers. Das Backend erzeugt
den JVM-Bytecode und durchläuft drei Phasen (Abbildung 3.2).
Abbildung 3.2: Synthese-Phasen des Compilers
Code-Emitter Ein weiterer AST-Parser erzeugt aus den Informationen des annotierten
AST und mit Hilfe der StringTemplate-Engine Quellcode für den Jasmin-Assembler.
(Abschnitt 3.5)
35
3 Der Compiler
Jasmin-Assembler Daraufhin wird dieser in Bytecode assembliert und in je einer .classDatei pro Klasse gespeichert. (Abschnitt 3.6)
Bytecode-Verifier Der generierte Bytecode wird schließlich einer Analyse unterzogen, um
sicherzustellen, daß alle strukturellen und semantischen Bedingungen der JVM erfüllt
sind. (Abschnitt 3.6)
Neben dem compilierten Bytecode speichert der Compiler auch den Assemblercode in
.jasmin-Dateien ab und gibt eine Visualisierung des AST im Graphviz -Format1 in der
Datei AST.dot aus.
In Abbildung 3.3 wird die zentrale Klasse CompilerPhases zur Steuerung des Compilers
dargestellt. Sie dient als Schnittstelle sowohl für das Kommandozeilen-Interface in der
Klasse Main als auch für die Klassen zur Durchführung automatisierter Tests während der
Entwicklung des Compilers (Details hierzu in Kapitel 4).
CompilerPhases
+lexerPrepare()
+parserPhase()
+symbolTablePhase()
+semanticCheckPhase()
+codeEmitterPhase()
+jasminAssemblerPhase()
+verifierPhase()
+readSource()
+readSource(String:file)
+saveJasminCode(outdir:String)
+saveByteCode(outdir:String)
+saveAstDotFile(outdir:String)
+getAst(): AstNode
+getSymbolTable(): ClassTable
+getJasminCode(): HashTable<String, String>
+setLvarAutoInit(autoInit:boolean)
+setFunProcMode(funProcMode:boolean)
+setDebugMode(debugMode:boolean)
Main
+main(args:String[])
CompilerTestBase
CompilerTestTool
CompilerTestTool2
Abbildung 3.3: Klassen zur Steuerung des Compilers
Zur Steuerung des Compilers stellt CompilerPhases folgende Funktionalität zur Verfügung:
• Einlesen des Quellprogramms und Abspeichern der Ausgaben des Compilers
• Aufruf der einzelnen Compiler-Phasen
• Zugriff auf die Zwischenergebnisse: AST, Symboltabellen und Jasmin-Assemblercode
• Einschalten des Debug-Modus, der erweiterte Fehlermeldungen ermöglicht.
• Umschalten zwischen MiniJavaFunProc- und MiniJavaOO-Modus
• Ein- und Ausschalten der automatischen Initialisierung lokaler Variablen
1
Siehe http://www.graphviz.org/
36
3.2 Parser und abstrakte Syntax
3.2 Parser und abstrakte Syntax
Parser und Lexer entstehen aus der kombinierten Grammatik MiniJavaOO.g. Wie Abbildung 3.4 zeigt, werden die beiden Klassen MiniJavaOOLexer und MiniJavaOOParser
sowie die Datei MiniJavaOO.token von ANTLR erzeugt. Letztere enthält die Definition der
Lexemklassen und wird neben dem Lexer und dem Parser auch von allen nachgelagerten
AST-Parsern genutzt.
org.antlr.runtime.Parser
org.antlr.runtime.Lexer
ParserBase
MiniJavaOOLexer
MiniJavaOOParser
Erzeugt
Erzeugt
Erzeugt
MiniJavaOO.g
MiniJavaOO.token
Abbildung 3.4: Parser- und Lexer-Klassen
Zusätzlich zu den Produktionen in EBNF-Notation enthalten ANTLR-Grammatiken auch
eingebetteten Javacode. Dieser sollte aus Gründen der Übersicht2 kurz gehalten sein. Hierzu
sind, soweit möglich, Regelaktionen und anderer Code in die Methoden einer Superklasse
ausgelagert. Bei einer kombinierten Grammatik ist dies derzeit nur für den Parser möglich:
die Klasse MiniJavaOOParser beerbt die Klasse ParserBase.
Die EBNF-Produktionen des Lexers werden in Anhang B.1, die des Parsers in Anhang B.2
angegeben. In den folgenden Abschnitten wird näher auf einige Aspekte des MiniJavaOOParsers eingegangen und die Transformation von MiniJavaOO-Quellcode in einen AST
erläutert.
3.2.1 Spracherweiterung über semantische Prädikate
Die im Vergleich zu MiniJavaFunProc neuen Sprachfeatures von MiniJavaOO können im
Parser über Gated Semantic Predicates (vgl. [8], S. 317 ff.) ein- und ausgeschaltet werden.
Beim Aufruf des Compilers kann so über eine Option MiniJavaFunProc oder MiniJavaOO
als Quellsprache ausgewählt werden. Die Erweiterungen sind in drei Gruppen gegliedert,
die jeweils durch ein Prädikat repräsentiert werden:
• declEverywhere erlaubt es, lokale Variablen beliebig innerhalb von Blöcken zu deklarieren,
2
Zudem bietet Eclipse innerhalb einer ANTLR-Grammatik nicht die übliche Hilfestellung bei der JavaProgrammierung.
37
3 Der Compiler
• enableOO schaltet die objektorientierten Features ein und
• enableExtensions steuert Erweiterungen wie die neuen Operatoren oder die Möglichkeit zur Definition einer main-Methode.
Die EBNF-Regel in Syntax 3.1 zeigt den Einsatz zweier semantischer Prädikate. Eine Funktion beginnt in MiniJavaFunProc mit dem Schlüsselwort func. Ist das semantische Prädikat enableExtensions eingeschaltet, ist func optional, und es muß der Typ der Funktion angegeben werden. Dieser ist entweder int oder, falls auch das semantische Prädikat
enableOO eingeschaltet ist, ein Klassenname.
Beispiel 3.1 (Parser-Regel mit semantischen Prädikaten)
funcStart → FUNC
| {enableExtensions}?=>
FUNC? ( INT | {enableOO}?=> CLASSIDENT )
In Abbildung 3.5 wird der von ANTLR zur Entscheidung der Regelalternativen erzeugte
DEA dargestellt.
FUNC
CLASSIDENT&&{enableExtensions}?
s5=>2
IDENT
s4=>1
s1
INT&&{enableExtensions}?
INT&&{enableExtensions}?
s0
s2=>2
CLASSIDENT&&{(enableExtensions&&enableOO)}?
s3=>2
Abbildung 3.5: DEA zur Regelentscheidung mit semantischen Prädikaten
3.2.2 Grenzen des LL(∗)-Parsergenerators
Wie in Abschnitt 2.3.2 beschrieben, können ANTLR-Grammatiken aufgrund des LL(∗)Parsergenerators sehr natürlich formuliert werden. Hierbei kann jedoch das Problem auftreten, daß der generierte Code zu groß wird. Die DEAs zur Regelentscheidung können
so komplex werden, daß einzelne Java-Methoden die Größenbegrenzung von 64 kB überschreiten, insbesondere wenn eine Grammatik mit Debugging-Support für ANTLRWorks
generiert wird.
Während der Entwicklung des MiniJavaOO-Parsers wurde diese Grenze mehrfach erreicht,
so daß die Grammatik überarbeitet werden mußte. Das folgende Beispiel verdeutlicht die
Problematik:
38
3.2 Parser und abstrakte Syntax
Im ersten Ansatz wurde für jede Regel der MiniJavaFunProc-Grammatik aus dem Compilerbau-Praktikum eine gleichlautende, aber mit dem Suffix OO“ versehene zweite Regel mit
”
den spezifischen Erweiterungen geschrieben und von der ursprünglichen auf diese verwiesen.
Da Ausdrücke Teil vieler Regeln sind, haben sich besonders die Regeln für atomare Ausdrücke (siehe Syntax 3.1) auf die Komplexität der DEAs ausgewirkt. Ein auf der Grundlage
dieser Version der Grammatik erzeugter Parser mit Debugging-Support beinhaltet Methoden, die die Größenbegrenzung von 64 kB nicht einhalten.
Syntax 3.1 (Parser-Regel für atomare Ausdrücke: Erster Ansatz)
AtomExpr →
|
|
|
|
|
|
IDENT
NUMBER
RLB expr RRB
FunProcCall
{enableExtensions}?=> IDENT ( INC | DEC )
{enableExtensions}?=> ( INC | DEC ) IDENT
{enableOO}?=> AtomExprOO
AtomExprOO →
|
|
|
MemberIdent
MethodCall
{enableExtensions}?=> MemberIdent ( INC | DEC )
{enableExtensions}?=> ( INC | DEC ) MemberIdent
In der überarbeiteten Version (siehe Syntax 3.2) sind die beiden Regeln zusammengeführt.
Außerdem faßt die Regel Identifier jetzt den einfachen Namen (vorher IDENT) mit dem
qualifizierten Namen (vorher MemberIdent) zusammen, und in der ersten Alternative sind
alle Möglichkeiten vereint, die mit einem Namen beginnen.
Syntax 3.2 (Parser-Regel für atomare Ausdrücke: Optimierte Fassung)
AtomExpr →
|
|
|
Identifier ( FunProcCall | {enableExtensions}?=> PostfixIncDec
{enableExtensions}?=> PraefixIncDec Identifier
NUMBER
RLB Expr RRB
)?
Zum Vergleich der generierten DEAs:
• Der ursprüngliche DEA für AtomExpr ist zyklisch und hat 27 Zustände.
• Der ursprüngliche DEA für AtomExprOO ist zyklisch und hat 21 Zustände.
• Der DEA der optimierten Version ist azyklisch und hat sechs Zustände.
39
3 Der Compiler
Da die Regel für Statement ähnlich komplex ist und fast jede andere Regel entweder Statements oder Ausdrücke beinhaltet, ist fast jeder DEA der ersten Version der Grammatik
azyklisch und hat mehr als 20 Zustände.
Nachdem alle Regelpaare, wie im Beispiel gezeigt, zusammengelegt und weitere Optimierungen in den Regeln Statement und Identifier durchgeführt wurden, gibt es in der gesamten Grammatik keinen zyklischen DEA mehr, und nur drei DEAs haben mehr als zehn
Zustände.
Man sollte also trotz der Möglichkeiten von ANTLR stets um einen kleinen Look-Ahead
bemüht sein. Die Mächtigkeit von LL(∗) kann dann ausgeschöpft werden, wenn ein Sprachelement nicht oder nur schwer anders auszudrücken ist. Für MiniJavaOO ist dies an keiner
Stelle nötig, da die Sprache so definiert ist, daß sie auch mit einem herkömmlichen LL(k)Parser umgesetzt werden kann.
3.2.3 Abstrakter Syntaxbaum
Die abstrakte Syntax des MiniJavaOO-Compilers orientiert sich an der Struktur von Java
und der JVM. Wie in Anhang B.3 dargestellt, ist sie wesentlich kompakter als die Parsersyntax (Anhang B.2). Dies ist möglich, da überflüssige syntaktische Elemente wegfallen
und viele syntaktische Bedingungen, die der Parser bereits geprüft hat, im AST vereinfacht
dargestellt werden können. Beispielsweise wird im Parser zwischen einem Funktionsblock,
der als letzte Anweisung ein return enthalten muß, und einem Prozedurblock unterschieden. Im AST sind beide einfach Blöcke, und return ist ein Teil der Statements. Der Block
der Funktion enthält, sichergestellt durch den Parser, die return-Anweisung.
Im wesentlichen finden im Parser folgende Transformationen statt:
• Prozeduren und Funktionen werden als Methodenknoten dargestellt.
• Variablen und Konstanten werden als Variablenknoten dargestellt.
• Die spezielle Klasse FunProcMainClass dient als Container für die globalen Sprachelemente, da diese in der JVM nicht direkt abgebildet werden können.
– Globale Funktionen und Prozeduren werden zu Instanzmethoden von
FunProcMainClass.
– Globale Variablen und Konstanten werden zu Instanzvariablen von
FunProcMainClass.
Der Name der Klasse kann durch einen Schalter beim Aufruf des Compilers geändert
werden. Die semantische Analyse stellt später sicher, daß die Klasse von den anderen
Klassen nicht aggregiert, instanziert oder erweitert wird.
40
3.2 Parser und abstrakte Syntax
• Ein Großteil der Token des Quellprogramms wird durch die Baumstruktur überflüssig
und daher nicht in den AST übernommen. Dies betrifft neben Klammerungen auch
Schlüsselwörter wie else.
Im Ergebnis ist die abstrakte Syntax für den Programmierer schwerer zu lesen als MiniJavaOO-Quellcode, kann aber um so besser maschinell verarbeitet werden. Sie ist nicht
nur kompakter, sondern erfordert auch einfachere Look-Ahead-DEAs zur Entscheidung
von Alternativen.
Darstellung in ANTLR
Ein AST wird in ANTLR durch Knoten-Objekte der Klasse CommonTree dargestellt (Abbildung 3.6). Ein Knoten besteht aus einem Token des Quellprogramms und einem Verweis
auf einen Elternknoten und die Kindknoten. Um den AST mit weiteren Informationen
anzureichern, wird eine Klasse AstNode abgeleitet, die die Annotation von Symbolen
ermöglicht. Eine entsprechende Klasse AstNodeAdaptor wird innerhalb der Parser als
Factory-Klasse und zur Steuerung der Traversierung genutzt. (Vgl. [8], S. 155 ff.)
org.antlr.runtime.tree.CommonTree
org.antlr.tree.CommonTreeAdaptor
+getParent(): CommonTree
+getChild(i:int): CommonTree
+getToken(): Token
AstNode
AstNodeAdaptor
+setSymbol(s:Symbol)
+getSymbol(): Symbol
Abbildung 3.6: Klasse AstNode zur Symbol-Annotation
In einer ANTLR-Grammtik wird ein AST durch die Syntax 3.3 notiert, die sowohl zur
Konstruktion (siehe [8], Kapitel 7) als auch zur Traversierung mittels einer AST-Grammatik
(vgl. [8], Kapitel 8) genutzt wird. Die verschachtelte Struktur wird durch die Klammerung
ausgedrückt, ^ markiert den Start eines neuen Teilbaums, wobei das erste Token nach der
öffnenden Klammer der Elternknoten ist.
Syntax 3.3 (Darstellung abstrakter Syntaxbäume in ANTLR-Grammatiken)
ast → ^( TOKEN ast∗ )
| TOKEN
| nil
Es folgen drei Beispiele für die Übersetzung von MiniJavaOO in abstrakte Syntax. Anhang C zeigt außerdem die ASTs zu den Beispiel-Programmen der Kapitel 1 und 2.
41
3 Der Compiler
Beispiel: Globale Sprachelemente
Der Ausschnitt aus dem Binärbaum-Programm in Listing 3.1 zeigt die Definitionen der
globalen Prozedur printBaum und der main-Methode.
Listing 3.1: (Binärbaum-Programm: Globale Prozedur und main-Methode)
27
28
29
30
31
32
void printBaum(Baum b) {
// gibt den gesamten Baum in Inorder aus
b.inorder();
// gibt nur die Knoten 4 und 3 aus
b.l.l.l.inorder();
}
33
34
35
36
37
38
// Hauptprogramm
main {
baum = erzeugeBaum();
printBaum(baum);
}
Bei der Transformation in abstrakte Syntax (Abbildung 3.7) werden sowohl main als auch
printBaum zu Methoden der Klasse FunProcMainClass. Der Code ist in diesem speziellen Fall umfangreicher und nicht kompakter geworden. Das Beispiel verdeutlicht auch den
Aufbau der Klassenknoten in der abstrakten Syntax. Erster Kindknoten ist der Klassenname, daraufhin folgen die Superklasse, der Konstruktor, die Instanzvariablen und schließlich die Methoden.
CLASS
FunProcMainClass
EXTENDS
FunProcMainClass
Object
BLOCK
MODIFIER
void
public
FIELDS
...
METHOD
main
PARAMS
METHODS
METHOD
BLOCK
MODIFIER
...
public
void
...
printBaum
PARAMS
BLOCK
PARAM
...
Baum
b
Abbildung 3.7: Binärbaum-Programm: Globale Prozedur und main-Methode im AST
Beispiel: Variablendeklaration
Der Quellcode in Listing 3.2 zeigt einen Ausschnitt aus der ANTLR-Parser-Grammatik. Der
Regel zur Variablendeklaration varDecl wird als Parameter ein AST übergeben, der bei
einer Instanzvariablen den Zugriffsmodifizierer und ansonsten einen leeren Baum enthält
(siehe (AstNode)adaptor.nil() in Regel localVarDecl). Da in einer Anweisung
mehrere Variablen deklariert werden können, wird der Variablentyp, zurückgegeben von
42
3.2 Parser und abstrakte Syntax
der Regel varDeclTyp, jedem Aufruf von varAssign mitgegeben. Nach dem Operator
-> erfolgt die Produktion des AST. ANTLR erkennt, daß innerhalb der AST-Produktion
nur varAssign eine Liste von Bäumen ist, und erzeugt wegen des Operators + einen
VAR-Teilbaum je varAssign.
Listing 3.2: (Übersetzen von Variablendeklarationen in abstrakte Syntax)
1
2
3
4
varDecl[AstNode modifier]
: typ=varDeclTyp varAssign[typ.tree] (COMMA varAssign[typ.tree])*
ENDSTMNT
-> ^(VAR ^(MODIFIER {$modifier}) varAssign)+
;
5
6
7
8
9
varDeclTyp
: INT -> INT
|{enableOO}?=> CLASSIDENT -> SIMPLENAME[$CLASSIDENT]
;
10
11
12
13
14
varAssign[AstNode typ]
: IDENT (ASSIGN (identifier | NUMBER |
{enableOO}?=>constructorCall))?
-> {$typ} SIMPLENAME[$IDENT] identifier? NUMBER? constructorCall?
;
15
16
17
18
19
localVarDecl
: varDecl[(AstNode)adaptor.nil()]
-> varDecl
;
Die Parser-Grammatik verdeutlicht zwei Besonderheiten der AST-Konstruktion mit ANTLR:
• In varDecl werden die imaginären Token VAR und MODIFIER erzeugt. Sie sind
nicht Ergebnis des Lexers-Laufs und haben somit keine direkte Entsprechung im
Quellprogramm. Sie dienen der besseren Strukturierung des AST.
• In den Regeln varDeclTyp und varAssign findet jeweils eine Token-Transformation statt. Sowohl CLASSIDENT als auch IDENT werden zu einem imaginären
Token SIMPLENAME transformiert, in dem dieses mit den Werten des realen Token initialisiert wird. Dies vereinfacht die nachfolgenden AST-Parser und bedeutet
außerdem eine Anpassung an die Begriffswelt der JVM.
Es folgt ein Übersetzungsbeispiel. In Listing 3.3 werden in der Klasse Baum drei Instanzvariablen deklariert.
Listing 3.3: (Binärbaum-Programm: Deklaration von Instanzvariablen)
40
41
class Baum extends Object {
// rechter und linker Teilbaum
43
3 Der Compiler
public Baum r, l;
// Wert des Knotens
protected int v;
42
43
44
Abbildung 3.8 zeigt einen Ausschnitt aus dem zugehörigen AST mit je einem Teilbaum
pro Variable unterhalb des Knotens FIELDS. Unterhalb des VAR-Knotens folgen die Modifier, der Typ und schließlich der Name der Variablen. Entsprechend der Konstruktion in
varDecl verweisen die ersten beiden Deklarationen auf dieselben Blätter mit den Token
public und Baum.
class
Baum
extends
Baum
Object
BLOCK
MODIFIER
FIELDS
Baum
public
VAR
r
...
VAR
MODIFIER
VAR
l
MODIFIER
int
v
protected
Abbildung 3.8: Binärbaum-Programm: Deklaration von Instanzvariablen im AST
Beispiel: Ausdrücke
Das letzte Beispiel in Listing 3.4 zeigt ein weiteres, ausdrucksstarkes Feature von ANTLRGrammatiken. Teil einer üblichen LL(k)-Grammatik für Ausdrücke mit Operator-Vorrang
ist ein Summen-Ausdruck, dessen Teilausdrücke Multiplikations-Ausdrücke sind.
Listing 3.4: (Übersetzen von Summen-Ausdrücken in abstrakte Syntax)
1
2
3
sumExpr
: (multExpr -> multExpr) ((op=ADD|op=SUB) e=multExpr -> ^($op
$sumExpr $e))*
;
Im einfachsten Fall ist der Summen-Ausdruck ein Multiplikations-Ausdruck, und der erste ->-Operator gibt diesen zurück. Besteht der Summen-Ausdruck jedoch aus mehreren verknüpften Multiplikations-Ausdrücken, so wird die zweite AST-Produktion ^($op
$sumExpr $e) für jeden weiteren Teilausdruck einmal ausgeführt. Die Besonderheit ist
die Variable $sumExpr, die jeweils den bisher produzierten AST enthält.
Hierzu ein Beispiel: Listing 3.5 zeigt den Rumpf der Methode berechneAusdruck aus
dem Ackermann-Programm. Er enthält zwei Ausdrücke. Der zweite in Zeile 18 besteht,
abgesehen von der Multiplikation am Ende der Rechnung, nur aus Summen.
44
3.2 Parser und abstrakte Syntax
Listing 3.5: (Ackermann-Programm: Ausdrücke)
13
{
// verschachtelte arithmetische Ausdruecke
ergebnis = x * x + ((56 + 9) / 3 - 5);
// Bildschirmausgaben
print(ergebnis);
print(1 + 2 + 3 + 4 + 5 * 6);
14
15
16
17
18
19
}
In Abbildung 3.9 wird der zugehörige AST dargestellt. Ganz rechts, unter dem printKnoten, ist der Summen-Ausdruck zu erkennen. Wegen der Struktur der Regel sumExpr
wird dieser wie folgt von unten nach oben aufgebaut:
• $sumExpr enthält den Teilbaum 1, dieser wird zum linken Ast von 1+2.
• $sumExpr enthält den Teilbaum 1+2, dieser wird zum linken Ast von 1+2+3.
• $sumExpr enthält den Teilbaum 1+2+3, dieser wird zum linken Ast von 1+2+3+4.
BLOCK
=
print
print
ergebnis
+
ergebnis
+
*
-
x
/
x
5
+
3
56
9
+
+
1
+
*
4
5
6
3
2
Abbildung 3.9: Ackermann-Programm: Ausdrücke im AST
Die Übersetzung des mit Klammern verschachtelten Ausdrucks aus Zeile 15 zeigt, daß die
Klammerung im AST wegfällt. Sie wird, ebenso wie die Operator-Vorrang-Regeln, durch
die Anordnung der Knoten wiedergegeben.
3.2.4 Fehlerbehandlung
Die Behandlung von Syntaxfehlern in ANTLR-Parsern basiert auf einer Recovery-Strategie
(siehe [8], S. 246 ff.). Der Parser meldet Fehler über eine Ausgabe auf System.err und
versucht, durch das Überlesen oder Einfügen von Token wieder in einen korrekten Zustand zu gelangen. Dieses Verhalten ist für viele Anwendungen angemessen, z. B. für den
Syntax-Check innerhalb einer Entwicklungsumgebung oder für eine DSL innerhalb eines
Softwaresystems.
45
3 Der Compiler
Innerhalb eines Compilers sollte der Parser jedoch beim ersten Fehler eine Exception werfen und abbrechen. Wie dies umgesetzt wird, beschreibt [8], S. 241 ff.: In der Klasse
ParserBase werden Methoden von org.antlr.runtime.Parser überlagert. In einigen Fehlerkonstellationen führte dies allerdings nicht zum gewünschten Ergebnis, erst
eine Recherche auf der Mailingliste von ANTLR zeigte, daß zusätzlich noch die Methode
recoverFromMismatchedToken überlagert werden muß.
Exception-Hierarchie
Obwohl es die Syntax von ANTLR-Grammatiken erlaubt, eigene Exception-Klassen anzugeben, übernimmt ANTLR diese z. Zt. nicht in die throws-Anweisungen der generierten
Parser-Methoden. Daher müssen eigene Exception-Klassen von RecognitionException
abgeleitet werden. Abbildung 3.10 zeigt die sich so ergebende Exception-Hierarchie des
MiniJavaOO-Compilers.
org.antlr.runtime.RecognitionException
CodePosition
CompilerException
-desc: String
ErrorCode
-code: int
-desc: String
+get(n:int)
SymbolTableException
SemanticException
Abbildung 3.10: Exception-Klassen zur Fehlerbehandlung
Die Klasse CompilerException realisiert die spezifische Fehlerbehandlung des Compilers. Während RecognitionException keine Möglichkeit zum Zugriff auf die ausgegebene Fehlermeldung bietet, kann einer CompilerException ein beschreibender Text
mitgegeben werden. Das aggregierte CodePosition-Objekt zeigt auf die Fehlerstelle im
Quellprogramm, und durch Objekte der Klasse ErrorCode können Exceptions der gleichen Klasse weiter durch einen eindeutigen Fehlercode mit zugeordneter Fehlermeldung
differenziert werden. Jeder spezifische Fehler kann so durch seinen Code in den an späterer
Stelle gezeigten (Kapitel 4) automatisierten Tests identifiziert werden.
Spezialfall Lexer
Auch der Lexer hat eine automatische Recovery-Funktion. Diese gibt jedoch keine Fehlermeldungen aus und kann auch nicht durch Methodenüberlagerung ausgeschaltet werden.
Hier sorgt ebenfalls eine Suche auf der Mailingliste für Abhilfe durch die in Listing 3.6 gezeigte Regel für die Lexemklasse ANY. Durch die Platzierung als letzte Regel innerhalb des
Lexers in Kombination mit dem Greedy-Wildcard-Operator ’.’ werden alle lexikalischen
Fehler in einem ANY-Token aufgefangen, das an den Parser weitergereicht wird. Der Parser
46
3.3 Symboltabellen und Typsystem
beendet die Verarbeitung, sobald er auf das ANY-Token stößt, da es in keiner Parser-Regel
vorkommt. Die Exception des Lexers wird in this.e gespeichert und führt zur Ausgabe
der Fehlermeldung syntax error: invalid character, der Meldung, die in Error-Code 101
hinterlegt ist.
Listing 3.6: (Lexer-Regel für das ANY-Token zur Fehlerbehandlung)
1
2
3
ANY
: .
{
CodePosition pos = new CodePosition($line, $pos);
if($line >= 0) {
this.e = new CompilerException(ErrorCode.get(101), "lexer",
pos);
System.err.println(e.getMessage());
}
4
5
6
7
8
}
9
10
;
3.3 Symboltabellen und Typsystem
Neben dem AST werden für die semantische Analyse Symboltabellen benötigt, in denen
alle im Quellprogramm vorkommenden Klassen-, Methoden- und Variablendefinitionen verzeichnet sind. Die Symboltabellen sind entsprechend der Struktur des Quellprogramms in
einem Baum angeordnet, dessen Wurzel die Klassentabelle ist. Die Klassentabelle ist das
Verzeichnis der im Programm gültigen Typen.
3.3.1 Design
Das UML-Diagramm in Abbildung 3.11 zeigt die implementierte Klassen-Hierarchie für
die Symboltabellen. Jede Definition wird durch ein Objekt einer Unterklasse von Symbol
dargestellt. Ein Symbol zeichnet sich durch seinen Namen und die Position der Definition
im Quelltext aus. Eine Symboltabelle ist ein Symbol, welches das Interface SymbolTable
implementiert und weitere Symbole in einer Hash-Tabelle aggregiert:
• Ein Klassensymbol aggregiert Symbole für jede seiner Instanzvariablen und Methoden.
• Ein Methodensymbol aggregiert Symbole für die Parametervariablen sowie ein Blocksymbol für den Rumpf. Da Parametervariablen semantisch den lokalen Variablen
entsprechen, werden sie durch die Klasse LocalVariableSymbol abgebildet.
47
3 Der Compiler
• Blocksymbole aggregieren Symbole für lokale Variablen und Blöcke. Sie haben einen
künstlichen Namen und sind für Scope-Prüfungen von lokalen Variablen von Bedeutung.
<<interface>>
<<abstract>>
SymbolTable
Symbol
<<abstract>>
SymbolWithTable
<<abstract>>
VariableSymbol
ClassTable
ClassSymbol
FieldSymbol
MethodSymbol
LocalVariableSymbol
BlockSymbol
Abbildung 3.11: Klassen für Symbole und Symboltabellen
Die Klassensymbole werden von einem Objekt der Klasse ClassTable aggregiert und
erweitern das in Abbildung 3.12 gezeigte Typsystem. Die Basisklasse Object wird durch
ein künstliches Symbol der Singleton-Klasse3 ObjectClassSymbol abgebildet und in die
Klassentabelle eingefügt. Die primitiven Typen werden ebenfalls als Singletons realisiert.
<<interface>>
Type
ClassTable
ClassSymbol
<<abstract>>
PrimitiveType
ObjectClassSymbol
VoidType
Abbildung 3.12: Klassen für das Typsystem
3
Siehe http://sourcemaking.com/design patterns/singleton
48
IntType
3.3 Symboltabellen und Typsystem
Auf die Vorteile des vorgestellten Designs für Namensauflösungen, Zugriffs- und Typprüfungen wie auch auf einzelne Details der verschiedenen Klassen wird während der Beschreibung
der semantischen Analyse in Abschnitt 3.4 eingegangen.
3.3.2 Traversieren des AST
Zum Aufbau der Symboltabellen, aber auch in den späteren Compiler-Phasen muß der AST
traversiert werden. In der ANTLR-Anwendergemeinde werden hierzu drei Möglichkeiten
diskutiert (vgl. [9] und [13]):
1. Tiefensuche mit dem Visitor Pattern
2. Manuell erstellte Tree Walker
3. Von ANTLR generierte Tree Walker
Das Visitor Pattern wird als die ungeeignetste Lösung angesehen, da es die Knoten isoliert
behandelt. Nicht betrachtet werden die Struktur des Baums und der Kontext eines Knotens.
Der Austausch von Informationen zwischen den Knoten ist umständlich.
Die Diskussion für oder wider einen automatisch generierten Tree Walker ähnelt in vielerlei
Hinsicht der Diskussion um automatisch generierte Parser, und tatsächlich wird in [9]
dargelegt, weshalb das Traversieren eines AST im Grunde ein Parserproblem ist. Eine
entgegengesetzte Argumentation findet sich in [13].
Zur Entwicklung des MiniJavaOO-Compilers werden automatisch generierte Tree Walker
eingesetzt. Hierzu wird zunächst eine EBNF-Grammatik entwickelt, die die Struktur des
AST zum Ausdruck bringt. In [8], Kapitel 8 werden die Syntax und die Möglichkeiten
einer entsprechenden Tree-Grammatik beschrieben. Die Grammatik wird für jede einzelne
Compiler-Phase kopiert und mit Actioncode versehen. Durch die Art der Positionierung
des Actioncodes innerhalb der Regeln kann nicht nur für jeden Knotentyp, sondern auch
abhängig vom Kontext des Knotens entschieden werden, ob die Traversierung in Preorder,
Postorder oder Inorder erfolgt.
3.3.3 Aufbau der Symboltabellen
In Abbildung 3.13 sind die beiden Parser zum Aufbau der Tabellen abgebildet. Wie alle
AST-Parser des MiniJavaOO-Compilers beerben sie die Klasse AstWalkerBase, die Zugriff auf die Klassentabelle und die Symboltabellen sowie den Namen der speziellen Klasse
FunProcMainClass gewährt sowie Methoden zur Fehlerbehandlung implementiert.
Klassen
Bevor die Symboltabellen aufgebaut werden können, müssen alle Klassennamen bekannt
sein, da die Klassensymbole innerhalb der Symboltabellen als Typangabe von Variablen und
49
3 Der Compiler
org.antlr.runtime.tree.TreeParser
AstWalkerBase
SymbolTableConstructionWalkerBase
SymbolTableConstructionWalker
Erzeugt
SymbolTableConstructionWalker.g
ClassTableConstructionWalker
Erzeugt
ClassTableConstructionWalker.g
Abbildung 3.13: Klassen der AST-Parser zum Aufbau der Symboltabellen
Methoden gespeichert werden. Hierzu ruft der Parser aus SymbolTableConstructionWalker zunächst den Parser aus ClassTableConstructionWalker auf, bevor er selbst
den AST traversiert. In der Klasse SymbolTableConstructionWalkerBase werden
hierzu und für die Symbol-Annotation Methoden definiert.
Listing 3.7 zeigt die Regel zum Aufbau der Klassentabelle. Hinter dem Token SIMPLENAME
ist ein Block Javacode eingefügt, der eine Instanz von ClassSymbol erzeugt (Zeile 5) und
der versucht, diese in die Klassentabelle einzufügen. Wird eine SymbolTableException
geworfen, so wird diese, angereichert mit Informationen des Parsers, als Exception vom Typ
SemanticException an den Compiler weitergereicht (Zeile 11). Die Syntax zur Analyse
des AST entspricht der in Abschnitt 3.2.3 vorgestellten Syntax zur Synthese des AST.
Listing 3.7: (AST-Parser-Regel zum Aufbau der Klassentabelle)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classDef
: ^( CLASS SIMPLENAME
{
CodePosition pos = new CodePosition($SIMPLENAME.line,
$SIMPLENAME.pos);
ClassSymbol s = new ClassSymbol($SIMPLENAME.text, pos);
try {
this.getClassTable().addClass(s);
}
catch(SymbolTableException e)
{
throw new SemanticException(e.getErrorCode(),
$SIMPLENAME.text, pos);
}
}
^(EXTENDS SIMPLENAME)
constructor
50
3.3 Symboltabellen und Typsystem
^(FIELDS varDecl*)
^(METHODS method*)
16
17
)
18
19
;
Methoden, Blöcke und Variablen
Im Parser für den Aufbau der Symboltabellen wird für jede Variable, jeden Parameter,
jeden Block und jede Methode ein Symbol erzeugt. Der entsprechende Knoten im AST
wird mit dem Symbol annotiert und das Symbol in die aktuelle Symboltabelle eingefügt.
Listing 3.8 verdeutlicht die baumartige Verknüpfung der Symboltabellen. Vor dem Betreten
eines Blocks ist this.currentSymbolTable eine Referenz entweder auf ein Methodensymbol oder ein Blocksymbol. Nachdem das Symbol für den gerade betretenen Block der
aktuellen Symboltabelle hinzugefügt wurde (Zeile 7), wird es selbst zur aktuellen Symboltabelle für die folgenden Blockelemente (Zeile 8). Beim Verlassen des Blocks wiederum wird
dann die ursprüngliche Symboltabelle zur aktuellen (Zeile 18), deren Referenz dem Konstruktor für das neue Blocksymbol in Zeile 6 übergeben wurde. In den Regeln für Methoden
und Klassen wird analog vorgegangen.
Listing 3.8: (AST-Parser-Regel zum Aufbau der Symboltabelle: Block)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
block
: ^( BLOCK
{
CodePosition pos = new CodePosition($BLOCK.line, $BLOCK.pos);
try {
BlockSymbol s = new BlockSymbol(pos,
this.currentSymbolTable);
this.currentSymbolTable.addSymbol(s);
this.currentSymbolTable = s;
$BLOCK.setSymbol(s);
}
catch(SymbolTableException e) {
throw new SemanticException(e.getErrorCode(), "BLOCK", pos);
}
}
blockElement*
)
{
this.currentSymbolTable =
this.currentSymbolTable.getParentTable();
}
;
51
3 Der Compiler
In Listing 3.9 wird die Regel für die Definition einer Instanzvariablen gezeigt, die sich
Informationen aus dem Kontext des Knotens zunutze macht. Die Regel declTyp (Zeile 2)
liefert ein Type-Object an fieldDecl zurück, welches in Zeile 5 dem Konstruktor als
Argument übergeben werden kann. In Zeile 10 wird abgefragt, ob die optionale Regel
varDeclValue (Zeile 2) ausgeführt wurde.
Listing 3.9: (AST-Parser-Regel zum Aufbau der Symboltabelle: Instanzvariable)
1
2
3
4
5
6
fieldDecl
: ^(VAR ^(MODIFIER FINAL? accessModifier) declTyp SIMPLENAME
varDeclValue?)
{
CodePosition pos = new CodePosition($SIMPLENAME.line,
$SIMPLENAME.pos);
FieldSymbol s = new FieldSymbol($SIMPLENAME.text,
$declTyp.type, $classDef::symbol, pos);
this.addSymbol($SIMPLENAME, s);
7
$SIMPLENAME.setSymbol(s);
s.setAccess($accessModifier.access);
if($varDeclValue.text != null) {
s.setValue($varDeclValue.text);
}
if($FINAL != null) {
s.setFinal();
if($varDeclValue.text != null) {
s.setInitialized();
}
}
8
9
10
11
12
13
14
15
16
17
18
}
19
;
20
Die weiteren Regeln zum Aufbau der Symboltabellen folgen dem gleichen Prinzip.
Beispiel: Binärbaum-Programm
Abbildung 3.14 stellt die Klassentabelle und die Symboltabellen des Binärbaums aus Listing 1.1 dar. Die Symboltabellen werden durch weiße Rechtecke, die Symbole durch graue
Rechtecke repräsentiert. Symbole, die ihrerseits Symboltabellen enthalten, sind mit diesen über die durchgezogenen Linien verbunden. Die gestrichelten Linien symbolisieren die
Verknüpfung der Klassensymbole innerhalb der Klassentabelle, wodurch die Vererbungshierarchie abgebildet wird. Bei der Suche in den Symboltabellen kann sich entsprechend
den Pfeilen bewegt werden.
In der Abbildung ist zu erkennen, daß Blocksymbole einen künstlichen Namen erhalten, der
52
3.3 Symboltabellen und Typsystem
**
**
+
$#*
$#*
%*%$
"$&
%*%$
"$&
#!#)
#!#)
)&(
)&(
)!"%
$)#$
)!"%
$)#$
)(%&
)(%&
)'%(
)'%(
&"'
&"'
!"#$
!"#$
!%#$
&("%
!%#$
&("%
Abbildung 3.14: Binärbaum-Programm: Symboltabellen
53
3 Der Compiler
die Zeile und die Spalte ihrer öffnenden geschweiften Klammer beinhaltet. An der Methode
setValue (siehe Mitte rechts) in der Klasse Baum kann die Darstellung verschachtelter
Blöcke in den Symboltabellen nachvollzogen werden.
Durch die Transformation der globalen Sprachelemente in die Klasse FunProcMainClass
ist im AST auch ein Konstruktor entstanden, der sich nun als Methodensymbol in den Symboltabellen wiederfindet (siehe oben rechts). Da der zugehörige Block keine Repräsentation
im Quellprogramm hat, trägt er den Namen $l0c0 für Zeile 0 bzw. Spalte 0.
Ein weiteres Zwischenergebnis des Compilers nach dem Aufbau der Symboltabellen sind die
Symbol-Annotationen im AST. Abbildung 3.15 zeigt den AST der Klasse Baum. Knoten im
AST, die mit einem Symbol annotiert sind, sind grau hervorgehoben. Neben den Blöcken
sind nun alle Knoten und Blätter, die einen Namen deklariert haben, annotiert. Sie dienen
als Einstiegspunkte in die Symboltabellen. Der Übersicht halber sind die Teilbäume unter
den Blöcken nur angedeutet, sie enthalten keine weiteren Annotationen.
class
Baum
extends
Baum
Object
BLOCK
MODIFIER
FIELDS
Baum
VAR
r
...
VAR
VAR
MODIFIER
public
l
MODIFIER
int
v
protected
...
METHODS
METHOD
MODIFIER
void
setValue
public
METHOD
PARAMS
BLOCK
MODIFIER
PARAM
...
public
int
void
inorder
PARAMS
BLOCK
...
v
Abbildung 3.15: Binärbaum-Programm: Annotation der Deklarationen im AST
3.3.4 Fehlerbehandlung
Auch für die von ANTLR generierten AST-Parser müssen entsprechend den Aussagen aus
Abschnitt 3.2.4 einige Methoden aus org.antlr.runtime.tree.TreeParser überschrieben werden. Dies erfolgt für alle AST-Parser des Compilers zentral in der Klasse
AstWalkerBase. Neben den vom Parser geworfenen Exceptions der Klasse RecognitionExcepion werden vom Actioncode auch eigene Exceptions vom Typ SemanticException
geworfen. Wie aus den Listings des vorherigen Abschnitts ersichtlich ist, basieren sie meist
54
3.4 Semantische Analyse
auf Exceptions, die von den Symboltabellen geworfen werden. Im AST-Parser werden diese
mit Informationen zur Position des Fehlers im Quelltext angereichert.
3.4 Semantische Analyse
Nachdem nun die Symbol- und Klassentabellen ausgebaut wurden und der AST über die
Symbole mit den Tabellen verknüpft ist, kann die semantische Analyse durchgeführt werden. Hierzu bedient sich der Compiler wieder eines AST-Parsers (Abbildung 3.16). Außer
der Prüfung der semantischen Bedingungen finden auch Namensauflösungen und die damit
verbundenen Symbol-Annotationen im AST statt.
org.antlr.runtime.tree.TreeParser
AstWalkerBase
SemantikCheckWalkerBase
SemanticCheckWalker
Erzeugt
SemanticCheckWalker.g
Abbildung 3.16: Klassen des AST-Parsers für die semantische Analyse
Die Methoden zur semantischen Analyse sind in der Klasse SemantikCheckWalkerBase
(Abbildung 3.17) implementiert.
Neben dem AST sowie den darin annotierten Token und Symbolen haben die Methoden
zur semantischen Analyse Zugriff auf die aktuelle Symboltabelle (getSymbolTable) und
das aktuelle Klassen-Symbol (getClassSymbol). Wie dies jeweils beim Betreten bzw.
Verlassen einer Block-, Methoden- oder Klassenregel realisiert wird, wurde anhand von
Listing 3.8 in Abschnitt 3.3.3 erläutert.
Die Methoden isLvarAutoInit und setLvarAutoInit steuern das Verhalten der
Analyse bezüglich der automatischen Initialisierung von lokalen Variablen. Diese kann beim
Aufruf des Compilers ein- oder ausgeschaltet werden.
3.4.1 Semantische Prüfungen
Methoden, deren Name mit check beginnt, führen eine semantische Prüfung durch. Sie
werden, den semantischen Bedingungen der Sprachspezifikation folgend (Anhang A), an den
entsprechenden Stellen im AST-Parser aufgerufen. Der für die Prüfung relevante Teilbaum
wird als Parameter übergeben. Bei erfolgreicher Prüfung geben die Methoden kein Ergebnis
55
3 Der Compiler
SemantikCheckWalkerBase
+getSymbolTable(): SymbolTable
+setSymbolTable(st:SymbolTable)
+getClassSymbol(): ClassSymbol
+setClassSymbol(c:ClassSymbol)
+isLvarAutoInit(): boolean
+setLvarAutoInit(autoInit:boolean)
+checkVariable(name:AstNode)
+checkLocalVariable(name:AstNode)
+checkInScope(name:AstNode)
+checkHidesLocalVariableOrParameter(name:AstNode)
+checkNotThis(name:AstNode)
+thisNotAllowed(name:AstNode)
+checkAssignToFinal(name:AstNode)
+checkInitialized(name:AstNode)
+setInitialized(name:AstNode)
+checkMethod(name:AstNode)
+checkNumberOfArguments(name:AstNode,actual:int)
+checkTypeOfArgument(name:AstNode,actual:Type,
index:int,method:MethodSymbol)
+checkOverride(name:AstNode)
+checkConstructorName(name:AstNode,class:String)
+checkExtendsChain(name:AstNode)
+checkType(name:AstNode,actual:Type,expected:Type)
+checkintType(name:AstNode)
+checkIntType(name:AstNode,actualType:Type)
+resolveClassName(name:AstNode): AstNode
+resolveSimpleNameInMethodAndClass(name:AstNode): AstNode
+resolveNameInMethod(name:AstNode): AstNode
+resolveQualifiedName(qname:List<AstNode>,
inSpecialClass:boolean): AstNode
Abbildung 3.17: Klasse SemantikCheckWalkerBase
zurück. Im anderen Falle wird der Compiler zur Ausgabe einer Fehlermeldung mit Angabe
der relevanten Stelle im Quellcode veranlaßt. Hierzu werden von den Methoden Exceptions
vom Typ SemanticException geworfen. Die Fehlermeldungen orientieren sich an den
Formulierungen der Fehlermeldungen von Eclipse.
Listing 3.10 zeigt die AST-Parser-Regel zur Prüfung der Zuweisung. Die in Zeile 2 aufgerufene AST-Parser-Regel name führt die Namensauflösung der linken Seite der Zuweisung
durch (Details hierzu beschreibt Abschnitt 3.4.2.) und liefert die letzte Komponente des
Namens an assignment zurück. Diese wird nun in den Zeilen 4 bis 6 semantischen Prüfungen unterzogen:
• Ist der Name das Schlüsselwort this, so bricht der Compiler mit Error-Code 411
und der Meldung this not allowed here ab.
• Referenziert der Name keine Variable, so lautet der Fehler field or local variable name
expected, repräsentiert durch Error-Code 403.
• Fehler 406 mit der Meldung final variable cannot be assigned tritt auf, falls die Variable als final deklariert wurde.
In Zeile 7 wird die Variable als initialisiert markiert. Daraufhin erfolgt die Prüfung der Typverträglichkeit der Zuweisung: In den Zeilen 12 bis 18 wird der Typ des R-Value festgestellt
56
3.4 Semantische Analyse
und in den Zeilen 19 und 20 mit dem Typ des L-Value verglichen. Tritt eine Typverletzung
auf, so wird je nach Art des Fehlers entweder type mismatch: cannot convert to int oder type
mismatch: cannot convert to void oder type mismatch: cannot convert type ausgegeben.
Listing 3.10: (AST-Parser-Regel zur Prüfung der Zuweisung)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assignment
: ^( ASSIGN name
{
this.checkNotThis($name.lastName);
this.checkVariable($name.lastName);
this.checkAssignToFinal($name.lastName);
this.setInitialized($name.lastName);
}
expr?
constructorCall?
{
Type actual= null;
if($expr.start != null) {
actual = $expr.type;
}
else if($constructorCall.start != null) {
actual = $constructorCall.type;
}
this.checkType($ASSIGN, actual,
((VariableSymbol)$name.lastName.getSymbol()).getType());
}
)
;
Es folgt ein Überblick über die verschiedenen Prüfmethoden. Weitere Beispiele zum Einsatz der Methoden werden im nächsten Abschnitt genannt. Obwohl den Methoden ein
AstNode-Object übergeben wird, findet die eigentliche Prüfung in der Regel an annotierten Symbolen statt. Das AstNode-Object enthält allerdings nützliche Informationen zur
Ausgabe der Fehlermeldung, wie etwa Zeile und Spalte des betroffenen Token im Quellprogramm.
Prüfen von Variablen
• checkVariable prüft, ob ein Symbol eine Variable ist.
• checkLocalVariable prüft, ob ein Symbol eine lokale Variable ist.
• checkInScope prüft, ob eine Variable im Scope ihrer Deklaration ist.
• checkNotThis bricht ab, falls das Symbol this ist.
57
3 Der Compiler
• checkAssignToFinal verhindert die Zuweisung an als final deklarierte Variablen.
• checkInitialized prüft, ob eine Variable bereits initialisiert wurde.
• checkHidesLocalVariableOrParameter prüft, ob die Deklaration einer lokalen Variablen eine bereits deklarierte lokale Variable oder einen Methoden-Parameter
überdeckt.
Prüfen von Methoden
• checkMethod prüft, ob ein Symbol eine Methode ist.
• checkNumberOfArguments stellt sicher, daß eine Methode mit der richtigen Anzahl Argumente aufgerufen wurde.
• checkTypeOfArgument prüft für jedes Argument die Typverträglichkeit mit der
Signatur.
Prüfungen zur Objektorientierung
• checkConstructorName stellt sicher, daß der Name eines Konstruktors dem Namen der Klasse entspricht.
• checkExtendsChain überprüft die Zyklenfreiheit des Ableitungsbaums.
• checkOverride prüft die semantischen Bedingungen der Methodenüberlagerung.
Prüfungen zur Typverträglichkeit
• checkType stellt sicher, daß ein Symbol kompatibel zu einem Typ ist.
• checkIntType stellt sicher, daß ein Symbol vom Typ Integer ist.
3.4.2 Namensauflösung
Die Methoden, deren Namen mit resolve beginnen, dienen der Auflösung von Namen.
Sie nehmen einen Teilbaum des AST entgegen, der den aufzulösenden Namen repräsentiert,
suchen dessen Bestandteile in den Symboltabellen und geben bei erfolgreicher Prüfung die
letzte Teilkomponente des Namens, annotiert mit dem relevanten Symbol, zurück. Tritt
eine Verletzung der semantischen Bedingungen auf oder kann ein Name nicht aufgelöst
werden, so wird auch hier eine SemanticException geworfen, die den Compiler zum
Abbruch und zur Ausgabe einer Fehlermeldung veranlaßt.
58
3.4 Semantische Analyse
In Listing 3.11 ist die relevante Parser-Regel abgebildet, die drei Fälle unterscheidet (vgl.
Anhang A.4):
Der Name ist ein einfacher Name Traversiert der AST-Parser gerade die spezielle Klasse
FunProcMainClass, so muß der Name eine globale Methode, eine globale Variable
oder eine lokale Variable sein. Hierzu sucht die Methode resolveSimpleNameInMethodAndClass (Zeile 5) das zugehörige Symbol in den Symboltabellen. Die Suche beginnt in der Tabelle des aktuellen Blocks und endet in der Symboltabelle der
aktuellen Klasse.
In einer anderen Klasse muß der Name eine lokale Variable sein. Dementsprechend
wird in der Symboltabelle des aktuellen Blocks und der darüber liegenden Blöcke bis
zur Symboltabelle der aktuellen Methode gesucht (Zeile 8). Nach der Auflösung des
Namens folgt die Prüfung des Scopes (Zeile 10).
Der Name ist this Befindet sich der Parser gerade in der speziellen Klasse FunProcMainClass, also innerhalb globaler Methoden, so ist this nicht erlaubt, und der Compiler bricht ab (Zeile 15). Im anderen Falle wird der Name als lokale Variable der
aktuellen Methode aufgelöst (Zeile 18). Jede Instanzmethode enthält ein Variablensymbol für this.
Der Name ist ein qualifizierter Name Ein aus mehreren Komponenten bestehender qualifizierter Name wird durch die Methode resolveQualifiedName aufgelöst und
geprüft. Neben der Symbol-Annotation findet hier auch die Prüfung des Zugriffsschutzes für Instanzvariablen und Methoden statt. Die Implementierung wird im folgenden
erläutert.
Listing 3.11: (AST-Parser-Regel zur Namensauflösung)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
name returns [AstNode lastName]
: SIMPLENAME
{
if($classDef::inSpecialMainClass) {
$lastName =
this.resolveSimpleNameInMethodAndClass($SIMPLENAME);
}
else {
$lastName = this.resolveSimpleNameInMethod($SIMPLENAME);
}
this.checkInScope($SIMPLENAME);
}
| THIS
{
if($classDef::inSpecialMainClass) {
this.thisNotAllowed($THIS);
}
else {
59
3 Der Compiler
$lastName = this.resolveSimpleNameInMethod($THIS);
18
}
19
}
| ^(QUALIFIEDNAME qualifiedName+=THIS? qualifiedName+=SIMPLENAME+)
{
$lastName = this.resolveQualifiedName($qualifiedName,
$classDef::inSpecialMainClass);
}
;
20
21
22
23
24
25
Der Methode resolveQualifiedName (Listing 3.12) wird der qualifizierte Name als
Liste von AstNode-Objekten übergeben, über die in einer Schleife iteriert wird (Zeile 8).
Jedes AstNode-Objekt repräsentiert einen einfachen Namen. Innerhalb der Schleife werden
zwei Fälle unterschieden:
1. Komponente Befindet sich der Parser in der speziellen Klasse FunProcMainClass, so
wird der Name in den Symboltabellen der Methode und der Klasse gesucht (Zeile 16).
Wird er gefunden, so muß er eine Variable repräsentieren (Zeile 22) und darf nicht
this sein (Zeile 20). Methoden sind nur als letzte Komponente zulässig.
Befindet sich der Parser in einer anderen Klasse, so wird nur in der Methode gesucht
(Zeile 26). Der Name muß eine lokale Variable sein (Zeile 28). In beiden Fällen wird
der Scope (Zeile 30) überprüft, und es wird sichergestellt, daß die Variable vom
Referenztyp ist (Zeile 31). Der Klassenname der Referenz wird gespeichert, um bei der
nächsten Komponente prüfen zu können, ob sie ein Member der Klasse ist. Schließlich
wird der AST der Komponente mit dem gefundenen Symbol annotiert (Zeile 32).
n. Komponente Es wird in der Symboltabelle der Klasse der vorherigen Komponente nach
dem Namen gesucht (Zeile 36). Ist die aktuelle Komponente nicht die letzte Komponente des Namens, so muß sie eine Instanzvariable (Zeile 39) vom Referenztyp
(Zeile 40) repräsentieren. Die letzte Komponente darf Instanzvariable oder Instanzmethode beliebigen Typs sein.
In jedem Fall muß der Zugriffsschutz geprüft werden (Zeile 42): Hat die Klasse, in der
sich der Parser gerade befindet, Zugriff auf die Instanzvariable oder Instanzmethode,
die die aktuelle Komponente referenziert? Ist der Zugriff erlaubt, so kann schließlich
das gefundene Symbol im AST annotiert werden (Zeile 43). Die letzte Namenskomponente wird als Ergebnis der Methode zurückgegeben (Zeile 49). Die Parser-Regel
Name gibt diese an die sie aufrufende Regel zum Zweck der Typprüfung weiter.
Wie aus Zeile 2 ersichtlich wird, wirft die Methode eine SemanticException. Diese ist
entweder eine weitergeleitete Exceptions der verschiedenen check-Methoden, oder es werden Exceptions der Symboltabellen abgefangen (Zeile 46), die von den search-Methoden
geworfen werden. Letztere werden als semantische Exceptions, angereichert um Angaben
zur Position des Fehlers im Quellprogramm, an den Compiler weitergereicht (Zeile 47).
60
3.4 Semantische Analyse
Listing 3.12: (Methode zum Auflösen qualifizierter Namen)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public AstNode resolveQualifiedName(List<AstNode> qualifiedName,
boolean inSpecialMainClass)
throws SemanticException {
AstNode currentName = null;
ClassSymbol currentClass = null;
CodePosition p = null;
Symbol s = null;
try {
for(int i = 0; i < qualifiedName.size(); i++) {
currentName = qualifiedName.get(i);
p = new CodePosition(currentName.getLine(),
currentName.getCharPositionInLine());
// 1. Komponente
if(i == 0) {
if(inSpecialMainClass) {
// Feld oder lokale Variable in FunProc
s = this.searchLocalVariableOrClassMember(
currentName.getText(),
(BlockSymbol) this.symbolTable);
if(s.getName() == "this") {
this.thisNotAllowed(currentName);
}
this.checkVariable(currentName);
}
else {
// lokale Variable sonst
s = this.searchLocalVariable(currentName.getText(),
(BlockSymbol) this.symbolTable);
this.checkLocalVariable(currentName);
}
this.checkInScope(currentName);
currentClass = this.getTypeOfReferenceVariable(currentName);
currentName.setSymbol(s);
}
// n. Komponente
else {
s = this.searchClassMember(currentName.getText(),
currentClass);
// Nicht letzte Komponente muss Referenztyp sein
if(i < qualifiedName.size()-1) {
this.checkField(currentName);
currentClass = this.getTypeOfReferenceVariable(currentName);
}
checkAccess(s, p);
currentName.setSymbol(s);
}
61
3 Der Compiler
}
} catch (SymbolTableException e) {
throw new SemanticException(e.getErrorCode(),
currentName.getText(), p);
}
return currentName;
45
46
47
48
49
50
}
Beispiel: Binärbaum-Programm
Das Ergebnis der Namensauflösung in den Methodenrümpfen der Klasse Baum wird in Abbildung 3.18 dargestellt. Waren nach dem Aufbau der Symboltabellen lediglich die Knoten
der Deklarationen und Blöcke annotiert, sind es nun alle im Programm vorkommenden
Namen. Durch diese vollständige Annotation des AST ist in den nachfolgenden CompilerPhasen keine erneute Suche in den Symboltabellen nötig.
METHODS
METHOD
MODIFIER
void
setValue
public
int
METHOD
PARAMS
BLOCK
MODIFIER
PARAM
if
public
v
<=
v
BLOCK
10
v
this
l
inorder
PARAMS
BLOCK
INVOKE
print
INVOKE
ARGLIST
QUALIFIEDNAME
QUALIFIEDNAME
this
v
this
r
ARGLIST
inorder
BLOCK
0
=
QUALIFIEDNAME
this
inorder
QUALIFIEDNAME
if
>=
void
v
v
Abbildung 3.18: Binärbaum-Programm: Annotationen im AST nach der Namensauflösung
Suche in den Symboltabellen
Die in resolveQualifiedName benutzten search-Methoden machen sich die polymorphen Eigenschaften der Symbol-Klassen (vgl. Abschnitt 3.3.3, Abbildung 3.11) und die Verknüpfung der Tabellen miteinander zunutze. Abbildung 3.19 verdeutlicht die Hierarchie der
Symboltabellen sowie die Einstiegs- und Ausstiegspunkte der Methoden aus Listing 3.13.
Es wird jeweils die Methode getSymbolRecursive aufgerufen, der als Parameter ein
62
3.4 Semantische Analyse
class-Objekt übergeben wird, welches definiert, wann der rekursive Aufstieg in den Symboltabellen stoppen soll.
Abbildung 3.19: Symbolsuche
Listing 3.13: (Methoden zur Suche in den Symboltabellen)
1
2
3
4
private Symbol searchLocalVariable(String name, SymbolTable st)
throws SymbolTableException {
return st.getSymbolRecursive(name, MethodSymbol.class);
}
5
6
7
8
9
private Symbol searchLocalVariableOrClassMember(String name,
SymbolTable st)
throws SymbolTableException {
return st.getSymbolRecursive(name, ObjectClassSymbol.class);
}
10
11
12
13
14
private Symbol searchClassMember(String name, ClassSymbol st)
throws SymbolTableException {
return st.getSymbolRecursive(name, ObjectClassSymbol.class);
}
In Listing 3.14 ist die Implementierung von getSymbolRecursive in der Klasse SymbolWithTable notiert. Wird das Symbol in der aktuellen Symboltabelle nicht gefunden (Zeile 2), so wird die Suche durch Rekursion in der darüber liegenden Symboltabelle fortgesetzt
(Zeile 4). Die Rekursion stoppt, falls die aktuelle Symboltabelle eine Instanz der Klasse
63
3 Der Compiler
stopClass ist (Zeile 3) oder das gesuchte Symbol gefunden wurde (Zeile 9).
Listing 3.14: (Methode getSymbolRecursive)
1
2
3
4
5
6
7
8
9
10
11
public Symbol getSymbolRecursive(String name, Class stopClass) throws
SymbolTableException {
if(! this.table.containsKey(name)) {
if(! stopClass.isInstance(this)) {
return this.parentTable.getSymbolRecursive(name, stopClass);
}
throw new SymbolTableException(ErrorCode.get(301));
}
else {
return this.table.get(name);
}
}
Es stellt sich heraus, daß das recht komplexe Klassendesign der Symbole und Symboltabellen eine kompakte Implementierung der Suche ermöglicht.
Zugriffsschutz
Zur Prüfung des Zugriffs ruft resolveQualifiedName die Methode checkAccess auf
(Listing 3.15). Diese delegiert die eigentliche Prüfung an die Methode accessOk (Zeile 2).
Listing 3.15: (Methode checkAccess zur Zugriffsschutz-Prüfung)
1
2
3
4
5
6
7
8
9
10
private void checkAccess(Symbol s, CodePosition p) throws
SemanticException {
if(! accessOk((AccessProtected) s)) {
if(s instanceof FieldSymbol) {
throw new SemanticException(ErrorCode.get(416), s.getName(), p);
}
else if(s instanceof MethodSymbol) {
throw new SemanticException(ErrorCode.get(417), s.getName(), p);
}
}
}
Die Methode accessOk (Listing 3.16) muß sowohl Instanzvariablen als auch Instanzmethoden prüfen, daher implementieren die Klassen FieldSymbol und MethodSymbol das
gemeinsame Interface AccessProtected und aggregieren ein Objekt vom Typ AccessModifier (Abbildung 3.20).
Der klassenbasierte Zugriffsschutz unterscheidet drei Fälle (vgl. Anhang A.2):
64
3.5 Generieren von Jasmin-Assemblercode
<<interface>>
AccessProtection
+setAccess(a:AccessModifier)
+getAccess(): AccessModifier
+getParentClass(): ClassSymbol
MethodSymbol
FieldSymbol
AccessModifier
+includes(a:AccessModifier): boolean
Abbildung 3.20: Klassen für den Zugriffsschutz
1. Entspricht die Klasse des zugriffsgeschützten Symbols der aktuellen Klasse (Zeile 2),
so ist der Zugriff in jedem Fall gewährt.
2. Ist die aktuelle Klasse eine Unterklasse der Klasse des zugriffsgeschützten Symbols
(Zeile 6), so wird der Zugriffsmodifizierer protected bzw. public erwartet.
3. Im anderen Falle wird der Zugriffsmodifizierer public erwartet (Zeile 12).
Listing 3.16: (Methode accessOk zur Zugriffsschutz-Prüfung)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean accessOk(AccessProtected s) {
if(this.classSymbol.equals(s.getParentClass())) {
// Selbe Klasse braucht private, protected oder public
return true;
}
if(this.classSymbol.isSubClassOf(s.getParentClass())) {
// Unterklasse braucht protected oder public
return s.getAccess().includes(AccessModifier.getProtected());
}
else {
// Sonst public benoetigt
return s.getAccess().includes(AccessModifier.getPublic());
}
}
3.5 Generieren von Jasmin-Assemblercode
Um aus den Informationen im AST und den Symboltabellen Bytecode für die JVM zu generieren, wird zunächst mit einem Code-Emitter Assemblercode erzeugt, der in der nächsten
65
3 Der Compiler
Phase mit Jasmin assembliert wird. Weitere Aufgabe dieser Phase ist die Berechnung des
Platzbedarfs von Operanden-Stacks.
3.5.1 Struktur des Assemblercodes
Bevor die Implementierung des Code-Emitters beschrieben wird, behandelt dieser folgende
Abschnitt die Struktur des Assemblercodes am Beispiel der Klasse Baum.
Es muß zwischen den Anweisungen des Assemblers, die mit einem Punkt beginnen, und den
Maschinenbefehlen der JVM unterschieden werden. Die Assembleranweisungen (siehe [6])
sind spezifisch für Jasmin und codieren die Struktur der Klassen. Die Maschinenbefehle
sind in der Spezifikation der JVM (siehe [4], Kapitel 6, S. 171 ff.) definiert und werden für
den eigentlichen Code innerhalb der Methoden benötigt.
In den Zeilen 1 bis 6 (Listing 3.17) sind der Klassenname, die Superklasse und die Instanzvariablen der Klasse Baum definiert. Die Zugriffsmodifizierer werden wie in Java angegeben.
Mögliche Typangaben sind I für Integer, V für Void oder auch die Angabe eines Klassennamens, dem ein L (für Load) vorangestellt ist und der mit einem Semikolon abgeschlossen
wird.
Listing 3.17: (Binärbaum-Programm: Klassendefinition und Instanzvariablen)
1
2
.class public Baum
.super java/lang/Object
3
4
5
6
.field public r LBaum;
.field public l LBaum;
.field protected v I
Die Zeilen 8 bis 10 (Listing 3.18) leiten die Methode setvalue ein. In den runden Klammern sind die Typen der Argumente notiert, hinter den Klammern steht der Rückgabetyp.
Es folgt die Angabe der Größe des Operanden-Stacks und der Anzahl der lokalen Variablen,
die innerhalb der Methode definiert sind. Hierzu zählen auch die Parametervariablen und
this. Abgeschlossen wird die Methode in Zeile 27.
Innerhalb der Methode ist der Code für zwei verschachtelte if-Statements zu sehen (vgl.
Listing 1.1, Zeilen 45 bis 51), die durch für Assemblercode typische Sprünge realisiert werden. Am Beispiel des inneren if-Statements wird im nächsten Abschnitt die Funktionsweise
des Code-Emitters erläutert.
Listing 3.18: (Binärbaum-Programm: Methode setValue in Assemblercode)
8
9
10
11
.method public setValue(I)V
.limit stack 2
.limit locals 2
iload 1
66
3.5 Generieren von Jasmin-Assemblercode
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ldc 10
if_icmpgt c0
iload 1
ldc 0
if_icmplt c1
aload_0
iload 1
putfield Baum/v I
goto e1
c1:
e1:
goto e0
c0:
e0:
return
.end method
Die Methode inorder in Listing 3.19 zeigt die Übersetzung qualifizierter Namen, des
Methodenaufrufs und des print-Statements. Der Methodenaufruf this.l.inorder()
wird durch die Zeilen 32 bis 34 abgebildet. Zunächst wird die Objektreferenz this auf den
Stack geladen. Der Befehl getfield nimmt die Referenz vom Stack und legt den Wert der
Instanzvariablen l auf den Stack. Der nächste Befehl nimmt wiederum diese Objektreferenz
vom Stack und führt die Methode inorder auf ihr aus. Entsprechend ist in den Zeilen 39
bis 41 die Übersetzung von this.r.inorder() angegeben.
Die Zeilen 35 bis 38 zeigen den Assemblercode des Befehls print(this.v), die Übersetzung wird in Abschnitt 3.5.3 im Zusammenhang mit der Berechnung der von der Methode
benötigten Stackgröße erläutert.
Listing 3.19: (Binärbaum-Programm: Methode inorder in Assemblercode)
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
.method public inorder()V
.limit stack 2
.limit locals 1
aload_0
getfield Baum/l LBaum;
invokevirtual Baum/inorder()V
getstatic java/lang/System/out Ljava/io/PrintStream;
aload_0
getfield Baum/v I
invokevirtual java/io/PrintStream/println(I)V
aload_0
getfield Baum/r LBaum;
invokevirtual Baum/inorder()V
return
.end method
67
3 Der Compiler
Listing 3.20 zeigt schließlich den Konstruktor der Klasse, der in der JVM durch den speziellen Namen <init> repräsentiert wird. In den Zeilen 48 und 49 wird der Konstruktor
der Superklasse Object aufgerufen.
Listing 3.20: (Binärbaum-Programm: Konstruktor in Assemblercode )
45
46
47
48
49
50
51
.method public <init>()V
.limit stack 1
.limit locals 1
aload_0
invokespecial java/lang/Object/<init>()V
return
.end method
3.5.2 Funktionsweise des Code-Emitters
Implementiert wird der Code-Emitter durch einen AST-Parser (Abbildung 3.21), der sich
der Template-Engine StringTemplate bedient, um strukturierten Text auszugeben. StringTemplate ist eine in ANTLR integrierte Bibliothek für Java, mit deren Hilfe Text aus
einzelnen Fragmenten flexibel zusammengesetzt werden kann. Hierzu werden Templates
mit Platzhaltern definiert, in die andere Templates, Strings oder Java-Objekte eingefügt
werden können. Die Templates können beliebig verschachtelt werden und befinden sich in
der Datei JasminCodeEmitterTemplates.stg.
org.antlr.runtime.tree.TreeParser
AstWalkerBase
JasminCodeEmitterWalkerBase
JasminCodeEmitterWalker
Erzeugt
JasminCodeEmitterWalker.g
org.antlr.strintemplate.StringTemplate
Laden der Templates
JasminCodeEmitterTemplates.stg
Abbildung 3.21: Klassen des AST-Parsers für den Code-Emitter
Nachstehend wird die Funktionsweise des Code-Emitters anhand der Übersetzung des ifStatements verdeutlicht. Listing 3.21 zeigt ein Beispiel aus der Methode setValue der
Klasse Baum.
68
3.5 Generieren von Jasmin-Assemblercode
Listing 3.21: (Binärbaum-Programm: if-Statement)
49
50
51
if v >= 0 {
this.v = v;
}
Die Übersetzung in Assemblercode wird in Listing 3.22 dargestellt. Zunächst wird der
Wert der lokalen Variablen v (Zeile 14), dann die Konstante 0 (Zeile 15) auf den Stack
geladen. Die beiden Werte auf dem Stack werden in Zeile 16 verglichen, und falls v < 0
ist, die inverse Logik also, wird zu Marke c1 (Zeile 21) hinter den if-Zweig gesprungen.
Anderenfalls wird zunächst die Objektreferenz this (Zeile 17) und danach der Wert der
lokalen Variable v auf den Stack geladen. Der Befehl putfield in Zeile 19 nimmt den
obersten Wert vom Stack und speichert ihn in der Instanzvariablen Baum/v der nun oben
auf dem Stack liegenden Objektreferenz (this). Schließlich wird in Zeile 20 an das Ende
des if-Statements gesprungen.
Listing 3.22: (Jasmin-Assemblercode: if-Statement)
14
15
16
17
18
19
20
21
22
iload 1
ldc 0
if_icmplt c1
aload_0
iload 1
putfield Baum/v I
goto e1
c1:
e1:
Das Beispiel verdeutlicht, daß der Code-Emitter nicht zwischen if-Statements mit oder
ohne else-Zweig unterscheidet und daher hier überflüssigen Code erzeugt. Das Beispiel
enthält keinen else-Zweig, weshalb eine Sprungmarke genügt hätte und der goto-Befehl
unnötig ist. Die Assembler-Befehle eines else-Zweigs lägen zwischen den Sprungmarken.
Zudem zeigt das Beispiel, daß lokale Variablen in der JVM nicht über ihren Namen, sondern
durch einen Index angesprochen werden. this ist die erste lokale Variable mit Index 0,
v die zweite mit Index 1. Ein weiteres Merkmal ist, daß die JVM-Befehle typsicher sind,
d. h. iload lädt eine lokale Variable vom Typ Integer, aload eine lokale Variable vom
Typ Objektreferenz. Auch der Befehl putfield ist typsicher, wegen des I am Ende des
Befehls erwartet er einen Integer-Wert auf dem Stack.
Listing 3.23 zeigt die AST-Parser-Regel zur Übersetzung des if-Statements. Die Ausgabe
von StringTemplates innerhalb einer ANTLR-Grammatik wird analog zur Ausgabe eines
AST mit dem Operator -> notiert (vgl. [8], Kapitel 9, S. 195 ff.). Ergebnis der Regel ist das
in Zeile 7 aufgerufene Template ifBlock, welches vier Parameter hat. Mit den Parametern
werden die im Template angegebenen Platzhalter gefüllt.
69
3 Der Compiler
Im ersten Argument wird das von der in Zeile 3 aufgerufenen Regel condition (Zeilen
13 bis 22) zurückgegebene Template übergeben, im zweiten und dritten Argument sind
es die von den beiden statement-Regeln (Zeilen 4 und 5) zurückgelieferten Templates.
Die zweite statement-Regel repräsentiert den optionalen else-Zweig und wird ggf. nicht
aufgerufen. In diesem Fall beinhaltet die Variable $s2.st in Zeile 9 eine null-Referenz,
wodurch der Platzhalter elsestatement im ifBlock-Template mit einem leeren String
gefüllt wird.
Listing 3.23: (AST-Parser-Regel: if-Statement im Code-Emitter)
1
2
3
4
5
6
7
8
9
10
11
ifBlock
: ^( IF {$statement::labelId=$method::labelId++;}
condition
s1=statement
s2=statement?
)
-> ifBlock(condition={$condition.st},
ifstatement={$s1.st},
elsestatement={$s2.st},
id={$statement::labelId})
;
12
13
14
15
16
17
18
19
20
21
condition
: ^(compOp expr1=expr expr2=expr)
{
this.getMethod().decStackSize(2);
}
-> condition(compop={$compOp.st},
expr1={$expr1.st},
expr2={$expr2.st})
;
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
compOp
: NE
->
| EQ
->
| LE
->
| GE
->
| LT
->
| GT
->
;
70
compOpNe(id={$statement::labelId})
compOpEq(id={$statement::labelId})
compOpLe(id={$statement::labelId})
compOpGe(id={$statement::labelId})
compOpLt(id={$statement::labelId})
compOpGt(id={$statement::labelId})
3.5 Generieren von Jasmin-Assemblercode
Als viertes Argument wird eine für dieses if-Statement eindeutige Nummer zur Benamung der im Assemblercode benötigten Sprungmarken übergeben. Die Nummer wird in
Zeile 2 erzeugt, in $statement::labelId gespeichert und ist innerhalb der aktuellen Methode eindeutig. In der JVM sind Sprünge über Methodengrenzen hinaus unzulässig, daher hat jede Methode ihren eigenen Namensraum für Sprungmarken. Die Variable
$statement::labelId ist eine in der Regel statement definierte Scope-Variable, die
dem Informationsaustausch von Regeln untereinander dient (vgl. [8], S. 135 ff.). Dies bedeutet, daß alle von der statement-Regel aufgerufenen Regeln Zugriff auf die Variable haben.
Die Regel compOp in den Zeilen 23 bis 36 macht davon Gebrauch, um zu den Sprungmarken
passende bedingte Sprünge zu erzeugen. Wird allerdings unterhalb von statement wieder
statement aufgerufen, so hat die Variable für diesen Teilbaum einen anderen Wert, und
es werden für ineinander verschachtelte if-Statements jeweils eindeutige Sprungmarken
erzeugt (vgl. Listing 3.18).
In der condition-Regel in den Zeilen 13 bis 22 werden in gleicher Weise wie in der
ifBlock-Regel die Templates der untergeordneten Regeln compOp und expr in die Platzhalter des Templates condition eingefügt. Stack-Berechnungen wie die in Zeile 16 werden
im nächsten Abschnitt erläutert.
In Listing 3.24 werden die Templates zu den Regeln dargestellt. Im Template ifBlock
wird zunächst die Condition erzeugt (Zeile 2), die die beiden Teilausdrücke auf dem Stack
berechnet (Zeilen 11 und 12), bevor, abhängig vom logischen Operator, einer der bedingten
Sprünge (Zeilen 16 bis 38) ausgeführt wird. Ziel des Sprungs ist der else-Zweig (Zeile 6).
Hinter der Condition wird der if-Zweig eingefügt (Zeile 3) und dann mit goto hinter den
else-Zweig gesprungen.
Listing 3.24: (Template: if-Statement im Code-Emitter)
1
2
3
4
5
6
7
8
ifBlock(condition, ifstatement, elsestatement, id) ::= <<
<condition>
<ifstatement>
goto e<id>
c<id>:
<elsestatement>
e<id>:
>>
9
10
11
12
13
14
condition(compop, expr1, expr2) ::= <<
<expr1>
<expr2>
<compop>
>>
15
16
17
18
compOpNe(id) ::= <<
if_icmpeq c<id>
>>
71
3 Der Compiler
19
20
21
22
compOpEq(id) ::= <<
if_icmpne c<id>
>>
23
24
25
26
compOpLt(id) ::= <<
if_icmpge c<id>
>>
27
28
29
30
compOpGt(id) ::= <<
if_icmple c<id>
>>
31
32
33
34
compOpLe(id) ::= <<
if_icmpgt c<id>
>>
35
36
37
38
compOpGe(id) ::= <<
if_icmplt c<id>
>>
Im nächsten Abschnitt folgt ein weiteres Beispiel zur Arbeit des Code-Emitters, das auch
die Einbeziehung von im AST annotierten Symbolen zeigt.
3.5.3 Ermitteln der Größe von Methoden-Stacks
Die JVM legt zur Laufzeit für jeden Methodenaufruf einen Frame auf den Stack des aktuellen Threads. Bestandteil eines solchen Frames ist der Operanden-Stack für die Berechnungen innerhalb des Methodenrumpfs. Da im Bytecode Informationen zum Platzbedarf
der Frames enthalten sein müssen, wird die Größe des Operanden-Stack zur Compilezeit
berechnet (vgl. Abschnitt 2.2.1).
Es ließe sich vermuten, daß die Berechnung der Stackgröße zur Compilezeit nicht möglich
ist. Dies trifft zu, falls ein Assemblerprogramm wie das in Listing 3.25 geschrieben wird.
Das Beispiel zeigt eine Schleife, die in jedem Durchlauf die Konstante 1 auf den Stack legt,
bis die lokale Variable mit Index 1 gleich der lokalen Variablen mit Index 2 ist. In den
Zeilen 2 bis 4 findet der Vergleich mit bedingtem Sprung statt. Zeile 5 legt eine weitere 1
auf den Stack. In den Zeilen 6 bis 8 wird die lokale Variable mit Index 1 inkrementiert,
bevor in Zeile 9 wieder zum Anfang der Schleife gesprungen wird. Der benötigte Stackplatz
ist also von den lokalen Variablen abhängig und daher erst zur Laufzeit bekannt.
Listing 3.25: (Ein Stack unbekannter Größe in Assemblercode)
loop:
iload 1
1
2
72
3.5 Generieren von Jasmin-Assemblercode
3
4
5
6
7
8
9
10
iload 2
if_icmpeq end
ldc 1
ldc 1
iload 1
istore 1
goto loop
end:
Die JVM ist jedoch zur Übersetzung höherer Programmiersprachen, nicht zur manuellen
Assembler-Programmierung gedacht. Tatsächlich zeigt sich bei der Entwicklung des Compilers, daß der benötigte Stackbedarf für alle Sprachelemente von MiniJavaOO berechenbar
ist.
Zur Berechnung der benötigten Stackgröße werden die Methoden incStackSize und
decstackSize des Symbols der Methode, die der AST-Parser gerade traversiert, aufgerufen. Entsprechend der Struktur der erzeugten Assemblerbefehle wird so die Stackgröße
in den Parser-Regeln inkrementiert und dekrementiert sowie das Maximum bestimmt. Bei
der Erzeugung des Methodenkopfs kann dann auf den Wert zurückgegriffen werden, um
einen Platzhalter im Methoden-Template zu füllen.
Listing 3.26 zeigt das Template für das print-Statement. Zunächst wird die Referenz für
System.out auf den Stack gelegt, dann die auszugebende Expression, die einen Wert
auf dem Stack zurückläßt, berechnet und schließlich mit invokevirtual die statische
Methode println aufgerufen. println nimmt die Referenz für System.out und den
Wert von Expression vom Stack und führt die Ausgabe durch.
Listing 3.26: (Template: print-Statement im Code-Emitter)
1
2
3
4
5
print(expr) ::= <<
getstatic java/lang/System/out Ljava/io/PrintStream;
<expr>
invokevirtual java/io/PrintStream/println(I)V
>>
Die zugehörige Parser-Regel in Listing 3.27 erhöht die Stackgröße vor der Ausführung
der Regel expr um 1 (Zeile 1). In der Regel expr wird die Stackgröße entsprechend der
Struktur des Ausdrucks weiter inkrementiert und dekrementiert, in der Summe wird der
Wert um 1 erhöht. Letztlich muß wegen des println-Befehls in Zeile 3 die Stackgröße um
2 dekrementiert werden.
Listing 3.27: (AST-Parser-Regel: print-Statement im Code-Emitter)
1
2
3
print
: ^(PRINT {this.getMethod().incStackSize(1);} expr)
{
73
3 Der Compiler
this.getMethod().decStackSize(2);
}
-> print(expr={$expr.st})
4
5
6
;
7
Die AST-Parser-Regel in Listing 3.28 zeigt ein Beispiel, das sich auch die im AST annotierten Symbole zunutze macht. Vor dem Aufruf einer Methode werden deren Argumente auf
dem Stack berechnet (Zeile 4). Entsprechend viele Werte liegen auf dem Stack, die durch
den Methodenaufruf wieder entfernt werden. In den Zeilen 7 und 8 wird auf das Methodensymbol zugegriffen und die Stackgröße um die Anzahl der Parameter dekrementiert.
Listing 3.28: (AST-Parser-Regel: Methodenaufruf im Code-Emitter)
1
2
3
4
5
6
7
8
9
10
11
12
13
invocation
: ^( INVOKE
name[CodeEmitterNameMode.INVOKE]
^(ARGLIST args+=expr*)
)
{
MethodSymbol m = (MethodSymbol) $name.lastname.getSymbol();
this.getMethod().decStackSize(m.getNumberOfParams());
}
-> invocation(ref={$name.reference},
args={$args},
name={$name.st})
;
Dem Prinzip der beiden gezeigten Beispiele folgend, wird innerhalb jeder AST-ParserRegel des Code-Emitters die Stackgröße inkrementiert und dekrementiert. Die Methode incStackSize speichert jeweils das Maximum, so daß nach der Traversierung des
Methoden-Blocks der benötigte Stackbedarf feststeht.
3.6 Generieren und Verifizieren von JVM-Bytecode
Nachdem nun sowohl für jede im Quellprogramm definierte Klasse als auch für die globalen Sprachelemente in der speziellen Klasse FunProcMainClass Assemblercode erzeugt
wurde, muß dieser noch in auf der JVM lauffähigen Bytecode transferiert werden.
Der Jasmin-Assembler bietet hierzu, neben einem Kommandozeilen-Interface, ein JavaAPI (Abbildung 3.22). Der Assemblercode der einzelnen Klassen wird in je ein Objekt vom
Typ jasmin.ClassFile eingelesen (Methode readJasmin) und, falls kein Fehler beim
Parsen erkannt wird (Methode errorCount), mit der Methode write in ein Byte-Array
geschrieben.
74
3.6 Generieren und Verifizieren von JVM-Bytecode
jasmin.ClassFile
+readJasmin(input:Reader,name:String,numberLines:boolean)
+errorCount()
+getClassName(): String
+write(outp:OutputStream)
Abbildung 3.22: Java-API des Jasmin-Assemblers
Um sicherzustellen, daß der generierte Bytecode die strukturellen und semantischen Bedingungen der JVM erfüllt, ist es sinnvoll, ihn durch einen Verifier zu überprüfen. Außerdem
werden auf diese Weise noch einmal die Abhängigkeiten der Klassen untereinander überprüft. Der Jasmin-Assembler übersetzt jede Klasse für sich.
Als Verifier kommt die in Abschnitt 2.2.2 genannte Bytecode Engineering Library zum
Einsatz. In Abbildung 3.23 werden die benötigten Klassen und Methoden dargestellt.
org.apache.bcel.classfile.ClassParser
+ClassParser(input:ByteArrayInputStream)
+parse(): JavaClass
org.apache.bcel.verifier.VerifierFactory
+getVerifier(className:String)
org.apache.bcel.Repository
+addClass(clazz:JavaClass)
org.apache.bcel.verifier.Verifier
+doPass1()
+doPass2()
+doPass3a()
+doPass3b()
Abbildung 3.23: Klassen des Bytecode-Verifiers
Zunächst wird der Bytecode mit Hilfe der Klasse ClassParser in ein Repository eingelesen. Dann wird für jede Klasse ein Verifier-Object erzeugt, das die drei in der
JVM-Spezifikation (vgl. [4], Abschnitt 4.9, S. 140 ff.) genannten Phasen durchläuft:
Pass 1 Der erste Durchlauf überprüft Formatierung, Struktur und Syntax des Bytecodes.
Pass 2 In der zweiten Phase werden die semantischen Bedingungen verifiziert, die ohne
Untersuchung der Methodenrümpfe überprüft werden können. Dies beinhaltet z. B.
die Prüfung von Namen, Typangaben von Instanzvariablen und Methoden oder Angaben zur Superklasse.
Pass 3 Während der dritten Phase werden die Rümpfe der Methoden mit einer Datenflußanalyse durchlaufen. Es werden z. B. die Größe der Operanden-Stacks, die Argumente
der Methodenaufrufe oder die Typkompatibilität von Zuweisungen verifiziert.
75
3 Der Compiler
Tritt während der Assembler- oder Verifier-Phase ein Fehler auf, so bricht der Compiler wie
üblich mit einer CompilerException und der Ausgabe der von Jasmin oder der BCEL
produzierten Fehlermeldung ab.
Werden die beiden Phasen ohne Fehler durchlaufen, so speichert der Compiler schließlich
den Bytecode, den Assemblercode und die visuelle Darstellung des AST im GraphvizFormat in Dateien ab.
76
4 Testgetriebene Compiler-Entwicklung
Nachdem im vorigen Kapitel die Implementierung des MiniJavaOO-Compilers behandelt
wurde, soll nun auf die testgetriebene Entwicklung des Compilers eingegangen werden.
Zu Beginn des zweiten Sprints, während des Studiums der Dokumentation zu gUnit, kommt
die Idee auf, den Compiler testgetrieben zu entwickeln. Der Grundgedanke ist, nicht die im
Test Driven Development (TDD)1 üblichen Unit-Tests durchzuführen, sondern die Tests
auf die Ein- und Ausgabe der einzelnen Compiler-Phasen zu beziehen. Voraussetzung für
die Entwicklung von Phase n ist, daß Phase n − 1 bereits getestet wurde. So kann als
Test-Eingabe stets ein MiniJavaOO-Programm genutzt werden.
Ein Beispiel zur Verdeutlichung: Bei der Entwicklung der Compiler-Phase zum Aufbau der
Symboltabellen werden für die verschiedenen Symbole und Symboltabellen jeweils kleine
MiniJavaOO-Programme geschrieben. Die Test-Eingaben durchlaufen den bereits fertiggestellten und getesteten Parser. Der Parser erzeugt den AST, der als Eingabe für den
AST-Parser zum Aufbau der Symboltabellen dient. Der AST-Parser wird nun von Testfall
zu Testfall weiterentwickelt. Die durch die Tests überprüfte Ausgabe ist eine eindeutige
textuelle Darstellung der Symboltabellen.
Die ersten Compiler-Phasen werden mit gUnit getestet. Ab der semantischen Analyse ist
dies nicht mehr möglich, da gUnit maximal zwei Parser hintereinander schalten kann.
Die AST-Parser der semantischen Analyse und des Code-Emitters werden daher mit einer Kombination aus jUnit und StringTemplate getestet. Der AST-Parser zum Aufbau
der Klassentabelle wird, nach außen unsichtbar, durch den AST-Parser zum Aufbau der
Symboltabellen aufgerufen. So kann auch diese Phase noch mit gUnit getestet werden.
4.1 Tests mit gUnit
Mit gUnit können zwei verschiedene Testarten geschrieben werden. Ein Test kann prüfen, ob
das Eingabeprogramm im Sinne der Syntax korrekt ist. Weiterhin kann überprüft werden,
ob die vom Parser erzeugte Ausgabe, z. B. ein AST, den Erwartungen entspricht.
1
Siehe http://www.frankwestphal.de/TestgetriebeneEntwicklung.html
77
4 Testgetriebene Compiler-Entwicklung
Parser und AST-Konstruktion
Listing 4.1 zeigt den Anfang einer gUnit-Test-Datei. In der ersten Zeile wird definiert, welche Grammatik zu testen ist. Ab Zeile 4 erfolgen die eigentlichen Tests, jeweils bezogen auf
eine Regel der Grammatik. Zunächst werden vollständige Programme, anschließend lokale
Konstantendeklarationen getestet. Es können sowohl innerhalb der Datei angegebene Programmteile (etwa die Variablendeklaration in Zeile 9) als auch umfangreichere, in externen
Dateien gespeicherte Programme (wie in Zeile 5) getestet werden. Hinter dem Test-Code
wird jeweils durch OK oder FAIL notiert, ob der Parser die Syntax als korrekt oder fehlerhaft erkennen soll. In den Konstantendeklarationen aus den Zeilen 11 und 12 fehlt jeweils
die Wertzuweisung, so daß hier ein FAIL erwartet wird.
Listing 4.1: (gUnit-Test: Parser)
1
2
gunit MiniJavaOO;
@header{package net.pasdziernik.minijavaoo.compiler.generated;}
3
4
5
6
ooProgram:
MiniJavaOO_test_1.mjava OK
MiniJavaOO_test_mrieke_1.mjava OK
7
8
9
10
11
12
13
14
15
localConstDecl:
"final int a=4, b=56, d=2;" OK
"final int a = 6;" OK
"final int a=1,b;" FAIL
"final int b;" FAIL
"final int a = b;" OK
"final int a = b.c;" OK
"final int a = b.c.d;" OK
Um die Anzahl der Testfälle zu begrenzen, sind die Tests so aufgebaut, daß für jede Regel
nur die spezifischen Aspekte des entsprechenden Sprachkonstrukts getestet werden müssen. In den EBNF-Regeln für if und while (Syntax 4.1) sind sowohl Expression als
auch Statement als auch Condition enthalten. Die Tests in Listing 4.2 setzen voraus, daß
mögliche Ausprägungen von Expression und Statement bereits getestet wurden, weshalb
Statement hier z. B. stets durch einen leeren Block repräsentiert wird. Die Regel Condition
wird zusammen mit der Regel WhileStatement getestet, so daß dies in den Tests zu if
nicht nochmals erfolgt.
Syntax 4.1 (Kontrollstrukturen und Conditions)
IfStatement → if Condition Statement ( else Statement
WhileStatement → while Condition Statement
Condition → Expression CondOperator Expression
CondOperator → == | != | < | > | <= | >=
78
)?
4.1 Tests mit gUnit
Listing 4.2: (gUnit-Test: if und while im Parser)
1
2
3
4
5
6
7
8
9
whileBlock:
"while 0 < c { }" OK
"while 0 <= c.d { }" OK
"while 0 > c.d { }" OK
"while 0 >= c.d { }" OK
"while 0 == c.d { }" OK
"while 0 != c.d { }" OK
"while 0 { }" FAIL
"while 0 < c.d < f { }" FAIL
10
11
12
13
ifBlock:
"if a<b { }" OK
"if a<b { } else { }" OK
Die Erzeugung der abstrakten Syntaxbäume für die Konstantendeklarationen aus Listing 4.1 wird in Listing 4.3 getestet. Statt OK oder FAIL wird nun nach -> der erwartete
AST in der üblichen ANTLR-Syntax angegeben.
Listing 4.3: (gUnit-Test: Lokale Konstanten im Parser)
1
2
3
4
5
6
7
8
9
10
11
localConstDecl:
"final int a=4, b=56, d=2;"
-> (VAR (MODIFIER final) int
(VAR (MODIFIER final) int
"final int a = 6;"
-> (VAR (MODIFIER final) int
"final int a = b;"
-> (VAR (MODIFIER final) int
"final int a = b.c;"
-> (VAR (MODIFIER final) int
"final int a = b.c.d;"
-> (VAR (MODIFIER final) int
a 4) (VAR (MODIFIER final) int b 56)
d 2)
a 6)
a b)
a (QUALIFIEDNAME b c))
a (QUALIFIEDNAME b c d))
Einen Testfall für eine Klassendeklaration zeigt Listing 4.4. Ein mehrzeiliges Eingabeprogramm wird durch doppelte eckige Klammern begrenzt.
Listing 4.4: (gUnit-Test: Klasse im Parser)
1
2
3
4
5
6
classDef:
<<
class A extends Object {
A(){}
public void f()
{}
79
4 Testgetriebene Compiler-Entwicklung
7
8
9
10
}
>>
-> "(class A (extends Object) (A BLOCK) FIELDS (METHODS
(METHOD (MODIFIER public) void f PARAMS BLOCK)))"
Durch die beiden verschiedenen Testarten können Lexer und Parser zunächst ohne Ausgabe
entwickelt werden, um danach die AST-Konstruktion in die Regeln einzufügen.
Symboltabellen
Wie in Abschnitt 3.2.3 beschrieben, muß der Parser AST-Knoten der Klasse AstNode erzeugen, damit Symbol-Annotationen möglich sind. gUnit kann allerdings von CommonTree
abgeleitete Klassen nicht testen. Daher ist es nötig, den Quellcode von gUnit entsprechend
anzupassen. Dies betrifft den gUnit-Interpreter, da der gUnit-Generator für jUnit-Tests
während der Entwicklung des MiniJavaOO-Compilers keine Anwendung findet. Das Design von gUnit gestattet es nicht, die Anpassung durch Klassen-Ableitungen zu realisieren,
vielmehr muß die Klasse gUnitExecutor direkt geändert werden. Listing 4.5 verdeutlicht
das Prinzip: In der Klasse wird an drei Stellen ein neues Parser-Objekt erzeugt (Zeile 1).
Unmittelbar danach wird, falls es sich um einen MiniJavaOO-Parser handelt (Zeile 3),
ein zu AstNode passendes Adapter-Objekt erzeugt und mit dem Parser-Objekt verknüpft
(Zeile 5).
Listing 4.5: (Anpassen von gUnit an die Klasse AstNode)
1
Object parObj = parConstructor.newInstance(parArgs);
2
3
4
5
6
if(parObj instanceof MiniJavaOOParser) {
MiniJavaOOParser mp = (MiniJavaOOParser) parObj;
mp.setTreeAdaptor(new AstNodeAdaptor());
}
Weitere Voraussetzung für den Eingabe-Ausgabe-Test ist eine formale Darstellung der verschachtelten Symboltabellen als String. Die toString-Methode der Symboltabellen erzeugt hierzu eine den EBNF-Regeln in Syntax 4.2 entsprechende Ausgabe. Eine Symboltabelle besteht aus einer Auflistung von Symbolen innerhalb spitzer Klammern. Ein Symbol
ist durch runde Klammern begrenzt und hat einen Namen sowie eine Angabe zur Art des
Symbols. Je nach Art des Symbols kommt noch eine Typangabe, z. B. bei Variablensymbolen, oder eine Symboltabelle, z. B. bei Methodensymbolen, hinzu. Der Name eines Blocks
beginnt mit $ und beinhaltet die Zeilen- und Spaltenangabe der öffnenden geschweiften
Klammer des Blocks.
Syntax 4.2 (String-Darstellung von Symboltabellen)
SymbolTabelle → < Symbol ∗ >
Symbol → ( SymbolName : SymbolArt
80
( : Typ ) ? ( : SymbolTabelle ) ? )
4.1 Tests mit gUnit
SymbolName → ClassName | SimpleName | BlockName
BlockName → $l [0−9]+ c [0−9]+
SymbolArt → CLASS | METH | BLOCK | FIELD | FFIELD
Typ → V | I | ClassName
| LVAR | FLVAR
Da die textuelle Darstellung der Symboltabellen schnell lang wird, werden in Listing 4.6
zwei kürzere Beispiele für Testfälle angegeben. Bereits die Symboltabelle des aus einem leeren Block bestehenden minimalen MiniJavaOO-Programms ist wegen der Darstellung der
globalen Sprachelemente als spezielle Klasse FunProcMainClass recht umfangreich. Es
wird deutlich, daß die Symboltabelle zahlreiche Symbole enthält, die keine direkte Entsprechung im Quellprogramm haben, z. B. das Symbol für den Konstruktor von FunProcMainClass oder das Schlüsselwort this, welches in jeder Methode als Symbol einer lokalen
Variablen auftritt. Die Beispiele zeigen, daß es nicht einfach ist, entsprechende Testfälle zu
formulieren.
Listing 4.6: (gUnit-Test: AST-Parser für die Symboltabellen)
1
program walks ooProgram:
2
3
4
5
<<
{}
>> ->
6
7
8
9
10
"<(Object:CLASS::<>)(FunProcMainClass:CLASS:Object:<
(FunProcMainClass:METH:V:<(this:LVAR:FunProcMainClass)
($l0c0:BLOCK:<>)>)(main:METH:V:<(this:LVAR:FunProcMainClass)
($l2c2:BLOCK:<>)>)>)>"
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<<
{}
class A extends Object
{
A() {}
}
class B extends A
{
B() {}
public void f() {
A a;
B b = new B();
}
}
>> ->
27
28
29
"<(Object:CLASS::<>)(FunProcMainClass:CLASS:Object:<
(FunProcMainClass:METH:V:<(this:LVAR:FunProcMainClass)
81
4 Testgetriebene Compiler-Entwicklung
30
31
32
33
34
($l0c0:BLOCK:<>)>)(main:METH:V:<(this:LVAR:FunProcMainClass)
($l2c2:BLOCK:<($l2c2:BLOCK:<>)>)>)>)
(A:CLASS:Object:<(A:METH:V:<(this:LVAR:A)($l6c8:BLOCK:<>)>)>)
(B:CLASS:A:<(B:METH:V:<(this:LVAR:B)($l11c8:BLOCK:<>)>)
(f:METH:V:<(this:LVAR:B)($l12c20:BLOCK:<(a:LVAR:A)(b:LVAR:B)>)>)>)>"
Der Parser zum Aufbau der Symboltabellen bricht ab, falls von den Symboltabellen Exceptions geworfen werden. Auch dies wird testgetrieben entwickelt, das Prinzip entspricht
den im nächsten Abschnitt beschriebenen Tests für die semantische Analyse.
4.2 Tests mit jUnit und StringTemplate
Zum Testen von mehr als zwei hintereinander geschalteten Parsern wie auch zum Testen der
Fehlerbehandlung des Compilers kann gUnit nicht eingesetzt werden. Eine Kombination aus
StringTemplate, jUnit und einiger Hilfsklassen (siehe Abbildung 4.1) schafft hier Abhilfe.
Die Klasse CompilerTestBase bietet vier Methoden, die jeweils ein MiniJavaOO-Programm entgegennehmen und die Ausgabe der jeweiligen Compiler-Phase zurückliefern:
• testParser liefert den AST, dargestellt als String.
• testSymbolTableConstruction gibt die Klassen- und Symboltabellen als String
zurück.
• testSemanticCheck liefert keine String-Ausgabe. Hier erfolgen Tests über den
Error-Code von geworfenen Exceptions.
• testCodeEmitter gibt eine Hash-Tabelle zurück, die die Klassennamen des Quellprogramms als Schlüssel und den Assemblercode der Klassen als Wert enthält.
Die Methoden dienen zur Steuerung der Klasse CompilerPhases (siehe Abschnitt 3.1).
Zur Formulierung der Testfälle wird zunächst die Klasse CompilerTestTool entwickelt. Dem Konstruktor werden Name und Pfad einer StringTemplate-Datei übergeben,
die den MiniJavaOO-Quellcode und die erwarteten Ausgaben der Tests enthält. Mit den
verschiedenen set-Methoden können die Eingabeprogramme und die erwarteten Ausgaben der Testfälle aus ineinander verschachtelten Quelltexten mit Platzhaltern zusammengefügt werden. So können mehrere Testfälle, die sich nur in wenigen Codezeilen unterscheiden, effizient dargestellt werden. Die eigentlichen Tests werden mit den TestOk- und
TestFail-Methoden durchgeführt. Bei semantischen Tests wird nur eine Eingabe erzeugt,
und die TestOk-Methode erwartet, daß keine SemanticException geworfen wird. Die
TestFail-Methode erwartet eine Exception mit dem als Parameter angegebenen ErrorCode. Die Test-Methode für den Code-Emitter erwartet, daß die Ausgabe des Tests dem
Ausgabe-Template entspricht.
82
4.2 Tests mit jUnit und StringTemplate
CompilerTestBase
+testParser(input:String): String
+testSymbolTableConstruction(input:String): String
+testSemanticCheck(input:String)
+testCodeEmitter(input:String): HashTable<String,
String>
CompilerTestTool
+CompilerTestTool(path:String,file:String)
+setInBaseTemplate(template:String)
+setInTemplate(name:String,template:String)
+setOutBaseTemplate(template:String)
+setOutTemplate(name:String,template:String)
+setOutFieldInitsTemplate(template:String)
+reset()
+sematicTestOk(template:String)
+semanticTestOk(name:String,template:String)
+semanticTestFail(template:String,code:int)
+semanticTestFail(name:String,template:String,
code:int)
+emitterTestOk(template:String)
CompilerPhases
CompilerTestTool2
+CompilerTestTool2(path:String,file:String)
+reset()
+symbolTableConstructionTestOk(source:String)
+symbolTableConstructionTestFail(source:String,
code:int)
+testEmitterOk(source:String,expected:HashTable<String,
String>)
SourceTemplate
+SourceTemplate(path:String,file:String)
+reset()
+load(template:String)
+add(template:String)
+add(name:String,template:String)
+addCode(name:String,code:String)
+toString(): String
Abbildung 4.1: Klassen für die jUnit-Tests
Bei der Formulierung von Tests für den Code-Emitter zeigt sich, daß Templates für Tests
mit mehreren Klassen mit den Methoden der Klasse CompilerTestTool nicht formuliert
werden können. Daher werden die Klassen CompilerTestTool2 und SourceTemplate
programmiert, die flexibler in der Schachtelung von Templates sind. Die Tests für die Fehlerbehandlung der Symboltabellen-Phase wurden im nachhinein von CompilerTestTool
auf CompilerTestTool2 migriert. Die Tests der semantischen Analyse sowie ein Teil der
Code-Emitter-Tests wurden aus Zeitmangel nicht migriert.
Die folgenden Beispiele verdeutlichen die Anwendung der Test-Klassen in der semantischen
Analyse und im Code-Emitter.
Semantische Analyse
Der Ausschnitt aus einer jUnit-Methode in Listing 4.7 zeigt den Test der semantischen
Bedingungen der Methodenüberlagerung. Zunächst wird ein Basis-Template geladen (Zeile 3). Das Template enthält ein MiniJavaOO-Programm mit einer Basisklasse, die verschiedene Methoden definiert, und eine abgeleitete Klasse mit einem Platzhalter <inner>
(Listing 4.8, Zeile 1 bis 13). Es folgen vier Tests. Der erste in Zeile 4 lädt den Inhalt des
Templates testMethodOverrideSignatureOk in das Basis-Template und läßt das erhaltene Programm durch die Compiler-Phasen bis zur semantischen Analyse laufen. Der
Test ist erfolgreich, wenn keine Exception geworfen wird. Die Methodenüberlagerungen in
den drei folgenden Tests haben falsche Signaturen, daher wird vom Compiler erwartet,
jeweils eine SemanticException mit dem Error-Code 423 zu werfen.
83
4 Testgetriebene Compiler-Entwicklung
Listing 4.7: (jUnit-Test: Semantik der Methodenüberlagerung)
1
2
3
4
5
6
7
@Test
public void testMethodOverride() throws RecognitionException {
ctt.setInBaseTemplate("testMethodOverrideBase");
ctt.semanticTestOk("testMethodOverrideSignatureOk");
ctt.semanticTestFail("testMethodOverrideSignatureFail1", 423);
ctt.semanticTestFail("testMethodOverrideSignatureFail2", 423);
ctt.semanticTestFail("testMethodOverrideSignatureFail3", 423);
Listing 4.8: (Templates zum Test der Methodenüberlagerung)
1
2
3
4
5
6
7
8
9
10
11
12
13
testMethodOverrideBase(inner) ::= <<
class A extends Object {
A() {}
public int mpub() { return 0;}
protected void mpro(int i, int j) {}
private A mpri(int i, B b, Object o) { A a = new A(); return a;}
}
class B extends A {
B() {}
<inner>
}
{}
>>
14
15
16
17
18
19
testMethodOverrideSignatureOk() ::= <<
public int mpub() { return 0;}
protected void mpro(int i, int j) {}
private A mpri(int i, B b, Object o) { A a = new A(); return a;}
>>
20
21
22
23
testMethodOverrideSignatureFail1() ::= <<
public int mpub(int i) { return 0;}
>>
24
25
26
27
testMethodOverrideSignatureFail2() ::= <<
protected void mpro() {}
>>
28
29
30
31
testMethodOverrideSignatureFail3() ::= <<
protected void mpro(int i) {}
>>
Nach diesem Schema werden alle Bedingungen der statischen Semantik testgetrieben entwickelt. Ein Teil wird bereits in der Phase zum Aufbau der Symboltabellen abgeprüft.
84
4.2 Tests mit jUnit und StringTemplate
Code-Emitter
Die testgetriebene Entwicklung der Phase zur Erzeugung des JVM-Assemblercodes soll am
Beispiel einer sich rekursiv aufrufenden Methode demonstriert werden. In Listing 4.9 ist der
Testfall notiert. Das Eingabe-Programm besteht in diesem Fall aus einem einfachen Template ohne Platzhalter (Listing 4.10, Zeile 1 bis 9) und wird in Zeile 3 geladen. Die erwartete
Ausgabe wird in den Zeilen 5 bis 9 zusammengesetzt. Zunächst werden in das allgemeine
Klassen-Template (Listing 4.10, Zeile 21 bis 31) die Platzhalter für den Klassennamen und
die Basisklasse gefüllt, dann wird das Template für die rekursive Methode (Listing 4.10,
Zeile 11 bis 19) in den Platzhalter <inner> eingefügt. Schließlich wird in Zeile 8 die gerade
zusammengesetzte Klasse der erwarteten Ausgabe hinzugefügt, ebenso in Zeile 9 das Template für eine leere main-Methode. Zeile 11 ruft dann das CompilerTestTool2-Object
zur Durchführung des Tests auf.
Listing 4.9: (Test eines Methodenaufrufs)
1
2
3
@Test
public void testQualifiedNameMethod() throws RecognitionException {
source.load("testQualifiedNameMethod_in");
4
classBase.addCode("name", "A");
classBase.addCode("base", "java/lang/Object");
classBase.add("testQualifiedNameMethod_out");
expected.put("A", classBase.toString());
expected.put("FunProcMainClass", funProcBase.toString());
5
6
7
8
9
10
ctt.testEmitterOk(source.toString(), expected);
11
12
}
Listing 4.10: (Templates: Test eines Methodenaufrufs)
1
2
3
4
5
6
7
8
9
testQualifiedNameMethod_in() ::= <<
{}
class A extends Object {
A() {}
public void m() {
this.m();
}
}
>>
10
11
12
13
14
15
16
testQualifiedNameMethod_out() ::= <<
.method public m()V
.limit stack 1
.limit locals 1
aload_0
invokevirtual A/m()V
85
4 Testgetriebene Compiler-Entwicklung
17
18
19
return
.end method
>>
20
21
classBase(name, base, inner, constructor) ::= <<
22
23
24
.class public <name>
.super <base>
25
26
<inner>
27
28
<if(constructor)><constructor><else><emptyconstructor(base=base)><endif>
29
30
31
>>
32
33
34
35
36
37
38
39
40
41
emptyconstructor(base) ::= <<
.method public \<init>()V
.limit stack 1
.limit locals 1
aload_0
invokespecial <base>/\<init>()V
return
.end method
>>
Nach diesem Muster werden die verschiedenen Teile des Code-Emitters testgetrieben entwickelt. Es wird deutlich, daß die Formulierung größerer Testfälle komplex werden kann.
Das Design der Hilfsklassen für die jUnit-Tests hat sich mit den Anforderungen des MiniJavaOO-Compilers entwickelt. Es kann die Grundlage für ein generisches Framework zur
Durchführung von Ein-Ausgabe-Tests auf der Basis von jUnit und StingTemplate liefern.
86
5 Zusammenfassung und Ausblick
Im folgenden wird zunächst ein Rückblick auf die Inhalte der einzelnen Kapitel dieser Arbeit
gegeben, um daraufhin die während der Compiler-Programmierung gemachten Erfahrungen
im Hinblick auf die Zielsetzung des Praxisprojekts zu erörtern. Die Arbeit schließt mit
einem Ausblick auf mögliche Ansatzpunkte zur Verbesserung und Weiterentwicklung des
Compilers.
Rückblick
In der Einleitung wurden die Motivation zur näheren Beschäftigung mit dem Compilerbau und die mit der Entwicklung des Compilers verfolgten Ziele definiert. Zudem wurden
die einzelnen Compiler-Phasen kurz vorgestellt sowie ein Beispielprogramm und dessen
Übersetzung gezeigt.
Das zweite Kapitel stellte die Quellsprache MiniJavaOO bzw. die Zielmaschine JVM vor
und erörterte die Auswahl von Hilfsmitteln zur Programmierung. Es folgte ein Überblick über die Organisation des Entwicklungsprozesses mit Praktiken der agilen Methode
Scrum.
Design und Implementierung des Compilers waren Thema des dritten Kapitels. Es wurden
Ausschnitte aus dem Quellcode des Compilers besprochen und die Zwischenergebnisse der
einzelnen Compiler-Phasen anhand der MiniJavaOO-Beispiele Binärbaum-Programm und
Ackermann-Programm beschrieben.
Im letzten Kapitel der Arbeit wurde schließlich die testgetriebene Entwicklung des Compilers betrachtet, die zunächst mit Hilfe der Grammatik-Testsuite gUnit und dann mit einer
Kombination aus StringTemplate und jUnit erfolgte.
Ziele und Erfahrungen
In der Einleitung wurden drei Ziele formuliert, die nun im Hinblick auf die während der
Entwicklung des Compilers gemachten Erfahrungen wieder aufgegriffen werden:
Weiterentwicklung einer Programmiersprache Die prozedurale Sprache MiniJavaFunProc wurde zur objektorientierten Programmiersprache MiniJavaOO weiterentwickelt.
Im Fokus stand dabei die Kompatibilität zu Java, so daß die entsprechenden Sprachspezifikationen in [3] und [11] die Grundlage für die Spezifikation aus Anhang A
legten.
87
5 Zusammenfassung und Ausblick
Rückblickend hätte eine weniger vorsichtige Erweiterung der Sprache sowohl die
Sprachspezifikation als auch die Implementierung des Compilers an vielen Stellen
vereinfacht. Zwei Beispiele zur Verdeutlichung:
Die Syntax beschränkt Konstanten auf den Typ Integer. Zur Initialisierung ist ein
Integer-Literal oder der Wert eines einfachen Namens erlaubt. Variablen dürfen alternativ eine Objektreferenz aufnehmen und erlauben bei der Initialisierung ein IntegerLiteral, den Wert eines einfachen Namens oder den Aufruf eines Konstruktors. Wären
Konstanten nicht im Typ beschränkt und, so wie bei der Zuweisung, beliebige Ausdrücke bei der Initialisierung zugelassen, und wäre der Aufruf des Konstruktors auch
ein Ausdruck, so wären sowohl die Syntax-Regeln als auch die Regeln der statischen
Semantik wesentlich kompakter.
Weiterhin sollte in der Syntax nicht zwischen Funktionen und Prozeduren unterschieden werden. Eine einfache semantische Bedingung, die sicherstellt, daß eine nichtvoid Methode als letzte Anweisung ein Return-Statement enthält, vereinfachte die
Grammatik von MiniJavaOO zusätzlich.
Realisierung eines Mehrphasen-Compilers Die Entscheidung, den Compiler mit Hilfe
von ANTLR zu programmieren, hat sich als richtig erwiesen. ANTLR bietet ein
durchgängiges Framework zur Entwicklung eines typischen Mehrphasen-Compilers:
• Generatoren für Parser und Lexer auf der Grundlage von EBNF-Grammatiken
• Spezielle Syntax zur Transformation eines Quellprogramms in einen AST
• Generatoren für Parser zum Traversieren des AST
• Spezielle Syntax zum Erzeugen der Ausgabe des Code-Emitters
• Unterstützung für automatisierte Grammatik-Tests durch gUnit
Die Unterstützung durch die Entwicklungswerkzeuge Eclipse und ANTLRWorks ist
hilfreich. Dennoch besteht Verbesserungsbedarf. Es ist umständlich, die Fehlerbehandlung von ANTLR auf die eigenen Bedürfnisse auszurichten, und gUnit bietet
keine Möglichkeit, um mehr als zwei hintereinander geschaltete Parser zu testen.
Durch die Anwendung der agilen Methode Scrum, wenn diese auch an die Bedürfnisse
eines einzelnen Entwicklers angepaßt wurde, konnte der Entwicklungsprozeß geplant
und gesteuert werden. Die testgetriebene Entwicklung gab während der Implementierung einer neuen Phase die Sicherheit, daß die Implementierung der vorherigen Phase
erfolgreich war. Allerdings hätte die Methode noch konsequenter Anwendung finden
können. Es wurden Tests für die Ein- und Ausgaben der einzelnen Phasen, nicht aber
für Methoden der Hilfsklassen (Symboltabellen, Exceptions usw.) geschrieben. Desweiteren wurde manchmal im Eifer der Programmierung vergessen, die Tests vor der
Implementierung zu schreiben.
88
Wissensvertiefung: Java-Plattform Die Implementierung des Code-Emitters hat bestätigt, daß Java und die JVM einander sehr nahe sind. Im Vergleich zum M32-Assembler
ist die JVM auf einem sehr hohen Abstraktionsniveau angesiedelt, welches die Übersetzung statisch typisierter, objektorientierter Programmiersprachen einfach gestaltet. Der Schwerpunkt des MiniJavaOO-Compilers liegt daher auf der semantischen
Analyse.
Die Spezialisierung der JVM auf statische, objektorientierte Sprachen bringt jedoch
nicht nur Vorteile. So ist es derzeit umständlich, dynamische Sprachen wie Ruby1
oder Groovy 2 auf der JVM umzusetzen, weshalb entsprechende Erweiterungen der
JVM in Planung sind3 .
Mögliche Verbesserungen und Erweiterungen
Vor der Erweiterung des Compilers um neue Sprachfeatures sollten zunächst die Sprachspezifikation vereinfacht und der Compiler entsprechend angepaßt werden. Weiterhin könnte sich die Parser-Grammatik zum besseren Verständnis des Compiler-Quellcodes näher an
den Bezeichnungen in der Sprachspezifikation orientieren. Eine weitere Vereinfachung des
Compilers kann durch eine Überarbeitung der abstrakten Syntax hin zu einer noch kompakteren Darstellung erreicht werden.
Nach der Vereinfachung des Compilers kann die Sprache um eine import-Anweisung erweitert werden, um so insbesondere die Benutzung der String- und Collection-Klassen der
JVM zu ermöglichen. Voraussetzung sind die Ausweitung der Zugriffsschutzprüfung auf
Klassen und die Ergänzung der Symboltabellen um Informationen zu den importierten
Klassen. Letzteres kann mit Hilfe der Reflection-API umgesetzt werden. Es ist zusätzlich
zu prüfen, ob die Einführung von Interfaces eine weitere nötige Voraussetzung ist.
Ebenso von Interesse sind die Weiterentwicklung der Klassen CompilerTestTool2 und
SourceTemplate zu einem allgemeinen Compiler-Testframework auf der Basis von jUnit
und StringTemplate wie auch die Implementierung des MiniJavaOO-Compilers mit einer
Programmiersprache wie Scala, die einen LL(∗)-Parser direkt in die Syntax der Sprache
integriert. Hierdurch wird der Sprachmix aus Java- und ANTLR-Code vermieden, und die
Parser-Generator-Läufe werden überflüssig.
1
Siehe http://jruby.codehaus.org/
Siehe http://groovy.codehaus.org/
3
Siehe http://www.jcp.org/en/jsr/detail?id=292
2
89
Literaturverzeichnis
[1] H. Abelson, G. J. Sussman, J. Sussman: Struktur und Interpretation von Computerprogrammen: Eine Informatik-Einführung. Springer, 4. Auflage, 2001.
[2] W.-G. Bleek, H. Wolf: Agile Softwareentwicklung. dpunkt.verlag, 2008.
[3] J. Gosling, B. Joy, G. L. Steele: The Java Language Specification. Addison-Wesley
Longman, 3., aktualisierte Auflage, 2005.
[4] T. Lindholm, F. Yellin: The Java Virtual Machine Specification. Addison-Wesley
Longman, 2., aktualisierte Auflage, 1999.
[5] J. Meyer, T. Downing: JAVA Virtual Machine. O’Reilly, 1997.
[6] J. Meyer: JASMIN USER GUIDE. Handbuch auf der Jasmin-Homepage, 1996. Online: http://jasmin.sourceforge.net/guide.html, abgerufen: 1.2.2009.
[7] T. Parr: LL(*)-Parsing. Vortrag während der ANTLR-Workshops 2005. Online: http://www.antlr.org/workshop/ANTLR2005/presentations/LL-Star.ppt, abgerufen: 1.2.2009.
[8] T. Parr: The Definitive ANTLR Reference. The Pragmatic Programmers, 2007.
[9] T. Parr: Translators Should Use Tree Grammars. Artikel auf der ANTLR-Homepage,
2004. Online: http://antlr.org/article/1100569809276/use.tree.grammars.tml, abgerufen: 10.2.2009.
[10] M. Rieke: Entwicklung eines OO-Compilers für M32-Assemblercode und Erweiterung
des M32-Simulators. FH Aachen, SS 2009, Bachelorarbeit.
[11] R. Stärk, J. Schmid, E. Börger: Java and the Java Virtual Maschine. Springer-Verlag,
2001.
[12] P. Terry: Compiling with C# and Java. Addison-Wesley, 2004.
[13] A. Tripp: Manual Tree Walking Is Better Than Tree Grammars. Artikel auf der
ANTLR-Homepage.
Online: http://www.antlr.org/article/1170602723163/treewalkers.html, abgerufen:
10.2.2009.
[14] N. Wirth: Grundlagen und Techniken des Compilerbaus. Oldenbourg, 2., bearbeitete
Auflage, 2008.
90
A Syntax und statische Semantik von
MiniJavaOO
Verfasser: Michael Pasdziernik und Manuel Rieke
Die folgenden Erläuterungen stellen die Syntax von MiniJavaOO sowie die von einem Compiler zu prüfenden Bedingungen der statischen Semantik vor. Die angegebenen EBNFProduktionen sollen die Syntax der Sprache möglichst leicht verständlich machen und entsprechen nicht den Parser-Grammatiken der entwickelten Compiler. Zusätzlich wird in
Anhang A.9 die komplette Syntax der Sprache als Diagramm dargestellt.
Als Grundlage für die Beschreibung der Sprache soll das Beispiel in Listing A.1 dienen.
Dieses Beispiel deckt nicht alle, jedoch die wichtigsten Einzelheiten von MiniJavaOO ab.
Listing A.1: Beispielprogramm
1
2
3
int iGlobal = 23;
int iGlobal2;
BspClass myGlobalClass;
4
5
func globalFunction(int iParameter){
6
return 10 + iParameter * 3;
7
8
}
9
10
11
12
void globalProcedure(int iValue){
iGlobal2 = iValue;
}
13
14
15
class BspClass extends Object{
protected int a;
16
17
18
19
20
21
22
23
24
BspClass(){
this.a = 12;
}
public void test(){
this.test2();
}
private void test2(int iVar){
print(iVar);
91
A Syntax und statische Semantik von MiniJavaOO
}
25
26
}
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
main{
int iLocal;
while iLocal < iGlobal
iLocal++;
if iGlobal >= 23
print(3);
else{
print(4);
}
/* Dies
ist ein mehrzeiliger
Kommentar */
BspClass myClass = new BspClass();
// Dies ein einzeiliger Kommentar
myClass.test();
myClass = new Class2();
myClass.test();
myClass.test2(iLocal);
46
47
}
48
49
50
51
52
class Class2 extends BspClass{
Class2{
this.a = 1;
53
}
public void test(){
print(2)
}
54
55
56
57
58
}
A.1 Programmstruktur
Ein MiniJavaOO-Programm besteht aus der Definition globaler Variablen (Abschnitt A.5)
und globaler Methoden (Abschnitt A.6) sowie einer main-Methode, mit der die Ausführung
des Programms beginnt. Um die Kompatibilität zu MiniJavaFunProc zu erhalten, kann
statt der main-Methode auch ein einzelnes Statement angegeben werden (Abschnitt A.8).
Selbstdefinierte Typen in Form von Klassen (Abschnitt A.2) können vor oder nach der
main-Methode definiert werden.
92
A.2 Klassen
Syntax A.1 (MiniJavaOO-Programm)
Program → GlobalVariables Class∗ GlobalMethods
GlobalVariables → ( FinalVariable | Variable )∗
GlobalMethods → Method ∗
ProgramStart → ( main Block ) | Statement
ProgramStart
Class∗
Statische Semantik A.2 (MiniJavaOO-Programm)
(1) Die main-Methode kann nicht explizit aufgerufen werden.
(2) Klassen haben keinen Zugriff auf globale Variablen und globale Methoden.
Kommentare können wie in Java als Zeilen- oder Blockkommentar notiert werden.
Beispiel
Das Beispiel aus Listing A.2 ist äquivalent zu dem Beispiel aus Listing A.3.
Listing A.2: main-Methode zum Programmstart
1
int a = 3;
2
3
4
5
main{
a = 4;
}
Listing A.3: Einzelnes Statement zum Programmstart
1
int a = 3;
2
3
a = 4;
Am Beispiel aus Listing A.1 kann man sehr gut die sonstige Programmstruktur von MiniJavaOO erkennen. Kommentare werden wie in Java geschrieben, ein Beispiel wird in Listing
A.1 gegeben.
A.2 Klassen
Bei der Definition einer Klasse werden nach dem Klassennamen (Abschnitt A.3) und der
Unterklasse zunächst die Instanzvariablen (Abschnitt A.5), dann der Konstruktor und
schließlich die Instanzmethoden (Abschnitt A.6) angegeben. Instanzvariablen und Instanzmethoden sind nach dem Grad des Zugriffsschutzes sortiert. MiniJavaOO kennt keine statischen Methoden oder Variablen innerhalb von Klassen.
93
A Syntax und statische Semantik von MiniJavaOO
Syntax A.3 (Klassen)
Class → class ClassName extends ClassName ClassDefinition
ClassDefinition → { InstanceVariables Constructor InstanceMethods
InstanceVariables → ( public ( FinalVariable | Variable ) )∗
( protected ( FinalVariable | Variable ) )∗
( private ( FinalVariable | Variable ) )∗
InstanceMethods → ( public Method )∗
( protected Method )∗
( private Method )∗
}
Statische Semantik A.4 (Klassen)
(1) Zyklenfreiheit der Ableitung: Eine Klasse darf sich nicht direkt oder indirekt selbst
erweitern (vgl. [11], S. 48).
Zugriffsschutz
Die Sichtbarkeit von Instanzmethoden und Instanzvariablen wird durch die Zugriffsmodifizierer eingeschränkt. Im Vergleich zu Java (vgl. [3], S. 138 ff.) ist der Zugriffsschutz in
MiniJavaOO aufgrund des Fehlens von Paketen einfacher definiert. Es gibt keinen DefaultZugriff.
Statische Semantik A.5 (Zugriffsschutz)
(1) Eine als public deklarierte Instanzvariable oder Instanzmethode ist in globalen
Methoden, der main-Methode und allen Klassen sichtbar.
(2) Eine als protected deklarierte Instanzvariable oder Instanzmethode ist in der sie
definierenden Klasse und in von dieser abgeleiteten Klassen sichtbar.
(3) Eine als private deklarierte Instanzvariable oder Instanzmethode ist in der sie
definierenden Klasse sichtbar.
Konstruktor und Konstruktoraufruf
Eine MiniJavaOO-Klasse hat einen parameterlosen Konstruktor. Beim Aufruf des Konstruktors wird zunächst der Konstruktor der Basisklasse aufgerufen und daraufhin der
Konstruktorrumpf (Abschnitt A.8) abgearbeitet.
Syntax A.6 (Konstruktordefinition und Konstruktoraufruf )
Constructor → ClassName () Block
ConstructorCall → new ClassName ()
94
A.2 Klassen
Statische Semantik A.7 (Konstruktordefinition und Konstruktoraufruf )
(1) In der Konstruktordefinition muß der Name des Konstruktors dem Klassennamen
entsprechen (vgl. [11], S. 73).
(2) Beim Konstruktoraufruf muß ClassName eine im Programm definierte Klasse oder
Object referenzieren.
Beispiel
Listing A.4 zeigt ein Minimalbeispiel einer Klasse. Diese Beispielklasse enthält in diesem
Fall nur die von Object geerbten Attribute und Routinen, besitzt jedoch keine eigenen.
Listing A.4: Minimalbeispiel einer Klasse
1
2
3
class SimpleClass extends Object
{
SimpleClass(){
4
}
5
6
7
}
Das globale Beispiel aus Listing A.1 enthält zwei weitere Beispiele zur Deklarierung einer
Klasse. Das erste Beispiel befindet sich in den Zeilen 14 bis 26:
14
15
class BspClass extends Object{
protected int a;
16
BspClass(){
this.a = 12;
}
public void test(){
this.test2();
}
private void test2(int iVar){
print(iVar);
}
17
18
19
20
21
22
23
24
25
26
}
Das zweite Beispiel ist in den Zeilen 50 bis 61 zu sehen:
50
51
52
class Class2 extends BspClass{
Class2{
this.a = 1;
53
54
}
95
A Syntax und statische Semantik von MiniJavaOO
public void test(){
print(2)
}
55
56
57
58
}
A.3 Typen, Literale, Bezeichner und Namen
MiniJavaOO kennt die grundlegenden Typen Integer mit 32-Bit Breite und Objektreferenz.
Integer-Literale (NUMBER) werden in positiver Dezimalschreibweise angegeben. Objektreferenzen können auf Objekte der Klasse Object und von Object abgeleitete und im
Programm definierte Klassen zeigen. Die Basisklasse Object enthält keine Methoden oder
Attribute.
Syntax A.8 (Typangaben und Literale)
Type → int | ClassName
NUMBER → [1−9] [0−9]*
Statische Semantik A.9 (Typangabe)
(1) Bei einer Typangabe muß ClassName eine im Programm definierte Klasse oder
Object referenzieren (vgl. [11], S. 47 und S. 51).
Klassennamen bestehen aus einem Bezeichner, beginnend mit einem Großbuchstaben (UPPERIDENT ). Namen (SimpleName) für Variablen und Methoden beginnen mit einem
Kleinbuchstaben (LowerIdent).
Syntax A.10 (Bezeichner und Namen)
ClassName → UPPERIDENT
UPPERIDENT → [A−Z] ( [0−9] | [a−z] | [A−Z] )∗
SimpleName → LOWERIDENT
LOWERIDENT → [a−z] ( [0−9] | [a−z] | [A−Z] )∗
Beispiel
Im globalen Beispiel aus Listing A.1 beginnen Attribute sowie lokale und globale Variablen
alle mit kleinem Buchstaben. Zeilen 1 bis 3:
1
2
3
int iGlobal = 23;
int iGlobal2;
BspClass myGlobalClass;
96
A.4 Referenzieren von Variablen und Methoden
Zeile 15:
15
protected int a;
A.4 Referenzieren von Variablen und Methoden
Der Zugriff auf Variablen und Methoden erfolgt über ihren Namen. Bei globalen und lokalen Variablen sowie globalen Methoden ist dies der einfache Name (Abschnitt A.3). Bei
Instanzvariablen und Instanzmethoden wird der qualifizierte Name angegeben. Er besteht
aus einer Sequenz einfacher Namen, die durch den Punktoperator getrennt werden. (Vgl. [3],
S. 113 ff.)
Syntax A.11 (Namen)
Name → this | SimpleName | QualifiedName
QualifiedName → ( this | SimpleName ) ( . SimpleName
)+
Statische Semantik A.12 (Namen)
(1) this ist nur innerhalb einer Klasse ein gültiger Name.
(2) Ein einfacher Name innerhalb einer Klasse muß eine lokale Variable referenzieren.
(3) Ein einfacher Name innerhalb einer globalen Methode muß eine lokale oder globale
Variable oder eine globale Methode referenzieren.
(4) Die erste Komponente eines qualifizierten Namens innerhalb einer globalen Methode
muß eine lokale oder globale Variable referenzieren.
(5) Die erste Komponente eines qualifizierten Namens innerhalb einer Klasse muß this
oder eine lokale Variable referenzieren.
(6) Eine mittlere Komponente eines qualifizierten Namens muß eine Instanzvariable referenzieren, die in der Klasse oder einer Oberklasse der vorherigen Komponente
deklariert und im Kontext des Zugriffs entsprechend den Zugriffsregeln (siehe Abschnitt A.2) sichtbar ist.
(7) Die Instanzvariable einer mittleren Komponente muß ein Referenztyp sein.
(8) Die letzte Komponente eines qualifizierten Namens muß eine Instanzvariable oder
Instanzmethode referenzieren, die in der Klasse oder einer Oberklasse der vorherigen
Komponente deklariert und im Kontext des Zugriffs entsprechend den Zugriffsregeln
(siehe Abschnitt A.2) sichtbar ist.
(9) Eine lokale Variable als Bestandteil eines Namens muß im Scope ihrer Deklaration
sein (siehe Abschnitt A.5, vgl. [11], S. 35).
97
A Syntax und statische Semantik von MiniJavaOO
Beispiel
Das globale Beispiel aus Listing A.1 zeigt verschiedene Variablenreferenzierungen, so etwa
in Zeile 31:
iLocal++;
31
In dieser Zeile wird iLocal referenziert und der Wert dieser Variablen um 1 erhöht. Eine
Methode wird beispielsweise in der Main-Methode referenziert. Zeilen 42 bis 45:
myClass.test();
myClass = new Class2();
myClass.test();
myClass.test2(iLocal);
42
43
44
45
A.5 Variablen
Ein Deklarations-Statement kann mehrere Einzeldeklarationen enthalten. Als final deklarierte Variablen sind auf den Typ Integer beschränkt und müssen bei der Deklaration
mit dem Wert einer anderen Variablen (Abschnitt A.4) oder einem Integer-Literal (Abschnitt A.3) initialisiert werden. Nicht als final deklarierte Variablen können außerdem
durch einen Konstruktoraufruf (Abschnitt A.2) initialisiert werden.
Syntax A.13 (Deklaration von Variablen)
FinalVariable → final int SingleFinalVariable ( , SingleFinalVariable
SingleFinalVariable → SimpleName = NUMBER
Variable → Type SingleVariable ( , SingleVariable )∗ ;
SingleVariable → SimpleName ( = ( NUMBER | ConstructorCall ) )?
)∗ ;
Variablen werden automatisch mit 0 oder einer null-Referenz initialisiert.
Statische Semantik A.14 (Deklaration von Variablen)
(1) Die Deklaration einer globalen Variablen darf eine bereits deklarierte globale Variable
nicht überdecken.
(2) Die Deklaration einer lokalen Variablen darf eine bereits deklarierte lokale Variable1
nicht überdecken (vgl. [11], S. 35 und S. 52).
(3) Die Deklaration einer Instanzvariablen darf eine bereits deklarierte Instanzvariable
oder Instanzmethode der gleichen Klasse nicht überdecken (vgl. [11], S. 53).
1
Der Begriff lokale Variable beinhaltet auch Methoden-Parameter.
98
A.6 Methoden
(4) Die Deklaration einer Instanzvariablen darf eine Instanzvariable einer Superklasse
überdecken (vgl. [11], S. 53).
(5) Ist die deklarierte Variable vom Typ Integer, so muß der Initialisierungswert vom
Typ Integer sein.
(6) Ist die deklarierte Variable ein Referenztyp, so muß der Initialisierungswert eine Referenz auf ein Objekt der gleichen Klasse oder einer Unterklasse sein.
Statische Semantik A.15 (Scope von Variablen)
(1) Eine globale Variable ist in allen globalen Methoden sowie der main-Methode sichtbar.
(2) Der Scope einer lokalen Variablen reicht von der Deklaration bis zum Ende des Blocks,
in dem sie deklariert wurde. Dies schließt auch weitere, in den Block verschachtelte
Blöcke ein.
(3) Lokale Parametervariablen sind im gesamten Methodenblock sichtbar.
Beispiel
2
int iGlobal2;
In Zeile 2 des globalen Beispiels (Listing A.1) wird eine globale Variable definiert und
automatisch mit dem Wert 0 initialisiert.
Ein Beispiel für eine finale Variable, auch Konstante genannt, ist folgender Programmcode:
1
final int iFinal = 4;
Diese Konstante wird innerhalb ihres Scopes genauso referenziert wie eine normale Variable.
A.6 Methoden
MiniJavaOO unterscheidet die Methodentypen Funktion und Prozedur. Prozedurdefinitionen werden mit dem Schlüsselwort void eingeleitet, Funktionsdefinitionen mit dem
Typ des Rückgabewerts. Aus Kompatibilitätsgründen zu MiniJavaFunProc kann eine Funktionsdefinition auch mit dem Schlüsselwort func beginnen, folgt daraufhin keine Typangabe, so ist die Funktion vom Typ Integer. Funktionen liefern als letztes Statement ihres
Blocks (Abschnitt A.8) einen Integer-Wert oder eine Objektreferenz zurück. Die Aufrufparameter zählen zu den lokalen Variablen einer Methode.
99
A Syntax und statische Semantik von MiniJavaOO
Syntax A.16 (Definition und Aufruf von Methoden)
Method → Procedure | Function
Procedure → void SimpleName ( Parameters? ) Block
Parameters → Type SimpleName ( , Type SimpleName )∗
Function → FunctionStart SimpleName ( Parameters? ) FunctionBlock
FunctionStart → func | ( func? Type )
FunctionBlock → { BlockElement∗ return Expression ; }
MethodInvocation → Name ( Arguments? )
Arguments → Expression ( , Expression )∗
Während Funktionsaufrufe immer Teil eines Ausdrucks sind (Abschnitt A.7), treten Prozeduraufrufe als Statement (Abschnitt A.8) auf. MiniJavaOO erlaubt das Überlagern von
Instanzmethoden durch die Neudefinition in einer abgeleiteten Klasse. Die Überladung von
Methoden ist nicht möglich.
Statische Semantik A.17 (Methodendefinition)
(1) Die Definition einer globalen Methode darf nicht eine bereits definierte globale Variable oder globale Methode überdecken.
(2) Die Definition einer Instanzmethode darf nicht eine in dieser oder in einer Superklasse
definierte Instanzvariable überdecken.
(3) Die Definition einer Instanzmethode darf eine Instanzmethode einer Superklasse mit
gleichem Namen und gleicher Signatur überlagern (vgl. [11], S. 55).
(4) Die überlagernde Methode darf im Vergleich zur überlagerten Methode die Sichtbarkeit nicht weiter einschränken (vgl. [11], S. 55).
(5) Ist die Methode vom Typ Integer, so muß der Ausdruck des return-Statements
einen Integer-Wert liefern.
(6) Ist die Methode vom Referenztyp, so muß der Ausdruck des return-Statements eine
Objektreferenz derselben oder einer abgeleiteten Klasse liefern.
(7) Die Namen der Parametervariablen sind paarweise verschieden (vgl. [11], S. 51).
(8) Innerhalb eines Methodenaufrufs muß Name eine Methode referenzieren.
(9) Beim Methodenaufruf müssen Typ und Anzahl der Argumente mit der Signatur
der Methode übereinstimmen. Ist der Typ eines Parameters eine Klasse, so muß das
entsprechende Argument Objektreferenzen der gleichen oder einer abgeleiteten Klasse
referenzieren.
(10) Das Schlüsselwort func darf bei globalen Methodendeklarationen nicht weggelassen
werden.
100
A.7 Ausdrücke
Beispiel
Das Globalbeispiel (Listing A.1) beinhaltet eine globale Funktion und eine globale Prozedur
(Zeilen 5 bis 12):
5
func globalFunction(int iParameter){
6
return 10 + iParameter * 3;
7
8
}
9
10
11
12
void globalProcedure(int iValue){
iGlobal2 = iValue;
}
Wie man erkennen kann, werden Routinenparameter äquivalent zu Java angegeben.
A.7 Ausdrücke
Ein Ausdruck kann eine Objektreferenz oder einen Integer-Wert zurückliefern. Atomare
Bestandteile können Methodenaufrufe (Abschnitt A.6), Variablenwerte (Abschnitt A.4)
und Integer-Literale (Abschnitt A.3) sein. Zusammengesetzte Ausdrücke sind immer arithmetische Berechnungen. Neben der Addition, Subtraktion, Multiplikation und Division ist
die Restwertberechnung möglich. Die Inkrement- und Dekrement-Operation kann auch als
einzelnes Statement (Abschnitt A.8) ausgeführt werden.
Syntax A.18 (Ausdrücke)
Expression →
|
|
|
|
|
ArithOperator
Increment →
Decrement →
Name
MethodInvocation
NUMBER
( Expression )
Increment | Decrement
Expression ArithOperator Expression
→ + | - | * | / | %
( ++ Name ) | ( Name ++ )
( -- Name ) | ( Name -- )
Statische Semantik A.19 (Ausdrücke)
(1) Jeder Teilausdruck eines arithmetischen Ausdrucks muß vom Typ Integer sein (vgl. [11],
S. 37).
(2) Eine innerhalb eines Ausdrucks aufgerufene Methode muß eine Funktion sein.
101
A Syntax und statische Semantik von MiniJavaOO
(3) Name muß eine Variable referenzieren.
(4) Eine zu inkrementierende oder dekrementierende Variable muß vom Typ Integer sein
(vgl. [11], S. 37).
(5) Eine zu inkrementierende oder dekrementierende Variable darf nicht final sein.
Beispiel
In Zeile 7 des Beispiels aus Listing A.1 wird eine Rechnung durchgeführt:
return 10 + iParameter * 3;
7
Dabei wird die Punkt-vor-Strich-Regel eingehalten. Der Restwert-Operator (%) wird dabei
zu den Multiplikationsoperatoren, zu denen auch die Multiplikation (*) und die Division (/)
gehören, gezählt.
A.8 Blöcke und Statements
Blöcke bestehen aus Variablendeklarationen (Abschnitt A.5) sowie Statements und können
ineinander verschachtelt werden. Statements sind die Bildschirmausgabe über print, Kontrollstrukturen, Zuweisungen, Inkrement- und Dekrement-Anweisungen (Abschnitt A.7)
sowie der Aufruf von Methoden (Abschnitt A.6).
Syntax A.20 (Blöcke und Statements)
Block → { BlockElement∗ }
BlockElement → ( Variable | FinalVariable
Statement → Block
| Print
| ControlStructure
| Assignment
| Increment | Decrement ;
| MethodInvocation ;
Print → print ( Expression ) ;
| Statement )
Das print-Statement gibt einen Wert in die Standardausgabe aus.
Statische Semantik A.21 (Blöcke und Statements)
(1) Ein mit print auszugebender Ausdruck muß vom Typ Integer sein.
(2) Beim Methodenaufruf muß die Methode eine Prozedur sein.
102
A.8 Blöcke und Statements
(3) Inkrement- und Dekrement-Anweisungen sind nur in Postfix-Schreibweise zulässig.
Kontrollstrukturen
Als Kontrollstrukturen stehen die bedingte Ausführung über if mit optionalem elseZweig und die while-Schleife zur Verfügung. Die Angabe der Bedingungen erfolgt über
einfache logische Ausdrücke.
Syntax A.22 (Kontrollstrukturen und logische Ausdrücke)
ControlStructure → IfStatement | WhileStatement
IfStatement → if Condition Statement ( else Statement
WhileStatement → while Condition Statement
Condition → Expression CondOperator Expression
CondOperator → == | != | < | > | <= | >=
)?
Statische Semantik A.23 (Conditions)
(1) Die beiden Teilausdrücke müssen vom Typ Integer sein (vgl. [11], S. 37).
Zuweisung
Mit der Zuweisung kann Variablen der Wert eines Ausdrucks (Abschnitt A.7) oder die Referenz auf ein durch einen Konstruktoraufruf erzeugtes Objekt (Abschnitt A.2) zugewiesen
werden.
Syntax A.24 (Zuweisung)
Assignment → Name
= ( Expression
| ConstructorCall ) ;
Statische Semantik A.25 (Zuweisung)
(1) Die Zuweisung an this ist nicht möglich.
(2) Name muß eine Variable referenzieren.
(3) Die Variable darf nicht final sein.
(4) Ist die Variable Name vom Typ Integer, so muß der Wert der Zuweisung vom Typ
Integer sein (vgl. [11], S. 37).
(5) Ist die Variable Name ein Referenztyp, so muß der Wert der Zuweisung eine Referenz
der gleichen Klasse oder einer Unterklasse sein (vgl. [11], S. 37).
103
A Syntax und statische Semantik von MiniJavaOO
Beispiel
Zeilen 30 bis 36 aus dem Globalbeispiel (Listing A.1) veranschaulichen den Umgang mit
Statements und Blöcken.
while iLocal < iGlobal
iLocal++;
if iGlobal >= 23
print(3);
else{
print(4);
}
30
31
32
33
34
35
36
Im Vergleich zu Java ist es wichtig zu erkennen, daß die logischen Ausdrücke in MiniJavaOO nicht in Klammern stehen dürfen. Auch existieren keine Bedingungsblöcke wie in
Java.
104
A.9 Syntaxdiagramme
A.9 Syntaxdiagramme
Program
² GlobalMethods
¯
GlobalVariables
¯
¯°
±² Class
±
°
²
°
± ProgramStart
²
¯
¯°
±² Class
±
°
GlobalVariables
²
¯
±²¯ FinalVariable
²¯°
± Variable
°
±
°
GlobalMethods
²
¯
±² Method
±
¯°
°
ProgramStart
²
¯
¯ main
Block
±
°
± Statement
²
°
Class
²
¯
class °ClassName
±
²
¯
extends °ClassName
±
ClassDefinition
ClassDefinition
²¯
{
InstanceVariables
±°
Constructor
InstanceMethods
²¯
}
±°
105
A Syntax und statische Semantik von MiniJavaOO
InstanceVariables
¯
²
¯
±² public
¯ FinalVariable
±
°
± Variable
²¯
²¯°
°
±
°
²
°
²¯
±¯
²
¯
±² protected
¯ FinalVariable
±
°
± Variable
²¯°
°
±
°
²
°
²
±¯
²
¯
±² private
¯ FinalVariable
±
°
± Variable
±
106
²¯°
°
°
A.9 Syntaxdiagramme
InstanceMethods
¯
²¯
²
¯
±² public
Method
±
°
¯°
±
°
²
°
±¯
²¯
²
¯
±² protected
Method
±
°
¯°
±
°
²
°
±¯
²
¯
±² private
Method
±
°
±
²
¯°
°
Constructor
ClassName
²¯
²¯
(
Block
)
±°
±°
ConstructorCall
²
¯
new
ClassName
± °
²¯
²¯
(
)
±°
±°
Name
²
¯
¯ this
±
°
²
± SimpleName
°
± QualifiedName
°
QualifiedName
²
¯
¯ this
±
°
± SimpleName
²¯
²² .
SimpleName
±°
¯
°±
°
FinalVariable
²
¯
²
¯
final °
int
SingleFinalVariable
±
± °
²¯
²;
±°
¯
²¯
±² ,
SingleFinalVariable
±°
±
¯°
°
107
A Syntax und statische Semantik von MiniJavaOO
SingleFinalVariable
SimpleName
²¯
NUMBER
=
±°
Variable
Type
²¯
²;
±°
¯
SingleVariable
²¯
±² ,
SingleVariable
±°
±
¯°
°
SingleVariable
SimpleName
²¯
¯
=
±°
²
±¯ NUMBER
²°
± ConstructorCall
°
Method
¯ Procedure
²
± Function
°
Procedure
²
¯
void °SimpleName
±
²¯
¯
(
±°
± Parameters
²¯
²)
Block
±°
°
Parameters
Type
SimpleName
¯
²¯
±² ,
Type
±°
²
SimpleName
±
¯°
°
Function
FunctionStart
SimpleName
²¯
¯
(
±°
± Parameters
FunctionStart
²
¯
¯ func
±
°
±¯
± func
108
²
² Type
°
°
²¯
²)
FunctionBlock
±°
°
A.9 Syntaxdiagramme
FunctionBlock
²¯
¯
{
±°
±² BlockElement
±
²
¯
² return
Expression
±
°
¯°
²¯
²¯
;
}
±°
±°
°
MethodInvocation
Name
²¯
¯
(
±°
± Arguments
²¯
²)
±°
°
Arguments
Expression
¯
²¯
±² ,
Expression
±°
±
²
¯°
°
Expression
¯ Name
²
± MethodInvocation
°
± NUMBER
°
²¯
±(
Expression
±°
± Increment
²¯
°
)
±°
°
± Decrement
± Expression
°
ArithOperator
Expression
°
ArithOperator
²¯
¯+
²
±°
²¯
±°
±°
²¯
±*
°
±°
²¯
±/
°
±°
²¯
±%
°
±°
109
A Syntax und statische Semantik von MiniJavaOO
Increment
² ¯
¯ ++
²
Name
± °
² ¯
± Name
°
++
± °
Decrement
² ¯
¯- ²
Name
± °
² ¯
± Name
°
- ± °
Block
²¯
¯
{
±°
±² BlockElement
±
²¯
²}
±°
¯°
°
BlockElement
¯ Variable
²
± FinalVariable
°
± Statement
°
Statement
¯ Block
²
± Print
°
± ControlStructure
°
± Assignment
°
± Increment
± Decrement
²¯
°
;
±°
²¯
°
;
±°
± MethodInvocation
²¯
°
;
±°
Print
print
²¯
(
Expression
±°
ControlStructure
110
¯ IfStatement
²
± WhileStatement
°
²¯
²¯
)
;
±°
±°
A.9 Syntaxdiagramme
IfStatement
² ¯
if
Condition
± °
Statement
²
¯
± else
Statement
±
°
WhileStatement
²
¯
while °Condition
±
Condition
Expression
¯
²
°
Statement
CondOperator
Expression
CondOperator
² ¯
¯ ==
²
± °
² ¯
± !=
°
± °
²¯
±<
°
±°
²¯
±>
°
±°
² ¯
± <=
°
± °
² ¯
± >=
°
± °
Assignment
Name
²¯
¯ Expression
=
±°
± ConstructorCall
²¯
²;
±°
°
111
B Grammatiken
B.1 Lexer
Schlüsselwörter:
FINAL → final
INT → int
VOID → void
FUNC → func
RETURN → return
IF → if
ELSE → else
WHILE → while
PRINT → print
CLASS → class
EXTENDS → extends
PUBLIC → public
PROTECTED → protected
PRIVATE → private
NEW → new
MAIN → main
THIS → this
Operatoren:
PO → .
NE → !=
EQ → ==
LE → <=
GE → >=
LT → <
GT → >
ADD → +
SUB → -
112
B.1 Lexer
MULT → *
DIV → /
MOD → %
INC → ++
DEC → -ASSIGN → =
Struktur:
RLB → (
RRB → )
CLB → {
CRB → }
COMMA → ,
ENDSTMNT → ;
Literale und Bezeichner:
NUMBER → 0 | ( [1−9] [0−9]* )
UPPERIDENT → [A−Z] ( [0−9] | [a−z] | [A−Z] )∗
LOWERIDENT → [a−z] ( [0−9] | [a−z] | [A−Z] )∗
CLASSIDENT → UPPERIDENT
IDENT → LOWERIDENT
113
B Grammatiken
B.2 Parser
Program → ConstDecl ∗ VarDecl ∗
{enableOO}?=> ClassDef ∗
Procedure∗ Function∗
MainFunction
{enableOO}?=> ClassDef ∗
MainFunction → {enableExtensions}?=> MAIN Block
| Statement
ConstDecl → FINAL ConstDeclTyp ConstAssign ( COMMA ConstAssign
ConstDeclTyp → INT
ConstAssign → IDENT ASSIGN ( Identifier | NUMBER )
)∗ ENDSTMNT
VarDecl → VarDeclTyp VarAssign ( COMMA VarAssign )∗ ENDSTMNT
VarDeclTyp → INT
| {enableOO}?=> CLASSIDENT
VarAssign → IDENT ( ASSIGN ( IDENT | NUMBER | {enableOO}?=> ConstructorCall ) )?
LocalVarDecl → VarDecl
LocalConstDecl → ConstDecl
Procedure → VOID IDENT RLB ParamDecl ? RRB ProcBlock
ProcBlock → {!declEverywhere}?=> CLB LocalConstDecl ? LocalVarDecl ? Statement
| {declEverywhere}?=> Block
CRB
Function → FuncStart IDENT RLB ParamDecl ? RRB FuncBlock
FuncStart → FUNC
| {enableExtensions}?=> FUNC? ( INT | {enableOO}?=> CLASSIDENT )
FuncBlock → {!declEverywhere}?=> CLB LocalConstDecl ? LocalVarDecl ? Statement
RETURN Expr ENDSTMNT CRB
| {declEverywhere}?=> CLB BlockElement∗ RETURN Expr ENDSTMNT CRB
ParamDecl → SingleParamDecl ( COMMA SingleParamDecl
SingleParamDecl → SingleParamDeclTyp IDENT
SingleParamDeclTyp → INT
| {enableOO}?=> CLASSIDENT
Block → CLB BlockElement∗ CRB
BlockElement → Statement
| {declEverywhere}?=> LocalConstDecl
114
)∗
B.2 Parser
| {declEverywhere}?=> LocalVarDecl
Expr → SumExpr
SumExpr → MultExpr ( ( ADD | SUB ) MultExpr )∗
MultExpr → AtomExpr ( MultOp AtomExpr )∗
MultOp → MULT | DIV | {enableExtensions}?=> MOD
AtomExpr → Identifier ( FunProcCall | {enableExtensions}?=> PostfixIncDec
| {enableExtensions}?=> PraefixIncDec Identifier
| NUMBER
| RLB Expr RRB
)?
PostfixIncDec → INC | DEC
PraefixIncDec → INC | DEC
Identifier → {!enableOO}?=> IDENT
| {enableOO}?=> ( IDENT | THIS ) ( IdentifierPart+ )?
IdentifierPart → PO IDENT
Statement → IdentifierStatement ENDSTMNT
| enableExtensions?=> PraefixIncDec Identifier ENDSTMNT
| Print ENDSTMNT
| Block
| IfBlock
| WhileBlock
IdentifierStatement → Identifier ( Assignment | FunProcCall | PostfixIncDec )
Assignment → ASSIGN ( Expr | {enableOO}?=> ConstructorCall )
FunProcCall → RLB ( Expr ( COMMA Expr )∗ )? RRB
Print → PRINT RLB Expr RRB
IfBlock → IF Condition Statement ( ELSE Statement )?
WhileBlock → WHILE Condition Statement
Condition → Expr CompOp Expr
CompOp → NE | EQ | LE | GE | LT | GT
ClassDef → CLASS CLASSIDENT EXTENDS
CLB
( PUBLIC FieldDecl )∗
( PROTECTED FieldDecl )∗
( PRIVATE FieldDecl )∗
PUBLIC? Constructor
( PUBLIC Method )∗
( PROTECTED Method )∗
CLASSIDENT
115
B Grammatiken
( PRIVATE Method )∗
CRB
FieldDecl → VarDecl | ConstDecl
Method → Function | Procedure
Constructor → CLASSIDENT RLB RRB Block
ConstructorCall → NEW CLASSIDENT RLB RRB
116
B.3 AST-Parser
B.3 AST-Parser
Program → ClassDef +
FieldDecl → ^( VAR ^( MODIFIER FINAL? AccessModifier ) DeclTyp
SIMPLENAME VarDeclValue? )
VarDecl → ^( VAR ^( MODIFIER FINAL? ) DeclTyp SIMPLENAME
VarDeclValue? )
DeclTyp → INT | SIMPLENAME
VarDeclValue → Name | NUMBER | ConstructorCall
AccessModifier → PUBLIC | PROTECTED
| PRIVATE
Method → ^( METHOD ^( MODIFIER AccessModifier ) MethodTyp
SIMPLENAME ^( PARAMS ParamDecl ∗ Block )
MethodTyp → INT | SIMPLENAME | VOID
ParamDecl → ^( PARAM
ParamDeclTyp SIMPLENAME )
ParamDeclTyp → INT | SIMPLENAME
Block → ^( BLOCK blockElement∗ )
BlockElement → Statement | VarDecl
Expr →
|
|
|
|
|
|
|
|
|
^(ADD Expr
^(SUB Expr
^(MULT Expr
^(DIV Expr
^(MOD Expr
NUMBER
Name
Invocation
PostfixIncDec
PraefixIncDec
Expr
Expr
Expr
Expr
Expr
)
)
)
)
)
PostfixIncDec → ^( POSTFIX IncDec Name )
PraefixIncDec → ^( PRAEFIX IncDec Name )
IncDec → INC | DEC
Name → SIMPLENAME
| THIS
| ^( QUALIFIEDNAME
THIS? SIMPLENAME+
)
Statement → Assignment
117
B Grammatiken
|
|
|
|
|
|
|
|
Invocation
PostfixIncDec
PraefixIncDec
Print
MethodReturn
Block
IfBlock
WhileBlock
Assignment → ^( ASSIGN Name Expr ? ConstructorCall ? )
Invocation → ^( INVOKE Name ^( ARGLIST
Expr ∗ ) )
Print → ^( PRINT Expr )
MethodReturn → ^( RETURN Expr )
IfBlock → ^( IF Condition Statement Statement? )
WhileBlock → ^( WHILE Condition Statement )
Condition → ^( CompOp Expr Expr )
CompOp → NE | EQ | LE | GE | LT | GT
ClassDef → ^( CLASS SIMPLENAME ^(EXTENDS SIMPLENAME) Constructor
^( FIELDS FieldDecl ∗ ) ^(METHODS Method ∗ ) )
Constructor → ^( CONSTRUCTOR block )
ConstructorCall → ^( NEW SIMPLENAME)
118
C Übersetzungsbeispiele
C.1 Binärbaum-Programm
Nachstehend werden die Übersetzungen des Binärbaum-Programms aus Listing 1.1 in abstrakte Syntax sowie JVM-Assemblercode angegeben.
Abstrakte Syntax
CLASS
FunProcMainClass
EXTENDS
FunProcMainClass
Object
BLOCK
FIELDS
METHODS
VAR
MODIFIER
Baum
METHOD
baum
MODIFIER
public
void
...
main
...
PARAMS
public
baum
BLOCK
=
INVOKE
INVOKE
printBaum
erzeugeBaum
ARGLIST
ARGLIST
baum
...
METHOD
MODIFIER
public
MODIFIER
int
Baum
VAR
i
erzeugeBaum
PARAMS
VAR
0
MODIFIER
Baum
b
VAR
new
MODIFIER
Baum
QUALIFIEDNAME
aktuell
BLOCK
i
ARGLIST
setValue
Baum
4
i
aktuell
aktuell
INVOKE
QUALIFIEDNAME
r
while
b
<
BLOCK
=
new
INVOKE
=
Nil
aktuell
QUALIFIEDNAME
=
QUALIFIEDNAME
POSTFIX
new
l
=
aktuell
Baum
aktuell
ARGLIST
setValue
QUALIFIEDNAME
aktuell
=
QUALIFIEDNAME
i
++
aktuell
new
l
Nil
QUALIFIEDNAME
aktuell
i
l
...
METHOD
MODIFIER
void
printBaum
public
PARAMS
PARAM
Baum
b
BLOCK
INVOKE
QUALIFIEDNAME
b
inorder
INVOKE
ARGLIST
b
QUALIFIEDNAME
ARGLIST
r
inorder
r
r
return
Abbildung C.1: Binärbaum-Programm: Globale Sprachelemente im AST
119
r
new
Nil
b
C Übersetzungsbeispiele
class
Baum
extends
Baum
Object
FIELDS
BLOCK
MODIFIER
VAR
Baum
METHODS
VAR
r
l
VAR
MODIFIER
METHOD
MODIFIER
public
int
v
MODIFIER
protected
void
...
setValue
public
PARAMS
BLOCK
PARAM
if
int
v
<=
v
BLOCK
10
if
>=
v
BLOCK
0
=
QUALIFIEDNAME
this
v
...
METHOD
MODIFIER
void
inorder
public
QUALIFIEDNAME
this
l
PARAMS
BLOCK
INVOKE
print
INVOKE
ARGLIST
QUALIFIEDNAME
QUALIFIEDNAME
inorder
this
v
this
r
ARGLIST
inorder
Abbildung C.2: Binärbaum-Programm: Klasse Baum im AST
class
Nil
extends
Nil
Baum
this
v
METHODS
BLOCK
=
QUALIFIEDNAME
FIELDS
10
METHOD
MODIFIER
public
void
setValue
METHOD
PARAMS
PARAM
int
BLOCK
MODIFIER
void
inorder
PARAMS
public
v
Abbildung C.3: Binärbaum-Programm: Klasse Nil im AST
120
BLOCK
v
C.1 Binärbaum-Programm
JVM-Assemblercode
Listing C.1: (Binärbaum-Programm: Globale Sprachelemente in Assemblercode)
1
2
3
.class public FunProcMainClass
.super java/lang/Object
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.field public baum LBaum;
.method public main()V
.limit stack 2
.limit locals 1
aload_0
aload_0
invokevirtual FunProcMainClass/erzeugeBaum()LBaum;
putfield FunProcMainClass/baum LBaum;
aload_0
aload_0
getfield FunProcMainClass/baum LBaum;
invokevirtual FunProcMainClass/printBaum(LBaum;)V
return
.end method
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
.method public erzeugeBaum()LBaum;
.limit stack 3
.limit locals 4
ldc 0
istore 1
new Baum
dup
invokespecial Baum/<init>()V
astore 2
aload 2
astore 3
w0:
iload 1
ldc 4
if_icmpge c0
aload 3
iload 1
invokevirtual Baum/setValue(I)V
aload 3
new Nil
dup
invokespecial Nil/<init>()V
putfield Baum/r LBaum;
aload 3
121
C Übersetzungsbeispiele
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
new Baum
dup
invokespecial Baum/<init>()V
putfield Baum/l LBaum;
aload 3
getfield Baum/l LBaum;
astore 3
iload 1
ldc 1
iadd
istore 1
goto w0
c0:
aload 3
iload 1
invokevirtual Baum/setValue(I)V
aload 3
new Nil
dup
invokespecial Nil/<init>()V
putfield Baum/l LBaum;
aload 3
new Nil
dup
invokespecial Nil/<init>()V
putfield Baum/r LBaum;
aload 2
areturn
.end method
73
74
75
76
77
78
79
80
81
82
83
84
85
.method public printBaum(LBaum;)V
.limit stack 1
.limit locals 2
aload 1
invokevirtual Baum/inorder()V
aload 1
getfield Baum/l LBaum;
getfield Baum/l LBaum;
getfield Baum/l LBaum;
invokevirtual Baum/inorder()V
return
.end method
86
87
88
89
90
.method public <init>()V
.limit stack 1
.limit locals 1
aload_0
122
C.1 Binärbaum-Programm
91
92
93
invokespecial java/lang/Object/<init>()V
return
.end method
94
95
96
97
98
99
100
101
102
103
.method static public main([Ljava/lang/String;)V
.limit stack 2
.limit locals 1
new FunProcMainClass
dup
invokespecial FunProcMainClass/<init>()V
invokevirtual FunProcMainClass/main()V
return
.end method
Listing C.2: (Binärbaum-Programm: Klasse Baum in Assemblercode)
1
2
.class public Baum
.super java/lang/Object
3
4
5
6
.field public r LBaum;
.field public l LBaum;
.field protected v I
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.method public setValue(I)V
.limit stack 2
.limit locals 2
iload 1
ldc 10
if_icmpgt c0
iload 1
ldc 0
if_icmplt c1
aload_0
iload 1
putfield Baum/v I
goto e1
c1:
e1:
goto e0
c0:
e0:
return
.end method
28
29
30
31
.method public inorder()V
.limit stack 2
.limit locals 1
123
C Übersetzungsbeispiele
32
33
34
35
36
37
38
39
40
41
42
43
aload_0
getfield Baum/l LBaum;
invokevirtual Baum/inorder()V
getstatic java/lang/System/out Ljava/io/PrintStream;
aload_0
getfield Baum/v I
invokevirtual java/io/PrintStream/println(I)V
aload_0
getfield Baum/r LBaum;
invokevirtual Baum/inorder()V
return
.end method
44
45
46
47
48
49
50
51
.method public <init>()V
.limit stack 1
.limit locals 1
aload_0
invokespecial java/lang/Object/<init>()V
return
.end method
Listing C.3: (Binärbaum-Programm: Klasse Nil in Assemblercode)
1
2
3
.class public Nil
.super Baum
4
5
6
7
8
9
.method public setValue(I)V
.limit stack 0
.limit locals 2
return
.end method
10
11
12
13
14
15
.method public inorder()V
.limit stack 0
.limit locals 1
return
.end method
16
17
18
19
20
21
22
23
24
.method public <init>()V
.limit stack 2
.limit locals 1
aload_0
invokespecial Baum/<init>()V
aload_0
ldc 10
putfield Baum/v I
124
C.1 Binärbaum-Programm
25
26
return
.end method
125
C Übersetzungsbeispiele
C.2 Ackermann-Programm
Im folgenden werden die Übersetzungen des Ackermann-Programms aus Listing 2.1 in
abstrakte Syntax sowie JVM-Assemblercode angegeben.
Abstrakte Syntax
CLASS
FunProcMainClass
EXTENDS
FunProcMainClass
Object
BLOCK
MODIFIER
final
int
FIELDS
METHODS
VAR
VAR
5
MODIFIER
b
public
METHOD
int
i
MODIFIER
public
void
...
...
main
PARAMS
BLOCK
public
BLOCK
=
i
while
INVOKE
0
<
BLOCK
+
b
print
i
2
berechneAusdruck
=
INVOKE
ackermann
i
ARGLIST
i
i
+
i
2
...
METHOD
MODIFIER
void
berechneAusdruck
public
PARAMS
VAR
MODIFIER
final
int
x
BLOCK
VAR
9
MODIFIER
int
BLOCK
ergebnis
=
ergebnis
+
*
-
x
/
x
print
print
ergebnis
5
+
3
56
9
+
+
+
1
+
*
4
5
3
2
Abbildung C.4: Ackermann-Programm im AST 1
126
+
6
1
ARGLIST
C.2 Ackermann-Programm
...
METHOD
MODIFIER
int
ackermann
public
PARAMS
PARAM
int
a
BLOCK
PARAM
int
b
VAR
MODIFIER
int
ret
if
0
==
b
return
BLOCK
0
=
ret
1
if
ret
==
a
BLOCK
0
==
b
1
ret
BLOCK
if
=
=
=
ret
INVOKE
2
ret
+
ackermann
b
2
a
ARGLIST
-
INVOKE
1
ackermann
ARGLIST
a
-
b
Abbildung C.5: Ackermann-Programm im AST 2
JVM-Assemblercode
Listing C.4: (Ackermann-Programm in Assemblercode)
1
2
3
.class public FunProcMainClass
.super java/lang/Object
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.field final public b I
.field public i I
.method public main()V
.limit stack 5
.limit locals 1
aload_0
ldc 0
putfield FunProcMainClass/i I
w0:
aload_0
getfield FunProcMainClass/i I
ldc 2
iadd
aload_0
getfield FunProcMainClass/b I
if_icmpge c0
getstatic java/lang/System/out Ljava/io/PrintStream;
127
1
C Übersetzungsbeispiele
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
aload_0
aload_0
getfield FunProcMainClass/i I
aload_0
getfield FunProcMainClass/i I
ldc 2
iadd
invokevirtual FunProcMainClass/ackermann(II)I
invokevirtual java/io/PrintStream/println(I)V
aload_0
aload_0
getfield FunProcMainClass/i I
ldc 1
iadd
putfield FunProcMainClass/i I
goto w0
c0:
aload_0
invokevirtual FunProcMainClass/berechneAusdruck()V
return
.end method
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
.method public berechneAusdruck()V
.limit stack 4
.limit locals 3
ldc 9
istore 1
iconst_0
istore 2
iload 1
iload 1
imul
ldc 56
ldc 9
iadd
ldc 3
idiv
ldc 5
isub
iadd
istore 2
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 2
invokevirtual java/io/PrintStream/println(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc 1
ldc 2
128
C.2 Ackermann-Programm
69
70
71
72
73
74
75
76
77
78
79
80
iadd
ldc 3
iadd
ldc 4
iadd
ldc 5
ldc 6
imul
iadd
invokevirtual java/io/PrintStream/println(I)V
return
.end method
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
.method public ackermann(II)I
.limit stack 6
.limit locals 4
ldc 0
istore 3
iload 2
ldc 0
if_icmpne c0
ldc 1
istore 3
goto e0
c0:
iload 1
ldc 0
if_icmpne c1
iload 2
ldc 1
if_icmpne c2
ldc 2
istore 3
goto e2
c2:
iload 2
ldc 2
iadd
istore 3
e2:
goto e1
c1:
aload_0
iload 1
ldc 1
isub
aload_0
129
C Übersetzungsbeispiele
116
117
118
119
120
121
122
123
124
125
126
127
iload 1
iload 2
ldc 1
isub
invokevirtual FunProcMainClass/ackermann(II)I
invokevirtual FunProcMainClass/ackermann(II)I
istore 3
e1:
e0:
iload 3
ireturn
.end method
128
129
130
131
132
133
134
135
136
137
138
.method public <init>()V
.limit stack 2
.limit locals 1
aload_0
invokespecial java/lang/Object/<init>()V
aload_0
ldc 5
putfield FunProcMainClass/b I
return
.end method
139
140
141
142
143
144
145
146
147
148
.method static public main([Ljava/lang/String;)V
.limit stack 2
.limit locals 1
new FunProcMainClass
dup
invokespecial FunProcMainClass/<init>()V
invokevirtual FunProcMainClass/main()V
return
.end method
130
Herunterladen