03. MIPS Control
Control Flow#
So far, our programs execute linearly: one instruction after another. That is insufficient for if, while, or for, all of which need to choose the next instruction based on a condition.
MIPS implements this using branches, jumps, and labels. A conditional branch says: if this comparison is true, continue execution at the given label.
bge $t0, $t1, out_of_range # if (x >= limit) goto out_of_range;An unconditional branch always jumps:
b loop__cond # goto loop__cond;Simplified C#
Directly translating structured C can feel like a leap. The course's most useful technique is to first rewrite the program into simplified C, where each statement has an obvious assembly counterpart.
flowchart TD
C["Normal C"] --> S["Simplified C<br/>labels + goto"]
S --> M["MIPS<br/>labels + branches"]
This is not a recommendation to use goto in ordinary C. It exposes the control flow already hidden inside loops and conditionals.
- Split complex expressions into temporary values.
- Rewrite structured control flow using labels and
goto. - Test the simplified C if practical.
- Allocate registers for every live value.
- Translate one line at a time.

Branch Instructions#
| MIPS | Branches when |
|---|---|
beq $t0, $t1, label |
$t0 == $t1 |
bne $t0, $t1, label |
$t0 != $t1 |
blt $t0, $t1, label |
$t0 < $t1 |
ble $t0, $t1, label |
$t0 <= $t1 |
bgt $t0, $t1, label |
$t0 > $t1 |
bge $t0, $t1, label |
$t0 >= $t1 |
bltz $t0, label |
$t0 < 0 |
bgez $t0, label |
$t0 >= 0 |
blez $t0, label |
$t0 <= 0 |
bgtz $t0, label |
$t0 > 0 |
b label |
always |
Many friendly comparison branches are pseudo-instructions. Signed and unsigned comparisons are not interchangeable because a register stores bits, not a permanent C type. The comparison instruction decides how to interpret those bits.
For example, the pattern 0xFFFFFFFF means -1 when interpreted as a signed 32-bit integer, but 4,294,967,295 when interpreted as unsigned. A signed comparison therefore places it below 1; an unsigned comparison places it above 1. Use signed comparisons for ordinary C int values and the corresponding unsigned comparisons for unsigned int values. The stored bits have not changed—only the meaning used by the comparison has changed. Consult the course instruction reference for the exact branch forms.
li $t0, -1
li $t1, 1
blt $t0, $t1, signed_less # taken: -1 < 1
bltu $t0, $t1, unsigned_less # not taken: 4294967295 < 1 is falseImportant
- Same bits, different branch
If $t0 = 0x80000000 and $t1 = 0, would a signed “less than” branch and an unsigned “less than” branch make the same decision?
Note
- Reasoned answer
No. Signed interpretation treats 0x80000000 as the most negative 32-bit integer, so it is less than zero. Unsigned interpretation treats the same bits as 2,147,483,648, so it is greater than zero. The register contents stay fixed; the comparison rule changes.
Branches, Jumps, and Calls#
These instructions all change control flow, but they express different intentions:
| Form | Effect | Typical use |
|---|---|---|
beq, bne, blt, … |
go to a label only if a condition is true | if, loop condition |
b label |
always go to a nearby label; pseudo-instruction | loop back-edge, skip an else |
j label |
unconditional jump; real instruction | low-level unconditional transfer |
jal function |
jump and save a return address in $ra |
function call |
jr $ra |
jump to the address held in a register | function return |
For ordinary structured translations, the course style generally uses conditional branches and b. Do not replace a function call with b or j: neither records where the callee should return. MIPS Functions develops jal and jr properly.
Translating an if#
Suppose we begin with:
if (n % 2 == 0) {
print_even();
}To skip the body when the condition is false, simplified C uses the opposite comparison:
int remainder = n % 2;
if (remainder != 0) goto if_even__end;
print_even();
if_even__end:If n is in $t0:
rem $t1, $t0, 2 # int remainder = n % 2;
bne $t1, 0, if_even__end # if (remainder != 0) goto if_even__end;
# print the even message
if_even__end:The common trap is branching on the original condition. Ask: what condition means the body should be skipped?
Translating if / else#
if (temperature < 18) {
state = COLD;
} else {
state = WARM;
}becomes:
if (temperature >= 18) goto if_cold__else;
state = COLD;
goto if_cold__end;
if_cold__else:
state = WARM;
if_cold__end:and then:
bge $t0, 18, if_cold__else
li $t1, COLD
b if_cold__end
if_cold__else:
li $t1, WARM
if_cold__end:The first branch skips the true body. The unconditional branch after that body prevents execution from falling through into the else body.
else if#
An else if chain is a sequence of tests whose successful bodies jump to one shared end:
if (mark >= 85) {
grade = 'A';
} else if (mark >= 75) {
grade = 'B';
} else if (mark >= 65) {
grade = 'C';
} else {
grade = 'F';
}The simplified form makes the shared end explicit:
if (mark < 85) goto grade__check_b;
grade = 'A';
goto grade__end;
grade__check_b:
if (mark < 75) goto grade__check_c;
grade = 'B';
goto grade__end;
grade__check_c:
if (mark < 65) goto grade__else;
grade = 'C';
goto grade__end;
grade__else:
grade = 'F';
grade__end: blt $t0, 85, grade__check_b
li $t1, 'A'
b grade__end
grade__check_b:
blt $t0, 75, grade__check_c
li $t1, 'B'
b grade__end
grade__check_c:
blt $t0, 65, grade__else
li $t1, 'C'
b grade__end
grade__else:
li $t1, 'F'
grade__end:Here the letters are simply four example bands: A, B, C, and F. Test order matters. A mark of 90 also satisfies >= 75, so the highest threshold must be checked first.
Boolean Expressions#
C uses short-circuit evaluation. In A && B, B is evaluated only if A is true. In A || B, B is evaluated only if A is false. This can affect correctness when the second expression performs work or would be unsafe to evaluate.
Logical AND#
if (x >= 0 && x <= 100) {
in_bounds();
} else {
out_of_bounds();
}Every part of an && must succeed, so any failure can jump directly to the false path:
blt $t0, 0, bounds__else
bgt $t0, 100, bounds__else
# in bounds
b bounds__end
bounds__else:
# out of bounds
bounds__end:Logical OR#
if (age >= 30 || level < 10) {
replace_milk();
} else {
keep_milk();
}Any successful part of an || can jump directly to the true path:
bge $t0, 30, milk__replace
blt $t1, 10, milk__replace
# keep milk
b milk__end
milk__replace:
# replace milk
milk__end:Nested Expressions#
For y < 10 || (z > 50 && w < 5), reason in paths:
blt $t0, 10, condition__true
ble $t1, 50, condition__end
bge $t2, 5, condition__end
condition__true:
# condition met
condition__end:If y < 10, the entire expression is already true. Otherwise, both right-hand comparisons must pass.
Short-circuiting is sometimes required for safety, not merely speed. In pointer != NULL && *pointer == 0, the memory access must not occur when the pointer is null:
beq $t0, 0, condition__false # pointer == NULL: do not dereference
lw $t1, 0($t0)
bne $t1, 0, condition__false
# complete condition is trueTranslating both sides into temporary 0/1 values first would lose that safety. Translate the paths of the Boolean expression instead.
Loops#
A loop contains four conceptual regions:
flowchart TD
I["initialisation"] --> C{"another iteration?"}
C -->|yes| B["body"]
B --> S["step"]
S --> C
C -->|no| E["end"]
Use consistent labels:
loop_name__init
loop_name__cond
loop_name__body
loop_name__step
loop_name__end

