Compilers and computer architecture: A realistic compiler to MIPS

Similar documents
Code Generation. The Main Idea of Today s Lecture. We can emit stack-machine-style code for expressions via recursion. Lecture Outline.

We can emit stack-machine-style code for expressions via recursion

Code Generation. Lecture 12

Code Generation. Lecture 19

Functions in MIPS. Functions in MIPS 1

Lecture Outline. Topic 1: Basic Code Generation. Code Generation. Lecture 12. Topic 2: Code Generation for Objects. Simulating a Stack Machine

Lecture 5. Announcements: Today: Finish up functions in MIPS

Lectures 5. Announcements: Today: Oops in Strings/pointers (example from last time) Functions in MIPS

Today. Putting it all together

COMP 303 Computer Architecture Lecture 3. Comp 303 Computer Architecture

Do-While Example. In C++ In assembly language. do { z--; while (a == b); z = b; loop: addi $s2, $s2, -1 beq $s0, $s1, loop or $s2, $s1, $zero

MIPS Programming. A basic rule is: try to be mechanical (that is, don't be "tricky") when you translate high-level code into assembler code.

Implementing Procedure Calls

CSE Lecture In Class Example Handout

Course Administration

Calling Conventions. Hakim Weatherspoon CS 3410, Spring 2012 Computer Science Cornell University. See P&H 2.8 and 2.12

Function Calling Conventions 1 CS 64: Computer Organization and Design Logic Lecture #9

Control Instructions. Computer Organization Architectures for Embedded Computing. Thursday, 26 September Summary

Control Instructions

CSE Lecture In Class Example Handout

MIPS Functions and Instruction Formats

Instructions: Assembly Language

The Activation Record (AR)

Subroutines. int main() { int i, j; i = 5; j = celtokel(i); i = j; return 0;}

CA Compiler Construction

Code Generation. Lecture 30

Chapter 2. Computer Abstractions and Technology. Lesson 4: MIPS (cont )

Branch Addressing. Jump Addressing. Target Addressing Example. The University of Adelaide, School of Computer Science 28 September 2015

Compilers and computer architecture Code-generation (2): register-machines

Lecture 5: Procedure Calls

Instruction Set Architectures (4)

Lecture Outline. Code Generation. Lecture 30. Example of a Stack Machine Program. Stack Machines

Prof. Kavita Bala and Prof. Hakim Weatherspoon CS 3410, Spring 2014 Computer Science Cornell University. See P&H 2.8 and 2.12, and A.

Chapter 2A Instructions: Language of the Computer

Run-time Environments

Run-time Environments

The remote testing experiment. It works! Code Generation. Lecture 12. Remote testing. From the cs164 newsgroup

Procedure Calls Main Procedure. MIPS Calling Convention. MIPS-specific info. Procedure Calls. MIPS-specific info who cares? Chapter 2.7 Appendix A.

ECE232: Hardware Organization and Design

CS61C : Machine Structures

MIPS Functions and the Runtime Stack

ECE331: Hardware Organization and Design

Computer Architecture

Lecture 7: Procedures and Program Execution Preview

Run-time Environments. Lecture 13. Prof. Alex Aiken Original Slides (Modified by Prof. Vijay Ganesh) Lecture 13

CPS311 Lecture: Procedures Last revised 9/9/13. Objectives:

CS 61c: Great Ideas in Computer Architecture

The plot thickens. Some MIPS instructions you can write cannot be translated to a 32-bit number

Memory Usage 0x7fffffff. stack. dynamic data. static data 0x Code Reserved 0x x A software convention

Lecture 7: Procedures

Midterm II CS164, Spring 2006

MIPS R-format Instructions. Representing Instructions. Hexadecimal. R-format Example. MIPS I-format Example. MIPS I-format Instructions

CS 61C: Great Ideas in Computer Architecture Strings and Func.ons. Anything can be represented as a number, i.e., data or instruc\ons

ECE260: Fundamentals of Computer Engineering

Chapter 2. Instructions: Language of the Computer. Adapted by Paulo Lopes

CS153: Compilers Lecture 8: Compiling Calls

CS64 Week 5 Lecture 1. Kyle Dewey

The plot thickens. Some MIPS instructions you can write cannot be translated to a 32-bit number

Code Generation Super Lectures

CS61C : Machine Structures

MIPS Assembly (Functions)

MIPS Procedure Calls. Lecture 6 CS301

Instruction Set Architecture

Machine Language Instructions Introduction. Instructions Words of a language understood by machine. Instruction set Vocabulary of the machine

EE 361 University of Hawaii Fall

Code Generation & Parameter Passing

Topic 3-a. Calling Convention 2/29/2008 1

Thomas Polzer Institut für Technische Informatik

Lecture 5: Procedure Calls

CSCI 402: Computer Architectures. Instructions: Language of the Computer (3) Fengguang Song Department of Computer & Information Science IUPUI.

Computer Architecture. Chapter 2-2. Instructions: Language of the Computer

Compiling Code, Procedures and Stacks

SPIM Procedure Calls

Function Calling Conventions 2 CS 64: Computer Organization and Design Logic Lecture #10

comp 180 Lecture 10 Outline of Lecture Procedure calls Saving and restoring registers Summary of MIPS instructions

CS 61C: Great Ideas in Computer Architecture. MIPS Instruction Formats

We will study the MIPS assembly language as an exemplar of the concept.

Procedure Calling. Procedure Calling. Register Usage. 25 September CSE2021 Computer Organization

CS 316: Procedure Calls/Pipelining

Run-time Environment

Procedures and Stacks

Functions and the MIPS Calling Convention 2 CS 64: Computer Organization and Design Logic Lecture #11

See P&H 2.8 and 2.12, and A.5-6. Prof. Hakim Weatherspoon CS 3410, Spring 2015 Computer Science Cornell University

Review of Activation Frames. FP of caller Y X Return value A B C

Computer Science 2500 Computer Organization Rensselaer Polytechnic Institute Spring Topic Notes: MIPS Programming

CS 61C: Great Ideas in Computer Architecture More MIPS, MIPS Functions

CS61C Machine Structures. Lecture 12 - MIPS Procedures II & Logical Ops. 2/13/2006 John Wawrzynek. www-inst.eecs.berkeley.

COL728 Minor2 Exam Compiler Design Sem II, Answer all 5 questions Max. Marks: 20

CS 61c: Great Ideas in Computer Architecture

CS 110 Computer Architecture Lecture 6: More MIPS, MIPS Functions

ECE 15B Computer Organization Spring 2010

LAB C Translating Utility Classes

UCB CS61C : Machine Structures

Anne Bracy CS 3410 Computer Science Cornell University

Code Genera*on for Control Flow Constructs

ELEC / Computer Architecture and Design Fall 2013 Instruction Set Architecture (Chapter 2)

CS 110 Computer Architecture MIPS Instruction Formats

Lecture 2. Instructions: Language of the Computer (Chapter 2 of the textbook)

Rui Wang, Assistant professor Dept. of Information and Communication Tongji University.

Introduction to the MIPS. Lecture for CPSC 5155 Edward Bosworth, Ph.D. Computer Science Department Columbus State University

Transcription:

1 / 1 Compilers and computer architecture: A realistic compiler to MIPS Martin Berger November 2017

Recall the function of compilers 2 / 1

3 / 1 Recall the structure of compilers Source program Lexical analysis Intermediate code generation Syntax analysis Optimisation Semantic analysis, e.g. type checking Code generation Translated program

4 / 1 Introduction Now we look at more realistic code generation. In the previous two lectures we investigated key issues in compilation for more realistic source and target languages, such as procedures and memory alignment. We also introduced the MIPS architecture, which has an especially clean instruction set architecture, is widely used (embedded systems, PS2, PSP), and has deeply influenced other CPU architectures (e.g. ARM).

5 / 1 Source language The language we translate to MIPS is a simple imperative language with integers as sole data type and recursive procedures with arguments. Here s its grammar. P D; P D D def ID (A) = E A A ne ɛ A ne ID, A ne ID E INT ID if E = E then E else E E + E E E ID(EA) x := E EA ɛ EA ne EA ne E E, EA ne Here ID ranges over identifiers, and INT over integers.

6 / 1 Source language The language we translate to MIPS is a simple imperative language with integers as sole data type and recursive procedures with arguments. Here s its grammar. P D; P D D def ID (A) = E A A ne ɛ A ne ID, A ne ID E INT ID if E = E then E else E E + E E E ID(EA) x := E EA ɛ EA ne EA ne E E, EA ne Here ID ranges over identifiers, and INT over integers. The first declared procedure is the entry point (i.e. will be executed when the program is run) and must take 0 arguments. Procedure names must be distinct.

Source language The language we translate to MIPS is a simple imperative language with integers as sole data type and recursive procedures with arguments. Here s its grammar. P D; P D D def ID (A) = E A A ne ɛ A ne ID, A ne ID E INT ID if E = E then E else E E + E E E ID(EA) x := E EA ɛ EA ne EA ne E E, EA ne Here ID ranges over identifiers, and INT over integers. The first declared procedure is the entry point (i.e. will be executed when the program is run) and must take 0 arguments. Procedure names must be distinct. All variables are of type integer and procedures return integers. We assume that the program passed semantic analysis. 7 / 1

8 / 1 Example program def myfirstprog () = fib ( 3 ); def fib ( n ) = if n = 0 then 1 else if n = 1 then 1 else fib ( n-1 ) + fib ( n-2 )

9 / 1 Generating code for the language We use MIPS as an accumulator machine. So we are using only a tiny fraction of MIPS s power. This is to keep the compiler easy.

10 / 1 Generating code for the language We use MIPS as an accumulator machine. So we are using only a tiny fraction of MIPS s power. This is to keep the compiler easy. Recall that in an accumulator machine all operations:

11 / 1 Generating code for the language We use MIPS as an accumulator machine. So we are using only a tiny fraction of MIPS s power. This is to keep the compiler easy. Recall that in an accumulator machine all operations: the first argument is assumed to be in the accumulator;

12 / 1 Generating code for the language We use MIPS as an accumulator machine. So we are using only a tiny fraction of MIPS s power. This is to keep the compiler easy. Recall that in an accumulator machine all operations: the first argument is assumed to be in the accumulator; all remaining arguments sit on the (top of the) stack;

13 / 1 Generating code for the language We use MIPS as an accumulator machine. So we are using only a tiny fraction of MIPS s power. This is to keep the compiler easy. Recall that in an accumulator machine all operations: the first argument is assumed to be in the accumulator; all remaining arguments sit on the (top of the) stack; the result of the operation is stored in the accumulator;

14 / 1 Generating code for the language We use MIPS as an accumulator machine. So we are using only a tiny fraction of MIPS s power. This is to keep the compiler easy. Recall that in an accumulator machine all operations: the first argument is assumed to be in the accumulator; all remaining arguments sit on the (top of the) stack; the result of the operation is stored in the accumulator; after finishing the operation, all arguments are removed from the stack. The code generator we will be presenting guarantees that all these assumptions always hold.

15 / 1 Generating code for the language To use MIPS as an accumulator machine we need to decide what registers to use as stack pointer and accumulator. We make the following assumptions (which are in line with the assumptions the MIPS community makes, see previous lecture slides).

16 / 1 Generating code for the language To use MIPS as an accumulator machine we need to decide what registers to use as stack pointer and accumulator. We make the following assumptions (which are in line with the assumptions the MIPS community makes, see previous lecture slides). We use the general purpose register $a0 as accumulator.

17 / 1 Generating code for the language To use MIPS as an accumulator machine we need to decide what registers to use as stack pointer and accumulator. We make the following assumptions (which are in line with the assumptions the MIPS community makes, see previous lecture slides). We use the general purpose register $a0 as accumulator. We use the general purpose register $sp as stack pointer.

18 / 1 Generating code for the language To use MIPS as an accumulator machine we need to decide what registers to use as stack pointer and accumulator. We make the following assumptions (which are in line with the assumptions the MIPS community makes, see previous lecture slides). We use the general purpose register $a0 as accumulator. We use the general purpose register $sp as stack pointer. The stack pointer always points to the first free byte above the stack.

19 / 1 Generating code for the language To use MIPS as an accumulator machine we need to decide what registers to use as stack pointer and accumulator. We make the following assumptions (which are in line with the assumptions the MIPS community makes, see previous lecture slides). We use the general purpose register $a0 as accumulator. We use the general purpose register $sp as stack pointer. The stack pointer always points to the first free byte above the stack. The stack grows downwards.

20 / 1 Generating code for the language To use MIPS as an accumulator machine we need to decide what registers to use as stack pointer and accumulator. We make the following assumptions (which are in line with the assumptions the MIPS community makes, see previous lecture slides). We use the general purpose register $a0 as accumulator. We use the general purpose register $sp as stack pointer. The stack pointer always points to the first free byte above the stack. The stack grows downwards. We could have made other choices.

21 / 1 Assumption about data types Our source language has integers.

22 / 1 Assumption about data types Our source language has integers. We will translate them to the built-in 32 bit MIPS data-type.

23 / 1 Assumption about data types Our source language has integers. We will translate them to the built-in 32 bit MIPS data-type. Other choices are possible (e.g. 64 bits, infinite precision, 16 bit etc). This one is by far the simplest.

24 / 1 Assumption about data types Our source language has integers. We will translate them to the built-in 32 bit MIPS data-type. Other choices are possible (e.g. 64 bits, infinite precision, 16 bit etc). This one is by far the simplest. For simplicity, we won t worry about over/underflow of arithmetic operations.

25 / 1 Code generation Let s start easy and generate code expressions.

26 / 1 Code generation Let s start easy and generate code expressions. For simplicity we ll ignore some issues like placing alignment commands.

27 / 1 Code generation Let s start easy and generate code expressions. For simplicity we ll ignore some issues like placing alignment commands. As with the translation to an idealised accumulator machine a few weeks ago, we compile expressions by recursively walking the AST. We want to write the following: def genexp ( e : Exp ) = if e is of form IntLiteral ( n ) then... Variable ( x ) then... If ( cond, thenbody, elsebody ) then... Add ( l, r ) then... Sub ( l, r ) then... Call ( f, args ) then... } }

