Pre-test loops
prerequisite
A pre-test loop repeats a set of instructions, or walks a collection such as an array, checking its condition before every pass. In C, the for loop and the while loop are both pre-test loops. This lesson builds one in assembly.
What a pre-test loop is
A pre-test loop is a type of loop in which the loop condition is checked before each pass through the body, including the first. Consequently, a pre-test loop may run zero or more times: the body is skipped entirely if the condition is false the first time it is checked.
In C, the for loop and the while loop both check their condition before each pass through the loop body. The following code shows the same pre-test loop in C, written both ways.
#include <stdio.h>int main() { // for loop implementation in C // Printing the numbers 1 to 5 printf("Pre-test loop implementation\n"); for (int i = 1; i <= 5; i++) { printf("%d\n", i); } printf("Loop finished\n"); return 0;}#include <stdio.h>int main() { // while loop implementation // Printing the numbers 1 to 5 printf("Pre-test loop implementation\n"); int i; i = 1; while (i <= 5) { printf("%d\n", i); i = i + 1; } printf("Loop finished\n"); return 0;}Both loops print the numbers 1 to 5; only the for and while syntax differs.
note
The term pre-conditional loop refers to a pre-test loop. The two terms are interchangeable.
The logic behind it
Both loops produce the same output, and their underlying logic is:
PRINT "Pre-test loop implementation"DECLARE i AS INTEGERi = 1IF i <= 5 THEN PRINT i i = i + 1 JUMP BACK TO THE IF CHECKPRINT "Loop finished"That pseudocode maps straight onto compare-and-branch. The steps are:
- Set the loop variable to its starting value.
- State and check the loop condition.
- Run the loop body if the condition is satisfied.
- Branch back to the condition check.
- Leave the loop when the condition is no longer satisfied.
note
The logic reads more like a while loop. Assembly has no direct one-to-one equivalent of a for loop or a while loop. Instead, an assembly program reproduces the order in which these loops run their parts. When turning a for loop into assembly, it helps to rewrite it as a while loop first: for (i = 1; i <= 5; i++) { body } becomes i = 1; while (i <= 5) { body; i++; }. The starting value moves above the loop and the step moves to the end of the body, which makes the flow of control easier to translate.
The assembly implementation
The C code and pseudocode above can be written in assembly as follows:
ldr x0, =fmt_head bl printf mov i_r, 1 // i = 1 b loopcond // test before the first passloopbody: ldr x0, =fmt_i mov w1, i_r // pass i to printf bl printf add i_r, i_r, 1 // i = i + 1loopcond: cmp i_r, 5 b.le loopbody // go again while i <= 5 ldr x0, =fmt_done bl printfThe assembly code above implements a pre-test loop that prints the values of i from 1 to 5. The loop variable i is kept in register w19, which the full program names i_r with define(i_r, w19), while x0 and w1 are used to pass arguments to printf. i lives in w19 rather than a lower-numbered register because printf is free to overwrite x0 to x18, while x19 to x28 come back from a call unchanged.
The program first prints the introductory string and then sets i_r to 1, the initial value of i. The b loopcond instruction then transfers execution to the loop condition before the loop body has run.
At loopcond, the cmp instruction compares the value in i_r with 5. The b.le loopbody instruction checks the result of this comparison. If i_r is less than or equal to 5, execution branches to loopbody. Otherwise, the branch is not taken and execution continues to the instructions following the loop.
Inside loopbody, the current value of i_r is passed to printf and printed. The add instruction then increases i_r by 1. After the loop body finishes, execution naturally continues to the loopcond label, where the condition is checked again.
This process continues until i_r becomes greater than 5. At that point, b.le loopbody is not taken, so execution continues to the code after the loop, which prints Loop finished.
note
The loop condition is checked before the loop body runs. The condition is evaluated using the cmp instruction and the conditional branch that follows it. In this implementation, the loopcond label marks the loop condition, while the loopbody label marks the loop body.
The loop condition sits after the loop body in the assembly code. This layout takes advantage of the default downward flow of execution. After the loop body runs, execution naturally continues to the loop condition without needing an extra branch from the loop body back to the condition. An initial branch to loopcond is still required to start the loop. In exchange, no extra branch is needed at the end of each pass.
The whole program
The following code is the complete pre-test loop program, including the data section, the prologue, and the epilogue. It prints Pre-test loop implementation, then the numbers 1 to 5 on lines of their own, then Loop finished.
note
main changes w19 without saving it first, a shortcut these lessons take only in main. A subroutine you write yourself must save any of x19 to x28 it changes and restore them before it returns, as Subroutines: saved registers, pointers, and big arguments shows.
When the body never runs
The test comes first, so a pre-test loop whose condition is false at the start skips its body completely. The program below is the same loop with i starting at 6. The first check, 6 <= 5, is false, so b.le never jumps, and the program prints only Pre-test loop implementation and Loop finished.
This is why the b loopcond before the loop matters. Without it, execution would fall straight into loopbody and print 6 before the first check. A loop that runs its body before the first check has a name of its own, the post-test loop, and it is the subject of the next lesson.
Watch it run
A debugger runs a program one instruction at a time so you can see what each instruction does. Every runnable program on this page has the usual debugger tools under and beside its editor:
- Step one instruction. Press step. The highlighted line is the next instruction to run, and the registers it changed flash.
- Stop at a line. Run the program (or press step) once first, then click in the margin just left of a line number to set a breakpoint, a mark that makes run stop before that line. Press run again to carry on to the next stop.
- Step over a call. A
bl printftakes a few steps whileprintfruns, and the highlight stays on theblline until the call returns.printfis library code, not yours, so there is nothing of yours to watch inside it. To get past a call in one go, set a breakpoint on the line after thebland press run. - Read a register in decimal, hex, or binary. The dec and hex buttons above the registers switch how every value is shown. For binary, open the program in the full playground with its Open in playground link and use the convert tab there.
- Step back. The back button undoes one step, so you can watch a branch decide a second time.
Try it on the first program: run it (or press step) once, then set a breakpoint on the cmp line just after loopcond: and press run. Each time the program stops, x19 (the full 64-bit register whose low half is w19) holds the next value of i: 1, then 2, and so on up to 6. At 6 the test fails, and the next press of run finishes the program.
pitfall
A common mistake from this lesson, with a broken program and its fix that you can run:
Check yourself
- A pre-test loop starts
iat 10 and repeats whilei < 3. How many times does its body run? - Rewrite
for (n = 8; n > 0; n = n - 2)as awhileloop. What does the loop variable hold when the loop ends? - Which instruction in the program above makes the loop test its condition before the first pass?
- In the first program, what is printed if
b.le loopbodyis changed tob.lt loopbody?
answers
show answers
- Zero times: the first check,
10 < 3, is false. n = 8; while (n > 0) { body; n = n - 2; }, andnholds 0 at the end.b loopcond, the jump to the test that comes before the body.- The header, 1 to 4, and
Loop finished: the loop now stops whenireaches 5 instead of after printing it.
Practice
- Sum from one to n: add every integer from 1 to n with a counting loop.
- Count by sevens: step a counter by 7 and print each value.
- Fibonacci goes large: slide a pair of values forward
ntimes inxregisters. - gcd, the Euclid way: repeat a remainder step while a value is not zero.
- Collatz countdown: loop until n reaches 1, halving it or tripling it inside.
- The overeager counter: find the loop test that runs one pass too many.
- Basic quiz: loops: when the test runs, where the step goes, and which branch sets the last pass.
- Fill in the blank: loops (core): the jump to the test, the branch back into the body, and a countdown with no
cmp. Itscbnzline comes with Decisions in one instruction: csel, cinc, and cbz.
When you take a program into the full playground, the playground's interface walkthrough points at each part of the screen in turn, from the editor and the run controls to the registers and the console.