
- Compiler Design - Home
- Compiler Design - Overview
- Compiler Design - Architecture
- Phases
- Compiler Design - Phases
- Compiler Design - Global Optimization
- Compiler Design - Local Optimization
- Lexical Analysis
- Compiler Design - Lexical Analysis
- Compiler Design - Regular Expressions
- Compiler Design - Finite Automata
- Compiler Design - Language Elements
- Compiler Design - Lexical Tokens
- Compiler Design - FSM
- Compiler Design - Lexical Table
- Compiler Design - Sequential Search
- Compiler Design - Binary Search Tree
- Compiler Design - Hash Table
- Syntax Analysis
- Compiler Design - Syntax Analysis
- Compiler Design - Parsing Types
- Compiler Design - Grammars
- Compiler Design - Classes Grammars
- Compiler Design - Pushdown Automata
- Compiler Design - Ambiguous Grammar
- Parsing
- Compiler Design - Top-Down Parser
- Compiler Design - Bottom-Up Parser
- Compiler Design - Simple Grammar
- Compiler Design - Quasi-Simple Grammar
- Compiler Design - LL(1) Grammar
- Error Recovery
- Compiler Design - Error Recovery
- Semantic Analysis
- Compiler Design - Semantic Analysis
- Compiler Design - Symbol Table
- Run Time
- Compiler Design - Run-time Environment
- Code Generation
- Compiler Design - Code Generation
- Converting Atoms to Instructions
- Compiler Design - Transfer of Control
- Compiler Design - Register Allocation
- Forward Transfer of Control
- Reverse Transfer of Control
- Code Optimization
- Compiler Design - Code Optimization
- Compiler Design - Intermediate Code
- Basic Blocks and DAGs
- Control Flow Graph
- Compiler Design - Peephole Optimization
- Implementing Translation Grammars
- Compiler Design - Attributed Grammars
Transfer of Control in Compiler Design
In compiler design, the code generation phase plays a crucial role, particularly in intermediate code generation. Code execution typically follows a sequence, but it does not always proceed sequentially from one statement to the next. This is because programs often require branch instructions, loops, and function calls, which introduce decision-making and repetition. This redirection of execution flow is known as the transfer of control.
In this chapter, we will explore the concept of transfer of control, discuss its different types, and examine how compilers handle it using conditional branching, function calls, and jumps.
What is Transfer of Control?
Transfer of control occurs when a program does not execute instructions in a strict manner. Like top-to-bottom order may not be followed always. Instead, execution jumps to another part of the program based on conditions, function calls, or loops.
When we are making compilers handling the transfer of control is important for for generating correct machine code. The compiler must translate high-level control structures (like if statements, loops, and function calls) and they are converted into low-level jump and branch instructions.
This process can be categorized into three main types they are as follows −
- Unconditional Jumps − Directly jump to another location in the program.
- Conditional Branches − Jump only if a certain condition is met.
- Function Calls and Returns − Transfer execution to a subroutine and return afterward.
The compiler must correctly understand, analyze, generate, and optimize control flow instructions to ensure smooth execution. Let us see these things into a greater detail.
Unconditional Jumps
From the name itself we can understand that unconditional jump (are also known as goto statements) They are used to transfers execution to a different location in the program without checking any condition. These jumps are often used in loop constructs (while, for), function returns or exception handling.
Example of GOTO Statement in C
goto label; ... label: printf("Jumped here!");
Intermediate Representation (IR) Using Atoms
(JMP, L1) # Jump to label L1 (LBL, L1) # Define label L1 (PRINT, "Jumped here!")
Translation to Machine Instructions (MIPS Example)
J L1 # Jump to L1 L1: li $v0, 4 la $a0, str syscall
Here, J L1 is special. This causes execution to jump directly to L1. This is skipping any instructions in between.
Conditional Branching
Conditional branching instructions are for transferring control based on a comparison result. This is used in:
- If-else statements
- Loop conditions
- Switch cases
Example of If-Else Statement in Java
Take a look at the following example −
if (x > y) max = x; else max = y;
Intermediate Representation Using Atoms
(TST, x, y, >, L1) # If x > y, jump to L1 (MOV, y, , max) # Else block: max = y (JMP, L2) # Skip then-block (LBL, L1) (MOV, x, , max) # Then block: max = x (LBL, L2)
Translation to Machine Code (MIPS Example)
lw $t1, x lw $t2, y ble $t1, $t2, ELSE # If x y, go to ELSE # THEN block move $t3, $t1 # max = x j END ELSE: move $t3, $t2 # max = y END:
Here, instructions like "ble $t1, $t2, ELSE" checks if x <= y. If true, control jumps to ELSE. Otherwise, the THEN block executes.
Loops and Iteration
Loops are another form of control transfer where execution repeats a section of code multiple times. The compiler here must generate branch instructions for the following −
- Check the loop condition.
- Jump to the loop body if true.
- Exit the loop if false.
Example of While Loop in C
Take a look at the following example −
while (i < 10) { i = i + 1; }
Intermediate Representation Using Atoms
(LBL, L1) (TST, i, 10, <, L2) # If i >= 10, exit loop (ADD, i, 1, i) # i = i + 1 (JMP, L1) # Repeat loop (LBL, L2)
Translation to Machine Code (MIPS Example)
L1: lw $t1, i li $t2, 10 bge $t1, $t2, L2 # If i >= 10, exit loop addi $t1, $t1, 1 # i = i + 1 sw $t1, i j L1 # Repeat loop L2:
We can see the "bge" instruction checks if i is greater than or equal to 10. If this is true, the loop exits; otherwise, execution jumps back to L1 and continuing the loop.
Function Calls and Returns
Function calls also transfer control from the caller or invoker to the function and back once execution completes. Compilers must manage the followings −
- Saving and restoring registers.
- Managing the function call stack.
- Returning values correctly.
Example of Function Call in C
Take a look at the following function call in C programming −
int add(int a, int b) { return a + b; } int main() { int sum = add(5, 10); }
Intermediate Representation Using Atoms
(CALL, add, 5, 10, T1) # Call function add(a, b) (MOV, T1, , sum) # Store result in sum
Translation to Machine Code (MIPS Example)
main: li $a0, 5 li $a1, 10 jal add # Jump to function and store return address move $t0, $v0 # Store return value j exit add: add $v0, $a0, $a1 # Perform addition jr $ra # Return to caller
Here, we can see "jal add" jumps to the function, and "jr $ra" returns control back to main.
We can list the control transfer methods −
- Unconditional Jumps (JMP) − Transfers control without conditions.
- Conditional Branching (TST, BLE, BNE) − Jumps based on logical conditions.
- Loops (WHILE, FOR) − Uses branches to repeat execution.
- Function Calls (CALL, RETURN) − Transfers execution to a subroutine and returns afterward.
Conclusion
In this chapter, we explained the concept of "transfer of control in compiler design" and how compilers translate high-level control structures into low-level machine instructions. We examined unconditional jumps, conditional branches, loops, and function calls, along with step-by-step translations from atoms to machine code.
Control flow is fundamental in programming, and the compiler plays a crucial role in managing branch instructions, function calls, and loops efficiently. By handling these transfers correctly, compilers can generate optimized machine code that ensures programs execute accurately and efficiently.