28 / 1 Code generation: integer literals Let s start with the simplest case. def genexp ( e : Exp ) = if e is of form IntLiteral ( n ) then li $a0 n

29 / 1 Code generation: integer literals Let s start with the simplest case. def genexp ( e : Exp ) = if e is of form IntLiteral ( n ) then li $a0 n Convention: code in red is MIPS code to be executed at run-time. Code in black is compiler code. We are also going to be a bit sloppy about the datatype MIPS_I of MIPS instructions.

30 / 1 Code generation: integer literals Let s start with the simplest case. def genexp ( e : Exp ) = if e is of form IntLiteral ( n ) then li $a0 n Convention: code in red is MIPS code to be executed at run-time. Code in black is compiler code. We are also going to be a bit sloppy about the datatype MIPS_I of MIPS instructions. This preserves all invariants to do with the stack and the accumulator as required. Recall that li is a pseudo instruction and will be expanded by the assembler into several real MIPS instructions.

31 / 1 Code generation: addition def genexp ( e : Exp ) = if e is of form Add ( l, r ) then genexp ( l ) sw $a0 0($sp) addiu $sp $sp -4 genexp ( r ) lw $t1 4($sp) add $a0 $t1 $a0 addiu $sp $sp 4 Note that this evaluates from left to right! Recall also that the stack grows downwards and that the stack pointer points to the first free memory cell above the stack.

