From 98735fd6ba430bc8e4ef32a59e5db9a30eda8711 Mon Sep 17 00:00:00 2001 From: Matthias Raba Date: Fri, 21 Jun 2024 09:03:47 +0200 Subject: [PATCH] updated bytecode.md --- doc/bytecode.md | 44 +++++++++++++++++++++++++++----------------- src/Main.hs | 15 +++++++-------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/doc/bytecode.md b/doc/bytecode.md index b980830..1002122 100644 --- a/doc/bytecode.md +++ b/doc/bytecode.md @@ -4,20 +4,7 @@ Die Bytecodegenerierung ist letztendlich eine zweistufige Transformation: `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. - -## 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. +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. ## 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: ``` - methodsWithInjectedConstructor = injectDefaultConstructor methods 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) 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: @@ -66,4 +52,28 @@ 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. -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/src/Main.hs b/src/Main.hs index 3f4c329..bbb43b6 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,6 +1,5 @@ module Main where -import Example import Typecheck import Parser.Lexer (alexScanTokens) import Parser.JavaParser @@ -14,20 +13,20 @@ main = do args <- getArgs let filename = if null args then error "Missing filename, I need to know what to compile" - else args!!0 + else head args let outputDirectory = takeDirectory filename print ("Compiling " ++ filename) file <- readFile filename let untypedAST = parse $ alexScanTokens file - let typedAST = (typeCheckCompilationUnit untypedAST) - let assembledClasses = map (\(typedClass) -> classBuilder typedClass emptyClassFile) typedAST + let typedAST = typeCheckCompilationUnit untypedAST + let assembledClasses = map (`classBuilder` emptyClassFile) typedAST - mapM_ (\(classFile) -> let + mapM_ (\classFile -> let fileContent = pack (serialize classFile) - fileName = outputDirectory ++ "/" ++ (className classFile) ++ ".class" + fileName = outputDirectory ++ "/" ++ className classFile ++ ".class" in Data.ByteString.writeFile fileName fileContent - ) assembledClasses + ) assembledClasses + -