diff --git a/HOME_EXAM.md b/HOME_EXAM.md index 046d480551c28d03bc7bac185385ec575ffa2ef5..243dc984c498fa2b4cbe1797488b13a155c6f3bb 100644 --- a/HOME_EXAM.md +++ b/HOME_EXAM.md @@ -1,56 +1,314 @@ -# Home Exam D7050E - -- Fork this repo and put your answers (or links to answers) in THIS file. +# Home Exam D7050E: Maxence Cornaton ## Your repo -- Link to your repo here: +https://gitlab.com/Abrikot/rust-parser ## Your syntax - Give an as complete as possible EBNF grammar for your language -- Give an example that showcases all rules of your EBNF. The program should "do" something as used in the next excercise. +The EBNF grammar file can be found here: https://gitlab.com/Abrikot/rust-parser/blob/master/ebnf.txt. + +``` +(* Basic definitions *) +letter = "a" | "b"| "c" | "d" | "e" | "f" | "g" | "h" | "i"| "j" | "k" | "l" | "m" | "n" | "o" | "p"| "q" | "r" | "s" | "t" | "u" | "v" | "w"| "x" | "y" | "z" ; +digit_excluding_zero = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; +digit = "0" | digit_excluding_zero; + +number = digit_excluding_zero | { digit }; +boolean = "true" | "false"; +terminal = number | boolean; +primitive_type = "i32" | "bool"; +type = [ &, [ "mut" ], type] | primitive_type; + +(* Operations *) +binary_operator = "+" + | "-" + | "*" + | "/" + | "%" + | "&&" + | "||" + | "==" + | "^" + | "<" + | "!="; +binary_operation = (parenthesized_expression | unary_operation | terminal | variable), binary_operator, expression; +unary_operator = "-" + | "!"; +unary_operation = unary_operator, expression; + +(* Expressions *) +parenthesized_expression = "(", expression, ")"; +expression = block + | if + | function_call + | binary_operation + | unary_operation + | parenthesized_expression + | terminal + | variable + | reference + | dereference; +reference = "&", ["mut"], expression; +dereference = "*", expression; + +(* Variables *) +object_name = { letter | "_" }; +variable = object_name; +declaration = "let", variable, ":", type, "=", expression; + +(* Statements *) +left_hand_side = variable + | ("&", left_hand_side) + | ("*", left_hand_side); +assignment = left_hand_side, "=",expression; + +non_returning_statement = (while | declaration | assignment | expression), ";"; +returning_statement = "return", expression, ";"; + +(* Blocks *) +block = "{", [{ non_returning_statement }], [ returning_statement ], "}"; + +(* Conditionals *) +if = "if", expression, block, ["else", block]; +while = "while", expression, block; + +(* Functions *) +parameters_list = [{ "mut", object_name, ":", type }]; +return_type = ["->", primitive_type]; +function = "fn", object_name, "(", parameters_list, ")", return_type, block; + +(* Functions calls *) +arguments_list = [expression, [{",", expression}]]; +function_call = object_name, "(", arguments_list, ")"; + +(* File *) +file = [{ function }]; +``` + +- Give an example that showcases all rules of your EBNF. The program should "do" something as used in the next exercise. + +The example can be found here: https://gitlab.com/Abrikot/rust-parser/blob/master/tests/examples/home_exam.rs. +```rust +fn main() { + let mut integer_variable: i32 = 1; + integer_variable = -(integer_variable + *&integer_variable - 1); + + let boolean_variable: bool = true; + + if boolean_variable { + println(integer_variable); + }; + + { + let mut reference_on_integer: &mut i32 = &mut integer_variable; + *reference_on_integer = 16; + }; + + while integer_variable != 0 { + integer_variable = integer_variable / 2; + println(integer_variable); + }; + + if integer_variable != 0 { + println(true); + } else { + println(false); + }; + + println(test_if_equals(integer_variable + 3, 3)); + + + let mut other_variable: i32 = 1; + let mut reference_on_other_variable: &mut i32 = &mut other_variable; + assign_value_to_reference(reference_on_other_variable, 6); + println(*reference_on_other_variable); +} + +fn test_if_equals(x: i32, y: i32) -> bool { + return x == y; +} + +fn assign_value_to_reference(reference: &mut i32, value: i32) { + *reference = value; +} +``` + - If you support pointers, make sure your example covers pointers as well. -- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation. +Pointers are party supported: we can't assign to a reference and a reference can't be returned. This partial support is covered in my example. +- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation? +Every requirement in the README.md seems to be fulfilled. I have done everything in this part except the `ParseResult` enum and the `parse_i32` function. ## Your semantics -- Give an as complete as possible Structural Operetional Semantics (SOS) for your language +- Give an as complete as possible Structural Operational Semantics (SOS) for your language + +The SOS rules can be found here: https://gitlab.com/Abrikot/rust-parser/blob/master/SOS%20rules.pdf. + +- Explain (in text) what an interpretation of your example should produce, do that by dry running your given example step by step. Relate back to the SOS rules. You may skip repetitions to avoid cluttering. + +Following series describes what my example should produce: +1) Declare an integer variable named `integer_variable` which value should be `1`. _(rule 28)_ +2) Compute a new value and assign this value to this variable. This new value should be `-1`. _(rules 1, 3, 7, 28)_ +3) Declare a boolean variable name `boolean_variable` which value should be `true`. _(rule 28)_ +4) Test the value of `boolean_variable`. As it should be `true`, print the value of the integer variable (`-1`). _(rules 23, 24)_ +5) In a block: + 5) Declare a reference to the integer variable called `reference_on_integer`. _(rule 28)_ + 5) Assign the value `16` to the variable pointed by `reference_on_integer`. _(rule 28)_ +6) Loop while the integer variable value is not `0`: _(rules 26, 27)_ + 6) Divide the value of the integer variable by `2` and assign this new value to this variable. _(rules 4, 28)_ + 6) Print the new value of this variable. Should print `8`, `4`, `2`, `1`, `0`. _(rule 23)_ +7) Test whether the integer variable value is different of `0`. As it is false, print `false`. _(rules 23, 25)_ +8) Print the result of the call to the function `test_if_equals` with the arguments `integer_variable + 3` (which resolves to `3`) and `3`. Should print `true`. _(rule 1, 16, 23)_ +9) Declare an integer variable named `other_variable` which value should be `1`. _(rule 28)_ +10) Declare a reference called `reference_on_other_variable` to the integer variable called `other_variable`. _(rule 28)_ +9) Call the function `assign_value_to_reference` and assign the value `6` to the variable called `other_variable`. _(rules 23, 28)_ +10) Print the current value of the variable pointed by `reference_on_other_variable`: `6`. _(rule 23)_ -- Explain (in text) what an interpretation of your example should produce, do that by dry running your given example step by step. Relate back to the SOS rules. You may skip repetions to avoid cluttering. +- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation? -- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation. +My interpreter is able to correctly execute programs according to my SOS. Furthermore, it panics when encountering an evaluation error (whether it is a type error or another error, e.g. division by 0). + +I implemented all of this alone. ## Your type checker - Give an as complete as possible set of Type Checking Rules for your language (those rules look very much like the SOS rules, but over types not values). +The type checking rules can be found here: https://gitlab.com/Abrikot/rust-parser/blob/master/Type%20checking%20rules.pdf. + - Demonstrate each "type rule" by an example. -- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation. +Each following bullet point is formed as follow: `expression` => `type the expression should have`. +1. `1 + 2` => `i32` +2. `1 * 2` => `i32` +3. `1 - 2` => `i32` +4. `1 / 2` => `i32` +5. `1 % 2` => `i32` +6. `1 ^ 2` => `i32` +7. `-2` => `i32` +8. `true && false` => `bool` +9. `true || false` => `bool` +10. `!true` => `bool` +11. `true == false` => `bool` +12. `1 == 2` => `bool` +13. `true != false` => `bool` +14. `1 != 2` => `bool` +15. `1 < 2` => `bool` +16. `(1)` => `i32` +17. `(true)` => `bool` +18. `add_2_then_return(5)` => `i32` +19. `apply_and_then_return(true, false)` => `bool` +20. `println(3)` => `∅` +21. `if true {1} else {2}` => `i32` +22. `if true {true} else {false}` => `bool` +23. `if true {println(true);} else {println(false);}` => `∅` +24. `if true {println(true);}` => `∅` +25. `while true {}` => `∅` +26. `let x: i32 = 5;` => `∅` + +- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation? + +My solution correctly reject ill-typed programs according to my typing rules. I implemented all of this alone. ## Your borrrow checker - Give a specification for well versus ill formed borrows. (What are the rules the borrow checker should check). -- Demonstrate the cases of ill formed borrows that your borrow checker is able to detect and reject. - -- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation. +Basically, the borrow checker should check the following rules: + +1) when creating a immutable reference to a variable, no mutable reference should exist on this variable. +2) when creating a mutable reference to a variable, no other reference should exist on this variable. -## Your LLVM backend +It can be summarized as: at any given time, you can have _either_ one mutable reference or any number of immutable references (from the [Rust Book](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#the-rules-of-references)). -- Let your backend produces LLVM-IR for your example program. +- Demonstrate the cases of ill formed borrows that your borrow checker is able to detect and reject. -- Describe where and why you introduced allocations and phi nodes. +##### Mutable vs Immutable +✗ The following code will be rejected: two mutable references to the same variable can't exist in the same block. +```rust +fn main() { + let mut a: i32 = 1; + let b: &mut i32 = &mut a; + let c: &mut i32 = &mut a; +} +``` + +✗ Same goes for the two next codes: you can't have an immutable and a mutable reference to the same variable in the same block: +```rust +fn main() { + let mut a: i32 = 1; + let b: &mut i32 = &mut a; + let c: &i32 = &a; +} +``` + +```rust +fn main() { + let mut a: i32 = 1; + let b: &i32 = &a; + let c: &mut i32 = &mut a; +} +``` + +##### Block scope +✗ The following piece of code will be rejected: +```rust +fn main() { + let mut a: i32 = 1; + let b: &mut i32 = &mut a; + { + let c: &mut i32 = &mut a; + }; +} +``` + +✓ But the following piece of code will be accepted: +```rust +fn main() { + let mut a: i32 = 1; + { + let b: &mut i32 = &mut a; + }; + let c: &mut i32 = &mut a; +} +``` + +##### Functions +✗ The following code will be rejected: +```rust +fn main() { + let mut a: i32 = 1; + function(&mut a, &mut a); +} + +fn function(b: &mut i32, c: &mut i32) {} +``` + +✓ But the following piece of code will be accepted: +```rust +fn main() { + let mut a: i32 = 1; + + function(&a, &a); +} + +fn function(b: &i32, c: &i32) {} +``` + +- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation? + +My borrow checker rejects borrow errors according to lexical scoping. I have worked on this alone. -- If you have added optimization passes and/or attributed your code for better optimization (using e.g., `noalias`). +## Your LLVM backend -- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation. +As I hadn't much time to work on the LLVM backend, this part is empty. ## Overal course goals and learning outcomes. @@ -58,12 +316,28 @@ Comment on the alignment of the concrete course goals (taken from the course des - Lexical analysis, syntax analysis, and translation into abstract syntax. +As I had already taken a course on Compiler Construction last year, I already knew most of this part. However, I had only worked with a parser generator (ANTLR 4). Thus, I gained some knowledge on how to actually parse and analyse by hand. + - Regular expressions and grammars, context-free languages and grammars, lexer and parser generators. [Nom is lexer/parser library (and replaces the need for a generator, while lalr-pop is a classical parser generator)] +Same as above: I already new a lot about regular expressions and grammars. However, I didn't know and so never used a lexer/parser library. It's really different from a classical parser generator. That's a part I learned about. While I still prefer classical parser generators for their capability to maintain separated code and grammar, it's still a good point to have worked with Nom. + - Identifier handling and symbol table organization. Type-checking, logical inference systems. [SOS is a logical inference system] +As it was not my part, I hadn't worked on identifier handling, symbol table organization, type-checking nor logical inference systems during last year's project. Having to think about it in this new project was a good thing. +Defining a logical inference system such as I did with the SOS sometimes is cumbersome: writing "if a resolves to n1 and b resolves to n2 then a + b resolves to n1 + n2" looks redundant so I don't really get the point of doing so. + - Intermediate representations and transformations for different languages. [LLVM is a cross language compiler infrastructure] -- Code optimization and register allocation. Machine code generation for common architectures. [LLVM is a cross target compiler infrastructure, doing the "dirty work" of optimazation/register allocation leveraging the SSA form of the LLVM-IR] +I had never worked with LLVM nor a similar tool before this course. I didn't even know they exist! It's great to see and good to know that such a tool exist. As I did not have enough time, I haven't been able to work on this part. It would have been rewarding but harder to set up - LLVM for Rust only works on Linux so I would have had to build a virtual machine and work on it. + +- Code optimization and register allocation. Machine code generation for common architectures. [LLVM is a cross target compiler infrastructure, doing the "dirty work" of optimization/register allocation leveraging the SSA form of the LLVM-IR] +Code optimization, register allocation and machine code generation were parts of my previous course so I already knew about them. +Regarding the work I have done on it, I haven't had time to work on code optimization, register allocation nor machine code generation. + +---- Comment on additional things that you have experienced and learned throughout the course. + +As you can see in my previous answers, I gained little knowledge in Compiler Construction. However, gained knowledge in Rust is huge! Borrowing model is such a powerful one I would have been sad not to have learned about it once in my whole school years. +Rust is still new but it will probably be well-used in the next decade so having done a scholar project using this language is a really good thing. \ No newline at end of file