32 / 1 Code generation: addition def genexp ( e : Exp ) = if e is of form Add ( l, r ) then genexp ( l ) sw $a0 0($sp) addiu $sp $sp -4 genexp ( r ) lw $t1 4($sp) add $a0 $t1 $a0 addiu $sp $sp 4 Note that this evaluates from left to right! Recall also that the stack grows downwards and that the stack pointer points to the first free memory cell above the stack. Question: Why not store the result of compiling the left argument directly in $t0?

33 / 1 Code generation: addition def genexp ( e : Exp ) = if e is of form Add ( l, r ) then genexp ( l ) sw $a0 0($sp) addiu $sp $sp -4 genexp ( r ) lw $t1 4($sp) add $a0 $t1 $a0 addiu $sp $sp 4 Note that this evaluates from left to right! Recall also that the stack grows downwards and that the stack pointer points to the first free memory cell above the stack. Question: Why not store the result of compiling the left argument directly in $t0? Consider 1+(2+3)

34 / 1 Code generation: minus We want to translate e e. We need new MIPS command: sub reg1 reg2 reg3 It subtracts the content of reg3 from the content of reg2 and stores the result in reg1. I.e. reg1 := reg2 - reg3.

35 / 1 Code generation: minus def genexp ( e : Exp ) = if e is of form Minus ( l, r ) then genexp ( l ) sw $a0 0 $sp addiu $sp $sp -4 genexp ( r ) lw $t1 4($sp) sub $a0 $t1 $a0 // only change from addition addiu $sp $sp 4 Note that sub $a0 $t1 $a0 deducts $a0 from $t1.

36 / 1 Code generation: conditional We want to translate if e 1 = e 2 then e else e. We need two new MIPS commands: beq reg1 reg2 label b label beq branches (= jumps) to label if the content of reg1 is identical to the content of reg2. Otherwise it does nothing and moves on to the next command.

37 / 1 Code generation: conditional We want to translate if e 1 = e 2 then e else e. We need two new MIPS commands: beq reg1 reg2 label b label beq branches (= jumps) to label if the content of reg1 is identical to the content of reg2. Otherwise it does nothing and moves on to the next command. In contrast b makes an unconditional jump to label.

38 / 1 Code generation: conditional def genexp ( e : Exp ) = if e is of form If ( l, r, thenbody, elsebody ) then val elsebranch = newlabel () // not needed val thenbranch = newlabel () val exitlabel = newlabel () genexp ( l ) sw $a0 0($sp) addiu $sp $sp -4 genexp ( r ) lw $t1 4($sp) addiu $sp $sp 4 beq $a0 $t1 thenbranch elsebranch + ":" genexp ( elsebody ) b exitlabel thenbranch + ":" genexp ( thenbody ) exitlabel + ":" }

