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.