User-defined functions in Gustave %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% The details of code generation for function definitions and calls are dominated by two things: the layout of the activation record and the info needed to be stored in the symbol table. There are many ways to deal with each of these. I'll describe one approach here. The activation record for a function is preceded by space on the stack for return values and arguments. Then we have the link register and frame pointer followed by local variables. Finally, the last section of the activation record is space for temp values. So, for a function like: my_fun(a1: INTEGER; a2:INTEGER): INTEGER is local x1:INTEGER x2:INTEGER do ... which also needs three temporaries, the activation record on the stack during the execution of my_fun would look like: Offset (relative to fp) ------ | ... | |=========| | Return | +16 |---------| | a2 | +12 |---------| | a1 | +8 |=========| | lr | |---------| fp -> | old fp | |---------| | x1 | -4 |---------| | x2 | -8 |---------| | temp 0 | -12 |---------| | temp 1 | -16 |---------| sp -> | temp 2 | -20 |=========| Code generation for Access expression %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Before calling a function, the calling routine needs to allocate new space on the stack for the return value and arguments of the function. The information on the number of arguments should be available in the symbol table entry for the function. That is, MethodEntry should have a field giving the number of arguments (The corresponding class in the type checker would need to know the types of each, but in code generation, knowing the number is sufficient.) Once the proper amount has been subtracted from sp, the caller needs to evaluate the actual arguments and store them in the appropriate spot on the stack. That is, argument 1 should go in [sp, #0], argument 2 in [sp, #4], argument 3 in [sp, #8], etc. At that point, the "bl" op is generated. Then we need code for the caller to wrap-up the call. That is, add the appropiate amount onto sp to reset the stack pointer and store the return value (which will now be located in [sp, #-4]) into the appropriate temp. The Visitor code for the Access tree will return that temp number just like the other expression visitors do. Code generation for AccessStmt %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% A function call statement is almost identical to an expression. The only difference is that there is no return value. So the generated code will end with the reset of sp - there's no value to set in a temp. Code generation for MethodDec %%%%%%%%%%%%%%%%%%%%%%%%%% When processing the MethodDec tree, the first job is to create the symbol table entry for the function. As mentioned above, this should be a new class MethodEntry that you define to hold the necessary information for code generation. As we saw above, this object should at least hold the number of return values and arguments. It should also have a local symbol table with entries for the return variables, arguments, and local variables. This is what DeclReader does for us. Now we need to generate code for the body of the function. As mentioned in class, we can't really output the initial code for the function until we know how many temporary values we need. So, we should generate the code into a long string, say StringBuffer codeString which we will then print when we're ready. One fairly simple way to do this is to define a function void emit(String s) which just appends s+"\n" to codeString. Then replacing "System.out.println" with "emit" when outputting code lines, will put everything into the holding string. Or you could do something similar with an ArrayList. The body of the function consists of statements and expressions that you know how to handle (and mostly have written). The main changes you need to make in existing code involve IdExpr and Assign trees. The point is that you now have two types of variables: local and global (the instance variables). When processing an IdExpr tree, the first thing you need to determine is whether or not this is a local variable. You do that by looking up the name in the current local symbol table. As mentioned in class, there should be an instance variable in the Coder class for the current local table, say "localTable". When you are processing a MethodDec tree, the localTable is set to the corresponding table. When you are processing the main program body ("make" method), the localTable is set to null. So, in the IdExpr Visitor code, you check if the localTable is not null, then see if "n.name" is listed. If not, you have a global variable and your existing code will work (almost - see comments on temps below). Otherwise, you have a local variable and the table entry will tell you the assigned offset value for that variable. Now, instead of loading a value from "V_"+n.name you are going to load from "[fp, #"+offset+"]" and then store it into the appropriate temp location. Similarly, when generating code for an Assign, the appropriate code to store a value will depend on whether the variable is local or not. About temps %%%%%%%%%%% We have a difference accessing global temps in the main program body (which live at =temps plus some offset) and local temps in a function body (which live in the activation record). One approach would be to check whether localTable is null to decide which case you're in whenever generating access to a temporary. If you do this, you might want to write a method something like emitLoadTemp(String reg, int t) which emits the appropriate code to load temp "t" into register "reg". This would allow the local/global check to live one place in your code. You would also want a similar emitStoreTemp(). What do you do with local temps? Well, the offsets for the local temps should start just after the local variables. So, if instead of initializing the value returned by newTemp to zero when starting a new function, we initialize it to the number of local variables (I think plus 1, to allow for fp on stack), then we can access temp t at [fp, #-4*t]. That is, we use "fp" rather than a register holding the address of temps. So, for instance, in the example above, our first temp in my_fun would actually be 3 rather than 0. Then that temp would be found at offset -4*3 = -12 as shown on the diagram Another possibility would be to avoid needing a special case for global temps by moving the global temps to the stack. That is, we would create a little activation record for the main program with no local variables - just temps. Final printing of function code %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Once you've processed the function body, the corresponding code is in codeString and we're ready to actually output the assembly code for the function. Assuming we're printing my_fun above, we start with basic lines of code: .section .text .align 2 .global FN_my_fun @ Using FN_+name as label .type FN_my_fun, %function FN_my_fun: @ generate starting label push {fp, lr} mov fp, sp sub sp, sp, #20 @ clear space for locals and @ temps. We should subtract @ 4 times (the number of @ locals plus temps) The number of locals is in the symbol table, and the number of temps comes from the getTemps() method of the ExprCoder. In fact, if you used the idea of just adjusting the start value for tempVal to the number of locals + 1, then 4*(ecoder.getTemps()-1) is the number you want here. --- Then you print codeString --- And then you wrap up with: mov sp, fp pop {fp, pc} .size FN_my_fun, .-FN_my_fun @ using the appropiate name -mike slattery - apr 2019