newlabel returns new, distinct string every time it is called. 39 / 1 Code generation: conditional def genexp ( e : Exp ) = if e is of form If ( l, r, thenbody, elsebody ) then val elsebranch = newlabel () // not needed val thenbranch = newlabel () val exitlabel = newlabel () genexp ( l ) sw $a0 0($sp) addiu $sp $sp -4 genexp ( r ) lw $t1 4($sp) addiu $sp $sp 4 beq $a0 $t1 thenbranch elsebranch + ":" genexp ( elsebody ) b exitlabel thenbranch + ":" genexp ( thenbody ) exitlabel + ":" }

40 / 1 Code generation: procedure calls/declarations The code a compiler emits for procedure calls and declarations depends on the layout of the activation record (AR).

41 / 1 Code generation: procedure calls/declarations The code a compiler emits for procedure calls and declarations depends on the layout of the activation record (AR). The AR stores all the data that s needed to execute an invocation of a procedure.

42 / 1 Code generation: procedure calls/declarations The code a compiler emits for procedure calls and declarations depends on the layout of the activation record (AR). The AR stores all the data that s needed to execute an invocation of a procedure. ARs are held on the stack, because procedure entries and exits are adhere to a bracketing discipline.

43 / 1 Code generation: procedure calls/declarations The code a compiler emits for procedure calls and declarations depends on the layout of the activation record (AR). The AR stores all the data that s needed to execute an invocation of a procedure. ARs are held on the stack, because procedure entries and exits are adhere to a bracketing discipline. Note that invocation result and (some) procedure arguments are often passed in register not in AR (for efficiency) main f( 3 ) f( 2 ) Main's AR Result Argument: 3 Return address Result Argument: 2 Return address

Code generation: procedure calls/declarations For our simple language, we can make do with a simple AR layout: 44 / 1

45 / 1 Code generation: procedure calls/declarations For our simple language, we can make do with a simple AR layout: The result is always in the accumulator, so no need for to store the result in the AR.

46 / 1 Code generation: procedure calls/declarations For our simple language, we can make do with a simple AR layout: The result is always in the accumulator, so no need for to store the result in the AR. The only variables in the language are procedure parameters. We hold them in AR: for the procedure call f(e 1,..., e n ) just push the result of evaluating e 1,..., e n onto the stack.

47 / 1 Code generation: procedure calls/declarations For our simple language, we can make do with a simple AR layout: The result is always in the accumulator, so no need for to store the result in the AR. The only variables in the language are procedure parameters. We hold them in AR: for the procedure call f(e 1,..., e n ) just push the result of evaluating e 1,..., e n onto the stack. The AR needs to store the return address.

48 / 1 Code generation: procedure calls/declarations For our simple language, we can make do with a simple AR layout: The result is always in the accumulator, so no need for to store the result in the AR. The only variables in the language are procedure parameters. We hold them in AR: for the procedure call f(e 1,..., e n ) just push the result of evaluating e 1,..., e n onto the stack. The AR needs to store the return address. The stack calling discipline ensures that on procedure exit $sp is the same as on procedure entry.

49 / 1 Code generation: procedure calls/declarations For our simple language, we can make do with a simple AR layout: The result is always in the accumulator, so no need for to store the result in the AR. The only variables in the language are procedure parameters. We hold them in AR: for the procedure call f(e 1,..., e n ) just push the result of evaluating e 1,..., e n onto the stack. The AR needs to store the return address. The stack calling discipline ensures that on procedure exit $sp is the same as on procedure entry. Also: no registers need to be preserved in accumulator machines. Why?

Code generation: procedure calls/declarations For our simple language, we can make do with a simple AR layout: The result is always in the accumulator, so no need for to store the result in the AR. The only variables in the language are procedure parameters. We hold them in AR: for the procedure call f(e 1,..., e n ) just push the result of evaluating e 1,..., e n onto the stack. The AR needs to store the return address. The stack calling discipline ensures that on procedure exit $sp is the same as on procedure entry. Also: no registers need to be preserved in accumulator machines. Why? Because no register is used except for the accumulator and $t0, and when a procedure is invoked, all previous evaluations of expressions are already discharged or tucked away on the stack. 50 / 1

51 / 1 Code generation: procedure calls/declarations So ARs for a procedure with n arguments look like this: caller s FP argument n... argument 1 return address A pointer to the top of current AR (i.e. where the return address sits) is useful (though not necessary) see later. This pointer is called frame pointer and lives in register $fp. We need to restore the caller s FP on procedure exit, so we store it in the AR upon procedure entry. The FP makes accessing variables easier (see later).

52 / 1 Code generation: procedure calls/declarations So ARs for a procedure with n arguments look like this: caller s FP argument n... argument 1 return address A pointer to the top of current AR (i.e. where the return address sits) is useful (though not necessary) see later. This pointer is called frame pointer and lives in register $fp. We need to restore the caller s FP on procedure exit, so we store it in the AR upon procedure entry. The FP makes accessing variables easier (see later). Arguments are stored in reverse order to make indexing a bit easier.

53 / 1 Code generation: procedure calls/declarations Let s look at an example: assume we call f (7, 100, 33) Caller's AR Caller's AR FP Caller's FP Third argument: 33 Jump Caller's FP Third argument: 33 Second argument: 100 First argument: 7 Second argument: 100 First argument: 7 Caller's responsibility SP FP SP Return address Callee's responsibility

54 / 1 Code generation: procedure calls/declarations To be able to get the return addess for a procedure call easily, we need a new MIPS instruction: jal label Note that jal stands for jump and link. This instruction does the following:

55 / 1 Code generation: procedure calls/declarations To be able to get the return addess for a procedure call easily, we need a new MIPS instruction: jal label Note that jal stands for jump and link. This instruction does the following: Jumps unconditionally to label, stores the address of next instruction (syntactically following jal label) in register $ra.

56 / 1 Code generation: procedure calls/declarations To be able to get the return addess for a procedure call easily, we need a new MIPS instruction: jal label Note that jal stands for jump and link. This instruction does the following: Jumps unconditionally to label, stores the address of next instruction (syntactically following jal label) in register $ra. On many other architectures, the return address is automatically placed on the stack by a call instruction.

57 / 1 Code generation: procedure calls/declarations To be able to get the return addess for a procedure call easily, we need a new MIPS instruction: jal label Note that jal stands for jump and link. This instruction does the following: Jumps unconditionally to label, stores the address of next instruction (syntactically following jal label) in register $ra. On many other architectures, the return address is automatically placed on the stack by a call instruction. On MIPS we must push the return address on stack explicitly. This can only be done by callee, because address is available only after jal has executed.

