Skip to content

Commit 6404987

Browse files
authored
Merge pull request #26 from Java-rs/codegen
Updated Documentation
2 parents fc5bf7c + 9e42ccf commit 6404987

File tree

8 files changed

+123
-111
lines changed

8 files changed

+123
-111
lines changed

docs/Project-Doc.md

Lines changed: 70 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
## Supported types
2-
3-
Unterstützte Typen sind in [Types](../lib/src/types.rs) definiert.
4-
Mögliche Kombinationen sind den [Tests](../lib/testcases) zu entnehmen.
5-
6-
71
## Parser
82

9-
Geschrieben von: Viktoria Gönnheimer, Sander Stella
3+
Geschrieben von: Tori Gönnheimer, Sander Stella
104

11-
Der Parser akzeptiert den text eines Java Programms und gibt einen Abstract Syntax Tree (AST) zurück.
5+
Ausführliche Mithilfe (v.a. beim Schreiben der Grammatik): Val Richter
6+
7+
Der Parser akzeptiert den Text eines Java Programs und gibt einen Abstract Syntax Tree (AST) zurück.
128
Dafür wird die Libray [pest.rs](https://pest.rs/) verwendet, um das Inital Parsing durchzufüren.
13-
Für dieses inital parsing nutzt pest unsere vorher definiete Gramatik. Bei der Gramatik wurde sich primär and der Vorlesugn orientiert mit signifikaten Abänderungen um das parsing zu vereinfachen, sowie den Spezifikationen der Library nachzukommen.
9+
Für dieses inital Parsing nutzt pest unsere vorher definiete Gramatik.
10+
11+
Bei der Grammatik wurde sich Anfangs an den Hilfsmitteln der Vorlesung orientiert. Später wurde diese allerdings neu geschrieben, um sich stärker am verwendeten AST zu orientieren. Dadurch wurde auch die Implementation des restlichen Parsers sehr viel erleichtert.
1412
Das Egebnis, welches Pest zurückgibt wird von uneren Parser-Funktionen analysiert und umgewandelt.
1513
Dabei wird wie folgt vorgegangenen:
16-
- Eine Funktion schaut sich die aktuelle Regel an
17-
- Es wird der Entsprechende Code zu dieser Regel ausgeführt
18-
- Sofern Subregeln in dieser Regel vorkommen, wird die entsprechende Funktion aufgerufen
1914

15+
- Eine Funktion schaut sich die aktuelle Regel an
16+
- Es wird der Entsprechende Code zu dieser Regel ausgeführt
17+
- Sofern Subregeln in dieser Regel vorkommen, wird die entsprechende Funktion aufgerufen
18+
19+
- Eine Funktion schaut sich die aktuelle Regel an
20+
- Es wird der entsprechende Code zu dieser Regel ausgeführt
21+
- Sofern Subregeln in dieser Regel vorkommen wird die entsprechende Funktion aufgerufen
2022

2123
## Typechecker
2224

@@ -57,47 +59,72 @@ Folgende Fehler werden vom Typechecker erkannt:
5759

5860
## Codegenerierung
5961

60-
ByteCode-Umwandlung, Bugfixes, StackSize und viele Verbesserungen: Val Richter
62+
Geschrieben von: Marion Hinkel und Benedikt Brandmaier im Pair Programming
63+
64+
StackMapTable-Implementation, Mithilfe bei Transformation von DIR zu Bytes und sonstigem Bugfixing: Val Richter
6165

62-
Definition DIR(Duck Intermediate Representation), ConstantPool, LocalVarPool, Methoden zur Instruction-generierung, BugFixes, etwas ByteCode-Umwandlung und Umwandlung relativer in absolute Jumps: Marion Hinkel und Benedikt Brandmaier im Pair Programming
66+
Zur Bytecode-Generierung wird der Typed Abstract Syntax Tree (TAST) in Java Bytecode umgewandelt.
67+
Es wurden keine Libraries (wie z.B. [ASM](https://asm.ow2.io/javadoc/)) verwendet.
68+
Für die Codegenerierung wird eine Intermediate Representation (IR) genutzt, die eine Class-ähnliche Struktur
69+
(mit Konstantenpool, LocalVarpool, Methoden mit Code als Instruktionen, etc.) besitzt.
70+
Diese IR wird dann komplett manuell in Java Bytecode übersetzt. Dies hat sehr viel Zeit gekostet,
71+
da z.B. die Stack-Size, der Konstantenpool, LocalVarPool und alle Jumps manuell berechnet werden mussten.
6372

64-
Zur Bytecode-generierung wird der Typed Abstract Syntax Tree(TAST) in Java Bytecode komplett selber
65-
umgewandelt. Dafür wird eine Intermediate Representation (IR) genutzt, die eine Class-ähnliche Struktur(mit Konstantenpool, LocalVarpool, Methoden mit Code als Instruktionen, etc.)
66-
besitzt. Diese IR wird dann komplett manuell in Java Bytecode übersetzt. Dies hat dem Code-gen Team sehr viel
67-
Zeit gekostet, da z.B. die Stack-Size, der Konstantenpool, LocalVarpool und die Jumps manuell berechnet werden mussten.
73+
Zudem hatten wir zeitweise eigene Instructions für relative Jumps implementiert, die wir dann in absolute Jumps umgerechnet haben,
74+
da wir dachten, dass die JVM keine relativen Jumps unterstützt. Dieser Glauben kam daher, dass die Ausgabe von `javap` die
75+
Ziel-Adressen von Jumps immer als absolute Adressen angezeigt hat. Es stellte sich dann aber heraus, dass die JVM eigentlich
76+
nur relative Jumps versteht und nur javap diese schon umgerechnet dargestellt hat. Aber selbst danch waren die relativen Jumps
77+
noch sehr fehleranfällig und hatten häufig off-by-one Errors.
6878

69-
Zudem hatten wir zeitweise eigene Relative Instructions implementiert, da wir dachten, dass die JVM keine relativen Jumps
70-
unterstützt, hatten dann allerdings mit Try-and-Error herausgefunden, dass javap sich die absoluten Addressen ausrechnet
71-
und für die JVM normale jumps als relative Jumps behandelt. Auch die relativen Jumps waren aber sehr fehleranfällig und
72-
hatten häufig off-by-one Errors.
79+
Zudem musste eine [StackMapTable](https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-4.html#jvms-4.7.4) per Hand implementiert werden,
80+
da die JVM unsere Klassen sonst nicht geladen hat. Die Implementation dieser war ebenfalls sehr zeitaufwendig, da an sich ein ganzer
81+
Typchecker für den generierten Bytecode implementiert werden musste, um korrekte StackMapTables zu generieren.
7382

74-
Zudem musste eine StackMapTable implementiert werden, da die JVM sonst unsere Klassen nicht lädt.
75-
Das Troubleshooten von Testfehlern war auch sehr aufwending da oft javap gar nicht erst den Fehler im Klassencode ausgab
83+
Das Troubleshooten von Testfehlern war auch sehr aufwendig da oft javap gar nicht erst den Fehler im Klassencode ausgab
7684
und wir mit einem Hex-Editor die Klassen von Hand analysieren mussten, da es auch kein anderes Tool gab, um solche Fehler
77-
auszugeben und die Zeit fehlte ein Eigenes zu schreiben.
85+
auszugeben und die Zeit fehlte ein eigenes Tool dafür zu schreiben.
7886

79-
Da es auch keine Dokumentation gibt, die in etwa zeigt, welcher Bytecode für welche Operationen genutzt wird, mussten wir
87+
Da es auch keine gute Dokumentation gibt, die in etwa zeigt, welcher Bytecode für welche Operationen genutzt wird, mussten wir
8088
uns die Bytecode-Spezifikationen anschauen und sehr viel mit Tools wie javap und [godbolt](https://godbolt.org/) arbeiten
81-
in die wir manuell Java Code eingeben und schauten, was für Bytecode bei verschiedenen Operationenkombinationen generiert
82-
wird, was sehr zeitaufwendig war.
83-
84-
Auch sehr schwierig war die Implementation einer StackMapTable, da Java diese erwartet. Diese ist eine Tabelle, die für
85-
jede Instruktion die Typen der Elemente auf dem Stack in komprimiertem Format angibt. Diese Tabelle muss manuell
86-
erstellt werden und über die Typen aller Variablen, die in den Stack geschrieben wurden Bescheid wissen.
89+
(wir haben auch https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-3.html genutzt, aber diese Dokumentation nutzt
90+
sehr spezifische Instruktionen und wurde deswegen selten genutzt) in die wir manuell Java Code eingeben konnten, um zu sehen,
91+
welcher Bytecode bei verschiedenen Operationskombinationen generiert wird, was sehr zeitaufwendig war.
8792

8893
## Testing
8994

90-
Das Testen des Codegens war sehr aufwendig, er besteht aus diesen Schritten:
91-
92-
1. Ein handgeschriebener TAST wird geladen
93-
2. Eine Java Klasse wird erstellt die jede Methode im TAST aufruft
94-
3. Die java Klasse wird mit javac kompiliert und ausgeführt, wobei die Ausgabe in einer Variable gespeichert wird
95-
4. Fürs Debugging wird die Java Klasse mit javap in Bytecode umgewandelt und in eine Datei geschrieben
96-
5. Der TAST wird in eine DIR umgewandelt und zu Bytecode umgewandelt
97-
6. Der Bytecode wird in eine .class-Datei geschrieben
98-
7. Die .class-Datei wird mit javap in Bytecode umgewandelt und in eine Datei geschrieben
99-
8. Die vom Codegen generierte .class-Datei wird ausgeführt und die Ausgabe in einer Variable gespeichert
100-
9. Die Ausgaben der richtigen Java Klasse und der vom Codegen generierten Klasse werden verglichen
95+
Geschrieben von: Val Richter
96+
97+
Jeder Test besteht aus einer Java-Klasse (liegt jeweils in `lib/testcases`) und aus dem dazugehörigen getypten AST, welcher für jeden Test per Hand geschrieben wurde. Mit diesen beiden Teilen testen wir dann den Parser, Typchecker und die Codegenerierung. Außerdem testen wir auch, dass der handgeschriebene TAST korrekt ist. Wie diese Tests funktionieren, wird im Folgenden ausgeführt.
98+
99+
Um zu testen, dass die handgeschriebenen TASTs korrekt sind, wird aus dem TAST ein syntaktisch korrektes Java-Programm geschrieben. Alle Typinformationen des TAST werden dabei ignoriert.
100+
Das so erstellte Java-Programm wird dann in eine Datei geschrieben und mit `javac` kompiliert. Daraufhin wird überprüft, dass die kompilierte `.class`-Datei identisch mit der `.class`-Datei ist,
101+
die man beim Kompilieren des originalen Java-Programms erhält. Eine nervige Besonderheit bei diesem Vorgehen ist, dass der vom AST generierte Java-Code die originale Java-Datei überschreiben muss,
102+
da der `javac` Compiler sonst minimal unterschiedliche `.class`-Dateien schreibt. Trotzdem war dieser Test sehr hilfreich, da beim handschriftlichen Schreiben der teilweise sehr großen TASTs,
103+
sehr oft kleine Fehler gemacht wurden, die ansonsten nur schwer zu finden wären.
104+
105+
Außerdem schreiben wir bei dem Test des TASTs den erwarteten AST und TAST als `.json`-Dateien in den `lib/testcases` Ordner. Diese Dateien dienen vor allen bei der Implementation der anderen Teile des Compilers, da somit leicht sichtbar ist, wie der erwartete AST bzw. TAST aussehen sollten für den jeweiligen Test.
106+
107+
Das Testen des Parsers war relativ leicht. Hier musste nur die jeweilige Java-Datei gelesen und in den Parser gegeben werden. Sobald dieser dann den erstellten AST zurück gibt, kann überprüft werden, ob er identisch mit dem handgeschriebenen TAST ist. Dabei werden alle Typinformationen vom TAST vorher entfernt (wodurch wir den AST erhalten), weil der Parser ja noch keine Typinformationen hinzufügt.
108+
109+
Das Testen des Typcheckers läuft ähnlich ab. Der handgeschriebene TAST dient als erwartete Ausgabe des Typcheckers. Der AST, der als Eingabe für den Typchecker dient, wird über Entfernen der Typinformationen beim TAST generiert.
110+
111+
Das Testen des Codegens war dagegen sehr viel aufwendiger. Die Eingabe für die Codegenerierung ist mit den handgeschriebenen TASTs bereits gegeben. Um die Ausgabe zu überprüfen, hätte man allerdings die erwarteten Bytes per Hand aufschreiben müssen. Das wäre zu aufwendig und fehleranfällig gewesen. Stattdessen testen wir, dass die von uns geschriebene `.class`-Datei sich identisch mit der von `javac` erstellten `.class`-Datei verhält.
112+
113+
Dafür wurde zuerst die originale Java-Datei kompiliert. Zusätzlich wird eine Test-Datei geschrieben, welche eine `main` Funktion hat und alle Methoden der originalen Java-Datei aufruft. Über die handgeschriebenen TASTs wissen wir, welche Methoden die Klasse besitzt und welche Eingaben sie erwartet und was sie ausgibt. Die Eingaben werden pseudo-zufällig für den Test generiert. Falls die Methoden einen Wert ausgibt, schreibt die `main` Funktion der Test-Datei diesen Ausgabewert in die Konsole. Damit erhalten wir das erwartete Verhalten der originalen Java-Klasse.
114+
115+
Im nächsten Schritt überschreiben wir nun die `.class`-Datei der originalen Java-Datei mit den Bytes, die wir aus der Codegenerierung erhalten. Dann können wir Test-Datei nochmal ausführen. Diesmal versucht java aber natürlich die von uns geschriebene `.class`-Datei zu lesen und zu benutzen. Wenn die Test-klasse dann die selben Ausgaben macht, wissen wir, dass sich unsere `.class`-Datei genauso wie die originale `.class`-Datei verhält und unsere Codegenerierung entsprechend richtig funktioniert.
116+
117+
Zusätzlich nutzen wir in diesem Test auch das mit Java mitgelieferte Tool `javap`, welches uns erlaubt den originalen und den von uns generierten Bytecode zu disassemblieren. Wir schreiben die Ausgabe von `javap` dann in jeweils eine Datei (eine Datei für die originale von `javac` kompilierte Klasse und eine Datei für die von uns kompilierte Klasse). Diese Ausgaben sind zwar für den Test nicht notwendig, haben aber sehr geholfen bei Fehlersuche und Fehlerbehebung.
118+
119+
## Supported types
120+
121+
Unterstützte Typen sind in [Types](../lib/src/types.rs) definiert.
122+
Mögliche Kombinationen sind den [Tests](../lib/testcases) zu entnehmen.
123+
124+
## Supported types
125+
126+
Unterstützte Typen sind in [Types](../lib/src/types.rs) definiert.
127+
Mögliche Kombinationen sind den [Tests](../lib/testcases) zu entnehmen.
101128

102129
## AST-Definition
103130

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ There are three parts to the documentation:
44

55
## 1. Short user documentation
66

7-
[This](./User-Doc.md) is a short doument, detailing how to compile and run each separate part of the compiler.
7+
[This](./User-Doc.md) is a short document, detailing how to compile and run each separate part of the compiler.
88

99
## 2. UML Class-Diagram for AST
1010

docs/User-Doc.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
77
```
88

99
- [JDK 20](https://www.oracle.com/java/technologies/downloads/) (Wird in Tests für die Validierung des Codegens benötigt)
10+
- `java`, `javac` und `javap` müssen dabei für die Tests der Codegenerierung im Pfad sein, damit diese vom Test genutzt werden können.
1011

1112
# Ausführen
1213

@@ -22,22 +23,28 @@ cargo r -r -- <input_file> [<output_file>]
2223

2324
## Parser
2425

25-
2. Parser tests ausführen: `cargo test --lib test_parser`
26+
2. Parser Tests ausführen: `cargo test --lib test_parser`
2627

2728
Spezifischen Test ausführen: `cargo test --lib <test_name>::test_parser`
2829

2930
## Typchecker
3031

31-
2. Typechecker tests ausführen: `cargo test --lib test_typechecker`
32+
2. Typechecker Tests ausführen: `cargo test --lib test_typechecker`
3233

3334
Spezifischen Test ausführen: `cargo test --lib <test_name>::test_typechecker`
3435

3536
## Codegenerierung
3637

37-
2. Codegenerierung tests ausführen: `cargo test --lib test_codegen`
38+
2. Codegenerierung Tests ausführen: `cargo test --lib test_codegen`
3839

3940
Spezifischen Test ausführen: `cargo test --lib <test_name>::test_codegen`
4041

42+
## TAST
43+
44+
2. Ausführung der Tests von den handgeschriebenen TASTs: `cargo test --lib test_class`
45+
46+
Spezifischen Test ausführen: `cargo test --lib <test_name>::test_class`
47+
4148
## Testing
4249

43-
Zur ausführung aller tests: `cargo test --lib`. Dabei sollte beachtet werden, dass die Tests teilweise die selben Dateien schreiben und entsprechend Probleme aufkommen können, wenn alle Tests auf einmal ausgeführt werden. Diese Probleme treten nicht auf, wenn die Teile des Compilers (Parser, Typchecker, Codegenerierung) einzeln getestet werden.
50+
Zur ausführung aller Tests: `cargo test --lib`. Dabei sollte beachtet werden, dass die Tests teilweise die selben Dateien schreiben und entsprechend Probleme aufkommen können, wenn alle Tests gleichzeitig ausgeführt werden. Diese Probleme treten nicht auf, wenn die Teile des Compilers (Parser, Typchecker, Codegenerierung) einzeln getestet werden.

lib/src/codegen/ir.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ impl DIR {
7171

7272
result.extend_from_slice(&[0, 0]);
7373
result.extend_from_slice(&[]);
74+
println!("Generated bytecode succesfully!🎉💾");
7475
result
7576
}
7677
}

lib/src/parser/JavaGrammar.pest

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,10 @@ ReturnStmt = {"return" ~ Expr ~ ";"}
3434

3535
WhileStmt = {"while" ~ "(" ~ Expr ~ ")" ~ Stmt}
3636

37-
// @Question: Is this a correct and easy-to-use definition?
3837
IfElseStmt = {IfStmt ~ "else" ~ Stmt}
3938

4039
IfStmt = {"if" ~ "(" ~ Expr ~ ")" ~ Stmt}
4140

42-
// @Cleanup: Can maybe be unified with FieldVarDecl and AssignExpr?
4341
LocalVarDeclStmt = {JType ~ Identifier ~ ("="~Expr)? ~ ("," ~ Identifier ~ ("="~Expr)?)* ~ ";"}
4442

4543
StmtExpr = {AssignExpr | NewExpr | MethodCallExpr}

lib/src/parser/parser.rs

Lines changed: 27 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub fn parse_programm(file: &str) -> Result<Vec<Class>, Error<Rule>> {
2323
panic!();
2424
}
2525
let pased_clases = prg.into_inner().map(parse_class).collect();
26+
println!("Parsed program successfully!🎉✍️");
2627
Ok(pased_clases)
2728
}
2829

@@ -124,51 +125,6 @@ fn parse_BlockStmt(pair: Pair<Rule>) -> Vec<Stmt> {
124125
}
125126

126127
result
127-
/*
128-
let first = inner.next();
129-
if (first.is_none()) {
130-
return vec![];
131-
}
132-
let first = first.unwrap();
133-
134-
let result = vec![];
135-
for first in inner {
136-
match first.as_rule() {
137-
/*
138-
Rule::JType => {
139-
let jtype = parse_Type(first);
140-
let var_decels = inner.next().unwrap().into_inner();
141-
var_decels
142-
.map(|x| {
143-
let mut inner = x.into_inner();
144-
let other_name = next_id(&mut inner);
145-
match inner.next() {
146-
None => {
147-
Stmt::LocalVarDecl(jtype.clone(), other_name)
148-
}
149-
Some(expresion) => vec![
150-
Stmt::LocalVarDecl(jtype.clone(), other_name.clone()),
151-
Stmt::StmtExprStmt(StmtExpr::Assign(
152-
other_name,
153-
parse_expr(expresion),
154-
)),
155-
],
156-
}
157-
})
158-
.flatten()
159-
.collect()
160-
}
161-
162-
*/
163-
Rule::Stmt => parse_Stmt(first.into_inner().next().unwrap()),
164-
_ => {
165-
dbg!(first.as_rule());
166-
unreachable!()
167-
}
168-
}
169-
}
170-
result
171-
*/
172128
}
173129
_ => {
174130
unreachable!()
@@ -219,19 +175,36 @@ fn parse_Stmt(pair: Pair<Rule>) -> Vec<Stmt> {
219175
Rule::LocalVarDeclStmt => {
220176
let mut inners = pair.into_inner();
221177

178+
let mut result = vec![];
222179
let typeJ = parse_Type(inners.next().unwrap());
223-
let var_name = next_id(&mut inners);
224-
// StmtExprStmt
225-
let lVD = Stmt::LocalVarDecl(typeJ, var_name.clone());
226180

227-
match inners.next() {
228-
None => vec![lVD],
229-
Some(expr_pair) => {
230-
let expr =
231-
StmtExpr::Assign(Expr::LocalOrFieldVar(var_name), parse_expr(expr_pair));
232-
vec![lVD, Stmt::StmtExprStmt(expr)]
181+
let mut last_var_name = None;
182+
for inner in inners {
183+
match inner.as_rule() {
184+
Rule::Identifier => {
185+
if last_var_name.is_some() {
186+
result.push(Stmt::LocalVarDecl(typeJ.clone(), last_var_name.unwrap()));
187+
}
188+
last_var_name = Some(inner.as_str().trim().to_string());
189+
}
190+
Rule::Expr => {
191+
result.push(Stmt::LocalVarDecl(
192+
typeJ.clone(),
193+
last_var_name.as_ref().unwrap().clone(),
194+
));
195+
result.push(Stmt::StmtExprStmt(StmtExpr::Assign(
196+
Expr::LocalOrFieldVar(last_var_name.unwrap()),
197+
parse_expr(inner),
198+
)));
199+
last_var_name = None;
200+
}
201+
_ => unreachable!(),
233202
}
234203
}
204+
if last_var_name.is_some() {
205+
result.push(Stmt::LocalVarDecl(typeJ, last_var_name.unwrap()));
206+
}
207+
result
235208
}
236209
Rule::StmtExpr => {
237210
vec![Stmt::StmtExprStmt(parse_StmtExpr(

0 commit comments

Comments
 (0)