- Give an example that showcases all rules of your EBNF. The program should "do" something as used in the next exercise.
```rust
fnfibonacci(n:i32)->i32{
if(n==0){
return0;
}elseif(n==1){
return1;
}else{
returnfibonacci(n-2)+fibonacci(n-1);
}
}
fnmain(){
letfib_20:i32=fibonacci(20);
if(fib_20==6765){
print("fib_20_is_6765");
}
if(fib_20>7000){
print("fib_20_LT_7000");
}else{
print("fib_20_GT_7000");
fnmain()->i32{
returntest(1,true);// Should return 12
}
letmutand_res:bool=and_op(true,true);
fntest(muta:i32,b:bool)->i32{
a=a+1;
letc:bool=b&&true;
while(and_res){
and_res=false;
if(c){
leta:i32=100;
}
print("and_res_after_while_loop");
print(and_res);
letunary_res:i32=unary_op(1);
if(unary_res==-1){
print("unary_ok");
if(a==100){
a=0;
}else{
print("unary_failed");
a=a+1;
}
letprec_test:i32=prec_test();
if(prec_test==27){
print("prec_ok");
}else{
print("prec_failed");
}
}
a=prec(a);
fnand_op(a:bool,b:bool)->bool{
returna&&b;
if(a<=2){
a=-1;
}elseif(a==0){
a=-2;
}else{
a=a+1;
}
fnunary_op(x:i32)->i32{
return-x;
returna;
}
fnprec_test()->i32{
return2*10-3+2*5;
fnprec(a:i32)->i32{
return2+a*3;
}
```
- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation.
The parser was implemented using the [LALRPOP](https://github.com/lalrpop/lalrpop) parser generator. By following the LALRPOP tutorial, to learn the syntax and some of the functionality, an expression parser was implemented. Thus I didn't implemented the expression parser myself but I built upon it and extended it to implement the rest of the parser for my language.
The parser was implemented using the [LALRPOP](https://github.com/lalrpop/lalrpop) parser generator witch uses LR(1) by default. By following the LALRPOP tutorial, to learn the syntax and some of the functionality, an expression parser was implemented. Thus I didn't implemented the expression parser myself but I built upon it and extended it to implement the rest of the parser for my language.
When it comes precedence of the expression parser the tutorial already had it implemented for numerical expressions. When I extended the parser to support logical and relational operations the precedence of those operations works if you don't mix logical and relation operations together. So for the most part in my language sub expressions have to be parenthesized if they contain those operations.
When it comes precedence of the expression parser the tutorial already had it implemented for numerical expressions. When I extended the parser to support logical and relational operations the precedence of those operations will not always work, so for the most part in my language sub expressions have to be parenthesized if they contain those operations.
No error messages, except the already existing error messages from LALRPOP, have been implemented in the parser. And thus no error recovery is possible since the parser just forwards the LALRPOP error message and stops if an error occurs while parsing the program.
- 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.
The example program will be started by calling the main function. The first line is an assignment of variable `fib_20` which according to the SoS will be assigned the value of the function call `fibonacci(20)`. Thus the function call is evaluated and assigned to the variable. When the call is made the argument is `n = 20` which according to the SoS for equals (==) will evaluate to false for both the if- and elseif-condition inside the function body. Because of this the else-statement is executed and the recursion continues. After the recursion have reached the base case the function will return a `i32`. The variable `fib_20` now exists inside the scope of the main functions context.
The example program will be started by calling the main function. The main function contanins a return statement which according to the SoS will be evaluated and returned as the result of calling the main function. Since the expression of the return statement is a function call the function will be called with the arguments supplied according to the SoS. In this case the arguments are 1 and true.
The condition of the next if-statement is now evaluated and since `fib_20 = 6765` in the scope the condition will evaluate to true according to the SoS and the print command inside the if-body is executed. Since `fib_20` not is greater than 7000 the else-statement is executed in the next if-else statement.
The function `test` will then be called and since it takes a mutable i32 `a` and boolean `b` the functions context will start of with containing a scope consisting of these variables with the values of the arguments. Then `a` is assigned a new value according to the SoS which will be evaluated to `a = 2`. A new variable `c` is then declared with a let statement which according to the SoS will be assigned the value of the expression `b && true`.
After this a new mutable variable `and_res` is declared. The variable is assigned the value of the function call. The while-condition is then checked and since `and_res = true` the body is executed according to the SoS. In the body the variable is assigned a new value, which is fine since it's declared as mutable, but since the new value is false the while-condition will not evaluate to true and the while-body is not executed again. The value of `and_res` is then printed out to ensure that the value of the mutable variable was changed.
Then an if statement will be evaluated by computing the condtion `c` which evaluates to true. Then, according to the SoS, the then statement will be executed which contains a new variable declaration of the same name as the earlier declared variable `a`. But since the then statement is executed inside another scope the variable will not replace the value of `a` outside this scope.
This continues on to cover more of the EBNF but the operations is mostly repetition so I've skipped explaining them.
Another if statement is then evaluated but since the value in a did not get changed in this scope the else statement is executed according to the SoS which updates variable `a` to `a = 3`. Then `a` is once again assigned a new value which this time is a function call. After this `a = 11`. And another if statement is evaluated but since the conditions of if and else if won't be true the else statement will be executed. Now `a = 12` and a is returned.
- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation.
I implemented the interpreter myself and the interpreter executes programs according to the SOS defined above. Additionally it can also handle else-if statements, scoping and contexts (different variable values depending on the scope and context etc.).
I implemented the interpreter myself and the interpreter executes programs according to the SOS defined above. Additionally it can also handle else-if statements, scoping and contexts. In my implementation a context is simply a vector of scopes which allows for shadowing etc.
The interpreter also panics when encountering a evaluation error. For example, if the program contains
```rust
...
...
@@ -478,18 +459,21 @@ The type checker was fully implemented by me and it rejects ill-typed programs a
It is also able to report multiple type errors while at the same time not report other errors originating from an earlier detected error. This was done by maintaining a vector containing all the detected type errors during the type checking of the program. Then every time a type error is detected the error is pushed onto the vector and the type checker determines, if possible, the type that would have resulted if the expression was correctly typed. To demonstrate this, consider the following code
```rust
let a: bool = 5 + 5;
let b: bool = a && true;
let c: i32 = b + 1;
let a: i32 = 5 + 5* true;
let b: i32 = a + 10;
let c: bool = b == true;
```
running this code snippet will generate the following result
```
Error: Mismatched type for variable 'a'
Note: expected bool, found i32
Error: Binary operation '+' cannot be applied to type 'bool'
Error: Binary operation '*' cannot be applied to type 'bool'
Error: Mismatched type for operation '=='
Note: expected i32, found bool
Could not compile 'input.rs'
```
By this it can be seen that the type checker assumes that the type of ```a``` is correct when checking the assignment of ```b``` which indeed would be correctly typed if the former variable would have been correctly assigned a boolean value. Thus by following this pattern the type checker is able to continue type checking and reporting new errors, which can be seen when it reports that the assignment of ```c``` is incorrect (addition with boolean is not possible).
By this it can be seen that the type checker assumes that the type of ```a``` is correct when checking the assignment of ```b``` which indeed would be correctly typed if the former variable would have been correctly assigned a i32 value. This is done by concluding that `5 * true` would evaluate to a i32 value if it was correctly typed and then the entire expression `5 + 5 * true` will be correctly typed.
Thus by following this pattern the type checker is able to continue type checking and reporting new errors, which can be seen when it reports that the assignment of ```c``` is incorrect (boolean equals requires the same type on each side).
The type checker also makes sure that functions with a specified return type always returns. This is done by going through each function and making sure that a tailing return statement or a return inside an else-statement exists. If this is not the case the error is reported along with all other type errors found.
...
...
@@ -503,7 +487,134 @@ It also needs to keep track of the kind of borrow that have been made to a resou
**I did not implement a borrow checker for my language so the next rest of the questions regarding the borrow checker could not be answered.**
# Your LLVM backend
TODO
- Let your backend produces LLVM-IR for your example program.
- Describe where and why you introduced allocations and phi nodes.
Phi nodes were introduced in order to assign the correct value to a variables dependent on what control flow the execution took. This was used while compiling if-, elseif-, else- and while statements. Since the same variable could be used inside the different blocks of these satements and the resulting value of the variable should depend on what block the execution came from when entering the phi node.
Allocations were used whenever new variables were defined using let statements and when a function with parameters were defined. This is because all the local variables has to be allocated on the stack in order to use and manipulate them later in the code. The allocation also returns the pointer to the variables memory on the stack so its value can be updated using that pointer.
- If you have added optimization passes and/or attributed your code for better optimization (using e.g., noalias).
I did not add any optimization.
- Compare your solution to the requirements (as stated in the README.md). What are your contributions to the implementation.
When implementing the LLVM backend I started of by implementing the `crust.rs` example provided by you. From this I adapted it a bit to work with my AST in order to compile expressions. Then I started added functionality and during this I took a lot of inspiration from the inkwell [kaleidoscope](https://github.com/TheDan64/inkwell/tree/master/examples/kaleidoscope) example. But once again it had to be adapted to my AST and also add some other functionality such as while loops and else-if statements. The example only compiles one function so it also had to be reworked in order to compile entire programs containing multiple functions.
The LLVM backend can generate and compile code from my language but no optimization have been implemented. Also, since my language does not have a borrow checker aliasing is possible so noalias could not be passed anywhere.
# Overal course goals and learning outcomes
TODO
\ No newline at end of file
In this course I have learned a lot when it comes to programming since I have implemented a quite large project myself. Comparing this to other courses I enjoyed the heavy practical work required to implement the compiler. I also learned a lot of Rust since I have no prior experiance of the language. Although it should be noted that the learning curve in the beginning of the course was quite big since I had to learn Rust.
Regarding the course goals and what I have done:
- Parser:
- A lot of work with creating a suitable AST
- Maybe not so much learnt about lexical and syntax analysis since I used LALRPOP which basically did this for me without having to "know" a lot.
- EBNF
- Interpreter
- Had to come up with a way of handling variables in order to allow for shadowing etc.
- Type checker
- Worker a lot with figuring out a way to report multiple errors
- How to display errors in a suitable and easy way
- LLVM IR and JIT compilation
In general it feels like I learned a lot more about the practical parts of a compiler than the theoretical part. Mostly because I spent the most of the course on implementing the compiler and not really thinking or creating the theoretical analysis like SoS and EBNF.