58 / 1 Code generation: procedure calls Example of procedure call with 3 arguments. General case is similar.

59 / 1 Code generation: procedure calls Example of procedure call with 3 arguments. General case is similar. case Call ( f, List ( e1, e2, e3 ) ) then sw $fp 0($sp) // save FP on stack addiu $sp $sp -4 genexp ( e3 ) // we choose right-to-left ev. order sw $a0 0($sp) // save 3rd argument on stack addiu $sp $sp -4 genexp ( e2 ) sw $a0 0($sp) // save 2nd argument on stack addiu $sp $sp -4 genexp ( e1 ) sw $a0 0($sp) // save 1st argument on stack addiu $sp $sp -4 jal ( f + "_entry" ) // jump to f, save return // addr in $ra

Code generation: procedure calls Several things are worth noting. 60 / 1

Code generation: procedure calls Several things are worth noting. The caller first saves the FP (i.e. pointer to top of its own AR). 61 / 1

Code generation: procedure calls Several things are worth noting. The caller first saves the FP (i.e. pointer to top of its own AR). Then the caller saves procedure parameters in reverse order (right-to-left). 62 / 1

Code generation: procedure calls Several things are worth noting. The caller first saves the FP (i.e. pointer to top of its own AR). Then the caller saves procedure parameters in reverse order (right-to-left). Implicitly the caller saves the return address in $ra by executing jal. The return address is still not in the AR on the stack. The AR is incomplete. Completion is the callee s responsibility. 63 / 1

Code generation: procedure calls Several things are worth noting. The caller first saves the FP (i.e. pointer to top of its own AR). Then the caller saves procedure parameters in reverse order (right-to-left). Implicitly the caller saves the return address in $ra by executing jal. The return address is still not in the AR on the stack. The AR is incomplete. Completion is the callee s responsibility. How big is the AR? 64 / 1

Code generation: procedure calls Several things are worth noting. The caller first saves the FP (i.e. pointer to top of its own AR). Then the caller saves procedure parameters in reverse order (right-to-left). Implicitly the caller saves the return address in $ra by executing jal. The return address is still not in the AR on the stack. The AR is incomplete. Completion is the callee s responsibility. How big is the AR? For a procedure with n arguments the AR (without return address) is 4 + 4 n = 4(n + 2) bytes long. This is know at compile time and is important for the compilation of procedure bodies. 65 / 1

Code generation: procedure calls Several things are worth noting. The caller first saves the FP (i.e. pointer to top of its own AR). Then the caller saves procedure parameters in reverse order (right-to-left). Implicitly the caller saves the return address in $ra by executing jal. The return address is still not in the AR on the stack. The AR is incomplete. Completion is the callee s responsibility. How big is the AR? For a procedure with n arguments the AR (without return address) is 4 + 4 n = 4(n + 2) bytes long. This is know at compile time and is important for the compilation of procedure bodies. The translation of procedure invocations is generic in the number of procedure arguments, nothing particular about 3. 66 / 1

Code generation: procedure calls So far we perfectly adhere to the lhs of this picture (except 33, 100, 7). Caller's AR Caller's AR FP Caller's FP Third argument: 33 Jump Caller's FP Third argument: 33 Second argument: 100 First argument: 7 Second argument: 100 First argument: 7 Caller's responsibility SP FP Return address Callee's responsibility SP 67 / 1

68 / 1 Code generation: procedure calls, callee s side In order to compile a declaration d like def f ( x1,..., xn ) = body we use a procedure for compiling declarations like so: def gendecl ( d ) =...

Code generation: procedure calls, callee s side 69 / 1

70 / 1 Code generation: procedure calls, callee s side We need two new MIPS instructions: jr reg move reg reg

71 / 1 Code generation: procedure calls, callee s side We need two new MIPS instructions: jr reg move reg reg The former (jr reg) jumps to the address stored in register reg.

72 / 1 Code generation: procedure calls, callee s side We need two new MIPS instructions: jr reg move reg reg The former (jr reg) jumps to the address stored in register reg. The latter (move reg reg ) moves the content of register reg into the register reg.

73 / 1 Code generation: procedure calls, callee s side def gendecl ( d : Declaration ) = val sizear = ( 2 + d.args.size ) * 4 // each procedure argument takes 4 bytes, // in addition the AR stores the return // address and old FP d.id + "_entry:" // label to jump to move $fp $sp // FP points to top of current AR sw $ra 0($sp) // put return address on stack addiu $sp $sp -4 // now AR is fully created genexp ( d.body ) lw $ra 4($sp) // load return address into $ra // could also use $fp addiu $sp $sp sizear // pop AR off stack in one go lw $fp 0($sp) // restore old FP jr $ra // hand back control to caller

74 / 1 Code generation: procedure calls, callee s side Before call On entry On exit After call Caller's AR Caller's AR Caller's AR Caller's AR FP FP FP SP Caller's FP Caller's FP SP Third argument: 33 Third argument: 33 Second argument: 100 Second argument: 100 First argument: 7 First argument: 7 SP FP Return address SP

75 / 1 Code generation: procedure calls, callee s side Before call On entry On exit After call Caller's AR Caller's AR Caller's AR Caller's AR FP FP FP SP Caller's FP Caller's FP SP Third argument: 33 Third argument: 33 Second argument: 100 Second argument: 100 First argument: 7 First argument: 7 SP FP Return address SP So we preserve the invariant that the stack looks exactly the same before and after a procedure call!

Code generation: frame pointer Variables are just the procedure parameters in this language. 76 / 1

77 / 1 Code generation: frame pointer Variables are just the procedure parameters in this language. They are all on the stack in the AR, pushed by the caller. How do we access them? The obvious solution (use the SP with appropriate offset) does not work (at least not easily).

78 / 1 Code generation: frame pointer Variables are just the procedure parameters in this language. They are all on the stack in the AR, pushed by the caller. How do we access them? The obvious solution (use the SP with appropriate offset) does not work (at least not easily). Problem: The stack grows and shrinks when intermediate results are computed (in the accumulator machine approach), so the variables are not on a fixed offset from $sp. For example in def f ( x, y, z ) = x + ( ( x * z ) + ( y - y ) )

