USC Language Reference USCC Compiler
PA1: Recursive Descent Parser PA2: Semantic Analysis
PA5: Optimization Passes PA6: Register Allocation ITP 439
This site uses Just the Docs, a documentation theme for Jekyll.
Theme: auto Google Drive Piazza GitHub Gradescope Blackboard
PA3: LLVM IR
Useful Links
For this assignment, you will need to consult the LLVM documentation. Here are links to the documentation for the version of LLVM (15.x) we are using:
• LLVM Language Reference Manual – The “Instruction Reference” in particular will be useful to determine which instructions you will need to use for the variety of usage cases.
• LLVM Programmers Manual – There’s a lot of information in this document, but make sure you read the class hierarchy section which describes the relationship between User , Value , and
Instruction . The “Helpful Hints for Common Operations” section also has several examples of operations you will need to perform in PA3, PA4, and PA5.
• IRBuilderBase Class Reference – You will use the IRBuilder to create instructions; the base class contains the relevant functions.
Introduction
The endpoint of PA2 was a front-end for USCC that can identify syntactically and semantically valid USC programs. This generates an AST which can be printed to the console, but it cannot actually execute in any meaningful way. In this assignment, you will lower the AST to LLVM IR. This lowering process will lose a great deal of information about the higher-level source program, but the benefit is that the LLVM IR is more easily optimized and can ultimately be converted to machine executable code.
By default, the USCC compiler compiles into a .bc file which contains the LLVM IR (which is also referred to as LLVM bitcode). You can also instruct the driver to pretty-print the IR to stdout using the
-p command line flag. You don’t need to pass the -a or -l flags anymore.
For instance, once you complete this assignment, you could use the following args in Visual Studio
Code to compile and pretty-print the IR for the ssa01.usc program: “args”: [“-p”, “ssa01.usc”]
The output will look something like this:
; ModuleID = ‘main’
source_filename = “main”
@.str = private local_unnamed_addr constant [4 x i8] c”%d\0A\00″
declare i32 @printf(i8*, …)
define i32 @main() {
ret i32 0 }
solutions %
This example also illustrates the main properties of LLVM IR. Each function is represented with a control flow graph (CFG). A basic block in a CFG represents a sequential chain of execution, and it always ends with a terminator instruction, which represents a jump in control flow such as a branch or a return . The terminator instructions thus represent the edges in the CFG.
You’ll also notice that LLVM IR looks very similar to assembly code, with the notable exception that there is an infinite number of registers called virtual registers. One key property of these registers is they can only be assigned to once – this is called SSA form which will become more relevant in PA4 when you implement emitting SSA form. For now, we will largely bypass SSA form by using the stack to store local variables.
The test suite associated with this programming assignment is testEmit.py . This test suite first compiles USC code into LLVM bitcode, and then interprets this bitcode via lli . The output from executing each program is then compared against the expected output. This ensures that the bitcode is both well-formed and functionally correct.
To run this test suite, execute the following command from the tests directory: python3 testEmit.py
When you initially run this test suite, all 21 tests will fail. As you start filling in code in parse/ASTEmit.cpp , more and more tests will pass.
It is also possible to compile LLVM bitcode into platform-specific assembly via the llc tool. Once you have platform-specific assembly, it can be assembled into a native executable.
In this assignment, the vast majority of the code you will write goes in parse/ASTEmit.cpp , which contains the implementations of the emitIR function for each type of ASTNode . The emitIR function takes in a CodeContext by reference and returns a pointer to an llvm::Value . The CodeContext struct is defined in parse/Emitter.h . Of particular note is that the CodeContext contains a pointer to the current basic block called mBlock . This allows each emitIR call to know where the instructions should be added. This also means nodes that create basic blocks must update mBlock. The other member variables are used in some specific cases. For example, mGlobal can be used to access the global LLVM context.
Generally, the recommended way to add instructions to a basic block is by using the IRBuilder class. You can declare an instance of IRBuilder as follows:
IRBuilder<> build(block);
Where block is the basic block you want to add instructions to. Once you create the IRBuilder instance, there are many functions prefixed with Create that can be used to append instructions to the basic block. Conveniently, these functions return a pointer to the instruction (which for most instructions corresponds to a Value pointer).
Implementation
You should implement the emitIR functions in the order outlined below. If you do not follow this order, it will be more difficult to pinpoint the nodes that are not implemented properly.
ASTExprStmt
ASTExprStmt is very straightforward – it simply needs to call emitIR on the mExpr member variable. In this case, since an expression statement simply evaluates and does not return any value, the emitIR function should just return nullptr .
ASTCompoundStmt
A compound statement contains a list of declarations, followed by a list of statements. So this node just needs to emit each of the declarations, and then each of the statements. As with expression statements, we do not expect the node to return any values.
ASTConstantExpr
A constant expression is an expression representing a constant number. To create a ConstantInt , use the ConstantInt::get static method. This requires a type, which you can get using Type::getInt32Ty or getInt8Ty , depending on whether the constant is an integer or a character. Since this ConstantInt will
be used, you must return the pointer to it.
ASTReturnStmt
This is the first node that will actually generate instructions via an IRBuilder . You will want to either CreateRet or CreateRetVoid , depending on the type of the statement. If the return is not void, you
must first emitIR for the expression. This instruction does not need to be returned by emitIR .
If you’ve implemented the nodes up to this point properly, three tests should pass: emit02 , emit05 ,
and emit10 . ASTBinaryMathOp
This node should generate the lhs and rhs expressions, and then create and return appropriate instruction based on the op: add (+), sub (-), mul (*), sdiv (/), and srem (%). Note that for instructions that return values, you can provide a name hint that corresponds to a suggested name for the virtual register that’ll store the result. You should use names that make sense like “add” ,
“sub” , and so on. If the name is already in use, LLVM will automatically append a number to it to keep it unique.
ASTBinaryCmpOp
This is very similar to binary math ops, except you are generating ICmp instructions. One other difference is you need to zero-extend ( Zext ) the result of the ICmp math op into a Int32Ty . The reason for this is to allow for the C-style conversion of comparisons into integers.
At this point, the emit03 test should also pass. Creating Stack Space for Local Variables
For now, all local variables will exist on the stack. This is to work around the fact that LLVM uses an SSA form, meaning each virtual register can only be assigned once. In order to support this, you need to allocate stack space for these variables in parse/Symbols.cpp in ScopeTable::emitIR . Each function has a ScopeTable that encapsulates all of the variables declared within the function. Then when the code for the function is generated, the scope table will emit the static space for local variables.
The emitIR function is already implemented for arrays, because arrays are somewhat complex. You will need to implement it for regular variables. This just involves using an alloca instruction. When you alloca , you will need to also supply the correct LLVM type, which you can get with an
llvmType(true) member function call on the identifier. The identifier’s address should then be set (using setAddress ) to the result of the alloca.
There is one special case to handle, which is the case where the address already exists for the identifier, in which case getAddress() does not return null. This means that the identifier is a function parameter. In this case, the value stored in the address initially is actually the value of the parameter. So after you alloca , you will need to store the existing value into the newly-allocated address before calling setAddress . This creates a copy of the parameter (i.e. pass by value).
Reading/Writing to Local Variables
Reading and writing of local variables is encapsulated by Identifier::readFrom and Identifier::writeTo . As with the previous part, the code for arrays is provided for you. You just need
to create the load and store instructions for normal variables in the else cases. Remember that the address to load and store from is saved in the identifier. The first parameter to CreateLoad is the type stored in the pointer, which in this case is just getAddress()->getType()->getPointerElementType() .
At this point, test015 , emit04 , and emit09 should also pass. ASTIncExpr and ASTDecExpr
For these two nodes, you’ll want to read the identifier using readFrom , generate an add or sub instruction for the value, and then write it back into the identifier with writeTo . Once these two nodes are implemented, emit06 should also pass.
ASTNotExpr
For this node, you can’t simply generate the sub expression and then CreateNot . This is because CreateNot just performs a one’s complement. However, in C anything other than zero should return
zero. To implement this, you must ICmpEQ against zero and then ZExt to a 32-bit integer. ASTWhileStmt
This node will require creating basic blocks via BasicBlock::Create . The control flow of a while loop should be as follows:
This means the predecessor block has an unconditional branch to the while.cond block, which will either branch to while.body or while.end , depending on the result of the condition expression. The
while.body block has an unconditional branch to the while.cond block. Note that when you create a basic block, you can pass in a suggested name for the block via the second parameter. To help with debugging, you should follow the same naming convention as in the diagram.
The other aspect you need to make sure of is that you update the mBlock in the CodeContext as appropriate. So for example, when you are emitting the condition, you need to make sure mBlock is set to the block corresponding to while.cond .
Once while loops are implemented, you should now also pass the emit07 , emit08 , emit09 , opt05 , and opt06 test cases.
ASTAssignStmt
This node simply needs to emit the expression and write the value of the expression to the identifier.
If statements have two possibilities – either there is an if statement by itself, or an if statement with an associated else statement. The control flow of an if statement should be as follows:
As with the while loop, you will need to create the appropriate basic blocks and generate the code in these basic blocks.
Once you implement if statements, your code should now pass all the remaining test cases. Testing on GitHub
Once you pass all the tests locally, you should push your code to GitHub and manually run the PA3 unit tests. If they don’t pass or your code doesn’t compile, you should make the necessary fixes and try again.
For this assignment, there are a total of 21 test cases. Each test failure is -5 points.
Submitting Your Assignment
Once you have a GitHub Actions run you are happy with, to submit the assignment you need to submit this form on Gradescope. You will have to provide the full URL for the actions run you want us to grade, and also tell us if you are using any of your four slip days.
Making a Git Tag
Once you’ve submitted your assignment, you should make a tag so it’s easier to go back to the specific pa3 submission later, should you need to:
git tag pa3
git push origin –tags
Conclusion
You now have a functional compiler in that all USC source programs will compile into LLVM bitcode. If you wanted to, you could also emit actual machine code for your machine via llc. The next two labs will be focused on improvements to the LLVM IR generation. In PA4, you will greatly reduce the number of stack memory instructions by implementing generation of SSA form for local variables. In PA5, you will implement optimization passes that will further improve execution time of the generated bitcode. Finally, in PA6 you will implement register allocation to emit target-specific assembly.
This site is intended for individual educational use only. Redistribution of this content is prohibited without prior approval from the ITP 439 instructors, and may be deemed an academic integrity violation.
0), i32 %y2)
%y = alloca i32, align 4
%x = alloca i32, align 4
store i32 5, i32* %x, align 4
store i32 6, i32* %y, align 4
store i32 7, i32* %x, align 4
store i32 10, i32* %x, align 4
%x1 = load i32, i32* %x, align 4
%tobool = icmp ne i32 %x1, 0
br i1 %tobool, label %if.then, label %if.else
if.then: ; preds = %entry
store i32 20, i32* %y, align 4
br label %if.end
if.else: ; preds = %entry
store i32 15, i32* %y, align 4
br label %if.end
if.end: ; preds = %if.else, %if.then
%y2 = load i32, i32* %y, align 4
%0 = call i32 (i8*, …) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32
The getPointerElementType() function is deprecated (but still works) in LLVM 15.x and will be removed in LLVM 17.x. This is part of LLVM’s shift towards opaque pointers in the IR, which you can read more about here.
Make sure you submit this form, as otherwise we will not know that you have a submission you want us to grade. We only grade based on your GitHub Actions run and code on GitHub, and we will not download everyone’s repo. If your code does not compile or you do not submit the form, you will get a 0 on the assignment.
PA3: LLVM IR