Decisions & loops

Decisions & loops

By default the CPU runs your instructions in order, one after another, from top to bottom. On its own that can never make a choice or repeat work. A branch is how a program escapes that straight line: it checks a condition and, if the condition is true, tells the CPU to continue somewhere else instead of at the next line. Every decision and every loop is built from this one idea.

There is no if or for. You build them from labels (named positions) and branches (jump there when a condition holds). A label is just a name followed by :.

To understand how this actually executes, you must understand the Program Counter (PC). The PC is a special, hidden register that holds the memory address of the current instruction. A branch instruction actively rewrites the value of the PC, forcing the CPU to fetch its next instruction from your target label instead of simply moving to the next line of code.

The branch family

Every branch compares two registers and jumps to the label only if the test holds; otherwise it falls through to the next line. There are six core tests:

The six real branches
beq  a, b, label   # branch if a == b
bne  a, b, label   # branch if a != b
blt  a, b, label   # branch if a <  b   (signed)
bge  a, b, label   # branch if a >= b   (signed)
bltu a, b, label   # branch if a <  b   (unsigned)
bgeu a, b, label   # branch if a >= b   (unsigned)

beq and bne test equality and ignore sign. blt and bge are the ordering pair, less-than and greater-or-equal, comparing the registers as signed numbers. bltu and bgeu make the same comparison but treat the values as unsigned, which is what you want for addresses or counts that are never negative.

What do signed and unsigned actually mean? A register is just 32 bits, and the same bits can be read two ways. Read as unsigned, they are a plain whole number from 0 up to about 4 billion. Read as signed, the topmost bit is treated as a minus sign, so half the patterns count as negative numbers. The bits never change; only the interpretation does. That's why there are two sets of comparisons: use the signed ones (blt, bge) for ordinary numbers that might be negative, and the unsigned ones (bltu, bgeu) for values that are never negative, like memory addresses.

Notice there's no direct "branch if greater" or "branch if less-or-equal". You get those by swapping the two registers, and the assembler offers bgt and ble as pseudo-branches that do the swap for you, so bgt a, b becomes blt b, a. Two more shortcuts: beqz and bnez test a single register against zero, and j label jumps with no condition at all.

Sum 1..5 with a loop
li t0, 0         # sum   = 0
li t1, 1         # i     = 1
li t2, 6         # limit = 6  (loop while i < 6)

loop:
  bge t1, t2, done   # if i >= 6, leave the loop
  add t0, t0, t1     # sum += i
  addi t1, t1, 1     # i++
  j loop             # go check again

done:
  mv a0, t0          # print the sum (should be 15)
  li a7, 1
  ecall
  li a7, 10
  ecall

Trace it. t0 is the running sum (starts 0), t1 is the counter i (starts 1), t2 is the limit 6. Each pass through loop: bge t1, t2, done leaves once i reaches 6; otherwise add t0, t0, t1 adds i to the sum, addi t1, t1, 1 bumps i by one, and j loop jumps back to test again. So it adds 1, 2, 3, 4, 5 and done prints the total, 15.

💡
Use Step instead of Run to advance through the program. Watch t1 climb and the program counter (pc) jump back to loop. Each time pc resets to loop, the branch condition evaluated to false and the loop continues.
Forget the exit condition and you get an infinite loop. The simulator guards against true runaways, but your branch logic is yours to get right.