79 / 1 Code generation: frame pointer Variables are just the procedure parameters in this language. They are all on the stack in the AR, pushed by the caller. How do we access them? The obvious solution (use the SP with appropriate offset) does not work (at least not easily). Problem: The stack grows and shrinks when intermediate results are computed (in the accumulator machine approach), so the variables are not on a fixed offset from $sp. For example in def f ( x, y, z ) = x + ( ( x * z ) + ( y - y ) ) Solution:

80 / 1 Code generation: frame pointer Variables are just the procedure parameters in this language. They are all on the stack in the AR, pushed by the caller. How do we access them? The obvious solution (use the SP with appropriate offset) does not work (at least not easily). Problem: The stack grows and shrinks when intermediate results are computed (in the accumulator machine approach), so the variables are not on a fixed offset from $sp. For example in def f ( x, y, z ) = x + ( ( x * z ) + ( y - y ) ) Solution: Use frame pointer $fp.

81 / 1 Code generation: frame pointer Variables are just the procedure parameters in this language. They are all on the stack in the AR, pushed by the caller. How do we access them? The obvious solution (use the SP with appropriate offset) does not work (at least not easily). Problem: The stack grows and shrinks when intermediate results are computed (in the accumulator machine approach), so the variables are not on a fixed offset from $sp. For example in def f ( x, y, z ) = x + ( ( x * z ) + ( y - y ) ) Solution: Use frame pointer $fp. Always points to the top of current AR as long as invocation is active.

82 / 1 Code generation: frame pointer Variables are just the procedure parameters in this language. They are all on the stack in the AR, pushed by the caller. How do we access them? The obvious solution (use the SP with appropriate offset) does not work (at least not easily). Problem: The stack grows and shrinks when intermediate results are computed (in the accumulator machine approach), so the variables are not on a fixed offset from $sp. For example in def f ( x, y, z ) = x + ( ( x * z ) + ( y - y ) ) Solution: Use frame pointer $fp. Always points to the top of current AR as long as invocation is active. The FP does not (appear to) move, so we can find all variables at a fixed offset from $fp.

83 / 1 Code generation: variable use Let s compile x which is the i-th (starting to count from 1) parameter of def f(x1, x2,..., xn) = body works like this (using offset in AR): def genexp ( e : Exp ) = if e is of form Variable ( x ) then val offset = 4*i lw $a0 offset($fp)

84 / 1 Code generation: variable use Let s compile x which is the i-th (starting to count from 1) parameter of def f(x1, x2,..., xn) = body works like this (using offset in AR): def genexp ( e : Exp ) = if e is of form Variable ( x ) then val offset = 4*i lw $a0 offset($fp) Putting the arguments in reverse order on the stack makes the offseting calculation val offset = 4*i a tiny bit easier.

85 / 1 Code generation: variable use Let s compile x which is the i-th (starting to count from 1) parameter of def f(x1, x2,..., xn) = body works like this (using offset in AR): def genexp ( e : Exp ) = if e is of form Variable ( x ) then val offset = 4*i lw $a0 offset($fp) Putting the arguments in reverse order on the stack makes the offseting calculation val offset = 4*i a tiny bit easier. Key insight: access at fixed offset relative to a dynamically changing pointer. Offset and pointer location are known at compile time. This idea is pervasive in compilation.

86 / 1 Code generation: variable use Caller's AR Caller's FP Third argument: 33 Second argument: 100 First argument: 7 In the declaration def f (x, y, z) =..., we have: x is at address $fp + 4 y is at address $fp + 8 z is at address $fp + 12 Note that this work because indexing begins at 1 in this case, and arguments are pushed on stack from right to left. FP Return address

87 / 1 Translation of variable assignment Given that we know now that reading a variable is translated as if e is of form Variable ( x ) then val offset = 4*i lw $a0 offset($fp) How would you translate an assignment x := e

88 / 1 Translation of variable assignment Given that we know now that reading a variable is translated as if e is of form Variable ( x ) then val offset = 4*i lw $a0 offset($fp) How would you translate an assignment x := e Assume that the variable x is the i-th (starting to count from 1) formal parameter of the ambient procedure declaration.

89 / 1 Translation of variable assignment Given that we know now that reading a variable is translated as if e is of form Variable ( x ) then val offset = 4*i lw $a0 offset($fp) How would you translate an assignment x := e Assume that the variable x is the i-th (starting to count from 1) formal parameter of the ambient procedure declaration. def genexp ( exp : Exp ) = if exp is of form Assign ( x, e ) then val offset = 4*i genexp ( e ) sw $a0 offset($fp)

90 / 1 Translation of variable assignment Given that we know now that reading a variable is translated as if e is of form Variable ( x ) then val offset = 4*i lw $a0 offset($fp) How would you translate an assignment x := e Assume that the variable x is the i-th (starting to count from 1) formal parameter of the ambient procedure declaration. def genexp ( exp : Exp ) = if exp is of form Assign ( x, e ) then val offset = 4*i genexp ( e ) sw $a0 offset($fp) Easy!

91 / 1 Code generation: summary remarks The code of variable access, procedure calls and declarations depends totally on the layout of the AR, so the AR must be designed together with the code generator, and all parts of the code generator must agree on AR conventions. It s just as important to be clear about the nature of the stack (grows upwards or downwards), frame pointer etc. Access at fixed offset relative to dynamically changing pointer. Offset and pointer location are known at compile time.

92 / 1 Code generation: summary remarks The code of variable access, procedure calls and declarations depends totally on the layout of the AR, so the AR must be designed together with the code generator, and all parts of the code generator must agree on AR conventions. It s just as important to be clear about the nature of the stack (grows upwards or downwards), frame pointer etc. Access at fixed offset relative to dynamically changing pointer. Offset and pointer location are known at compile time. Code and layout also depends on CPU.

93 / 1 Code generation: summary remarks The code of variable access, procedure calls and declarations depends totally on the layout of the AR, so the AR must be designed together with the code generator, and all parts of the code generator must agree on AR conventions. It s just as important to be clear about the nature of the stack (grows upwards or downwards), frame pointer etc. Access at fixed offset relative to dynamically changing pointer. Offset and pointer location are known at compile time. Code and layout also depends on CPU. Code generation happens by recursive AST walk.

