bytecode #6
@ -4,20 +4,7 @@ Die Bytecodegenerierung ist letztendlich eine zweistufige Transformation:
|
|||||||
|
|
||||||
`Getypter AST -> [ClassFile] -> [[Word8]]`
|
`Getypter AST -> [ClassFile] -> [[Word8]]`
|
||||||
|
|
||||||
Vom AST, der bereits den Typcheck durchlaufen hat, wird zunächst eine Abbildung in die einzelnen ClassFiles vorgenommen. Diese ClassFiles werden anschließend in deren Byte-Repräsentation serialisiert.
|
Vom AST, der bereits den Typcheck durchlaufen hat, wird zunächst eine Abbildung in die einzelnen ClassFiles vorgenommen. Diese ClassFiles werden anschließend in deren Byte-Repräsentation serialisiert. Dieser Teil der Aufgabenstellung wurde gemeinsam von Christian Brier und Matthias Raba umgesetzt.
|
||||||
|
|
||||||
## Serialisierung
|
|
||||||
|
|
||||||
Damit Bytecode generiert werden kann, braucht es Strukturen, die die Daten halten, die letztendlich serialisiert werden. Die JVM erwartet den kompilierten Code in handliche Pakete verpackt. Die Struktur dieser Pakete ist [so definiert](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html).
|
|
||||||
|
|
||||||
Jede Struktur, die in dieser übergreifenden Class File vorkommt, haben wir in Haskell abgebildet. Es gibt z.B die Struktur "ClassFile", die wiederum weitere Strukturen wie z.B Informationen über Felder oder Methoden der Klasse. Alle diese Strukturen implementieren folgendes TypeClass:
|
|
||||||
|
|
||||||
```
|
|
||||||
class Serializable a where
|
|
||||||
serialize :: a -> [Word8]
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Struktur ClassFile ruft für deren Kinder rekursiv diese `serialize` Funktion auf. Am Ende bleibt eine flache Word8-Liste übrig, die Serialisierung ist damit abgeschlossen.
|
|
||||||
|
|
||||||
## Codegenerierung
|
## Codegenerierung
|
||||||
|
|
||||||
@ -32,7 +19,6 @@ Die Idee hinter beiden ist, dass sie jeweils zwei Inputs haben, wobei der Rückg
|
|||||||
Der Nutzer ruft beispielsweise die Funktion `classBuilder` auf. Diese wendet nach und nach folgende Transformationen an:
|
Der Nutzer ruft beispielsweise die Funktion `classBuilder` auf. Diese wendet nach und nach folgende Transformationen an:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
methodsWithInjectedConstructor = injectDefaultConstructor methods
|
methodsWithInjectedConstructor = injectDefaultConstructor methods
|
||||||
methodsWithInjectedInitializers = injectFieldInitializers name fields methodsWithInjectedConstructor
|
methodsWithInjectedInitializers = injectFieldInitializers name fields methodsWithInjectedConstructor
|
||||||
|
|
||||||
@ -47,7 +33,7 @@ Zuerst wird (falls notwendig) ein leerer Defaultkonstruktor in die Classfile ein
|
|||||||
2. Hinzufügen aller Methoden (nur Prototypen)
|
2. Hinzufügen aller Methoden (nur Prototypen)
|
||||||
3. Hinzufügen des Bytecodes in allen Methoden
|
3. Hinzufügen des Bytecodes in allen Methoden
|
||||||
|
|
||||||
Die Unterteilung von Schritt 2 und 3 ist deswegen notwendig, weil der Code einer Methode auch eine andere, erst nachher deklarierte Methode aufrufen kann. Nach Schritt 2 sind alle Methoden der Klasse bekannt. Wie beschrieben wird auch hier der Zustand über alle Faltungen mitgenommen. Jeder Schritt hat Zugriff auf alle Daten, die aus dem vorherigen Schritt bleiben. Sukkzessive wird eine korrekte ClassFile aufgebaut.
|
Die Unterteilung von Schritt 2 und 3 ist deswegen notwendig, weil der Code einer Methode auch eine andere, erst nachher deklarierte Methode aufrufen kann. Nach Schritt 2 sind alle Methoden der Klasse bekannt. Wie beschrieben wird auch hier der Zustand über alle Faltungen mitgenommen. Jeder Schritt hat Zugriff auf alle Daten, die aus dem vorherigen Schritt bleiben. Sukzessive wird eine korrekte ClassFile aufgebaut.
|
||||||
|
|
||||||
Besonders interessant ist hierbei Schritt 3. Dort wird das Verhalten jeder einzelnen Methode in Bytecode übersetzt. In diesem Schritt werden zusätzlich zu den `Buildern` noch die `Assembler` verwendet (Definition siehe oben.) Die Assembler funktionieren ähnlich wie die Builder, arbeiten allerdings nicht auf einer ClassFile, sondern auf dem Inhalt einer Methode: Sie verarbeiten jeweils ein Tupel:
|
Besonders interessant ist hierbei Schritt 3. Dort wird das Verhalten jeder einzelnen Methode in Bytecode übersetzt. In diesem Schritt werden zusätzlich zu den `Buildern` noch die `Assembler` verwendet (Definition siehe oben.) Die Assembler funktionieren ähnlich wie die Builder, arbeiten allerdings nicht auf einer ClassFile, sondern auf dem Inhalt einer Methode: Sie verarbeiten jeweils ein Tupel:
|
||||||
|
|
||||||
@ -67,3 +53,27 @@ assembleExpression (constants, ops, lvars) (TypedExpression _ NullLiteral) =
|
|||||||
Hier werden die Konstanten und lokalen Variablen des Inputs nicht berührt, dem Bytecode wird lediglich die Operation `aconst_null` hinzugefügt. Damit ist das Verhalten des gematchten Inputs - eines Nullliterals - abgebildet.
|
Hier werden die Konstanten und lokalen Variablen des Inputs nicht berührt, dem Bytecode wird lediglich die Operation `aconst_null` hinzugefügt. Damit ist das Verhalten des gematchten Inputs - eines Nullliterals - abgebildet.
|
||||||
|
|
||||||
Die Assembler rufen sich teilweise rekursiv selbst auf, da ja auch der AST verschachteltes Verhalten abbilden kann. Der Startpunkt für die Assembly einer Methode ist der Builder `methodAssembler`. Dieser entspricht Schritt 3 in der obigen Übersicht.
|
Die Assembler rufen sich teilweise rekursiv selbst auf, da ja auch der AST verschachteltes Verhalten abbilden kann. Der Startpunkt für die Assembly einer Methode ist der Builder `methodAssembler`. Dieser entspricht Schritt 3 in der obigen Übersicht.
|
||||||
|
|
||||||
|
## Serialisierung
|
||||||
|
|
||||||
|
Damit Bytecode generiert werden kann, braucht es Strukturen, die die Daten halten, die letztendlich serialisiert werden. Die JVM erwartet den kompilierten Code in handliche Pakete verpackt. Die Struktur dieser Pakete ist [so definiert](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html).
|
||||||
|
|
||||||
|
Jede Struktur, die in dieser übergreifenden Class File vorkommt, haben wir in Haskell abgebildet. Es gibt z.B die Struktur "ClassFile", die wiederum weitere Strukturen wie z.B Informationen über Felder oder Methoden der Klasse beinhaltet. Alle diese Strukturen implementieren folgende TypeClass:
|
||||||
|
|
||||||
|
```
|
||||||
|
class Serializable a where
|
||||||
|
serialize :: a -> [Word8]
|
||||||
|
```
|
||||||
|
|
||||||
|
Hier ist ein Beispiel anhand der Serialisierung der einzelnen Operationen:
|
||||||
|
|
||||||
|
```
|
||||||
|
instance Serializable Operation where
|
||||||
|
serialize Opiadd = [0x60]
|
||||||
|
serialize Opisub = [0x64]
|
||||||
|
serialize Opimul = [0x68]
|
||||||
|
...
|
||||||
|
serialize (Opgetfield index) = 0xB4 : unpackWord16 index
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Struktur ClassFile ruft für deren Kinder rekursiv diese `serialize` Funktion auf und konkateniert die Ergebnisse. Am Ende bleibt eine flache Word8-Liste übrig, die Serialisierung ist damit abgeschlossen. Da der Typecheck sicherstellt, dass alle referenzierten Methoden/Felder gültig sind, kann die Übersetzung der einzelnen Klassen voneinander unabhängig geschehen.
|
13
src/Main.hs
13
src/Main.hs
@ -1,6 +1,5 @@
|
|||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
import Example
|
|
||||||
import Typecheck
|
import Typecheck
|
||||||
import Parser.Lexer (alexScanTokens)
|
import Parser.Lexer (alexScanTokens)
|
||||||
import Parser.JavaParser
|
import Parser.JavaParser
|
||||||
@ -14,20 +13,20 @@ main = do
|
|||||||
args <- getArgs
|
args <- getArgs
|
||||||
let filename = if null args
|
let filename = if null args
|
||||||
then error "Missing filename, I need to know what to compile"
|
then error "Missing filename, I need to know what to compile"
|
||||||
else args!!0
|
else head args
|
||||||
let outputDirectory = takeDirectory filename
|
let outputDirectory = takeDirectory filename
|
||||||
print ("Compiling " ++ filename)
|
print ("Compiling " ++ filename)
|
||||||
file <- readFile filename
|
file <- readFile filename
|
||||||
|
|
||||||
let untypedAST = parse $ alexScanTokens file
|
let untypedAST = parse $ alexScanTokens file
|
||||||
let typedAST = (typeCheckCompilationUnit untypedAST)
|
let typedAST = typeCheckCompilationUnit untypedAST
|
||||||
let assembledClasses = map (\(typedClass) -> classBuilder typedClass emptyClassFile) typedAST
|
let assembledClasses = map (`classBuilder` emptyClassFile) typedAST
|
||||||
|
|
||||||
mapM_ (\(classFile) -> let
|
mapM_ (\classFile -> let
|
||||||
fileContent = pack (serialize classFile)
|
fileContent = pack (serialize classFile)
|
||||||
fileName = outputDirectory ++ "/" ++ (className classFile) ++ ".class"
|
fileName = outputDirectory ++ "/" ++ className classFile ++ ".class"
|
||||||
in Data.ByteString.writeFile fileName fileContent
|
in Data.ByteString.writeFile fileName fileContent
|
||||||
) assembledClasses
|
) assembledClasses
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user