Translating a while Loop#
int i = 0;
while (i < 10) {
print_int(i);
i++;
}Simplified C:
int i;
loop_i__init:
i = 0;
loop_i__cond:
if (i >= 10) goto loop_i__end;
loop_i__body:
print_int(i);
loop_i__step:
i++;
goto loop_i__cond;
loop_i__end:MIPS:
loop_i__init:
li $t0, 0 # int i = 0;
loop_i__cond:
bge $t0, 10, loop_i__end
loop_i__body:
li $v0, 1
move $a0, $t0
syscall # print_int(i);
loop_i__step:
addi $t0, $t0, 1 # i++;
b loop_i__cond
loop_i__end:Translating a do / while Loop#
A do / while checks its condition after the body, so the body always runs at least once:
int i = 0;
do {
print_int(i);
i++;
} while (i < 3);The simplified C keeps the condition at the bottom:
int i = 0;
loop_i__body:
print_int(i);
i++;
if (i < 3) goto loop_i__body;
loop_i__end:That structure maps directly to MIPS:
li $t0, 0 # int i = 0;
loop_i__body:
li $v0, 1
move $a0, $t0
syscall # print_int(i);
addi $t0, $t0, 1 # i++;
blt $t0, 3, loop_i__body # if (i < 3) repeat
loop_i__end:Compare this with a while: there is no initial branch that can skip the first body execution.
Translating a for Loop#
First rewrite the for as a while, then use the same regions:
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i * i;
}int sum = 0;
int i;
loop_i__init:
i = 1;
loop_i__cond:
if (i > 100) goto loop_i__end;
loop_i__body:
sum += i * i;
loop_i__step:
i++;
goto loop_i__cond;
loop_i__end: li $t0, 0 # int sum = 0;
loop_i__init:
li $t1, 1 # int i = 1;
loop_i__cond:
bgt $t1, 100, loop_i__end
loop_i__body:
mul $t2, $t1, $t1 # int square = i * i;
add $t0, $t0, $t2 # sum += square;
loop_i__step:
addi $t1, $t1, 1 # i++;
b loop_i__cond
loop_i__end:Practice: Tracing#
- With
$t0 = 8and$t1 = 5, isblt $t0, $t1, targettaken?
Note
- Answer
No. It tests 8 < 5, which is false; operand order matters.
- Trace this loop. Which values are added to
$t1, and what is its final value?
li $t0, 1
li $t1, 0
loop__cond:
bgt $t0, 4, loop__end
add $t1, $t1, $t0
addi $t0, $t0, 1
b loop__cond
loop__end:Note
- Answer
It adds 1, 2, 3, and 4. The final value of $t1 is 10 and $t0 is 5.
- In
x != 0 && 100 / x > 2, why must the second comparison be skipped whenx == 0?
Note
- Answer
C short-circuits &&. Evaluating the right side would divide by zero, so the MIPS path must branch to false immediately when x == 0.
Practice: Writing / Translation#
- Translate
if (x == y) result = 1;using$t0 = x,$t1 = y, and$t2 = result.
Note
- Answer
bne $t0, $t1, if_equal__end, then li $t2, 1, followed by if_equal__end:.
- Write the control-flow skeleton for
while (i < n), using$t0 = iand$t1 = n.
Note
- Answer
At loop__cond, use bge $t0, $t1, loop__end; place the body next; increment or otherwise update i; branch back to loop__cond; finish with loop__end:.
- How would you translate
A || Bwithout evaluatingBwhenAis already true?
Note
- Answer
Branch directly to the true body when A succeeds. Only let control reach the instructions for B when A is false.
break and continue#
break exits the current loop, so it becomes a branch to loop__end.
continue begins the next iteration. In a translated for loop, it must branch to loop__step, not directly to loop__cond, or the update is skipped.
For example:
for (int i = 0; i < n; i++) {
if (i == 7) break;
if (i % 2 != 0) continue;
sum += i;
} li $t0, 0 # int i = 0;
loop_i__cond:
bge $t0, $s0, loop_i__end
loop_i__body:
beq $t0, 7, loop_i__end # break
rem $t1, $t0, 2
bne $t1, 0, loop_i__step # continue
add $s1, $s1, $t0 # sum += i;
loop_i__step:
addi $t0, $t0, 1
b loop_i__cond
loop_i__end:Here $s0 represents n and $s1 represents sum. The important pattern is the destination: break → __end, but continue → __step for a for loop.
Practice: Quick Checks#
- Why does a simple
iftranslation usually branch on the opposite of the C condition?
Note
- Answer
The branch skips the body when the condition is false. For if (x < 10), a common translation is therefore bge $t0, 10, if__end.
- Where should
breakandcontinuebranch in a translatedforloop?
Note
- Answer
break branches to the loop's __end; continue branches to its __step so the update still occurs.
- What is the difference between
b labelandjal function?
Note
- Answer
Both transfer control, but jal also writes the resume address to $ra. b is not a function call.
Nested Loops#
Nested control flow uses the same translation rules, but every loop needs its own label family. For example:
int count = 0;
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 3; col++) {
if (row != col) {
count++;
}
}
}First expose both loops and the conditional in simplified C:
int row = 0;
loop_row__cond:
if (row >= 2) goto loop_row__end;
int col = 0;
loop_col__cond:
if (col >= 3) goto loop_col__end;
if (row == col) goto if_off_diagonal__end;
count++;
if_off_diagonal__end:
col++;
goto loop_col__cond;
loop_col__end:
row++;
goto loop_row__cond;
loop_row__end:With row in $t0, col in $t1, and count in $t2, the complete MIPS control flow is:
li $t2, 0 # int count = 0;
li $t0, 0 # int row = 0;
loop_row__cond:
bge $t0, 2, loop_row__end
li $t1, 0 # int col = 0;
loop_col__cond:
bge $t1, 3, loop_col__end
beq $t0, $t1, if_off_diagonal__end
addi $t2, $t2, 1 # count++;
if_off_diagonal__end:
addi $t1, $t1, 1 # col++;
b loop_col__cond
loop_col__end:
addi $t0, $t0, 1 # row++;
b loop_row__cond
loop_row__end:Read this from the labels outward: the conditional ends first, then the inner loop, then the outer loop. break would target the __end of whichever loop it belongs to. When nested loops access a two-dimensional array, this control-flow structure stays the same; only the element-address calculation changes. See MIPS Data and Memory.
Practice: Challenge#
- A nested-loop
breakappears inside the inner loop. Should it target the inner or outer__end, and how would you express an intentional exit from both loops?
Note
- Answer
A normal break targets the inner loop's __end. To leave both loops, use a separate clearly named label after the outer loop and branch there deliberately.
Checking a Translation#
Trace a tiny case by hand. For a loop which should process i = 0, 1, 2:
| Visit | i |
i >= 3 |
action |
|---|---|---|---|
| 1 | 0 |
false | body |
| 2 | 1 |
false | body |
| 3 | 2 |
false | body |
| 4 | 3 |
true | exit |
This exposes off-by-one errors and backwards conditions quickly. Running MIPS explains how to perform the same trace interactively.
An instruction-level trace makes the same idea concrete. For the condition/body/step sequence below, assume $t0 starts at 0 and $t1 starts at 0:
loop__cond:
bge $t0, 3, loop__end
add $t1, $t1, $t0
addi $t0, $t0, 1
b loop__cond
loop__end:| Condition visit | $t0 |
branch taken? | $t1 after body |
|---|---|---|---|
| 1 | 0 | no | 0 |
| 2 | 1 | no | 1 |
| 3 | 2 | no | 3 |
| 4 | 3 | yes | body skipped |
The final state is $t0 = 3, $t1 = 3. Record state after each instruction when a larger trace goes wrong; a per-iteration summary can otherwise hide the exact failing update.
Practice: Cumulative Translation#
- Write a complete mipsy program equivalent to this C. Do not assume the input is positive.
int n = read_int();
int sum = 0;
for (int i = 0; i <= n; i++) {
if (i % 2 == 0) {
sum += i;
}
}
print_int(sum);
print_character('\n');Note
- Cumulative Translation Answer
Use one loop condition which exits when i > n, then skip the addition when the remainder is non-zero. Starting i at zero means a negative n fails the first condition and correctly prints zero.
.text
main:
# Registers:
# - $t0: int n
# - $t1: int sum
# - $t2: int i
# - $t3: i % 2
li $v0, 5
syscall
move $t0, $v0 # int n = read_int();
li $t1, 0 # int sum = 0;
li $t2, 0 # int i = 0;
loop_i__cond:
bgt $t2, $t0, loop_i__end
loop_i__body:
rem $t3, $t2, 2
bne $t3, 0, loop_i__step
add $t1, $t1, $t2 # sum += i;
loop_i__step:
addi $t2, $t2, 1
b loop_i__cond
loop_i__end:
li $v0, 1
move $a0, $t1
syscall
li $v0, 11
li $a0, '\n'
syscall
li $v0, 0
jr $ra