94 / 1 Code generation: summary remarks The code of variable access, procedure calls and declarations depends totally on the layout of the AR, so the AR must be designed together with the code generator, and all parts of the code generator must agree on AR conventions. It s just as important to be clear about the nature of the stack (grows upwards or downwards), frame pointer etc. Access at fixed offset relative to dynamically changing pointer. Offset and pointer location are known at compile time. Code and layout also depends on CPU. Code generation happens by recursive AST walk. Industrial strength compilers are more complicated:

95 / 1 Code generation: summary remarks The code of variable access, procedure calls and declarations depends totally on the layout of the AR, so the AR must be designed together with the code generator, and all parts of the code generator must agree on AR conventions. It s just as important to be clear about the nature of the stack (grows upwards or downwards), frame pointer etc. Access at fixed offset relative to dynamically changing pointer. Offset and pointer location are known at compile time. Code and layout also depends on CPU. Code generation happens by recursive AST walk. Industrial strength compilers are more complicated: Try to keep values in registers, especially the current stack frame. E.g. compilers for MIPS usually pass first four procedure arguments in registers $a0 - $a3.

Code generation: summary remarks The code of variable access, procedure calls and declarations depends totally on the layout of the AR, so the AR must be designed together with the code generator, and all parts of the code generator must agree on AR conventions. It s just as important to be clear about the nature of the stack (grows upwards or downwards), frame pointer etc. Access at fixed offset relative to dynamically changing pointer. Offset and pointer location are known at compile time. Code and layout also depends on CPU. Code generation happens by recursive AST walk. Industrial strength compilers are more complicated: Try to keep values in registers, especially the current stack frame. E.g. compilers for MIPS usually pass first four procedure arguments in registers $a0 - $a3. Intermediate values, local variables are held in registers, not on the stack. 96 / 1

97 / 1 Non-integer procedure arguments What we have not covered is procedures taking non integer arguments.

98 / 1 Non-integer procedure arguments What we have not covered is procedures taking non integer arguments. This is easy: the only difference from a code generation perspective between integer types and other types as procedure arguments is the size of the data. But that size is known at compile-time (at least for languages that are statically typed). For example the type double is often 64 bits. So we reserve 8 bytes for arguments of that type in the procedure s AR layout. We may have to use two calls to lw and sw to load and store such arguments, but otherwise code generation is unchanged.

99 / 1 Non-integer procedure arguments Consider a procedure with the following signature: int f ( int x, double y, int z ) = {... } (Not valid in our mini-language). Assuming that double is stored as 64 bits, then the AR would look like on the right. 1632 1636 1640 1644 1648 1652 Caller's FP int x Double y int z return address

100 / 1 Non-integer procedure arguments Consider a procedure with the following signature: int f ( int x, double y, int z ) = {... } (Not valid in our mini-language). Assuming that double is stored as 64 bits, then the AR would look like on the right. 1632 1636 1640 1644 1648 1652 Caller's FP int x Double y int z return address How does the code generator know what size the variables have?

101 / 1 Non-integer procedure arguments Consider a procedure with the following signature: int f ( int x, double y, int z ) = {... } (Not valid in our mini-language). Assuming that double is stored as 64 bits, then the AR would look like on the right. 1632 1636 1640 1644 1648 1652 Caller's FP int x Double y int z return address How does the code generator know what size the variables have? Using the information stored in the symbol table, which was created by the type checker and passed to the code-generator.

102 / 1 Non-integer procedure arguments Due to the simplistic accumulator machine approach, cannot do the same with the return value, e.g. double f ( int x, double y, int z ) =...

103 / 1 Non-integer procedure arguments Due to the simplistic accumulator machine approach, cannot do the same with the return value, e.g. double f ( int x, double y, int z ) =... This is because the accumulator holds the return value of procedure calls, and the accumulator is fixed at 32 bits.

104 / 1 Non-integer procedure arguments Due to the simplistic accumulator machine approach, cannot do the same with the return value, e.g. double f ( int x, double y, int z ) =... This is because the accumulator holds the return value of procedure calls, and the accumulator is fixed at 32 bits. In this case we d have to move to an approach that holds the return value also in the AR (either for all arguments, or only for arguments that don t fit in a register we know at compile time which is which).

105 / 1 Example def sumto(n) = if n=0 then 0 else n+sumto(n-1) sumto_entry: move $fp $sp sw $ra 0($sp) addiu $sp $sp -4 lw $a0 4($fp) sw $a0 0($sp) addiu $sp $sp -4 li $a0 0 lw $t1 4($sp) addiu $sp $sp 4 beq $a0 $t1 then1 else0: lw $a0 4($fp) sw $a0 0($sp) addiu $sp $sp -4 sw $fp 0($sp) addiu $sp $sp -4 lw $a0 4($fp) sw $a0 0($sp) addiu $sp $sp -4 li $a0 1 lw $t1 4($sp) sub $a0 $t1 $a0 addiu $sp $sp 4 sw $a0 0($sp) addiu $sp $sp -4 jal sumto_entry lw $t1 4($sp) add $a0 $t1 $a0 addiu $sp $sp 4 b exit2 then1: li $a0 0 exit2: lw $ra 4($sp) addiu $sp $sp 12 lw $fp 0($sp) jr $ra

106 / 1 Interesting observations Several points are worth thinking about.

107 / 1 Interesting observations Several points are worth thinking about. Stack allocated memory is much faster than heap allocation, because (1) acquiring stack memory is just a constant-time push operation, and (2) the whole AR can be deleted (= popped off the stack) in a single, constant-time operation. We will soon learn about heap-allocation (section on garbage-collection), which is much more expensive. This is why low-level language (C, C++, Rust) don t have garbage collection (by default).

108 / 1 Interesting observations Several points are worth thinking about. Stack allocated memory is much faster than heap allocation, because (1) acquiring stack memory is just a constant-time push operation, and (2) the whole AR can be deleted (= popped off the stack) in a single, constant-time operation. We will soon learn about heap-allocation (section on garbage-collection), which is much more expensive. This is why low-level language (C, C++, Rust) don t have garbage collection (by default). The source language has recursion. The target language (MIPS) does not. What is recursion translated to?

109 / 1 Interesting observations Several points are worth thinking about. Stack allocated memory is much faster than heap allocation, because (1) acquiring stack memory is just a constant-time push operation, and (2) the whole AR can be deleted (= popped off the stack) in a single, constant-time operation. We will soon learn about heap-allocation (section on garbage-collection), which is much more expensive. This is why low-level language (C, C++, Rust) don t have garbage collection (by default). The source language has recursion. The target language (MIPS) does not. What is recursion translated to? Jumping!

110 / 1 Interesting observations Several points are worth thinking about. Stack allocated memory is much faster than heap allocation, because (1) acquiring stack memory is just a constant-time push operation, and (2) the whole AR can be deleted (= popped off the stack) in a single, constant-time operation. We will soon learn about heap-allocation (section on garbage-collection), which is much more expensive. This is why low-level language (C, C++, Rust) don t have garbage collection (by default). The source language has recursion. The target language (MIPS) does not. What is recursion translated to? Jumping! But what kind of jumping?

111 / 1 Interesting observations Several points are worth thinking about. Stack allocated memory is much faster than heap allocation, because (1) acquiring stack memory is just a constant-time push operation, and (2) the whole AR can be deleted (= popped off the stack) in a single, constant-time operation. We will soon learn about heap-allocation (section on garbage-collection), which is much more expensive. This is why low-level language (C, C++, Rust) don t have garbage collection (by default). The source language has recursion. The target language (MIPS) does not. What is recursion translated to? Jumping! But what kind of jumping? Backwards jumping.

Another interesting observation: inefficiency of the translation As already pointed out at the beginning of this course, stackand accumulator machines are inefficient. 112 / 1

113 / 1 Another interesting observation: inefficiency of the translation As already pointed out at the beginning of this course, stackand accumulator machines are inefficient. Consider this from the previous slide (compilation of parts of n = 0 in sumto): lw $a0 4($fp) sw $a0 0($sp) addiu $sp $sp -4 li $a0 0 lw $t1 4($sp) // first we load n into the // accumulator from the stack // then we push n back onto // the stack // now we load n back from // the stack into a temporary

114 / 1 Another interesting observation: inefficiency of the translation As already pointed out at the beginning of this course, stackand accumulator machines are inefficient. Consider this from the previous slide (compilation of parts of n = 0 in sumto): lw $a0 4($fp) sw $a0 0($sp) addiu $sp $sp -4 li $a0 0 lw $t1 4($sp) // first we load n into the // accumulator from the stack // then we push n back onto // the stack // now we load n back from // the stack into a temporary This is the price we pay for the simplicity of compilation strategy.

Another interesting observation: inefficiency of the translation As already pointed out at the beginning of this course, stackand accumulator machines are inefficient. Consider this from the previous slide (compilation of parts of n = 0 in sumto): lw $a0 4($fp) sw $a0 0($sp) addiu $sp $sp -4 li $a0 0 lw $t1 4($sp) // first we load n into the // accumulator from the stack // then we push n back onto // the stack // now we load n back from // the stack into a temporary This is the price we pay for the simplicity of compilation strategy. It s possible to do much better, e.g. saving it directly in $t1 using better compilation strategies and optimisation techniques. 115 / 1

116 / 1 Compiling whole programs So far we have only compiled expressions and single declarations, but a program is a sequence of declarations, and it is called from, and returns to the OS. To compile a whole program we do the following:

117 / 1 Compiling whole programs So far we have only compiled expressions and single declarations, but a program is a sequence of declarations, and it is called from, and returns to the OS. To compile a whole program we do the following: Creating the preamble, e.g. setting up data declarations, alignment commands etc.

118 / 1 Compiling whole programs So far we have only compiled expressions and single declarations, but a program is a sequence of declarations, and it is called from, and returns to the OS. To compile a whole program we do the following: Creating the preamble, e.g. setting up data declarations, alignment commands etc. Generate code for each declaration.

119 / 1 Compiling whole programs So far we have only compiled expressions and single declarations, but a program is a sequence of declarations, and it is called from, and returns to the OS. To compile a whole program we do the following: Creating the preamble, e.g. setting up data declarations, alignment commands etc. Generate code for each declaration. Emit code enabling the OS to call the first procedure (like Java s main other languages might have different conventions) to get the ball rolling. This essentially involves

120 / 1 Compiling whole programs So far we have only compiled expressions and single declarations, but a program is a sequence of declarations, and it is called from, and returns to the OS. To compile a whole program we do the following: Creating the preamble, e.g. setting up data declarations, alignment commands etc. Generate code for each declaration. Emit code enabling the OS to call the first procedure (like Java s main other languages might have different conventions) to get the ball rolling. This essentially involves 1. Creating (the caller s side of) an activation record.

121 / 1 Compiling whole programs So far we have only compiled expressions and single declarations, but a program is a sequence of declarations, and it is called from, and returns to the OS. To compile a whole program we do the following: Creating the preamble, e.g. setting up data declarations, alignment commands etc. Generate code for each declaration. Emit code enabling the OS to call the first procedure (like Java s main other languages might have different conventions) to get the ball rolling. This essentially involves 1. Creating (the caller s side of) an activation record. 2. Jump-and-link ing to the first procedure.

122 / 1 Compiling whole programs So far we have only compiled expressions and single declarations, but a program is a sequence of declarations, and it is called from, and returns to the OS. To compile a whole program we do the following: Creating the preamble, e.g. setting up data declarations, alignment commands etc. Generate code for each declaration. Emit code enabling the OS to call the first procedure (like Java s main other languages might have different conventions) to get the ball rolling. This essentially involves 1. Creating (the caller s side of) an activation record. 2. Jump-and-link ing to the first procedure. 3. Code that hands back control gracefully to the OS after program termination. Termination means doing a return to the place after (2). This part is highly OS specific.

123 / 1 Compiling whole programs Say we had a program declaring 4 procedures f1, f2, f3, and f4 in this order. Then a fully formed compiler would typically generate code as follows. preamble:...// e.g. alignment commands if needed entry_point: // this is where the OS jumps to // at startup... // create AR for initial call to f1 jal f1_entry // jump to f1... // cleanup, hand back control to OS f1_entry:... // f1 body code f2_entry:... // f2 body code f3_entry:... // f3 body code f4_entry:... // f4 body code